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
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
+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
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
View File
@@ -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);
}
}
+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 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::*;