Scrolling widgets
ManyUI.SB_THUMB — Constant
Thumb glyph. Width-1 BY CONSTRUCTION, asserted in the suite.
ManyUI.SB_TRACK_H — Constant
Track glyph. Width-1 BY CONSTRUCTION, asserted in the suite.
ManyUI.SB_TRACK_V — Constant
Track glyph. Width-1 BY CONSTRUCTION, asserted in the suite.
ManyUI.Scrollbar — Type
mutable struct Scrollbar{V<:Widget} <: WidgetThe visible scroll indicator for one axis of viewport.
PARAMETRIC on the viewport type – the Button{F} pattern – so viewport is a CONCRETE field and content_extent/layout_of dispatch statically. This is also what lets ONE Scrollbar serve a Scrollpane's Container canvas AND a TextArea, which scrolls data rather than children: the scrollable seam is three functions, not a type.
A SIBLING of the canvas, never a child of it: the core shifts only a node's children, so a bar is never inside a scrolled subtree and local_offset on it is always honest.
Fields
node::WidgetNode: Per-widget state.axis::ManyUI.ScrollAxis.Tmode::ManyUI.ScrollMode.Tviewport::Widget
ManyUI.Scrollbar — Method
A bar reporting on vp along axis.
ManyUI.Scrollpane — Type
Scrollpane(; ...) -> Scrollpane
Scrollpane(
child::Union{Nothing, Widget};
bar_y,
bar_x,
wheel_step,
wheel_step_x,
focusable,
id,
classes
) -> Scrollpane
A pane scrolling child, or an empty one.
The node geometry is:
Scrollpane display FLEX, direction COLUMN
+-- row FLEX ROW, grow 1
| +-- canvas grow 1, overflow SCROLL <-- THE WINDOW
| | +-- holder shrink 0, grow 0 <-- THE CONTENT
| | +-- child
| +-- vbar width 1 [iff bar_y !== NEVER]
+-- hbar height 1 [iff bar_x !== NEVER]Two of those lines carry the whole design and neither is negotiable:
shrink = 0f0on the HOLDER is what creates the overflow. The defaultshrinkis1f0, so without it the flex kernel absorbs the overflow and squeezes the content down to the window – an 8-row document in a 3-row window collapses to 3 rows and there is nothing left to scroll.align = Align.STARTon the canvas is what stops the holder's WIDTH being stretched to the window.Align.STRETCH, the default, would pin the content's width to the window's and horizontal scrolling could never happen at all.
The canvas is the WINDOW and the holder is the CONTENT, deliberately two nodes: one box cannot be both 3 rows tall (what you see) and 8 rows tall (what there is), and the scrollable seam reads the first from layout_of(canvas).content and the second from content_extent(canvas).
ManyUI.Scrollpane — Type
mutable struct Scrollpane <: WidgetA scrolling viewport over exactly ONE child.
Holds no offset of its own: the offset is node(w.canvas).scroll, the core field, so a wheel tick is one Dirty.PAINT mark and layout never runs. Scrollpane is therefore policy over a compositor that already knows how to shift an origin.
Focusable by default, so a pane with no focusable children is still keyboard-scrollable.
ONE child: wrap several in a Container, exactly as CSS makes you.
Fields
node::WidgetNode: Per-widget state.row::Containercanvas::Containerholder::Containerbar_y::ManyUI.ScrollMode.Tbar_x::ManyUI.ScrollMode.Twheel_step::Int64wheel_step_x::Int64
ManyUI._sb_metrics — Method
_sb_metrics(w::Scrollbar, track::Int64) -> NTuple{4, Int64}
(track, view, total, off) for w, read from the scrollable seam and nothing else.
It never measures anything and never touches the pane, which is why one Scrollbar serves a bare Container, a Scrollpane's canvas and a TextArea identically. off is clamped FOR DISPLAY only: the stored offset may legitimately be stale for one frame, and a thumb off the end of its track is worse than a thumb that led by one frame. Internal.
ManyUI._sb_offset — Method
_sb_offset(
start::Int64,
len::Int64,
track::Int64,
view::Int64,
total::Int64
) -> Int64
The scroll offset a thumb starting at start represents: the inverse of thumb_span's start mapping. Pure. Internal.
Exact at both ends by construction, because it inverts the same room/span ratio rather than deriving an independent one.
ManyUI._scroll_between — Method
_scroll_between(
vp::Widget,
w::Widget
) -> Union{Nothing, Offset}
The shift w has ALREADY been given by scrolling nodes strictly between it and vp, exclusive of vp itself.
ORIGIN when vp is w's parent, and nothing when vp is not an ancestor of w at all – which is how scroll_into_view! tells "already in the right place" from "not my descendant" without a second walk. Pure. Internal.
ManyUI._sp_acts — Method
_sp_acts(d::Dispatch) -> Bool
True while d is live and at or past its target.
CAPTURE is excluded deliberately: acting there would let an OUTER pane steal the wheel from the inner pane the pointer is actually over. Pure. Internal.
ManyUI._sp_box! — Method
_sp_box!(w::Widget, p::BoxPatch)
Give w the geometry p, so that it holds under BOTH layout paths.
inline_box and box are set together ON PURPOSE, and neither alone is enough. layout! reads box and never applies inline_box, so a test that skips the cascade would see BOX_DEFAULT; and apply_stylesheet! OVERWRITES box with apply(BOX_DEFAULT, ..., inline_box), so a box set at construction is gone the first time anything cascades. Writing box as exactly the value an empty cascade would compute makes the two paths agree by construction rather than by luck.
merge rather than assignment: the patch accumulates, so a later call cannot silently drop the geometry an earlier one established.
Internal: this is the pane's own machinery, never a user knob.
ManyUI._sp_is_content — Method
_sp_is_content(p::Scrollpane, d::Widget) -> Bool
True when d is a strict descendant of p's HOLDER, i.e. genuinely the user's content rather than the pane's own machinery. Pure. Internal.
row, canvas, the bars and the holder itself all answer false, which is what stops reveal! on a focused scrollbar rewinding the pane to the top.
ManyUI._sp_key_delta — Method
_sp_key_delta(
w::Scrollpane,
code::ManyUI.Key.T
) -> Union{Nothing, Offset}
The distance code scrolls w, or nothing when the key is not one of the pane's. Pure with respect to the tree. Internal.
HOME/END are a HUGE delta rather than a computed target, and letting scroll_to! clamp it IS the implementation: "go as far as you can" needs no knowledge of how far that is. typemax(Int) ÷ 2 so that scroll_of(w) + d cannot overflow.
ManyUI._sp_move! — Method
_sp_move!(w::Scrollpane, disp::Dispatch, d::Offset)
Move w's canvas by d and consume disp IFF the offset actually changed.
The whole of scroll chaining is this one rule: a pane at its limit does not consume, so the event bubbles to the next pane out and that pane takes over. No pane knows another exists. Internal.
ManyUI.content_extent — Method
content_extent(p::Scrollpane) -> Size
content_extent(viewport(p)): what p shows, not the shell around it.
ManyUI.content_extent — Method
content_extent(w::Widget) -> Size
The scrollable content extent of w, in cells, measured from its CONTENT-BOX ORIGIN.
Default: the bounding box of the laid-out children's MARGIN boxes, straight from the LayoutMap the engine already computed – one pass over the children, no re-measure, no relayout. That is why a wheel tick is free.
Reads layout_of(k), the UNSHIFTED absolute box, so the extent is independent of the current scroll offset and a scroll can never feed back into the extent.
A widget whose content is DATA rather than children overrides this; TextArea is the worked example. Pure.
ManyUI.max_scroll — Method
max_scroll(p::Scrollpane) -> Offset
max_scroll(viewport(p)): how far p can actually scroll.
ManyUI.max_scroll — Method
max_scroll(w::Widget) -> Offset
The largest in-range scroll offset: content_extent - content box, clamped at zero per axis, via clamp_scroll. Pure.
ManyUI.measure — Method
measure(w::Scrollbar, avail::Size) -> Size
Size(1, 0) vertical, Size(0, 1) horizontal: a bar is one cell thick and claims nothing on its long axis. Pure with respect to the tree.
ManyUI.mount! — Method
mount!(p::Scrollpane, c::Widget) -> Scrollpane
Mount c INTO the canvas, not onto the pane: children(pane) is the pane's own machinery and the user never addresses it. Throws ArgumentError when the canvas already holds a child.
ManyUI.on_event! — Method
on_event!(w::Scrollbar, d::Dispatch{MouseEvent})
Jump the viewport so the thumb centres on the pointer; drag continues it. LEFT button only.
local_offset(d) is measured from the bar's UNSHIFTED border box, which is correct here precisely because a scrollbar is never inside a scrolled subtree.
ManyUI.on_event! — Method
on_event!(w::Scrollpane, d::Dispatch{KeyEvent})
UP/DOWN/LEFT/RIGHT by wheel_step; PAGEUP/PAGEDOWN by one viewport LESS ONE ROW of overlap, so the reader keeps a landmark; HOME/END to the extremes of the vertical axis. Unmodified keys only, BUBBLE/AT_TARGET only, and consuming only on real movement.
A focused TextInput consumes LEFT/RIGHT AT TARGET, so the pane never sees them. That precedence costs zero special-casing.
ManyUI.on_event! — Method
on_event!(w::Scrollpane, d::Dispatch{MouseEvent})
Scroll on the wheel; shift swaps the axis.
Wheel events route to whatever is under the pointer and reach the pane on the way UP, so the INNERMOST pane gets first refusal. CONSUMES ONLY WHEN IT ACTUALLY MOVED: a pane at its limit lets the notch bubble to the next pane out – scroll chaining, out of the phase rule alone.
CAPTURE is excluded deliberately: acting there would let an outer pane steal the wheel from the inner pane the pointer is over.
ManyUI.reveal_child! — Method
reveal_child!(w::Scrollpane, d::Widget)
Bring descendant d into the pane's window. Overrides the widget.jl hook; a no-op for the pane's own machinery (row, canvas, the bars).
ManyUI.scroll_by! — Method
scroll_by!(w::Widget, d::Offset) -> Offset
scroll_to!(w, scroll_of(w) + d). Returns the stored offset.
ManyUI.scroll_into_view! — Method
scroll_into_view!(p::Scrollpane, w::Widget) -> Offset
scroll_into_view!(viewport(p), w): scroll p the minimum needed to reveal descendant w.
ManyUI.scroll_into_view! — Method
scroll_into_view!(vp::Widget, w::Widget) -> Offset
Scroll vp the MINIMUM needed to bring descendant w's margin box inside vp's content box, on both axes, via scroll_into_view. Returns the stored offset.
Reads layout_of, i.e. UNSHIFTED geometry, so it is correct whatever the current offset is, and idempotent. A no-op when w is not a descendant of vp.
Panes BETWEEN vp and w have already moved w and their shift is subtracted here, which is why reveal! must run NEAREST FIRST: an inner pane has to finish moving before vp can measure where w ended up.
ManyUI.scroll_of — Method
scroll_of(p::Scrollpane) -> Offset
scroll_of(viewport(p)): where p is currently scrolled to.
This is also what makes the generic scroll_by!(p, d) – scroll_to!(p, scroll_of(p) + d) – accumulate against the canvas's real offset instead of the pane's permanent zero.
ManyUI.scroll_to! — Method
scroll_to!(p::Scrollpane, o::Offset) -> Offset
scroll_to!(viewport(p), o). Returns the offset actually stored.
ManyUI.scroll_to! — Method
scroll_to!(w::Widget, o::Offset) -> Offset
Scroll w to o, CLAMPED per axis to 0:max_scroll(w). Returns the offset ACTUALLY STORED.
Returning the stored offset is what makes scroll chaining fall out for free: a caller detects "this pane is at its limit" with scroll_to!(w, o) === before and simply does not consume the event.
ManyUI.thumb_span — Method
thumb_span(
track::Int64,
view::Int64,
total::Int64,
off::Int64
) -> Tuple{Int64, Int64}
Thumb geometry on a track-cell track, for a view-cell window over total cells of content scrolled to off.
Returns (start, len), 1-based inclusive within the track, or (0, 0) when there is no track or nothing to scroll.
NORMATIVE:
len = clamp(round(Int, track * view / total), 1, track). NEVER zero: an invisible thumb is a broken scrollbar, and a 1-cell thumb on a 40-cell track is the honest rendering of a 40x document.startmaps0:(total - view)onto1:(track - len + 1), sooff == 0pins the thumb to the FIRST cell andoff == total - viewpins it to the LAST (start + len - 1 == track) EXACTLY. The two ends are the only positions a user can verify at a glance, so they are the two the arithmetic is written around.
Pure: four Ints, no widget, no layout, no buffer – the whole of this widget's behaviour is one table test.
ManyUI.viewport — Method
viewport(p::Scrollpane) -> Container
The scrolling node of p – what scroll_to!, max_scroll and content_extent take. p.canvas.