Skip to content

Latest commit

Β 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

slint-tree-view

A reusable TreeView component for Slint, packaged as a Slint library crate so any Slint project can pull it in as a regular Cargo dependency.

Slint does not yet ship a built-in TreeView (slint-ui/slint#505). This crate fills that gap using the canonical flat-list-with-depth pattern (recommended by the Slint team in discussion #1042) and exposes it via the experimental-module-builds feature introduced in Slint 1.14.

Basic example screenshot

The examples/basic/ app β€” a tiny interactive tree with branches, leaves, and a section header.

Features

  • Hierarchical display via flat-list-with-depth. The host flattens its tree into a Vec<TreeItem>; the component handles indentation and selection.
  • Full keyboard navigation: ↑/↓, Home/End, PageUp/PageDown (with page-size), ← (collapse / current-parent-change-requested), β†’ / + / * (expand / recursive expand), - (collapse), Enter (activate). Unmapped keys bubble via key-press-event.
  • Branch indicators with dedicated click target (clicks don't change current item), z: 1 so the indicator TouchArea wins over the row TouchArea when they overlap.
  • Activation policy β€” activation-mode: single-click | double-click. expands-on-double-click toggles whether double-clicking a branch expands it or activates it.
  • Full theming β€” every style property is settable per instance (colors, dimensions, glyphs), with the TreeViewStyle global supplying the defaults.
  • Non-selectable / disabled items. selectable: false for section headers (skipped by mouse); enabled: false for greyed-out items, applied via both disabled-text-color and a multiplicative disabled-opacity so it's visually obvious.
  • Accessibility hooks β€” every item exposes accessible-role: list-item + accessible-label.
  • Right-click β†’ context menu via pointer-event (filters for right-button-down; Slint 1.17 has no dedicated right-clicked).
  • Rust DX β€” TreeItem::branch() / ::leaf() / ::section() constructors; fluent builder methods (.with_icon(), .with_decoration(), .collapsed(), .disabled(), .non_selectable(), .with_item_type(), .with_user_data()); NO_PARENT sentinel; index_of_id() / index_of_id_model() lookup helpers; constructors take impl Into<slint::SharedString> so &str / String / SharedString all work without extra allocations.
  • #![deny(unsafe_code)] at the crate root (Slint's generated vtable module locally allows unsafe for its plumbing).

Quick start

1. Add the dependency

# Cargo.toml
[dependencies]
slint = "1.17"
slint-tree-view = "…"

[build-dependencies]
slint-build = { version = "1.17", features = ["experimental-module-builds"] }

2. Compile your .slint as usual

// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    slint_build::compile("ui/main.slint")?;
    Ok(())
}

3. Import and use

// ui/main.slint
import { TreeView, TreeItem } from "@TreeView";

export component App inherits Window {
    forward-focus: tree;

    tree := TreeView {
        model: [ /* TreeItem[] */ ];

        current-changed(id) => { /* update your editor pane */ }
        item-activated(id) => { /* open / paste / etc. */ }
        item-expand-requested(id)   => { /* flip expanded=true in your model */ }
        item-collapse-requested(id) => { /* flip expanded=false */ }
    }
}

4. Build the model from Rust

use slint_tree_view::{TreeItem, NO_PARENT};

fn build_model() -> Vec<TreeItem> {
    vec![
        TreeItem::branch(1, NO_PARENT, 0, "Inbox").with_icon("πŸ“₯"),
        TreeItem::leaf(2, 1, 1, "Welcome", "Hello world").with_icon("βœ‰οΈ"),
        TreeItem::section(100, NO_PARENT, 0, "Archived"),
        TreeItem::branch(3, NO_PARENT, 0, "2024")
            .with_icon("πŸ“¦")
            .collapsed(),
    ]
}

There's also a fully-runnable example at examples/basic/ β€” see Examples below.

API reference

TreeItem struct

export struct TreeItem {
    internal-id: int,         // stable id; passed back in every callback
    parent-internal-id: int,  // NO_PARENT (-1) = root; cached for jump-to-parent
    depth: int,               // 0 = top level; each unit adds `indentation`
    text: string,             // user-visible label
    decoration-text: string,  // host-chosen glyph; empty = nothing drawn
    item-type: int,           // app-defined discriminator; not interpreted
    user-data: string,        // opaque payload; never read by the component
    has-children: bool,       // structural hint; drives the branch indicator
    expanded: bool,           // current expand state; drives β–Ό vs β–Ά
    selectable: bool,         // false = section header / non-clickable
    enabled: bool,            // false = greyed out, ignored by mouse & keyboard
}

Rust constructors and builders

Constructor Description
TreeItem::branch(id, parent_id, depth, text) Has children, expanded by default, selectable+enabled.
TreeItem::leaf(id, parent_id, depth, text, user_data) No children, not expanded, selectable+enabled.
TreeItem::section(id, parent_id, depth, text) Non-selectable (section header). Still visible.
Builder Effect
.with_icon(glyph) / .with_decoration(glyph) Set decoration-text.
.collapsed() / .with_expanded(bool) Override the default expand state.
.disabled() Set enabled = false.
.non_selectable() Set selectable = false.
.with_item_type(int) Set item-type.
.with_user_data(string) Set user-data.

All text arguments are impl Into<slint::SharedString>, so &str, String, and slint::SharedString all work without extra allocations.

Free helpers

pub const NO_PARENT: i32 = -1;

pub fn index_of_id(items: &[TreeItem], internal_id: i32) -> Option<usize>;
pub fn index_of_id_model(model: &slint::ModelRc<TreeItem>, internal_id: i32) -> Option<usize>;

NO_PARENT is the sentinel for top-level items' parent-internal-id. The two index_of_id flavors are the lookup hosts need constantly when handling TreeView callbacks (which report ids) against their own model.

TreeView component

Properties

Property Type Default Description
model [TreeItem] [] The flattened tree data (in β€” the view never mutates it).
current-index int -1 Index of the current item, or -1.
current-internal-id int (read-only) Id mirror of current-index. Hosts should prefer correlating on this.
focused bool true When true, uses highlight-color; otherwise inactive-highlight-color.
items-expandable bool true Whether the user can expand/collapse interactively.
root-is-decorated bool true Draw branch indicators at the root level.
expands-on-double-click bool true Whether dblclick on a branch toggles it (vs. activates it).
activation-mode ActivationMode double-click Mouse activation policy (single-click / double-click).
hover-highlight bool true Highlight the item under the cursor.
page-size int -1 PageUp/PageDown step; -1 β‡’ heuristic 10.

Callbacks

Callback Signature When
current-changed (int id) Current item changed (click or keyboard).
item-clicked (int id) Row clicked.
item-double-clicked (int id) Row double-clicked.
item-activated (int id) Row activated (per activation-mode, or Enter, or dblclick on a leaf).
item-expand-requested (int id) Request to expand a branch.
item-collapse-requested (int id) Request to collapse a branch.
recursive-expand-requested (int id, bool expand, int depth) Recursive expand/collapse (depth == -1 = unlimited).
current-parent-change-requested (int id, int parent-id) Left-arrow on a collapsed branch / leaf: host should move current up.
custom-context-menu-requested (int id, length x, length y) Right-click.
key-press-event (KeyEvent) -> EventResult Any unmapped key. Return accept to handle, reject to bubble.

Keyboard bindings

Key Action
↑ / ↓ Move current by 1 (clamped; no wrap).
PageUp / PageDown Move current by page-size (or 10).
Home / End First / last item.
← Collapse a branch if expanded, else emit current-parent-change-requested to move up.
β†’ / + Expand a collapsed branch.
* Recursive expand (recursive-expand-requested).
- Collapse the branch unconditionally.
Enter Activate the item (gated on enabled).
other Forwarded to key-press-event.

Theming

Set the style on the instance. Every property of TreeViewStyle is mirrored as an in property on TreeView itself:

import { TreeView } from "@TreeView";

TreeView {
    highlight-color: #dbeafe;
    highlighted-text-color: #1e40af;
    row-height: 24px;
    indentation: 16px;
}

That is also the way to bind the tree to your own theme, since a global from one compilation unit can be read by another component in the same unit:

import { MyTheme } from "theme.slint";

TreeView {
    highlight-color: MyTheme.selection-bg;
    text-color: MyTheme.text;
}

From Rust, set it through the component that owns the tree β€” expose a property on your window and bind it, or set the TreeView directly if it is your root component.

Why not the global?

The TreeViewStyle global supplies the defaults and nothing more. It is not a supported way for a consumer to theme the widget, because neither route to it works:

  • TreeViewStyle::get(&my_window) from Rust resolves to your own compilation unit's globals table, not the one this library module reads. The setters return successfully and change nothing.
  • A global cannot be re-opened from a consumer .slint at all β€” that is a parse error, not an override.

Both were previously documented here and neither worked; see CHANGELOG 0.3.0.

Category Property Default
Colors background-color Palette.background
text-color Palette.foreground
disabled-text-color Palette.alternate-foreground
highlight-color / highlighted-text-color Palette.selection-background / Palette.selection-foreground
inactive-highlight-color / inactive-highlighted-text-color #e8e8e8 / black
hover-color #f0f7ff
branch-indicator-color #555
branch-line-color transparent (sets the indent strip's bg; real L/T guide lines are roadmap)
drop-indicator-color #2266aa (reserved for future DnD)
Dimensions row-height 28px
indentation 20px
branch-indicator-width 16px
decoration-spacing 6px
horizontal-padding StyleMetrics.layout-padding
Opacity disabled-opacity 0.6 (multiplied onto disabled rows on top of the greyed text color)
Glyphs expanded-branch-indicator β–Ό
collapsed-branch-indicator β–Ά

Behavior flags (items-expandable, root-is-decorated, expands-on-double-click, activation-mode, hover-highlight, page-size) are per-instance properties on TreeView, not on the global β€” they can differ between TreeViews in the same app. The global only carries theme colors / dimensions / glyphs.

Palette awareness (v0.2.0+): six semantic defaults above (background-color, text-color, disabled-text-color, highlight-color / highlighted-text-color, horizontal-padding) are sourced from Slint's Palette and StyleMetrics globals. The widget therefore follows the host application's theme automatically β€” including light/dark mode via Palette.color-scheme β€” with zero host wiring. Setting any of these properties on a TreeView instance still overrides the theme, exactly as before.

Examples

examples/basic/ is a minimal standalone crate (it carries its own Cargo.lock and is intentionally not part of the root workspace) that pulls in slint-tree-view and wires up a tiny interactive tree. It demonstrates the four things every consumer has to do:

Run it from inside the example directory:

cd examples/basic
cargo run

(cargo run -p tree-view-basic-example from the repo root does not work β€” the example is workspace-excluded by design, so the root has no package by that name to dispatch to. Use cd or, as a one-liner from the root, cargo run --manifest-path examples/basic/Cargo.toml.)

Slint language limitations (and how we work around them)

A few behaviors are impossible to implement inside the .slint file because the Slint language is declarative and intentionally limited. The workarounds all push the work to the host via callbacks:

  • No while loops, no mutable locals. Slint let bindings are immutable; there is no var or while. This means the "skip past non-selectable items" walk cannot be expressed. step-current (the Up/Down handler) moves by exactly 1; the host's current-changed handler sees whatever landed and can adjust.
  • No array search. "Find the item whose internal-id == X" needs a loop. So current-parent-change-requested is delegated to the host: the row caches parent-internal-id, and the component emits it directly. The host (which has its own index structure) finds the parent's index and updates current-index. Same story for finding an item by id from Rust β€” use the provided index_of_id / index_of_id_model helpers.
  • No viewport-size introspection. Slint's ListView doesn't expose the visible-row count from outside, so PageUp/PageDown use page-size (default heuristic 10).

Roadmap

Likely additions as the need arises (contributions welcome):

  • accessible-expanded / accessible-disabled bindings as soon as Slint supports them on AccessibleRole::list-item (currently a 1.17 gap).
  • Multi-selection. selected-internal-ids: [int] alongside current-index, plus a selection-mode flag (single / multi / extended). Shift+Click and Ctrl+Click would arrive via the existing key-press-event callback (Slint reports modifier state on the KeyEvent).
  • Multi-column support. Single-column for v1; adding a columns: [TreeColumn] property (header label + per-item cell) is the natural extension.
  • Drag & drop reorder. Per-item Rectangle already has the structure to host a DropArea; what's missing is the protocol for the host to express "this item accepts drops onto / between". auto-expand-delay-ms would fold in once DnD exists.
  • Inline editing (F2). When editing-id is set, swap the item's Text for a LineEdit and emit edit-committed(id, new-text) on focus loss / Enter.
  • Real branch guide lines. branch-line-color currently colors the whole indent strip; drawing proper L/T guide lines needs a Path overlay.
  • Animations. animated flag for expand/collapse transitions.
  • Right-to-left layout. Currently hardcoded LTR.

Non-goals

  • Lazy loading / on-demand item materialization. Slint's ListView doesn't expose a "give me items N..M" hook, so true lazy loading isn't possible without changing the model contract. For 10k+ item trees, the host should filter at the source.
  • Custom cell renderers / delegates. If you need rich per-item UI (buttons, progress bars), wrap TreeView in your own component that overlays them.

Acknowledgments

  • The Slint team for the toolkit and the experimental-module-builds feature.
  • KDAB's article on building reusable Slint UI libraries β€” the inspiration for packaging this as a crate.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages