gpui_component/setting/fields/
number.rs1use 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},
11 setting::{
12 AnySettingField, RenderOptions,
13 fields::{SettingFieldRender, get_value, set_value},
14 },
15};
16
17#[derive(Clone, Debug)]
18pub struct NumberFieldOptions {
19 pub min: f64,
21 pub max: f64,
23 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_entity = window.use_keyed_state(
69 SharedString::from(format!(
70 "number-state-{}-{}-{}",
71 options.page_ix(),
72 options.group_ix(),
73 options.item_ix()
74 )),
75 cx,
76 |window, cx| {
77 let input = cx.new(|cx| {
82 InputState::new(window, cx)
83 .default_value(value.to_string())
84 .step(num_options.step)
85 .min(num_options.min)
86 .max(num_options.max)
87 });
88 let _subscriptions = vec![cx.subscribe_in(&input, window, {
89 move |state: &mut State, input, event: &InputEvent, _window, cx| match event {
90 InputEvent::Change => {
91 input.update(cx, |input, cx| {
92 let text = input.value();
93 if text == state.initial_value.to_string() {
94 return;
95 }
96
97 if let Ok(parsed) = text.parse::<f64>() {
103 let clamped = parsed.clamp(num_options.min, num_options.max);
104 set_value(clamped, cx);
105 state.initial_value = clamped;
106 }
107 });
108 }
109 _ => {}
110 }
111 })];
112
113 State {
114 input,
115 initial_value: value,
116 _subscriptions,
117 }
118 },
119 );
120
121 let sync_options = self.options.clone();
124 state_entity.update(cx, |state, cx| {
125 state.input.update(cx, |input, cx| {
126 input.set_step(Some(sync_options.step.into()), window, cx);
127 input.set_min(Some(sync_options.min), window, cx);
128 input.set_max(Some(sync_options.max), window, cx);
129 });
130 if state.initial_value != value {
131 state.initial_value = value;
132 state.input.update(cx, |input, cx| {
133 input.set_value(SharedString::from(value.to_string()), window, cx);
134 });
135 }
136 });
137
138 let state = state_entity.read(cx);
139
140 NumberInput::new(&state.input)
141 .disabled(options.is_disabled())
142 .with_size(options.size())
143 .map(|this| {
144 if options.layout().is_horizontal() {
145 this.w_32()
146 } else {
147 this.w_full()
148 }
149 })
150 .refine_style(style)
151 .into_any_element()
152 }
153}