142 lines
5.0 KiB
EmacsLisp
142 lines
5.0 KiB
EmacsLisp
;;; etaf-counter-example.el --- Retained state example -*- lexical-binding: t; -*-
|
||
|
||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
;;; Commentary:
|
||
|
||
;; This example keeps state inside one retained Component, derives display
|
||
;; values with `etaf-computed', and routes named mutations through an Action.
|
||
|
||
;;; Code:
|
||
|
||
(require 'etaf)
|
||
|
||
(defconst etaf-counter-example-buffer-name "*ETAF Counter Example*"
|
||
"Buffer opened by `etaf-counter-example-open'.")
|
||
|
||
(etaf-action-define etaf-counter-example-update (runtime count operation)
|
||
"Apply OPERATION to COUNT inside RUNTIME."
|
||
(ignore runtime)
|
||
(etaf-set-value
|
||
count
|
||
(pcase operation
|
||
('increment (1+ (etaf-value count)))
|
||
('decrement (max 0 (1- (etaf-value count))))
|
||
('reset 0)
|
||
(_ (user-error "Unknown counter operation: %S" operation)))))
|
||
|
||
(defun etaf-counter-example--header (title)
|
||
"Return the counter header for TITLE."
|
||
(etaf-view
|
||
(column :class "hero"
|
||
(text :class "eyebrow" "BEST PRACTICE / RETAINED STATE")
|
||
(text :font-weight 'bold (expr :value title))
|
||
(text :color "#66706A"
|
||
"State belongs to setup; rendering only reads it."))))
|
||
|
||
(defun etaf-counter-example--metrics (count double status)
|
||
"Return metric cards for COUNT, DOUBLE, and STATUS."
|
||
(etaf-view
|
||
(flex :width '(680) :flex-flow '(row wrap) :gap '(1 (12))
|
||
(box :class "metric"
|
||
(text (expr :value (format "COUNT %d" (etaf-value count)))))
|
||
(box :class "metric"
|
||
(text (expr :value (format "DOUBLE %d" (etaf-value double)))))
|
||
(box :class "metric"
|
||
(text (expr :value (format "STATE %s" (etaf-value status))))))))
|
||
|
||
(defun etaf-counter-example--action (count label host-ref operation)
|
||
"Return one COUNT action named LABEL using HOST-REF and OPERATION."
|
||
(etaf-view
|
||
(box :class "action" :ref host-ref :role 'button
|
||
:use (list (etaf-focusable))
|
||
:on-press (lambda ()
|
||
(etaf-dispatch 'etaf-counter-example-update
|
||
count operation))
|
||
(text (expr :value label)))))
|
||
|
||
(defun etaf-counter-example--actions (count)
|
||
"Return the action group for COUNT."
|
||
(etaf-view
|
||
(flex :width '(680) :flex-flow '(row wrap) :gap '(1 (12))
|
||
(expr :value
|
||
(etaf-counter-example--action
|
||
count "− DECREMENT" 'counter-decrement 'decrement))
|
||
(expr :value
|
||
(etaf-counter-example--action
|
||
count "RESET" 'counter-reset 'reset))
|
||
(expr :value
|
||
(etaf-counter-example--action
|
||
count "+ INCREMENT" 'counter-increment 'increment)))))
|
||
|
||
(etaf-define-component etaf-counter-example-card (&key title initial-value)
|
||
"Render a retained counter named TITLE starting at INITIAL-VALUE."
|
||
:styles
|
||
(styles
|
||
("&" :width (680) :color "#252A2E" :bgcolor "#F8F5EE")
|
||
(".hero" :width (680) :padding (1 (18)) :border "#8F432F"
|
||
:bgcolor "#FFFDF8" :text-align center)
|
||
(".eyebrow" :color "#8F432F" :font-weight bold)
|
||
(".metric" :flex-grow 1 :flex-shrink 1 :flex-basis (200)
|
||
:min-width (180) :padding (1 (14)) :border "#6D8A73"
|
||
:bgcolor "#DCEBDD" :text-align center)
|
||
(".action" :flex-grow 1 :flex-shrink 1 :flex-basis (160)
|
||
:min-width (140) :padding (1 (12)) :border "#4E7890"
|
||
:bgcolor "#D9EAF2" :text-align center :font-weight bold)
|
||
(".note" :width (680) :padding (1 (16)) :border "#8D887F"
|
||
:color "#4D5651" :bgcolor "#EEEAE2"))
|
||
:setup
|
||
(let* ((count (etaf-ref (or initial-value 0) :name 'counter))
|
||
(double (etaf-computed
|
||
(lambda () (* 2 (etaf-value count)))
|
||
:name 'counter-double))
|
||
(status (etaf-computed
|
||
(lambda ()
|
||
(if (zerop (etaf-value count)) "READY" "ACTIVE"))
|
||
:name 'counter-status)))
|
||
(lambda ()
|
||
(etaf-view
|
||
(column
|
||
(expr :value (etaf-counter-example--header title))
|
||
(box :height 1)
|
||
(expr :value
|
||
(etaf-counter-example--metrics count double status))
|
||
(box :height 1)
|
||
(expr :value (etaf-counter-example--actions count))
|
||
(box :height 1)
|
||
(box :class "note"
|
||
(text
|
||
"Public path: Event → Action → Ref → Computed → Runtime commit")))))))
|
||
|
||
;;;###autoload
|
||
(defun etaf-counter-example-view ()
|
||
"Return the counter example View."
|
||
(etaf-view
|
||
(etaf-counter-example-card
|
||
:title "Counter workspace"
|
||
:initial-value 2)))
|
||
|
||
;;;###autoload
|
||
(defun etaf-counter-example-open ()
|
||
"Mount the counter example and return its buffer."
|
||
(interactive)
|
||
(let ((buffer (etaf-mount etaf-counter-example-buffer-name
|
||
(etaf-counter-example-view))))
|
||
(when (called-interactively-p 'interactive)
|
||
(pop-to-buffer buffer))
|
||
buffer))
|
||
|
||
;;;###autoload
|
||
(defun etaf-counter-example-close ()
|
||
"Unmount and kill the counter example buffer."
|
||
(interactive)
|
||
(when-let* ((runtime
|
||
(etaf-runtime-for-buffer etaf-counter-example-buffer-name)))
|
||
(etaf-unmount runtime))
|
||
(when-let* ((buffer (get-buffer etaf-counter-example-buffer-name)))
|
||
(kill-buffer buffer)))
|
||
|
||
(provide 'etaf-counter-example)
|
||
|
||
;;; etaf-counter-example.el ends here
|