1use crate::layout::subpixel_snap_rect;
9use crate::theme::MacTheme;
10use egui::{
11 Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget,
12};
13use std::ops::RangeInclusive;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum MacLevelIndicatorStyle {
18 #[default]
20 Continuous,
21 Discrete,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum MacLevelOrientation {
28 #[default]
30 Horizontal,
31 Vertical,
33}
34
35pub struct MacLevelIndicator<'a> {
56 value: f32,
57 range: RangeInclusive<f32>,
58 warning_value: f32,
59 critical_value: f32,
60 peak_hold_value: Option<f32>,
61 style: MacLevelIndicatorStyle,
62 orientation: MacLevelOrientation,
63 segment_count: Option<usize>,
64 segmented_threshold_colors: bool,
65 tick_marks: usize,
66 length: Option<f32>,
67 thickness: Option<f32>,
68 enabled: bool,
69 theme: Option<&'a MacTheme>,
70}
71
72impl<'a> MacLevelIndicator<'a> {
73 pub fn new(value: f32, range: RangeInclusive<f32>) -> Self {
75 let min_v = *range.start();
76 let max_v = *range.end();
77 let span = max_v - min_v;
78 Self {
79 value,
80 range,
81 warning_value: min_v + span * 0.5,
82 critical_value: min_v + span,
83 peak_hold_value: None,
84 style: MacLevelIndicatorStyle::Continuous,
85 orientation: MacLevelOrientation::Horizontal,
86 segment_count: None,
87 segmented_threshold_colors: false,
88 tick_marks: 0,
89 length: None,
90 thickness: None,
91 enabled: true,
92 theme: None,
93 }
94 }
95
96 pub fn warning_value(mut self, warning: f32) -> Self {
98 self.warning_value = warning;
99 self
100 }
101
102 pub fn critical_value(mut self, critical: f32) -> Self {
104 self.critical_value = critical;
105 self
106 }
107
108 pub fn peak_hold(mut self, peak: f32) -> Self {
110 self.peak_hold_value = Some(peak);
111 self
112 }
113
114 pub fn style(mut self, style: MacLevelIndicatorStyle) -> Self {
116 self.style = style;
117 self
118 }
119
120 pub fn orientation(mut self, orientation: MacLevelOrientation) -> Self {
122 self.orientation = orientation;
123 self
124 }
125
126 pub fn segments(mut self, count: usize) -> Self {
128 self.segment_count = Some(count.max(1));
129 self
130 }
131
132 pub fn segmented_threshold_colors(mut self, enabled: bool) -> Self {
134 self.segmented_threshold_colors = enabled;
135 self
136 }
137
138 pub fn tick_marks(mut self, count: usize) -> Self {
140 self.tick_marks = count;
141 self
142 }
143
144 pub fn length(mut self, length: f32) -> Self {
146 self.length = Some(length);
147 self
148 }
149
150 pub fn thickness(mut self, thickness: f32) -> Self {
152 self.thickness = Some(thickness);
153 self
154 }
155
156 pub fn enabled(mut self, enabled: bool) -> Self {
158 self.enabled = enabled;
159 self
160 }
161
162 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
164 self.theme = Some(theme);
165 self
166 }
167
168 pub fn color_for_value(&self, theme: &MacTheme, sample_value: f32) -> Color32 {
170 if !self.enabled {
171 return theme.selected_control_inactive();
172 }
173 if sample_value >= self.critical_value {
174 theme.system_red()
175 } else if sample_value >= self.warning_value {
176 theme.system_yellow()
177 } else {
178 theme.system_green()
179 }
180 }
181
182 fn normalize(&self, v: f32) -> f32 {
183 let min_v = *self.range.start();
184 let max_v = *self.range.end();
185 let span = (max_v - min_v).max(f32::EPSILON);
186 ((v - min_v) / span).clamp(0.0, 1.0)
187 }
188
189 pub fn show(self, ui: &mut Ui) -> Response {
191 ui.add(self)
192 }
193}
194
195impl<'a> Widget for MacLevelIndicator<'a> {
196 fn ui(self, ui: &mut Ui) -> Response {
197 let fallback_theme;
198 let theme = match self.theme {
199 Some(t) => t,
200 None => {
201 fallback_theme = MacTheme::from_ctx(ui.ctx());
202 &fallback_theme
203 }
204 };
205
206 let lm = theme.level_indicator();
207 let len = self.length.unwrap_or_else(|| lm.default_length.points());
208 let thick = self.thickness.unwrap_or_else(|| lm.thickness.points());
209 let size = match self.orientation {
210 MacLevelOrientation::Horizontal => Vec2::new(len, thick),
211 MacLevelOrientation::Vertical => Vec2::new(thick, len),
212 };
213
214 let (raw_rect, response) = ui.allocate_exact_size(size, Sense::hover());
215 let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
216 let painter = ui.painter_at(rect);
217
218 let outer_r = CornerRadius::same(lm.corner_radius.points() as u8);
219 painter.rect(
220 rect,
221 outer_r,
222 theme.control_background(),
223 Stroke::new(lm.border_width.points(), theme.separator()),
224 StrokeKind::Inside,
225 );
226
227 let inset = lm.track_inset.points();
228 let inner = rect.shrink(inset);
229 let frac = self.normalize(self.value);
230 let min_v = *self.range.start();
231 let max_v = *self.range.end();
232 let span = (max_v - min_v).max(f32::EPSILON);
233
234 match self.style {
235 MacLevelIndicatorStyle::Continuous => {
236 let seg_r = CornerRadius::same(lm.segment_corner_radius.points() as u8);
237 let fill_color = self.color_for_value(theme, self.value);
238 if frac > 0.0 {
239 let fill_rect = match self.orientation {
240 MacLevelOrientation::Horizontal => Rect::from_min_size(
241 inner.min,
242 Vec2::new(inner.width() * frac, inner.height()),
243 ),
244 MacLevelOrientation::Vertical => {
245 let h = inner.height() * frac;
246 Rect::from_min_max(
247 Pos2::new(inner.left(), inner.bottom() - h),
248 inner.max,
249 )
250 }
251 };
252 painter.rect_filled(fill_rect, seg_r, fill_color);
253 }
254 }
255 MacLevelIndicatorStyle::Discrete => {
256 let seg_count = self
257 .segment_count
258 .unwrap_or_else(|| (lm.default_segment_count.points().round() as usize).max(1));
259 let gap = lm.segment_spacing.points();
260 let seg_r = CornerRadius::same(lm.segment_corner_radius.points() as u8);
261 let total_axis = match self.orientation {
262 MacLevelOrientation::Horizontal => inner.width(),
263 MacLevelOrientation::Vertical => inner.height(),
264 };
265 let total_gaps = gap * ((seg_count.saturating_sub(1)) as f32);
266 let seg_len = ((total_axis - total_gaps) / (seg_count as f32)).max(1.0);
267
268 for idx in 0..seg_count {
269 let seg_frac_end = (idx + 1) as f32 / (seg_count as f32);
270 let seg_mid_frac = (idx as f32 + 0.5) / (seg_count as f32);
271 let seg_val = min_v + seg_mid_frac * span;
272 let is_lit = frac >= (idx as f32 / (seg_count as f32))
273 && frac > 0.0
274 && (frac >= seg_frac_end
275 || idx == ((frac * seg_count as f32).floor() as usize));
276
277 let seg_rect = match self.orientation {
278 MacLevelOrientation::Horizontal => {
279 let x = inner.left() + (idx as f32) * (seg_len + gap);
280 Rect::from_min_size(
281 Pos2::new(x, inner.top()),
282 Vec2::new(seg_len, inner.height()),
283 )
284 }
285 MacLevelOrientation::Vertical => {
286 let y_bottom = inner.bottom() - (idx as f32) * (seg_len + gap);
287 Rect::from_min_max(
288 Pos2::new(inner.left(), y_bottom - seg_len),
289 Pos2::new(inner.right(), y_bottom),
290 )
291 }
292 };
293
294 let color = if is_lit {
295 if self.segmented_threshold_colors {
296 self.color_for_value(theme, seg_val)
297 } else {
298 self.color_for_value(theme, self.value)
299 }
300 } else {
301 theme.control()
302 };
303 painter.rect_filled(seg_rect, seg_r, color);
304 }
305 }
306 }
307
308 if self.tick_marks > 0 {
309 let tick_stroke = Stroke::new(lm.border_width.points(), theme.secondary_label());
310 let tick_len = lm.track_inset.points() + lm.border_width.points();
311 let denom = (self.tick_marks.saturating_sub(1)).max(1) as f32;
312 for idx in 0..self.tick_marks {
313 let t_frac = if self.tick_marks == 1 {
314 0.5
315 } else {
316 (idx as f32) / denom
317 };
318 match self.orientation {
319 MacLevelOrientation::Horizontal => {
320 let tx = inner.left() + inner.width() * t_frac;
321 painter.line_segment(
322 [
323 Pos2::new(tx, rect.bottom() - tick_len),
324 Pos2::new(tx, rect.bottom()),
325 ],
326 tick_stroke,
327 );
328 }
329 MacLevelOrientation::Vertical => {
330 let ty = inner.bottom() - inner.height() * t_frac;
331 painter.line_segment(
332 [
333 Pos2::new(rect.right() - tick_len, ty),
334 Pos2::new(rect.right(), ty),
335 ],
336 tick_stroke,
337 );
338 }
339 }
340 }
341 }
342
343 if let Some(peak) = self.peak_hold_value {
344 let peak_frac = self.normalize(peak);
345 let peak_color = self.color_for_value(theme, peak);
346 let peak_stroke = Stroke::new(lm.peak_line_width.points(), peak_color);
347 match self.orientation {
348 MacLevelOrientation::Horizontal => {
349 let px = inner.left() + inner.width() * peak_frac;
350 painter.line_segment(
351 [Pos2::new(px, inner.top()), Pos2::new(px, inner.bottom())],
352 peak_stroke,
353 );
354 }
355 MacLevelOrientation::Vertical => {
356 let py = inner.bottom() - inner.height() * peak_frac;
357 painter.line_segment(
358 [Pos2::new(inner.left(), py), Pos2::new(inner.right(), py)],
359 peak_stroke,
360 );
361 }
362 }
363 }
364
365 response
366 }
367}