mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 04:06:10 +02:00
big ui moment
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
-- modules/behaviors/Persistable.lua
|
||||
--
|
||||
-- Concrete behavior: generic public-property persistence across the immediate-
|
||||
-- mode recreation cycle (behavior-mode-unification task 12).
|
||||
--
|
||||
-- Owns the ONE piece of Element save/restore state that is NOT subsystem state:
|
||||
-- the snapshot of an element's own public scalar fields (`text`, `display`,
|
||||
-- `opacity`, `x`, `width`, ...). Event-driven mutations to these fields (a
|
||||
-- release callback changing `text`, a toggle hiding a panel via `display =
|
||||
-- false`) must survive the per-frame Element recreation that defines immediate
|
||||
-- mode. Persistable captures them in `saveState` and reapplies them in
|
||||
-- `restoreState`, so the caller never branches on mode.
|
||||
--
|
||||
-- This behavior is the final home for the former `Element:saveState` `_props`
|
||||
-- block and the former `Element:restoreState` `_props` block (~20 LOC moved out
|
||||
-- of Element.lua). With it in place, `Element:saveState` / `Element:restoreState`
|
||||
-- collapse to a pure behavior-dispatch loop and Element owns zero property-
|
||||
-- extraction logic — every persisted slice is owned by exactly one behavior.
|
||||
--
|
||||
-- Attachment rule (shouldAttach): every element. Persistable attaches
|
||||
-- unconditionally (mirrors the pre-refactor invariant that every element's
|
||||
-- public scalar props were scanned). The actual snapshot is mode-gated inside
|
||||
-- `saveState` (immediate-mode-only, matching the legacy contract); in retained
|
||||
-- mode `saveState` returns nil and `restoreState` is a no-op unless a snapshot
|
||||
-- is explicitly passed.
|
||||
--
|
||||
-- Registry ordering: Persistable is intentionally placed LAST in the behavior
|
||||
-- registry. `restoreState` applies `_props` AFTER every other behavior has
|
||||
-- hydrated its subsystem state, so a persisted public-prop mutation (e.g.
|
||||
-- `text = "mutated"`) overrides the freshly-restored TextEditor/Select state —
|
||||
-- preserving the legacy restore ordering (behaviors first, `_props` tail).
|
||||
--
|
||||
-- State ownership (per the locked Behavior contract):
|
||||
-- * The persisted props live ON the element (they ARE the element's public
|
||||
-- fields). The behavior instance is stateless + immutable and shared.
|
||||
-- * The snapshot is returned under the `_props` key (prefixed with `_` so
|
||||
-- the public-prop scan itself skips it — avoiding self-recursion).
|
||||
|
||||
local _pkg = (...):match("^(.-)behaviors%.") or "modules."
|
||||
local Behavior = require(_pkg .. "Behavior")
|
||||
|
||||
-- Resolve the Element class from an element instance (mirrors Clickable /
|
||||
-- Themed). Element instances are created via `setmetatable({}, Element)`, so
|
||||
-- their metatable IS the Element class — giving access to Element._StateManager
|
||||
-- without threading deps through the hook signature.
|
||||
local function ElementClass(element)
|
||||
return getmetatable(element)
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- shouldAttach (class-level predicate, no element required)
|
||||
-- ============================================================================
|
||||
|
||||
-- Every element's public scalar props are persistable, so this behavior
|
||||
-- attaches unconditionally. The mode gate lives inside saveState (it needs the
|
||||
-- runtime mode, which is only available with an element via StateManager).
|
||||
local function shouldAttach()
|
||||
return true
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- saveState — snapshot public scalar fields (immediate-mode-only).
|
||||
-- ============================================================================
|
||||
|
||||
-- Mirrors the former `Element:saveState` `_props` block exactly:
|
||||
-- * Only string keys NOT prefixed with `_` (so internal fields like
|
||||
-- `_renderer`, `_themeState`, `_initProps` are excluded).
|
||||
-- * Only scalar values (numbers, strings, booleans); tables and functions
|
||||
-- are excluded (children, padding, onEvent, ...).
|
||||
-- Returns `{ _props = {...} }` when there is at least one persistable prop and
|
||||
-- the element is in immediate mode; nil otherwise (retained mode no-op —
|
||||
-- state lives on the element directly there, so nothing to snapshot).
|
||||
local function saveState(element)
|
||||
local Element = ElementClass(element)
|
||||
if not Element._StateManager.isImmediateMode() then
|
||||
return nil
|
||||
end
|
||||
local props = {}
|
||||
for k, v in pairs(element) do
|
||||
if type(k) == "string" and k:sub(1, 1) ~= "_" and type(v) ~= "table" and type(v) ~= "function" then
|
||||
props[k] = v
|
||||
end
|
||||
end
|
||||
if next(props) then
|
||||
return { _props = props }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- restoreState — reapply the persisted public-prop snapshot onto a fresh
|
||||
-- element (mode-agnostic; only fires when a `_props` slice is present).
|
||||
-- ============================================================================
|
||||
|
||||
-- Applies persisted mutations on top of whatever the constructor + other
|
||||
-- behaviors already set, so event-driven changes from the previous frame
|
||||
-- override the declarative props of the recreated element. Runs last in the
|
||||
-- behavior dispatch (Persistable is the registry tail) to preserve the legacy
|
||||
-- restore ordering (subsystem restore first, `_props` override last).
|
||||
local function restoreState(element, state)
|
||||
if not state or not state._props then
|
||||
return
|
||||
end
|
||||
for k, v in pairs(state._props) do
|
||||
element[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- onAttach / onUpdate / onDraw / onDetach — no-ops.
|
||||
-- ============================================================================
|
||||
|
||||
-- Persistable owns no subsystem and allocates no per-element state (the
|
||||
-- "state" it persists IS the element's own fields). The lifecycle is purely
|
||||
-- save/restore.
|
||||
|
||||
-- ============================================================================
|
||||
-- Build the (stateless, shared, immutable) behavior instance.
|
||||
-- ============================================================================
|
||||
|
||||
local Persistable = Behavior.new({
|
||||
saveState = saveState,
|
||||
restoreState = restoreState,
|
||||
shouldAttach = shouldAttach,
|
||||
})
|
||||
|
||||
-- Expose the predicate at module level so callers/tests can reference it
|
||||
-- directly without an element instance (mirrors Behavior.shouldAttach /
|
||||
-- Clickable.shouldAttach).
|
||||
Persistable.shouldAttach = shouldAttach
|
||||
|
||||
return Persistable
|
||||
Reference in New Issue
Block a user