mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
207 lines
9.4 KiB
Lua
207 lines
9.4 KiB
Lua
-- modules/behaviors/Selectable.lua
|
|
--
|
|
-- Concrete behavior: Select state-machine lifecycle for dropdown-style
|
|
-- select groups. Owns the per-element Select subsystem initialization, the
|
|
-- managed-frame layout sync each frame, and select save/restore across the
|
|
-- immediate-mode recreation cycle.
|
|
--
|
|
-- This behavior consolidates the legacy `if self._selectState` / `if
|
|
-- self.selectOption` branches that previously lived inside Element.lua:
|
|
--
|
|
-- * Select subsystem init (formerly Element:_initSubSystems lines ~810-825 —
|
|
-- `Select.initSelectParent` / `Select.initSelectOption`).
|
|
-- * Managed-frame adoption (formerly Element:_initPositioning lines ~1700-
|
|
-- 1702 — `Select.adoptSelectFrame`).
|
|
-- * Per-frame frame-state sync (formerly Element:update line ~2747 —
|
|
-- `Select.ensureFrameState`).
|
|
-- * Save/restore of select open/value/label (formerly the `select` branch of
|
|
-- Element:saveState / Element:restoreState).
|
|
--
|
|
-- Element retains `self._selectState` and `self.selectOption` for backward-
|
|
-- compat field access; runtime state lives ON THE ELEMENT. The behavior itself
|
|
-- is stateless + immutable (a single shared instance attaches to every
|
|
-- selectable element).
|
|
--
|
|
-- The 20 Element select-API delegate methods (openSelect, closeSelect,
|
|
-- toggleSelect, isSelectOpen, getSelectValue, setSelectValue, ...) stay as
|
|
-- 1-line forwarders into the Select module — the behavior owns the
|
|
-- *lifecycle* (attach / update / save / restore / detach), not the API
|
|
-- surface (per task 05 spec notes).
|
|
--
|
|
-- State ownership (per the locked Behavior contract):
|
|
-- * Per-element runtime state lives ON THE ELEMENT (self._selectState,
|
|
-- self.selectOption, self._selectParentElement, ...).
|
|
-- * The behavior instance is stateless + immutable and shared across elements.
|
|
-- * Element-class-level dependencies are resolved via `getmetatable(element)`
|
|
-- (which IS the Element class set by Element._construct), so the hook
|
|
-- signature stays exactly `(element, ...)` with no DI parameters.
|
|
|
|
local _pkg = (...):match("^(.-)behaviors%.") or "modules."
|
|
local Behavior = require(_pkg .. "Behavior")
|
|
|
|
-- Resolve the Element class from an element instance.
|
|
-- `setmetatable({}, Element)` in `_construct` makes the instance metatable BE
|
|
-- the Element class, so this yields Element._Select, Element._Context,
|
|
-- Element._StateManager, etc. without threading deps through the hook signature.
|
|
local function ElementClass(element)
|
|
return getmetatable(element)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- shouldAttach (class-level predicate, no element required)
|
|
-- ============================================================================
|
|
|
|
-- Mirrors the cases that previously caused Element to initialize a Select
|
|
-- subsystem. An element owns select state exactly when it declares a
|
|
-- `selectParent` config (the dropdown trigger) or a `selectOption` config (an
|
|
-- option inside a dropdown). Checking the props (rather than the runtime
|
|
-- `_selectState`) lets shouldAttach run before onAttach initializes the
|
|
-- subsystem, matching the auto-attach contract established by Clickable /
|
|
-- TextEditable.
|
|
local function shouldAttach(props)
|
|
props = props or {}
|
|
return type(props.selectParent) == "table" or type(props.selectOption) == "table"
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- onAttach — initialize the Select subsystem (formerly Element:_initSubSystems
|
|
-- lines ~810-825) and adopt the managed frame (formerly Element:_initPositioning
|
|
-- lines ~1700-1702).
|
|
-- ============================================================================
|
|
|
|
local function onAttach(element)
|
|
local Element = ElementClass(element)
|
|
|
|
-- Initialize the appropriate select role. Mirrors the legacy _initSubSystems
|
|
-- block exactly: selectParent → initSelectParent (sets _selectState +
|
|
-- immediate-mode restore from StateManager); selectOption → initSelectOption
|
|
-- (sets the option value/label/disabled).
|
|
if type(element.selectParent) == "table" then
|
|
Element._Select.initSelectParent(element, element.selectParent)
|
|
end
|
|
|
|
if type(element.selectOption) == "table" then
|
|
Element._Select.initSelectOption(element, element.selectOption)
|
|
end
|
|
|
|
-- Adopt the managed dropdown frame. This was formerly the tail of
|
|
-- _initPositioning (after the select parent's own addChild). It creates the
|
|
-- select anchor, reparents the frame under it, and syncs visibility. Moving
|
|
-- it here is safe because onAttach runs after _initPositioning: the parent's
|
|
-- own positioning is finalized, so the anchor's geometry can be computed.
|
|
if element._selectState and type(element.selectParent) == "table" and element.selectParent.selectFrame ~= nil then
|
|
Element._Select.adoptSelectFrame(element, element.selectParent.selectFrame)
|
|
end
|
|
|
|
-- Backfill option registration for children added BEFORE this behavior
|
|
-- attached. The auto-attach pass runs at the very end of Element.new
|
|
-- (after _finalizeConstruction, which processes declarative `children`).
|
|
-- Declarative select-option children are addChild'd to this element during
|
|
-- _finalizeConstruction — at that point _selectState did not yet exist (this
|
|
-- onAttach had not run), so their registerWithSelectParent call walked the
|
|
-- parent chain, found no _selectState, and returned early. Re-scan now that
|
|
-- _selectState is initialized so these options are registered + reparented
|
|
-- into the managed frame exactly like runtime-added options.
|
|
-- (registerWithSelectParent is idempotent — it skips options already
|
|
-- registered — so this is a no-op for children added after _selectState was
|
|
-- set, e.g. the common `FlexLove.new({ parent = sp, selectOption = {...} })`
|
|
-- pattern.)
|
|
if element._selectState then
|
|
for _, child in ipairs(element.children) do
|
|
if child.selectOption then
|
|
Element._Select.registerWithSelectParent(child)
|
|
Element._Select.attachOptionToManagedFrame(child)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function onDetach(element)
|
|
-- Clear select-managed fields so the element can be GC'd cleanly in immediate
|
|
-- mode (formerly part of Element:_cleanup). This mirrors the select-clearing
|
|
-- block that lived in Element:_cleanup; Element:destroy separately routes
|
|
-- through Select.cleanupDestroy for full teardown (idempotent with this).
|
|
if element.selectParent then
|
|
element.selectParent.onChange = nil
|
|
end
|
|
element._selectState = nil
|
|
element._managedSelectOwner = nil
|
|
element._managedSelectFrame = nil
|
|
element._managedSelectAnchor = nil
|
|
element._managedSelectBaseOpacity = nil
|
|
element._managedSelectBaseVisibility = nil
|
|
element._managedSelectBaseDisabled = nil
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- onUpdate — per-frame managed-frame layout sync (formerly Element:update
|
|
-- line ~2747 — `Select.ensureFrameState`).
|
|
-- ============================================================================
|
|
|
|
local function onUpdate(element, dt)
|
|
local Element = ElementClass(element)
|
|
Element._Select.ensureFrameState(element)
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- onDraw — no-op.
|
|
-- ============================================================================
|
|
|
|
-- Select rendering is driven by the managed frame / anchor elements themselves
|
|
-- (visibility synced by Select.syncManagedFrameVisibility), not by the select
|
|
-- parent's draw path. The parent's own pixels are the theme/renderer's job.
|
|
local function onDraw() end
|
|
|
|
-- ============================================================================
|
|
-- saveState / restoreState — select open/value/label (formerly the `select`
|
|
-- branch of Element:saveState / Element:restoreState).
|
|
-- ============================================================================
|
|
|
|
-- Returns a snapshot under the `select` key to match the legacy immediate-mode
|
|
-- restoreState contract (Element:restoreState looked up state.select). The
|
|
-- behavior-dispatch loop merges behavior snapshots into the top-level state
|
|
-- table, so returning { select = ... } slots in identically to the old inline
|
|
-- `state.select = selectState` assignment.
|
|
local function saveState(element)
|
|
local Element = ElementClass(element)
|
|
local selectState = Element._Select.saveState(element)
|
|
if selectState then
|
|
return { select = selectState }
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Consumes the previously-saved snapshot keyed under `select`. The behavior-
|
|
-- dispatch loop passes the FULL top-level state table; this hook reads only
|
|
-- its own `state.select` slice, mirroring the legacy `if state.select then`
|
|
-- guard in Element:restoreState.
|
|
local function restoreState(element, state)
|
|
if not state then
|
|
return
|
|
end
|
|
local Element = ElementClass(element)
|
|
if state.select then
|
|
Element._Select.restoreState(element, state.select)
|
|
end
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- Build the (stateless, shared, immutable) behavior instance.
|
|
-- ============================================================================
|
|
|
|
local Selectable = Behavior.new({
|
|
onAttach = onAttach,
|
|
onDetach = onDetach,
|
|
onUpdate = onUpdate,
|
|
onDraw = onDraw,
|
|
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).
|
|
Selectable.shouldAttach = shouldAttach
|
|
|
|
return Selectable
|