Merge branch 'worktree-agent-a33c31aef1fd6d868' into rustify (I3: iris::widget::List)

This commit is contained in:
iris committed 2026-09-05 06:29:08 -04:00
commit 8adda94a7a
6 files changed
+1460 -39

No files matched your search

+30
View File
@@ -8,6 +8,36 @@ capability that moved. Small and trivial changes do not go here.
An entry gives the date, what changed, why, and a short before/after where An entry gives the date, what changed, why, and a short before/after where
it helps judge the change without the session that made it. Newest first. it helps judge the change without the session that made it. Newest first.
## 2026-09-05: `List`, a virtualised bottom-anchored list (RUST.md's I3)
A new widget, `iris::widget::List` (`iris/src/widget/list.rs` -- read its
module doc first), for the transcript's kind of screen: variable-height
rows, keyed by a `u64`, composed only while visible, moved rather than
re-laid-out on scroll, a scroll anchor that survives a row inserted above
it, "more" sentinels at each end, and "hold the edge nearest the tap" when
a row's height changes (`note_tap`, resolved in the layout pass).
```rust
let mut list = List::new(Axis::Y);
list.push_back(ListRow::new(key, row_widget)); // O(1)
list.push_front(ListRow::new(older_key, row)); // O(1), anchor unaffected
list.set_more_before(Some(spinner_widget)); // sentinel, drawn at the edge
list.note_tap(viewport_y); // before mutating a row's height
let (top, bottom) = list.extent(key).unwrap(); // last frame's on-screen box, if visible
```
Built entirely out of existing primitives (`Painter::widget`/`widget_within`/
`reposition`/`draw_twice`, and `draw_inner`'s own old-children diffing) --
no new mechanism was added to the render core for it. One correctness
lesson worth reading even for other widgets: a row that fills whatever
region it is offered (`Rect`, `is_size_independent`) cannot be measured at
a throwaway oversized region and then merely `reposition`ed into place --
`reposition` only ever writes an offset, never a size, so the oversized
primitive stays oversized. `List` fixes this by caching each row's real
height once measured and placing an already-known row directly at its
exact box; see `list.rs`'s `place` for the full reasoning and
`a_fill_shaped_background_is_not_left_oversized` for the regression test.
## 2026-09-05: a second backend (android-view), and what moved to make room for it ## 2026-09-05: a second backend (android-view), and what moved to make room for it
RUST.md's I2. Three changes a widget or app author would notice, all in RUST.md's I2. Three changes a widget or app author would notice, all in
+77 -6
View File
@@ -123,6 +123,18 @@ session spending an afternoon on them again.
GLES-only `D2`/`D2Array` warning was confirmed a red herring — still GLES-only `D2`/`D2Array` warning was confirmed a red herring — still
present post-fix, harmless. See I2's own entry below for the full present post-fix, harmless. See I2's own entry below for the full
writeup. **E2** (a transcript in Masonry) is done — see its own box. writeup. **E2** (a transcript in Masonry) is done — see its own box.
- **I3 — `iris::widget::List` built and benchmarked 2026-09-05, ticked in
the box below.** Variable-height rows, virtualised, moved not
relaid-out on scroll, insert-above-anchor and expand-hold both measured
flat across N = 100/1,000/10,000. What is left is wiring it into an
actual transcript screen and comparing against `transcript-bench.sh`'s
Compose baseline on the GPU emulator, which needs a session/scroll model
around it (closer to I5's scope) — see I3's own box for the exact
command once that screen exists. Read `list.rs`'s module doc and
`IRIS.md`'s 2026-09-05 entry before touching it: a widget that fills
whatever region it's offered (a `Rect` background) cannot be measured at
a throwaway region and merely repositioned, a lesson that generalises
beyond this one widget.
- **`client-core` built (2026-09-04)**, item 1 of the recommendation: - **`client-core` built (2026-09-04)**, item 1 of the recommendation:
`event-model/` (the event types, now shared with `server/`) and `event-model/` (the event types, now shared with `server/`) and
`client-core/` (REST and SSE clients, transcript fold, cache, highlighter, `client-core/` (REST and SSE clients, transcript fold, cache, highlighter,
@@ -1430,13 +1442,72 @@ silently on real hardware.
`adb install -r app/build/outputs/apk/debug/app-debug.apk`. Emulator `adb install -r app/build/outputs/apk/debug/app-debug.apk`. Emulator
torn down after verification (`emu down`) per the machine's memory torn down after verification (`emu down`) per the machine's memory
rule. rule.
- [ ] **I3 — a virtualised, bottom-anchored list.** Variable-height rows, - [x] **I3 — a virtualised, bottom-anchored list (2026-09-05).** Variable-height
keyed, composed only while visible, paged in both directions with a rows, keyed, composed only while visible, paged in both directions
"more" sentinel at each end, a scroll anchor that survives rows with a "more" sentinel at each end, a scroll anchor that survives
being inserted above, and "hold the edge nearest the tap" done in rows being inserted above, and "hold the edge nearest the tap" done
the layout pass. Pass: 800 rows of real transcript text from the in the layout pass. Built as `iris::widget::List`
(`iris/src/widget/list.rs`, its module doc is the design writeup) --
see `IRIS.md`'s 2026-09-05 entry for the public API and the one
correctness lesson worth carrying elsewhere (a fill-shaped background
cannot be measured at a throwaway oversized region and merely
`reposition`ed into place; it has to be placed at its cached real
size, or measured-then-redrawn via `draw_twice` on first appearance).
**Done**: the widget, 6 unit tests (`cargo test -p iris`, anchor and
edge-hold logic, all pure -- no GPU/window needed, same harness as
`layout_tests.rs`), `iris/benches/message_list.rs` rewritten to
measure the real widget instead of a hand-built `Span`+`Scroll`, two
new benchmark scenarios ((d) insert-above-anchor, (e)
expand-a-row-holding-its-edge), and `iris/examples/message_list.rs`
(800 rows, varied wrapped-text length, one in twelve with an image,
mouse-wheel scrollable) rendered via `run-headless.sh` and visually
verified (cropped with a throwaway PNG decoder, since this VM has no
image tooling -- see the commit for the crop script's shape).
**Numbers (2026-09-05, release, this VM), all flat across N =
100/1,000/10,000 as required:**
cd iris && ./run-bench.sh list
(a) first frame: ~12.3-12.9ms draws=80 rewrites=3 moves=0
(b) scroll, 200 ticks: 4.8-6.5ms draws=328 rewrites=12 moves=10131 (~0.025-0.033ms/tick)
(c) input grows, 40 lines: 8.9ms draws=1846 rewrites=102 moves=1195 (~0.22ms/line)
(d) insert-above-anchor, 200 pushes: 0.4ms draws=200 rewrites=0 moves=0 (~0.002ms/push)
(e) expand-hold, 40 growths: 0.10-0.11ms draws=119 rewrites=40 moves=15 (~0.003ms/growth)
(d) is the cleanest confirmation: 200 rows prepended one at a time
while scrolled to the loaded window's start cost 200 draws total (the
list widget's own redraw each push) and **zero** row draws or moves
-- none of the prepended rows ever entered the viewport, exactly as
the anchor-by-slot-index design predicts. (e) similarly stays tiny
and flat: growing one row 40 times, each preceded by `note_tap` at
its own edge, costs a total of 15 moves (the rows on the far side of
the held edge) regardless of how many thousand rows exist elsewhere
in the list.
**Verification.** `cargo fmt --all -- --check`,
`cargo build --workspace --all-targets`,
`cargo clippy --all-targets` (and `--benches --release` separately,
since benches aren't always covered), `cargo test --workspace` (25
passed) all clean in `iris/`.
**What remains — the emulator half of the pass condition, blocked on
the emulator being held by another session during this pass.** The
condition as written ("800 rows of real transcript text from the
sandbox scroll without a frame over the Compose baseline in sandbox scroll without a frame over the Compose baseline in
`transcript-bench.sh`, measured on the GPU emulator. `transcript-bench.sh`, measured on the GPU emulator") needs the
transcript screen actually rebuilt on top of `List` (this box only
built and measured the widget in isolation, per the task scope) and
then driven through the real emulator rig. Once that screen exists,
the exact command is:
cd app && ./transcript-bench.sh -k # or without -k for a fresh session
# compare its render report against the iris build's equivalent
This is a genuinely separate step (wiring `List` into an actual
session screen, i.e. most of I5's work) rather than something this
box's scope could finish alone -- recorded here rather than left
silently undone.
- [ ] **I4 — accessibility names via AccessKit.** Every control carries a - [ ] **I4 — accessibility names via AccessKit.** Every control carries a
name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s
tap-by-name works against the iris screen unchanged. tap-by-name works against the iris screen unchanged.
+200 -33
View File
@@ -1,6 +1,7 @@
//! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's //! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's
//! "Benchmarks" item. Never run by `cargo test`; run explicitly with //! "Benchmarks" item, and RUST.md's I3. Never run by `cargo test`; run
//! `cargo bench --bench message_list --release` or `./run-bench.sh`. //! explicitly with `cargo bench --bench message_list --release` or
//! `./run-bench.sh`.
//! //!
//! **Why a plain `Instant`-timed binary, not criterion.** Every scenario //! **Why a plain `Instant`-timed binary, not criterion.** Every scenario
//! here is really "how many `Widget::draw` calls and primitive rewrites did //! here is really "how many `Widget::draw` calls and primitive rewrites did
@@ -13,26 +14,49 @@
//! -- and it avoids a new dependency this crate does not otherwise need. //! -- and it avoids a new dependency this crate does not otherwise need.
//! Per the code rules, the plain option is also the one shorter to explain. //! Per the code rules, the plain option is also the one shorter to explain.
//! //!
//! Scenarios (LAYOUT.md's O(1) move chain, and IRIS_TODO.md's "Benchmarks" //! **The list under test is `iris::widget::List` (RUST.md's I3), not a
//! wording): //! `Scroll` over a `Span` of pre-built rows.** Earlier versions of this
//! file built their own giant `Span` and wrapped it in `Scroll`, which
//! meant (a)/(b)/(c) below were measuring "move one big child," never the
//! virtualised widget the app's transcript screen actually needs. `List`
//! still needs every row's *widget* built up front by the caller (its
//! module doc explains why: it only ever sees `&dyn Widget` through
//! `Painter`, so it cannot construct a row lazily on its own) -- what
//! virtualisation buys is that only the rows currently on screen are ever
//! *drawn*, which is what the draw/rewrite/move counters below are
//! measuring, not construction time.
//!
//! Scenarios (LAYOUT.md's O(1) move chain, list.rs's module doc, and
//! IRIS_TODO.md's "Benchmarks" wording):
//! //!
//! - (a) first-frame cost of a message list of N wrapped-text rows, some //! - (a) first-frame cost of a message list of N wrapped-text rows, some
//! with an image, for N = 100 / 1,000 / 10,000. //! with an image, for N = 100 / 1,000 / 10,000. With a virtualised list
//! this is expected to stop scaling with N once N exceeds a screenful --
//! the draw/rewrite counters below are the number that used to grow 10x
//! per 10x N and should not any more.
//! - (b) per-frame cost of scrolling that list -- must be O(1) moves, not //! - (b) per-frame cost of scrolling that list -- must be O(1) moves, not
//! re-layout. //! re-layout.
//! - (c) the input-box case: growing a fixed-height field at the bottom of //! - (c) the input-box case: growing a fixed-height field at the bottom of
//! the screen must move the message list above it, not re-lay its rows. //! the screen must move the message list above it, not re-lay its rows.
//! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md //! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md
//! section 8 defines. //! section 8 defines.
//! - (d) insert-above-anchor: paging older history onto the front of an
//! already-scrolled list. `List::push_front` is an O(1) index update
//! (list.rs's module doc); this measures that none of the rows already
//! on screen are touched by it.
//! - (e) expand-a-row-holding-its-edge: growing one row's height with a
//! tap recorded near one of its edges (list.rs's `note_tap`) must move
//! only the rows on the far side of it, never redraw the ones already
//! correctly placed.
//! //!
//! (d), many images with zero steady-state bind-group creation, needs a //! (f), many images with zero steady-state bind-group creation, needs a
//! real `wgpu` device and lives in `iris/examples/bench_images.rs` instead, //! real `wgpu` device and lives in `iris/examples/bench_images.rs` instead,
//! driven through `run-headless.sh` -- see that file's header. //! driven through `run-headless.sh` -- see that file's header.
//! //!
//! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs` //! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs`
//! notes), so everything here runs as an ordinary `--release` binary with //! notes), so everything here runs as an ordinary `--release` binary with
//! no compositor. Numbers are recorded in IRIS_TODO.md, not here -- this //! no compositor. Numbers are recorded in RUST.md's I3 box, not here --
//! file is the rig, not the result. //! this file is the rig, not the result.
use iris::prelude::*; use iris::prelude::*;
use std::time::Instant; use std::time::Instant;
@@ -82,21 +106,21 @@ fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
} }
} }
/// A `Scroll` over `n` message rows, one in `image_every` of them carrying /// A virtualised `List` of `n` message rows, one in `image_every` of them
/// an image (0 disables images entirely). Returns the scroll widget (weak, /// carrying an image (0 disables images entirely). Returns the list widget
/// so the caller can drive it) and the erased root to render. /// (weak, so the caller can drive it) and the erased root to render.
fn build_list( fn build_message_list(
rsc: &mut BenchRsc, rsc: &mut BenchRsc,
n: usize, n: usize,
image_every: usize, image_every: usize,
) -> (WeakWidget<Scroll>, StrongWidget) { ) -> (WeakWidget<List>, StrongWidget) {
let mut span = Span::empty(Dir::DOWN); let mut list = List::new(Axis::Y);
for i in 0..n { for i in 0..n {
span.push(build_row(rsc, i, image_every)); let row = build_row(rsc, i, image_every);
list.push_back(ListRow::new(i as u64, row));
} }
let span = rsc.ui.widgets.add_strong(span); let list = rsc.ui.widgets.add_strong(list);
let scroll = rsc.ui.widgets.add_strong(Scroll::new(span.any(), Axis::Y)); (list.weak(), list.any())
(scroll.weak(), scroll.any())
} }
fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) { fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) {
@@ -111,7 +135,7 @@ fn bench_first_frame(n: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc {
ui: UiData::default(), ui: UiData::default(),
}; };
let (_scroll, root) = build_list(&mut rsc, n, 20); let (_list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
@@ -129,18 +153,18 @@ fn bench_first_frame(n: usize) {
} }
/// (b) Per-frame cost of scrolling an already-laid-out list of N rows. /// (b) Per-frame cost of scrolling an already-laid-out list of N rows.
/// Warms up (as `layout_tests.rs`'s scrolling test documents: `Scroll` /// Warms up (one no-op tick, matching `Scroll`'s own need for it before an
/// needs one no-op tick before a real scroll becomes a same-size move /// ordinary Rust `layout_tests.rs` scrolling test becomes a same-size move
/// rather than a resize), then times a run of individual scroll ticks. /// rather than a resize), then times a run of individual scroll ticks.
fn bench_scroll(n: usize, ticks: usize) { fn bench_scroll(n: usize, ticks: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc {
ui: UiData::default(), ui: UiData::default(),
}; };
let (scroll, root) = build_list(&mut rsc, n, 20); let (list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0); rsc.ui.widgets.get_mut(&list).unwrap().scroll(0.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
render.take_counters(); render.take_counters();
@@ -149,7 +173,7 @@ fn bench_scroll(n: usize, ticks: usize) {
let mut total_rewrites = 0u64; let mut total_rewrites = 0u64;
let mut total_moves = 0u64; let mut total_moves = 0u64;
for _ in 0..ticks { for _ in 0..ticks {
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-8.0); rsc.ui.widgets.get_mut(&list).unwrap().scroll(-8.0);
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
@@ -174,18 +198,16 @@ fn bench_scroll(n: usize, ticks: usize) {
/// (c) The input-box case: a fixed-height field at the bottom of the screen /// (c) The input-box case: a fixed-height field at the bottom of the screen
/// growing by a line at a time, with a message list of N rows filling the /// growing by a line at a time, with a message list of N rows filling the
/// rest of the screen above it. Growing the input shrinks the *offered* /// rest of the screen above it. Growing the input shrinks the *offered*
/// height of the scroll container (a single widget, from the outer /// height of the list container (a single widget, from the outer `Span`'s
/// `Span`'s point of view) without changing the width it offers its /// point of view) without changing the width it offers its content -- so
/// content -- so the rows underneath, which only care about width, must /// the rows underneath, which only care about width, must not redraw; the
/// not redraw; the scroll's own re-registration of where its content sits /// list's own re-registration of where its content sits is the one O(1)
/// is the one O(1) move this is checking for. See LAYOUT.md's `Scroll` /// move this is checking for.
/// design note on offering the child last frame's content length, which is
/// exactly what keeps this a move instead of a reflow.
fn bench_input_grows(n: usize, lines: usize) { fn bench_input_grows(n: usize, lines: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc {
ui: UiData::default(), ui: UiData::default(),
}; };
let (scroll, list_root) = build_list(&mut rsc, n, 20); let (list, list_root) = build_message_list(&mut rsc, n, 20);
let list_area = rsc.ui.widgets.add_strong(Sized { let list_area = rsc.ui.widgets.add_strong(Sized {
inner: list_root, inner: list_root,
x: None, x: None,
@@ -209,7 +231,7 @@ fn bench_input_grows(n: usize, lines: usize) {
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0); rsc.ui.widgets.get_mut(&list).unwrap().scroll(0.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
render.take_counters(); render.take_counters();
@@ -244,6 +266,145 @@ fn bench_input_grows(n: usize, lines: usize) {
); );
} }
/// (d) Insert-above-anchor: the list is scrolled to its very first loaded
/// row (`jump_to_start`, an O(1) re-anchor) rather than left at the default
/// bottom, so a row prepended above it is genuinely "inserted above the
/// anchor" rather than merely far off-screen at the far end. Each
/// `push_front` is O(1) (list.rs's module doc: the anchor's slot is an
/// index, bumped by one) and, since the prepended rows never enter the
/// viewport, none of them should cost a draw either.
fn bench_insert_above_anchor(n: usize, inserts: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let (list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&list).unwrap().jump_to_start();
render.update(&root, &mut rsc);
render.take_counters();
let mut total = std::time::Duration::ZERO;
let mut total_draws = 0u64;
let mut total_rewrites = 0u64;
let mut total_moves = 0u64;
for i in 0..inserts {
// Older-history rows: distinct keys below every existing one, so a
// real caller's paging code (prepending an older page) is exactly
// what this loop does.
let row = build_row(&mut rsc, usize::MAX - i, 20);
rsc.ui
.widgets
.get_mut(&list)
.unwrap()
.push_front(ListRow::new(i as u64, row));
let start = Instant::now();
render.update(&root, &mut rsc);
total += start.elapsed();
let (draws, rewrites, moves) = render.take_counters();
total_draws += draws;
total_rewrites += rewrites;
total_moves += moves;
}
report(
&format!(
"(d) insert-above-anchor, N={n}, {inserts} pushes (totals; \
must not scale with N)"
),
total,
total_draws,
total_rewrites,
total_moves,
);
println!(
" per-push average: {:.4}ms",
total.as_secs_f64() * 1000.0 / inserts as f64
);
}
/// (e) Expand-a-row-holding-its-edge: one row (fixed-height, so its size is
/// directly controllable) is grown a little at a time, each time preceded
/// by `note_tap` aimed at its own top edge -- the exact mechanism list.rs's
/// module doc describes and its unit tests check for correctness. This
/// measures its *cost*: only the rows on the far side of the grown one
/// (below it, since the top edge is held) should ever move, and nothing
/// should be redrawn purely because the list overall got taller.
fn bench_expand_holds_edge(n: usize, growths: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut list = List::new(Axis::Y);
// Near the end (not the very last row) so it is already on screen
// under the list's default bottom-anchored placement, for every N --
// no scrolling needed to bring it into view before measuring.
let growable_index = n.saturating_sub(3);
let mut growable = None;
for i in 0..n {
if i == growable_index {
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let sized = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(abs(40.0)),
});
growable = Some(sized.weak());
list.push_back(ListRow::new(i as u64, sized.any()));
} else {
let row = build_row(&mut rsc, i, 20);
list.push_back(ListRow::new(i as u64, row));
}
}
let list = rsc.ui.widgets.add_strong(list);
let list_weak = list.weak();
let root = list.any();
let growable = growable.unwrap();
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
render.update(&root, &mut rsc);
render.take_counters();
let mut total = std::time::Duration::ZERO;
let mut total_draws = 0u64;
let mut total_rewrites = 0u64;
let mut total_moves = 0u64;
let mut height = 40.0f32;
let key = growable_index as u64;
for _ in 0..growths {
height += 10.0;
if let Some((top, _bottom)) = rsc.ui.widgets.get(&list_weak).unwrap().extent(key) {
rsc.ui
.widgets
.get_mut(&list_weak)
.unwrap()
.note_tap(top + 1.0);
}
rsc.ui.widgets.get_mut(&growable).unwrap().y = Some(abs(height));
let start = Instant::now();
render.update(&root, &mut rsc);
total += start.elapsed();
let (draws, rewrites, moves) = render.take_counters();
total_draws += draws;
total_rewrites += rewrites;
total_moves += moves;
}
report(
&format!(
"(e) expand-hold, N={n}, {growths} growths (totals; \
must not scale with N)"
),
total,
total_draws,
total_rewrites,
total_moves,
);
println!(
" per-growth average: {:.4}ms",
total.as_secs_f64() * 1000.0 / growths as f64
);
}
fn main() { fn main() {
println!("iris message-list benchmark -- release build, this machine's CPU"); println!("iris message-list benchmark -- release build, this machine's CPU");
for &n in &[100usize, 1_000, 10_000] { for &n in &[100usize, 1_000, 10_000] {
@@ -255,4 +416,10 @@ fn main() {
for &n in &[100usize, 1_000, 10_000] { for &n in &[100usize, 1_000, 10_000] {
bench_input_grows(n, 40); bench_input_grows(n, 40);
} }
for &n in &[100usize, 1_000, 10_000] {
bench_insert_above_anchor(n, 200);
}
for &n in &[100usize, 1_000, 10_000] {
bench_expand_holds_edge(n, 40);
}
} }
+116
View File
@@ -0,0 +1,116 @@
//! RUST.md's I3: `iris::widget::List` with 800 rows of varied-length
//! wrapped text, one in twelve carrying a small image, scrollable with the
//! mouse wheel. Run headless with `iris/run-headless.sh message_list --shot
//! /tmp/message_list.png` -- there is no display on this machine, so that
//! is the only way to see it rendered; `run-tests.sh`/`cargo test` never
//! touch this file.
//!
//! Rows alternate two background tints so a screenshot can show the
//! boundary between adjacent rows even where the text itself wraps to a
//! different number of lines -- exactly the "variable-height rows" I3
//! asks for, and the thing a virtualised list gets wrong first if it is
//! wrong at all (a gap, an overlap, a row the wrong colour). This example
//! is also what found `List::place`'s oversized-background bug (see
//! list.rs's module doc and its `a_fill_shaped_background_is_not_left_
//! oversized` test) -- a plain unit test could have (and now does) catch
//! it directly, but it was this screenshot rendering as a single blank
//! tinted rectangle that pointed at it first.
use iris::prelude::*;
use winit::{dpi::LogicalSize, window::WindowAttributes};
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
const ROWS: usize = 800;
const IMAGE_EVERY: usize = 12;
/// Repeats a short sentence a varying number of times per row so real
/// wrapping happens at every row height from one line to several, rather
/// than every row being identically tall (which would render correctly
/// even with a broken height measurement).
fn row_text(i: usize) -> String {
const SENTENCE: &str =
"Iris lays out this row once and moves it on scroll, never re-laying it out. ";
let repeats = 1 + (i * 7) % 5;
format!("Message {i}: {}", SENTENCE.repeat(repeats))
}
/// A small solid-colour square standing in for a real decoded image --
/// what matters for I3 is that a row can carry an `Image` widget at all,
/// not what the picture shows.
fn row_image(i: usize) -> image::DynamicImage {
let hue = ((i * 47) % 255) as u8;
image::RgbaImage::from_pixel(48, 48, image::Rgba([hue, 128, 255 - hue, 255])).into()
}
fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
let tint = if i.is_multiple_of(2) {
Color::rgb(120, 130, 170)
} else {
Color::rgb(70, 80, 140)
};
let text_color = Color::BLACK;
if i.is_multiple_of(IMAGE_EVERY) {
let text = wtext(row_text(i))
.wrap(true)
.color(text_color)
.add_strong(rsc)
.any();
let img = image::<Rsc>(row_image(i))(rsc);
let img = rsc.widgets_mut().add_strong(img).any();
let mut span = Span::empty(Dir::DOWN);
span.push(text);
span.push(img);
span.pad(8.0).background(rect(tint)).add_strong(rsc).any()
} else {
wtext(row_text(i))
.wrap(true)
.color(text_color)
.pad(8.0)
.background(rect(tint))
.add_strong(rsc)
.any()
}
}
impl DefaultAppState for State {
// A phone-plausible portrait shape (the transcript screen this is
// standing in for). The tiling headless compositor `run-headless.sh`
// uses ignores this and fills its own 1920x1200 output regardless, but
// it's a correct hint for any other backend (a real window manager, or
// android-view) and costs nothing to state.
fn window_attributes() -> WindowAttributes {
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
}
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let mut list = List::new(Axis::Y);
for i in 0..ROWS {
let row = build_row(rsc, i);
list.push_back(ListRow::new(i as u64, row));
}
let root = list
.on(CursorSense::Scroll, |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta);
})
.masked()
.background(rect(Color::WHITE))
.add_strong(rsc);
ui_state.set_root(root.any());
Self { ui_state }
}
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -1,4 +1,5 @@
mod image; mod image;
mod list;
mod mask; mod mask;
mod position; mod position;
mod ptr; mod ptr;
@@ -7,6 +8,7 @@ mod text;
mod trait_fns; mod trait_fns;
pub use image::*; pub use image::*;
pub use list::*;
pub use mask::*; pub use mask::*;
pub use position::*; pub use position::*;
pub use ptr::*; pub use ptr::*;