Skip to main content

mimium_guitools/
plot_window.rs

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            // The top panel is often a good place for a menu bar:
127
128            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                    // Drain the ring buffer even for hidden channels so
152                    // the producer never blocks.
153                    let (_req_repaint, drawn) = line.draw_line();
154                    // Only display channels that have received data.
155                    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}