diff --git a/src/app.rs b/src/app.rs index 1bfc50f..ee4b521 100644 --- a/src/app.rs +++ b/src/app.rs @@ -526,6 +526,22 @@ fn header_row(ui: &mut egui::Ui, th: &Theme, row_h: f32) { }); } +/// Fixed metric column widths for the process table (Write, Write freq). +pub(crate) const PROC_WRITE_COL_W: f32 = 100.0; +pub(crate) const PROC_FREQ_COL_W: f32 = 80.0; +pub(crate) const PROC_NAME_COL_W: f32 = 140.0; + +/// How process-table rate cells pin text. Exported for waterfall regression tests. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProcMetricAlign { + /// Text's right edge sits on the column's right edge (required — no staircase). + RightEdge, +} + +pub(crate) fn proc_metric_align() -> ProcMetricAlign { + ProcMetricAlign::RightEdge +} + /// Shared layout: Name | Path | Write | Write freq. fn proc_row( ui: &mut egui::Ui, @@ -537,10 +553,6 @@ fn proc_row( write: &str, freq: &str, ) { - const NAME_W: f32 = 140.0; - const WRITE_W: f32 = 100.0; - const FREQ_W: f32 = 80.0; - let name_style = |s: &str| { if is_header { RichText::new(s) @@ -565,23 +577,6 @@ fn proc_row( .color(th.palette.text_secondary) } }; - // Monospace metrics so column edges stay vertical (no length "waterfall"). - let metric = |s: &str, secondary: bool| { - let color = if is_header { - th.palette.text_secondary - } else if secondary { - th.palette.text_secondary - } else { - th.palette.text - }; - let mut rt = RichText::new(s).size(th.type_scale.caption).color(color); - if is_header { - rt = rt.strong(); - } else { - rt = rt.monospace(); - } - rt - }; let name_label = if is_header { name.to_string() @@ -589,11 +584,12 @@ fn proc_row( truncate_chars(name, 28) }; ui.add_sized( - [NAME_W, row_h], + [PROC_NAME_COL_W, row_h], egui::Label::new(name_style(&name_label)).truncate(), ); - let path_w = (ui.available_width() - (WRITE_W + FREQ_W + 8.0)).max(80.0); + let path_w = + (ui.available_width() - (PROC_WRITE_COL_W + PROC_FREQ_COL_W + 8.0)).max(80.0); let path_label = if is_header { path.to_string() } else if path.is_empty() { @@ -606,23 +602,63 @@ fn proc_row( egui::Label::new(path_style(&path_label)).truncate(), ); - // Right-to-left layout actually pins text to the column's right edge; - // Label::halign alone does not (text stayed left → staircase edge). - ui.allocate_ui_with_layout( - egui::vec2(WRITE_W, row_h), - Layout::right_to_left(Align::Center), - |ui| { - ui.set_min_width(WRITE_W); - ui.label(metric(write, false)); - }, + // Pin rates to the column right edge with painter text (Align2::RIGHT_CENTER). + // Label::halign / fragile RTL child UIs left a left-aligned staircase ("waterfall"). + debug_assert_eq!(proc_metric_align(), ProcMetricAlign::RightEdge); + metric_cell_right( + ui, + th, + PROC_WRITE_COL_W, + row_h, + write, + is_header, + false, ); - ui.allocate_ui_with_layout( - egui::vec2(FREQ_W, row_h), - Layout::right_to_left(Align::Center), - |ui| { - ui.set_min_width(FREQ_W); - ui.label(metric(freq, true)); - }, + metric_cell_right( + ui, + th, + PROC_FREQ_COL_W, + row_h, + freq, + is_header, + true, + ); +} + +/// Fixed-width process-table metric cell; text right-edge aligned. +fn metric_cell_right( + ui: &mut egui::Ui, + th: &Theme, + width: f32, + row_h: f32, + text: &str, + is_header: bool, + secondary: bool, +) { + let (rect, _) = ui.allocate_exact_size(egui::vec2(width, row_h), Sense::hover()); + if !ui.is_rect_visible(rect) { + return; + } + let color = if is_header { + th.palette.text_secondary + } else if secondary { + th.palette.text_secondary + } else { + th.palette.text + }; + let font = if is_header { + egui::FontId::proportional(th.type_scale.caption) + } else { + egui::FontId::monospace(th.type_scale.caption) + }; + // Small right pad so glyphs don't clip the cell edge. + let pos = egui::pos2(rect.right() - 4.0, rect.center().y); + ui.painter().text( + pos, + egui::Align2::RIGHT_CENTER, + text, + font, + color, ); } @@ -743,3 +779,112 @@ fn device_card( } }); } + +/// Waterfall regression guards — process-table Write / Write-freq layout. +#[cfg(test)] +mod waterfall_guards { + use super::{ + proc_metric_align, ProcMetricAlign, PROC_FREQ_COL_W, PROC_WRITE_COL_W, + }; + + /// Shipped source of this module (drives real path — not a reimplementation). + fn app_src() -> &'static str { + include_str!("app.rs") + } + + /// Body of `fn proc_table` through the next top-level `fn` (approx). + fn proc_table_src() -> &'static str { + let src = app_src(); + let start = src + .find("fn proc_table(") + .expect("proc_table must exist in shipped app.rs"); + let rest = &src[start..]; + // Next top-level function after proc_table's closing — use anomaly_table. + let end = rest + .find("\nfn anomaly_table(") + .or_else(|| rest.find("\n/// Process | Time | Usage")) + .unwrap_or(rest.len()); + &rest[..end] + } + + fn proc_row_src() -> &'static str { + let src = app_src(); + let start = src + .find("fn proc_row(") + .expect("proc_row must exist in shipped app.rs"); + let rest = &src[start..]; + let end = rest + .find("\nfn metric_cell_right(") + .or_else(|| rest.find("\nfn truncate_chars(")) + .unwrap_or(rest.len()); + &rest[..end] + } + + #[test] + fn no_rate_proportional_bars_under_process_rows() { + let body = proc_table_src(); + // Former waterfall: bar_w = rect.width() * (write_bps / max_write) + assert!( + !body.contains("max_write"), + "proc_table must not compute max_write for bar scaling" + ); + assert!( + !body.contains("bar_w"), + "proc_table must not draw rate-proportional bar_w underlines" + ); + assert!( + !body.contains("bar_h"), + "proc_table must not draw rate-proportional bar underlines" + ); + // Zebra backgrounds are OK; they use rect_filled without write-rate width. + assert!( + body.contains("zebra") || body.contains("rect_filled"), + "proc_table should still paint row backgrounds" + ); + } + + #[test] + fn write_and_freq_use_right_edge_metric_cells() { + assert_eq!(proc_metric_align(), ProcMetricAlign::RightEdge); + assert!(PROC_WRITE_COL_W > 0.0 && PROC_FREQ_COL_W > 0.0); + + let row = proc_row_src(); + assert!( + row.contains("metric_cell_right"), + "proc_row must route Write/Write-freq through metric_cell_right" + ); + assert!( + row.contains("PROC_WRITE_COL_W") && row.contains("PROC_FREQ_COL_W"), + "proc_row must use fixed metric column widths" + ); + + let cell = app_src(); + let cell_start = cell + .find("fn metric_cell_right(") + .expect("metric_cell_right must exist"); + let cell_body = &cell[cell_start..]; + let cell_end = cell_body + .find("\nfn truncate_chars(") + .unwrap_or(400.min(cell_body.len())); + let cell_body = &cell_body[..cell_end]; + assert!( + cell_body.contains("RIGHT_CENTER") || cell_body.contains("Align2::RIGHT_CENTER"), + "metric_cell_right must pin text with Align2::RIGHT_CENTER" + ); + assert!( + cell_body.contains("allocate_exact_size"), + "metric cells must reserve a fixed width" + ); + } + + #[test] + fn process_table_still_lists_required_columns() { + let src = app_src(); + assert!(src.contains("\"Name\"")); + assert!(src.contains("\"Path\"")); + assert!(src.contains("\"Write\"")); + assert!(src.contains("\"Write freq\"")); + assert!(src.contains("format_bps")); + assert!(src.contains("format_hz")); + } +}