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 theme_cow = MacTheme::resolve(self.theme, ui.ctx());
198 let theme: &MacTheme = &theme_cow;
199
200 let lm = theme.level_indicator();
201 let len = self.length.unwrap_or_else(|| lm.default_length.points());
202 let thick = self.thickness.unwrap_or_else(|| lm.thickness.points());
203 let size = match self.orientation {
204 MacLevelOrientation::Horizontal => Vec2::new(len, thick),
205 MacLevelOrientation::Vertical => Vec2::new(thick, len),
206 };
207
208 let (raw_rect, response) = ui.allocate_exact_size(size, Sense::hover());
209 let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
210 let painter = ui.painter_at(rect);
211
212 let outer_r = CornerRadius::same(lm.corner_radius.points() as u8);
213 painter.rect(
214 rect,
215 outer_r,
216 theme.control_background(),
217 Stroke::new(lm.border_width.points(), theme.separator()),
218 StrokeKind::Inside,
219 );
220
221 let inset = lm.track_inset.points();
222 let inner = rect.shrink(inset);
223 let frac = self.normalize(self.value);
224 let min_v = *self.range.start();
225 let max_v = *self.range.end();
226 let span = (max_v - min_v).max(f32::EPSILON);
227
228 match self.style {
229 MacLevelIndicatorStyle::Continuous => {
230 let seg_r = CornerRadius::same(lm.segment_corner_radius.points() as u8);
231 let fill_color = self.color_for_value(theme, self.value);
232 if frac > 0.0 {
233 let fill_rect = match self.orientation {
234 MacLevelOrientation::Horizontal => Rect::from_min_size(
235 inner.min,
236 Vec2::new(inner.width() * frac, inner.height()),
237 ),
238 MacLevelOrientation::Vertical => {
239 let h = inner.height() * frac;
240 Rect::from_min_max(
241 Pos2::new(inner.left(), inner.bottom() - h),
242 inner.max,
243 )
244 }
245 };
246 painter.rect_filled(fill_rect, seg_r, fill_color);
247 }
248 }
249 MacLevelIndicatorStyle::Discrete => {
250 let seg_count = self
251 .segment_count
252 .unwrap_or_else(|| (lm.default_segment_count.points().round() as usize).max(1));
253 let gap = lm.segment_spacing.points();
254 let seg_r = CornerRadius::same(lm.segment_corner_radius.points() as u8);
255 let total_axis = match self.orientation {
256 MacLevelOrientation::Horizontal => inner.width(),
257 MacLevelOrientation::Vertical => inner.height(),
258 };
259 let total_gaps = gap * ((seg_count.saturating_sub(1)) as f32);
260 let seg_len = ((total_axis - total_gaps) / (seg_count as f32)).max(1.0);
261
262 for idx in 0..seg_count {
263 let seg_frac_end = (idx + 1) as f32 / (seg_count as f32);
264 let seg_mid_frac = (idx as f32 + 0.5) / (seg_count as f32);
265 let seg_val = min_v + seg_mid_frac * span;
266 let is_lit = frac >= (idx as f32 / (seg_count as f32))
267 && frac > 0.0
268 && (frac >= seg_frac_end
269 || idx == ((frac * seg_count as f32).floor() as usize));
270
271 let seg_rect = match self.orientation {
272 MacLevelOrientation::Horizontal => {
273 let x = inner.left() + (idx as f32) * (seg_len + gap);
274 Rect::from_min_size(
275 Pos2::new(x, inner.top()),
276 Vec2::new(seg_len, inner.height()),
277 )
278 }
279 MacLevelOrientation::Vertical => {
280 let y_bottom = inner.bottom() - (idx as f32) * (seg_len + gap);
281 Rect::from_min_max(
282 Pos2::new(inner.left(), y_bottom - seg_len),
283 Pos2::new(inner.right(), y_bottom),
284 )
285 }
286 };
287
288 let color = if is_lit {
289 if self.segmented_threshold_colors {
290 self.color_for_value(theme, seg_val)
291 } else {
292 self.color_for_value(theme, self.value)
293 }
294 } else {
295 theme.control()
296 };
297 painter.rect_filled(seg_rect, seg_r, color);
298 }
299 }
300 }
301
302 if self.tick_marks > 0 {
303 let tick_stroke = Stroke::new(lm.border_width.points(), theme.secondary_label());
304 let tick_len = lm.track_inset.points() + lm.border_width.points();
305 let denom = (self.tick_marks.saturating_sub(1)).max(1) as f32;
306 for idx in 0..self.tick_marks {
307 let t_frac = if self.tick_marks == 1 {
308 0.5
309 } else {
310 (idx as f32) / denom
311 };
312 match self.orientation {
313 MacLevelOrientation::Horizontal => {
314 let tx = inner.left() + inner.width() * t_frac;
315 painter.line_segment(
316 [
317 Pos2::new(tx, rect.bottom() - tick_len),
318 Pos2::new(tx, rect.bottom()),
319 ],
320 tick_stroke,
321 );
322 }
323 MacLevelOrientation::Vertical => {
324 let ty = inner.bottom() - inner.height() * t_frac;
325 painter.line_segment(
326 [
327 Pos2::new(rect.right() - tick_len, ty),
328 Pos2::new(rect.right(), ty),
329 ],
330 tick_stroke,
331 );
332 }
333 }
334 }
335 }
336
337 if let Some(peak) = self.peak_hold_value {
338 let peak_frac = self.normalize(peak);
339 let peak_color = self.color_for_value(theme, peak);
340 let peak_stroke = Stroke::new(lm.peak_line_width.points(), peak_color);
341 match self.orientation {
342 MacLevelOrientation::Horizontal => {
343 let px = inner.left() + inner.width() * peak_frac;
344 painter.line_segment(
345 [Pos2::new(px, inner.top()), Pos2::new(px, inner.bottom())],
346 peak_stroke,
347 );
348 }
349 MacLevelOrientation::Vertical => {
350 let py = inner.bottom() - inner.height() * peak_frac;
351 painter.line_segment(
352 [Pos2::new(inner.left(), py), Pos2::new(inner.right(), py)],
353 peak_stroke,
354 );
355 }
356 }
357 }
358
359 response
360 }
361}
362
363pub fn calculate_pan_weights(pan: f32) -> (f32, f32) {
365 let pan_clamped = pan.clamp(-1.0, 1.0);
366 let pan_l = ((1.0 - pan_clamped) * 0.5).sqrt();
367 let pan_r = ((1.0 + pan_clamped) * 0.5).sqrt();
368 (pan_l, pan_r)
369}