GridView

Virtualized 2D tile grid bound to a ListModel<T> / ListDataSource.
Public types
| Kind | Name |
|---|---|
struct | TileContext — Context passed to the tile delegate for each realized tile |
struct | GridView — A virtualized 2D tile grid backed by a ListModel<T> |
struct | GridViewHandle — A handle on a GridView's scroll position, for a caller outside the widget: a toolbar, a "show current" command, a search result, an… |
enum | GridTabTraversal — How Tab moves out of (or within) the grid |
trait | SectionProvider — Partitions a flat model into display sections |
struct | GroupingSections — A SectionProvider built by partitioning consecutive equal-key runs of an (already ordered) model |
Public functions
GridView
GridViewHandle
| Returns | Function |
|---|---|
| Methods | |
Option<usize> | cursor() |
set_cursor(index: Option<usize>) | |
Signal<Option<usize>> | cursor_signal() |
scroll_to_index(index: usize, anchor: ScrollAnchor) | |
ensure_index_visible(index: usize, anchor: ScrollAnchor) | |
reveal_band(index: usize) |
SectionProvider
| Returns | Function |
|---|---|
| Methods | |
usize | section_count() |
usize | items_in_section(section: usize) |
String | section_title(section: usize) |
Vec<usize> { /* default implementation */ } | section_counts() |
Detailed description
GridView is the photo-gallery / icon-view / file-manager-grid /
collection-view widget — the 2D sibling of ListView
and TableView. It realizes only the
tiles currently visible (plus a buffer), reflows on resize, supports
single / multi selection with 2D keyboard navigation, and is fully
accessible (Role::Grid → Role::GridCell).
The layout is pluggable via GridLayoutStrategy: the stock
UniformGrid gives fixed tile size / fixed column count /
adaptive min-width grids, VariableRowGrid sizes each row to its
tallest tile, and VirtualizedMasonry flows items into the
currently-shortest column. Marquee selection, drag-reorder, sections
and sticky headers are layered over whichever one is in force.
GridView::new(model, |tc| {
Box::new(Card::new().child(TextWidget::new(lit!(&tc.item.name))))
})
.sizing(GridSizing::Adaptive { min_width: 120.0, max_width: None, height: 140.0 })
.spacing(8.0)
.selection(selection_model)
Column bounds and alignment
GridView::min_columns and GridView::max_columns bound the column
count that GridSizing::Fixed and GridSizing::Adaptive derive from the
width (GridSizing::FixedColumnCount ignores both). A floor the width
cannot afford narrows the tiles to share it, never overflowing
horizontally; the tile height stays the sizing's. GridView::column_alignment
places a block of columns narrower than the content at the leading edge (the
default), the centre or the trailing edge, swapping sides in a right-to-left
layout. Section headers and the detail band keep spanning the content width;
tiles, the rubber band, the insertion bar and the focus ring follow the
offset. An album grid of fixed 182 dp cards, two to ten columns, centred:
GridView::new(albums, |tc| album_card(tc))
.sizing(GridSizing::Fixed { width: 182.0, height: 230.0 })
.min_columns(2)
.max_columns(10)
.column_alignment(HAlignment::Center)
Selection by position or by key
GridView::selection takes a SelectionModel, which holds positions.
A grid that is sorted or filtered under its selection, or fed by a lazy
source, is built with GridView::from_source_keyed instead: a
KeyedSelectionModel holds the source's keys, so the selection stays on
the same items when they move. Both reach the grid through the facade the
other data views use (data_views::RowSelection), which turns each click,
key and rubber band into an index or a key.
The detail band
GridView::detail_row gives a tile a full-width band that opens directly
under the row holding it, the "album expansion" of a music library. The
band is not a tile and not a row of the grid: a layout strategy wrapped
around the grid's own (detail::DetailRowStrategy) moves everything below
the open row down by the band, so virtualization, the scroll range, the
focus ring and the rubber band account for it without knowing about it, and
tiles keep their row and column numbers. The band widget is a child of this
root rather than of the body pane, so it survives the pane's rebuilds on
scroll and resize, and a lazy source loading a window. A waterfall has no
rows and ignores it. See the module docs of grid_view/detail.rs.
Focus inside the band is revealed like focus anywhere: this root answers
the framework's WidgetEvent::ScrollIntoView, which a scroll container is
sent when a descendant it clips takes focus.
Scrolling from outside
GridView::handle returns a GridViewHandle, a cheap clone usable
before the grid is mounted and through all its rebuilds:
scroll_to_index,
ensure_index_visible and
reveal_band (a tile and its open band
together). A handle computes no offset when it is called: it records the
request, and the next layout applies it once the band is measured and the
scroll anchoring has run. Opening a band and scrolling to a tile under it
in one turn therefore lands the tile where it was asked to be, which an
offset written from outside does not: the layout keeps the content under
the top edge still across the band opening, and takes that offset for
one of the old layout. The builder's own scroll_to_index and
ensure_index_visible act at call time, so before the first layout they
do nothing.
let handle = grid.handle();
// later, from a command:
expanded.set(Some(album));
handle.reveal_band(album);
The keyboard cursor
The grid has one current tile, its keyboard cursor: the tile the arrow
keys step from, the one the focus ring marks while the grid holds keyboard
focus, and the one it names to assistive technology as its active
descendant. The same GridViewHandle reads and moves it:
cursor,
set_cursor (clamped to the last tile;
keyboard focus, the selection and the scroll offset are left alone) and
cursor_signal, which an application
observes to key a status line or its context actions on the current tile.
Like a reveal, a cursor set before the grid is mounted is there at its
first build.
The cursor and the selection are one tile in a single selection: the keys
move them together, and a selection written from outside moves the cursor
onto it. In a multiple selection they are separate: Ctrl+Arrow moves
the cursor and leaves the selection, so after →, Ctrl+→, Ctrl+↓ the
selection is still tile 0 while the cursor, the ring and the active
descendant are on tile 4 (in three columns), and a selection written from
outside leaves the cursor where it was. Read the cursor, not the first
selected tile, for "the tile the user is on".
An open detail band is on the keyboard path
already: ↓ on the open tile moves focus into its band, onto its first
control, while the cursor stays on the tile, and ↑ from the band's top
comes back to it. A band with no control to focus is stepped over. An
application has no walk of its own to write.
A long band
A band that holds hundreds of rows, an album's tracks say, need not build
them all. GridView::detail_visible_span_signal publishes the part of
the open band on screen, in band-local coordinates, and only when it
changes, so the band can realize the rows in the span and stand spacers
in for the rest, with no copy of the grid's placement arithmetic:
const ROW: f32 = 24.0;
let grid = GridView::new(albums, album_card);
let span = grid.detail_visible_span_signal().clone();
grid.detail_row(move |tc| {
let n = tc.item.tracks.len();
let first = span.map(|s| s.as_ref().map_or(0, |r| (r.start / ROW) as usize));
let last = span.map(move |s| {
s.as_ref().map_or(0, |r| ((r.end / ROW).ceil() as usize).min(n))
});
// `window` holds tracks[first..last], refilled when the span moves.
let window = track_window(&tc.item.tracks, &first, &last);
Some(Box::new(
VStack::new()
.child(FixedSize::new().height(first.map(|f| *f as f32 * ROW)))
.child(Repeater::new(window, |track| track_row(track)))
.child(FixedSize::new().height(last.map(move |l| (n - *l) as f32 * ROW))),
))
})
Pan to scroll
The view installs common::scrollable::ScrollableBehavior,
which gives it the shared wheel arithmetic, a finger's pan and the
PanClaim that puts it on a pan's claimant chain. A pan scrolls it, the
release coasts, and a pan it cannot absorb hands the whole event to the
container outside — never a residual. Vertical only, despite the grid: this
view owns no horizontal offset, so a horizontal pan is declined and chains
outward. A pan that starts on a tile scrolls rather than activating it.
The rubber band, and why it is not on this node
In teksilo_data::SelectionMode::Multi a drag on the empty background
sweeps a selection rectangle. That drag deliberately does not live on
this view's own node, which is the one carrying the PanClaim.
The reason it was structural has since been fixed in the framework: a node
that both declares a claim and carries on_drag is now given a say through
PointerSequence::defer_own_drag, which resolves its
teksilo_tokens::DragActivation against the claim it is competing with.
Before that arm existed the claim holder's own drag latched at drag_slop
and decided the sequence before the claim could win at pan_slop, so a
Multi-selection grid did not scroll under a finger from anywhere at all,
and a finger on the background swept a band immediately rather than after a
hold.
What the surface still buys is scope, and that is why it stays. This
root's children are the body pane, the scrollbar, the pinned section header,
the empty view, the loading view and the focus overlay — all siblings, most
of them filling the same rectangle. A marquee hung here would be a drag
ancestor of every one of them; hung on a DragSurface that encloses the body
pane alone, it sweeps the background and nothing else. The wrapper is also
not this view's to remove in any case: the four row views wrap each row
in one, and a row is not a claimant, so defer_own_drag — which refuses
anything but a live MemberRole::Pan member — cannot reach it. See
crate::data_views::DragSurface.
So the body pane carries a no-op tap that gives it an arena of its own (the
press is captured inside the surface, not by it) and the marquee's drag
hangs on a DragSurface that strictly encloses the pane — the shape the tree
arms DragActivation for through its ancestor walk, which is what makes the
marquee wait for a long press under a finger and latch at 5 dp under a mouse.
Every tile carries that same no-op tap as well, for a reason with nothing to do with dragging. With the pane holding one, a tile without an arena of its own leaves the pane as the press captor — and a release is dispatched to the captor and then bubbled target→root, which never reaches a tile beneath it. A plain selectable grid lost both its finger tap and, under a mouse, the release that collapses a multi-selection that way.
Density
The picture above is the widget at TargetDensity::Compact, the mouse-and-keyboard ladder. Below is the same subject on the same canvas with only the ladder changed, so what moves is the density and nothing else — where the subject no longer fits, that is what the denser targets cost it at that size. See docs/density-and-targets.md.
Touch

API reference
📖 Full rustdoc API for this module
pub struct TileContext
Import: use teksilo::widgets::TileContext;
Context passed to the tile delegate for each realized tile.
Richer than ListView's (index, &item, selected) — carries the 2D
grid coordinates and focus state (mirrors TableView's CellContext).
There is intentionally no is_hovered: hover changes on every
mouse-move and is handled per-tile inside the delegate's own widget
(its interaction signal), never by rebuilding the grid.
#![allow(unused)] fn main() { pub struct TileContext<'a, T: 'static> { /* fields */ } }
Fields
index:usize— Flat model index.row:usize— Row in the logical grid (0-based).col:usize— Column in the logical grid (0-based).item:&'a T— Borrow of the item.is_selected:bool— Whether this tile is in the selection set.is_focused:bool— Whether this tile is the keyboard-focus current item. A build-time snapshot — the canonical focus indicator is the grid's painted focus ring (it does not rebuild tiles), so a delegate reading this for custom styling accepts a one-rebuild lag.
pub struct GridView
Import: use teksilo::widgets::GridView;
A virtualized 2D tile grid backed by a ListModel<T>.
#![allow(unused)] fn main() { pub struct GridView<T: 'static> { /* fields */ } }
Methods
pub fn new( model: ListModel<T>, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static, ) -> Self
Create a grid backed by a ListModel<T>. The delegate builds the
widget for each tile from a TileContext.
pub fn from_source<S: teksilo_data::ListDataSource<Item = T>>( source: S, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static, ) -> Self
Create a grid backed by any ListDataSource (large / external data).
pub fn from_source_keyed<S: teksilo_data::ListDataSource<Item = T>>( source: S, keyed: KeyedSelectionModel<S::Key>, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static, ) -> Self where S::Key: ItemKey,
Create a grid backed by any ListDataSource with keyed selection.
The KeyedSelectionModel<S::Key> holds the selected items by the
source's key, not by position, so a selection survives the grid being
sorted (by artist, by year), narrowed by a filter, or a lazy source
sliding its loaded window: it stays on the same items. Clicks,
Ctrl/⌘-clicks, Shift-clicks, the keyboard and the rubber band all write
keys. An item the source no longer shows (removed, or filtered out
here) leaves the selection, as in ListView.
on_selection_changed still reports
indices: the positions the selected keys have when the selection
changes. A sort moves them without changing the selection, so it does
not fire. The view stays GridView<T>; the index↔key mapping is
captured from the concrete source here. Mutually exclusive with
selection (the last one set wins).
pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self
Enable or disable the whole view. A disabled view greys out and stops accepting focus / selection / keyboard input (arena-gated).
pub fn sizing(mut self, sizing: impl Into<Prop<GridSizing>>) -> Self
Set the tile sizing / column-count policy.
Accepts a plain GridSizing (static) or a Signal<GridSizing>
(reactive). A bound signal is observed at BindingLevel::Rebuild: when
it changes, build() rebuilds the cached layout strategy and reflows —
the internal scroll_y / focused_index / selection are field signals on
the same widget instance, so they survive the rebuild (no scroll jump).
This is the card-size-slider path; mirrors
TabWidget::sizing.
pub fn tile_size(mut self, width: f32, height: f32) -> Self
Sugar for GridSizing::Fixed — every tile is exactly width × height.
pub fn column_count(mut self, count: usize, tile_height: f32) -> Self
Sugar for GridSizing::FixedColumnCount — exactly count columns.
pub fn variable_row_heights(mut self, estimated: f32) -> Self
Switch to variable row heights: each row is sized to its tallest
tile (SwiftUI LazyVGrid semantics). estimated seeds rows that
haven't been measured yet; the scroll position is anchored when an
estimate is later corrected. Combine with
item_height for exact heights.
pub fn item_height(mut self, f: impl Fn(usize) -> f32 + 'static) -> Self
Supply an exact per-item natural height. Width-independent, so it
doesn't depend on the runtime column count: VariableRowGrid sizes
each row to max(item_height(i)) over its items. Implies variable row
heights, gives an exact scrollbar, and removes anchoring jitter.
pub fn waterfall(mut self, estimated: f32) -> Self
Switch to a Pinterest-style waterfall: per-item variable heights flow
into the currently-shortest column. Column count comes from the
configured sizing; heights are auto-measured (or
exact via item_height). estimated seeds
unmeasured items.
pub fn column_spacing(mut self, spacing: f32) -> Self
Horizontal gap between tiles (default 8).
pub fn row_spacing(mut self, spacing: f32) -> Self
Vertical gap between tile rows (default 8).
pub fn spacing(mut self, spacing: f32) -> Self
Set both column and row spacing.
pub fn content_inset(mut self, inset: EdgeInsets) -> Self
Inset from the scroll-content edge to the tiles.
pub fn min_columns(mut self, count: usize) -> Self
Never fewer than count columns (at least 1).
Bounds the count a GridSizing::Fixed or GridSizing::Adaptive
derives from the width; ignored by GridSizing::FixedColumnCount,
whose count is already the answer. When the width cannot afford
count columns of the sizing's width, the tiles narrow to share
the width — the grid never overflows horizontally. The tile height is
kept as the sizing gives it (a card gets narrower, not shorter), so a
tile that must hold its aspect ratio sizes its own content from the
width it is handed. If min_columns exceeds
max_columns, the maximum wins.
pub fn max_columns(mut self, count: usize) -> Self
Never more than count columns (at least 1), however wide the viewport.
Bounds the count a GridSizing::Fixed or GridSizing::Adaptive
derives from the width; ignored by GridSizing::FixedColumnCount.
Under Fixed the tiles keep their width and the leftover is placed by
column_alignment; under Adaptive they
stretch to share the width of the capped count, up to max_width.
pub fn column_alignment(mut self, alignment: HAlignment) -> Self
Where the block of columns sits in the viewport when it is narrower
than the content: HAlignment::Leading (the default, the tiles
against the leading inset, leftover trailing), Center or
Trailing. Leading and trailing swap in a right-to-left layout. Only
the columns move: a section header and a detail_row
band span the content width. The order of the columns within the
block is the same in both directions.
Only a layout that leaves width over has anything to place: a
GridSizing::Fixed grid, an Adaptive one with max_width, or any
grid capped by max_columns.
pub fn selection(mut self, sel: SelectionModel) -> Self
Set the index-based selection model (modes None / Single /
Multi). It holds positions: for a selection that stays on its items
through a sort or a filter, build the grid with
from_source_keyed instead.
pub fn on_selection_changed(mut self, f: impl Fn(&BTreeSet<usize>) + 'static) -> Self
Called whenever the selection set changes — including programmatic changes — with the new set of selected indices. For a keyed selection these are the positions its keys have at that moment.
Because it also fires for programmatic changes, it carries no input
modifiers and no EventContext;
for the modifiers held at the moment of input, use
on_tile_activate, which receives one.
pub fn marquee_selection(mut self, enabled: bool) -> Self
Enable / disable rubber-band marquee selection (default enabled; only
active when the selection model is in Multi mode).
pub fn wrap_navigation(mut self, enabled: bool) -> Self
Whether arrow navigation wraps across row/grid edges (default false).
pub fn tab_traversal(mut self, traversal: GridTabTraversal) -> Self
How Tab moves out of (or within) the grid (default OutOfGrid).
pub fn show_scrollbar(mut self, show: bool) -> Self
Suppress the internal scrollbar (mount your own via the signal accessors so it survives rebuilds).
pub fn overscroll_behavior(mut self, behavior: OverscrollBehavior) -> Self
Scroll-chaining behavior at the boundary (default Chain).
pub fn smooth_scrolling(mut self, enabled: bool) -> Self
Enable or disable animated wheel scrolling (enabled by default).
pub fn smooth_scroll_duration(mut self, duration: Duration) -> Self
Duration of the smooth scroll animation (default 150 ms).
pub fn scroll_bar_style(mut self, style: ScrollBarMode) -> Self
How the scroll bar is displayed (default Permanent). Overlay
and Thin float the bar over the content instead of reserving a
layout column, mirroring ScrollArea::scroll_bar_style.
pub fn scroll_y_signal(&self) -> &Signal<f32>
The vertical scroll offset signal.
pub fn max_scroll_y_signal(&self) -> &Signal<f32>
The maximum scroll offset signal (content_height - viewport_height).
pub fn viewport_ratio_y_signal(&self) -> &Signal<f32>
The vertical viewport-to-content ratio signal (drives the thumb size).
pub fn detail_visible_span_signal(&self) -> &Signal<Option<std::ops::Range<f32>>>
The part of the open detail band that is on screen, in band-local
coordinates: max(0, scroll_y − band.y) .. min(band.height, scroll_y + viewport_height − band.y), where band.y is the band's top in the
grid's content. None while no band is open, and while the open band
is entirely off screen.
Written by each layout after the scroll offset is clamped, and only when it changes, so an observer is not told of a scroll that leaves the band's visible part as it was. Meant for a band holding far more than a viewport shows, hundreds of tracks say, that realizes only the rows in the span: a window of rows between two spacers, with no copy of the grid's placement arithmetic. See the module docs, "A long band".
pub fn handle(&self) -> GridViewHandle
A GridViewHandle on this grid: scroll it to a tile, or to a tile
and its detail band, from outside the widget.
Usable at once and for the grid's whole life: a request made before
the grid is mounted is applied at its first layout, and the handle
keeps working through every rebuild. A request is applied by the
layout, after it has measured the detail band and settled the scroll
anchoring, so one made in the same turn as opening a band lands where
the new layout puts the tile. Prefer it to
scroll_to_index and
ensure_index_visible, which compute
the offset when they are called.
pub fn ensure_index_visible(&self, index: usize, anchor: ScrollAnchor)
Bring index into view: nothing when the tile is already fully
visible, otherwise a scroll per anchor, as
scroll_to_index.
Computed at call time from the last layout's geometry and clamped to
its scroll range. Before the grid has been laid out there is no
geometry and this does nothing, and a change of layout made in the
same turn (a detail band opened above the tile) is not yet counted.
GridViewHandle::ensure_index_visible has neither limit.
pub fn scroll_to_index(&self, index: usize, anchor: ScrollAnchor)
Scroll to index, forcing its position in the viewport per anchor;
ScrollAnchor::Auto scrolls the least that shows it.
Computed at call time, with the limits
ensure_index_visible documents: it
does nothing before the grid's first layout.
GridViewHandle::scroll_to_index has neither limit.
pub fn sections<P: SectionProvider>(mut self, provider: P) -> Self
Group the flat model into sections, rendering a header above each section's tile band. Sections compose with the uniform tile layout.
pub fn section_header_delegate( mut self, f: impl Fn(usize, &str) -> Box<dyn Widget> + 'static, ) -> Self
Custom section-header widget builder (section_index, title). Without
it a default bold-text header is used.
pub fn section_header_height(mut self, height: f32) -> Self
Height of each section header row (default 28).
pub fn pinned_section_headers(mut self, enabled: bool) -> Self
Keep the current section's header pinned to the top while scrolling
through it (SwiftUI pinnedViews:[.sectionHeaders]).
pub fn a11y_label(mut self, label: impl Into<String>) -> Self
Accessible label for the grid container.
pub fn style(mut self, style: impl GridViewStyle) -> Self
Per-call Tier-3 decoration style override (focus ring, marquee,
insertion bar, pinned-header surface). Precedence: this override →
theme.style_slots.grid_view → the stock RecipeGridViewStyle.
pub fn empty_view(mut self, f: impl Fn() -> Box<dyn Widget> + 'static) -> Self
Widget shown when the model is empty.
pub fn loading_view(mut self, f: impl Fn() -> Box<dyn Widget> + 'static) -> Self
Widget overlaid while is_loading reads true.
pub fn is_loading(mut self, flag: impl Into<Prop<bool>>) -> Self
Reactive loading flag; when true the loading_view
is shown above the grid.
pub fn reorderable(mut self, enabled: bool) -> Self
Enable intra-grid drag reordering (and keyboard Alt+Arrow). The move is
routed through the source's accept_drop (a built-in ListModel
reorders via move_item; an external source applies its own command).
pub fn exportable(mut self, mode: DragTransferMode) -> Self where T: Clone,
Make tiles droppable outside this view — on a
DropTarget, another data view, or the OS.
A dragged tile (or the whole selection, when the pressed tile is part of
a multi-selection) carries clones of its items in a public
RowDragData<T>, so a foreign receiver can pull
them out with payload.get_typed::<RowDragData<T>>() /
DropTarget::on_drop_typed::<RowDragData<T>>() — no serialization. This
also makes tiles a drag source even without reorderable.
mode chooses what happens to the origin rows once a foreign target
accepts them: DragTransferMode::Move removes them (via the source's
on_drag_out, or on_rows_transferred_out),
DragTransferMode::Copy leaves them. A same-view reorder is never a
transfer, so mode never affects it. Requires T: Clone.
pub fn export_external(mut self, f: impl Fn(&[T]) -> Vec<(String, Vec<u8>)> + 'static) -> Self where T: Clone,
Additionally advertise the dragged tiles as MIME data so they can be
dropped on a DropZone or exported to another
application / window via the OS. f maps the dragged items to
(mime_type, bytes) pairs (e.g. text/plain, text/uri-list, an
app-specific application/x-…). Implies exportable
(defaulting to DragTransferMode::Move if not already set). Requires
T: Clone.
pub fn on_rows_transferred_out( mut self, f: impl Fn(&[usize], &mut teksilo_core::widget::EventContext) + 'static, ) -> Self
Override how rows moved out to a foreign target are removed from this
view. Receives the dragged rows' indices (descending-safe) and the live
context. Without this, an exportable
Move drag removes them through the source's
on_drag_out (works out of the box for a ListModel).
pub fn accept_foreign_rows(mut self, accept: bool) -> Self
Accept exported rows dropped from a different view or source without
writing a custom ListDataSource. Pair with
on_rows_received, which is handed the dropped
items and the insertion index. (Same-view reorder is
reorderable; a custom ListDataSource can still
accept foreign drops through its can_accept/accept_drop instead.)
pub fn on_rows_received( mut self, f: impl Fn(Vec<T>, usize, &mut teksilo_core::widget::EventContext) + 'static, ) -> Self
Handler for rows accepted via accept_foreign_rows:
(items, insertion_index, ctx). Insert them into your model at the
index.
pub fn on_item_drop( mut self, f: impl Fn( teksilo_core::drag_payload::DragPayload, usize, &mut teksilo_core::widget::EventContext, ) -> bool + 'static, ) -> Self
Accept external drops at a flat insertion index. Returns true when
the drop is accepted.
pub fn on_tile_activate( mut self, f: impl Fn(usize, &mut teksilo_core::widget::EventContext) + 'static, ) -> Self
Called when a tile is activated (a click per activate_on,
or Enter on the focused tile) — the "open / default action", distinct
from selection.
pub fn activate_on(mut self, mode: crate::data_views::ActivateOn) -> Self
Choose single- vs double-click tile activation (default
ActivateOn::DoubleClick). Enter activates in either
mode.
pub fn tile_context_menu( mut self, f: impl Fn(usize, Point, &mut teksilo_core::widget::EventContext) -> Option<Box<dyn Widget>> + 'static, ) -> Self
Per-tile context-menu factory: (index, pointer_position, ctx) →
optional menu widget.
pub fn type_ahead_label(mut self, f: impl Fn(usize) -> String + 'static) -> Self
Supply a per-item label for type-ahead navigation (typing letters jumps to the first matching item). Required to enable type-ahead.
pub fn tile_a11y_label(mut self, f: impl Fn(usize) -> String + 'static) -> Self
Supply a per-item accessible name applied to each tile's GridCell
(Node::label), so a screen reader announces a concise item name in
addition to the row/column position. Without it, the cell's name is left
to its contents.
pub fn type_ahead_timeout(mut self, timeout: std::time::Duration) -> Self
Type-ahead reset timeout (default 500 ms; ZERO disables).
pub fn expanded_index(mut self, expanded: Signal<Option<usize>>) -> Self
The disclosed tile: Some(index) opens a full-width band under the
row holding tile index (see detail_row),
None closes it. The grid writes it when a tile is activated (not for
a tile with nothing to disclose), and follows writes made from
outside. Without this the grid keeps its own signal.
pub fn detail_row( mut self, f: impl Fn(&TileContext<'_, T>) -> Option<Box<dyn Widget>> + 'static, ) -> Self
The content of the detail band: the "album expansion" of a music library, a full-width band opened directly under the row that holds the activated tile.
f receives the disclosed tile's TileContext, the same one the
tile delegate gets, and returns the band's content, or None when the
tile has nothing to disclose. It is also asked when a tile is
activated, to learn whether it has anything, and its answer dropped,
so it should be cheap and have no side effects. The band pushes the
following rows down, and stays under its tile when a resize changes
the column count.
With a band, activating a tile (a click per
activate_on, or Enter) opens it and scrolls it
into view, and activating the open tile closes it;
on_tile_activate still fires. A tile with
nothing to disclose opens nothing. In a single selection, Enter also
selects the tile whose band it opens, unless the application has an
activation of its own; a multiple selection is left as it is. Space on
the open tile closes it. ↓ from the open tile
moves focus to the first focusable control in the band, scrolling it
into view, and ↑ from there, when the control does not use it, returns
to the tile. A band that closes with focus inside it hands focus back
to the grid, on its tile. Arrows between tiles step over the band, and
the rubber band selects no band.
The band follows its tile when the tile moves. A source with item keys
is followed through a sort or a filter (a reset); a source without
them is followed through inserts, removals and moves, and closes the
band on a reset. A ListModel's keys are its positions, which
identify no item across a reset, so it counts as a source without
keys here, through from_source as through
new. The band closes when its tile is removed.
The band is measured height-for-width at the content width, or sized
by detail_row_height. It works with the
uniform grid, variable_row_heights and
sections. A waterfall has no
rows to put it under and ignores it. A band that opens, closes or
moves above the viewport leaves the visible tiles where they are. Its
content is rebuilt when the band moves to another tile, when its own
tile's item arrives from a lazy source, and when the grid's rows
change (an insert, a removal, a move, an update, a reset); a lazy
source loading other windows leaves it as it is.
Accessibility: the band is a Role::Group, read after its tile's
row, named after its tile's tile_a11y_label,
or labelled by the tile, or, with the tile outside the realized
window, "Details of item N" in the user's language. A tile carries an
expanded state, Expand / Collapse actions and, while open, a
controls relation to the band. Without
detail_row_when every tile does, one with
nothing to disclose included, since knowing otherwise means building
its band, and that one stays collapsed. The grid's row and column
counts and the tiles' positions do not count the band.
pub fn detail_row_when(mut self, f: impl Fn(&TileContext<'_, T>) -> bool + 'static) -> Self
Which tiles have a detail band, answered without building one: f
receives the tile's TileContext, as
detail_row does, and returns false for a tile
with nothing to disclose.
Such a tile is not a disclosure. Assistive technology hears no
expanded state on it and is offered no Expand or Collapse, which it
would be otherwise. Activating it opens nothing, detail_row is not
asked for it, and expanded_index naming it
opens no band.
f is asked as each tile is built: when it comes into the realized
window, after a change to the data, and when its selectedness flips.
Answer from the item, cheaply and with no side effects, and agree with
detail_row: a tile f accepts and detail_row gives nothing for is
offered as expandable, and opens nothing. A tile of a lazy source
whose item has not arrived is offered until it does. Without
detail_row, this does nothing.
pub fn detail_row_height(mut self, f: impl Fn(usize) -> f32 + 'static) -> Self
The detail band's height for the tile at index, instead of measuring
it. See detail_row.
pub struct GridViewHandle
Import: none — GridViewHandle is public in a private module of teksilo-widgets and nothing re-exports it.
A handle on a GridView's scroll position, for a
caller outside the widget: a toolbar, a "show current" command, a search
result, an observer of the application's own state.
Taken with GridView::handle, cheap to clone,
and usable at once: a request made before the grid is mounted is kept and
applied at its first layout, and the handle keeps working through every
rebuild of the grid. Each method records a request and asks for a
relayout; the layout applies it after it has measured the detail band and
settled the scroll anchoring, so a request made in the same turn as a
change of layout lands where the new layout puts the tile:
// Open the band of tile 0 and put tile 30 at the top, in one turn.
expanded.set(Some(0));
handle.scroll_to_index(30, ScrollAnchor::Start);
Only the latest request is applied: a later one replaces one still pending. A request for a tile the grid does not have when it is applied does nothing.
The handle also reads and moves the grid's keyboard cursor, the tile
the arrow keys step from, the focus ring marks and assistive technology is
told is current: cursor,
set_cursor and cursor_signal.
In a multiple selection the cursor and the selection are separate: the
accelerator with an arrow moves the cursor alone, and a selection written
from outside leaves the cursor where it was.
#![allow(unused)] fn main() { pub struct GridViewHandle { /* fields */ } }
Methods
pub fn cursor(&self) -> Option<usize>
The tile the keyboard cursor is on, or None while the grid has no
cursor: it has not been navigated, clicked or given one by
set_cursor. With no cursor the keys start from
the first selected tile, and from the first or last tile when nothing
is selected.
In a single selection the cursor follows the selection: the keys move
them together, and a selection written from outside moves the cursor
onto it. In a multiple selection they are separate: the accelerator
with an arrow (Ctrl+Arrow) moves the cursor alone, and a selection
written from outside leaves the cursor where it was.
pub fn set_cursor(&self, index: Option<usize>)
Move the keyboard cursor to tile index, or remove it with None.
An index past the last tile is clamped to the last tile; on an empty grid the cursor is removed. The focus ring moves at the next paint, while the grid holds keyboard focus, and the grid names the tile as its active descendant at the next accessibility update when the tile is realized. The next arrow key steps from it.
Keyboard focus and the selection are left alone, and the grid does not
scroll: pair it with ensure_index_visible
to bring the tile into view. Usable before the grid is mounted (the
first build finds the cursor there) and across its rebuilds. In a
single selection the next change of selection puts the cursor back on
the selected tile.
pub fn cursor_signal(&self) -> Signal<Option<usize>>
A read-only signal of the cursor, as cursor reports
it, for an application to observe or bind: a status line naming the
current tile, actions enabled for it. It changes with every write of
the cursor, by the keys, a click, the data shifting it or
set_cursor.
pub fn scroll_to_index(&self, index: usize, anchor: ScrollAnchor)
Scroll to tile index, forcing its position in the viewport per
anchor: ScrollAnchor::Start puts its top at the viewport's top,
Center centres it,
End puts its bottom at the viewport's bottom,
and Auto scrolls the least that shows it.
Clamped to the scroll range. Applied at the next layout.
pub fn ensure_index_visible(&self, index: usize, anchor: ScrollAnchor)
Bring tile index into view: nothing when it is already fully
visible, otherwise a scroll per anchor, as
scroll_to_index. Applied at the next
layout, against the layout it will be shown in.
pub fn reveal_band(&self, index: usize)
Scroll so tile index and its open detail band are both visible:
the least scroll that shows the span from the top of the tile's row
to the bottom of its band, and the row first when the two do not fit
together, so the band reads as the tile's. This is what the grid does
itself when a user opens a band.
When index's band is not open at the layout that applies it (no
detail_row, another tile disclosed,
or a tile with nothing to disclose), the tile alone is brought into
view, as ensure_index_visible with
ScrollAnchor::Auto. Open the band and call this in the same turn:
the band is measured before the request is applied.
pub enum GridTabTraversal
Import: none — GridTabTraversal is public in a private module of teksilo-widgets and nothing re-exports it.
How Tab moves out of (or within) the grid.
#![allow(unused)] fn main() { pub enum GridTabTraversal { /* variants */ } }
Variants
OutOfGrid— Tab releases focus to the next focusable widget in the window.WithinGrid— Tab advances to the next tile (wrapping rows); Shift+Tab the previous.
pub trait SectionProvider
Import: none — SectionProvider is public in a private module of teksilo-widgets and nothing re-exports it.
Partitions a flat model into display sections.
#![allow(unused)] fn main() { pub trait SectionProvider: 'static { /* associated items below */ } }
Associated items
fn section_count(&self) -> usize;
Number of sections.
fn items_in_section(&self, section: usize) -> usize;
Number of items in section.
fn section_title(&self, section: usize) -> String;
Display title for section.
fn section_counts(&self) -> Vec<usize> { /* default implementation */ }
Per-section item counts, in order. Used by the layout strategy.
pub struct GroupingSections
Import: none — GroupingSections is public in a private module of teksilo-widgets and nothing re-exports it.
A SectionProvider built by partitioning consecutive equal-key runs of
an (already ordered) model. The titles come from each run's key.
#![allow(unused)] fn main() { pub struct GroupingSections { /* fields */ } }