Skip to main content

pfx_editor_style/widgets/
number.rs

1use egui::{
2    Align2, CornerRadius, Event, EventFilter, Key, Rect, Response, Sense, Stroke, StrokeKind, Ui,
3    Vec2, Widget,
4};
5
6use super::{State, place, shaped};
7use crate::theme::Theme;
8
9pub struct Number<'a> {
10    value: &'a mut f32,
11    unit: &'a str,
12    speed: f32,
13    decimals: usize,
14    width: f32,
15}
16
17impl<'a> Number<'a> {
18    pub fn new(value: &'a mut f32) -> Number<'a> {
19        Number {
20            value,
21            unit: "",
22            speed: 0.01,
23            decimals: 3,
24            width: 76.0,
25        }
26    }
27
28    pub fn unit(mut self, unit: &'a str) -> Number<'a> {
29        self.unit = unit;
30        self
31    }
32
33    pub fn speed(mut self, speed: f32) -> Number<'a> {
34        self.speed = speed;
35        self
36    }
37
38    pub fn decimals(mut self, decimals: usize) -> Number<'a> {
39        self.decimals = decimals;
40        self
41    }
42
43    pub fn width(mut self, width: f32) -> Number<'a> {
44        self.width = width;
45        self
46    }
47}
48
49pub fn format(value: f32, decimals: usize) -> String {
50    let text = format!("{value:.decimals$}");
51    match text.strip_prefix('-') {
52        Some(rest) if rest.chars().all(|c| c == '0' || c == '.') => rest.to_string(),
53        _ => text,
54    }
55}
56
57impl Widget for Number<'_> {
58    fn ui(self, ui: &mut Ui) -> Response {
59        let theme = Theme::of(ui.ctx());
60        let size = Vec2::new(self.width, theme.row_height);
61        let (rect, mut response) = ui.allocate_exact_size(size, Sense::click_and_drag());
62        let id = response.id;
63        let mut editing: Option<String> = ui.data(|data| data.get_temp(id));
64        if response.dragged() && editing.is_none() {
65            let delta = response.drag_delta().x * self.speed;
66            if delta != 0.0 {
67                *self.value += delta;
68                response.mark_changed();
69            }
70        }
71        if response.clicked() && editing.is_none() {
72            editing = Some(format(*self.value, self.decimals));
73            response.request_focus();
74        }
75        if editing.is_some() && ui.memory(|memory| memory.has_focus(id)) {
76            ui.memory_mut(|memory| {
77                memory.set_focus_lock_filter(
78                    id,
79                    EventFilter {
80                        tab: false,
81                        horizontal_arrows: true,
82                        vertical_arrows: true,
83                        escape: true,
84                    },
85                )
86            });
87            let events = ui.input(|input| input.events.clone());
88            for event in events {
89                let Some(text) = editing.as_mut() else { break };
90                match event {
91                    Event::Text(typed) => text.push_str(&typed),
92                    Event::Key {
93                        key: Key::Backspace,
94                        pressed: true,
95                        ..
96                    } => {
97                        text.pop();
98                    }
99                    Event::Key {
100                        key: Key::Enter,
101                        pressed: true,
102                        ..
103                    } => {
104                        if let Ok(value) = text.trim().parse::<f32>() {
105                            *self.value = value;
106                            response.mark_changed();
107                        }
108                        editing = None;
109                        response.surrender_focus();
110                    }
111                    Event::Key {
112                        key: Key::Escape,
113                        pressed: true,
114                        ..
115                    } => {
116                        editing = None;
117                        response.surrender_focus();
118                    }
119                    _ => {}
120                }
121            }
122        } else if let Some(text) = editing.take()
123            && let Ok(value) = text.trim().parse::<f32>()
124        {
125            *self.value = value;
126            response.mark_changed();
127        }
128        match &editing {
129            Some(text) => ui.data_mut(|data| {
130                data.insert_temp(id, text.clone());
131            }),
132            None => ui.data_mut(|data| {
133                data.remove::<String>(id);
134            }),
135        }
136        if !ui.is_rect_visible(rect) {
137            return response;
138        }
139        let state = State::of(&response, editing.is_some() || response.dragged());
140        let painter = ui.painter();
141        painter.rect(
142            rect,
143            CornerRadius::same(theme.radius),
144            theme.well,
145            Stroke::new(theme.stroke, state.border(&theme)),
146            StrokeKind::Inside,
147        );
148        let value_font = theme.font(theme.size_code);
149        let unit_width = if self.unit.is_empty() {
150            0.0
151        } else {
152            let unit = shaped(
153                painter,
154                self.unit,
155                value_font.clone(),
156                theme.muted,
157                theme.labels,
158            );
159            place(
160                painter,
161                rect.right_center() - Vec2::new(6.0, 0.5),
162                Align2::RIGHT_CENTER,
163                unit,
164                theme.muted,
165            )
166            .width()
167                + 6.0
168        };
169        let right = rect.right_center() - Vec2::new(6.0 + unit_width, 0.5);
170        match &editing {
171            Some(text) => {
172                let shown = painter.text(
173                    right - Vec2::new(3.0, 0.0),
174                    Align2::RIGHT_CENTER,
175                    text,
176                    value_font,
177                    theme.text,
178                );
179                let caret = Rect::from_min_max(
180                    shown.right_top() + Vec2::new(1.0, 0.0),
181                    shown.right_bottom() + Vec2::new(3.0, 0.0),
182                );
183                painter.rect_filled(caret, CornerRadius::ZERO, theme.hot);
184            }
185            None => {
186                painter.text(
187                    right,
188                    Align2::RIGHT_CENTER,
189                    format(*self.value, self.decimals),
190                    value_font,
191                    theme.code,
192                );
193            }
194        }
195        response
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn negative_zero_reads_as_zero() {
205        assert_eq!(format(-0.0001, 3), "0.000");
206        assert_eq!(format(-0.5, 2), "-0.50");
207        assert_eq!(format(30.0, 0), "30");
208    }
209}