1use crate::layout::subpixel_snap_rect;
7use crate::theme::MacTheme;
8use egui::{CornerRadius, Key, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget};
9use std::ops::RangeInclusive;
10
11pub struct MacStepper<'a> {
33 value: &'a mut f64,
34 range: RangeInclusive<f64>,
35 step: f64,
36 theme: Option<&'a MacTheme>,
37 width: Option<f32>,
38 height: Option<f32>,
39 corner_radius: Option<f32>,
40 wraps: bool,
41 enabled: bool,
42}
43
44impl<'a> MacStepper<'a> {
45 pub fn new(value: &'a mut f64, range: RangeInclusive<f64>, step: f64) -> Self {
47 Self {
48 value,
49 range,
50 step,
51 theme: None,
52 width: None,
53 height: None,
54 corner_radius: None,
55 wraps: false,
56 enabled: true,
57 }
58 }
59
60 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
62 self.theme = Some(theme);
63 self
64 }
65
66 pub fn width(mut self, width: f32) -> Self {
68 self.width = Some(width);
69 self
70 }
71
72 pub fn height(mut self, height: f32) -> Self {
74 self.height = Some(height);
75 self
76 }
77
78 pub fn corner_radius(mut self, r: f32) -> Self {
80 self.corner_radius = Some(r);
81 self
82 }
83
84 pub fn wraps(mut self, wraps: bool) -> Self {
86 self.wraps = wraps;
87 self
88 }
89
90 pub fn enabled(mut self, enabled: bool) -> Self {
92 self.enabled = enabled;
93 self
94 }
95
96 pub fn show(self, ui: &mut Ui) -> Response {
98 ui.add(self)
99 }
100}
101
102pub(crate) fn consume_step_arrow_mult(
105 ui: &mut Ui,
106 key: Key,
107 cmd_mult: f64,
108 shift_mult: f64,
109) -> Option<f64> {
110 ui.input_mut(|i| {
111 if i.consume_key(egui::Modifiers::COMMAND, key)
112 || i.consume_key(egui::Modifiers::CTRL, key)
113 || i.consume_key(egui::Modifiers::MAC_CMD, key)
114 {
115 Some(cmd_mult)
116 } else if i.consume_key(egui::Modifiers::SHIFT, key) {
117 Some(shift_mult)
118 } else if i.consume_key(egui::Modifiers::NONE, key) {
119 Some(1.0)
120 } else {
121 None
122 }
123 })
124}
125
126fn eval_auto_repeat(
127 ui: &mut Ui,
128 held: bool,
129 state_id: egui::Id,
130 now: f64,
131 repeat_delay: f64,
132 repeat_interval: f64,
133) -> bool {
134 if held {
135 ui.ctx().request_repaint_after_secs(repeat_interval as f32);
136 match ui.data_mut(|d| d.get_temp::<(f64, f64)>(state_id)) {
137 None => {
138 ui.data_mut(|d| d.insert_temp(state_id, (now, now)));
139 false
140 }
141 Some((press_start, last_repeat)) => {
142 if now - press_start >= repeat_delay && now - last_repeat >= repeat_interval {
143 ui.data_mut(|d| d.insert_temp(state_id, (press_start, now)));
144 true
145 } else {
146 false
147 }
148 }
149 }
150 } else {
151 ui.data_mut(|d| d.remove_temp::<(f64, f64)>(state_id));
152 false
153 }
154}
155
156impl<'a> Widget for MacStepper<'a> {
157 fn ui(self, ui: &mut Ui) -> Response {
158 let theme = MacTheme::resolve(self.theme, ui.ctx());
159
160 let metrics = theme.stepper();
161 let width = self.width.unwrap_or_else(|| metrics.width.points());
162 let height = self.height.unwrap_or_else(|| metrics.height.points());
163 let corner_radius = self
164 .corner_radius
165 .unwrap_or_else(|| metrics.corner_radius.points());
166 let divider_thickness = metrics.divider_thickness.points();
167 let arrow_size = metrics.arrow_size.points();
168 let arrow_stroke = metrics.arrow_stroke_width.points();
169
170 let (raw_rect, mut response) =
171 ui.allocate_exact_size(Vec2::new(width, height), Sense::click());
172 let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
173
174 let top_rect = Rect::from_min_max(rect.min, Pos2::new(rect.max.x, rect.center().y));
175 let bottom_rect = Rect::from_min_max(Pos2::new(rect.min.x, rect.center().y), rect.max);
176
177 let top_id = response.id.with("mac_stepper_up");
178 let bottom_id = response.id.with("mac_stepper_down");
179
180 let top_resp = ui.interact(
181 top_rect,
182 top_id,
183 if self.enabled {
184 Sense::click()
185 } else {
186 Sense::hover()
187 },
188 );
189 let bottom_resp = ui.interact(
190 bottom_rect,
191 bottom_id,
192 if self.enabled {
193 Sense::click()
194 } else {
195 Sense::hover()
196 },
197 );
198
199 let min_val = *self.range.start();
200 let max_val = *self.range.end();
201 if *self.value < min_val {
202 *self.value = min_val;
203 } else if *self.value > max_val {
204 *self.value = max_val;
205 }
206 let mut changed = false;
207
208 let clicked_top = top_resp.clicked();
209 let clicked_bottom = bottom_resp.clicked();
210 if clicked_top
211 || clicked_bottom
212 || top_resp.is_pointer_button_down_on()
213 || bottom_resp.is_pointer_button_down_on()
214 {
215 response.request_focus();
216 }
217
218 let now = ui.input(|i| i.time);
219 let repeat_delay = metrics.auto_repeat_delay.points() as f64;
220 let repeat_interval = metrics.auto_repeat_interval.points() as f64;
221
222 let repeat_top = eval_auto_repeat(
223 ui,
224 self.enabled && top_resp.is_pointer_button_down_on(),
225 top_id,
226 now,
227 repeat_delay,
228 repeat_interval,
229 );
230 let repeat_bottom = eval_auto_repeat(
231 ui,
232 self.enabled && bottom_resp.is_pointer_button_down_on(),
233 bottom_id,
234 now,
235 repeat_delay,
236 repeat_interval,
237 );
238
239 let ptr_mods = ui.input(|i| {
240 let mut m = i.modifiers;
241 for ev in &i.raw.events {
242 if let egui::Event::PointerButton { modifiers, .. } = ev {
243 m |= *modifiers;
244 }
245 }
246 m
247 });
248 let shift_mult = metrics.shift_multiplier.points() as f64;
249 let cmd_mult = metrics.cmd_multiplier.points() as f64;
250 let ptr_mult = if ptr_mods.command || ptr_mods.ctrl || ptr_mods.mac_cmd {
251 cmd_mult
252 } else if ptr_mods.shift {
253 shift_mult
254 } else {
255 1.0
256 };
257
258 let key_up_mult = (self.enabled && response.has_focus())
259 .then(|| consume_step_arrow_mult(ui, Key::ArrowUp, cmd_mult, shift_mult))
260 .flatten();
261 let key_down_mult = (self.enabled && response.has_focus())
262 .then(|| consume_step_arrow_mult(ui, Key::ArrowDown, cmd_mult, shift_mult))
263 .flatten();
264
265 let step_up_mult = if self.enabled && (clicked_top || repeat_top) {
266 Some(ptr_mult)
267 } else {
268 key_up_mult
269 };
270
271 let step_down_mult = if self.enabled && (clicked_bottom || repeat_bottom) {
272 Some(ptr_mult)
273 } else {
274 key_down_mult
275 };
276
277 let eps = 1e-9 * self.step.abs().max(1.0);
278
279 if let Some(mult) = step_up_mult {
281 let next_val = *self.value + self.step * mult;
282 if next_val <= max_val + eps {
283 *self.value = next_val.min(max_val);
284 } else if self.wraps {
285 *self.value = min_val;
286 } else {
287 *self.value = max_val;
288 }
289 changed = true;
290 }
291
292 if let Some(mult) = step_down_mult {
294 let next_val = *self.value - self.step * mult;
295 if next_val >= min_val - eps {
296 *self.value = next_val.max(min_val);
297 } else if self.wraps {
298 *self.value = max_val;
299 } else {
300 *self.value = min_val;
301 }
302 changed = true;
303 }
304
305 if changed {
306 response.mark_changed();
307 }
308
309 let painter = ui.painter_at(rect);
310
311 if self.enabled && response.has_focus() {
313 theme.paint_focus_ring(&painter, rect, corner_radius);
314 }
315
316 let bg_color = theme.control_background();
318 let border_stroke = if response.has_focus() {
319 Stroke::new(metrics.border_width.points(), theme.control_accent())
320 } else {
321 Stroke::new(metrics.border_width.points(), theme.separator())
322 };
323
324 painter.rect(
325 rect,
326 CornerRadius::same(corner_radius as u8),
327 bg_color,
328 border_stroke,
329 StrokeKind::Inside,
330 );
331
332 if self.enabled {
334 if top_resp.is_pointer_button_down_on() {
335 painter.rect_filled(
336 top_rect,
337 CornerRadius {
338 nw: corner_radius as u8,
339 ne: corner_radius as u8,
340 sw: 0,
341 se: 0,
342 },
343 theme.selected_control_inactive(),
344 );
345 } else if top_resp.hovered() {
346 painter.rect_filled(
347 top_rect,
348 CornerRadius {
349 nw: corner_radius as u8,
350 ne: corner_radius as u8,
351 sw: 0,
352 se: 0,
353 },
354 theme.control(),
355 );
356 }
357
358 if bottom_resp.is_pointer_button_down_on() {
359 painter.rect_filled(
360 bottom_rect,
361 CornerRadius {
362 nw: 0,
363 ne: 0,
364 sw: corner_radius as u8,
365 se: corner_radius as u8,
366 },
367 theme.selected_control_inactive(),
368 );
369 } else if bottom_resp.hovered() {
370 painter.rect_filled(
371 bottom_rect,
372 CornerRadius {
373 nw: 0,
374 ne: 0,
375 sw: corner_radius as u8,
376 se: corner_radius as u8,
377 },
378 theme.control(),
379 );
380 }
381 }
382
383 let divider_y = rect.center().y;
385 painter.line_segment(
386 [
387 Pos2::new(rect.left(), divider_y),
388 Pos2::new(rect.right(), divider_y),
389 ],
390 Stroke::new(divider_thickness, theme.separator()),
391 );
392
393 let arrow_color = if !self.enabled {
395 theme.placeholder_text()
396 } else {
397 theme.label()
398 };
399 let arrow_stroke_style = Stroke::new(arrow_stroke, arrow_color);
400 let arrow_half_w = arrow_size * 0.5;
401 let arrow_delta = metrics.arrow_delta.points();
402
403 let up_cx = top_rect.center().x;
405 let up_cy = top_rect.center().y;
406 let up_tip = Pos2::new(up_cx, up_cy - arrow_delta);
407 let up_left = Pos2::new(up_cx - arrow_half_w, up_cy + arrow_delta);
408 let up_right = Pos2::new(up_cx + arrow_half_w, up_cy + arrow_delta);
409 painter.line_segment([up_left, up_tip], arrow_stroke_style);
410 painter.line_segment([up_tip, up_right], arrow_stroke_style);
411
412 let down_cx = bottom_rect.center().x;
414 let down_cy = bottom_rect.center().y;
415 let down_tip = Pos2::new(down_cx, down_cy + arrow_delta);
416 let down_left = Pos2::new(down_cx - arrow_half_w, down_cy - arrow_delta);
417 let down_right = Pos2::new(down_cx + arrow_half_w, down_cy - arrow_delta);
418 painter.line_segment([down_left, down_tip], arrow_stroke_style);
419 painter.line_segment([down_tip, down_right], arrow_stroke_style);
420
421 response
422 }
423}