1use gpui::{
2 actions, prelude::FluentBuilder as _, px, AnyElement, App, Context, Entity, EventEmitter,
3 FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, RenderOnce,
4 SharedString, StyleRefinement, Styled, Window,
5};
6
7use crate::{
8 button::{Button, ButtonVariants as _},
9 h_flex, ActiveTheme, Disableable, IconName, Sizable, Size, StyleSized, StyledExt as _,
10};
11
12use super::{Input, InputState};
13
14actions!(number_input, [Increment, Decrement]);
15
16const CONTEXT: &str = "NumberInput";
17pub fn init(cx: &mut App) {
18 cx.bind_keys(vec![
19 KeyBinding::new("up", Increment, Some(CONTEXT)),
20 KeyBinding::new("down", Decrement, Some(CONTEXT)),
21 ]);
22}
23
24#[derive(IntoElement)]
26pub struct NumberInput {
27 state: Entity<InputState>,
28 placeholder: SharedString,
29 size: Size,
30 prefix: Option<AnyElement>,
31 suffix: Option<AnyElement>,
32 appearance: bool,
33 disabled: bool,
34 style: StyleRefinement,
35}
36
37impl NumberInput {
38 pub fn new(state: &Entity<InputState>) -> Self {
40 Self {
41 state: state.clone(),
42 size: Size::default(),
43 placeholder: SharedString::default(),
44 prefix: None,
45 suffix: None,
46 appearance: true,
47 disabled: false,
48 style: StyleRefinement::default(),
49 }
50 }
51
52 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
54 self.placeholder = placeholder.into();
55 self
56 }
57
58 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
60 self.prefix = Some(prefix.into_any_element());
61 self
62 }
63
64 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
66 self.suffix = Some(suffix.into_any_element());
67 self
68 }
69
70 pub fn appearance(mut self, appearance: bool) -> Self {
72 self.appearance = appearance;
73 self
74 }
75
76 fn on_increment(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
77 state.update(cx, |state, cx| {
78 state.on_action_increment(&Increment, window, cx);
79 })
80 }
81
82 fn on_decrement(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
83 state.update(cx, |state, cx| {
84 state.on_action_decrement(&Decrement, window, cx);
85 })
86 }
87}
88
89impl Disableable for NumberInput {
90 fn disabled(mut self, disabled: bool) -> Self {
91 self.disabled = disabled;
92 self
93 }
94}
95
96impl InputState {
97 fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
98 self.on_number_input_step(StepAction::Increment, window, cx);
99 }
100
101 fn on_action_decrement(&mut self, _: &Decrement, window: &mut Window, cx: &mut Context<Self>) {
102 self.on_number_input_step(StepAction::Decrement, window, cx);
103 }
104
105 fn on_number_input_step(&mut self, action: StepAction, _: &mut Window, cx: &mut Context<Self>) {
106 if self.disabled {
107 return;
108 }
109
110 cx.emit(NumberInputEvent::Step(action));
111 }
112}
113
114#[derive(Clone, Copy, PartialEq, Eq)]
115pub enum StepAction {
116 Decrement,
117 Increment,
118}
119pub enum NumberInputEvent {
120 Step(StepAction),
121}
122impl EventEmitter<NumberInputEvent> for InputState {}
123
124impl Focusable for NumberInput {
125 fn focus_handle(&self, cx: &App) -> FocusHandle {
126 self.state.focus_handle(cx)
127 }
128}
129
130impl Sizable for NumberInput {
131 fn with_size(mut self, size: impl Into<Size>) -> Self {
132 self.size = size.into();
133 self
134 }
135}
136
137impl Styled for NumberInput {
138 fn style(&mut self) -> &mut StyleRefinement {
139 &mut self.style
140 }
141}
142
143impl RenderOnce for NumberInput {
144 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
145 let focused = self.state.focus_handle(cx).is_focused(window);
146
147 h_flex()
148 .id(("number-input", self.state.entity_id()))
149 .key_context(CONTEXT)
150 .on_action(window.listener_for(&self.state, InputState::on_action_increment))
151 .on_action(window.listener_for(&self.state, InputState::on_action_decrement))
152 .flex_1()
153 .input_size(self.size)
154 .px(self.size.input_px() / 2.)
155 .when(self.appearance, |this| {
156 this.bg(cx.theme().background)
157 .border_color(cx.theme().input)
158 .border_1()
159 .rounded(cx.theme().radius)
160 .refine_style(&self.style)
161 })
162 .when(self.disabled, |this| this.bg(cx.theme().muted))
163 .when(focused, |this| this.focused_border(cx))
164 .child(
165 Button::new("-")
166 .ghost()
167 .with_size(self.size.smaller())
168 .icon(IconName::Minus)
169 .compact()
170 .tab_stop(false)
171 .disabled(self.disabled)
172 .on_click({
173 let state = self.state.clone();
174 move |_, window, cx| {
175 Self::on_decrement(&state, window, cx);
176 }
177 }),
178 )
179 .child(
180 Input::new(&self.state)
181 .appearance(false)
182 .disabled(self.disabled)
183 .px(px(2.))
184 .gap_0()
185 .when_some(self.prefix, |this, prefix| this.prefix(prefix))
186 .when_some(self.suffix, |this, suffix| this.suffix(suffix)),
187 )
188 .child(
189 Button::new("+")
190 .ghost()
191 .with_size(self.size.smaller())
192 .icon(IconName::Plus)
193 .compact()
194 .tab_stop(false)
195 .disabled(self.disabled)
196 .on_click({
197 let state = self.state.clone();
198 move |_, window, cx| {
199 Self::on_increment(&state, window, cx);
200 }
201 }),
202 )
203 }
204}