Documentation
¶
Overview ¶
Package virtual adds live-data list virtualization on top of the go-widgets/toolkit widget set. Where the core ListBox / Table / TreeView window a static []string, the widgets here bind to a live mvvm.ObservableList and realise ONLY the rows (or cells) that intersect the viewport, so a model with a million items costs the same per frame as one with a handful.
Two widgets are provided, both toolkit.Widget (Draw / OnEvent / Bounds / SetBounds via the embedded toolkit.Base):
- VirtualList — a scrollable vertical list with a per-row draw callback and optional VARIABLE row heights. The scroll-offset → first-visible-row lookup is O(1) when every row is the same height and O(log n) via a Fenwick (binary-indexed) prefix-sum tree when heights vary.
- VirtualGrid — the gengrid analogue: it reflows N uniform cells into as many columns as the width allows and realises only the visible cells.
Both subscribe to their model's ListEvent stream and keep the scroll anchor stable across mutations: an insert (or remove) ABOVE the viewport shifts the offset so the rows on screen do not jump, while a change below the top item leaves the offset untouched. Rendering reuses painter.Clipper (when the back-end supports it) to clip the partially-visible trailing item to the exact viewport edge.
Index ¶
- Constants
- type CardList
- type CardState
- type VirtualGrid
- func (g *VirtualGrid[T]) Close()
- func (g *VirtualGrid[T]) Draw(p painter.Painter, th *toolkit.Theme)
- func (g *VirtualGrid[T]) OnEvent(ev toolkit.Event)
- func (g *VirtualGrid[T]) ScrollBy(delta int)
- func (g *VirtualGrid[T]) ScrollByRows(delta int)
- func (g *VirtualGrid[T]) ScrollTo(offset int)
- func (g *VirtualGrid[T]) SetBounds(r toolkit.Rect)
- func (g *VirtualGrid[T]) VisibleRange() (first, count int)
- type VirtualList
- func (v *VirtualList[T]) Close()
- func (v *VirtualList[T]) Draw(p painter.Painter, th *toolkit.Theme)
- func (v *VirtualList[T]) OnEvent(ev toolkit.Event)
- func (v *VirtualList[T]) ScrollBy(delta int)
- func (v *VirtualList[T]) ScrollByRows(delta int)
- func (v *VirtualList[T]) ScrollTo(offset int)
- func (v *VirtualList[T]) VisibleRange() (first, count int)
Constants ¶
const ( // DefaultPullRows is the pull distance, in rows scrolled toward an edge // while the viewport is already within one screen of it, that a CardList // requires before it fires OnReachTop / OnReachBottom. A single wheel notch // (one row) is a micro-nudge and must not trigger a fetch; a deliberate pull // of several rows does. Overridable per instance via PullRows. DefaultPullRows = 3 )
Tunables shared by every CardList. They are package constants rather than per-instance fields because a card feed reads best when its chrome (the selection ring weight, the read-item veil, the pull-to-fetch strip) is uniform across the whole application; the one knob a caller is expected to touch — how hard a pull has to be before it triggers a fetch — is the PullRows field.
const DefaultRowHeight = 20
DefaultRowHeight is the row height a VirtualList uses when its RowHeight callback is nil — a comfortable uniform default that mirrors the core ListBox's proportions.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CardList ¶ added in v0.156.0
type CardList[T any] struct { *VirtualList[T] // CardRender draws one card into rectangle r (its exact on-screen span) for // item i, given its display state. It is invoked only for cards in the // viewport. CardList paints the selection ring and the dim veil ON TOP of // whatever CardRender draws, so the caller only draws the card's own content. CardRender func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T, state CardState) // Selected is the index of the selected card, or -1 for none. It doubles as // the keyboard cursor. Selected int // OnSelect fires with the new index whenever selection changes (arrow / page // key or click). nil is safe. OnSelect func(i int) // OnActivate fires with Selected when the selected card is activated (Enter, // the card-feed analogue of opening it). nil is safe. OnActivate func(i int) // Dimmed reports whether card i is "already read" / muted. CardList never // stores read state itself — the application owns it — it only asks. nil // means no card is dimmed. Dimmed func(i int) bool // OnReachTop fires when the viewport is deliberately pulled to within one // screen of the top (load older items when newest is at the bottom). OnReachTop func() // OnReachBottom fires when the viewport is deliberately pulled to within one // screen of the bottom. OnReachBottom func() // PullRows overrides DefaultPullRows: how many rows of edge-ward pull, while // already within one screen of that edge, are needed before OnReach* fires. // Zero uses DefaultPullRows. PullRows int // FetchingTop / FetchingBottom, when set by the app, draw a spinning // pull-to-fetch strip over the top / bottom viewport edge and make Animating // report true so a host keeps ticking. The app sets one true when it starts // a fetch (typically from OnReach*) and clears it when the fetch lands. FetchingTop bool FetchingBottom bool // TopLabel / BottomLabel are optional captions drawn beside the strip // spinner (e.g. "Loading older…"). Empty draws just the spinner. TopLabel string BottomLabel string // contains filtered or unexported fields }
CardList binds a scrollable feed of cards to a live mvvm.ObservableList by composing a virtual.VirtualList[T]: the VirtualList owns the model subscription, the O(1)/O(log n) offset↔row index, the stable scroll anchor across mutations, and the recycled per-row drawing — CardList adds only what a card feed needs on top of a plain virtual list:
- a card-shaped Render wrapper that calls the caller's CardRender and then paints the selection ring and the read-item veil over it;
- keyboard + click selection (Selected / OnSelect / OnActivate) with scroll-into-view, the card-feed analogue of ListBox's cursor;
- infinite scroll: OnReachTop / OnReachBottom fire when the viewport is pulled within one screen of an edge, gated by a per-edge accumulator so a micro-nudge does not spam the loader;
- pull-to-fetch strips: while FetchingTop / FetchingBottom a spinning strip is drawn over the corresponding edge, and CardList is itself an toolkit.Animator so a host present loop driving toolkit.TreeAnimating / toolkit.TickTree spins those strips with no per-app bookkeeping;
- ScrollToBottom for the "newest at the bottom, open at the bottom" feed.
CardList embeds *VirtualList[T], so ScrollTo / ScrollBy / ScrollByRows / VisibleRange / Close / Bounds / SetBounds and the Model / RowHeight / ScrollOffset fields are all reachable directly; it overrides only Draw and OnEvent.
func NewCardList ¶ added in v0.156.0
func NewCardList[T any]( model *mvvm.ObservableList[T], rowHeight func(i int) int, cardRender func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T, state CardState), ) *CardList[T]
NewCardList builds a CardList over model with the given per-row height and card-draw callbacks, wiring the underlying VirtualList (and its model subscription) immediately. Selection starts empty (Selected == -1).
func (*CardList[T]) Animating ¶ added in v0.156.0
Animating reports whether a pull-to-fetch strip is spinning — true exactly when a fetch is in flight — making CardList an toolkit.Animator so a host stops repainting once both strips are idle.
func (*CardList[T]) Draw ¶ added in v0.156.0
Draw paints the cards (via the embedded VirtualList) and then, over the top, any active pull-to-fetch strip. The strip spinners' Active state is synced to the Fetching flags here so an app toggling a flag needs no other wiring.
func (*CardList[T]) OnEvent ¶ added in v0.156.0
OnEvent handles selection + activation keys, wheel scrolling, and clicks; it replaces VirtualList.OnEvent (which only scrolls). Scroll and keyboard navigation both feed the infinite-scroll accumulator via noteScroll.
func (*CardList[T]) ScrollToBottom ¶ added in v0.156.0
func (c *CardList[T]) ScrollToBottom()
ScrollToBottom scrolls to the very end of the content — the "newest at the bottom, open at the bottom" gesture. The offset clamps to the maximum, so it is safe on a feed shorter than the viewport.
func (*CardList[T]) Tick ¶ added in v0.156.0
Tick advances whichever strip spinners are active by dt seconds. The host calls it once per frame (directly or through toolkit.TickTree); an inactive strip is left untouched, so a stopped feed costs nothing.
type CardState ¶ added in v0.156.0
type CardState struct {
// Selected is true for the one card at CardList.Selected.
Selected bool
// Dimmed is true when CardList.Dimmed reports this card as read/muted.
Dimmed bool
}
CardState carries the per-card display flags a CardList computes for each visible card and hands to the caller's CardRender: whether the card is the selected one (so the caller can lift it, or simply let CardList draw the ring on top) and whether it is dimmed (an "already read" item — the caller may mute its own content, and CardList additionally veils it).
type VirtualGrid ¶
type VirtualGrid[T any] struct { toolkit.Base // Model is the live backing collection. Model *mvvm.ObservableList[T] // CellSize is every cell's fixed footprint in painter units. CellSize toolkit.Size // Render draws one cell into rectangle r (its exact on-screen span), // invoked only for the cells currently in the viewport. Render func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T) // ScrollOffset is the pixel offset of the viewport top from the top of the // content. Reads clamp it; prefer ScrollTo / ScrollBy. ScrollOffset int // contains filtered or unexported fields }
VirtualGrid reflows N uniform-sized cells (CellSize) into as many columns as the widget width allows and realises only the cells that intersect the viewport — the recycled 2-D card / thumbnail grid. It binds to a live mvvm.ObservableList and keeps its scroll anchor stable across mutations above the viewport, exactly like VirtualList.
func NewVirtualGrid ¶
func NewVirtualGrid[T any]( model *mvvm.ObservableList[T], cell toolkit.Size, render func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T), ) *VirtualGrid[T]
NewVirtualGrid builds a VirtualGrid over model with the given cell size and draw callback, wiring the model subscription immediately.
func (*VirtualGrid[T]) Close ¶
func (g *VirtualGrid[T]) Close()
Close unsubscribes from the model. Safe to call more than once.
func (*VirtualGrid[T]) Draw ¶
func (g *VirtualGrid[T]) Draw(p painter.Painter, th *toolkit.Theme)
Draw paints only the cells intersecting the viewport, each at its reflowed column/row position minus the scroll offset. When the content overflows it clips (if the painter supports Clipper) so partially-visible edge rows are trimmed to the viewport.
func (*VirtualGrid[T]) OnEvent ¶
func (g *VirtualGrid[T]) OnEvent(ev toolkit.Event)
OnEvent handles a wheel EventScroll by scrolling whole cell-rows; every other event kind is ignored.
func (*VirtualGrid[T]) ScrollBy ¶
func (g *VirtualGrid[T]) ScrollBy(delta int)
ScrollBy shifts the pixel scroll offset by delta, clamped.
func (*VirtualGrid[T]) ScrollByRows ¶
func (g *VirtualGrid[T]) ScrollByRows(delta int)
ScrollByRows shifts the viewport by whole cell-rows (negative scrolls up).
func (*VirtualGrid[T]) ScrollTo ¶
func (g *VirtualGrid[T]) ScrollTo(offset int)
ScrollTo sets the pixel scroll offset, clamped to the valid range, and resyncs the anchor.
func (*VirtualGrid[T]) SetBounds ¶
func (g *VirtualGrid[T]) SetBounds(r toolkit.Rect)
SetBounds positions the grid and resyncs the anchor, since a width change reflows the columns (and so changes which item sits at the top-left).
func (*VirtualGrid[T]) VisibleRange ¶
func (g *VirtualGrid[T]) VisibleRange() (first, count int)
VisibleRange returns the index of the first visible cell and the number of cells intersecting the viewport (a whole number of rows' worth, clamped to the model length). Allocation-free.
type VirtualList ¶
type VirtualList[T any] struct { toolkit.Base // Model is the live backing collection. Setting it (via the field or // NewVirtualList) rewires the subscription on the next operation. Model *mvvm.ObservableList[T] // RowHeight returns row i's pixel height. A constant function yields the // uniform O(1) fast path; a varying one drives the Fenwick index. nil means // a uniform DefaultRowHeight. RowHeight func(i int) int // Render draws one row into rectangle r (its exact on-screen span). It is // invoked only for rows currently in the viewport. Render func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T) // ScrollOffset is the pixel offset of the viewport top from the top of the // content. Reads clamp it to [0, maxOffset]; prefer ScrollTo / ScrollBy. ScrollOffset int // contains filtered or unexported fields }
VirtualList binds a scrollable vertical list to a live mvvm.ObservableList and a per-row draw callback, realising only the rows that intersect the viewport. Row heights may be uniform (a constant RowHeight, or nil for DefaultRowHeight) or variable (an arbitrary RowHeight function); the scroll-offset → first-visible-row lookup is O(1) in the uniform case and O(log n) via a Fenwick prefix-sum tree in the variable case. It subscribes to the model and holds the scroll anchor stable across inserts / removes above the viewport.
func NewVirtualList ¶
func NewVirtualList[T any]( model *mvvm.ObservableList[T], rowHeight func(i int) int, render func(p painter.Painter, th *toolkit.Theme, r toolkit.Rect, i int, item T), ) *VirtualList[T]
NewVirtualList builds a VirtualList over model with the given per-row height and draw callbacks, wiring the model subscription immediately.
func (*VirtualList[T]) Close ¶
func (v *VirtualList[T]) Close()
Close unsubscribes from the model. Safe to call more than once.
func (*VirtualList[T]) Draw ¶
func (v *VirtualList[T]) Draw(p painter.Painter, th *toolkit.Theme)
Draw paints only the rows intersecting the viewport, positioning row i at its exact content Y minus the scroll offset. When the content overflows the bounds it pushes a clip rect (if the painter supports Clipper) so the partially-visible trailing (and leading) row is clipped to the viewport edge.
func (*VirtualList[T]) OnEvent ¶
func (v *VirtualList[T]) OnEvent(ev toolkit.Event)
OnEvent handles a wheel EventScroll by scrolling whole rows; every other event kind is ignored (the row-content widgets the Render callback draws handle their own input).
func (*VirtualList[T]) ScrollBy ¶
func (v *VirtualList[T]) ScrollBy(delta int)
ScrollBy shifts the pixel scroll offset by delta, clamped.
func (*VirtualList[T]) ScrollByRows ¶
func (v *VirtualList[T]) ScrollByRows(delta int)
ScrollByRows shifts the viewport by whole rows (negative scrolls up), snapping the offset to the resulting row's top so wheel scrolling advances a row at a time regardless of variable heights.
func (*VirtualList[T]) ScrollTo ¶
func (v *VirtualList[T]) ScrollTo(offset int)
ScrollTo sets the pixel scroll offset, clamped to the valid range.
func (*VirtualList[T]) VisibleRange ¶
func (v *VirtualList[T]) VisibleRange() (first, count int)
VisibleRange returns the index of the first visible row and the number of rows that intersect the viewport (including the partially-visible top and bottom rows). It is O(1) for uniform heights and O(log n) otherwise, and allocation-free, so it is safe to call every scroll tick.