diff --git a/src/app.rs b/src/app.rs index 5847012..fb6c88d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -263,11 +263,12 @@ impl UsageApp { ); 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); - anomaly_list(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); + } }); } }); @@ -386,34 +387,139 @@ impl UsageApp { } } -fn anomaly_list(ui: &mut egui::Ui, th: &Theme, hits: &[AnomalyHit]) { - // Newest first under the gauge. +/// 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() { - if i > 0 { - ui.add_space(th.spacing.xs.max(2.0)); - } - ui.vertical(|ui| { - ui.label( - RichText::new(&hit.proc_name) - .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), + 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, ); - // Rate on its own line so it doesn't crowd the name/time. - ui.label( - RichText::new(format_bps(hit.rate_bps)) - .size(th.type_scale.caption) - .color(th.palette.text_secondary), + }); + } +} + +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, ); }); + } 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) { let (rect, _) = ui.allocate_exact_size(egui::vec2(ui.available_width(), row_h), Sense::hover());