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
9pub 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}