mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 04:51:24 +00:00
About: refit credits tables when what they measure changes
The columns became resizable, so the table keeps its own widths. A longer name, a changed cell or a resized window left the old widths in place and clipped text. The table now goes back to its fit when a column measure or the clip rect width changes, which scrolling never moves. Adds tests that drag every column edge, change the fit and scroll a widened table, as CodeRabbit asked for the drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fe0fbf852b
commit
9748583f84
@@ -343,15 +343,17 @@ fn column_width<'a>(ui: &egui::Ui, header: &str, cells: impl Iterator<Item = &'a
|
||||
}
|
||||
|
||||
/// A credits table: columns of `widths`, the `None` one taking what the others leave (at least
|
||||
/// `flex_min`). The header stays put while the rows scroll, within `height` (header included). Too
|
||||
/// [`NAME_MIN_W`]). The header stays put while the rows scroll, within `height` (header included). Too
|
||||
/// narrow for every column, it scrolls sideways rather than clip one. Every
|
||||
/// column edge can be dragged, down to [`MIN_COLUMN`] (the `None` column to `flex_min`): widening
|
||||
/// a column grows the table, as in a spreadsheet, and only then does it scroll sideways.
|
||||
/// column edge can be dragged, down to [`MIN_COLUMN`] (the `None` column to [`NAME_MIN_W`]): widening
|
||||
/// a column grows the table, as in a spreadsheet, and only then does it scroll sideways. When the
|
||||
/// window, a column's width or `flex_text` (the width the `None` column's text needs) changes, the
|
||||
/// columns go back to this fit, dropping any dragged widths.
|
||||
fn credits_table(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
widths: &[Option<f32>],
|
||||
flex_min: f32,
|
||||
flex_text: f32,
|
||||
height: f32,
|
||||
header: impl FnOnce(egui_extras::TableRow<'_, '_>),
|
||||
body: impl FnOnce(egui_extras::TableBody<'_>),
|
||||
@@ -362,9 +364,16 @@ fn credits_table(
|
||||
// The table puts a row gap under its header.
|
||||
let body_h = (height - TABLE_HEADER_H - ui.spacing().item_spacing.y).max(TABLE_ROW_H);
|
||||
egui::ScrollArea::horizontal().id_salt((id, "h")).auto_shrink([false, true]).show(ui, |ui| {
|
||||
let flex = (ui.available_width() - fixed).max(flex_min);
|
||||
let flex = (ui.available_width() - fixed).max(NAME_MIN_W);
|
||||
// No gaps between cells: their padding spaces them, and the header rule runs unbroken.
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
// The table keeps its own column widths, so a change in what they were fitted to has to clear
|
||||
// them. Scrolling can nudge the available width, so this compares the clip rect, which scrolling
|
||||
// never moves.
|
||||
let fit = egui::Id::unique(("credits_fit", id));
|
||||
let measured = (widths.to_vec(), flex_text, ui.clip_rect().width());
|
||||
let refit = ui.data_mut(|d| d.get_temp::<(Vec<Option<f32>>, f32, f32)>(fit)).as_ref() != Some(&measured);
|
||||
ui.data_mut(|d| d.insert_temp(fit, measured));
|
||||
let mut b = egui_extras::TableBuilder::new(ui)
|
||||
.id_salt(id)
|
||||
.striped(true)
|
||||
@@ -373,10 +382,13 @@ fn credits_table(
|
||||
.min_scrolled_height(body_h)
|
||||
.max_scroll_height(body_h)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center));
|
||||
if refit {
|
||||
b.reset();
|
||||
}
|
||||
for w in widths {
|
||||
b = b.column(match w {
|
||||
Some(w) => egui_extras::Column::initial(*w).at_least(MIN_COLUMN).clip(true),
|
||||
None => egui_extras::Column::initial(flex).at_least(flex_min).clip(true),
|
||||
None => egui_extras::Column::initial(flex).at_least(NAME_MIN_W).clip(true),
|
||||
});
|
||||
}
|
||||
b.header(TABLE_HEADER_H, header).body(body);
|
||||
@@ -399,11 +411,13 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, height: f32) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let names = v.names;
|
||||
let widths = contributor_widths(ui, list);
|
||||
let shown: Vec<String> = list.iter().map(|c| c.name(names)).collect();
|
||||
let name_text = column_width(ui, SortKey::Name.label().1, shown.iter().map(String::as_str));
|
||||
credits_table(
|
||||
ui,
|
||||
"credits_table",
|
||||
&widths,
|
||||
NAME_MIN_W,
|
||||
name_text,
|
||||
height,
|
||||
|mut header| {
|
||||
for k in SortKey::ALL {
|
||||
@@ -526,12 +540,13 @@ fn models_in(ui: &mut egui::Ui, models: &[Model], total_commits: u64) {
|
||||
let total = total_commits.max(models.iter().map(|m| m.commits).max().unwrap_or(0));
|
||||
let rows: Vec<[String; 6]> = models.iter().map(|m| model_cells(m, total)).collect();
|
||||
let widths = model_widths(ui, &rows);
|
||||
let model_text = column_width(ui, tr(MODEL_HEADS[1]), rows.iter().filter_map(|r| r.get(1)).map(String::as_str));
|
||||
let numeric = |i: usize| i >= 3;
|
||||
credits_table(
|
||||
ui,
|
||||
"credits_models",
|
||||
&widths,
|
||||
NAME_MIN_W,
|
||||
model_text,
|
||||
table_height(ui, rows.len()),
|
||||
|mut header| {
|
||||
for (i, h) in MODEL_HEADS.iter().enumerate() {
|
||||
|
||||
@@ -267,3 +267,121 @@ fn the_about_window_draws_every_tab() {
|
||||
assert_eq!(app.ui.about_tab, tab);
|
||||
}
|
||||
}
|
||||
|
||||
const WINDOW: f32 = 640.5;
|
||||
const WIDTHS: [Option<f32>; 3] = [None, Some(80.0), Some(80.0)];
|
||||
const NAME: f32 = 120.0;
|
||||
|
||||
/// One frame of a table long enough to scroll, in a window `width` wide; the rects of its header cells.
|
||||
fn table_frame(ctx: &egui::Context, width: f32, widths: &[Option<f32>], name: f32, events: Vec<egui::Event>) -> Vec<Rect> {
|
||||
let screen_rect = Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 300.0)));
|
||||
let raw = egui::RawInput { screen_rect, events, ..Default::default() };
|
||||
let mut cells = Vec::new();
|
||||
let out = ctx.run_ui(raw, |ui| {
|
||||
cells.clear();
|
||||
credits_table(
|
||||
ui,
|
||||
"test",
|
||||
widths,
|
||||
name,
|
||||
240.0,
|
||||
|mut header| {
|
||||
for _ in widths {
|
||||
header.col(|ui| cells.push(ui.max_rect()));
|
||||
}
|
||||
},
|
||||
|body| {
|
||||
body.rows(TABLE_ROW_H, 40, |mut row| {
|
||||
for _ in widths {
|
||||
row.col(|ui| {
|
||||
ui.label("x");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
out.drop_without_applying_deltas();
|
||||
cells
|
||||
}
|
||||
|
||||
/// [`table_frame`] at the usual size.
|
||||
fn settled_frame(ctx: &egui::Context) -> Vec<Rect> {
|
||||
table_frame(ctx, WINDOW, &WIDTHS, NAME, Vec::new())
|
||||
}
|
||||
|
||||
/// A new context with the table open and settled, and the rects of its header cells.
|
||||
fn opened_table() -> (egui::Context, Vec<Rect>) {
|
||||
let ctx = egui::Context::default();
|
||||
settled_frame(&ctx);
|
||||
let cells = settled_frame(&ctx);
|
||||
(ctx, cells)
|
||||
}
|
||||
|
||||
/// Drags a column's right edge (cells have no gap between them), then lets go.
|
||||
fn drag_edge(ctx: &egui::Context, cell: Rect, by: f32) {
|
||||
let start = egui::pos2(cell.right(), cell.center().y);
|
||||
let halfway = start + vec2(by / 2.0, 0.0);
|
||||
let end = start + vec2(by, 0.0);
|
||||
let moved = egui::Event::PointerMoved;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
for event in [moved(start), button(start, true), moved(halfway), moved(end), button(end, false)] {
|
||||
table_frame(ctx, WINDOW, &WIDTHS, NAME, vec![event]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_column_can_be_dragged_at_its_right_edge_and_stays_there() {
|
||||
let (_, opened) = opened_table();
|
||||
for (dragged, cell) in opened.iter().enumerate() {
|
||||
let (ctx, _) = opened_table();
|
||||
drag_edge(&ctx, *cell, -30.0);
|
||||
settled_frame(&ctx);
|
||||
let after = settled_frame(&ctx);
|
||||
for (column, (was, now)) in opened.iter().zip(&after).enumerate() {
|
||||
let want = if column == dragged { was.width() - 30.0 } else { was.width() };
|
||||
assert!((now.width() - want).abs() < 1.0, "dragging column {dragged}: column {column} is {now:?}, was {was:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_fit_replaces_the_column_widths() {
|
||||
let (ctx, opened) = opened_table();
|
||||
drag_edge(&ctx, opened[0], -30.0);
|
||||
let longer_name = table_frame(&ctx, WINDOW, &WIDTHS, NAME + 80.0, Vec::new());
|
||||
assert!((longer_name[0].width() - opened[0].width()).abs() < 1.0, "a longer name drops a dragged width: {longer_name:?}");
|
||||
let wider = table_frame(&ctx, WINDOW, &[None, Some(200.0), Some(80.0)], NAME, Vec::new());
|
||||
let widths: Vec<f32> = wider.iter().map(|c| c.width().round()).collect();
|
||||
assert!(widths.ends_with(&[200.0, 80.0]), "a longer cell widens its column: {widths:?}");
|
||||
assert!(wider[2].right() < WINDOW, "the table still fits the window: {wider:?}");
|
||||
let narrower = table_frame(&ctx, 400.0, &WIDTHS, NAME, Vec::new());
|
||||
assert!(narrower[2].right() < 400.0, "a narrower window refits the table: {narrower:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_sideways_keeps_dragged_widths() {
|
||||
let (ctx, opened) = opened_table();
|
||||
drag_edge(&ctx, opened[0], 100.0);
|
||||
settled_frame(&ctx);
|
||||
let dragged = settled_frame(&ctx);
|
||||
assert!((dragged[0].width() - opened[0].width() - 100.0).abs() < 1.0, "the drag took: {dragged:?}");
|
||||
table_frame(&ctx, WINDOW, &WIDTHS, NAME, vec![egui::Event::PointerMoved(dragged[0].center())]);
|
||||
let mut scrolled = false;
|
||||
for delta in [-13.3, -27.77, -41.1, 30.6] {
|
||||
let wheel = egui::Event::MouseWheel {
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(delta, 0.0),
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
phase: egui::TouchPhase::Move,
|
||||
};
|
||||
table_frame(&ctx, WINDOW, &WIDTHS, NAME, vec![wheel]);
|
||||
for _ in 0..8 {
|
||||
let cells = settled_frame(&ctx);
|
||||
assert!((cells[0].width() - dragged[0].width()).abs() < 1.0, "scrolled by {delta}: {cells:?}");
|
||||
scrolled |= cells[0].left() < dragged[0].left() - 1.0;
|
||||
}
|
||||
}
|
||||
assert!(scrolled, "the wheel never moved the table sideways");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user