Skip to repository content886 lines · 35.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:46:27.268Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
number_field.rs
1use std::{
2 fmt::Display,
3 num::{NonZero, NonZeroU32, NonZeroU64},
4 rc::Rc,
5 str::FromStr,
6};
7
8use editor::Editor;
9use gpui::{
10 AccessibleAction, ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers, Role,
11 TextAlign, TextStyleRefinement, WeakEntity,
12};
13
14use settings::{
15 CenteredPaddingSettings, CodeFade, DelayMs, FontSize, FontWeightContent, InactiveOpacity,
16 MinimumContrast,
17};
18use ui::prelude::*;
19use zed_actions::editor::{MoveDown, MoveUp};
20
21#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum NumberFieldMode {
23 #[default]
24 Read,
25 Edit,
26}
27
28pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static {
29 fn default_format(value: &Self) -> String {
30 format!("{}", value)
31 }
32 fn default_step() -> Self;
33 fn large_step() -> Self;
34 fn small_step() -> Self;
35 fn min_value() -> Self;
36 fn max_value() -> Self;
37 fn saturating_add(self, rhs: Self) -> Self;
38 fn saturating_sub(self, rhs: Self) -> Self;
39}
40
41macro_rules! impl_newtype_numeric_stepper_float {
42 ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
43 impl NumberFieldType for $type {
44 fn default_step() -> Self {
45 $default.into()
46 }
47
48 fn large_step() -> Self {
49 $large.into()
50 }
51
52 fn small_step() -> Self {
53 $small.into()
54 }
55
56 fn min_value() -> Self {
57 $min.into()
58 }
59
60 fn max_value() -> Self {
61 $max.into()
62 }
63
64 fn saturating_add(self, rhs: Self) -> Self {
65 $type((self.0 + rhs.0).min(Self::max_value().0))
66 }
67
68 fn saturating_sub(self, rhs: Self) -> Self {
69 $type((self.0 - rhs.0).max(Self::min_value().0))
70 }
71 }
72 };
73}
74
75macro_rules! impl_newtype_numeric_stepper_int {
76 ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
77 impl NumberFieldType for $type {
78 fn default_step() -> Self {
79 $default.into()
80 }
81
82 fn large_step() -> Self {
83 $large.into()
84 }
85
86 fn small_step() -> Self {
87 $small.into()
88 }
89
90 fn min_value() -> Self {
91 $min.into()
92 }
93
94 fn max_value() -> Self {
95 $max.into()
96 }
97
98 fn saturating_add(self, rhs: Self) -> Self {
99 $type(self.0.saturating_add(rhs.0).min(Self::max_value().0))
100 }
101
102 fn saturating_sub(self, rhs: Self) -> Self {
103 $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0))
104 }
105 }
106 };
107}
108
109#[rustfmt::skip]
110impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
111impl_newtype_numeric_stepper_float!(
112 FontWeightContent,
113 50.,
114 100.,
115 10.,
116 FontWeightContent::THIN,
117 FontWeightContent::BLACK
118);
119impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
120impl_newtype_numeric_stepper_float!(FontSize, 1.0, 4.0, 0.5, 6.0, 72.0);
121impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
122impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
123impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000);
124impl_newtype_numeric_stepper_float!(
125 CenteredPaddingSettings,
126 0.05,
127 0.2,
128 0.1,
129 CenteredPaddingSettings::MIN_PADDING,
130 CenteredPaddingSettings::MAX_PADDING
131);
132
133macro_rules! impl_numeric_stepper_int {
134 ($type:ident) => {
135 impl NumberFieldType for $type {
136 fn default_step() -> Self {
137 1
138 }
139
140 fn large_step() -> Self {
141 10
142 }
143
144 fn small_step() -> Self {
145 1
146 }
147
148 fn min_value() -> Self {
149 <$type>::MIN
150 }
151
152 fn max_value() -> Self {
153 <$type>::MAX
154 }
155
156 fn saturating_add(self, rhs: Self) -> Self {
157 self.saturating_add(rhs)
158 }
159
160 fn saturating_sub(self, rhs: Self) -> Self {
161 self.saturating_sub(rhs)
162 }
163 }
164 };
165}
166
167macro_rules! impl_numeric_stepper_nonzero_int {
168 ($nonzero:ty, $inner:ty) => {
169 impl NumberFieldType for $nonzero {
170 fn default_step() -> Self {
171 <$nonzero>::new(1).unwrap()
172 }
173
174 fn large_step() -> Self {
175 <$nonzero>::new(10).unwrap()
176 }
177
178 fn small_step() -> Self {
179 <$nonzero>::new(1).unwrap()
180 }
181
182 fn min_value() -> Self {
183 <$nonzero>::MIN
184 }
185
186 fn max_value() -> Self {
187 <$nonzero>::MAX
188 }
189
190 fn saturating_add(self, rhs: Self) -> Self {
191 let result = self.get().saturating_add(rhs.get());
192 <$nonzero>::new(result.max(1)).unwrap()
193 }
194
195 fn saturating_sub(self, rhs: Self) -> Self {
196 let result = self.get().saturating_sub(rhs.get()).max(1);
197 <$nonzero>::new(result).unwrap()
198 }
199 }
200 };
201}
202
203macro_rules! impl_numeric_stepper_float {
204 ($type:ident) => {
205 impl NumberFieldType for $type {
206 fn default_format(value: &Self) -> String {
207 format!("{:.2}", value)
208 }
209
210 fn default_step() -> Self {
211 1.0
212 }
213
214 fn large_step() -> Self {
215 10.0
216 }
217
218 fn small_step() -> Self {
219 0.1
220 }
221
222 fn min_value() -> Self {
223 <$type>::MIN
224 }
225
226 fn max_value() -> Self {
227 <$type>::MAX
228 }
229
230 fn saturating_add(self, rhs: Self) -> Self {
231 (self + rhs).clamp(Self::min_value(), Self::max_value())
232 }
233
234 fn saturating_sub(self, rhs: Self) -> Self {
235 (self - rhs).clamp(Self::min_value(), Self::max_value())
236 }
237 }
238 };
239}
240
241impl_numeric_stepper_float!(f32);
242impl_numeric_stepper_float!(f64);
243impl_numeric_stepper_int!(isize);
244impl_numeric_stepper_int!(usize);
245impl_numeric_stepper_int!(i32);
246impl_numeric_stepper_int!(u32);
247impl_numeric_stepper_int!(i64);
248impl_numeric_stepper_int!(u64);
249
250impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
251impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
252impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
253
254type OnChangeCallback<T> = Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>;
255
256#[derive(IntoElement, RegisterComponent)]
257pub struct NumberField<T: NumberFieldType = usize> {
258 id: ElementId,
259 value: T,
260 focus_handle: FocusHandle,
261 mode: Entity<NumberFieldMode>,
262 /// Stores a weak reference to the editor when in edit mode, so buttons can update its text
263 edit_editor: Entity<Option<WeakEntity<Editor>>>,
264 /// Stores the on_change callback in Entity state so it's not stale in focus_out handlers
265 on_change_state: Entity<Option<OnChangeCallback<T>>>,
266 /// Tracks the last prop value we synced to, so we can detect external changes (like reset)
267 last_synced_value: Entity<Option<T>>,
268 format: Box<dyn FnOnce(&T) -> String>,
269 large_step: T,
270 small_step: T,
271 step: T,
272 min_value: T,
273 max_value: T,
274 on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
275 on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
276 tab_index: Option<isize>,
277 aria_label: Option<SharedString>,
278 aria_description: Option<SharedString>,
279}
280
281impl<T: NumberFieldType> NumberField<T> {
282 pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
283 let id = id.into();
284
285 let (mode, focus_handle, edit_editor, on_change_state, last_synced_value) =
286 window.with_id(id.clone(), |window| {
287 let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
288 let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
289 let edit_editor = window.use_state(cx, |_, _| None);
290 let on_change_state: Entity<Option<OnChangeCallback<T>>> =
291 window.use_state(cx, |_, _| None);
292 let last_synced_value: Entity<Option<T>> = window.use_state(cx, |_, _| None);
293 (
294 mode,
295 focus_handle,
296 edit_editor,
297 on_change_state,
298 last_synced_value,
299 )
300 });
301
302 Self {
303 id,
304 mode,
305 edit_editor,
306 on_change_state,
307 last_synced_value,
308 value,
309 focus_handle: focus_handle.read(cx).clone(),
310 format: Box::new(T::default_format),
311 large_step: T::large_step(),
312 step: T::default_step(),
313 small_step: T::small_step(),
314 min_value: T::min_value(),
315 max_value: T::max_value(),
316 on_reset: None,
317 on_change: Rc::new(|_, _, _| {}),
318 tab_index: None,
319 aria_label: None,
320 aria_description: None,
321 }
322 }
323
324 pub fn min(mut self, min: T) -> Self {
325 self.min_value = min;
326 self
327 }
328
329 pub fn max(mut self, max: T) -> Self {
330 self.max_value = max;
331 self
332 }
333
334 pub fn mode(self, mode: NumberFieldMode, cx: &mut App) -> Self {
335 self.mode.write(cx, mode);
336 self
337 }
338
339 pub fn tab_index(mut self, tab_index: isize) -> Self {
340 self.tab_index = Some(tab_index);
341 self
342 }
343
344 pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
345 self.on_change = Rc::new(on_change);
346 self
347 }
348
349 /// Sets the label announced by assistive technology.
350 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
351 self.aria_label = Some(label.into());
352 self
353 }
354
355 /// Sets the supplementary description announced by assistive technology
356 /// after the field's name, role, and value.
357 pub fn aria_description(mut self, description: impl Into<SharedString>) -> Self {
358 self.aria_description = Some(description.into());
359 self
360 }
361
362 fn sync_on_change_state(&self, cx: &mut App) {
363 self.on_change_state
364 .update(cx, |state, _| *state = Some(self.on_change.clone()));
365 }
366}
367
368#[derive(Clone, Copy)]
369enum ValueChangeDirection {
370 Increment,
371 Decrement,
372}
373
374/// Best-effort conversion of a numeric field value to `f64` for reporting to
375/// assistive technology. Goes through the `Display` representation because
376/// `NumberFieldType` has no general numeric conversion.
377fn a11y_numeric_value(value: &impl Display) -> Option<f64> {
378 format!("{}", value).parse::<f64>().ok()
379}
380
381/// Best-effort conversion of an assistive-technology-provided `f64` back into
382/// a field value. Falls back to the rounded value for integer field types.
383fn a11y_value_to_field_value<T: NumberFieldType>(value: f64) -> Option<T> {
384 format!("{}", value)
385 .parse::<T>()
386 .ok()
387 .or_else(|| format!("{}", value.round()).parse::<T>().ok())
388}
389
390impl<T: NumberFieldType> RenderOnce for NumberField<T> {
391 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
392 // Sync the on_change callback to Entity state so focus_out handlers can access it
393 self.sync_on_change_state(cx);
394
395 let is_edit_mode = matches!(*self.mode.read(cx), NumberFieldMode::Edit);
396
397 let get_step = {
398 let large_step = self.large_step;
399 let step = self.step;
400 let small_step = self.small_step;
401 move |modifiers: Modifiers| -> T {
402 if modifiers.shift {
403 large_step
404 } else if modifiers.alt {
405 small_step
406 } else {
407 step
408 }
409 }
410 };
411
412 let clamp_value = {
413 let min = self.min_value;
414 let max = self.max_value;
415 move |value: T| -> T {
416 if value < min {
417 min
418 } else if value > max {
419 max
420 } else {
421 value
422 }
423 }
424 };
425
426 let change_value = {
427 move |current: T, step: T, direction: ValueChangeDirection| -> T {
428 let new_value = match direction {
429 ValueChangeDirection::Increment => current.saturating_add(step),
430 ValueChangeDirection::Decrement => current.saturating_sub(step),
431 };
432 clamp_value(new_value)
433 }
434 };
435
436 let get_current_value = {
437 let value = self.value;
438 let edit_editor = self.edit_editor.clone();
439
440 Rc::new(move |cx: &App| -> T {
441 if !is_edit_mode {
442 return value;
443 }
444 edit_editor
445 .read(cx)
446 .as_ref()
447 .and_then(|weak| weak.upgrade())
448 .and_then(|editor| editor.read(cx).text(cx).parse::<T>().ok())
449 .unwrap_or(value)
450 })
451 };
452
453 let update_editor_text = {
454 let edit_editor = self.edit_editor.clone();
455
456 Rc::new(move |new_value: T, window: &mut Window, cx: &mut App| {
457 if !is_edit_mode {
458 return;
459 }
460 let Some(editor) = edit_editor
461 .read(cx)
462 .as_ref()
463 .and_then(|weak| weak.upgrade())
464 else {
465 return;
466 };
467 editor.update(cx, |editor, cx| {
468 editor.set_text(format!("{}", new_value), window, cx);
469 });
470 })
471 };
472
473 let step_value = {
474 let on_change = self.on_change.clone();
475 let get_current_value = get_current_value.clone();
476 let update_editor_text = update_editor_text.clone();
477
478 Rc::new(
479 move |direction: ValueChangeDirection,
480 modifiers: Modifiers,
481 window: &mut Window,
482 cx: &mut App| {
483 let current_value = get_current_value(cx);
484 let step = get_step(modifiers);
485 let new_value = change_value(current_value, step, direction);
486
487 update_editor_text(new_value, window, cx);
488 on_change(&new_value, window, cx);
489 },
490 )
491 };
492
493 let set_value = {
494 let on_change = self.on_change.clone();
495 let update_editor_text = update_editor_text.clone();
496
497 move |raw_value: f64, window: &mut Window, cx: &mut App| {
498 let Some(parsed) = a11y_value_to_field_value::<T>(raw_value) else {
499 return;
500 };
501 let new_value = clamp_value(parsed);
502 update_editor_text(new_value, window, cx);
503 on_change(&new_value, window, cx);
504 }
505 };
506
507 let bg_color = cx.theme().colors().surface_background;
508 let hover_bg_color = cx.theme().colors().element_hover;
509
510 let border_color = cx.theme().colors().border_variant;
511 let focus_border_color = cx.theme().colors().border_focused;
512
513 let base_button = |icon: IconName| {
514 h_flex()
515 .cursor_pointer()
516 .p_1p5()
517 .size_full()
518 .justify_center()
519 .overflow_hidden()
520 .border_1()
521 .border_color(border_color)
522 .bg(bg_color)
523 .hover(|s| s.bg(hover_bg_color))
524 .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color))
525 .child(Icon::new(icon).size(IconSize::Small))
526 };
527
528 h_flex()
529 .id(self.id.clone())
530 .role(Role::SpinButton)
531 .when_some(self.aria_label.clone(), |this, label| {
532 this.aria_label(label)
533 })
534 .when_some(self.aria_description.clone(), |this, description| {
535 this.aria_description(description)
536 })
537 .when_some(a11y_numeric_value(&self.value), |this, value| {
538 this.aria_numeric_value(value)
539 })
540 .when_some(a11y_numeric_value(&self.min_value), |this, min| {
541 this.aria_min_numeric_value(min)
542 })
543 .when_some(a11y_numeric_value(&self.max_value), |this, max| {
544 this.aria_max_numeric_value(max)
545 })
546 .when_some(a11y_numeric_value(&self.step), |this, step| {
547 this.aria_numeric_value_step(step)
548 })
549 // Some assistive technology (e.g. Orca via AT-SPI's Value
550 // interface) can only set an absolute value, while other screen
551 // readers dispatch Increment/Decrement, so support all three.
552 .on_a11y_action(AccessibleAction::SetValue, {
553 move |data, window, cx| {
554 if let Some(gpui::accesskit::ActionData::NumericValue(value)) = data {
555 set_value(*value, window, cx);
556 }
557 }
558 })
559 .on_a11y_action(AccessibleAction::Increment, {
560 let step_value = step_value.clone();
561 move |_, window, cx| {
562 step_value(
563 ValueChangeDirection::Increment,
564 Modifiers::default(),
565 window,
566 cx,
567 );
568 }
569 })
570 .on_a11y_action(AccessibleAction::Decrement, {
571 let step_value = step_value.clone();
572 move |_, window, cx| {
573 step_value(
574 ValueChangeDirection::Decrement,
575 Modifiers::default(),
576 window,
577 cx,
578 );
579 }
580 })
581 .track_focus(&self.focus_handle)
582 .gap_1()
583 .when_some(self.on_reset, |this, on_reset| {
584 this.child(
585 IconButton::new("reset", IconName::RotateCcw)
586 .icon_size(IconSize::Small)
587 .aria_label("Reset to Default")
588 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
589 .on_click(on_reset),
590 )
591 })
592 .child({
593 h_flex()
594 .map(|decrement| {
595 let decrement_handler = {
596 let step_value = step_value.clone();
597
598 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
599 step_value(
600 ValueChangeDirection::Decrement,
601 click.modifiers(),
602 window,
603 cx,
604 );
605 }
606 };
607
608 decrement.child(
609 base_button(IconName::Dash)
610 .id((self.id.clone(), "decrement_button"))
611 .role(Role::Button)
612 .aria_label("Decrement")
613 .rounded_tl_sm()
614 .rounded_bl_sm()
615 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
616 .on_click(decrement_handler),
617 )
618 })
619 .child({
620 h_flex()
621 .min_w_16()
622 .size_full()
623 .border_y_1()
624 .border_color(border_color)
625 .bg(bg_color)
626 .in_focus(|this| this.border_color(focus_border_color))
627 .child(match *self.mode.read(cx) {
628 NumberFieldMode::Read => h_flex()
629 .px_1()
630 .flex_1()
631 .justify_center()
632 .child(
633 Label::new((self.format)(&self.value)).color(Color::Muted),
634 )
635 .into_any_element(),
636 NumberFieldMode::Edit => {
637 let expected_text = format!("{}", self.value);
638
639 let editor = window.use_state(cx, {
640 let expected_text = expected_text.clone();
641
642 move |window, cx| {
643 let mut editor = Editor::single_line(window, cx);
644
645 editor.set_text_style_refinement(TextStyleRefinement {
646 color: Some(cx.theme().colors().text),
647 text_align: Some(TextAlign::Center),
648 ..Default::default()
649 });
650
651 editor.set_text(expected_text, window, cx);
652
653 let editor_weak = cx.entity().downgrade();
654
655 self.edit_editor.update(cx, |state, _| {
656 *state = Some(editor_weak);
657 });
658
659 editor
660 .register_action::<MoveUp>({
661 let on_change = self.on_change.clone();
662 let editor_handle = cx.entity().downgrade();
663 move |_, window, cx| {
664 let Some(editor) = editor_handle.upgrade()
665 else {
666 return;
667 };
668 editor.update(cx, |editor, cx| {
669 if let Ok(current_value) =
670 editor.text(cx).parse::<T>()
671 {
672 let step =
673 get_step(window.modifiers());
674 let new_value = change_value(
675 current_value,
676 step,
677 ValueChangeDirection::Increment,
678 );
679 editor.set_text(
680 format!("{}", new_value),
681 window,
682 cx,
683 );
684 on_change(&new_value, window, cx);
685 }
686 });
687 }
688 })
689 .detach();
690
691 editor
692 .register_action::<MoveDown>({
693 let on_change = self.on_change.clone();
694 let editor_handle = cx.entity().downgrade();
695 move |_, window, cx| {
696 let Some(editor) = editor_handle.upgrade()
697 else {
698 return;
699 };
700 editor.update(cx, |editor, cx| {
701 if let Ok(current_value) =
702 editor.text(cx).parse::<T>()
703 {
704 let step =
705 get_step(window.modifiers());
706 let new_value = change_value(
707 current_value,
708 step,
709 ValueChangeDirection::Decrement,
710 );
711 editor.set_text(
712 format!("{}", new_value),
713 window,
714 cx,
715 );
716 on_change(&new_value, window, cx);
717 }
718 });
719 }
720 })
721 .detach();
722
723 cx.on_focus_out(&editor.focus_handle(cx), window, {
724 let on_change_state = self.on_change_state.clone();
725 move |this, _, window, cx| {
726 if let Ok(parsed_value) =
727 this.text(cx).parse::<T>()
728 {
729 let new_value = clamp_value(parsed_value);
730 let on_change =
731 on_change_state.read(cx).clone();
732
733 if let Some(on_change) = on_change.as_ref()
734 {
735 on_change(&new_value, window, cx);
736 }
737 };
738 }
739 })
740 .detach();
741
742 editor
743 }
744 });
745
746 let focus_handle = editor.focus_handle(cx);
747 let is_focused = focus_handle.is_focused(window);
748
749 if !is_focused {
750 let current_text = editor.read(cx).text(cx);
751 let last_synced = *self.last_synced_value.read(cx);
752
753 // Detect if the value changed externally (e.g., reset button)
754 let value_changed_externally = last_synced
755 .map(|last| last != self.value)
756 .unwrap_or(true);
757
758 let should_sync = if value_changed_externally {
759 true
760 } else {
761 match current_text.parse::<T>().ok() {
762 Some(parsed) => parsed == self.value,
763 None => true,
764 }
765 };
766
767 if should_sync && current_text != expected_text {
768 editor.update(cx, |editor, cx| {
769 editor.set_text(expected_text.clone(), window, cx);
770 });
771 }
772
773 self.last_synced_value
774 .update(cx, |state, _| *state = Some(self.value));
775 }
776
777 let focus_handle = if self.tab_index.is_some() {
778 focus_handle.tab_index(0isize).tab_stop(true)
779 } else {
780 focus_handle
781 };
782
783 h_flex()
784 .id((self.id.clone(), "editor"))
785 .role(Role::TextInput)
786 .when_some(self.aria_label.clone(), |this, label| {
787 this.aria_label(label)
788 })
789 .when_some(
790 self.aria_description.clone(),
791 |this, description| this.aria_description(description),
792 )
793 .flex_1()
794 .h_full()
795 .track_focus(&focus_handle)
796 .when(is_focused, |this| {
797 this.border_1()
798 .border_color(cx.theme().colors().border_focused)
799 })
800 .child(editor)
801 .on_action::<menu::Confirm>({
802 move |_, window, _| {
803 window.blur();
804 }
805 })
806 .into_any_element()
807 }
808 })
809 })
810 .map(|increment| {
811 let increment_handler = {
812 let step_value = step_value.clone();
813
814 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
815 step_value(
816 ValueChangeDirection::Increment,
817 click.modifiers(),
818 window,
819 cx,
820 );
821 }
822 };
823
824 increment.child(
825 base_button(IconName::Plus)
826 .id((self.id.clone(), "increment_button"))
827 .role(Role::Button)
828 .aria_label("Increment")
829 .rounded_tr_sm()
830 .rounded_br_sm()
831 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
832 .on_click(increment_handler),
833 )
834 })
835 })
836 }
837}
838
839impl Component for NumberField<usize> {
840 fn scope() -> ComponentScope {
841 ComponentScope::Input
842 }
843
844 fn name() -> &'static str {
845 "Number Field"
846 }
847
848 fn description() -> &'static str {
849 "A numeric input element with increment and decrement buttons."
850 }
851
852 fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
853 let default_ex = window.use_state(cx, |_, _| 100.0);
854 let edit_ex = window.use_state(cx, |_, _| 500.0);
855
856 v_flex()
857 .gap_6()
858 .children(vec![
859 single_example(
860 "Button-Only Number Field",
861 NumberField::new("number-field", *default_ex.read(cx), window, cx)
862 .on_change({
863 let default_ex = default_ex.clone();
864 move |value, _, cx| default_ex.write(cx, *value)
865 })
866 .min(1.0)
867 .max(100.0)
868 .into_any_element(),
869 ),
870 single_example(
871 "Editable Number Field",
872 NumberField::new("editable-number-field", *edit_ex.read(cx), window, cx)
873 .on_change({
874 let edit_ex = edit_ex.clone();
875 move |value, _, cx| edit_ex.write(cx, *value)
876 })
877 .min(100.0)
878 .max(500.0)
879 .mode(NumberFieldMode::Edit, cx)
880 .into_any_element(),
881 ),
882 ])
883 .into_any_element()
884 }
885}
886