pfx_editor_style/widgets/
number.rs1use 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}