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