Skip to main content

herogpui_components/
meter.rs

1//! Meter — HeroUI v3 `Meter` (value within a known range).
2//!
3//! A meter reports a static measurement inside a known range (disk usage,
4//! password strength), where a `ProgressBar` reports advancing work. The two
5//! share their track geometry, so this delegates the bar rendering to
6//! [`ProgressBar`].
7
8use gpui::{App, IntoElement, Pixels, RenderOnce, SharedString, Window};
9use herogpui_core::{Color, Size};
10
11use crate::progress::ProgressBar;
12
13/// HeroUI Meter. Supports `value` in `0..max` with
14/// optional label and fill color.
15#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
16#[derive(IntoElement)]
17pub struct Meter {
18    id: gpui::ElementId,
19    value: f32,
20    min_value: f32,
21    max_value: f32,
22    size: Size,
23    color: Color,
24    label: Option<SharedString>,
25    value_label: Option<SharedString>,
26    /// `Meter.ValueLabel`'s render props (`percentage`, `valueText`), forwarded
27    /// to the bar that draws them.
28    value_content: Option<std::sync::Arc<dyn Fn(f32, &str) -> gpui::AnyElement + 'static>>,
29    show_value: bool,
30    /// `formatOptions` — forwarded to the bar, which writes the label.
31    format: Option<herogpui_core::NumberFormat>,
32    /// The corner radius, forwarded to the bar that paints the track and its
33    /// fill.
34    radius: Option<Pixels>,
35    /// The `sx` slot, refined over the root style at the end of render.
36    sx: Option<Box<gpui::StyleRefinement>>,
37}
38
39impl Meter {
40    /// `value` — also accepted positionally by [`Meter::new`].
41    pub fn value(mut self, value: f32) -> Self {
42        self.value = value;
43        self
44    }
45
46    /// `Meter.ValueLabel`'s render function — handed `percentage` (0-100) and
47    /// `valueText`, like the bar's.
48    pub fn value_content(
49        mut self,
50        render: impl Fn(f32, &str) -> gpui::AnyElement + 'static,
51    ) -> Self {
52        self.value_content = Some(std::sync::Arc::new(render));
53        self
54    }
55
56    /// Creates a meter with the given id and value.
57    pub fn new(id: impl Into<gpui::ElementId>, value: f32) -> Self {
58        Self {
59            id: id.into(),
60            value_content: None,
61            value,
62            min_value: 0.0,
63            max_value: 100.0,
64            size: Size::Md,
65            color: Color::Accent,
66            label: None,
67            value_label: None,
68            show_value: false,
69            format: None,
70            radius: None,
71            sx: None,
72        }
73    }
74
75    /// Sets the minimum value of the range.
76    pub fn min_value(mut self, v: f32) -> Self {
77        self.min_value = v;
78        self
79    }
80
81    /// Sets the maximum value of the range.
82    pub fn max_value(mut self, v: f32) -> Self {
83        self.max_value = v;
84        self
85    }
86
87    /// `valueLabel` — replaces the generated percentage.
88    pub fn value_label(mut self, text: impl Into<SharedString>) -> Self {
89        self.value_label = Some(text.into());
90        self
91    }
92
93    /// `formatOptions` — v3 defaults to `{style: "percent"}`.
94    pub fn format_options(mut self, format: herogpui_core::NumberFormat) -> Self {
95        self.format = Some(format);
96        self
97    }
98
99    /// The corner radius, forwarded to the bar the meter delegates to: the bar
100    /// paints the track and its fill, so the override travels with the
101    /// delegation the same way the `sx` slot does. Not a v3 prop; the removed
102    /// v2 `radius` prop is prohibited and this is a per-component repository
103    /// extension.
104    pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
105        self.radius = Some(radius.into());
106        self
107    }
108
109    /// Sets the meter size.
110    pub fn size(mut self, s: Size) -> Self {
111        self.size = s;
112        self
113    }
114
115    /// Sets the color role.
116    pub fn color(mut self, c: Color) -> Self {
117        self.color = c;
118        self
119    }
120
121    /// The one slot for caller-owned low-level styling: GPUI's styling methods
122    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
123    /// applied to the meter's root element after every value the size and the
124    /// active theme chose, so they win. The meter draws no root of its own —
125    /// the bar it delegates to is the meter's root — so the override travels
126    /// down with it.
127    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
128        crate::util::refine_sx(&mut self.sx, style);
129        self
130    }
131
132    /// Sets the label text.
133    pub fn label(mut self, l: impl Into<SharedString>) -> Self {
134        self.label = Some(l.into());
135        self
136    }
137
138    /// Show `value/max` next to the label (like `Meter.Output`).
139    pub fn show_value(mut self, v: bool) -> Self {
140        self.show_value = v;
141        self
142    }
143}
144
145impl RenderOnce for Meter {
146    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
147        // `useMeter` delegates every attribute to `useProgressBar` and
148        // changes only the role, so the delegation carries it.
149        let mut p = ProgressBar::new(self.id)
150            .as_meter()
151            .value(self.value)
152            .min_value(self.min_value)
153            .max_value(self.max_value)
154            .size(self.size)
155            .color(self.color)
156            .show_value_label(self.show_value);
157        if let Some(format) = self.format.clone() {
158            p = p.format_options(format);
159        }
160        if let Some(render) = self.value_content {
161            p = p.value_content(move |percentage, text, _| render(percentage, text));
162        }
163        if let Some(vl) = self.value_label {
164            p = p.value_label(vl);
165        }
166        if let Some(l) = self.label {
167            p = p.label(l.to_string());
168        }
169        // The bar owns the geometry: it paints the track and its fill, so the
170        // radius rides the delegation like the `sx` refinement below.
171        if let Some(radius) = self.radius {
172            p = p.radius(radius);
173        }
174        // The bar's root is this meter's root: hand the refinement down rather
175        // than box the bar in a wrapper of its own.
176        if let Some(sx) = self.sx {
177            p = p.sx_refinement(sx);
178        }
179        p.into_any_element()
180    }
181}
182
183crate::util::impl_component_styled!(Meter);