Skip to main content

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, Disableable, 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 step_set_value = set_value.clone();
67        let num_options = self.options.clone();
68
69        let state_entity = window.use_keyed_state(
70            SharedString::from(format!(
71                "number-state-{}-{}-{}",
72                options.page_ix(),
73                options.group_ix(),
74                options.item_ix()
75            )),
76            cx,
77            |window, cx| {
78                let input =
79                    cx.new(|cx| InputState::new(window, cx).default_value(value.to_string()));
80                let _subscriptions = vec![
81                    cx.subscribe_in(&input, window, {
82                        move |state: &mut State, input, event: &NumberInputEvent, window, cx| {
83                            match event {
84                                NumberInputEvent::Step(action) => {
85                                    let value = input.read(cx).value();
86                                    if let Ok(value) = value.parse::<f64>() {
87                                        let new_value = if *action == StepAction::Increment {
88                                            value + num_options.step
89                                        } else {
90                                            value - num_options.step
91                                        };
92                                        let clamp_value =
93                                            new_value.clamp(num_options.min, num_options.max);
94
95                                        input.update(cx, |input, cx| {
96                                            input.set_value(
97                                                SharedString::from(clamp_value.to_string()),
98                                                window,
99                                                cx,
100                                            );
101                                        });
102                                        step_set_value(clamp_value, cx);
103                                        state.initial_value = clamp_value;
104                                    }
105                                }
106                            }
107                        }
108                    }),
109                    cx.subscribe_in(&input, window, {
110                        move |state: &mut State, input, event: &InputEvent, window, cx| match event
111                        {
112                            InputEvent::Change => {
113                                input.update(cx, |input, cx| {
114                                    let value = input.value();
115                                    if value == state.initial_value.to_string() {
116                                        return;
117                                    }
118
119                                    if let Ok(value) = value.parse::<f64>() {
120                                        let clamp_value =
121                                            value.clamp(num_options.min, num_options.max);
122
123                                        set_value(clamp_value, cx);
124                                        state.initial_value = clamp_value;
125                                        if clamp_value != value {
126                                            input.set_value(
127                                                SharedString::from(clamp_value.to_string()),
128                                                window,
129                                                cx,
130                                            );
131                                        }
132                                    }
133                                });
134                            }
135                            _ => {}
136                        }
137                    }),
138                ];
139
140                State {
141                    input,
142                    initial_value: value,
143                    _subscriptions,
144                }
145            },
146        );
147
148        // Sync the displayed value when the underlying setting changed externally
149        state_entity.update(cx, |state, cx| {
150            if state.initial_value != value {
151                state.initial_value = value;
152                state.input.update(cx, |input, cx| {
153                    input.set_value(SharedString::from(value.to_string()), window, cx);
154                });
155            }
156        });
157
158        let state = state_entity.read(cx);
159
160        NumberInput::new(&state.input)
161            .disabled(options.is_disabled())
162            .with_size(options.size())
163            .map(|this| {
164                if options.layout().is_horizontal() {
165                    this.w_32()
166                } else {
167                    this.w_full()
168                }
169            })
170            .refine_style(style)
171            .into_any_element()
172    }
173}