etaf/etaf-behavior.el
Kinneyzhang 43b17192d9 feat: implement unified etaf architecture
Deliver the unified View and Component model with retained Runtime, reactive scopes, Context, Behaviors, events, Actions, styles, Resources, Data, official UI Components, and Playground examples.\n\nVerification: make check and make load pass in the independent repository; sibling Ebox core tests pass 544/544.
2026-08-05 02:56:13 +08:00

133 lines
4.8 KiB
EmacsLisp

;;; etaf-behavior.el --- Non-visual :use Behaviors -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; A Behavior is a reusable bundle of semantic attributes and optional setup
;; cleanup. It is data attached through `:use', never a View node. Local
;; `:on-*' callbacks remain the simpler choice for one-off interaction.
;;; Code:
(require 'cl-lib)
(require 'etaf-reactive)
(define-error 'etaf-behavior-error "Invalid ETAF Behavior")
(cl-defstruct (etaf-behavior-spec
(:constructor etaf--behavior-spec-create))
"A name-plus-attributes non-visual Behavior specification."
name
attributes
install)
(cl-defstruct (etaf-behavior-context
(:constructor etaf--behavior-context-create))
"Runtime context supplied to an optional Behavior installer."
runtime
path
host-props)
(defvar etaf--current-behavior-context nil
"Behavior context active while one Behavior installs.")
;;;###autoload
(defun etaf-current-behavior-context ()
"Return the Behavior context active during an installer, or nil."
etaf--current-behavior-context)
(defun etaf-behavior-create (name &rest attributes)
"Create a Behavior spec named NAME from keyword ATTRIBUTES.
The reserved `:install' attribute is an optional zero-argument installer that
returns nil or a cleanup function; it is not merged into Host properties."
(unless (and (symbolp name) (not (keywordp name)))
(signal 'etaf-behavior-error
(list (format "Behavior name must be a symbol: %S" name))))
(unless (and (proper-list-p attributes)
(zerop (% (length attributes) 2)))
(signal 'etaf-behavior-error
(list "Behavior attributes must be keyword/value pairs")))
(let ((tail attributes)
(semantic nil)
install
seen)
(while tail
(let ((key (pop tail)))
(unless (keywordp key)
(signal 'etaf-behavior-error
(list (format "Behavior attribute must be a keyword: %S"
key))))
(when (memq key seen)
(signal 'etaf-behavior-error
(list (format "Duplicate Behavior attribute: %S" key))))
(push key seen)
(let ((value (pop tail)))
(if (eq key :install)
(setq install value)
(setq semantic (append semantic (list key value)))))))
(when (and install (not (functionp install)))
(signal 'etaf-behavior-error
(list ":install must be a function or nil")))
(etaf--behavior-spec-create :name name
:attributes semantic
:install install)))
(defmacro etaf-define-behavior (name arguments &rest body)
"Define ordinary Behavior constructor NAME with ARGUMENTS and BODY.
The constructor must return `etaf-behavior-create' data. This is a function
definition convenience, not a second runtime or visual node model."
(declare (indent 2) (debug defun))
(unless (symbolp name)
(signal 'etaf-behavior-error (list "Behavior name must be a symbol")))
`(defun ,name ,arguments
,(if (stringp (car body)) (pop body)
(format "Construct the `%s' ETAF Behavior." name))
,@body))
;;;###autoload
(defun etaf-focusable (&rest attributes)
"Return a focusable Behavior from ATTRIBUTES.
When ATTRIBUTES does not specify `:tab-index', the Behavior supplies the
standard first tab stop, zero. An explicit nil keeps the Host unfocusable."
(unless (plist-member attributes :tab-index)
(setq attributes (append attributes (list :tab-index 0))))
(apply #'etaf-behavior-create 'focusable attributes))
;;;###autoload
(defun etaf-toggleable (&rest attributes)
"Return a Behavior converting ATTRIBUTES into a toggleable press action.
`:value' and `:on-change' configure the action; all other attributes remain
part of the returned Behavior."
(let ((value (plist-get attributes :value))
(on-change (plist-get attributes :on-change)))
(unless (functionp on-change)
(signal 'etaf-behavior-error
(list "toggleable requires a function-valued :on-change")))
(let (remaining)
(while attributes
(let ((key (pop attributes))
(item (pop attributes)))
(unless (memq key '(:value :on-change))
(setq remaining (append remaining (list key item))))))
(apply #'etaf-behavior-create
'toggleable
(append remaining
(list :on-press
(lambda ()
(funcall
on-change
(not
(if (or (etaf-ref-p value)
(etaf-computed-p value))
(etaf-value value)
value))))))))))
(provide 'etaf-behavior)
;;; etaf-behavior.el ends here