Variable-height lists
A chat-grade virtualized list whose items have different heights, one design from 100 messages to 10 million. It has a two-level height index, scrolling anchored to an item, wrapped-text measuring on demand and a file-backed source, and is drawn the way Blender text view is drawn.
- Rust
A chat is the hard case for a list. Every message has its own height (a one-word reply, a paragraph, a screenshot), the height depends on how wide the window is, new messages arrive at the bottom while you read older ones, and history loads at the top. A list that only handles rows of equal height can’t do it, and one that keeps a height for every message stops being cheap when there are ten million of them.
blenderui-vlist is one design for all of it. A hundred messages and ten million take the same code path. Only what you scrolled through costs memory, a frame paints only the messages in view straight to the screen, and what you are looking at doesn’t move when the heights around it change.


examples/site_chat.rs).A chat in 20 lines
You give the list your messages and keep its state in an entity. chat_list is the element; a click on a message selects it.
use blenderui::prelude::*;use blenderui::vlist::{chat_list, synth, theme_palette, ChatList, ChatStyle, ListState, Options, VecSource};
struct Chat { list: Entity<ListState<VecSource, ChatStyle>> }
impl Render for Chat { fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { rsx! { <div class="w-full h-full"> {chat_list(&self.list)} </div> } }}
// when opening the window: open at the newest message, follow new oneslet opts = Options { start_at_end: true, ..Options::default() };let messages = ChatList::from_messages(synth::messages(0, 1_000_000), theme_palette(), 760.0, 436.0, opts);let list = cx.new(|cx| ListState::new(messages, cx));// from anywhere: list.update(cx, |s, _| s.push(message));use blenderui::prelude::*;use blenderui::vlist::{chat_list, synth, theme_palette, ChatList, ChatStyle, ListState, Options, VecSource};
struct Chat { list: Entity<ListState<VecSource, ChatStyle>> }
impl Render for Chat { fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { div().size_full().child(chat_list(&self.list)) }}
let opts = Options { start_at_end: true, ..Options::default() };let messages = ChatList::from_messages(synth::messages(0, 1_000_000), theme_palette(), 760.0, 436.0, opts);let list = cx.new(|cx| ListState::new(messages, cx));// from anywhere: list.update(cx, |s, _| s.push(message));A Message is an id, a time, a sender, some flags, an optional image height and the text. Whether a message starts a new run (and shows the avatar and name) or a new day (and shows a date divider) is decided when you push it and kept in the message’s flags, so a message’s height depends on that message alone and any message can be measured without its neighbours.
The same crate lists anything else: build a List from your own ItemSource (where the items come from), Measurer (how an item lays out) and ItemPainter (how it looks).
One index for every size
The core of the list is HeightIndex, which answers two questions fast: which item is at this pixel offset, and how far down is this item. Blender’s own text view answers them by walking and wrapping lines every frame (textview_draw in space_info/textview.cc), which is linear in the number of lines. A table of running totals (OffsetIndices, BLI_offset_indices.hh) is cheap to read but has to be rebuilt whenever one height changes. The index here is the updatable version of the second idea, in two levels:
top level: a Fenwick tree over blocks spare slots ........................ spare slots [ . . . ][ B0 ][ B1 ][ B2 ] ... [ Bk ][ . . . ] | | | | each block holds up to 1024 consecutive items: Uniform count, one height (equal rows) Estimated count, text bytes (never looked at) Measured prefix sums, one bit/item (scrolled through)- Blocks are cheap until you look at them. A block nobody scrolled to is a count and a byte total, about 40 bytes for 512 messages. Looking at it measures the messages in view and turns that one block into prefix sums (4 bytes per message). Memory follows what was scrolled through, not the length.
- One block is a flat array. A list of up to 1024 items is a single block and the tree is skipped, so a hundred messages cost what a plain array of running totals costs.
- Equal rows cost nothing. A list built with
ListView::uniformisUniformblocks: ten million 24 px rows are a few hundred kilobytes, and a block turns into prefix sums only when an item of a different height arrives. - Both ends are cheap. The tree has spare slots on both sides, so adding a page of older history or a new message adds a block without shifting anything; the tree doubles when a side fills up.
- A height change costs O(log blocks). The new height adds a delta to the block’s running totals, then up the tree. Setting many at once (
set_heights) touches each block once. - Heights are integers. One unit is 1/64 px, in a
u32per item and au64for offsets, so ten million 60 px rows (600 million px) stay exact;f32stops being exact at about 16 million px. Bad input (NaN, negative, huge) is clamped at the one function that converts. - A new width forgets, lazily. Wrapped text has different heights at another width, so a resize turns every measured block back into an estimate (and releases its memory); the messages in view are measured again as they are drawn.
Estimates are not guesses: pad + lines x line height, with the lines worked out from the text bytes and the wrap width, and a calibration factor that every real measurement adjusts. The scrollbar of a list nobody has scrolled yet is about right, and converges as you read.
Scrolling that doesn’t jump
The position isn’t a pixel offset. It is an anchor: the item at the top of the viewport and how far into it the viewport starts. Items are drawn relative to the anchor in small coordinates, which gives the behaviour a chat needs:
- A height changing above the viewport (an image arrived, a block got measured) can’t move what you see, because nothing you see is positioned from there.
- Prepending a page of older history doesn’t jump: the anchor moves with its item.
- Stick to bottom keeps the end of the list at the bottom edge while new messages arrive, stops when you scroll up, and starts again when you scroll back to the end. It is a second kind of position (the end, with no anchor), so opening a ten-million-message chat at the newest message touches only the last screenful.
- Streaming text into the last message re-measures one message per frame, however many tokens arrived.
jump_to(item, Align::Center)(orjump_to_id) takes O(log blocks) plus one page, and re-aims each frame until the target is measured, so estimates between here and there can’t leave it off.- Dragging the scrollbar far picks an anchor from the estimate. Measuring what lands there doesn’t move the anchor, so nothing fights the pointer; only the thumb adjusts.
The keyboard (PageUp, PageDown, arrows, Home, End), the wheel and trackpad (a notched wheel eases in over a few frames, a trackpad applies directly) and a Blender-style scrollbar are in the core, so every framework behaves the same. An idle list asks for no frames.
Measuring without laying out
Heights come from a Measurer: how wide an item’s text may be, and how tall everything that isn’t text is. The wrapping is Blender’s textview_wrap_offsets in pixels. It makes one pass over the text and records where each line starts, and the result is cached per text, width and size in the same bounded, generation-tagged cache the data table uses for text runs (Blender’s glyph cache design). Plain ASCII, which is nearly all chat, is measured from a per-character advance table filled lazily from your text engine, so a message costs a table lookup per byte rather than a call into the shaper. Other characters are measured one at a time and remembered.
A frame measures what is in view and nothing else. While you fling, it has a budget (half a millisecond by default); messages it couldn’t measure keep their estimated height and are drawn as a skeleton, and the next frames catch up. When the list is idle, prefetch measures ahead of the scroll direction, so a flick finds the next screens measured. Images and embeds carry their height in the record, so their loading shifts nothing.
Where the items come from
An ItemSource is len, the text bytes in a range (so an estimate needs no per-item data), fetch of a range into a reusable buffer, and the id of an item and the item of an id. ensure_loaded and cancel are the hooks for items that load asynchronously: the list asks for what is on screen only when you are not flinging, and cancels what it no longer needs. Two sources are included:
VecSource: messages in memory, withpush,prepend,set_textandremove.log::LogSource: a file-backed, append-only log. Each message is a fixed 16-byte record plus text in an arena, read with positioned reads (seek_readon Windows,read_aton Unix) so a read costs the same at message 10 as at message 10 million. Building the index over it needs two 8-byte reads per block, because the byte total of a range is a subtraction. Ten million messages are 1.1 GB on disk and a few megabytes in memory.
Measured
What these benchmarks are
Every chart below answers one question: how much does it cost to show a long chat, and does that cost grow with the number of messages? The chat is a list of messages of different heights (wrapped text, images, run headers), scrolled continuously in a real window.
- The three libraries.
blenderui-vlistis this crate.gpui listis the variable-height list that ships with gpui.gpui-component virtual_listis the one in gpui-component 0.9.0. All three show the same messages in the same window on the same gpui. - The sizes. 100 messages is a normal chat. 1,000,000 and 10,000,000 are the limit cases: an archive, a log viewer, a support inbox.
- Scroll frame. The time the main thread spends from the start of a frame’s render to the end of paint while the list is being scrolled. It leaves out the GPU and the wait for the screen’s refresh. Lower is smoother; a 60 Hz screen allows 16.7 ms.
- Memory above an empty window. How much the process grows when it builds and shows the list, not counting the messages themselves. At 10M messages the messages are a 1.1 GB file on disk that every library reads from, so this is the cost of the list’s own bookkeeping (positions, heights, caches). An app that keeps its messages in memory pays for them on top, in any library.
- Setup time. From handing the list its messages to being ready to draw the first frame.
- The index chart compares the list’s height index with the simplest approach, a flat array of running totals, to show what each costs when one message’s height changes (an image loads, text is edited, the window is resized).
All numbers are on one laptop (below), median of 3 runs, release builds. They show how the libraries scale on that machine, not absolute speed on yours; the code to rerun them is in blenderui-vlist/examples and blenderui-vlist-gpui.
Results
All numbers are on a Ryzen 7 5800U laptop (8 cores, 16 GB), Windows 11, release builds, on AC power with other programs open. They are medians of 3 runs, one process per run. The code is in blenderui-vlist/examples and blenderui-vlist-gpui.
The index against a flat Vec of running totals (bench_core), the simplest correct implementation. Both are O(1) per item to read; the flat Vec pays O(n) to change a height.
Changing one item's height
The height index against a flat Vec of running totals, release build, median of 5
Lower is better
20,000xcheaper than a flat array at 10M items
- HeightIndex
- Flat Vec
- 10,000
- HeightIndex: 53 ns
- Flat Vec: 684 ns
- 1,000,000
- HeightIndex: 112 ns
- Flat Vec: 81,849 ns
- 10,000,000
- HeightIndex: 182 ns
- Flat Vec: 3,825,956 ns
Open benchClose bench
| Items | What | HeightIndex | Flat Vec |
|---|---|---|---|
| 100 | A frame's lookups: find an item, read 30 heights | 53 ns | 34 ns |
| 100 | Set one height / set the last item | 22 / 11 ns | 15 / 1.4 ns |
| 10,000 | Set one height, random item | 53 ns | 684 ns |
| 10,000 | Prepend a 500-item page | 0.3 us | 17 us |
| 1,000,000 | Item at a pixel offset (all measured, random) | 136 ns | 63 ns |
| 1,000,000 | Set one height, random item | 112 ns | 81,849 ns |
| 1,000,000 | Prepend a 500-item page | 0.1 us | 1,838 us |
| 1,000,000 | Build with nothing measured / heap per item | 0.19 ms / 0.13 B | needs every height / 8 B |
| 1,000,000 | Heap after touching only the last 60 items | 189 KB | 8 MB |
| 10,000,000 | Item at a pixel offset (all measured, random) | 536 ns | 387 ns |
| 10,000,000 | Set one height, random item | 182 ns | 3,825,956 ns |
| 10,000,000 | Prepend a 500-item page | 0.1 us | 26,647 us |
| 10,000,000 | Build with nothing measured / heap per item | 0.86 ms / 0.12 B | needs every height / 8 B |
Single-threaded, inputs seeded. Heap is live bytes from a counting allocator. Reading is where the flat array wins (about 1.5 times faster to find an item by offset when everything is measured, and far faster at 100 items in absolute ns); changing anything is where it loses. Not measured: other machines.
Source: bindings/rust/blenderui-vlist/examples/bench_core.rs · Ryzen 7 5800U laptop, Windows 11, on AC power
At 100 items, the one-block index costs about 20 ns more per frame than the flat array (a frame’s lookups: find the first item and read 30 heights: 53 ns against 34 ns), a few thousandths of a percent of a 60 Hz frame. From 10,000 items, changing one height or adding a page of history is 13 to 20,000 times cheaper than the flat array, and the untouched list costs about 0.1 byte per item.
Whole frames of the core (bench_frames): layout and paint of the chat into a counting sink, so this is the list’s own cost, without a text engine or a rasterizer (the adapters add theirs).
| Items (source) | Index build | First frame | Scroll frame p50 / p99 | Settled frame p50 | Jump p50 | Allocations per frame (scroll / settled) | Working set |
|---|---|---|---|---|---|---|---|
| 100 (memory) | 0.0 ms | 50 us | 4.0 / 4.9 us | 4.0 us | 3.9 us | 0.02 / 0 | 2 MB |
| 10,000 (memory) | 0.3 ms | 48 us | 4.7 / 10.9 us | 3.5 us | 13 us | 2.2 / 0 | 5 MB |
| 1,000,000 (memory) | 19 ms | 61 us | 4.7 / 11.0 us | 3.8 us | 27 us | 2.2 / 0 | 205 MB (the messages) |
| 1,000,000 (file log) | 8 ms | 71 us | 4.9 / 15 us | 4.1 us | 37 us | 2.2 / 0 | 2 MB |
| 10,000,000 (file log) | 79 ms | 78 us | 4.8 / 19 us | 2.7 us | 36 us | 2.3 / 0 | 3.6 MB |
The sink only counts calls and the text width is a length times a constant, so this is the list's own cost, not a text engine's or a rasterizer's. The scroll allocations are the two a newly measured message needs (its cache entry); a frame that shows messages already measured allocates nothing. The file log was just written, so its pages come from the operating system's cache. Streaming a token into the last message (edit, layout, paint) takes about 20 us.
On real gpui, against gpui’s own list and gpui-component’s virtual_list (bench_list_gpui): the same chat messages, the same window, one process per library and size. A frame is the main thread’s time from the start of the root view’s render to the end of paint; GPU submission and the 60 Hz wait are not in it.
Variable-height list: scroll frame (median)
Real gpui window, chat messages, main thread from render to end of paint, median of 3 runs
Lower is better
about 2xfaster per scrolled frame at 100, 1M and 10M messages
- blenderui-vlist
- gpui list
- gpui-component virtual_list
- 100
- blenderui-vlist: 0.54 ms
- gpui list: 1 ms
- gpui-component virtual_list: 1.04 ms
- 1,000,000
- blenderui-vlist: 0.71 ms
- gpui list: 1.32 ms
- gpui-component virtual_list: 1.3 ms
- 10,000,000file log
- blenderui-vlist: 0.74 ms
- gpui list: 1.44 ms
- gpui-component virtual_list: 1.3 ms
Open benchClose bench
| Items | Library | Setup | First frame | Scroll frame p50 / p95 / p99 | Allocations per frame | RAM above the empty window | Jump p50 |
|---|---|---|---|---|---|---|---|
| 100 | blenderui-vlist | 0 ms | 508 ms | 0.54 / 0.84 / 0.92 ms | 134 | 0 MB | 0.53 ms |
| gpui list | 0 ms | 515 ms | 1.00 / 1.39 / 1.47 ms | 793 | 0 MB | 1.83 ms | |
| virtual_list | 0.1 ms | 496 ms | 1.04 / 1.46 / 1.56 ms | 923 | 0 MB | 1.94 ms | |
| 10,000 | blenderui-vlist | 0 ms | 501 ms | 0.69 / 1.12 / 1.46 ms | 152 | 5 MB | 1.41 ms |
| gpui list | 0.6 ms | 506 ms | 1.30 / 1.70 / 1.86 ms | 939 | 1 MB | 2.60 ms | |
| virtual_list | 8.4 ms | 507 ms | 1.26 / 1.72 / 1.85 ms | 1,035 | 0 MB | 2.10 ms | |
| 1,000,000 | blenderui-vlist | 0.3 ms | 224 ms | 0.71 / 1.16 / 1.39 ms | 152 | 2 MB | 1.47 ms |
| gpui list | 58 ms | 371 ms | 1.32 / 1.78 / 1.89 ms | 982 | 86 MB | 2.58 ms | |
| virtual_list | 628 ms | 836 ms | 1.30 / 1.76 / 1.91 ms | 1,091 | 10 MB | 2.15 ms | |
| 10,000,000 (file log) | blenderui-vlist | 82 ms | 591 ms | 0.74 / 1.22 / 1.44 ms | 153 | 6 MB | 1.54 ms |
| gpui list | 537 ms | 1,995 ms | 1.44 / 1.87 / 2.03 ms | 957 | 910 MB | 2.82 ms | |
| virtual_list | 7,285 ms | 7,843 ms | 1.30 / 1.82 / 2.17 ms | 984 | 151 MB | 2.23 ms |
Ryzen 7 5800U laptop, 16 GB, Windows 11, Direct3D 11. Frame time is the main thread from the start of the root view's render to the end of paint; GPU submission and the 60 Hz wait are not in it (all three show 60 FPS). Scrolling is 57 px per frame, driven the same way for all three; the empty window with the same harness takes 0.13 ms and 102 allocations per frame. RAM is the working set after the scroll minus the same window with 100 items (about 29 MB) and minus the data. Nobody hit the 4 GB or 150 s limits, so there are no failures to report: gpui's list held ten million items in 910 MB and virtual_list took 7.3 s to build its size table. virtual_list's sizes come from a cheap synthetic text width, which favours it. Our first frame at 100 and 10,000 items is window creation. Not measured: GPU time, other machines, selection and keyboard latency, streaming updates on the competitors. The machine was in normal use during the runs.
Source: bindings/rust/blenderui-vlist-gpui/bench/results.md · Ryzen 7 5800U laptop, Windows 11, on AC power
Variable-height list: memory above an empty window
Real gpui window: how much the process grows for the list, not counting the messages (at 10M they are a file on disk)
Lower is better
6 MBfor 10 million messages (gpui list: 910 MB)
- blenderui-vlist
- gpui list
- gpui-component virtual_list
- 100
- blenderui-vlist: 0 MB
- gpui list: 0 MB
- gpui-component virtual_list: 0 MB
- 1,000,000
- blenderui-vlist: 2 MB
- gpui list: 86 MB
- gpui-component virtual_list: 10 MB
- 10,000,000file log
- blenderui-vlist: 6 MB
- gpui list: 910 MB
- gpui-component virtual_list: 151 MB
Open benchClose bench
| Items | Library | Setup | First frame | Scroll frame p50 / p95 / p99 | Allocations per frame | RAM above the empty window | Jump p50 |
|---|---|---|---|---|---|---|---|
| 100 | blenderui-vlist | 0 ms | 508 ms | 0.54 / 0.84 / 0.92 ms | 134 | 0 MB | 0.53 ms |
| gpui list | 0 ms | 515 ms | 1.00 / 1.39 / 1.47 ms | 793 | 0 MB | 1.83 ms | |
| virtual_list | 0.1 ms | 496 ms | 1.04 / 1.46 / 1.56 ms | 923 | 0 MB | 1.94 ms | |
| 10,000 | blenderui-vlist | 0 ms | 501 ms | 0.69 / 1.12 / 1.46 ms | 152 | 5 MB | 1.41 ms |
| gpui list | 0.6 ms | 506 ms | 1.30 / 1.70 / 1.86 ms | 939 | 1 MB | 2.60 ms | |
| virtual_list | 8.4 ms | 507 ms | 1.26 / 1.72 / 1.85 ms | 1,035 | 0 MB | 2.10 ms | |
| 1,000,000 | blenderui-vlist | 0.3 ms | 224 ms | 0.71 / 1.16 / 1.39 ms | 152 | 2 MB | 1.47 ms |
| gpui list | 58 ms | 371 ms | 1.32 / 1.78 / 1.89 ms | 982 | 86 MB | 2.58 ms | |
| virtual_list | 628 ms | 836 ms | 1.30 / 1.76 / 1.91 ms | 1,091 | 10 MB | 2.15 ms | |
| 10,000,000 (file log) | blenderui-vlist | 82 ms | 591 ms | 0.74 / 1.22 / 1.44 ms | 153 | 6 MB | 1.54 ms |
| gpui list | 537 ms | 1,995 ms | 1.44 / 1.87 / 2.03 ms | 957 | 910 MB | 2.82 ms | |
| virtual_list | 7,285 ms | 7,843 ms | 1.30 / 1.82 / 2.17 ms | 984 | 151 MB | 2.23 ms |
Ryzen 7 5800U laptop, 16 GB, Windows 11, Direct3D 11. Frame time is the main thread from the start of the root view's render to the end of paint; GPU submission and the 60 Hz wait are not in it (all three show 60 FPS). Scrolling is 57 px per frame, driven the same way for all three; the empty window with the same harness takes 0.13 ms and 102 allocations per frame. RAM is the working set after the scroll minus the same window with 100 items (about 29 MB) and minus the data. Nobody hit the 4 GB or 150 s limits, so there are no failures to report: gpui's list held ten million items in 910 MB and virtual_list took 7.3 s to build its size table. virtual_list's sizes come from a cheap synthetic text width, which favours it. Our first frame at 100 and 10,000 items is window creation. Not measured: GPU time, other machines, selection and keyboard latency, streaming updates on the competitors. The machine was in normal use during the runs.
Source: bindings/rust/blenderui-vlist-gpui/bench/results.md · Ryzen 7 5800U laptop, Windows 11, on AC power
Variable-height list: setup time
From handing the list its messages to being ready to draw, real gpui, median of 3 runs
Lower is better
7.3 s to 82 msbuilding the 10M list (virtual_list vs ours)
- blenderui-vlist
- gpui list
- gpui-component virtual_list
- 100
- blenderui-vlist: 0 ms
- gpui list: 0 ms
- gpui-component virtual_list: 0.1 ms
- 1,000,000
- blenderui-vlist: 0.3 ms
- gpui list: 58 ms
- gpui-component virtual_list: 628 ms
- 10,000,000file log
- blenderui-vlist: 82 ms
- gpui list: 537 ms
- gpui-component virtual_list: 7,285 ms
Open benchClose bench
| Items | Library | Setup | First frame | Scroll frame p50 / p95 / p99 | Allocations per frame | RAM above the empty window | Jump p50 |
|---|---|---|---|---|---|---|---|
| 100 | blenderui-vlist | 0 ms | 508 ms | 0.54 / 0.84 / 0.92 ms | 134 | 0 MB | 0.53 ms |
| gpui list | 0 ms | 515 ms | 1.00 / 1.39 / 1.47 ms | 793 | 0 MB | 1.83 ms | |
| virtual_list | 0.1 ms | 496 ms | 1.04 / 1.46 / 1.56 ms | 923 | 0 MB | 1.94 ms | |
| 10,000 | blenderui-vlist | 0 ms | 501 ms | 0.69 / 1.12 / 1.46 ms | 152 | 5 MB | 1.41 ms |
| gpui list | 0.6 ms | 506 ms | 1.30 / 1.70 / 1.86 ms | 939 | 1 MB | 2.60 ms | |
| virtual_list | 8.4 ms | 507 ms | 1.26 / 1.72 / 1.85 ms | 1,035 | 0 MB | 2.10 ms | |
| 1,000,000 | blenderui-vlist | 0.3 ms | 224 ms | 0.71 / 1.16 / 1.39 ms | 152 | 2 MB | 1.47 ms |
| gpui list | 58 ms | 371 ms | 1.32 / 1.78 / 1.89 ms | 982 | 86 MB | 2.58 ms | |
| virtual_list | 628 ms | 836 ms | 1.30 / 1.76 / 1.91 ms | 1,091 | 10 MB | 2.15 ms | |
| 10,000,000 (file log) | blenderui-vlist | 82 ms | 591 ms | 0.74 / 1.22 / 1.44 ms | 153 | 6 MB | 1.54 ms |
| gpui list | 537 ms | 1,995 ms | 1.44 / 1.87 / 2.03 ms | 957 | 910 MB | 2.82 ms | |
| virtual_list | 7,285 ms | 7,843 ms | 1.30 / 1.82 / 2.17 ms | 984 | 151 MB | 2.23 ms |
Ryzen 7 5800U laptop, 16 GB, Windows 11, Direct3D 11. Frame time is the main thread from the start of the root view's render to the end of paint; GPU submission and the 60 Hz wait are not in it (all three show 60 FPS). Scrolling is 57 px per frame, driven the same way for all three; the empty window with the same harness takes 0.13 ms and 102 allocations per frame. RAM is the working set after the scroll minus the same window with 100 items (about 29 MB) and minus the data. Nobody hit the 4 GB or 150 s limits, so there are no failures to report: gpui's list held ten million items in 910 MB and virtual_list took 7.3 s to build its size table. virtual_list's sizes come from a cheap synthetic text width, which favours it. Our first frame at 100 and 10,000 items is window creation. Not measured: GPU time, other machines, selection and keyboard latency, streaming updates on the competitors. The machine was in normal use during the runs.
Source: bindings/rust/blenderui-vlist-gpui/bench/results.md · Ryzen 7 5800U laptop, Windows 11, on AC power
What is shared
The list is split so that nothing is written twice:
| Crate | What it holds |
|---|---|
blenderui-paint |
Geometry and colors, the PaintSink trait a framework implements once, the theme Palette, the bounded TextCache, and the test helpers. Both view cores depend on it. |
blenderui-table |
The data table core. |
blenderui-vlist |
The height index, the view (anchor, frame, scrolling), the measuring and wrapping, the sources and the chat look. std only. |
blenderui (vlist, sink) |
The adapter: an entity, an element, the engine’s PaintSink (shared with the table), input events. |
blenderui-paint-gpui, blenderui-vlist-gpui |
The gpui PaintSink (shared with the table) and the gpui element. Outside the main workspace because gpui is a huge build. |
An adapter holds no list logic. It owns a List, forwards pointer, wheel and key events, lays a frame out while rendering (which measures, so it needs the text engine and mutable access) and paints from the result with read-only access.
Limitations
- Selecting text across messages is not built; whole messages can be selected.
- A background thread measuring ahead is not built. Prefetch runs on the UI thread in idle time, under a budget. Results are applied between frames, so nothing races, but a very slow shaper still spends UI time.
- Damage regions. A scroll repaints the list’s whole canvas. Reusing the previous frame and painting only the newly exposed strip needs engine support the adapters don’t use yet.
- Complex scripts are measured per character in the fast path and shaped properly when painted by the engine, so a line of Arabic or emoji sequences can wrap a pixel or two differently from how it draws.
- One height clamp. An item can be at most 16,384 px tall.
- Group headers after a prepend. The message that was first before a page of older history was prepended keeps the header it had; recompute its flags if your history can continue a run.
- No bottom alignment for short lists. A list shorter than the viewport sits at the top.
- JavaScript and Python wrappers: coming soon. The list is built once, in Rust.
Last updated