1use std::panic::Location;
64
65use crate::event::{UiEvent, UiEventKind};
66use crate::selection::Selection;
67use crate::tokens;
68use crate::tree::*;
69use crate::widgets::button::button;
70use crate::widgets::text_input::{
71 TextInputOpts, apply_event_with as text_input_apply, text_input_with,
72};
73
74#[derive(Clone, Copy, Debug)]
83pub struct NumericInputOpts<'a> {
84 pub min: Option<f64>,
87 pub max: Option<f64>,
90 pub step: f64,
92 pub decimals: Option<u8>,
97 pub placeholder: Option<&'a str>,
99}
100
101impl Default for NumericInputOpts<'_> {
102 fn default() -> Self {
103 Self {
104 min: None,
105 max: None,
106 step: 1.0,
107 decimals: None,
108 placeholder: None,
109 }
110 }
111}
112
113impl<'a> NumericInputOpts<'a> {
114 pub fn min(mut self, v: f64) -> Self {
115 self.min = Some(v);
116 self
117 }
118 pub fn max(mut self, v: f64) -> Self {
119 self.max = Some(v);
120 self
121 }
122 pub fn step(mut self, v: f64) -> Self {
123 self.step = v;
124 self
125 }
126 pub fn decimals(mut self, v: u8) -> Self {
127 self.decimals = Some(v);
128 self
129 }
130 pub fn placeholder(mut self, p: &'a str) -> Self {
131 self.placeholder = Some(p);
132 self
133 }
134}
135
136#[track_caller]
143pub fn numeric_input(
144 value: &str,
145 selection: &Selection,
146 key: &str,
147 opts: NumericInputOpts<'_>,
148) -> El {
149 let caller = Location::caller();
150
151 let dec = button("−")
152 .at_loc(caller)
153 .key(format!("{key}:dec"))
154 .ghost()
155 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
156 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
157 let inc = button("+")
158 .at_loc(caller)
159 .key(format!("{key}:inc"))
160 .ghost()
161 .width(Size::Fixed(tokens::CONTROL_HEIGHT))
162 .height(Size::Fixed(tokens::CONTROL_HEIGHT));
163
164 let mut text_opts = TextInputOpts::default();
165 if let Some(p) = opts.placeholder {
166 text_opts = text_opts.placeholder(p);
167 }
168 let field_key = format!("{key}:field");
169 let field = text_input_with(value, selection, &field_key, text_opts).width(Size::Fill(1.0));
170
171 row([dec, field, inc])
177 .at_loc(caller)
178 .key(key.to_string())
179 .gap(tokens::RING_WIDTH)
180 .align(Align::Center)
181 .height(Size::Fixed(tokens::CONTROL_HEIGHT))
182}
183
184pub fn apply_event(
194 value: &mut String,
195 selection: &mut Selection,
196 key: &str,
197 opts: &NumericInputOpts<'_>,
198 event: &UiEvent,
199) -> bool {
200 if matches!(event.kind, UiEventKind::Click | UiEventKind::Activate) {
201 let inc_key = format!("{key}:inc");
202 let dec_key = format!("{key}:dec");
203 if event.route() == Some(inc_key.as_str()) {
204 step_value(value, opts, 1);
205 return true;
206 }
207 if event.route() == Some(dec_key.as_str()) {
208 step_value(value, opts, -1);
209 return true;
210 }
211 }
212
213 let field_key = format!("{key}:field");
219 if event.target_key() != Some(field_key.as_str()) {
220 return false;
221 }
222
223 let text_opts = match opts.placeholder {
224 Some(p) => TextInputOpts::default().placeholder(p),
225 None => TextInputOpts::default(),
226 };
227
228 let prev_value = value.clone();
234 let prev_selection = selection.clone();
235 let changed = text_input_apply(value, selection, &field_key, event, &text_opts);
236 if changed && !is_acceptable_numeric_progress(value) {
237 *value = prev_value;
238 *selection = prev_selection;
239 return false;
240 }
241 changed
242}
243
244fn is_acceptable_numeric_progress(s: &str) -> bool {
245 s.is_empty()
246 || s.chars()
247 .all(|c| matches!(c, '0'..='9' | '.' | 'e' | 'E' | '+' | '-'))
248}
249
250fn step_value(value: &mut String, opts: &NumericInputOpts<'_>, dir: i32) {
251 let parsed = value
255 .parse::<f64>()
256 .ok()
257 .unwrap_or_else(|| opts.min.unwrap_or(0.0));
258 let stepped = parsed + (dir as f64) * opts.step;
259 let clamped = clamp_opt(stepped, opts.min, opts.max);
260 *value = format_numeric(clamped, opts.decimals);
261}
262
263fn clamp_opt(n: f64, min: Option<f64>, max: Option<f64>) -> f64 {
264 let n = if let Some(hi) = max { n.min(hi) } else { n };
265 if let Some(lo) = min { n.max(lo) } else { n }
266}
267
268fn format_numeric(n: f64, decimals: Option<u8>) -> String {
269 match decimals {
270 Some(d) => format!("{:.*}", d as usize, n),
271 None if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e18 => {
272 format!("{}", n as i64)
276 }
277 None => format!("{n}"),
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::event::{KeyModifiers, UiTarget};
285 use crate::tree::Rect;
286
287 fn click(key: &str) -> UiEvent {
288 UiEvent::synthetic_click(key)
289 }
290
291 fn text_event(target_key: &str, text: &str) -> UiEvent {
295 UiEvent {
296 path: None,
297 key: Some(target_key.to_string()),
298 target: Some(UiTarget {
299 key: target_key.to_string(),
300 node_id: format!("/{target_key}"),
301 rect: Rect::new(0.0, 0.0, 100.0, 32.0),
302 tooltip: None,
303 scroll_offset_y: 0.0,
304 }),
305 pointer: None,
306 key_press: None,
307 text: Some(text.to_string()),
308 selection: None,
309 modifiers: KeyModifiers::default(),
310 click_count: 0,
311 kind: UiEventKind::TextInput,
312 }
313 }
314
315 #[test]
316 fn inc_steps_value_up_by_step() {
317 let mut value = String::from("3");
318 let mut sel = Selection::default();
319 let opts = NumericInputOpts::default().step(2.0);
320 assert!(apply_event(
321 &mut value,
322 &mut sel,
323 "n",
324 &opts,
325 &click("n:inc")
326 ));
327 assert_eq!(value, "5");
328 }
329
330 #[test]
331 fn dec_steps_value_down_by_step() {
332 let mut value = String::from("3");
333 let mut sel = Selection::default();
334 let opts = NumericInputOpts::default().step(0.5).decimals(1);
335 assert!(apply_event(
336 &mut value,
337 &mut sel,
338 "n",
339 &opts,
340 &click("n:dec")
341 ));
342 assert_eq!(value, "2.5");
343 }
344
345 #[test]
346 fn inc_clamps_to_max() {
347 let mut value = String::from("99");
348 let mut sel = Selection::default();
349 let opts = NumericInputOpts::default().min(0.0).max(100.0);
350 let opts = opts.step(5.0);
352 assert!(apply_event(
353 &mut value,
354 &mut sel,
355 "n",
356 &opts,
357 &click("n:inc")
358 ));
359 assert_eq!(value, "100");
360 }
361
362 #[test]
363 fn dec_clamps_to_min() {
364 let mut value = String::from("1");
365 let mut sel = Selection::default();
366 let opts = NumericInputOpts::default().min(0.0).max(100.0);
367 assert!(apply_event(
368 &mut value,
369 &mut sel,
370 "n",
371 &opts,
372 &click("n:dec")
373 ));
374 assert_eq!(value, "0");
375 assert!(apply_event(
377 &mut value,
378 &mut sel,
379 "n",
380 &opts,
381 &click("n:dec")
382 ));
383 assert_eq!(value, "0");
384 }
385
386 #[test]
387 fn empty_value_treated_as_min_when_set() {
388 let mut value = String::new();
389 let mut sel = Selection::default();
390 let opts = NumericInputOpts::default().min(10.0).max(100.0);
391 assert!(apply_event(
393 &mut value,
394 &mut sel,
395 "n",
396 &opts,
397 &click("n:inc")
398 ));
399 assert_eq!(value, "11");
400 }
401
402 #[test]
403 fn empty_value_treated_as_zero_when_no_min() {
404 let mut value = String::new();
405 let mut sel = Selection::default();
406 let opts = NumericInputOpts::default();
407 assert!(apply_event(
408 &mut value,
409 &mut sel,
410 "n",
411 &opts,
412 &click("n:inc")
413 ));
414 assert_eq!(value, "1");
415 }
416
417 #[test]
418 fn unparseable_value_treated_as_zero_when_no_min() {
419 let mut value = String::from("abc");
420 let mut sel = Selection::default();
421 let opts = NumericInputOpts::default();
422 assert!(apply_event(
423 &mut value,
424 &mut sel,
425 "n",
426 &opts,
427 &click("n:inc")
428 ));
429 assert_eq!(value, "1");
430 }
431
432 #[test]
433 fn ignores_unrelated_keys() {
434 let mut value = String::from("3");
435 let mut sel = Selection::default();
436 let opts = NumericInputOpts::default();
437 assert!(!apply_event(
439 &mut value,
440 &mut sel,
441 "n",
442 &opts,
443 &click("other:inc")
444 ));
445 assert_eq!(value, "3");
446 }
447
448 #[test]
449 fn decimals_format_pads_zeros() {
450 let mut value = String::from("0");
451 let mut sel = Selection::default();
452 let opts = NumericInputOpts::default().step(0.10).decimals(2);
453 assert!(apply_event(
454 &mut value,
455 &mut sel,
456 "n",
457 &opts,
458 &click("n:inc")
459 ));
460 assert_eq!(value, "0.10");
461 }
462
463 #[test]
464 fn no_decimals_strips_trailing_zero() {
465 let mut value = String::from("0");
466 let mut sel = Selection::default();
467 let opts = NumericInputOpts::default().step(1.0);
468 assert!(apply_event(
469 &mut value,
470 &mut sel,
471 "n",
472 &opts,
473 &click("n:inc")
474 ));
475 assert_eq!(value, "1");
478 }
479
480 #[test]
481 fn text_event_for_other_widget_is_ignored() {
482 let mut value = String::from("42");
487 let mut sel = Selection::default();
488 let opts = NumericInputOpts::default();
489 assert!(!apply_event(
492 &mut value,
493 &mut sel,
494 "n",
495 &opts,
496 &text_event("other-input", "x"),
497 ));
498 assert_eq!(value, "42");
499 }
500
501 #[test]
502 fn text_event_filter_rejects_non_numeric_chars() {
503 let mut value = String::from("12");
507 let mut sel = Selection::default();
508 let opts = NumericInputOpts::default();
509 assert!(!apply_event(
510 &mut value,
511 &mut sel,
512 "n",
513 &opts,
514 &text_event("n:field", "abc"),
515 ));
516 assert_eq!(value, "12");
517 }
518
519 #[test]
520 fn text_event_filter_accepts_partial_numeric_states() {
521 for partial in ["-", "1.", "1.5e", "1.5e+", ".5", "+"] {
525 let mut value = String::new();
526 let mut sel = Selection::default();
527 let opts = NumericInputOpts::default();
528 assert!(
529 apply_event(
530 &mut value,
531 &mut sel,
532 "n",
533 &opts,
534 &text_event("n:field", partial),
535 ),
536 "filter should accept partial value {partial:?}",
537 );
538 assert_eq!(value, partial, "value should equal {partial:?}");
539 }
540 }
541
542 #[test]
543 fn text_event_filter_accepts_full_numeric_paste() {
544 let mut value = String::new();
545 let mut sel = Selection::default();
546 let opts = NumericInputOpts::default();
547 assert!(apply_event(
548 &mut value,
549 &mut sel,
550 "n",
551 &opts,
552 &text_event("n:field", "42.5"),
553 ));
554 assert_eq!(value, "42.5");
555 }
556
557 #[test]
558 fn build_widget_has_three_children_and_correct_keys() {
559 let value = String::from("0");
560 let sel = Selection::default();
561 let opts = NumericInputOpts::default();
562 let el = numeric_input(&value, &sel, "n", opts);
563 assert_eq!(el.key.as_deref(), Some("n"));
564 assert_eq!(el.children.len(), 3, "decrement, field, increment");
565 assert_eq!(el.children[0].key.as_deref(), Some("n:dec"));
566 assert_eq!(el.children[1].key.as_deref(), Some("n:field"));
567 assert_eq!(el.children[2].key.as_deref(), Some("n:inc"));
568 }
569}