Skip to main content

agg_gui/widgets/performance/
run_mode.rs

1//! Reactive / Continuous run-mode plumbing for `PerformanceView`.
2//!
3//! Splits the `RunMode` enum and the two helper widgets out of the main
4//! `performance.rs` file purely for the 800-line per-file cap.  Everything
5//! here is re-exported from `crate::widgets::performance` so callers (and
6//! `agg_gui::widgets`) keep their existing import paths.
7//!
8//! Wiring:
9//!   * `RunMode`            — host loop policy (Reactive | Continuous).
10//!   * `shared_run_mode`    — convenience to build the `Rc<Cell<RunMode>>`
11//!                            handle that the selector reads / writes.
12//!   * `RunModeRow`         — two-button segmented control.
13//!   * `RunModeDesc`        — dynamic description label (FPS in Continuous).
14//!
15//! The host's main loop is expected to read the same `Rc<Cell<RunMode>>`
16//! to decide whether to pump frames; the widgets themselves only update the
17//! cell on click.  Reactive ≠ "stops the perf graph"; whether the graph keeps
18//! updating depends on `PerformanceView::with_history_redraw` and whatever
19//! the host's loop is doing.
20
21use std::cell::Cell;
22use std::rc::Rc;
23use std::sync::Arc;
24
25use crate::animation;
26use crate::draw_ctx::DrawCtx;
27use crate::event::{Event, EventResult};
28use crate::geometry::{Rect, Size};
29use crate::text::Font;
30use crate::widget::{paint_subtree, Widget};
31use crate::widgets::{Button, Label};
32
33use super::SharedFrameHistory;
34
35// ── RunMode enum + helper ─────────────────────────────────────────────────────
36
37/// How the host's event loop drives repaints.
38///
39/// `Reactive` is the agg-gui default: paint only when widgets request a draw
40/// (input, animation, or an explicit invalidation).  `Continuous` keeps the
41/// loop spinning every frame — useful for hosts that want a live perf graph
42/// or that drive a real-time simulation regardless of input.
43#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44pub enum RunMode {
45    Reactive,
46    Continuous,
47}
48
49/// Convenience: wrap a `RunMode` in the `Rc<Cell<…>>` plumbing that
50/// [`super::PerformanceView::with_run_mode_selector`] expects.
51pub fn shared_run_mode(initial: RunMode) -> Rc<Cell<RunMode>> {
52    Rc::new(Cell::new(initial))
53}
54
55// ── RunModeRow (two-button segmented control) ─────────────────────────────────
56
57/// Reactive / Continuous segmented selector composed from two real `Button`
58/// children that share an `Rc<Cell<RunMode>>`.  Used inside `PerformanceView`
59/// when [`super::PerformanceView::with_run_mode_selector`] is wired; safe to
60/// use standalone for hosts that want the picker without the perf graph.
61pub struct RunModeRow {
62    bounds: Rect,
63    children: Vec<Box<dyn Widget>>,
64}
65
66impl RunModeRow {
67    /// Total row height (button + a few pixels of vertical breathing room).
68    pub const ROW_HEIGHT: f64 = 32.0;
69    const BTN_H: f64 = 24.0;
70
71    pub fn new(font: Arc<Font>, run_mode: Rc<Cell<RunMode>>) -> Self {
72        let segments: [(&'static str, RunMode); 2] = [
73            ("Reactive", RunMode::Reactive),
74            ("Continuous", RunMode::Continuous),
75        ];
76        let children: Vec<Box<dyn Widget>> = segments
77            .iter()
78            .map(|(label, this_mode)| {
79                let mode_active = Rc::clone(&run_mode);
80                let mode_click = Rc::clone(&run_mode);
81                let this = *this_mode;
82                let btn = Button::new(*label, Arc::clone(&font))
83                    .with_font_size(12.0)
84                    .with_subtle()
85                    .with_active_fn(move || mode_active.get() == this)
86                    .on_click(move || {
87                        if mode_click.get() != this {
88                            mode_click.set(this);
89                            animation::request_draw();
90                        }
91                    });
92                Box::new(btn) as Box<dyn Widget>
93            })
94            .collect();
95        Self {
96            bounds: Rect::default(),
97            children,
98        }
99    }
100}
101
102impl Widget for RunModeRow {
103    fn type_name(&self) -> &'static str {
104        "RunModeRow"
105    }
106    fn bounds(&self) -> Rect {
107        self.bounds
108    }
109    fn set_bounds(&mut self, b: Rect) {
110        self.bounds = b;
111    }
112    fn children(&self) -> &[Box<dyn Widget>] {
113        &self.children
114    }
115    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
116        &mut self.children
117    }
118
119    fn layout(&mut self, available: Size) -> Size {
120        let row_h = Self::ROW_HEIGHT;
121        self.bounds = Rect::new(0.0, 0.0, available.width, row_h);
122        let gy = (row_h - Self::BTN_H) * 0.5;
123        let gap = 4.0;
124        let btn_w = ((available.width - gap) * 0.5).max(40.0);
125        for (i, child) in self.children.iter_mut().enumerate() {
126            child.layout(Size::new(btn_w, Self::BTN_H));
127            child.set_bounds(Rect::new(i as f64 * (btn_w + gap), gy, btn_w, Self::BTN_H));
128        }
129        Size::new(available.width, row_h)
130    }
131
132    fn paint(&mut self, _ctx: &mut dyn DrawCtx) {
133        // Buttons paint themselves through the framework's tree walk.
134    }
135
136    fn on_event(&mut self, _event: &Event) -> EventResult {
137        EventResult::Ignored
138    }
139}
140
141// ── RunModeDesc (dynamic description label) ───────────────────────────────────
142
143/// Dynamic label that mirrors the current run mode.
144///   Reactive   — "Only running UI code when there are animations or input."
145///   Continuous — "Running continuously as fast as possible.  FPS: X.X"
146///
147/// Lives in agg-gui so hosts that wire `RunMode` get the same wording every
148/// app uses without re-implementing the FPS readout.
149pub struct RunModeDesc {
150    bounds: Rect,
151    children: Vec<Box<dyn Widget>>,
152    run_mode: Rc<Cell<RunMode>>,
153    history: SharedFrameHistory,
154    label: Label,
155}
156
157impl RunModeDesc {
158    pub fn new(font: Arc<Font>, run_mode: Rc<Cell<RunMode>>, history: SharedFrameHistory) -> Self {
159        let mut label = Label::new("", Arc::clone(&font))
160            .with_font_size(10.0)
161            .with_wrap(true);
162        label.buffered = false;
163        Self {
164            bounds: Rect::default(),
165            children: Vec::new(),
166            run_mode,
167            history,
168            label,
169        }
170    }
171}
172
173impl Widget for RunModeDesc {
174    fn type_name(&self) -> &'static str {
175        "RunModeDesc"
176    }
177    fn bounds(&self) -> Rect {
178        self.bounds
179    }
180    fn set_bounds(&mut self, b: Rect) {
181        self.bounds = b;
182    }
183    fn children(&self) -> &[Box<dyn Widget>] {
184        &self.children
185    }
186    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
187        &mut self.children
188    }
189
190    fn layout(&mut self, available: Size) -> Size {
191        // Use the longer (reactive) string for height measurement so the
192        // layout is stable when the user flips modes mid-run.
193        self.label
194            .set_text("Only running UI code when there are animations or input.".to_owned());
195        let s = self
196            .label
197            .layout(Size::new(available.width, f64::MAX / 2.0));
198        self.label
199            .set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
200        let h = s.height.max(available.height).max(14.0);
201        self.bounds = Rect::new(0.0, 0.0, available.width, h);
202        Size::new(available.width, h)
203    }
204
205    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
206        let v = ctx.visuals();
207        let text = match self.run_mode.get() {
208            RunMode::Reactive => {
209                "Only running UI code when there are animations or input.".to_owned()
210            }
211            RunMode::Continuous => {
212                let hist = self.history.borrow();
213                let fps = if hist.mean_ms() < 0.001 {
214                    0.0
215                } else {
216                    1000.0 / hist.mean_ms()
217                };
218                format!("Running continuously as fast as possible. FPS: {fps:.1}")
219            }
220        };
221        self.label.set_text(text);
222        self.label.set_color(v.text_dim);
223
224        let lh = self.label.bounds().height;
225        let ly = ((self.bounds.height - lh) * 0.5).max(0.0);
226        ctx.save();
227        ctx.translate(0.0, ly);
228        paint_subtree(&mut self.label, ctx);
229        ctx.restore();
230    }
231
232    fn on_event(&mut self, _: &Event) -> EventResult {
233        EventResult::Ignored
234    }
235
236    fn needs_draw(&self) -> bool {
237        // Continuous mode shows an FPS readout that changes every frame;
238        // request a redraw so the number stays current even when nothing
239        // else in the tree is dirty.  Reactive mode keeps the description
240        // string constant — no extra wakeups needed.
241        matches!(self.run_mode.get(), RunMode::Continuous)
242    }
243}