1use std::{
2 collections::BTreeMap,
3 ops::RangeInclusive,
4 sync::{Arc, Mutex, atomic::Ordering},
5};
6
7use crate::plot_ui::{self, PlotUi};
8use eframe;
9
10use atomic_float::AtomicF64;
11use egui::{Color32, RichText, TextStyle};
12use egui_plot::{CoordinatesFormatter, Corner, Legend, Plot};
13use ringbuf::HeapCons;
14
15pub struct FloatParameter {
16 value: AtomicF64,
17 name: String,
18 range: RangeInclusive<AtomicF64>,
19}
20impl FloatParameter {
21 fn new(name: String, init: f64, min: f64, max: f64) -> Self {
22 Self {
23 value: AtomicF64::new(init),
24 name,
25 range: AtomicF64::new(min)..=AtomicF64::new(max),
26 }
27 }
28 pub fn get(&self) -> f64 {
29 self.value.load(Ordering::Relaxed)
30 }
31 pub fn set(&self, v: f64) {
32 self.value.store(v, Ordering::Relaxed)
33 }
34 pub fn set_range(&self, min: f64, max: f64) {
35 self.range.start().store(min, Ordering::Relaxed);
36 self.range.end().store(max, Ordering::Relaxed);
37 }
38 pub(crate) fn name(&self) -> &str {
39 self.name.as_str()
40 }
41}
42
43#[derive(Default)]
44pub struct PlotApp {
45 plot: Vec<plot_ui::PlotUi>,
46 pub(crate) sliders: Vec<Arc<FloatParameter>>,
47 #[cfg(feature = "osc")]
48 osc_receiver: Option<crate::osc::OscSliderReceiver>,
49 #[cfg(feature = "osc")]
50 osc_init_attempted: bool,
51 hue: f32,
52 autoscale: bool,
53}
54
55impl PlotApp {
56 pub fn new_test() -> Self {
57 let plot = vec![PlotUi::new_test("test")];
58 Self {
59 plot,
60 sliders: Vec::new(),
61 #[cfg(feature = "osc")]
62 osc_receiver: None,
63 #[cfg(feature = "osc")]
64 osc_init_attempted: false,
65 hue: 0.0,
66 autoscale: false,
67 }
68 }
69 const HUE_MARGIN: f32 = 1.0 / 8.0 + 0.3;
70 pub fn add_plot(&mut self, label: &str, buf: HeapCons<f64>) {
71 let [r, g, b] = egui::ecolor::Hsva::new(self.hue, 0.7, 0.7, 1.0).to_srgb();
72 self.hue += Self::HUE_MARGIN;
73 self.plot.push(PlotUi::new(
74 label,
75 buf,
76 Color32::from_rgba_premultiplied(r, g, b, 200),
77 ))
78 }
79 pub fn add_slider(
80 &mut self,
81 name: &str,
82 init: f64,
83 min: f64,
84 max: f64,
85 ) -> (Arc<FloatParameter>, usize) {
86 let param = FloatParameter::new(name.to_string(), init, min, max);
87 let p = Arc::new(param);
88 self.sliders.push(p.clone());
89 (p, self.sliders.len() - 1)
90 }
91 pub fn is_empty(&self) -> bool {
92 self.plot.is_empty()
93 }
94
95 pub fn suggested_viewport_size(&self) -> [f32; 2] {
96 let base_width = 420.0;
97 let top_panel_height = 36.0;
98 let plot_height = if self.plot.is_empty() { 120.0 } else { 220.0 };
99 let slider_header = if self.sliders.is_empty() { 0.0 } else { 24.0 };
100 let slider_rows = self.sliders.len() as f32;
101 let slider_height = slider_rows * 28.0;
102 let margin = 28.0;
103
104 let total_height = top_panel_height + plot_height + slider_header + slider_height + margin;
105
106 [base_width, total_height.clamp(260.0, 960.0)]
107 }
108}
109
110impl eframe::App for PlotApp {
111 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
112 if self.plot.is_empty() && self.sliders.is_empty() {
113 return;
114 }
115 #[cfg(feature = "osc")]
116 {
117 if !self.osc_init_attempted {
118 self.osc_receiver = crate::osc::OscSliderReceiver::from_env();
119 self.osc_init_attempted = true;
120 }
121 if let Some(receiver) = self.osc_receiver.as_mut() {
122 receiver.poll_and_apply(&self.sliders);
123 }
124 }
125 egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
126 egui::menu::bar(ui, |ui| {
129 egui::widgets::global_theme_preference_buttons(ui);
130 ui.add_space(16.0);
131 use egui::special_emojis::GITHUB;
132 ui.hyperlink_to(
133 format!("{GITHUB} mimium-rs on GitHub"),
134 "https://github.com/tomoyanonymous/mimium-rs",
135 );
136 ui.checkbox(&mut self.autoscale, "Auto Scale")
137 });
138 });
139
140 egui::CentralPanel::default().show(ctx, |ui| {
141 let plot = Plot::new("lines_demo")
142 .legend(Legend::default())
143 .show_axes(true)
144 .show_grid(true)
145 .allow_scroll(false)
146 .auto_bounds([true, self.autoscale].into())
147 .coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default());
148
149 plot.show(ui, |plot_ui| {
150 self.plot.iter_mut().for_each(|line| {
151 let (_req_repaint, drawn) = line.draw_line();
154 if line.has_data() {
156 plot_ui.line(drawn);
157 }
158 })
159 });
160
161 ui.ctx().request_repaint();
162 });
163 egui::TopBottomPanel::bottom("parameters")
164 .resizable(true)
165 .default_height(220.0)
166 .min_height(10.0)
167 .show(ctx, |ui| {
168 if !self.sliders.is_empty() {
169 ui.label("Parameters");
170 }
171
172 let grouped_sliders = self.sliders.iter().fold(
173 BTreeMap::<String, Vec<Arc<FloatParameter>>>::new(),
174 |mut groups, slider| {
175 let group_name = slider
176 .name
177 .rsplit_once('.')
178 .map(|(group, _)| group.to_string())
179 .unwrap_or_default();
180 groups.entry(group_name).or_default().push(slider.clone());
181 groups
182 },
183 );
184
185 egui::ScrollArea::vertical().show(ui, |ui| {
186 grouped_sliders.iter().for_each(|(group_name, sliders)| {
187 let draw_group = |ui: &mut egui::Ui| {
188 const LABEL_WIDTH: f32 = 80.0;
189 const MINMAX_WIDTH: f32 = 60.0;
190 const CURRENT_WIDTH: f32 = 60.0;
191
192 sliders.iter().for_each(|slider| {
193 let mut value = slider.get();
194 let mut min = slider.range.start().load(Ordering::Relaxed);
195 let mut max = slider.range.end().load(Ordering::Relaxed);
196 let min_id = egui::Id::new(("slider_min", slider.name.as_str()));
197 let max_id = egui::Id::new(("slider_max", slider.name.as_str()));
198
199 if let Some(saved_min) =
200 ui.ctx().data_mut(|d| d.get_persisted::<f64>(min_id))
201 {
202 min = saved_min;
203 }
204 if let Some(saved_max) =
205 ui.ctx().data_mut(|d| d.get_persisted::<f64>(max_id))
206 {
207 max = saved_max;
208 }
209
210 let label = slider
211 .name
212 .rsplit_once('.')
213 .map(|(_, leaf)| leaf)
214 .unwrap_or(slider.name.as_str());
215
216 let mut min_edited = false;
217 let mut max_edited = false;
218 let slider_changed = ui
219 .horizontal(|ui| {
220 ui.add_sized([LABEL_WIDTH, 0.0], egui::Label::new(label));
221
222 min_edited = ui
223 .scope(|ui| {
224 ui.style_mut().override_text_style =
225 Some(TextStyle::Small);
226 ui.add_sized(
227 [MINMAX_WIDTH, 0.0],
228 egui::DragValue::new(&mut min)
229 .speed(0.1)
230 .max_decimals(6),
231 )
232 .changed()
233 })
234 .inner;
235
236 let is_fine_adjust =
237 ui.input(|input| input.modifiers.shift);
238 let base_range = (max - min).abs();
239 let fine_step = (base_range / 10_000.0).max(1e-9);
240 let slider_widget = if is_fine_adjust {
241 egui::Slider::new(&mut value, min..=max)
242 .clamping(egui::SliderClamping::Always)
243 .show_value(false)
244 .smart_aim(false)
245 .step_by(fine_step)
246 } else {
247 egui::Slider::new(&mut value, min..=max)
248 .clamping(egui::SliderClamping::Always)
249 .show_value(false)
250 };
251
252 let slider_changed = ui.add(slider_widget).changed();
253
254 max_edited = ui
255 .scope(|ui| {
256 ui.style_mut().override_text_style =
257 Some(TextStyle::Small);
258 ui.add_sized(
259 [MINMAX_WIDTH, 0.0],
260 egui::DragValue::new(&mut max)
261 .speed(0.1)
262 .max_decimals(6),
263 )
264 .changed()
265 })
266 .inner;
267
268 ui.add_sized(
269 [CURRENT_WIDTH, 0.0],
270 egui::Label::new(
271 RichText::new(format!("{value:.4}"))
272 .text_style(TextStyle::Small),
273 ),
274 );
275
276 slider_changed
277 })
278 .inner;
279
280 if min_edited || max_edited {
281 if min > max {
282 if min_edited {
283 max = min;
284 } else {
285 min = max;
286 }
287 }
288 slider.set_range(min, max);
289 }
290
291 ui.ctx().data_mut(|d| {
292 d.insert_persisted(min_id, min);
293 d.insert_persisted(max_id, max);
294 });
295
296 if slider_changed {
297 slider.set(value);
298 }
299 });
300 };
301
302 if group_name.is_empty() {
303 draw_group(ui);
304 } else {
305 ui.collapsing(group_name, draw_group);
306 }
307 });
308 });
309 });
310 }
311}
312
313pub struct AsyncPlotApp {
314 pub window: Arc<Mutex<PlotApp>>,
315}
316
317impl eframe::App for AsyncPlotApp {
318 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
319 if let Ok(mut window) = self.window.lock() {
320 window.update(ctx, _frame);
321 }
322 }
323}