mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
big ui moment
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
-- modules/behaviors/Themed.lua
|
||||
--
|
||||
-- Concrete behavior: Renderer ownership + theme-state rendering.
|
||||
--
|
||||
-- Themed owns the per-element Renderer instance and the single
|
||||
-- `Renderer:draw` call that paints the core visual layers (background, image,
|
||||
-- theme 9-patch, borders, text, customDraw). It is the behavior-mode-unification
|
||||
-- replacement for the former `_initImageAndRenderer` Renderer creation block and
|
||||
-- the former first `self._renderer:draw(self, backdropCanvas)` call in
|
||||
-- Element:draw (behavior-mode-unification task 07).
|
||||
--
|
||||
-- Attachment rule (shouldAttach): every renderable Element. The pre-refactor
|
||||
-- code unconditionally created a Renderer for every Element and unconditionally
|
||||
-- called `Renderer:draw` in Element:draw; Themed mirrors that invariant so the
|
||||
-- Renderer is always available to subsystems that depend on it (TextEditor font
|
||||
-- / wrap delegation, ScrollManager scrollbar drawing) AND so visual rendering of
|
||||
-- background / border / theme / image layers is preserved for every element.
|
||||
-- Restricting attachment to `themeComponent`-only elements would break editable
|
||||
-- text fields and scrollable containers (which need a Renderer for subsystem
|
||||
-- delegation even when they have no theme component). The 9-patch theme-state
|
||||
-- rendering within `Renderer:draw` is a no-op for elements without a
|
||||
-- `themeComponent`, so always-attaching carries no rendering cost.
|
||||
--
|
||||
-- Themed and Imageable are paired (both configure the same `element._renderer`):
|
||||
-- Themed.onAttach creates the Renderer with the theme/blur config; Imageable
|
||||
-- (attached for imagePath/image elements) enriches the SAME renderer instance with
|
||||
-- image config + deferred image loading. They share `element._renderer`.
|
||||
--
|
||||
-- onUpdate is a no-op: theme-state transitions are DRIVEN by the Clickable
|
||||
-- behavior (whose onUpdate recomputes hover/press/focus and calls
|
||||
-- `renderer:setThemeState`). Themed only READS that state for rendering, so it has
|
||||
-- no per-frame update work.
|
||||
--
|
||||
-- saveState owns the blur-region snapshot (`state.blur`): the per-frame blur
|
||||
-- geometry + radius/quality used by the Blur cache for invalidation (formerly
|
||||
-- the inline `if self.backdropBlur or self.contentBlur` block of
|
||||
-- Element:saveState — behavior-mode-unification task 12). restoreState is a
|
||||
-- no-op: blur cache data is used for invalidation, not restoration (the Blur
|
||||
-- cache is keyed by element id and cleared via `Blur.clearElementCache` from
|
||||
-- FlexLove.endFrame, not replayed through restoreState).
|
||||
--
|
||||
-- State ownership (per the locked Behavior contract):
|
||||
-- * Per-element runtime state lives ON THE ELEMENT (`element._renderer`,
|
||||
-- `element._themeState`, `element.backdropBlur`, `element.contentBlur`).
|
||||
-- The behavior instance is stateless and shared.
|
||||
-- * `element._renderer` is recreated on attach; onDetach is a no-op — the
|
||||
-- reference is released when the element is GC'd (Element:_cleanup keeps
|
||||
-- element structure for inspection).
|
||||
|
||||
local _pkg = (...):match("^(.-)behaviors%.") or "modules."
|
||||
local Behavior = require(_pkg .. "Behavior")
|
||||
|
||||
-- Resolve the Element class from an element instance (mirrors Clickable).
|
||||
-- Element instances are created via `setmetatable({}, Element)`, so their
|
||||
-- metatable IS the Element class — giving access to Element._Renderer,
|
||||
-- Element._rendererDeps, etc. without threading deps through the hook signature.
|
||||
local function ElementClass(element)
|
||||
return getmetatable(element)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- shouldAttach (class-level predicate, no element required)
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
-- Returns true for every renderable Element. See file header for the rationale:
|
||||
-- the pre-refactor invariant was "every Element has a Renderer; Element:draw
|
||||
-- always calls Renderer:draw", and Thamed is the behavior-system embodiment of
|
||||
-- that invariant. Returns true for `themeComponent`-bearing props (the spec's
|
||||
-- headline case) and for every other element so subsystems/rendering stay intact.
|
||||
local function shouldAttach(props)
|
||||
return true
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- onAttach — create the Renderer with theme/blur config (formerly the
|
||||
-- Renderer.new block of Element:_initImageAndRenderer).
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
local function onAttach(element)
|
||||
local Element = ElementClass(element)
|
||||
|
||||
-- Create-or-reuse the Renderer. Thamed is the first render behavior in the
|
||||
-- registry, so it normally creates the instance; Imageable (if attached) will
|
||||
-- reuse this same instance for image config. Guarded so Imageable-onAttach-
|
||||
-- first (defensive) does not clobber an existing renderer.
|
||||
if element._renderer then
|
||||
return
|
||||
end
|
||||
|
||||
-- NOTE: backgroundColor/borderColor/opacity/cornerRadius/themeComponent are
|
||||
-- intentionally NOT passed here. Renderer:draw() reads them from the element
|
||||
-- as the single source of truth (see Renderer.lua draw()). Only renderer-owned
|
||||
-- state (theme, blur) is cached on the renderer; image config is added by the
|
||||
-- Imageable behavior. border is element-sourced too.
|
||||
element._renderer = Element._Renderer.new({
|
||||
theme = element.theme,
|
||||
scaleCorners = element.scaleCorners,
|
||||
scalingAlgorithm = element.scalingAlgorithm,
|
||||
contentBlur = element.contentBlur,
|
||||
backdropBlur = element.backdropBlur,
|
||||
}, Element._rendererDeps)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- onDraw — the single Renderer:draw call (formerly the first call in
|
||||
-- Element:draw). Paints all core visual layers for this element.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
local function onDraw(element, ctx)
|
||||
local renderer = element._renderer
|
||||
if not renderer then
|
||||
return
|
||||
end
|
||||
renderer:draw(element, ctx and ctx.backdropCanvas)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- onDetach — no-op. Element:_cleanup preserves element structure for
|
||||
-- inspection (the original invariant), so the Renderer reference is released
|
||||
-- when the element is GC'd rather than torn down here. Present as an explicit
|
||||
-- hook so the behavior conforms to the full lifecycle contract.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
local function onDetach() end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- saveState — blur-region snapshot (formerly the `blur` branch of
|
||||
-- Element:saveState). Returns `{ blur = {...} }` when the element configures a
|
||||
-- backdrop or content blur, so the Blur cache can invalidate by element id;
|
||||
-- nil otherwise. Mode-agnostic to match the legacy contract (the snapshot is
|
||||
-- only read back by the cache-invalidation path, which itself is
|
||||
-- immediate-mode-only via FlexLove.endFrame).
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
local function saveState(element)
|
||||
if not (element.backdropBlur or element.contentBlur) then
|
||||
return nil
|
||||
end
|
||||
local blur = {
|
||||
_blurX = element.x,
|
||||
_blurY = element.y,
|
||||
_blurWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right),
|
||||
_blurHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom),
|
||||
}
|
||||
if element.backdropBlur then
|
||||
blur._backdropBlurRadius = element.backdropBlur.radius
|
||||
blur._backdropBlurQuality = element.backdropBlur.quality or 5
|
||||
end
|
||||
if element.contentBlur then
|
||||
blur._contentBlurRadius = element.contentBlur.radius
|
||||
blur._contentBlurQuality = element.contentBlur.quality or 5
|
||||
end
|
||||
return { blur = blur }
|
||||
end
|
||||
|
||||
-- restoreState — no-op: blur cache data is used for invalidation, not
|
||||
-- restoration (see file header). Present so the behavior conforms to the
|
||||
-- lifecycle contract without replaying geometry that the cache recomputes.
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Build the (stateless, shared, immutable) behavior instance.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
local Themed = Behavior.new({
|
||||
onAttach = onAttach,
|
||||
onDetach = onDetach,
|
||||
onUpdate = function() end,
|
||||
onDraw = onDraw,
|
||||
saveState = saveState,
|
||||
restoreState = function() end,
|
||||
})
|
||||
|
||||
-- Expose the predicate at module level so callers/tests can reference it
|
||||
-- directly without an element instance (mirrors Behavior.shouldAttach /
|
||||
-- Clickable.shouldAttach).
|
||||
Themed.shouldAttach = shouldAttach
|
||||
|
||||
return Themed
|
||||
Reference in New Issue
Block a user