mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-14 23:31:27 +02:00
133 lines
6.1 KiB
Lua
133 lines
6.1 KiB
Lua
-- 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
|