big ui moment

This commit is contained in:
bryanthaboi
2026-08-03 11:50:49 -04:00
parent 8e5501a23b
commit f0f5bc7551
78 changed files with 34435 additions and 3519 deletions
+178
View File
@@ -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