Merge branch 'worktree-agent-a33c31aef1fd6d868' into rustify (I3: iris::widget::List)
This commit is contained in:
commit
8adda94a7a
6 files changed
+1460
-39
No files matched your search
@@ -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
|
||||
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
|
||||
|
||||
RUST.md's I2. Three changes a widget or app author would notice, all in
|
||||
|
||||
@@ -123,6 +123,18 @@ session spending an afternoon on them again.
|
||||
GLES-only `D2`/`D2Array` warning was confirmed a red herring — still
|
||||
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.
|
||||
- **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:
|
||||
`event-model/` (the event types, now shared with `server/`) and
|
||||
`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
|
||||
torn down after verification (`emu down`) per the machine's memory
|
||||
rule.
|
||||
- [ ] **I3 — a virtualised, bottom-anchored list.** Variable-height rows,
|
||||
keyed, composed only while visible, paged in both directions with a
|
||||
"more" sentinel at each end, a scroll anchor that survives rows
|
||||
being inserted above, and "hold the edge nearest the tap" done in
|
||||
the layout pass. Pass: 800 rows of real transcript text from the
|
||||
- [x] **I3 — a virtualised, bottom-anchored list (2026-09-05).** Variable-height
|
||||
rows, keyed, composed only while visible, paged in both directions
|
||||
with a "more" sentinel at each end, a scroll anchor that survives
|
||||
rows being inserted above, and "hold the edge nearest the tap" done
|
||||
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
|
||||
`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
|
||||
name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s
|
||||
tap-by-name works against the iris screen unchanged.
|
||||
|
||||
+200
-33
@@ -1,6 +1,7 @@
|
||||
//! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's
|
||||
//! "Benchmarks" item. Never run by `cargo test`; run explicitly with
|
||||
//! `cargo bench --bench message_list --release` or `./run-bench.sh`.
|
||||
//! "Benchmarks" item, and RUST.md's I3. Never run by `cargo test`; run
|
||||
//! explicitly with `cargo bench --bench message_list --release` or
|
||||
//! `./run-bench.sh`.
|
||||
//!
|
||||
//! **Why a plain `Instant`-timed binary, not criterion.** Every scenario
|
||||
//! 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.
|
||||
//! 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"
|
||||
//! wording):
|
||||
//! **The list under test is `iris::widget::List` (RUST.md's I3), not a
|
||||
//! `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
|
||||
//! 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
|
||||
//! re-layout.
|
||||
//! - (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.
|
||||
//! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md
|
||||
//! 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,
|
||||
//! driven through `run-headless.sh` -- see that file's header.
|
||||
//!
|
||||
//! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs`
|
||||
//! notes), so everything here runs as an ordinary `--release` binary with
|
||||
//! no compositor. Numbers are recorded in IRIS_TODO.md, not here -- this
|
||||
//! file is the rig, not the result.
|
||||
//! no compositor. Numbers are recorded in RUST.md's I3 box, not here --
|
||||
//! this file is the rig, not the result.
|
||||
|
||||
use iris::prelude::*;
|
||||
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
|
||||
/// an image (0 disables images entirely). Returns the scroll widget (weak,
|
||||
/// so the caller can drive it) and the erased root to render.
|
||||
fn build_list(
|
||||
/// A virtualised `List` of `n` message rows, one in `image_every` of them
|
||||
/// carrying an image (0 disables images entirely). Returns the list widget
|
||||
/// (weak, so the caller can drive it) and the erased root to render.
|
||||
fn build_message_list(
|
||||
rsc: &mut BenchRsc,
|
||||
n: usize,
|
||||
image_every: usize,
|
||||
) -> (WeakWidget<Scroll>, StrongWidget) {
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
) -> (WeakWidget<List>, StrongWidget) {
|
||||
let mut list = List::new(Axis::Y);
|
||||
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 scroll = rsc.ui.widgets.add_strong(Scroll::new(span.any(), Axis::Y));
|
||||
(scroll.weak(), scroll.any())
|
||||
let list = rsc.ui.widgets.add_strong(list);
|
||||
(list.weak(), list.any())
|
||||
}
|
||||
|
||||
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 {
|
||||
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();
|
||||
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.
|
||||
/// Warms up (as `layout_tests.rs`'s scrolling test documents: `Scroll`
|
||||
/// needs one no-op tick before a real scroll becomes a same-size move
|
||||
/// Warms up (one no-op tick, matching `Scroll`'s own need for it before an
|
||||
/// ordinary Rust `layout_tests.rs` scrolling test becomes a same-size move
|
||||
/// rather than a resize), then times a run of individual scroll ticks.
|
||||
fn bench_scroll(n: usize, ticks: usize) {
|
||||
let mut rsc = BenchRsc {
|
||||
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();
|
||||
render.resize((1080.0, 2000.0));
|
||||
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.take_counters();
|
||||
|
||||
@@ -149,7 +173,7 @@ fn bench_scroll(n: usize, ticks: usize) {
|
||||
let mut total_rewrites = 0u64;
|
||||
let mut total_moves = 0u64;
|
||||
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();
|
||||
render.update(&root, &mut rsc);
|
||||
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
|
||||
/// 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*
|
||||
/// height of the scroll container (a single widget, from the outer
|
||||
/// `Span`'s point of view) without changing the width it offers its
|
||||
/// content -- so the rows underneath, which only care about width, must
|
||||
/// not redraw; the scroll's own re-registration of where its content sits
|
||||
/// is the one O(1) move this is checking for. See LAYOUT.md's `Scroll`
|
||||
/// design note on offering the child last frame's content length, which is
|
||||
/// exactly what keeps this a move instead of a reflow.
|
||||
/// height of the list container (a single widget, from the outer `Span`'s
|
||||
/// point of view) without changing the width it offers its content -- so
|
||||
/// the rows underneath, which only care about width, must not redraw; the
|
||||
/// list's own re-registration of where its content sits is the one O(1)
|
||||
/// move this is checking for.
|
||||
fn bench_input_grows(n: usize, lines: usize) {
|
||||
let mut rsc = BenchRsc {
|
||||
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 {
|
||||
inner: list_root,
|
||||
x: None,
|
||||
@@ -209,7 +231,7 @@ fn bench_input_grows(n: usize, lines: usize) {
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((1080.0, 2000.0));
|
||||
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.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() {
|
||||
println!("iris message-list benchmark -- release build, this machine's CPU");
|
||||
for &n in &[100usize, 1_000, 10_000] {
|
||||
@@ -255,4 +416,10 @@ fn main() {
|
||||
for &n in &[100usize, 1_000, 10_000] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,4 +1,5 @@
|
||||
mod image;
|
||||
mod list;
|
||||
mod mask;
|
||||
mod position;
|
||||
mod ptr;
|
||||
@@ -7,6 +8,7 @@ mod text;
|
||||
mod trait_fns;
|
||||
|
||||
pub use image::*;
|
||||
pub use list::*;
|
||||
pub use mask::*;
|
||||
pub use position::*;
|
||||
pub use ptr::*;
|
||||
|
||||
Reference in new issue
Block a user