Scrolling widgets

ManyUI.ScrollbarType
mutable struct Scrollbar{V<:Widget} <: Widget

The 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.T

  • mode::ManyUI.ScrollMode.T

  • viewport::Widget

source
ManyUI.ScrollpaneType
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 = 0f0 on the HOLDER is what creates the overflow. The default shrink is 1f0, 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.START on 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).

source
ManyUI.ScrollpaneType
mutable struct Scrollpane <: Widget

A 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::Container

  • canvas::Container

  • holder::Container

  • bar_y::ManyUI.ScrollMode.T

  • bar_x::ManyUI.ScrollMode.T

  • wheel_step::Int64

  • wheel_step_x::Int64

source
ManyUI._sb_metricsMethod
_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.

source
ManyUI._sb_offsetMethod
_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.

source
ManyUI._scroll_betweenMethod
_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.

source
ManyUI._sp_actsMethod
_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.

source
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.

source
ManyUI._sp_is_contentMethod
_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.

source
ManyUI._sp_key_deltaMethod
_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.

source
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.

source
ManyUI.content_extentMethod
content_extent(p::Scrollpane) -> Size

content_extent(viewport(p)): what p shows, not the shell around it.

source
ManyUI.content_extentMethod
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.

source
ManyUI.max_scrollMethod
max_scroll(p::Scrollpane) -> Offset

max_scroll(viewport(p)): how far p can actually scroll.

source
ManyUI.max_scrollMethod
max_scroll(w::Widget) -> Offset

The largest in-range scroll offset: content_extent - content box, clamped at zero per axis, via clamp_scroll. Pure.

source
ManyUI.measureMethod
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.

source
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.

source
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.

source
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.

source
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.

source
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).

source
ManyUI.scroll_by!Method
scroll_by!(w::Widget, d::Offset) -> Offset

scroll_to!(w, scroll_of(w) + d). Returns the stored offset.

source
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.

source
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.

source
ManyUI.scroll_ofMethod
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.

source
ManyUI.scroll_to!Method
scroll_to!(p::Scrollpane, o::Offset) -> Offset

scroll_to!(viewport(p), o). Returns the offset actually stored.

source
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.

source
ManyUI.thumb_spanMethod
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.
  • start maps 0:(total - view) onto 1:(track - len + 1), so off == 0 pins the thumb to the FIRST cell and off == total - view pins 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.

source
ManyUI.viewportMethod
viewport(p::Scrollpane) -> Container

The scrolling node of p – what scroll_to!, max_scroll and content_extent take. p.canvas.

source