Skip to main content

loam_egui/
slider_edit.rs

1use egui::{vec2, Button, CursorIcon, DragValue, RichText, Slider, SliderClamping, Ui};
2
3#[derive(Copy, Clone, Debug, Default)]
4pub struct SliderInteraction {
5    pub changed: bool,
6    pub dragged: bool,
7}
8
9/// Right-click on the value cell opens a [`DragValue`] popup.
10pub fn slider_with_edit(
11    ui: &mut Ui,
12    value: &mut f32,
13    range: std::ops::RangeInclusive<f32>,
14    formatted: &str,
15    edit_suffix: &str,
16    edit_decimals: usize,
17    value_cell_w: f32,
18) -> SliderInteraction {
19    let slider_resp = ui.add(
20        Slider::new(value, range.clone())
21            .show_value(false)
22            .smart_aim(false)
23            .clamping(SliderClamping::Always),
24    );
25    let mut popup_changed = false;
26    let label_resp = ui.add_sized(
27        vec2(value_cell_w, 14.0),
28        Button::new(RichText::new(formatted).monospace())
29            .frame(false)
30            .small()
31            .truncate(),
32    );
33    label_resp
34        .on_hover_cursor(CursorIcon::ContextMenu)
35        .on_hover_text("Right-click to edit value")
36        .context_menu(|ui| {
37            let drag_resp = ui.add(
38                DragValue::new(value)
39                    .range(range)
40                    .suffix(edit_suffix)
41                    .fixed_decimals(edit_decimals),
42            );
43            if drag_resp.changed() {
44                popup_changed = true;
45            }
46        });
47    SliderInteraction {
48        changed: slider_resp.changed() || popup_changed,
49        dragged: slider_resp.dragged(),
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn screen() -> egui::Rect {
58        egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(800.0, 600.0))
59    }
60
61    #[test]
62    fn side_cell_width_is_fixed() {
63        let ctx = egui::Context::default();
64        let mut total_widths: Vec<f32> = Vec::new();
65        let layout_input = egui::RawInput {
66            screen_rect: Some(screen()),
67            time: Some(0.0),
68            ..Default::default()
69        };
70        for formatted in ["0", "+999.99", "+123456789.00"] {
71            let mut value = 0.5_f32;
72            let mut total = 0.0_f32;
73            let _ = ctx.run(layout_input.clone(), |ctx| {
74                egui::CentralPanel::default().show(ctx, |ui| {
75                    ui.horizontal(|ui| {
76                        let before = ui.next_widget_position().x;
77                        slider_with_edit(ui, &mut value, 0.0..=1.0, formatted, "", 2, 60.0);
78                        total = ui.next_widget_position().x - before;
79                    });
80                });
81            });
82            assert!(total > 60.0, "slider width was not measured: {total}");
83            total_widths.push(total);
84        }
85        let drift = total_widths
86            .iter()
87            .map(|width| (width - total_widths[0]).abs())
88            .fold(0.0_f32, f32::max);
89        assert!(
90            drift < 1.0,
91            "slider total width must be invariant across formatted-string lengths; \
92             got {:?} (drift {:.2})",
93            total_widths,
94            drift,
95        );
96    }
97}