1use crate::Theme;
32use egui::{Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum SpectrumColorMode {
37 #[default]
39 Solid,
40 Gradient,
42 Rainbow,
44}
45
46pub struct Spectrum<'a> {
48 bins: &'a [f32],
49 bands: usize,
50 height: Option<f32>,
51 color_mode: SpectrumColorMode,
52 peak_hold: bool,
53 peaks: Option<&'a [f32]>,
54 mirrored: bool,
55 bar_gap: f32,
56}
57
58impl<'a> Spectrum<'a> {
59 pub fn new(bins: &'a [f32]) -> Self {
63 Self {
64 bins,
65 bands: 32,
66 height: None,
67 color_mode: SpectrumColorMode::default(),
68 peak_hold: false,
69 peaks: None,
70 mirrored: false,
71 bar_gap: 2.0,
72 }
73 }
74
75 pub fn bands(mut self, bands: usize) -> Self {
77 self.bands = bands.max(1);
78 self
79 }
80
81 pub fn height(mut self, height: f32) -> Self {
83 self.height = Some(height);
84 self
85 }
86
87 pub fn color_mode(mut self, mode: SpectrumColorMode) -> Self {
89 self.color_mode = mode;
90 self
91 }
92
93 pub fn gradient(mut self) -> Self {
95 self.color_mode = SpectrumColorMode::Gradient;
96 self
97 }
98
99 pub fn rainbow(mut self) -> Self {
101 self.color_mode = SpectrumColorMode::Rainbow;
102 self
103 }
104
105 pub fn peak_hold(mut self, enabled: bool) -> Self {
107 self.peak_hold = enabled;
108 self
109 }
110
111 pub fn peaks(mut self, peaks: &'a [f32]) -> Self {
113 self.peaks = Some(peaks);
114 self.peak_hold = true;
115 self
116 }
117
118 pub fn mirrored(mut self, enabled: bool) -> Self {
120 self.mirrored = enabled;
121 self
122 }
123
124 pub fn bar_gap(mut self, gap: f32) -> Self {
126 self.bar_gap = gap;
127 self
128 }
129
130 pub fn show(self, ui: &mut Ui) -> Response {
132 let theme = Theme::current(ui.ctx());
133
134 let height = self.height.unwrap_or(theme.spacing_xl * 3.0);
136 let width = ui.available_width();
137
138 let (rect, response) = ui.allocate_exact_size(Vec2::new(width, height), Sense::hover());
139
140 if ui.is_rect_visible(rect) {
141 let painter = ui.painter();
142
143 painter.rect_filled(rect, theme.radius_sm, theme.bg_secondary);
145
146 let display_bands = if self.mirrored {
148 self.bands / 2
149 } else {
150 self.bands
151 };
152
153 let total_gap = self.bar_gap * (display_bands.saturating_sub(1)) as f32;
154 let bar_width = if self.mirrored {
155 (width - total_gap) / display_bands as f32 / 2.0 - self.bar_gap / 2.0
156 } else {
157 (width - total_gap) / display_bands as f32
158 };
159
160 let bins_per_band = self.bins.len() / display_bands.max(1);
161
162 for i in 0..display_bands {
164 let start = i * bins_per_band;
166 let end = ((i + 1) * bins_per_band).min(self.bins.len());
167 let slice = &self.bins[start..end];
168
169 let value = if slice.is_empty() {
170 0.0
171 } else {
172 slice.iter().cloned().fold(0.0_f32, f32::max)
174 };
175
176 let bar_height = value.clamp(0.0, 1.0) * (height - theme.spacing_xs * 2.0);
177
178 let color = self.get_bar_color(i, display_bands, value, &theme);
180
181 if self.mirrored {
182 let x_right = rect.center().x
184 + (i as f32 * (bar_width + self.bar_gap))
185 + self.bar_gap / 2.0;
186 let bar_rect = Rect::from_min_max(
187 Pos2::new(x_right, rect.max.y - theme.spacing_xs - bar_height),
188 Pos2::new(x_right + bar_width, rect.max.y - theme.spacing_xs),
189 );
190 painter.rect_filled(bar_rect, theme.radius_sm * 0.5, color);
191
192 let x_left = rect.center().x
194 - (i as f32 * (bar_width + self.bar_gap))
195 - bar_width
196 - self.bar_gap / 2.0;
197 let bar_rect_left = Rect::from_min_max(
198 Pos2::new(x_left, rect.max.y - theme.spacing_xs - bar_height),
199 Pos2::new(x_left + bar_width, rect.max.y - theme.spacing_xs),
200 );
201 painter.rect_filled(bar_rect_left, theme.radius_sm * 0.5, color);
202
203 if self.peak_hold {
205 let peak_value =
206 self.peaks.and_then(|p| p.get(i).cloned()).unwrap_or(value);
207 let peak_y = rect.max.y
208 - theme.spacing_xs
209 - peak_value.clamp(0.0, 1.0) * (height - theme.spacing_xs * 2.0);
210
211 painter.line_segment(
213 [
214 Pos2::new(x_right, peak_y),
215 Pos2::new(x_right + bar_width, peak_y),
216 ],
217 Stroke::new(theme.stroke_width * 2.0, theme.primary),
218 );
219 painter.line_segment(
221 [
222 Pos2::new(x_left, peak_y),
223 Pos2::new(x_left + bar_width, peak_y),
224 ],
225 Stroke::new(theme.stroke_width * 2.0, theme.primary),
226 );
227 }
228 } else {
229 let x = rect.min.x + (i as f32 * (bar_width + self.bar_gap));
231 let bar_rect = Rect::from_min_max(
232 Pos2::new(x, rect.max.y - theme.spacing_xs - bar_height),
233 Pos2::new(x + bar_width, rect.max.y - theme.spacing_xs),
234 );
235 painter.rect_filled(bar_rect, theme.radius_sm * 0.5, color);
236
237 if self.peak_hold {
239 let peak_value =
240 self.peaks.and_then(|p| p.get(i).cloned()).unwrap_or(value);
241 let peak_y = rect.max.y
242 - theme.spacing_xs
243 - peak_value.clamp(0.0, 1.0) * (height - theme.spacing_xs * 2.0);
244
245 painter.line_segment(
246 [Pos2::new(x, peak_y), Pos2::new(x + bar_width, peak_y)],
247 Stroke::new(theme.stroke_width * 2.0, theme.primary),
248 );
249 }
250 }
251 }
252
253 painter.rect_stroke(
255 rect,
256 theme.radius_sm,
257 Stroke::new(theme.border_width, theme.border),
258 egui::StrokeKind::Outside,
259 );
260 }
261
262 response
263 }
264
265 fn get_bar_color(&self, index: usize, total: usize, value: f32, theme: &Theme) -> Color32 {
266 match self.color_mode {
267 SpectrumColorMode::Solid => theme.primary,
268 SpectrumColorMode::Gradient => {
269 let t = value.clamp(0.0, 1.0);
271 Color32::from_rgb(
272 lerp_u8(theme.secondary.r(), theme.primary.r(), t),
273 lerp_u8(theme.secondary.g(), theme.primary.g(), t),
274 lerp_u8(theme.secondary.b(), theme.primary.b(), t),
275 )
276 }
277 SpectrumColorMode::Rainbow => {
278 let hue = (index as f32 / total as f32) * 360.0;
280 hsv_to_rgb(hue, 0.8, 0.9)
281 }
282 }
283 }
284}
285
286impl Widget for Spectrum<'_> {
287 fn ui(self, ui: &mut Ui) -> Response {
288 self.show(ui)
289 }
290}
291
292fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
294 (a as f32 + (b as f32 - a as f32) * t) as u8
295}
296
297fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Color32 {
298 let c = v * s;
299 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
300 let m = v - c;
301
302 let (r, g, b) = match (h / 60.0) as i32 {
303 0 => (c, x, 0.0),
304 1 => (x, c, 0.0),
305 2 => (0.0, c, x),
306 3 => (0.0, x, c),
307 4 => (x, 0.0, c),
308 _ => (c, 0.0, x),
309 };
310
311 Color32::from_rgb(
312 ((r + m) * 255.0) as u8,
313 ((g + m) * 255.0) as u8,
314 ((b + m) * 255.0) as u8,
315 )
316}