diff --git a/src/app.rs b/src/app.rs index fb6c88d..58cdd8c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -248,10 +248,13 @@ impl UsageApp { }; let color = heat_color(th, frac); th.card_frame().show(ui, |ui| { - ui.set_width(cell_w - 1.0); - ui.vertical_centered(|ui| { + // Non-justified: sibling cards in a wrap row can pass extra + // height; justified layout would stretch gaps in the table. + ui.with_layout(Layout::top_down(Align::Center), |ui| { + ui.set_width(cell_w - 1.0); + ui.spacing_mut().item_spacing.y = th.spacing.xs.max(2.0); + dim_label(ui, th, caption); - ui.add_space(th.spacing.xs.max(2.0)); arc_gauge( ui, th, @@ -261,14 +264,16 @@ impl UsageApp { label, color, ); - ui.add_space(th.spacing.xs.max(2.0)); dim_label(ui, th, &format!("scale {}", format_bps(max))); + + if !anomalies.is_empty() { + ui.add_space(th.spacing.sm); + ui.with_layout(Layout::top_down(Align::Min), |ui| { + ui.set_width(ui.available_width()); + anomaly_table(ui, th, anomalies); + }); + } }); - if !anomalies.is_empty() { - ui.add_space(th.spacing.sm); - // Full width under the gauge; left-aligned table. - anomaly_table(ui, th, anomalies); - } }); } }); @@ -389,135 +394,46 @@ impl UsageApp { /// Compact table under a disk gauge: Process | Time | Usage. fn anomaly_table(ui: &mut egui::Ui, th: &Theme, hits: &[AnomalyHit]) { - let row_h = th.spacing.control_height * 0.7; - let avail = ui.available_width(); - const TIME_W: f32 = 100.0; - const USAGE_W: f32 = 76.0; - let name_w = (avail - TIME_W - USAGE_W - th.spacing.sm * 2.0).max(48.0); - - // Header - anomaly_table_row( - ui, - th, - row_h, - true, - "Process", - "Time", - "Usage", - name_w, - TIME_W, - USAGE_W, - ); - - // Newest first - for (i, hit) in hits.iter().rev().enumerate() { - let zebra = if i % 2 == 0 { - th.palette.view_bg - } else { - th.palette.card_bg - }; - let (rect, _) = - ui.allocate_exact_size(egui::vec2(ui.available_width(), row_h), Sense::hover()); - ui.painter().rect_filled(rect, 0.0, zebra); - ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| { - anomaly_table_row( - ui, - th, - row_h, - false, - &hit.proc_name, - &hit.stamp, - &format_bps(hit.rate_bps), - name_w, - TIME_W, - USAGE_W, - ); - }); - } -} - -fn anomaly_table_row( - ui: &mut egui::Ui, - th: &Theme, - row_h: f32, - is_header: bool, - process: &str, - time: &str, - usage: &str, - name_w: f32, - time_w: f32, - usage_w: f32, -) { - if is_header { - let (rect, _) = - ui.allocate_exact_size(egui::vec2(ui.available_width(), row_h), Sense::hover()); - ui.painter() - .rect_filled(rect, th.spacing.radius_sm, th.palette.headerbar_bg); - ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| { - anomaly_table_cells( - ui, th, row_h, true, process, time, usage, name_w, time_w, usage_w, - ); + // Tight grid — no justified gaps when the parent card is stretched tall. + ui.spacing_mut().item_spacing = egui::vec2(th.spacing.sm, 2.0); + + egui::Grid::new(ui.id().with("anomaly_hits")) + .num_columns(3) + .min_col_width(40.0) + .spacing([th.spacing.sm, 2.0]) + .striped(true) + .show(ui, |ui| { + let head = |s: &str| { + RichText::new(s) + .size(th.type_scale.caption) + .strong() + .color(th.palette.text_secondary) + }; + ui.label(head("Process")); + ui.label(head("Time")); + ui.label(head("Usage")); + ui.end_row(); + + for hit in hits.iter().rev() { + ui.label( + RichText::new(truncate_chars(&hit.proc_name, 20)) + .size(th.type_scale.caption) + .strong() + .color(th.palette.warning), + ); + ui.label( + RichText::new(&hit.stamp) + .size(th.type_scale.caption) + .color(th.palette.text_secondary), + ); + ui.label( + RichText::new(format_bps(hit.rate_bps)) + .size(th.type_scale.caption) + .color(th.palette.text_secondary), + ); + ui.end_row(); + } }); - } else { - anomaly_table_cells( - ui, th, row_h, false, process, time, usage, name_w, time_w, usage_w, - ); - } -} - -fn anomaly_table_cells( - ui: &mut egui::Ui, - th: &Theme, - row_h: f32, - is_header: bool, - process: &str, - time: &str, - usage: &str, - name_w: f32, - time_w: f32, - usage_w: f32, -) { - let style = |s: &str, warn: bool| { - if is_header { - RichText::new(s) - .size(th.type_scale.caption) - .strong() - .color(th.palette.text_secondary) - } else if warn { - RichText::new(s) - .size(th.type_scale.caption) - .strong() - .color(th.palette.warning) - } else { - RichText::new(s) - .size(th.type_scale.caption) - .color(th.palette.text_secondary) - } - }; - - ui.horizontal(|ui| { - ui.set_min_height(row_h); - ui.add_space(th.spacing.xs.max(2.0)); - let name = if is_header { - process.to_string() - } else { - truncate_chars(process, 20) - }; - ui.add_sized( - [name_w, row_h], - egui::Label::new(style(&name, true)).truncate(), - ); - ui.add_sized( - [time_w, row_h], - egui::Label::new(style(time, false)).truncate(), - ); - ui.add_sized( - [usage_w, row_h], - egui::Label::new(style(usage, false)) - .halign(Align::RIGHT) - .truncate(), - ); - }); } fn header_row(ui: &mut egui::Ui, th: &Theme, row_h: f32) {