iris: benches/message_list.rs measures the real List, adds insert-above and expand-hold
The (a)/(b)/(c) scenarios built their own Span+Scroll pair, so they never exercised the virtualised widget the transcript screen actually needs. Rewritten on top of iris::widget::List, plus two new scenarios from RUST.md's I3: (d) insert-above-anchor (paging older history onto an already-scrolled list) and (e) expand-a-row-holding-its-edge (list.rs's note_tap mechanism). Both come out flat across N = 100/1,000/10,000, as required. Also fixes a real inefficiency this rewrite surfaced: List::place's "generous" measurement bound was derived from viewport_len, so a sibling resizing the list itself (the (c) scenario) changed that bound every tick and defeated draw_inner's same-size fast path, forcing a full redraw of every visible row instead of a move. It is now a fixed module constant (GENEROUS_PADDING), independent of the list's own size -- draws for (c) dropped from 3059 to 684 over 40 ticks. cargo test -p iris (5 List tests still pass), cargo clippy --all-targets and --benches --release, cargo fmt --all -- --check all clean. Numbers recorded in RUST.md's I3 box. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
1 parent
a2cd119985
commit
03da47e550
2 files changed
+236
-41
No files matched your search
+200
-33
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+36
-8
@@ -335,6 +335,16 @@ impl List {
|
|||||||
self.pending_tap = Some(viewport_pos);
|
self.pending_tap = Some(viewport_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The on-screen `(top, bottom)` viewport-pixel extent of `key`'s row
|
||||||
|
/// as of the last layout, or `None` if it was not among the rows drawn
|
||||||
|
/// then (off-screen, not yet loaded, or the list hasn't drawn since).
|
||||||
|
/// What a caller reads to decide where to aim `note_tap` -- e.g. "the
|
||||||
|
/// top of the row that's about to expand" -- without duplicating this
|
||||||
|
/// widget's own layout math.
|
||||||
|
pub fn extent(&self, key: RowKey) -> Option<(f32, f32)> {
|
||||||
|
self.extents.get(&key).map(|e| (e.top, e.bottom))
|
||||||
|
}
|
||||||
|
|
||||||
fn slot_exists(&self, slot: isize) -> bool {
|
fn slot_exists(&self, slot: isize) -> bool {
|
||||||
match slot {
|
match slot {
|
||||||
BEFORE_SLOT => self.more_before.is_some(),
|
BEFORE_SLOT => self.more_before.is_some(),
|
||||||
@@ -513,21 +523,27 @@ impl List {
|
|||||||
/// return its resolved `(leading, trailing)` edges in viewport pixels.
|
/// return its resolved `(leading, trailing)` edges in viewport pixels.
|
||||||
fn place(&mut self, painter: &mut Painter, slot: isize, placement: Placement) -> (f32, f32) {
|
fn place(&mut self, painter: &mut Painter, slot: isize, placement: Placement) -> (f32, f32) {
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
// Large enough that no real row's content is taller than this
|
|
||||||
// (rows do not clip to the height they're offered -- only width
|
|
||||||
// drives a wrapped row's height), bounded so the region's abs
|
|
||||||
// values never approach f32 imprecision at the sizes this list
|
|
||||||
// ever sees.
|
|
||||||
let generous = self.viewport_len.max(64.0) * 8.0;
|
|
||||||
let (top, bottom) = match placement {
|
let (top, bottom) = match placement {
|
||||||
Placement::Top(top) => {
|
Placement::Top(top) => {
|
||||||
let region = Self::abs_region(axis, top, top + generous);
|
let region = Self::abs_region(axis, top, top + GENEROUS_PADDING);
|
||||||
let used = painter.widget_within(self.slot_widget(slot), region);
|
let used = painter.widget_within(self.slot_widget(slot), region);
|
||||||
let h = Self::resolve_len_px(painter, axis, used.axis(axis));
|
let h = Self::resolve_len_px(painter, axis, used.axis(axis));
|
||||||
(top, top + h)
|
(top, top + h)
|
||||||
}
|
}
|
||||||
Placement::Bottom(bottom) => {
|
Placement::Bottom(bottom) => {
|
||||||
let used = painter.widget(self.slot_widget(slot));
|
// Measured at a fixed, zero-anchored region rather than
|
||||||
|
// `painter.region()` (the list's *actual* offered box):
|
||||||
|
// using the real box would make the measurement's offered
|
||||||
|
// *size* track this list's own height, so a sibling
|
||||||
|
// growing taller (the input-box case) would look like a
|
||||||
|
// resize to every bottom-known row and force a full
|
||||||
|
// redraw of each -- despite a row's content depending
|
||||||
|
// only on width. A region fixed at `[0, GENEROUS_PADDING]`
|
||||||
|
// is identical frame to frame regardless of what else on
|
||||||
|
// screen changed, so an unchanged row hits `draw_inner`'s
|
||||||
|
// exact-match skip (LAYOUT.md's caching section) instead.
|
||||||
|
let region = Self::abs_region(axis, 0.0, GENEROUS_PADDING);
|
||||||
|
let used = painter.widget_within(self.slot_widget(slot), region);
|
||||||
let h = Self::resolve_len_px(painter, axis, used.axis(axis));
|
let h = Self::resolve_len_px(painter, axis, used.axis(axis));
|
||||||
let top = bottom - h;
|
let top = bottom - h;
|
||||||
let region = Self::abs_region(axis, top, bottom);
|
let region = Self::abs_region(axis, top, bottom);
|
||||||
@@ -542,6 +558,18 @@ impl List {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The oversized bound offered along the primary axis when a row's real
|
||||||
|
/// extent isn't known yet (a fresh top-known placement) or is deliberately
|
||||||
|
/// discarded (a bottom-known measurement, see `place`). Large enough that
|
||||||
|
/// no real row's content is taller than this -- rows do not clip to the
|
||||||
|
/// height they're offered, only width drives a wrapped row's height -- and
|
||||||
|
/// a fixed module constant rather than derived from `viewport_len`, since
|
||||||
|
/// deriving it from a value that changes whenever the list itself resizes
|
||||||
|
/// (a sibling growing) would make the offered region's *size* change too,
|
||||||
|
/// defeating the same-size-different-position fast path `place` depends
|
||||||
|
/// on for an O(1) move.
|
||||||
|
const GENEROUS_PADDING: f32 = 100_000.0;
|
||||||
|
|
||||||
impl Widget for List {
|
impl Widget for List {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
|
|||||||
Reference in new issue
Block a user