Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Lists & Data

High-performance list components with virtualization for rendering thousands of items.


UniformList

Highest-performance list for items of equal, positive height. It measures one row and only renders the visible range, so large logs and tables do not create an element for every record:

#![allow(unused)]
fn main() {
use kael::{uniform_list, UniformListScrollHandle};

let scroll_handle = UniformListScrollHandle::new();

uniform_list(
    "log-entries",
    self.entries.len(),
    {
        let entries = self.entries.clone();
        move |range, _window, _cx| {
            entries[range.clone()]
                .iter()
                .map(|entry| {
                    div()
                        .px_3()
                        .py_1()
                        .text_sm()
                        .child(entry.message.clone())
                        .into_any_element()
                })
                .collect()
        }
    },
)
.track_scroll(scroll_handle.clone())
}

When to use: Log viewers, file lists, data tables — any list where every row has the same height.

The measured row must resolve to a finite height greater than zero. A zero, negative, NaN, or infinite height is treated as an empty viewport for that frame instead of attempting an invalid visible-range calculation. Use with_width_from_item(Some(index)) when a representative row is a better width sample than row zero.


List

Flexible list with alignment and overflow handling:

#![allow(unused)]
fn main() {
use kael::list;

// Basic list
list()
    .child(div().child("Item 1"))
    .child(div().child("Item 2"))
    .child(div().child("Item 3"))
}

RecyclingList

Virtualized list for items with different heights. Supply a delegate with stable estimated heights for rows that have not been measured yet:

#![allow(unused)]
fn main() {
use kael::{
    AnyElement, App, FontWeight, IntoElement, ListDelegate, Pixels, Window, div, px,
    recycling_list,
};
use std::sync::Arc;

#[derive(Clone)]
struct MessageDelegate {
    messages: Arc<[Message]>,
    // Increment when messages are inserted, removed, reordered, or their
    // estimated heights change.
    height_revision: u64,
}

impl ListDelegate for MessageDelegate {
    fn item_count(&self) -> usize {
        self.messages.len()
    }

    fn estimated_item_height(&self, index: usize) -> Pixels {
        let body_lines = self.messages[index].body.lines().count().max(1);
        px(44.0 + body_lines as f32 * 18.0)
    }

    fn estimated_heights_revision(&self) -> Option<u64> {
        Some(self.height_revision)
    }

    fn render_item(&self, index: usize, _window: &mut Window, _cx: &mut App) -> AnyElement {
        let msg = &self.messages[index];
        div()
            .p_3()
            .child(div().font_weight(FontWeight::BOLD).child(msg.sender.clone()))
            .child(div().text_sm().child(msg.body.clone()))
            .into_any_element()
    }
}

recycling_list(
    "messages",
    MessageDelegate {
        messages: self.messages.clone(),
        height_revision: self.message_height_revision,
    },
)
}

When to use: Chat messages, feed items — lists where rows vary in height.

Returning Some(revision) from estimated_heights_revision is the fast path: unchanged frames reuse the existing height sum-tree without an O(total items) estimation pass. Increment the revision whenever count, order, or estimates change. The default return value is None; that is safe for fully dynamic delegates because estimates are refreshed every frame, but it intentionally trades away the steady-state optimization.

Element pooling is opt-in through recycle_key and render_recycled_item. Kael sizes each keyed pool from the observed visible-and-overdraw high-water mark, so large viewports are not constrained by a small fixed pool. Only return elements whose supports_reuse() implementation is true, and fully update recycled content before returning it for a new index.


SortableList

Drag-to-reorder list with auto-scroll and insertion indicator:

#![allow(unused)]
fn main() {
use kael::sortable_list;

sortable_list(
    "layers",
    self.layers.len(),
    {
        let layers = self.layers.clone();
        move |index, _window, _cx| {
            div()
                .px_3()
                .py_2()
                .child(layers[index].name.clone())
                .into_any_element()
        }
    },
)
.on_reorder({
    let entity = entity.clone();
    move |from, to, _window, cx| {
        entity.update(cx, |this, cx| {
            let item = this.layers.remove(from);
            this.layers.insert(to, item);
            cx.notify();
        });
    }
})
}

When to use: Layer panels, playlist editors, kanban columns — anywhere users reorder items by dragging.


ScrollBar

Kael provides automatic scrollbars for any element with overflow_y_scroll() or overflow_y_auto() and a tracked ScrollHandle. The scrollbar appears as a native-style dark rounded thumb when content overflows — no extra code needed (see Layout & Styling).

For custom scroll bar rendering, use the explicit scroll_bar() widget:

#![allow(unused)]
fn main() {
use kael::scroll_bar;

scroll_bar(scroll_handle.clone())
    .render_with(|state, bounds, window, _cx| {
        // Custom scroll bar rendering
        // state.thumb_bounds, state.dragging
    })
}

Patterns

Virtual DataTable selection and paging

DataTable::new_virtual keeps only a bounded LRU of pages and virtualizes both rows and variable-width columns. A million-row select-all is represented as "all except these deselected rows", so it stores zero row indices until a user starts excluding rows:

DataTable::new_virtual(1_000_000, columns, 128, cx)
    .max_cached_pages(8)
    .show_selection(true)
    .on_fetch_page_request(|request, _window, cx| {
        // Fetch or compute only request.page_start()..page_start + page_size.
        // Commit with set_page_data_for(request, rows, cx); stale generations
        // are rejected automatically.
    })
    .on_selection_change_snapshot(|selection, _window, _cx| {
        println!(
            "{} selected; {} stored indices ({})",
            selection.selected_count(),
            selection.stored_index_count(),
            selection.representation_key(),
        );
    })

Use on_selection_change_snapshot for bulk actions. Its DataTableSelectionSnapshot::AllExcept { total_rows, deselected } variant is exact without expansion. The compatibility on_selection_change(&[usize], ...) callback is invoked only when the exact selected indices can be materialized within 16,384 items; it is intentionally skipped for a million-row all-except selection rather than reporting an empty or partial slice. Likewise, selected_rows() returns None for all-except state; this does not mean the selection is empty.

Virtual-table search and sort are query inputs for the backing source. Kael invalidates the current generation and cache; the application or a Kael Web Worker performs the large search/sort and returns the requested page. Kael does not scan or allocate the million-row logical range on the UI thread.

Data table with uniform_list

#![allow(unused)]
fn main() {
struct DataTable {
    rows: Vec<Row>,
    columns: Vec<Column>,
    scroll: UniformListScrollHandle,
}

impl Render for DataTable {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let columns = self.columns.clone();
        let rows = self.rows.clone();

        div().flex().flex_col().size_full()
            .child(self.render_header())
            .child(
                uniform_list("table-body", rows.len(), move |range, _w, _cx| {
                    rows[range.clone()].iter().map(|row| {
                        div().flex().flex_row()
                            .children(columns.iter().map(|col| {
                                div().w(px(col.width)).px_2().py_1()
                                    .child(row.get(&col.key).clone())
                            }))
                            .into_any_element()
                    }).collect()
                })
                .track_scroll(self.scroll.clone())
            )
    }
}
}