Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:46:27.268Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

number_field.rs

886 lines · 35.0 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.