Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GridView

GridView preview

Virtualized 2D tile grid bound to a ListModel<T> / ListDataSource.

Public types

KindName
structTileContext — Context passed to the tile delegate for each realized tile
structGridView — A virtualized 2D tile grid backed by a ListModel<T>
structGridViewHandle — A handle on a GridView's scroll position, for a caller outside the widget: a toolbar, a "show current" command, a search result, an…
enumGridTabTraversal — How Tab moves out of (or within) the grid
traitSectionProvider — Partitions a flat model into display sections
structGroupingSections — A SectionProvider built by partitioning consecutive equal-key runs of an (already ordered) model

Public functions

GridView

ReturnsFunction
Constructors
Selfnew(model: ListModel<T>, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static)
Selffrom_source<S: teksilo_data::ListDataSource<Item = T>>(source: S, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static)
Selffrom_source_keyed<S: teksilo_data::ListDataSource<Item = T>>(source: S, keyed: KeyedSelectionModel<S::Key>, delegate: impl Fn(&TileContext<'_, T>) -> Box<dyn Widget> + 'static)
Builder methods
Selfenabled(enabled: impl Into<Prop<bool>>)
Selfsizing(sizing: impl Into<Prop<GridSizing>>)
Selftile_size(width: f32, height: f32)
Selfcolumn_count(count: usize, tile_height: f32)
Selfvariable_row_heights(estimated: f32)
Selfitem_height(f: impl Fn(usize) -> f32 + 'static)
Selfwaterfall(estimated: f32)
Selfcolumn_spacing(spacing: f32)
Selfrow_spacing(spacing: f32)
Selfspacing(spacing: f32)
Selfcontent_inset(inset: EdgeInsets)
Selfmin_columns(count: usize)
Selfmax_columns(count: usize)
Selfcolumn_alignment(alignment: HAlignment)
Selfselection(sel: SelectionModel)
Selfon_selection_changed(f: impl Fn(&BTreeSet<usize>) + 'static)
Selfmarquee_selection(enabled: bool)
Selfwrap_navigation(enabled: bool)
Selftab_traversal(traversal: GridTabTraversal)
Selfshow_scrollbar(show: bool)
Selfoverscroll_behavior(behavior: OverscrollBehavior)
Selfsmooth_scrolling(enabled: bool)
Selfsmooth_scroll_duration(duration: Duration)
Selfscroll_bar_style(style: ScrollBarMode)
Selfsections<P: SectionProvider>(provider: P)
Selfsection_header_delegate(f: impl Fn(usize, &str) -> Box<dyn Widget> + 'static)
Selfsection_header_height(height: f32)
Selfpinned_section_headers(enabled: bool)
Selfa11y_label(label: impl Into<String>)
Selfstyle(style: impl GridViewStyle)
Selfempty_view(f: impl Fn() -> Box<dyn Widget> + 'static)
Selfloading_view(f: impl Fn() -> Box<dyn Widget> + 'static)
Selfis_loading(flag: impl Into<Prop<bool>>)
Selfreorderable(enabled: bool)
Selfexportable(mode: DragTransferMode)
Selfexport_external(f: impl Fn(&[T]) -> Vec<(String, Vec<u8>)> + 'static)
Selfon_rows_transferred_out(f: impl Fn(&[usize], &mut teksilo_core::widget::EventContext) + 'static)
Selfaccept_foreign_rows(accept: bool)
Selfon_rows_received(f: impl Fn(Vec<T>, usize, &mut teksilo_core::widget::EventContext) + 'static)
Selfon_item_drop(f: impl Fn( teksilo_core::drag_payload::DragPayload, usize, &mut teksilo_core::widget::EventContext, ) -> bool + 'static)
Selfon_tile_activate(f: impl Fn(usize, &mut teksilo_core::widget::EventContext) + 'static)
Selfactivate_on(mode: crate::data_views::ActivateOn)
Selftile_context_menu(f: impl Fn(usize, Point, &mut teksilo_core::widget::EventContext) -> Option<Box<dyn Widget>> + 'static)
Selftype_ahead_label(f: impl Fn(usize) -> String + 'static)
Selftile_a11y_label(f: impl Fn(usize) -> String + 'static)
Selftype_ahead_timeout(timeout: std::time::Duration)
Selfexpanded_index(expanded: Signal<Option<usize>>)
Selfdetail_row(f: impl Fn(&TileContext<'_, T>) -> Option<Box<dyn Widget>> + 'static)
Selfdetail_row_when(f: impl Fn(&TileContext<'_, T>) -> bool + 'static)
Selfdetail_row_height(f: impl Fn(usize) -> f32 + 'static)
Methods
&Signal<f32>scroll_y_signal()
&Signal<f32>max_scroll_y_signal()
&Signal<f32>viewport_ratio_y_signal()
&Signal<Option<std::ops::Range<f32>>>detail_visible_span_signal()
GridViewHandlehandle()
ensure_index_visible(index: usize, anchor: ScrollAnchor)
scroll_to_index(index: usize, anchor: ScrollAnchor)

GridViewHandle

SectionProvider

ReturnsFunction
Methods
usizesection_count()
usizeitems_in_section(section: usize)
Stringsection_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

GridView at Touch density

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 */ }
}