use std::any::Any;
use std::cell::Cell;
use std::rc::Rc;
use std::time::Instant;
use frust_core::{
BoxConstraints, BuildCtx, ChangeFlags, FrameTime, InputEvent, LayoutCtx, PaintCtx, PaintScene,
RenderRoot, ScrollDelta, View, Widget, any,
};
use frust_widgets::{
ChildKey, Column, ListView, ListViewWidget, ScrollView, list_view, scroll_view,
};
use kurbo::{Point, Size};
const ITEM_EXTENT: f64 = 50.0;
fn viewport() -> Size {
Size::new(320.0, 640.0)
}
struct NullScene;
impl PaintScene for NullScene {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: peniko::Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
}
struct ProbeView {
height: f64,
}
struct ProbeWidget {
height: f64,
}
impl View<()> for ProbeView {
type Element = ProbeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
ProbeWidget {
height: self.height,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut ProbeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
if prev.height != self.height {
element.height = self.height;
return ChangeFlags::LAYOUT;
}
ChangeFlags::NONE
}
}
impl Widget for ProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(bc.max().width, self.height))
}
fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.fill_rect(
Point::ORIGIN,
Size::new(1.0, self.height),
peniko::Color::from_rgb8(0x20, 0x78, 0xc8),
);
}
}
fn variable_height(index: usize) -> f64 {
40.0 + ((index * 37) % 60) as f64
}
const PING_ROWS: f64 = 3.0;
const HALF_PERIOD: usize = 40;
fn scroll_delta_for(frame: usize) -> f64 {
let dy = PING_ROWS * ITEM_EXTENT;
if (frame / HALF_PERIOD).is_multiple_of(2) {
dy
} else {
-dy
}
}
fn drive_frame<V: View<()>>(
root: &mut RenderRoot<(), V>,
logic: &mut impl FnMut(&mut ()) -> V,
state: &mut (),
window: Size,
ms: f64,
) {
root.rebuild(logic, state);
root.layout(window);
let mut sink = NullScene;
root.paint(&mut sink, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
}
fn run_listview(frames: usize, mut make_view: impl FnMut() -> ListView<()>) -> Vec<f64> {
let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
let mut state = ();
let win = viewport();
let mut logic = move |_: &mut ()| make_view();
drive_frame(&mut root, &mut logic, &mut state, win, 0.0);
drive_frame(&mut root, &mut logic, &mut state, win, 16.0);
let mut samples = Vec::with_capacity(frames);
let mut ms = 32.0;
for f in 0..frames {
root.event(
&mut state,
&InputEvent::Scroll {
position: Point::new(10.0, win.height / 2.0),
delta: ScrollDelta::Pixels(0.0, scroll_delta_for(f)),
},
);
let start = Instant::now();
drive_frame(&mut root, &mut logic, &mut state, win, ms);
samples.push(start.elapsed().as_secs_f64() * 1_000_000.0);
ms += 16.0;
}
samples
}
fn run_baseline(n: usize, frames: usize) -> Vec<f64> {
let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
let mut state = ();
let win = viewport();
let mut logic = move |_: &mut ()| -> ScrollView<()> {
scroll_view(Column(
(0..n)
.map(|_| {
any::<(), _>(ProbeView {
height: ITEM_EXTENT,
})
})
.collect(),
))
};
drive_frame(&mut root, &mut logic, &mut state, win, 0.0);
drive_frame(&mut root, &mut logic, &mut state, win, 16.0);
let mut samples = Vec::with_capacity(frames);
let mut ms = 32.0;
for f in 0..frames {
root.event(
&mut state,
&InputEvent::Scroll {
position: Point::new(10.0, win.height / 2.0),
delta: ScrollDelta::Pixels(0.0, scroll_delta_for(f)),
},
);
let start = Instant::now();
drive_frame(&mut root, &mut logic, &mut state, win, ms);
samples.push(start.elapsed().as_secs_f64() * 1_000_000.0);
ms += 16.0;
}
samples
}
struct Stats {
mean: f64,
p95: f64,
p99: f64,
}
fn stats(mut samples: Vec<f64>) -> Stats {
samples.sort_by(|a, b| a.partial_cmp(b).expect("frame timings are always finite"));
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
Stats {
mean,
p95: percentile(&samples, 0.95),
p99: percentile(&samples, 0.99),
}
}
fn percentile(sorted: &[f64], p: f64) -> f64 {
debug_assert!(!sorted.is_empty());
let rank = (((sorted.len() - 1) as f64) * p).round() as usize;
sorted[rank.min(sorted.len() - 1)]
}
fn print_row(config: &str, n: usize, s: Stats) {
println!(
"{config:<28} {n:>8} {:>12.2} {:>12.2} {:>12.2}",
s.mean, s.p95, s.p99
);
}
#[test]
#[ignore = "perf evidence, host-machine dependent (docs/TESTING.md T8): run via \
`cargo test -p frust-widgets --test list_virtualization_bench -- --ignored --nocapture`"]
fn flat_vs_n_rebuild_layout_cost() {
const FRAMES: usize = 500;
const NS: [usize; 3] = [100, 1_000, 10_000];
println!(
"\n{:<28} {:>8} {:>12} {:>12} {:>12}",
"config", "N", "mean_us", "p95_us", "p99_us"
);
for &n in &NS {
let samples = run_listview(FRAMES, move || {
list_view(n, ITEM_EXTENT, |_i| {
any::<(), _>(ProbeView {
height: ITEM_EXTENT,
})
})
});
print_row("listview_uniform", n, stats(samples));
}
for &n in &NS {
let samples = run_listview(FRAMES, move || {
ListView::builder_keyed(n, ITEM_EXTENT, ChildKey::new, |i| {
any::<(), _>(ProbeView {
height: variable_height(i),
})
})
.estimated_item_extent(ITEM_EXTENT)
});
print_row("listview_keyed_variable", n, stats(samples));
}
for &n in &NS {
let samples = run_baseline(n, FRAMES);
print_row("scrollview_column_baseline", n, stats(samples));
}
println!(
"\nExpected shape: listview_* mean/p95/p99 stay ~flat from N=1_000 to \
N=10_000; scrollview_column_baseline scales ~linearly with N (see this \
file's module docs and the task's completion summary for the recorded run)."
);
}
fn list_view_widget(root: &RenderRoot<(), ListView<()>>) -> &ListViewWidget {
let id = root.root_id().expect("root built");
(root.tree().pod(id).expect("root pod").widget() as &dyn Any)
.downcast_ref::<ListViewWidget>()
.expect("root is a ListViewWidget")
}
fn scrolled_window(
mut logic: impl FnMut(&mut ()) -> ListView<()>,
win: Size,
target_offset: f64,
) -> Vec<usize> {
let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
let mut state = ();
drive_frame(&mut root, &mut logic, &mut state, win, 0.0);
drive_frame(&mut root, &mut logic, &mut state, win, 16.0);
root.event(
&mut state,
&InputEvent::Scroll {
position: Point::new(10.0, win.height / 2.0),
delta: ScrollDelta::Pixels(0.0, target_offset),
},
);
drive_frame(&mut root, &mut logic, &mut state, win, 32.0);
let w = list_view_widget(&root);
assert_eq!(
w.offset(),
target_offset,
"the target offset is well inside [0, max_offset] for N=10_000, so the \
single wheel event lands exactly on it with no clamping"
);
w.window().to_vec()
}
struct CountingProbeView {
height: f64,
built: Rc<Cell<u32>>,
rebuilt: Rc<Cell<u32>>,
}
struct CountingProbeWidget {
height: f64,
}
impl View<()> for CountingProbeView {
type Element = CountingProbeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CountingProbeWidget {
self.built.set(self.built.get() + 1);
CountingProbeWidget {
height: self.height,
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut CountingProbeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
self.rebuilt.set(self.rebuilt.get() + 1);
element.height = self.height;
ChangeFlags::NONE
}
}
impl Widget for CountingProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(bc.max().width, self.height))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
#[test]
fn ten_thousand_rows_scrolled_midlist_materializes_only_the_window() {
const N: usize = 10_000;
const TARGET_OFFSET: f64 = 5_000.0 * ITEM_EXTENT;
const EXPECTED_START: usize = 4_998;
const EXPECTED_END: usize = 5_015;
let win = viewport();
let expected: Vec<usize> = (EXPECTED_START..EXPECTED_END).collect();
assert_eq!(
expected.len(),
17,
"sanity-check the hand derivation itself"
);
let uniform_window = scrolled_window(
move |_: &mut ()| {
list_view(N, ITEM_EXTENT, |_i| {
any::<(), _>(ProbeView {
height: ITEM_EXTENT,
})
})
},
win,
TARGET_OFFSET,
);
assert_eq!(
uniform_window, expected,
"uniform ListView must materialize exactly the closed-form window \
[{EXPECTED_START}, {EXPECTED_END}) at N={N} scrolled mid-list, never \
the full {N} rows"
);
let keyed_window = scrolled_window(
move |_: &mut ()| {
ListView::builder_keyed(N, ITEM_EXTENT, ChildKey::new, |_i| {
any::<(), _>(ProbeView {
height: ITEM_EXTENT,
})
})
.estimated_item_extent(ITEM_EXTENT)
},
win,
TARGET_OFFSET,
);
assert_eq!(
keyed_window, expected,
"keyed variable-extent ListView (height == estimate) must materialize \
the identical closed-form window as the uniform path"
);
let built = Rc::new(Cell::new(0u32));
let rebuilt = Rc::new(Cell::new(0u32));
let built_for_logic = built.clone();
let rebuilt_for_logic = rebuilt.clone();
let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
let mut state = ();
let mut logic = move |_: &mut ()| -> ScrollView<()> {
scroll_view(Column(
(0..N)
.map(|_| {
any::<(), _>(CountingProbeView {
height: ITEM_EXTENT,
built: built_for_logic.clone(),
rebuilt: rebuilt_for_logic.clone(),
})
})
.collect(),
))
};
drive_frame(&mut root, &mut logic, &mut state, win, 0.0);
assert_eq!(
built.get() as usize,
N,
"the Column baseline eagerly builds every one of the N rows on the \
very first frame, never a windowed subset"
);
drive_frame(&mut root, &mut logic, &mut state, win, 16.0);
assert_eq!(
rebuilt.get() as usize,
N,
"and every one of the N rows is reconciled again on the very next \
frame too — it holds all N widgets, not a materialized window"
);
assert_eq!(
built.get() as usize,
N,
"no additional builds on the second frame: the same N widgets are \
retained (not virtualized away and rebuilt), just all N reconciled \
every pass"
);
}