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