Skip to content

Data and Signals

ssbaxys edited this page Oct 26, 2026 · 1 revision

How to bind data to the UI so it updates by itself.

Signal<Integer> coins = Signal.of(120);
coins.set(150);
coins.update(c -> c + 10);
int now = coins.peek();                                  // read without subscribing

ReadableSignal<String> text = coins.map(c -> c + " coins");      // derived value
Computed<Boolean> rich = new Computed<>(() -> coins.get() > 1000);

Label.bound(text);                  // the label updates itself
new Toggle(Signal.of(true));        // input widgets read from and write to a signal
Subscription sub = coins.subscribe(c -> save(c));  // sub.close() to unsubscribe

Widgets subscribe to signals only while they are in the tree and unsubscribe on removal, so nothing leaks. Signals are single-threaded: change them on the render thread (for example via Minecraft.getInstance().execute(...)).

For large data: VirtualList<T> (tens of thousands of rows), DataView<T> (search, filters, sorting, pages), Table<T>, TreeView.

A list with tens of thousands of rows (VirtualList<T>)

Rows have the same height; only the visible part is laid out and drawn, so 10,000 or 100,000 entries cost as much as ten. Scrolling is smooth (a spring; a long jump with Home/End snaps close to the target and animates the last screen), with the wheel and a scrollbar; ↑/↓, PageUp/PageDown, Home/End move the selection, Enter and a double click call onActivate.

// Rows are drawn straight onto the Canvas, without a single element per row.
VirtualList<Item> list = new VirtualList<Item>(18, (c, item, i, x, y, w, h, row) -> {
            c.drawItem(item.id(), x + 2, y + 1, 16);
            c.drawText(item.name(), x + 22, y + 5, row.textColor(), false);   // row.selected(), row.hovered()
        })
        .bind(view.visible())            // or .items(list)
        .emptyText("Nothing found")
        .onActivate(this::give)
        .height(Dim.px(180));
list.selection().subscribe(this::preview);   // the selected entry; selection().set(x) selects from outside
VirtualList.ofText(16, Item::name);          // plain text

// Widget rows: as many are created as are visible (+1); scrolling puts other entries into them.
new VirtualList<Item>(24, null).rows(ItemRow::new, (row, item, index) -> row.show(item));

Methods: items(), refresh() (after editing the list in place), select(i), selectedIndex(), scrollTo(y, animate), ensureVisible(i, animate), firstVisible()/lastVisible(), visibleRows(n) (height without an explicit size), zebra(false). Style: virtual-list.

Search, filters, sorting and pages (data.DataView<T>)

A view over a list: the results are signals that VirtualList, Table and Paginator read.

DataView<Item> view = new DataView<>(items)            // or DataView.of(signalWithList)
        .searchBy(i -> i.name() + " " + i.category())  // search: all query words, ignoring case
        .sort(Comparator.comparing(Item::name))        // sort(c, true) descending, sort(null) data order
        .pageSize(50);                                 // 0 means no pages
new TextField(view.query());                           // the search line is a plain Signal<String>
view.setFilter("blocks", Item::isBlock);               // a named filter; setFilter("blocks", null) removes it
view.filterWhen("rare", onlyRare, i -> i.rarity() > 1);    // active while the signal is true
view.visible();   // entries on the current page    view.filtered();  // all matches    view.total();  // how many
view.page(); view.pageCount(); view.nextPage(); view.previousPage(); view.dispose();

A new query or filter goes back to the first page; an out-of-range page number is clamped. Search strings are lowercased once when the data changes.

Page switcher (Paginator): "« ‹ 1 … 4 5 6 … 20 › »", the current page highlight slides with a spring, ←/→/Home/End and clicks. new Paginator(view) or new Paginator(page, pageCount); siblings(2), edgeArrows(false). Style: paginator.

Table (Table) draws only the rows in the visible area (the ScrollPanel window, the screen edge), so a table with thousands of rows in a scroll panel stays fast. table.bind(view) shows the view's current page, and clicking a header sorts the whole view; table.bind(signal) takes rows from any signal.


← Animations · English home · Events and Input →

Clone this wiki locally