use std::cell::Cell;
use std::time::Instant;
use super::*;
use crate::test_support::{click, run};
use egui_kittest::Harness;
const SCREEN: egui::Vec2 = egui::vec2(500.0, 400.0);
fn input() -> egui::RawInput {
egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, SCREEN)), ..Default::default() }
}
#[test]
fn layout_math() {
let u = Layout::Uniform { rows: 10, height: 20.0 };
assert_eq!(u.total(), 200.0);
assert_eq!(u.row_at(39.9), 1);
assert_eq!(u.row_at(-5.0), 0);
assert_eq!(u.row_at(1e9), 9);
assert_eq!(u.visible(30.0, 70.0, 0), 1..4);
assert_eq!(u.visible(30.0, 70.0, 2), 0..6);
let mut o = Offsets::default();
let heights = |i: usize| if i == 2 { f32::NAN } else { 10.0 + i as f32 };
assert!(o.update(5, 1, &heights));
assert!(!o.update(5, 1, &heights), "same revision and count: kept");
assert_eq!(o.offsets, [0.0, 10.0, 21.0, 21.0, 34.0, 48.0]);
let v = Layout::Variable(&o.offsets);
assert_eq!(v.row_at(20.9), 1);
assert_eq!(v.row_at(21.0), 3, "zero-height row 2 is skipped");
assert_eq!(v.span(4), (34.0, 14.0));
assert_eq!(v.visible(0.0, 0.0, 0), 0..0);
assert_eq!(Layout::Variable(&[0.0]).visible(0.0, 100.0, 3), 0..0);
}
fn rows_painted(rows: usize, heights: RowHeights<'_>, frames: usize, scroll_to: &[usize]) -> Vec<usize> {
let ctx = egui::Context::default();
let mut counts = Vec::new();
for f in 0..frames {
let mut n = 0;
let target = scroll_to.get(f).copied();
run(&ctx, input(), |ui| {
VirtualList::new("big", rows, heights).scroll_to(target).show(ui, |ui, row| {
n += 1;
let _ = (ui, row);
});
});
counts.push(n);
}
counts
}
#[test]
fn hundred_thousand_uniform_rows_paint_only_the_visible_ones() {
let counts = rows_painted(100_000, RowHeights::Uniform(20.0), 4, &[0, 50_000, 99_999, 10]);
let max_visible = (SCREEN.y / 20.0) as usize + 2 * 2 + 2;
for c in counts {
assert!(c > 0 && c <= max_visible, "{c} rows painted, at most {max_visible} visible");
}
}
#[test]
fn variable_heights_are_summed_once_per_revision() {
let calls = Cell::new(0usize);
let height = |i: usize| {
calls.set(calls.get() + 1);
16.0 + (i % 7) as f32 * 4.0
};
let ctx = egui::Context::default();
let mut painted = Vec::new();
for (frame, revision) in [(0, 1u64), (1, 1), (2, 1), (3, 2)] {
calls.set(0);
let mut n = 0;
run(&ctx, input(), |ui| {
VirtualList::new("var", 100_000, RowHeights::Variable { revision, height: &height })
.scroll_to(Some(frame * 30_000))
.show(ui, |_, _| n += 1);
});
painted.push(n);
match frame {
0 | 3 => assert_eq!(calls.get(), 100_000, "frame {frame}: table built"),
_ => assert_eq!(calls.get(), 0, "frame {frame}: no per-row work"),
}
}
assert!(painted.iter().all(|n| *n > 0 && *n <= (SCREEN.y / 16.0) as usize + 6), "{painted:?}");
}
#[test]
fn frame_cost_is_independent_of_row_count() {
fn median_frame(rows: usize, variable: bool) -> f64 {
let ctx = egui::Context::default();
let height = |i: usize| 18.0 + (i % 3) as f32;
let heights = if variable { RowHeights::Variable { revision: 1, height: &height } } else { RowHeights::Uniform(20.0) };
let mut times = Vec::new();
for f in 0..40usize {
let target = Some((f * 7919) % rows);
let start = Instant::now();
run(&ctx, input(), |ui| {
VirtualList::new("perf", rows, heights).scroll_to(target).stripes(true).show(ui, |ui, row| {
ui.painter().rect_filled(row.rect.shrink(2.0), 0, egui::Color32::GRAY);
});
});
if f >= 5 {
times.push(start.elapsed().as_secs_f64());
}
}
times.sort_by(f64::total_cmp);
times[times.len() / 2]
}
for variable in [false, true] {
let small = median_frame(1_000, variable);
let big = median_frame(100_000, variable);
eprintln!("variable={variable}: 1k rows {:.3} ms, 100k rows {:.3} ms per frame", small * 1e3, big * 1e3);
assert!(big < small * 3.0 + 0.002, "variable={variable}: 100k rows {big:.6}s vs 1k rows {small:.6}s");
}
}
struct State {
selected: Option<usize>,
activated: Option<usize>,
visible: Range<usize>,
}
fn list_harness(rows: usize) -> Harness<'static, State> {
let mut h = Harness::builder().with_size(SCREEN).with_step_dt(0.02).build_ui_state(
move |ui, s: &mut State| {
let label = |_: usize| "item";
let out = VirtualList::new("files", rows, RowHeights::Uniform(20.0)).selected(s.selected).row_label(&label).show(ui, |ui, row| {
ui.label(if row.selected { "selected" } else { "row" });
});
if let Some(i) = out.select {
s.selected = Some(i);
}
if out.activated.is_some() {
s.activated = out.activated;
}
s.visible = out.visible;
},
State { selected: None, activated: None, visible: 0..0 },
);
crate::automation::install(&h.ctx);
h.step();
h.step();
h
}
#[test]
fn click_selects_keyboard_moves_and_scrolls_enter_activates() {
let mut h = list_harness(1000);
let row3 = crate::automation::find(&h.ctx, "files.row.3").unwrap();
assert_eq!(row3.label, "item");
click(&mut h, row3.center());
assert_eq!(h.state().selected, Some(3));
h.key_press(Key::ArrowDown);
h.step();
assert_eq!(h.state().selected, Some(4));
h.key_press(Key::End);
h.step();
h.step();
assert_eq!(h.state().selected, Some(999));
assert!(h.state().visible.contains(&999), "scrolled to the end: {:?}", h.state().visible);
assert_eq!(crate::automation::find(&h.ctx, "files.row.999").and_then(|e| e.selected), Some(true));
h.key_press(Key::PageUp);
h.step();
let after_page = h.state().selected.unwrap();
assert!(after_page < 999 && after_page > 950, "{after_page}");
h.key_press(Key::Enter);
h.step();
assert_eq!(h.state().activated, Some(after_page));
}
#[test]
fn empty_list_handles_keys() {
let mut h = list_harness(0);
let list = crate::automation::find(&h.ctx, "files").unwrap();
click(&mut h, list.center());
h.key_press(Key::ArrowDown);
h.key_press(Key::End);
h.step();
assert_eq!(h.state().selected, None);
}
#[test]
fn table_cells_are_virtualized_headers_click_and_columns_resize() {
struct T {
cells: usize,
header: Option<usize>,
widths: Vec<f32>,
}
let cols = [Column::new("Name", 160.0), Column::new("Size", 80.0), Column { resizable: false, ..Column::new("Kind", 100.0) }];
let mut h = Harness::builder().with_size(SCREEN).build_ui_state(
move |ui, s: &mut T| {
s.cells = 0;
let out = VirtualTable::new("table", &cols, 100_000, RowHeights::Uniform(20.0)).sort(Some(Sort { column: 0, ascending: true })).show(
ui,
|ui, row, col| {
s.cells += 1;
ui.label(if col == 0 { "name" } else { "x" });
let _ = row;
},
);
s.header = s.header.or(out.header_clicked);
s.widths = out.widths;
},
T { cells: 0, header: None, widths: Vec::new() },
);
crate::automation::install(&h.ctx);
h.step();
h.step();
let max_rows = (SCREEN.y / 20.0) as usize + 6;
assert!(h.state().cells > 0 && h.state().cells <= max_rows * 3, "{} cells", h.state().cells);
let size = crate::automation::find(&h.ctx, "table.header.1").unwrap();
assert_eq!(crate::automation::find(&h.ctx, "table.header.0").and_then(|e| e.selected), Some(true));
click(&mut h, size.center());
assert_eq!(h.state().header, Some(1));
let edge = egui::pos2(size.egui_rect().left(), size.center().y);
h.hover_at(edge);
h.step();
h.drag_at(edge);
h.step();
for dx in [10.0, 25.0, 40.0] {
h.hover_at(edge + egui::vec2(dx, 0.0));
h.step();
}
h.drop_at(edge + egui::vec2(40.0, 0.0));
h.step();
let w = h.state().widths[0];
assert!(w > 180.0 && w <= 200.5, "{w}");
assert_eq!(h.state().widths[1], 80.0);
}