Widget tree, box model and layout
ManyUI.DIRTY_ALL — Constant
PAINT | LAYOUT | STYLE. Note that it excludes the SUBTREE breadcrumb.
ManyUI.DirtyMask — Type
Bitset over Dirty.T values.
ManyUI.WidgetNode — Type
mutable struct WidgetNodePer-widget state, held by COMPOSITION: every widget HAS a node. No abstract-field inheritance anywhere.
parent/children are Widget-typed by necessity – a heterogeneous tree is the point – and every hot path recovers inference through node(w), whose return type is concrete.
Fields
id::Symbolclasses::Set{Symbol}type_name::Symbolparent::Union{Nothing, Widget}: Parent widget, ornothingat the root.children::Vector{Widget}inline_style::Style: Author-set style; highest specificity.inline_box::BoxPatch: Author-set box overrides; highest specificity.computed_style::Style: Post-cascade style.box::BoxStyle: Post-cascade layout inputs.layout::LayoutBox: Post-layout boxes, in ABSOLUTE coordinates.scroll::Offset: Cells this node's CHILDREN are shifted by at paint time.ORIGINis unscrolled.Offset(3, 0)means "scrolled right by 3", so a child paints three cells to the LEFT. Shifts the CHILDREN and NEVER this node's own render frame – that is what keepssize(buf)insiderender!invariant under scroll. A widget with no children that scrolls its own content reads this field itself.NEVER an input to
compute_layout: theLayoutMapis invariant under scrolling, which is why a wheel tick is a repaint and not a relayout.
dirty::UInt8: What is stale about this node.visible::Bool: False hides the node and its subtree.focusable::Bool: True makes the node part of the tab order.focused::Bool: True when THIS node holds focus. What:focusreads.Maintained by
focus!, not by a widget's ownfocusedcell: those are per-widget appearance, this is the one authoritative answer the cascade asks of every node.
focus_within::Bool: True when a DESCENDANT holds focus. What:focus-withinreads.A stored flag and not a walk of the subtree. The cascade asks this of every node against every rule, so answering it by descending would make one focus change cost the square of the tree;
focus!maintains it along the ONE chain from the focused node to the root.
app::Any: The owning App, once mounted.on_focus::Union{Nothing, Function}: Optional callback when focus is gained.on_blur::Union{Nothing, Function}: Optional callback when focus is lost.
ManyUI.WidgetNode — Type
WidgetNode(
id::Symbol,
classes::Set{Symbol},
type_name::Symbol,
parent::Union{Nothing, Widget},
children::Vector{Widget},
inline_style::Style,
inline_box::BoxPatch,
computed_style::Style,
box::BoxStyle,
layout::LayoutBox,
dirty::UInt8,
visible::Bool,
focusable::Bool,
app
) -> WidgetNode
WidgetNode(
id::Symbol,
classes::Set{Symbol},
type_name::Symbol,
parent::Union{Nothing, Widget},
children::Vector{Widget},
inline_style::Style,
inline_box::BoxPatch,
computed_style::Style,
box::BoxStyle,
layout::LayoutBox,
dirty::UInt8,
visible::Bool,
focusable::Bool,
app,
on_focus::Union{Nothing, Function}
) -> WidgetNode
WidgetNode(
id::Symbol,
classes::Set{Symbol},
type_name::Symbol,
parent::Union{Nothing, Widget},
children::Vector{Widget},
inline_style::Style,
inline_box::BoxPatch,
computed_style::Style,
box::BoxStyle,
layout::LayoutBox,
dirty::UInt8,
visible::Bool,
focusable::Bool,
app,
on_focus::Union{Nothing, Function},
on_blur::Union{Nothing, Function}
) -> WidgetNode
The pre-scroll POSITIONAL form: scroll defaults to ORIGIN.
Exists because adding a field silently invalidates the auto-generated positional constructor, and call sites in test_paint.jl and css_tests.jl depend on the 14-argument shape. Keep the argument order in lockstep with the struct.
ManyUI.WidgetNode — Method
WidgetNode(
id::Symbol,
classes::Set{Symbol},
type_name::Symbol,
parent::Union{Nothing, Widget},
children::Vector{Widget},
inline_style::Style,
inline_box::BoxPatch,
computed_style::Style,
box::BoxStyle,
layout::LayoutBox,
scroll::Offset,
dirty::UInt8,
visible::Bool,
focusable::Bool,
app
) -> WidgetNode
The positional form that names scroll explicitly, retained for compatibility after adding focus callbacks to WidgetNode.
ManyUI.WidgetNode — Method
WidgetNode(
;
id,
classes,
type_name,
visible,
focusable,
on_focus,
on_blur
) -> WidgetNode
A fresh node: no parent, no children, nothing cascaded, nothing laid out, everything dirty.
Base.getindex — Method
getindex(w::Widget, i::Int64) -> Widget
children(w)[i]. Pure.
Base.parent — Method
parent(w::Widget) -> Union{Nothing, Widget}
Parent of w, or nothing at the root. Pure.
ManyUI._attach! — Method
_attach!(p::Widget, c::Widget)
Link c (and its whole subtree) into p's tree: set the parent, bind the app, fire on_mount! in pre-order, then flag the new subtree for a cascade and a layout.
Internal: the three public entry points (mount!, mount! variadic and insert_child!) all funnel through here so the lifecycle is defined exactly once.
ManyUI._breadcrumb! — Method
_breadcrumb!(w::Widget)
Drop the Dirty.SUBTREE breadcrumb on every ancestor of w, and on nothing else.
Internal. The breadcrumb is a routing hint, not dirt: is_dirty(a) stays false for an ancestor that only carries it, which is exactly what makes E1 ("only that specific widget and its affected descendants") literally true.
ManyUI._find_first — Method
_find_first(
w::Widget,
cs::Vector{String}
) -> Union{Nothing, Widget}
Depth-first search for the first match, exiting as soon as it is found. Pure.
ManyUI._focus_callback! — Method
_focus_callback!(w::Widget)
Lifecycle hook: w has just gained focus. Default: reveal!(w), so TAB-ing to a widget inside a scrolling viewport scrolls it into view with no wiring at the call site.
OVERRIDING THIS REPLACES IT: a widget type with its own on_focus! MUST call reveal!(w) itself.
ManyUI._main_axis_is_definite — Method
_main_axis_is_definite(bs::BoxStyle) -> Bool
True when bs's main-axis size resolves without measuring its children.
CELLS and PERCENT are definite; AUTO sizes to content and FRACTION is a share of free space, so both genuinely depend on the subtree. Pure.
ManyUI._main_axis_length — Method
_main_axis_length(bs::BoxStyle) -> Length
The Length governing bs's own main-axis size: width for a ROW, height for a COLUMN. Pure.
ManyUI._match_any — Method
_match_any(w::Widget, cs::Vector{String}) -> Bool
True when w matches any compound in the parsed selector list. Pure.
ManyUI._match_compound — Method
_match_compound(w::Widget, sel::AbstractString) -> Bool
True when w satisfies every token of the validated compound selector sel. Pure.
ManyUI._parse_selector — Method
_parse_selector(sel::AbstractString) -> Vector{String}
Split a comma-separated selector list into validated compound selectors. Throws ArgumentError on anything this layer cannot express. Pure.
ManyUI.add_class! — Method
add_class!(w::Widget, c::Symbol) -> Widget
Add a class and mark Dirty.STYLE. Adding a class w already carries is a no-op: nothing cascades differently, so nothing is flagged. Returns w.
ManyUI.ancestors — Method
ancestors(w::Widget) -> Vector{Widget}
w's ancestors, from its parent up to the root. Pure.
ManyUI.app — Method
app(w::Widget) -> Any
The App this widget is mounted on, or nothing. Walks the tree.
ManyUI.border_title — Method
border_title(_::Widget) -> RichText
The caption drawn ON w's top border, or empty for none.
A SEAM, not a field, for the same reason the scrollable one is three functions: a title belongs to the handful of widgets that frame something, and a slot on every WidgetNode would charge the other thousands for it. Override it and any widget gains a caption.
It is a seam AT ALL because a widget cannot draw this itself. _paint_node! hands render! the CONTENT box, and the border is outside it, so a titled box drawing its own caption would have to reserve a content row – which puts the caption inside the frame rather than on it. The paint pass asks instead.
Empty by default, and the default returns a SHARED constant: this is asked of every node on every frame.
ManyUI.border_title_align — Method
border_title_align(_::Widget) -> ManyUI.Align.T
Where border_title(w) sits along the top edge: Align.START, Align.CENTER or Align.END. Align.START by default.
ManyUI.box — Method
box(w::Widget) -> BoxStyle
Post-cascade layout inputs of w. Pure.
ManyUI.children — Method
children(w::Widget) -> Tuple{Widget}
Children of w, in paint and tab order. Pure.
ManyUI.clamp_scroll — Method
clamp_scroll(
pos::Int64,
viewport::Int64,
content::Int64
) -> Int64
pos clamped to the range a scroll position may legally take: 0 when the content fits, 0:(content - viewport) otherwise.
THE definition of "clamped to the content extent". Pure, total, and shared by every scrolling widget so they cannot disagree – with no widget-to-widget dependency. Pure.
ManyUI.classes — Method
classes(w::Widget) -> Set{Symbol}
CSS classes of w. Pure.
ManyUI.clean! — Method
clean!(w::Widget)
Clear w's whole dirty mask.
ManyUI.clear_dirty — Method
clear_dirty(m::UInt8, k::ManyUI.Dirty.T) -> UInt8
m with k cleared. Pure.
ManyUI.computed_style — Method
computed_style(w::Widget) -> Style
Post-cascade style of w. Pure.
ManyUI.content_region — Method
content_region(w::Widget) -> Region
node(w).layout.content. Pure.
ManyUI.descendants — Method
descendants(w::Widget) -> Vector{Widget}
w's descendants in pre-order, excluding w. Pure.
ManyUI.dirty_root — Method
dirty_root(root::Widget) -> Union{Nothing, Widget}
The HIGHEST node carrying real Dirty.LAYOUT – the root of the minimal relayout. nothing when the tree is layout-clean.
Found by descending SUBTREE breadcrumbs, so it is O(depth), not O(n). When two independent branches are layout-dirty the descent stops at their common ancestor – the shallowest node a correct relayout can start from – even though that node carries only the breadcrumb. Pure.
ManyUI.escalate_auto! — Method
escalate_auto!(w::Widget)
Walk UP from w. An ancestor whose main-axis size is AUTO genuinely depends on its children, so its SUBTREE breadcrumb is PROMOTED to real LAYOUT.
Stops at the first ancestor with a definite (CELLS/PERCENT) size on the relevant axis – that ancestor keeps only SUBTREE, because its own box cannot move, and the walk goes no higher. This is what makes relayout! sublinear.
A promoted ancestor gets LAYOUT but NOT PAINT: whether its box actually changed is apply_layout!'s call to make, not ours.
ManyUI.has_class — Method
has_class(w::Widget, c::Symbol) -> Bool
True when w carries class c. Pure.
ManyUI.has_dirty — Method
has_dirty(m::UInt8, k::ManyUI.Dirty.T) -> Bool
True when k is set in m. Pure.
ManyUI.insert_child! — Method
insert_child!(p::Widget, i::Int64, c::Widget) -> Widget
Mount c at index i. Returns p.
ManyUI.is_dirty — Method
is_dirty(w::Widget, kind::ManyUI.Dirty.T) -> Bool
True when kind is set on w. Pure.
ManyUI.is_dirty — Method
is_dirty(w::Widget) -> Bool
True when any of PAINT, LAYOUT or STYLE is set on w. Pure.
Dirty.SUBTREE alone is deliberately NOT dirt.
ManyUI.is_focusable — Method
is_focusable(w::Widget) -> Bool
True when w takes part in the tab order. Pure.
ManyUI.is_visible — Method
is_visible(w::Widget) -> Bool
False when w or its subtree is hidden. Pure.
ManyUI.layout_of — Method
layout_of(w::Widget) -> LayoutBox
The four resolved boxes of w. Pure.
ManyUI.mark! — Function
mark!(w::Widget)
mark!(w::Widget, kind::ManyUI.Dirty.T)
E1. The full propagation – THE function Reactive and the widget API call. Rules, normative:
PAINT on w -> w ONLY. Never ancestors, never descendants.
LAYOUT on w -> LAYOUT|PAINT on w AND ALL DESCENDANTS (their
absolute regions derive from w's, so they are
genuinely stale -- this is "and its affected
descendants" in the EARS text), then
`escalate_auto!(w)`.
STYLE on w -> STYLE on w and all descendants (inheritance plus
descendant selectors).In EVERY case each ANCESTOR receives Dirty.SUBTREE and NOTHING ELSE. Marking an ancestor LAYOUT would make E1 false as written.
SIBLINGS ARE NEVER MARKED.
ManyUI.mark_dirty! — Function
mark_dirty!(w::Widget)
mark_dirty!(w::Widget, kind::ManyUI.Dirty.T)
E1. Set kind on w ONLY. Touches no ancestor and no descendant.
ManyUI.mark_subtree_dirty! — Method
mark_subtree_dirty!(w::Widget, kind::ManyUI.Dirty.T)
E1. Set kind on w AND on every descendant.
ManyUI.mount! — Method
mount!(p::Widget, cs::Widget...) -> Scrollpane
Mount several children in order. Returns p.
ManyUI.mount! — Method
mount!(p::Widget, c::Widget) -> Scrollpane
Append c to p, bind it to p's app, and call on_mount!. Returns p.
Throws ArgumentError when c already has a parent: a widget lives in exactly one tree, and silently re-parenting it would leave the old parent's children stale.
ManyUI.node — Method
node(w::Widget) -> WidgetNode
THE one required method per widget type. The default duck-types on the field name; override only for exotic widgets.
ManyUI.on_blur! — Method
on_blur!(w::Widget)
Lifecycle hook: w has just lost focus. Default no-op.
ManyUI.on_mount! — Method
on_mount!(w::Widget)
Lifecycle hook: w has just been added to a tree. Default no-op.
ManyUI.on_unmount! — Method
on_unmount!(w::Widget)
Lifecycle hook: w is about to leave its tree. Default no-op.
ManyUI.paint_offset — Method
paint_offset(w::Widget) -> Offset
The accumulated shift w is PAINTED with: the sum of every STRICT ancestor's scroll, negated. ORIGIN outside any scrolled subtree.
A node's OWN scroll is deliberately not included – it shifts that node's children, not itself. O(depth), and never on the frame path: _paint_node! threads the same value down for free. Pure.
ManyUI.painted_region — Method
painted_region(w::Widget) -> Region
Where w's border box is actually PAINTED: region(w) shifted by paint_offset(w). Identical to region(w) outside a scrolled subtree.
THIS, not region(w), is what a hit test must compare a pointer against: layout computes ABSOLUTE regions and knows nothing about scrolling, so region(w) is where w WOULD be if nothing had scrolled. Pure.
ManyUI.path_from_root — Method
path_from_root(w::Widget) -> Vector{Widget}
Root to w, inclusive of both. Pure.
ManyUI.query — Method
query(root::Widget, sel::AbstractString) -> Vector{Widget}
Every widget in root's tree matching the CSS selector sel, in pre-order. Pure.
Supports *, #id, .class, Type, compounds of those (Button.primary#ok) and comma-separated lists. Combinators live in css.jl; query is the dependency-free subset this layer can afford.
ManyUI.query_one — Method
query_one(
root::Widget,
sel::AbstractString
) -> Union{Nothing, Widget}
The first widget matching sel, or nothing. Pure.
ManyUI.query_one — Method
query_one(
root::Widget,
sel::AbstractString,
_::Type{T<:Widget}
) -> Widget
The first widget matching sel, asserted to be a T. Throws when absent or of the wrong type. Pure.
ManyUI.region — Method
region(w::Widget) -> Region
node(w).layout.border_box. Pure.
ManyUI.remove_class! — Method
remove_class!(w::Widget, c::Symbol) -> Widget
Remove a class and mark Dirty.STYLE. Removing an absent class is a no-op. Returns w.
ManyUI.replace_child! — Method
replace_child!(
p::Widget,
old::Widget,
new::Widget
) -> Widget
Swap old for new in place. Returns p.
ManyUI.reveal! — Method
reveal!(w::Widget)
Ask every ancestor of w, NEAREST FIRST, to bring w into view.
Nearest-first is load-bearing: an inner pane must finish moving before an outer pane measures where w ended up. Never on the frame path – focus changes are events, not frames.
ManyUI.reveal_child! — Method
reveal_child!(_::Widget, _::Widget)
Hook: bring d, a descendant of w, into w's visible area. Default no-op; a scrolling viewport overrides it.
The core therefore knows that a node MAY be able to reveal a descendant, and nothing whatever about how – there is no isa Scrollpane anywhere below layer 7.
ManyUI.root_of — Method
root_of(w::Widget) -> Widget
The root of w's tree; w itself when it has no parent. Pure.
ManyUI.scroll_into_view — Method
scroll_into_view(
pos::Int64,
viewport::Int64,
lo::Int64,
hi::Int64
) -> Int64
The scroll position NEAREST pos that puts the 0-based inclusive extent lo:hi inside a viewport-cell window starting at pos. Pure.
MINIMAL MOVEMENT: already-visible content does not move. When the extent is LARGER than the viewport its START wins – showing the top of an over-long item beats showing its bottom. Returns pos unchanged for a non-positive viewport (not laid out yet).
The asymmetry is deliberate and clamp cannot express it: clamp with an inverted lo/hi silently picks the wrong edge.
ManyUI.scroll_of — Method
scroll_of(w::Widget) -> Offset
node(w).scroll. Pure.
ManyUI.set_dirty — Method
set_dirty(m::UInt8, k::ManyUI.Dirty.T) -> UInt8
m with k set. Pure.
ManyUI.set_scroll! — Method
set_scroll!(w::Widget, o::Offset) -> Bool
Set the shift applied to w's CHILDREN at paint time. Returns true iff it changed.
Clamped at ZERO per axis only. The UPPER bound needs the content extent, which this layer cannot see – scroll_to! in widgets/scroll.jl owns it. An over-scroll is a UX bug (blank cells), never corruption.
Marks Dirty.PAINT and NOTHING ELSE, and this is the whole "a wheel tick does not re-run layout" claim: mark! with PAINT sets PAINT on w alone and drops only SUBTREE breadcrumbs on ancestors; it calls escalate_auto! ONLY for Dirty.LAYOUT. dirty_root then finds no real LAYOUT and returns nothing, so relayout! returns on its first line – and compute_layout never reads this field anyway.
mark!(w, Dirty.PAINT) and NOT mark_subtree_dirty!: the normative rule above is "PAINT on w -> w ONLY", and frame! repaints the whole tree unconditionally in any case.
ManyUI.set_visible! — Method
set_visible!(w::Widget, v::Bool)
Show or hide w and mark Dirty.LAYOUT. Setting the value w already has is a no-op.
ManyUI.toggle_class! — Method
toggle_class!(w::Widget, c::Symbol) -> Bool
Toggle a class and mark Dirty.STYLE. Returns the new membership.
ManyUI.type_name — Method
type_name(w::Widget) -> Symbol
Type name of w, as CSS type selectors see it. Pure.
ManyUI.unmount! — Method
unmount!(c::Widget) -> Widget
Call on_unmount! and detach c from its parent. Returns c.
The detached subtree keeps its own structure and is left dirty-clean; the OLD parent is marked Dirty.LAYOUT, because its remaining children restack.
ManyUI.walk — Method
walk(f, w::Widget)
Call f on w and every descendant, pre-order.
ManyUI.walk_visible — Method
walk_visible(f, w::Widget)
Like walk, but skips invisible subtrees entirely.
ManyUI.Reactive — Type
mutable struct Reactive{T}A reactive cell.
Assigning a value that differs (!=) from the current one marks owner dirty with kind AND posts a RefreshEvent, so a write from a worker task actually repaints.
Concretely typed: a widget field count::Reactive{Int} stays type-stable. owner is a small union, NOT Any.
label.text[] # read, zero cost
label.text[] = "hi" # write, marks dirty, schedules a frameChoosing Dirty.PAINT for state that can change size is a correctness bug, so the default is the conservative Dirty.LAYOUT.
Fields
value::Any: The current value.owner::Union{Nothing, Widget}: The widget marked dirty on a real change.kind::ManyUI.Dirty.T
ManyUI.Reactive — Method
An unbound cell holding v.
The parametric form pins T; the plain form infers it from v. Both default to the conservative Dirty.LAYOUT. Call bind_owner! (or attach_reactives!) to give the cell a widget to invalidate.
Base.getindex — Method
getindex(r::Reactive{T}) -> Any
Read the value. Type-stable and allocation-free. Pure.
Base.setindex! — Method
setindex!(r::Reactive{T}, v) -> Any
Write the value, converting to T first.
Base.setindex! — Method
setindex!(r::Reactive{T}, v) -> Any
E1. Write the value.
NO-OP – no dirty mark, no RefreshEvent – when v == r.value. The == guard means a redundant write costs one comparison and zero frames.
On a real change: r.value = v; mark!(r.owner, r.kind); and when app(r.owner) !== nothing, post!(app, RefreshEvent()).
ManyUI.attach_reactives! — Method
attach_reactives!(w::Widget)
Wire every Reactive field of w to w. Call at the END of a widget constructor.
Uses fieldnames plus getfield, and skips non-Reactive fields.
ManyUI.bind_owner! — Method
bind_owner!(r::Reactive, w::Widget)
Point r at the widget it invalidates.
ManyUI.Border — Type
struct BorderA border: a line style plus the style its glyphs are drawn in. isbits.
Fields
kind::ManyUI.BorderKind.T: Line style.style::Style: Style the glyphs are painted with.
ManyUI.BoxPatch — Type
struct BoxPatchThe optional-every-field twin of BoxStyle: nothing means "not specified".
Deliberately NOT isbits, and that is fine – it exists only at parse and cascade time, never in the render loop.
Fields
display::Union{Nothing, ManyUI.Display.T}: OverridesBoxStyle.displaywhen set.direction::Union{Nothing, ManyUI.Direction.T}: OverridesBoxStyle.directionwhen set.justify::Union{Nothing, ManyUI.Justify.T}: OverridesBoxStyle.justifywhen set.align::Union{Nothing, ManyUI.Align.T}: OverridesBoxStyle.alignwhen set.width::Union{Nothing, Length}: OverridesBoxStyle.widthwhen set.height::Union{Nothing, Length}: OverridesBoxStyle.heightwhen set.min_width::Union{Nothing, Length}: OverridesBoxStyle.min_widthwhen set.min_height::Union{Nothing, Length}: OverridesBoxStyle.min_heightwhen set.max_width::Union{Nothing, Length}: OverridesBoxStyle.max_widthwhen set.max_height::Union{Nothing, Length}: OverridesBoxStyle.max_heightwhen set.margin::Union{Nothing, Spacing}: OverridesBoxStyle.marginwhen set.padding::Union{Nothing, Spacing}: OverridesBoxStyle.paddingwhen set.border::Union{Nothing, Border}: OverridesBoxStyle.borderwhen set.gap::Union{Nothing, Int64}: OverridesBoxStyle.gapwhen set.overflow_x::Union{Nothing, ManyUI.Overflow.T}: OverridesBoxStyle.overflow_xwhen set.overflow_y::Union{Nothing, ManyUI.Overflow.T}: OverridesBoxStyle.overflow_ywhen set.grow::Union{Nothing, Float32}: OverridesBoxStyle.growwhen set.shrink::Union{Nothing, Float32}: OverridesBoxStyle.shrinkwhen set.
ManyUI.BoxPatch — Method
BoxPatch(; kwargs...) -> BoxPatch
Keyword constructor; every field defaults to nothing.
An unknown keyword throws ArgumentError: a stylesheet property that silently patched nothing would be invisible at every layer above.
ManyUI.BoxStyle — Type
struct BoxStyleThe cascade's output and the layout engine's input. isbits, and it lives inline in WidgetNode – no pointer chasing during layout.
Fields
display::ManyUI.Display.T: Whether and how the box participates in layout.direction::ManyUI.Direction.T: Main axis of the flex container.justify::ManyUI.Justify.T: Main-axis distribution of the children.align::ManyUI.Align.T: Cross-axis alignment of the children.width::Length: Preferred width.height::Length: Preferred height.min_width::Length: Lower width bound.min_height::Length: Lower height bound.max_width::Length: Upper width bound.max_height::Length: Upper height bound.margin::Spacing: Space outside the border box.padding::Spacing: Space between the border and the content.border::Border: Border drawn on the perimeter of the border box.gap::Int64: Cells inserted between adjacent children.overflow_x::ManyUI.Overflow.T: Horizontal overflow policy.overflow_y::ManyUI.Overflow.T: Vertical overflow policy.grow::Float32: Flex grow factor.shrink::Float32: Flex shrink factor.
ManyUI.BoxStyle — Method
BoxStyle(base::BoxStyle; kwargs...) -> BoxStyle
Functional update: a copy of base with the given fields replaced. Pure.
An unknown keyword throws ArgumentError rather than being ignored: a silently dropped BoxStyle(b; witdh = cells(3)) is a layout bug with no symptom at the call site.
ManyUI.BoxStyle — Method
BoxStyle(
;
display,
direction,
justify,
align,
width,
height,
min_width,
min_height,
max_width,
max_height,
margin,
padding,
border,
gap,
overflow_x,
overflow_y,
grow,
shrink
) -> BoxStyle
Keyword constructor with the documented defaults.
ManyUI.LayoutBox — Type
struct LayoutBoxThe four resolved boxes of one node, in ABSOLUTE 1-based screen coordinates.
INVARIANT: margin_box includes border_box includes padding_box includes content.
Fields
margin_box::Region: Outermost box; includes the margin.border_box::Region:margin_boxshrunk by the margin. The border is drawn ON its perimeter.padding_box::Region:border_boxshrunk by the border thickness.content::Region:padding_boxshrunk by the padding.
ManyUI.Length — Type
struct LengthA resolvable length. isbits.
Fields
kind::ManyUI.Dimension.T: How to readvalue.value::Float32: Cells, percent, or fr weight, perkind.
Base.isempty — Method
isempty(p::BoxPatch) -> Bool
True when p sets no field at all. Pure.
BoxPatch is an immutable struct, so === is structural (field-wise egal, recursively) rather than pointer identity – comparing against the all-nothing value is exactly "sets no field", in one shot.
Base.merge — Method
merge(a::BoxPatch, b::BoxPatch) -> BoxPatch
Merge two patches: b wins wherever it is set. Pure.
A monoid with identity BOX_PATCH_NONE, matching merge(::Style, ::Style). This is the box half of the cascade fold.
Base.parse — Method
parse(_::Type{Length}, s::AbstractString) -> Length
Parse a length, throwing ArgumentError on garbage. Pure.
Base.tryparse — Method
tryparse(
_::Type{Length},
s::AbstractString
) -> Union{Nothing, Length}
Parse a length, returning nothing instead of throwing.
Accepts "auto", "10", "10cells", "50%", "1fr", "3fr". Case-insensitive; surrounding whitespace is ignored. Pure.
ManyUI.apply — Method
apply(base::BoxStyle, p::BoxPatch) -> BoxStyle
Apply p onto base: p wins wherever it is set. Pure.
The identity is BOX_PATCH_NONE: apply(b, BOX_PATCH_NONE) === b.
ManyUI.border_glyphs — Method
border_glyphs(k::ManyUI.BorderKind.T) -> NTuple{8, Char}
The eight glyphs of a border kind: (tl, top, tr, right, br, bottom, bl, left) – clockwise from the top-left. Pure.
NONE and BLANK both yield spaces. They differ in thickness, not in their glyphs: NONE reserves no cells and is never drawn, whereas BLANK reserves its cells and paints them blank.
Every glyph is width-1 by construction (verified in test_paint.jl); a width-2 glyph here would desynchronise the grid on every border.
ManyUI.box_overhead — Method
box_overhead(bs::BoxStyle) -> Spacing
Total non-content spacing: margin + border + padding. Pure.
ManyUI.cells — Method
cells(n::Real) -> Length
An absolute length in cells. Pure.
ManyUI.definite_size — Method
definite_size(d::Length, available::Int64) -> Int64
Resolve a definite length against available. CELLS and PERCENT only; PERCENT rounds via round(Int). Pure.
Throws ArgumentError for AUTO and FRACTION: neither can be resolved without measuring content or knowing the free space, so a silent 0 would be a layout bug that never announces itself.
ManyUI.is_definite — Method
is_definite(d::Length) -> Bool
True for CELLS and PERCENT – the kinds resolvable without measuring content. Pure.
ManyUI.layout_box — Method
layout_box(bs::BoxStyle, margin_box::Region) -> LayoutBox
Derive all four boxes from an outer margin_box and a BoxStyle.
THE single definition of the box model: no other file may re-derive these. Pure.
ManyUI.outer_size — Method
outer_size(bs::BoxStyle, inner::Size) -> Size
The margin-box size needed to hold a content box of inner. Pure.
This is the sole content-box -> outer-box conversion in the package, and so it is what pins BoxStyle.width/height to the CONTENT box (the CSS box-sizing: content-box default).
ManyUI.pct — Method
pct(n::Real) -> Length
A percentage of the parent's content box. Pure.
ManyUI.thickness — Method
thickness(b::Border) -> Spacing
Cells the border occupies per edge: NO_SPACING for NONE, one cell on every edge otherwise (BLANK included). Pure.
ManyUI.LayoutMap — Type
The pure layout pass's output: a node to its four resolved boxes.
ManyUI._apportion — Method
_apportion(
weights::AbstractVector{<:Real},
total::Int64
) -> Vector{Int64}
Share total cells out over weights, largest-remainder-first with ties going to the LOWER index, so sum(result) == total exactly whenever sum(weights) > 0. Pure.
ManyUI._arrange! — Method
_arrange!(
lm::IdDict{Widget, LayoutBox},
w::Widget,
bs::BoxStyle,
content::Region
)
Size and place every laid-out child of w inside content, then recurse. Pure with respect to the tree. Pure.
ManyUI._bound_outer — Method
_bound_outer(
len::Length,
over::Int64,
ref::Int64,
lower::Bool
) -> Int64
Resolve a min_*/max_* bound to an OUTER extent. An AUTO bound means "unbounded": 0 for a lower bound, typemax(Int) for an upper one. Pure.
ManyUI._content_extent — Method
_content_extent(
w::Widget,
len::Length,
horiz::Bool,
main::Bool,
avail::Size
) -> Int64
Resolve one axis of w to a CONTENT extent against the parent content box avail. FRACTION contributes nothing here – it claims free space later. Pure.
ManyUI._cross_inset — Method
_cross_inset(sp::Spacing, horiz::Bool) -> Int64
Cross-axis component of a Spacing. Pure.
ManyUI._cross_length — Method
_cross_length(bs::BoxStyle, horiz::Bool) -> Length
The Length sizing the cross axis of bs. Pure.
ManyUI._cross_of — Method
_cross_of(s::Size, horiz::Bool) -> Int64
Cross-axis extent of s. Pure.
ManyUI._is_reverse — Method
_is_reverse(d::ManyUI.Direction.T) -> Bool
True when d runs the main axis backwards. Pure.
ManyUI._is_row — Method
_is_row(d::ManyUI.Direction.T) -> Bool
True when the main axis of d is horizontal. Pure.
ManyUI._laid_out — Method
_laid_out(w::Widget) -> Bool
True when w takes part in layout at all. Pure.
ManyUI._main_direction — Method
_main_direction(bs::BoxStyle) -> ManyUI.Direction.T
The direction a container actually arranges its children along: Display.BLOCK is COLUMN whatever direction says. Pure.
ManyUI._main_inset — Method
_main_inset(sp::Spacing, horiz::Bool) -> Int64
Main-axis component of a Spacing. Pure.
ManyUI._main_length — Method
_main_length(bs::BoxStyle, horiz::Bool) -> Length
The Length sizing the main axis of bs. Pure.
ManyUI._main_of — Method
_main_of(s::Size, horiz::Bool) -> Int64
Main-axis extent of s. Pure.
ManyUI._measure_outer — Method
_measure_outer(w::Widget, avail::Size) -> Size
The margin-box size w wants, given the parent content box avail. Used by the default measure. Pure.
ManyUI._place! — Method
_place!(
lm::IdDict{Widget, LayoutBox},
w::Widget,
margin_box::Region
)
Record the four boxes of w in lm, then arrange its children. Pure with respect to the tree: lm is the only thing written.
ManyUI._split_even — Method
_split_even(total::Int64, k::Int64) -> Vector{Int64}
Split total cells into k as-equal-as-possible parts, the remainder going to the lowest indices. Pure.
ManyUI.apply_layout! — Method
apply_layout!(lm::IdDict{Widget, LayoutBox})
The impure shell: write lm into each WidgetNode.layout, clear Dirty.LAYOUT and Dirty.SUBTREE on every written node, and set Dirty.PAINT on any node whose LayoutBox actually CHANGED.
Nodes absent from lm are left untouched, breadcrumbs included: an ancestor's SUBTREE may still be carrying a PAINT-dirty descendant.
ManyUI.compute_layout — Method
compute_layout(
root::Widget,
viewport::Region
) -> IdDict{Widget, LayoutBox}
U2 + E4. PURE: tree plus viewport to boxes. Mutates NOTHING, and does not touch WidgetNode.layout. This is what makes the layout engine testable without an App, a Buffer, or a terminal.
viewport is root's MARGIN box, so root's own width/height are not consulted: the root fills what it is given. Every other node's margin box is derived from its parent's CONTENT box, and layout_box(bs, margin_box) derives all four regions – no file may re-derive them.
Two passes per container:
- MEASURE (bottom-up): resolve CELLS/PERCENT against the parent content box; AUTO calls
measure(child, avail). EveryLengthsizes the CONTENT box, andbox_overheadis added on top of it to reach the margin box that the flex kernel distributes over. - DISTRIBUTE (top-down): FRACTION items claim free space pro-rata by
frweight AFTER 1 and BEFORE grow;flex_distributeapplies grow/shrink to the residual, clamped bymin_*/max_*;justify_offsetsplaces along the main axis;cross_alignplaces and stretches along the cross axis.
Display.BLOCK is FLEX with direction COLUMN, align STRETCH and the children's grow forced to zero – ONE code path, no special case. Display.NONE and invisible subtrees are ABSENT from the returned map and claim no main-axis space.
Overflow is NOT clipped here: the map reports true geometry under every Overflow policy, and paint.jl clips through a BufferView.
ManyUI.cross_align — Method
cross_align(
size::Int64,
align::ManyUI.Align.T,
available::Int64
) -> Tuple{Int64, Int64}
Cross-axis (offset, size) for one item. Align.STRETCH returns (0, available). An item larger than available is given offset zero under every mode rather than a negative offset. Pure.
ManyUI.flex_distribute — Method
flex_distribute(
base::Vector{Int64},
grow::Vector{Float32},
shrink::Vector{Float32},
available::Int64
) -> Vector{Int64}
THE heart of flex. Pure. Distributes available over the base sizes using the grow/shrink factors.
When sum(base) < available the slack is shared out by grow weight. When sum(base) > available the excess is taken back by CSS SCALED shrink weight (shrink[i] * base[i]); an item whose share exceeds its own size freezes at zero rather than going negative, and the residue is re-offered to the items still able to shrink. When no item can absorb the slack at all the base sizes are returned unchanged – an overflowing layout is clipped at paint time, never mangled here.
NORMATIVE: fractional leftovers resolve LARGEST-REMAINDER-FIRST, so sum(result) == available EXACTLY whenever any item can absorb the slack. Integer cells: no one-cell drift, ever. Ties in the remainder go to the LOWER index.
ManyUI.justify_offsets — Method
justify_offsets(
sizes::Vector{Int64},
justify::ManyUI.Justify.T,
gap::Int64,
available::Int64
) -> Vector{Int64}
Main-axis start offsets, 0-based within the container's content box, for each item, given the item sizes, gap and total available. Pure.
The free space is available - sum(sizes) - gap * (n - 1), CLAMPED AT ZERO: when the items overflow, every mode degenerates to START rather than placing an item at a negative offset. Free space is shared out largest-remainder-first, so the trailing edge never drifts a cell.
ManyUI.layout! — Method
layout!(root::Widget, viewport::Region)
U2 + E4. An unconditional FULL pass over the whole tree; assigns layout on every visible node.
This is what a ResizeEvent calls – "instantly recalculate the bounding boxes of the ENTIRE layout tree". Never consults dirty flags.
ManyUI.measure — Method
measure(w::Widget, avail::Size) -> Size
Intrinsic content size of w given the available space.
Override for leaf widgets – a Label returns its wrapped text extent via text_width. The default is the union of the children's OUTER (margin-box) measures laid out along display/direction, with gap between them; a widget with no laid-out child therefore measures Size(0, 0). Drives Dimension.AUTO.
avail is the CONTENT size of the parent, and it is what PERCENT child lengths resolve against.
Pure with respect to the tree: no dirty marks, no field writes.
ManyUI.relayout! — Method
relayout!(root::Widget, viewport::Region)
E1 + U2. INCREMENTAL: recomputes from dirty_root(root) down ONLY.
dirty_root(root) === nothing– no-op, returns immediately.dirty_root(root) === root– falls back tolayout!.- otherwise –
compute_layouton the dirty subtree, anchored at the subtree root's EXISTINGmargin_box, thenapply_layout!.
Two functions, not one with a force::Bool keyword: E4 mandates a full recompute on resize, E1 mandates a minimal one on state change. Two requirements, two functions, two test suites.