use "collections"
actor Stack is CompositeWidget
"""
A container that holds multiple named children but only renders the
active one. Used for tabbed interfaces where switching between views
should preserve child state.
"""
let _state: WidgetState
let _children_by_name: Map[String val, Widget tag]
let _children_order: Array[(String val, Widget tag)]
var _active_name: (String val | None) = None
var _input_actor: (InputActor tag | None) = None
new create(p: WidgetParent tag) =>
_state = WidgetState(p)
_children_by_name = Map[String val, Widget tag]
_children_order = Array[(String val, Widget tag)]
fun ref state(): WidgetState => _state
be set_input_actor(input: InputActor tag) =>
"""
Store a reference to the InputActor for focus scope gating.
"""
_input_actor = input
be add_child(name: String val, widget: Widget tag) =>
"""
Register a child by name. The first child added becomes active.
If the Stack already has dimensions, the child is resized immediately.
Non-active children have their scope disabled if input_actor is set.
"""
_children_by_name(name) = widget
_children_order.push((name, widget))
register_child(widget)
if _active_name is None then
_active_name = name
else
// Non-active child: disable its scope for focus gating
match _input_actor
| let ia: InputActor tag => ia.disable_scope(widget)
end
end
// If already sized, resize the child to fill
if (_state.width > 0) or (_state.height > 0) then
widget.resize(_state.width, _state.height)
end
be show(name: String val) =>
"""
Switch the active child. No-op if the name doesn't exist or is
already active.
"""
// Check if name exists
if not _children_by_name.contains(name) then return end
// Check if already active
match _active_name
| let current: String val if current == name => return
end
// Disable old scope, enable new scope
match _input_actor
| let ia: InputActor tag =>
// Disable old active child's scope
match _active_name
| let old_name: String val =>
try
let old_widget = _children_by_name(old_name)?
ia.disable_scope(old_widget)
end
end
// Enable new child's scope
try
let new_widget = _children_by_name(name)?
ia.enable_scope(new_widget)
end
end
_active_name = name
// Re-render with the new active child
render_and_send()
be receive_grid(widget: Any tag, grid: Grid) =>
"""
Store all children's grids, but only trigger a deferred render if
the grid came from the active child.
"""
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 _is_active_widget(widget) then
if not s.dirty then
s.dirty = true
_deferred_render()
end
end
return
end
end
end
be resize(w: USize, h: USize) =>
"""
Resize ALL children to full container size, not just the active one.
"""
_state.width = w
_state.height = h
for child in _state.child_grids.values() do
(let cw, _) = child
match cw
| let r: Widget tag => r.resize(w, h)
end
end
render_and_send()
fun ref render(): Grid =>
"""
Return the active child's grid directly. If no active child,
return the background.
"""
match _active_name
| let name: String val =>
try
let widget = _children_by_name(name)?
for entry in _state.child_grids.values() do
(let w, let grid) = entry
if w is widget then
return grid
end
end
end
end
render_background()
be _register_focusable(widget: Widget tag, scope: Widget tag) =>
"""
Package-private. Register a focusable widget with the InputActor, using
the given scope (a direct child of this Stack). Called by UIBuilder so
that register_focusable and disable_scope both go through this actor,
guaranteeing causal ordering on the InputActor.
"""
match _input_actor
| let ia: InputActor tag =>
ia.register_focusable(widget, scope)
end
be _flush(cb: {()} val) =>
"""
Package-private. Calls cb after all previously queued behaviors on this
actor have been processed. Used by tests to establish causal ordering
between add_child (which sends disable_scope) and a subsequent query.
"""
cb()
fun ref _is_active_widget(widget: Any tag): Bool =>
"""
Check if the given widget is the currently active child.
"""
match _active_name
| let name: String val =>
try
let active = _children_by_name(name)?
active is widget
else
false
end
else
false
end