gpui_component/setting/fields/
number.rs

1use std::rc::Rc;
2
3use gpui::{
4    AnyElement, App, AppContext as _, Entity, IntoElement, SharedString, StyleRefinement, Styled,
5    Subscription, Window, prelude::FluentBuilder as _,
6};
7
8use crate::{
9    AxisExt, Sizable, StyledExt,
10    input::{InputEvent, InputState, NumberInput, NumberInputEvent, StepAction},
11    setting::{
12        AnySettingField, RenderOptions,
13        fields::{SettingFieldRender, get_value, set_value},
14    },
15};
16
17#[derive(Clone, Debug)]
18pub struct NumberFieldOptions {
19    /// The minimum value for the number input, default is `f64::MIN`.
20    pub min: f64,
21    /// The maximum value for the number input, default is `f64::MAX`.
22    pub max: f64,
23    /// The step value for the number input, default is `1.0`.
24    pub step: f64,
25}
26
27impl Default for NumberFieldOptions {
28    fn default() -> Self {
29        Self {
30            min: f64::MIN,
31            max: f64::MAX,
32            step: 1.0,
33        }
34    }
35}
36
37pub(crate) struct NumberField {
38    options: NumberFieldOptions,
39}
40
41impl NumberField {
42    pub(crate) fn new(options: Option<&NumberFieldOptions>) -> Self {
43        Self {
44            options: options.cloned().unwrap_or_default(),
45        }
46    }
47}
48
49struct State {
50    input: Entity<InputState>,
51    initial_value: f64,
52    _subscriptions: Vec<Subscription>,
53}
54
55impl SettingFieldRender for NumberField {
56    fn render(
57        &self,
58        field: Rc<dyn AnySettingField>,
59        options: &RenderOptions,
60        style: &StyleRefinement,
61        window: &mut Window,
62        cx: &mut App,
63    ) -> AnyElement {
64        let value = get_value::<f64>(&field, cx);
65        let set_value = set_value::<f64>(&field, cx);
66        let num_options = self.options.clone();
67
68        let state = window
69            .use_keyed_state(
70                SharedString::from(format!(
71                    "number-state-{}-{}-{}",
72                    options.page_ix, options.group_ix, options.item_ix
73                )),
74                cx,
75                |window, cx| {
76                    let input =
77                        cx.new(|cx| InputState::new(window, cx).default_value(value.to_string()));
78                    let _subscriptions = vec![
79                        cx.subscribe_in(&input, window, {
80                            move |_, input, event: &NumberInputEvent, window, cx| match event {
81                                NumberInputEvent::Step(action) => input.update(cx, |input, cx| {
82                                    let value = input.value();
83                                    if let Ok(value) = value.parse::<f64>() {
84                                        let new_value = if *action == StepAction::Increment {
85                                            value + num_options.step
86                                        } else {
87                                            value - num_options.step
88                                        };
89                                        input.set_value(
90                                            SharedString::from(new_value.to_string()),
91                                            window,
92                                            cx,
93                                        );
94                                    }
95                                }),
96                            }
97                        }),
98                        cx.subscribe_in(&input, window, {
99                            move |state: &mut State, input, event: &InputEvent, window, cx| {
100                                match event {
101                                    InputEvent::Change => {
102                                        input.update(cx, |input, cx| {
103                                            let value = input.value();
104                                            if value == state.initial_value.to_string() {
105                                                return;
106                                            }
107
108                                            if let Ok(value) = value.parse::<f64>() {
109                                                let clamp_value =
110                                                    value.clamp(num_options.min, num_options.max);
111
112                                                set_value(clamp_value, cx);
113                                                state.initial_value = clamp_value;
114                                                if clamp_value != value {
115                                                    input.set_value(
116                                                        SharedString::from(clamp_value.to_string()),
117                                                        window,
118                                                        cx,
119                                                    );
120                                                }
121                                            }
122                                        });
123                                    }
124                                    _ => {}
125                                }
126                            }
127                        }),
128                    ];
129
130                    State {
131                        input,
132                        initial_value: value,
133                        _subscriptions,
134                    }
135                },
136            )
137            .read(cx);
138
139        NumberInput::new(&state.input)
140            .with_size(options.size)
141            .map(|this| {
142                if options.layout.is_horizontal() {
143                    this.w_32()
144                } else {
145                    this.w_full()
146                }
147            })
148            .refine_style(style)
149            .into_any_element()
150    }
151}