use "collections"
trait tag WidgetParent
"""
Anything that can receive a Grid from a child widget.
Both the Compositor and box containers implement this.
"""
be receive_grid(widget: Any tag, grid: Grid)
trait tag Widget
"""
Base trait for all widget actors. Users implement their own actors
with this trait.
Required: `state()` returns the WidgetState field, `render()` produces
the grid. All other behaviors have default implementations.
The `fun ref` methods are uncallable from outside the actor because
external references are always `tag` capability.
"""
// -- Required: user must implement these --
fun ref state(): WidgetState
"""
Return the widget's state. Backed by a field on the actor.
"""
fun ref render(): Grid
"""
Produce a grid representing the current widget state.
"""
// -- Provided: default implementations --
fun ref render_and_send() =>
"""
Render the widget and send the grid to the parent.
"""
state().parent.receive_grid(this, render())
be resize(w: USize, h: USize) =>
"""
Update allocated size and re-render.
"""
state().width = w
state().height = h
render_and_send()
be trigger_render() =>
"""
Force a re-render and send to parent.
"""
render_and_send()
be receive_key(key: KeyEvent) =>
"""
Handle a key event. Default: ignore.
"""
None
be receive_focus() =>
"""
Called when this widget receives focus. Default: re-render.
"""
render_and_send()
be receive_blur() =>
"""
Called when this widget loses focus. Default: re-render.
"""
render_and_send()
be set_debug_bg(color: Color) =>
"""
Set a debug background color to visualize this widget's allocated space.
Use Default to disable.
"""
state().debug_bg = color
render_and_send()
trait tag CompositeWidget is (Widget & WidgetParent)
"""
A widget that contains child widgets. Extends Widget with child grid
storage, compositing, and resize propagation.
Uses dirty-flag coalescing: when a child grid arrives, the grid is
stored and the widget marks itself dirty. A deferred self-message
triggers the actual recompose. Multiple grids arriving in quick
succession are batched into a single render pass.
The user implements `state()`. Override `render_background()` to draw
custom content behind children (default is an empty grid).
Override `render()` for custom child compositing (e.g. HBox/VBox).
"""
// -- Required: user must implement --
fun ref render_background(): Grid =>
"""
Produce the background grid before children are composited on top.
Default: empty grid using the widget's debug_bg color.
"""
Grid.filled(state().width, state().height, state().empty_cell())
// -- Provided: child registration --
fun ref register_child(widget: Widget tag) =>
"""
Register a child widget. Required before the child can send grids
or receive resize propagation. Call from the constructor after
creating child widgets.
"""
state().child_grids.push((widget, Grid.filled(0, 0, Cell.empty())))
// -- Provided: compositing render --
fun ref render(): Grid =>
"""
Render background, then overlay all child grids on top.
Non-empty cells from children overwrite the background.
"""
let bg = render_background()
let s = state()
let w = s.width
let h = s.height
let grids = s.child_grids
if grids.size() == 0 then
return bg
end
let empty = s.empty_cell()
let cells: Array[Cell] iso = recover iso
let size = w * h
let arr = Array[Cell](size)
// Copy background
for row in Range(0, h) do
for col in Range(0, w) do
match bg(col, row)
| let c: Cell => arr.push(c)
| GridCellOutOfBounds => arr.push(empty)
end
end
end
arr
end
// Blit child grids on top
for child in grids.values() do
(_, let grid) = child
for row in Range(0, grid.height.min(h)) do
for col in Range(0, grid.width.min(w)) do
match grid(col, row)
| let c: Cell =>
if (c.char != ' ') or (c.attrs != 0) then
try cells((row * w) + col)? = c end
end
end
end
end
end
let cells_val: Array[Cell] val = consume cells
GridFactory(w, h, cells_val)
// -- Provided: coalesced child management --
be receive_grid(widget: Any tag, grid: Grid) =>
"""
Receive a child's grid and store it. Only updates grids for children
previously registered via register_child, pack_start/pack_end, or
set_child. Unknown senders are ignored.
"""
let s = state()
for i in Range(0, s.child_grids.size()) do
try
(let w, _) = s.child_grids(i)?
if w is widget then
s.child_grids(i)? = (w, grid)
if not s.dirty then
s.dirty = true
_deferred_render()
end
return
end
end
end
be _deferred_render() =>
"""
Runs after all queued receive_grid messages have been processed.
Performs one render pass and sends the result to the parent.
"""
state().dirty = false
render_and_send()
be resize(w: USize, h: USize) =>
"""
Update size, propagate resize to all children, and re-render.
"""
let s = state()
s.width = w
s.height = h
for child in s.child_grids.values() do
(let cw, _) = child
match cw
| let r: Widget tag => r.resize(w, h)
end
end
render_and_send()