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 title: SharedString,
41}
42
43impl NumberField {
44 pub(crate) fn new(options: Option<&NumberFieldOptions>, title: SharedString) -> Self {
45 Self {
46 options: options.cloned().unwrap_or_default(),
47 title,
48 }
49 }
50}
51
52struct State {
53 input: Entity<InputState>,
54 initial_value: f64,
55 _subscriptions: Vec<Subscription>,
56}
57
58impl SettingFieldRender for NumberField {
59 fn render(
60 &self,
61 field: Rc<dyn AnySettingField>,
62 options: &RenderOptions,
63 style: &StyleRefinement,
64 window: &mut Window,
65 cx: &mut App,
66 ) -> AnyElement {
67 let value = get_value::<f64>(&field, cx);
68 let set_value = set_value::<f64>(&field, cx);
69 let num_options = self.options.clone();
70
71 let state_entity = window.use_keyed_state(
72 SharedString::from(format!(
73 "number-state-{}-{}-{}",
74 options.page_ix(),
75 options.group_ix(),
76 options.item_ix()
77 )),
78 cx,
79 |window, cx| {
80 let input = cx.new(|cx| {
85 InputState::new(window, cx)
86 .default_value(value.to_string())
87 .step(num_options.step)
88 .min(num_options.min)
89 .max(num_options.max)
90 });
91 let _subscriptions = vec![cx.subscribe_in(&input, window, {
92 move |state: &mut State, input, event: &InputEvent, _window, cx| match event {
93 InputEvent::Change => {
94 input.update(cx, |input, cx| {
95 let text = input.value();
96 if text == state.initial_value.to_string() {
97 return;
98 }
99
100 if let Ok(parsed) = text.parse::<f64>() {
106 let clamped = parsed.clamp(num_options.min, num_options.max);
107 set_value(clamped, cx);
108 state.initial_value = clamped;
109 }
110 });
111 }
112 _ => {}
113 }
114 })];
115
116 State {
117 input,
118 initial_value: value,
119 _subscriptions,
120 }
121 },
122 );
123
124 let sync_options = self.options.clone();
127 state_entity.update(cx, |state, cx| {
128 state.input.update(cx, |input, cx| {
129 input.set_step(Some(sync_options.step.into()), window, cx);
130 input.set_min(Some(sync_options.min), window, cx);
131 input.set_max(Some(sync_options.max), window, cx);
132 });
133 if state.initial_value != value {
134 state.initial_value = value;
135 state.input.update(cx, |input, cx| {
136 input.set_value(SharedString::from(value.to_string()), window, cx);
137 });
138 }
139 });
140
141 let state = state_entity.read(cx);
142
143 NumberInput::new(&state.input)
144 .accessibility_label(self.title.clone())
145 .disabled(options.is_disabled())
146 .with_size(options.size())
147 .map(|this| {
148 if options.layout().is_horizontal() {
149 this.w_32()
150 } else {
151 this.w_full()
152 }
153 })
154 .refine_style(style)
155 .into_any_element()
156 }
157}