Skip to main content

features/
features.rs

1//! Example: Feature flags demo
2//!
3//! What it demonstrates
4//! - Dynamically toggling the boolean options defined in
5//!   [`LivePlotConfig::features`] via UI checkboxes.
6//! - Using a `LivePlotApp` instance and feeding it a simple
7//!   sine/cosine waveform (basically the same producer used in the
8//!   `sine_cosine` example).
9//!
10//! The checkboxes appear at the top of the window and will modify
11//! various parts of the live-plot UI as the user flips them.  Not all
12//! flags have a visible effect (some are placeholders in the
13//! configuration), but this example shows how to inspect and apply the
14//! settings at runtime.
15//!
16//! How to run
17//! ```bash
18//! cargo run --example features
19//! ```
20
21use eframe::{egui, NativeOptions};
22use liveplot::config::ScopeButton;
23use liveplot::{channel_plot, FeatureFlags, LivePlotApp, PlotPoint};
24use std::time::{Duration, SystemTime, UNIX_EPOCH};
25
26/// Application state for the example.
27struct FeaturesApp {
28    plot: LivePlotApp,
29    features: FeatureFlags,
30    // producer handles so we can push data from a background thread
31    _sink: liveplot::PlotSink,
32    _tr_sine: liveplot::Trace,
33    _tr_cos: liveplot::Trace,
34}
35
36impl FeaturesApp {
37    fn new() -> Self {
38        // create shared sink/receiver pair and register traces
39        let (sink, rx) = channel_plot();
40        let tr_sine = sink.create_trace("sine", None);
41        let tr_cos = sink.create_trace("cosine", None);
42
43        // spawn the producer thread (1 kHz sample rate as in sine_cosine.rs)
44        let sink_clone = sink.clone();
45        let sine_clone = tr_sine.clone();
46        let cos_clone = tr_cos.clone();
47        std::thread::spawn(move || {
48            const FS_HZ: f64 = 1000.0;
49            const F_HZ: f64 = 3.0;
50            let dt = Duration::from_millis(1);
51            let mut n: u64 = 0;
52            loop {
53                let t = n as f64 / FS_HZ;
54                let s_val = (2.0 * std::f64::consts::PI * F_HZ * t).sin();
55                let c_val = (2.0 * std::f64::consts::PI * F_HZ * t).cos();
56                let t_s = SystemTime::now()
57                    .duration_since(UNIX_EPOCH)
58                    .map(|d| d.as_secs_f64())
59                    .unwrap_or(0.0);
60                let _ = sink_clone.send_point(&sine_clone, PlotPoint { x: t_s, y: s_val });
61                let _ = sink_clone.send_point(&cos_clone, PlotPoint { x: t_s, y: c_val });
62                n = n.wrapping_add(1);
63                std::thread::sleep(dt);
64            }
65        });
66
67        let plot = LivePlotApp::new(rx);
68        let features = FeatureFlags::default();
69
70        Self {
71            plot,
72            features,
73            _sink: sink,
74            _tr_sine: tr_sine,
75            _tr_cos: tr_cos,
76        }
77    }
78
79    /// Apply the currently selected feature flags to the embedded plot
80    /// panel.  This mutates fields on `LivePlotPanel` and some of the
81    /// underlying `ScopeData`/`TraceLook` structures so that toggles have
82    /// an immediate visible effect.
83    fn apply_features(&mut self) {
84        let f = &self.features;
85        let panel = &mut self.plot.main_panel;
86
87        // Determine button lists based on feature flags.  We start with the
88        // full default set and drop any that have been explicitly disabled.
89        let mut btns = ScopeButton::all_defaults();
90        if !f.pause_resume {
91            btns.retain(|b| *b != ScopeButton::PauseResume);
92        }
93        if !f.clear_all {
94            btns.retain(|b| *b != ScopeButton::ClearAll);
95        }
96        if !f.scopes {
97            // the "Scopes" button is purely navigational; hiding it is
98            // the only behaviour we can control here.
99            btns.retain(|b| *b != ScopeButton::Scopes);
100        }
101
102        panel.top_bar_buttons = if f.top_bar {
103            // show the filtered default list in the top bar
104            Some(btns.clone())
105        } else {
106            Some(vec![])
107        };
108        panel.sidebar_buttons = if f.sidebar { Some(btns) } else { Some(vec![]) };
109
110        // legend / grid toggle
111        for scope in panel.liveplot_panel.get_data_mut() {
112            scope.show_legend = f.legend;
113            scope.show_grid = f.grid;
114        }
115
116        // tick-label thresholds via the helper method
117        panel.liveplot_panel.set_tick_label_thresholds(
118            if f.y_tick_labels {
119                250.0
120            } else {
121                f32::INFINITY
122            },
123            if f.x_tick_labels {
124                200.0
125            } else {
126                f32::INFINITY
127            },
128        );
129
130        // rebuild right-side panels list based on feature flags, but
131        // keep existing panels around so their internal state (visibility,
132        // detachment, etc.) isn't wiped each frame.  This mirrors the
133        // strategy we use for the FFT bottom panel above.
134        {
135            let hk = panel.hotkeys.clone();
136
137            // remove any panels whose feature has been disabled
138            panel.right_side_panels.retain(|p| {
139                if p.downcast_ref::<liveplot::panels::traces_ui::TracesPanel>()
140                    .is_some()
141                {
142                    f.sidebar
143                } else if p
144                    .downcast_ref::<liveplot::panels::math_ui::MathPanel>()
145                    .is_some()
146                {
147                    f.sidebar && f.math
148                } else if p
149                    .downcast_ref::<liveplot::panels::hotkeys_ui::HotkeysPanel>()
150                    .is_some()
151                {
152                    f.sidebar && f.hotkeys
153                } else if p
154                    .downcast_ref::<liveplot::panels::thresholds_ui::ThresholdsPanel>()
155                    .is_some()
156                {
157                    f.sidebar && f.thresholds
158                } else if p
159                    .downcast_ref::<liveplot::panels::triggers_ui::TriggersPanel>()
160                    .is_some()
161                {
162                    f.sidebar && f.triggers
163                } else if p
164                    .downcast_ref::<liveplot::panels::measurment_ui::MeasurementPanel>()
165                    .is_some()
166                {
167                    f.sidebar && f.measurement
168                } else {
169                    // unknown panel type, keep it
170                    true
171                }
172            });
173
174            // add missing panels for which the feature is enabled
175            if f.sidebar
176                && !panel.right_side_panels.iter().any(|p| {
177                    p.downcast_ref::<liveplot::panels::traces_ui::TracesPanel>()
178                        .is_some()
179                })
180            {
181                panel
182                    .right_side_panels
183                    .push(Box::new(liveplot::panels::traces_ui::TracesPanel::default()));
184            }
185            if f.sidebar
186                && f.math
187                && !panel.right_side_panels.iter().any(|p| {
188                    p.downcast_ref::<liveplot::panels::math_ui::MathPanel>()
189                        .is_some()
190                })
191            {
192                panel
193                    .right_side_panels
194                    .push(Box::new(liveplot::panels::math_ui::MathPanel::default()));
195            }
196            if f.sidebar
197                && f.hotkeys
198                && !panel.right_side_panels.iter().any(|p| {
199                    p.downcast_ref::<liveplot::panels::hotkeys_ui::HotkeysPanel>()
200                        .is_some()
201                })
202            {
203                panel.right_side_panels.push(Box::new(
204                    liveplot::panels::hotkeys_ui::HotkeysPanel::new(hk.clone()),
205                ));
206            }
207            if f.sidebar
208                && f.thresholds
209                && !panel.right_side_panels.iter().any(|p| {
210                    p.downcast_ref::<liveplot::panels::thresholds_ui::ThresholdsPanel>()
211                        .is_some()
212                })
213            {
214                panel.right_side_panels.push(Box::new(
215                    liveplot::panels::thresholds_ui::ThresholdsPanel::default(),
216                ));
217            }
218            if f.sidebar
219                && f.triggers
220                && !panel.right_side_panels.iter().any(|p| {
221                    p.downcast_ref::<liveplot::panels::triggers_ui::TriggersPanel>()
222                        .is_some()
223                })
224            {
225                panel.right_side_panels.push(Box::new(
226                    liveplot::panels::triggers_ui::TriggersPanel::default(),
227                ));
228            }
229            if f.sidebar
230                && f.measurement
231                && !panel.right_side_panels.iter().any(|p| {
232                    p.downcast_ref::<liveplot::panels::measurment_ui::MeasurementPanel>()
233                        .is_some()
234                })
235            {
236                panel.right_side_panels.push(Box::new(
237                    liveplot::panels::measurment_ui::MeasurementPanel::default(),
238                ));
239            }
240        }
241
242        #[cfg(feature = "fft")]
243        {
244            // Rather than rebuild the bottom-panels list on every frame (which
245            // would reset each panel's `PanelState` and make it impossible to
246            // show the FFT panel after clicking the button), we only add or
247            // remove the FFT panel when the corresponding feature flag
248            // changes.  This keeps the panel object alive across frames so
249            // its `visible`/`detached` state is preserved.
250            if f.fft {
251                let has_fft = panel.bottom_panels.iter().any(|p| {
252                    p.downcast_ref::<liveplot::panels::fft_ui::FftPanel>()
253                        .is_some()
254                });
255                if !has_fft {
256                    panel
257                        .bottom_panels
258                        .push(Box::new(liveplot::panels::fft_ui::FftPanel::default()));
259                }
260            } else {
261                panel.bottom_panels.retain(|p| {
262                    p.downcast_ref::<liveplot::panels::fft_ui::FftPanel>()
263                        .is_none()
264                });
265            }
266        }
267
268        // note: a handful of flags still don't modify the UI:
269        // * `markers` – there is no public API to toggle every trace's
270        //   `show_points` flag, so this checkbox is only illustrative.
271        // * `export` – the export panel button is shown/hidden but we don't
272        //   implement any export logic here.
273        // Other flags (`scopes`, `pause_resume`, `clear_all`, `grid`, etc.)
274        // now drive visible buttons or overlays as expected.
275    }
276}
277
278impl eframe::App for FeaturesApp {
279    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
280        // data is produced on a background thread; nothing to do here
281
282        // draw checkboxes at the top
283        egui::TopBottomPanel::top("features_top").show(ctx, |ui| {
284            ui.label("Toggle features:");
285            ui.horizontal_wrapped(|ui| {
286                ui.checkbox(&mut self.features.top_bar, "top_bar");
287                ui.checkbox(&mut self.features.sidebar, "sidebar");
288                ui.checkbox(&mut self.features.markers, "markers");
289                ui.checkbox(&mut self.features.thresholds, "thresholds");
290                ui.checkbox(&mut self.features.triggers, "triggers");
291                ui.checkbox(&mut self.features.measurement, "measurement");
292                ui.checkbox(&mut self.features.export, "export");
293                ui.checkbox(&mut self.features.math, "math");
294                ui.checkbox(&mut self.features.hotkeys, "hotkeys");
295                ui.checkbox(&mut self.features.fft, "fft");
296                ui.checkbox(&mut self.features.x_tick_labels, "x_tick_labels");
297                ui.checkbox(&mut self.features.y_tick_labels, "y_tick_labels");
298                ui.checkbox(&mut self.features.grid, "grid");
299                ui.checkbox(&mut self.features.legend, "legend");
300                ui.checkbox(&mut self.features.scopes, "scopes");
301                ui.checkbox(&mut self.features.pause_resume, "pause_resume");
302                ui.checkbox(&mut self.features.clear_all, "clear_all");
303            });
304        });
305
306        // Apply flags every frame (cost negligible)
307        self.apply_features();
308
309        // render the plot panel
310        egui::CentralPanel::default().show(ctx, |ui| {
311            self.plot.main_panel.update_embedded(ui);
312        });
313
314        // keep redrawing at roughly 60Hz
315        ctx.request_repaint_after(Duration::from_millis(16));
316    }
317}
318
319fn main() -> eframe::Result<()> {
320    let app = FeaturesApp::new();
321    eframe::run_native(
322        "Feature Flags Example",
323        NativeOptions::default(),
324        Box::new(|_cc| Ok(Box::new(app))),
325    )
326}