From 4a05c12889495e41f4e00ef0d87eb39be99e009e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 6 Oct 2026 17:36:17 -0400 Subject: [PATCH] =?UTF-8?q?Panics=20=C2=B7=20P12=20Data=20Display:=20featu?= =?UTF-8?q?red=20columns,=20gauges,=20usage=20bars=20and=20stars=20meet=20?= =?UTF-8?q?readings=20past=20their=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_display/comparison.rs | 24 +++++++++++++++++------ src/data_display/measures.rs | 36 ++++++++++++---------------------- src/data_display/stars.rs | 4 +++- src/data_display/tests.rs | 27 ++++++++++++++++++++++++- tasks/panics.md | 3 ++- 5 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/data_display/comparison.rs b/src/data_display/comparison.rs index f1877c3..3117e15 100644 --- a/src/data_display/comparison.rs +++ b/src/data_display/comparison.rs @@ -35,12 +35,13 @@ impl Comparison { /// Tints column `ix`, such as the plan you suggest. pub fn featured(mut self, ix: usize) -> Self { - assert!( - ix < self.columns.len(), - "column {ix} is past the last of {}", - self.columns.len() - ); - self.featured = Some(ix); + self.featured = (ix < self.columns.len()).then_some(ix); + if self.featured.is_none() { + log::error!( + "comparison: column {ix} of {}; none featured", + self.columns.len() + ); + } self } @@ -156,6 +157,17 @@ impl RenderOnce for Comparison { mod tests { use super::Comparison; + #[test] + fn a_featured_column_past_the_last_features_none() { + assert_eq!(Comparison::new(["Free", "Pro"]).featured(5).featured, None); + assert_eq!( + Comparison::new(["Free", "Pro"]).featured(1).featured, + Some(1) + ); + let later = Comparison::new(["Free", "Pro"]).featured(1).featured(5); + assert_eq!(later.featured, None, "a later bad index clears the first"); + } + #[test] #[should_panic(expected = "row Storage needs a cell per option")] fn a_row_needs_a_cell_per_option() { diff --git a/src/data_display/measures.rs b/src/data_display/measures.rs index b39dbc8..a89d2b5 100644 --- a/src/data_display/measures.rs +++ b/src/data_display/measures.rs @@ -188,7 +188,7 @@ pub struct Gauge { } impl Gauge { - /// `value` lies within `min..=max`. + /// A `value` past `min..=max` pegs the dial and shows as it is. pub fn new( id: impl Into, label: impl Into, @@ -200,10 +200,6 @@ impl Gauge { min < max, "a gauge needs min below max, got {min} and {max}" ); - assert!( - (min..=max).contains(&value), - "{value} is outside {min}..={max}" - ); Self { id: id.into(), label: label.into(), @@ -236,7 +232,7 @@ impl Gauge { impl RenderOnce for Gauge { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let (min, max) = self.range; - let share = ((self.value - min) / (max - min)) as f32; + let share = ((self.value - min) / (max - min)).clamp(0.0, 1.0) as f32; let theme = cx.theme(); let (limits, track, tones) = (self.thresholds, theme.colors.border, tones(&theme.colors)); let dial = gliding(self.id.clone(), share, window, cx, move |at| { @@ -340,14 +336,9 @@ impl UsageBar { } impl UsageBar { - /// What the parts use and whether they fill the total, within float error. + /// What the parts use and whether they fill the total, within float error; over it they fill it. fn used(&self) -> (f64, bool) { let used: f64 = self.parts.iter().map(|(_, amount)| amount).sum(); - assert!( - used <= self.total * (1.0 + 1e-9), - "the parts ({used}) pass the total ({})", - self.total - ); (used, used >= self.total * (1.0 - 1e-9)) } @@ -365,11 +356,7 @@ impl RenderOnce for UsageBar { let theme = cx.theme(); let colors = &theme.colors; let (used, full) = self.used(); - assert!( - self.parts.len() <= colors.chart.len(), - "a usage bar has colors for {} parts", - colors.chart.len() - ); + let hue = |ix: usize| colors.chart[ix % colors.chart.len()]; let total = self.total; let grown = |share: f64| { let mut part = div().h_full().flex_basis(relative(0.0)); @@ -383,7 +370,7 @@ impl RenderOnce for UsageBar { .h(theme.meter_track()) .children(self.parts.iter().enumerate().map(|(ix, (_, amount))| { grown(amount / total) - .bg(colors.chart[ix]) + .bg(hue(ix)) .when(ix == 0, |part| part.rounded_l_full()) .when(ix == last && full, |part| part.rounded_r_full()) })) @@ -400,12 +387,7 @@ impl RenderOnce for UsageBar { .flex() .items_center() .gap_1p5() - .child( - div() - .size(theme.status_dot()) - .rounded_full() - .bg(colors.chart[ix]), - ) + .child(div().size(theme.status_dot()).rounded_full().bg(hue(ix))) .child(div().text_color(colors.fg).child(name.clone())) .child( tabular(div()) @@ -440,6 +422,12 @@ mod tests { assert_eq!(tone(0.97, limits, &colors), colors.danger); } + #[test] + fn parts_past_the_total_fill_it() { + let over = UsageBar::new(10.0).part("a", 8.0).part("b", 5.0); + assert_eq!(over.used(), (13.0, true)); + } + #[test] fn parts_that_sum_to_the_total_fill_it_despite_float_error() { assert!(UsageBar::new(0.3).part("a", 0.1).part("b", 0.2).used().1); diff --git a/src/data_display/stars.rs b/src/data_display/stars.rs index 14a71de..91d7d74 100644 --- a/src/data_display/stars.rs +++ b/src/data_display/stars.rs @@ -76,7 +76,9 @@ impl Stars { impl RenderOnce for Stars { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let (score, max) = (self.score, self.max); - assert!(score <= max as f32, "a score of {score} is above {max}"); + if score > max as f32 { + log::error!("stars: a score of {score} is above {max}; all filled"); + } let theme = cx.theme(); let (on, off) = (theme.colors.accent, theme.colors.border_strong); let side = theme.icon_size(self.size); diff --git a/src/data_display/tests.rs b/src/data_display/tests.rs index 8d2889a..ba07428 100644 --- a/src/data_display/tests.rs +++ b/src/data_display/tests.rs @@ -9,7 +9,10 @@ use gpui::{ TestAppContext, VisualTestContext, Window, div, point, px, }; -use super::{Badge, Carousel, DescriptionList, PropertyGrid, PropertyGroup, Statistic, Tone}; +use super::{ + Badge, Carousel, DescriptionList, Gauge, PropertyGrid, PropertyGroup, Stars, Statistic, Tone, + UsageBar, +}; use crate::{ primitives::Measure, theme::{TextSize, Theme}, @@ -283,3 +286,25 @@ fn left_from_the_first_slide_wraps_to_the_last(cx: &mut TestAppContext) { settle(cx); assert_eq!(front(&places), Some(0)); } + +/// Readings past their scale: a gauge over its max, stars over theirs, more parts than hues. +struct Over; + +impl Render for Over { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let parts = (0..10).fold(UsageBar::new(100.0), |bar, ix| { + bar.part(format!("p{ix}"), 12.0) + }); + div() + .child(Gauge::new("cpu", "CPU", 140.0, 0.0, 100.0)) + .child(Stars::new(6.5)) + .child(parts) + } +} + +#[gpui::test] +fn readings_past_their_scale_draw(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| Over); + cx.run_until_parked(); +} diff --git a/tasks/panics.md b/tasks/panics.md index 0e2ade9..675ef1c 100644 --- a/tasks/panics.md +++ b/tasks/panics.md @@ -26,7 +26,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca - [x] P09 Overlays — `src/overlays` - [x] P10 Feedback — `src/feedback` - [x] P11 Loading & Motion — `src/motion` -- [ ] P12 Data Display — `src/data_display` +- [x] P12 Data Display — `src/data_display` - [ ] P13 Lists & Trees — `src/lists` - [ ] P14 Tables — `src/tables` - [ ] P15 Charts — `src/charts` @@ -76,3 +76,4 @@ One line per item: what changed, what stayed and why, review rounds. - P09: a `Lightbox` or `Tour` past its last item logs and closes, handing focus back. Kept: dialog Enter without `focus_first` and popover face builders on an owner's opener (static builder misuse). Review: 1 round. - P10: no change. Kept: a toast's stay above zero (one value). Review: 1 round. - P11: a buffer at or behind the value draws none; `slide` was fixed in P05. Kept: values in 0..=1, segment and skeleton counts (one value each), builder misuse, reorder's permutation. Review: 1 round. +- P12: a featured column past the columns features none; a gauge past its scale pegs; usage parts past the total fill it and repeat hues past eight; stars above max log and fill. Kept: one-value contracts, a comparison row's shape, construction. Review: 3 rounds (round 2 met an edit that had not applied).