ebox/ebox-layout-config.el
Kinneyzhang 79f5bc23d1 feat: add CSS sizing and native text interaction capabilities
Normalize size units and intrinsic sizing across Elisp and native layout. Add help, pointer, hover-style and keymap support with reusable interaction adapters.

Keep content updates local, preserve scroll caches and hover borders, and avoid rebuilding retained plans and ownership metadata for stable geometry.

Validation: make check and native-rust-tests passed; targeted native interaction and scroll publication regressions passed.
2026-09-09 22:25:18 +08:00

156 lines
6.1 KiB
EmacsLisp

;;; ebox-layout-config.el --- Typed Box layout selection -*- lexical-binding: t; -*-
;;; Commentary:
;; Owns the small backend-neutral value that selects one Box child-layout
;; algorithm. Layout modules construct validated variants; the value itself
;; carries no runtime identity, children, measurement, or publication state.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'ebox-size)
(declare-function ebox-flex-layout-config-props-p
"ebox-flex" (props))
(declare-function ebox-grid-layout-config-props-p
"ebox-grid" (props))
(declare-function ebox-flex-layout-create "ebox-flex" (&rest plist))
(declare-function ebox-grid-layout-create "ebox-grid" (&rest plist))
(cl-defstruct
(ebox-layout-config
(:constructor ebox-layout-config--create))
"Canonical typed layout selection for one BoxNode."
kind
props)
(defconst ebox-layout-config--simple-cross-align-values
'(start center end stretch)
"Canonical Row/Column cross-axis alignment values.")
(defun ebox-layout-config--simple-axis-props-p (props kind)
"Return non-nil when PROPS is canonical Row/Column KIND config data."
(and (equal (mapcar #'car (seq-partition props 2))
'(:item-gap :cross-align))
(let ((gap (plist-get props :item-gap))
(align (plist-get props :cross-align)))
(and (ebox-size-value-for-axis-p
gap (if (eq kind 'column) 'block 'inline))
(memq align ebox-layout-config--simple-cross-align-values)))))
(defun ebox-layout-config--simple-axis-create (kind plist)
"Return validated simple-axis KIND LayoutConfig from PLIST."
(unless (and (proper-list-p plist) (zerop (% (length plist) 2)))
(error "Ebox %S LayoutConfig requires an even plist: %S" kind plist))
(let ((seen (make-hash-table :test #'eq)))
(cl-loop for (key _value) on plist by #'cddr
unless (memq key '(:item-gap :cross-align))
do (error "Ebox %S LayoutConfig does not accept %S" kind key)
do (when (gethash key seen)
(error "Duplicate Ebox %S LayoutConfig property: %S" kind key))
do (puthash key t seen)))
(let ((props
(list :item-gap (if (plist-member plist :item-gap)
(plist-get plist :item-gap)
(if (eq kind 'column) '(lh 0) '(px 0)))
:cross-align (if (plist-member plist :cross-align)
(plist-get plist :cross-align)
'stretch))))
(ebox-size-validate-for-axis
(plist-get props :item-gap) :item-gap
(if (eq kind 'column) 'block 'inline))
(unless (ebox-layout-config--simple-axis-props-p props kind)
(error "Invalid Ebox %S LayoutConfig properties: %S" kind props))
(ebox-layout-config--create :kind kind :props props)))
(defun ebox-layout-config-validate (config)
"Return CONFIG after revalidating its variant-owned properties."
(unless (ebox-layout-config-p config)
(error "Expected an Ebox LayoutConfig: %S" config))
(let* ((kind (ebox-layout-config-kind config))
(props (ebox-layout-config-props config))
(valid-p
(pcase kind
('normal (null props))
((or 'row 'column)
(ebox-size-validate-for-axis
(plist-get props :item-gap) :item-gap
(if (eq kind 'column) 'block 'inline))
(ebox-layout-config--simple-axis-props-p props kind))
('flex
(and (fboundp 'ebox-flex-layout-config-props-p)
(ebox-flex-layout-config-props-p props)))
('grid
(and (fboundp 'ebox-grid-layout-config-props-p)
(ebox-grid-layout-config-props-p props)))
(_ nil))))
(unless valid-p
(error "Invalid %S LayoutConfig properties: %S"
kind props)))
config)
(defun ebox-layout-config--copy (config)
"Return a detached copy of validated CONFIG."
(ebox-layout-config-validate config)
(ebox-layout-config--create
:kind (ebox-layout-config-kind config)
:props (copy-tree (ebox-layout-config-props config))))
(defun ebox-layout-config-with-props (config props)
"Return CONFIG's variant with detached canonical PROPS.
PROPS must already use the variant's normalized representation."
(ebox-layout-config-validate config)
(ebox-layout-config-validate
(ebox-layout-config--create
:kind (ebox-layout-config-kind config)
:props (copy-tree props))))
;;;###autoload
(defun ebox-normal-layout-create ()
"Return the canonical Normal layout config."
(ebox-layout-config--create :kind 'normal :props nil))
;;;###autoload
(defun ebox-row-layout-create (&rest plist)
"Return canonical RowConfig from evaluated PLIST."
(ebox-layout-config--simple-axis-create 'row plist))
;;;###autoload
(defun ebox-column-layout-create (&rest plist)
"Return canonical ColumnConfig from evaluated PLIST."
(ebox-layout-config--simple-axis-create 'column plist))
(defun ebox-layout-config-property-names (tag)
"Return canonical property names owned by TAG's LayoutConfig."
(pcase tag
('flex
'(:flex-direction :flex-wrap :justify-content
:align-items :align-content :row-gap :column-gap))
('grid
'(:grid-template-columns :grid-template-rows
:grid-auto-columns :grid-auto-rows :grid-auto-flow
:row-gap :column-gap
:justify-items :align-items :justify-content :align-content))
('box nil)
((or 'row 'column) '(:item-gap :cross-align))
(_ (error "Unknown Ebox Box author form: %S" tag))))
(defun ebox-layout-config-for-form (tag properties)
"Return TAG's typed LayoutConfig from evaluated layout PROPERTIES."
(pcase tag
('box
(when properties
(error "Ebox Normal layout does not accept config properties: %S"
properties))
(ebox-normal-layout-create))
('row (apply #'ebox-row-layout-create properties))
('column (apply #'ebox-column-layout-create properties))
('flex (apply #'ebox-flex-layout-create properties))
('grid (apply #'ebox-grid-layout-create properties))
(_ (error "Unknown Ebox Box author form: %S" tag))))
(provide 'ebox-layout-config)
;;; ebox-layout-config.el ends here