Containers
Scroll view
Containers that scroll their children.
ui.Scroll and ui.ScrollHorizontal scroll their children with the wheel,
the touchpad, their scroll bar and the keyboard: the arrow keys, Page Up and
Page Down, Space, Home and End scroll the container around the focus, or
under the pointer, unless the focused element takes those keys, as a text
input does. ui.ScrollBoth scrolls both ways, as a canvas or a wide table
does. A notch of a mouse wheel glides over a short ease out, as browsers
scroll it, and notches in a row add up, until the app scrolls the
container (a ScrollState, a ListState's ScrollTo, ScrollIntoView
and ScrollToEnd, or ScrollIntoView of an element in it), which takes
over; a touchpad follows the fingers, as a finger dragged on a
touchscreen does on Linux, and while the desktop asks for less
motion, a notch scrolls at once.
ui.Scroll(c).Grow(1).Padding(16).Gap(12).Children(func() {
for _, p := range app.posts {
post(c, p)
}
})Give them a size, or grow them in their parent. For the rows of a collection, use a list, which builds only those in view.
Laid out right to left, a container scrolling sideways
shows the start of its content, at the right, first, and ScrollState.X
counts from there: 0 is the start and MaxX the end, as Flutter's offsets
are.
Keeping the offset
A scroll container keeps its offset as it keeps its other state, by its
place in the view or its Key. To read the offset, set it or keep it in
the app's state, give the container a ui.ScrollState with TrackScroll:
the container shows its content from the state's X and Y, writes them
as the user scrolls, and sets MaxX and MaxY to how far they go. Y = 0
scrolls to the top and math.MaxFloat32 to the end, and a ScrollState
for each page a container shows keeps each page's place. A log that follows
its end, unless the user scrolled up from it:
if app.log.Y >= app.log.MaxY {
app.log.Y = math.MaxFloat32
}
ui.Scroll(c).TrackScroll(&app.log).Grow(1).Children(app.lines)ScrollTo(x, y, animation) scrolls there smoothly, as a spring or a curve
(see Motion), turning toward a new place if called again on
the way; the user scrolling stops it, and Scrolling() reports whether it
is on its way. Where the desktop asks for less motion, it scrolls at once:
if ui.Button(c, "Back to top").Clicked() {
app.feed.ScrollTo(0, 0, ui.Smooth)
}Sticky elements
Sticky keeps an element in the flow, where it takes its room, and holds
it inside the scroll container around it while its parent is in view, as
CSS's position: sticky: Top, Right, Bottom and Left (or Start
and End, and their percentages of the container's size) say how far in
from the container's edges, inside its border, it stays. It goes no
further than its parent's content box, so as its parent scrolls away it
takes the element along, and the next parent's comes up in its place. A
day's heading stays at the top of a timeline until the next day pushes it
out:
ui.Scroll(c).Fill().Children(func() {
for _, day := range app.days {
ui.Column(c).Children(func() {
ui.Text(c, day.Title).Bold().Padding(8, 16).Background(t.Surface).Sticky().Top(0)
for _, ev := range day.Events {
eventRow(c, ev)
}
})
}
})Each axis follows the nearest container scrolling that way (or a header
following another sideways with ScrollWith), or else the window. So the
check box choosing all the rows of a table scrolling sideways stays at its
start, while the header row it is in sticks to the top of the page that
scrolls down, which CSS would not do, as a container scrolling one way
there scrolls both:
ui.Scroll(c).Fill().Children(func() {
ui.ScrollHorizontal(c).Shrink(0).Children(func() {
ui.Column(c).Width(app.tableWidth()).Children(func() {
ui.Row(c).Background(t.Surface).Sticky().Top(0).Children(func() {
ui.Checkbox(c, &app.all, "").Background(t.Surface).Sticky().Start(0)
headerCells(c)
})
rows(c)
})
})
})Where both insets of an axis are set and the container is too small for
both, Top wins, and Left, or Right right to left.
A sticky element paints above its siblings, as absolute ones do, so what
scrolls under it needs no clip, but give it a background; the pointer,
assistive technology, what is attached to it (AttachTo) and
ScrollIntoView find it where it shows. The focus and assistive technology
still take it in its place among its siblings. It does nothing to an
Absolute element or to one inside a text. A list builds only
the rows in view, so give its section headers with ListState.Header,
which pins them, rather than making a row sticky.
Scrolling into view
ScrollIntoView scrolls the containers around an element as little as
shows it, once the frame is laid out, so that it works in the frame that
adds the element: a new message, or the item the keys chose. Call it in that
frame only, or the user could not scroll the element away. Rows a List
has not built scroll into view with its ListState.
if app.added {
ui.Text(c, msg.Text).ScrollIntoView()
}ScrollIntoViewOptions, as the web's scrollIntoView options, line the
element up elsewhere, or glide there. Block lines it up down the
containers and Inline across them: ui.ScrollNearest, the default,
scrolls as little as shows it, and ui.ScrollStart, ui.ScrollCenter and
ui.ScrollEnd line its start, center or end up with each container's,
inside its border; right to left, the inline start is
at the right. Animation, when set, glides every container around it
there as the animation says, as the web's behavior: "smooth", rather
than at once: the user scrolling, or the app, stops it, and where the
desktop asks for less motion it scrolls at once. The current match of a
search, centered:
if app.matchMoved {
hit.ScrollIntoView(ui.ScrollIntoViewOptions{Block: ui.ScrollCenter, Animation: &ui.Smooth})
}The zero options are those of ScrollIntoView() without them.
The keyboard focus scrolls into view as it moves. An element with a
TextInputClient of the app's (text input clients)
scrolls its caret into view, where BoundsForRange places it, rather than
its whole box: a code editor as high as its lines shows the line being
edited as it takes the focus, not its first.
Snapping
ScrollSnap makes a container come to rest at the places the elements in
it mark with SnapAlign, as CSS's scroll-snap-type and
scroll-snap-align do, on the axes it scrolls: a strip of cards that
always rests showing a card from its start, a carousel's pages, or the
sections of a long page.
ui.ScrollHorizontal(c).ScrollSnap(ui.SnapMandatory).Padding(16).Gap(12).Children(func() {
for _, card := range app.cards {
cardView(c, card).Width(240).Shrink(0).SnapAlign(ui.Start)
}
})SnapAlign(ui.Start), ui.Center or ui.End lines the element's start,
center or end up with the container's content box, inside its padding, so
the first card of the strip rests where it starts; right to left, the
start is at the right. SnapAlignX and SnapAlignY mark one axis only, as
a ScrollBoth that snaps its columns and scrolls freely down them. The
elements are any inside the container, not only its children, short of
another scroll container, whose own they are; a List's rows mark theirs
as they are built.
The container snaps as the user's scrolling settles, as browsers snap:
- a touchpad, the momentum of a fling after it, a finger on a touchscreen, and a scroll bar's thumb once it is let go glide on to the nearest place;
- a notch of a mouse wheel, the arrow keys, Page Up and Page Down, Home and End and a press on a scroll bar's track go to the next place that way, past where it was: the nearest to where they would have scrolled. At the last place, the wheel scrolls the container around it.
ui.SnapMandatory always rests at a place: it lines one up as it first
shows, and the wheel and the keys go from place to place.
ui.SnapProximity rests at one only where the scrolling stops within a
third of the view of it, and wherever it stops in between. An element
larger than the view, as a tall section, rests anywhere it fills the
view, so the wheel scrolls through it before going on to the next.
Resting at an element, the container stays at it as the layout moves it,
as when the window resizes or rows above change height, and an element
that comes into view (ScrollIntoView, or the focus moving) lines up
the place of the element marked around it. Where the app scrolls the
container, with its ScrollState, ScrollTo or a ListState, it stays
where the app puts it. While the desktop asks for less motion, it goes to
rest at once.
Scroll bars
A scroll container whose content overflows it shows the thumbs of its
scroll bars over its content while the pointer is over it, which the user
drags; they take no room. Right to left, the vertical bar is on the left.
The theme's ScrollbarWidth sets their width and its Scrollbar their
color.
ScrollbarStyle styles them further: the color of the track behind the
thumb, the thumb's hover and drag colors, the widths of the bar and its
thumb, the radius of its corners, and the shortest thumb. It overrides
Scrollbar and ScrollbarWidth where it sets a field; its own zero fields
follow the theme, so a theme that sets neither draws as it always has.
The defaults are MyGo's own: a 6 DIP rounded thumb in a 12 DIP bar, 2 DIPs
clear of its ends. Set a style in a theme for every scroll bar of the app,
on a container for its own, or on an editor's State.Scrollbar:
t := *c.Theme()
// As VS Code's editor: a 14 DIP square thumb in a 14 DIP bar, flush
// with its ends.
t.ScrollbarStyle = ui.ScrollbarStyle{
TrackWidth: 14, ThumbWidth: 14, Square: true, Flush: true, MinLength: 20,
Thumb: ui.RGBA(121, 121, 121, 0.4),
Hover: ui.RGBA(100, 100, 100, 0.7),
Active: ui.RGBA(191, 191, 191, 0.4),
}
c.SetTheme(&t)TrackWidth is the width of the bar's track and hit area, in DIPs, 0 for
ThumbWidth plus 6; ThumbWidth is the width of the thumb, 0 for the
theme's ScrollbarWidth; Radius rounds its corners, 0 for half its
width, and Square leaves them square, as VS Code's scroll bar does;
Flush leaves no gap between the thumb and the ends of its track, as VS
Code's, and otherwise 2 DIPs clear them; MinLength is the shortest it
gets, 0 for 24 DIPs. Track fills the bar behind the thumb, and zero
paints none. Thumb colors the thumb at rest, Hover while the pointer
is over the bar, and Active while it is dragged; zero Thumb takes the
theme's Scrollbar, and zero Hover and Active take Thumb. A kept
bar (KeepScrollbars) shows its Hover color, as it looks on hover.
A container's own style wins over the theme's:
ui.Scroll(c).ScrollbarStyle(ui.ScrollbarStyle{TrackWidth: 14, ThumbWidth: 14, Square: true, Flush: true}).Children(func() { /* ... */ })ScrollbarInsets moves the bars in from the container's edges, CSS style
as Padding: the vertical bar runs from the top inset to the bottom one,
the right inset in from the right, and the horizontal bar from the left
inset to the right one, the bottom inset up. So the bars keep clear of
what floats over the content, as a toolbar it scrolls under, as AppKit's
and UIKit's do:
ui.Scroll(c).Fill().Padding(64, 16, 16).ScrollbarInsets(64, 0, 0).Children(func() { /* ... */ })
// The toolbar floats over the top 64 DIPs.
ui.Row(c).Absolute().Top(0).Left(0).Right(0).Height(64).Children(func() { /* ... */ })Scrollbars(false) takes the bars away, and the presses on them, from a
container that draws its own chrome or shows where it is by itself, as a
carousel or a map: the wheel, TrackScroll, the keyboard and assistive
technology still scroll it.
ScrollbarAxes(horizontal, vertical) controls the two indicators and
their pointer hit targets independently. For a view with its own vertical
navigation, keep the horizontal bar and hide the vertical one:
ui.ScrollBoth(c).Fill().ScrollbarAxes(true, false).Children(func() { /* ... */ })Hidden axes still scroll by wheel, touchpad, TrackScroll, keyboard focus
and assistive technology. Hiding the axis being dragged cancels that drag
without clicking the content underneath. The remaining bar keeps its track
and leaves the shared corner clear.
Scrollbars(false) hides both regardless of call order; Scrollbars(true)
preserves the axis settings. Repeating ScrollbarAxes uses its last value,
and omitting it in the next build enables both axes again.
KeepScrollbars(true) keeps them showing while the content overflows,
with the pointer over the container or not, for a view whose place in its
content the user should see at a glance, as a long log or a diff filling
the window. They look as they do on hover, on the track if the style has
one, so nothing changes as the pointer comes over them, and take presses
as they do then; Scrollbars(false) and ScrollbarAxes still hide them.
All three apply to Scroll, ScrollHorizontal, ScrollBoth, List and
Table, are left at their defaults by leaving them out, and are not inherited:
ui.List(c, &app.rows, len(app.lines), app.line).Fill().KeepScrollbars(true)An element that scrolls itself rather than through a container, as the
code editor and the terminal do, draws its bars with ui.Scrollbar, so
that they look as the containers' do and follow the theme: Painter.Scrollbar
paints the track and thumb, and Thumb and OffsetAt map a drag of it to
an offset. A view that draws marks on its bar paints ScrollbarTrack,
then the marks, then ScrollbarThumb. Show it as a container does, while
the pointer is over the element or drags the thumb, or always where the
app keeps the bars, as the code editor's KeepScrollbars does:
bar := ui.Scrollbar{Box: r, View: r.H, Content: contentH, Offset: scrollY}
if hovered || dragging {
p.Scrollbar(bar, ui.ScrollbarState{Hovered: hovered, Dragging: dragging})
}An element inside that zooms or pans with the wheel, the touchpad and pinches, as a chart, takes them from the container: see Scrolling and pinching.
Accessibility
Assistive technology sees a scroll area, which it scrolls to show what it reads.