1use std::rc::Rc;
2
3use editor::Editor;
4use gpui::{AnyElement, ElementId, Focusable, TextStyleRefinement};
5use settings::Settings as _;
6use theme_settings::ThemeSettings;
7use ui::{Tooltip, prelude::*, rems};
8
9#[derive(IntoElement)]
10pub struct SettingsInputField {
11 id: Option<ElementId>,
12 initial_text: Option<String>,
13 placeholder: Option<&'static str>,
14 confirm: Option<Rc<dyn Fn(Option<String>, &mut Window, &mut App)>>,
15 tab_index: Option<isize>,
16 use_buffer_font: bool,
17 display_confirm_button: bool,
18 display_clear_button: bool,
19 clear_on_confirm: bool,
20 action_slot: Option<AnyElement>,
21 color: Option<Color>,
22}
23
24impl SettingsInputField {
25 pub fn new() -> Self {
26 Self {
27 id: None,
28 initial_text: None,
29 placeholder: None,
30 confirm: None,
31 tab_index: None,
32 use_buffer_font: false,
33 display_confirm_button: false,
34 display_clear_button: false,
35 clear_on_confirm: false,
36 action_slot: None,
37 color: None,
38 }
39 }
40
41 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
42 self.id = Some(id.into());
43 self
44 }
45
46 pub fn with_initial_text(mut self, initial_text: String) -> Self {
47 self.initial_text = Some(initial_text);
48 self
49 }
50
51 pub fn with_placeholder(mut self, placeholder: &'static str) -> Self {
52 self.placeholder = Some(placeholder);
53 self
54 }
55
56 pub fn on_confirm(
57 mut self,
58 confirm: impl Fn(Option<String>, &mut Window, &mut App) + 'static,
59 ) -> Self {
60 self.confirm = Some(Rc::new(confirm));
61 self
62 }
63
64 pub fn display_confirm_button(mut self) -> Self {
65 self.display_confirm_button = true;
66 self
67 }
68
69 pub fn display_clear_button(mut self) -> Self {
70 self.display_clear_button = true;
71 self
72 }
73
74 pub fn clear_on_confirm(mut self) -> Self {
75 self.clear_on_confirm = true;
76 self
77 }
78
79 pub fn action_slot(mut self, action: impl IntoElement) -> Self {
80 self.action_slot = Some(action.into_any_element());
81 self
82 }
83
84 pub(crate) fn tab_index(mut self, arg: isize) -> Self {
85 self.tab_index = Some(arg);
86 self
87 }
88
89 pub fn with_buffer_font(mut self) -> Self {
90 self.use_buffer_font = true;
91 self
92 }
93
94 pub fn color(mut self, color: Color) -> Self {
95 self.color = Some(color);
96 self
97 }
98}
99
100impl RenderOnce for SettingsInputField {
101 fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
102 let settings = ThemeSettings::get_global(cx);
103 let use_buffer_font = self.use_buffer_font;
104 let color = self.color.map(|c| c.color(cx));
105 let styles = TextStyleRefinement {
106 font_family: use_buffer_font.then(|| settings.buffer_font.family.clone()),
107 font_size: use_buffer_font.then(|| rems(0.75).into()),
108 color,
109 ..Default::default()
110 };
111
112 let first_render_initial_text = window.use_state(cx, |_, _| self.initial_text.clone());
113
114 let editor = if let Some(id) = self.id {
115 window.use_keyed_state(id, cx, {
116 let initial_text = self.initial_text.clone();
117 let placeholder = self.placeholder;
118 let mut confirm = self.confirm.clone();
119
120 move |window, cx| {
121 let mut editor = Editor::single_line(window, cx);
122 let editor_focus_handle = editor.focus_handle(cx);
123 if let Some(text) = initial_text {
124 editor.set_text(text, window, cx);
125 }
126
127 if let Some(confirm) = confirm.take()
128 && !self.display_confirm_button
129 && !self.display_clear_button
130 && !self.clear_on_confirm
131 {
132 cx.on_focus_out(
133 &editor_focus_handle,
134 window,
135 move |editor, _, window, cx| {
136 let text = Some(editor.text(cx));
137 confirm(text, window, cx);
138 },
139 )
140 .detach();
141 }
142
143 if let Some(placeholder) = placeholder {
144 editor.set_placeholder_text(placeholder, window, cx);
145 }
146 editor.set_text_style_refinement(styles);
147 editor
148 }
149 })
150 } else {
151 window.use_state(cx, {
152 let initial_text = self.initial_text.clone();
153 let placeholder = self.placeholder;
154 let mut confirm = self.confirm.clone();
155
156 move |window, cx| {
157 let mut editor = Editor::single_line(window, cx);
158 let editor_focus_handle = editor.focus_handle(cx);
159 if let Some(text) = initial_text {
160 editor.set_text(text, window, cx);
161 }
162
163 if let Some(confirm) = confirm.take()
164 && !self.display_confirm_button
165 && !self.display_clear_button
166 && !self.clear_on_confirm
167 {
168 cx.on_focus_out(
169 &editor_focus_handle,
170 window,
171 move |editor, _, window, cx| {
172 let text = Some(editor.text(cx));
173 confirm(text, window, cx);
174 },
175 )
176 .detach();
177 }
178
179 if let Some(placeholder) = placeholder {
180 editor.set_placeholder_text(placeholder, window, cx);
181 }
182 editor.set_text_style_refinement(styles);
183 editor
184 }
185 })
186 };
187
188 // When settings change externally (e.g. editing settings.json), the page
189 // re-renders but use_keyed_state returns the cached editor with stale text.
190 // Reconcile with the expected initial_text when the editor is not focused,
191 // so we don't clobber what the user is actively typing.
192 if let Some(initial_text) = &self.initial_text
193 && let Some(first_initial) = first_render_initial_text.read(cx)
194 {
195 if initial_text != first_initial && !editor.read(cx).is_focused(window) {
196 *first_render_initial_text.as_mut(cx) = self.initial_text.clone();
197 let weak_editor = editor.downgrade();
198 let initial_text = initial_text.clone();
199
200 window.defer(cx, move |window, cx| {
201 weak_editor
202 .update(cx, |editor, cx| {
203 editor.set_text(initial_text, window, cx);
204 })
205 .ok();
206 });
207 }
208 }
209
210 let weak_editor = editor.downgrade();
211 let weak_editor_for_button = editor.downgrade();
212 let weak_editor_for_clear = editor.downgrade();
213
214 let clear_on_confirm = self.clear_on_confirm;
215 let clear_on_confirm_for_button = self.clear_on_confirm;
216
217 let theme_colors = cx.theme().colors();
218
219 let display_confirm_button = self.display_confirm_button;
220 let display_clear_button = self.display_clear_button;
221 let confirm_for_button = self.confirm.clone();
222 let is_editor_empty = editor.read(cx).text(cx).trim().is_empty();
223 let is_editor_focused = editor.read(cx).is_focused(window);
224
225 h_flex()
226 .group("settings-input-field-editor")
227 .relative()
228 .py_1()
229 .px_2()
230 .h_8()
231 .min_w_64()
232 .rounded_md()
233 .border_1()
234 .border_color(theme_colors.border)
235 .bg(theme_colors.editor_background)
236 .when_some(self.tab_index, |this, tab_index| {
237 let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true);
238 this.track_focus(&focus_handle)
239 .focus(|s| s.border_color(theme_colors.border_focused))
240 })
241 .child(editor)
242 .child(
243 h_flex()
244 .absolute()
245 .top_1()
246 .right_1()
247 .invisible()
248 .when(is_editor_focused, |this| this.visible())
249 .group_hover("settings-input-field-editor", |this| this.visible())
250 .when(
251 display_clear_button && !is_editor_empty && is_editor_focused,
252 |this| {
253 this.child(
254 IconButton::new("clear-button", IconName::Close)
255 .icon_size(IconSize::Small)
256 .icon_color(Color::Muted)
257 .tooltip(Tooltip::text("Clear"))
258 .on_click(move |_, window, cx| {
259 let Some(editor) = weak_editor_for_clear.upgrade() else {
260 return;
261 };
262 editor.update(cx, |editor, cx| {
263 editor.set_text("", window, cx);
264 });
265 }),
266 )
267 },
268 )
269 .when(
270 display_confirm_button && !is_editor_empty && is_editor_focused,
271 |this| {
272 this.child(
273 IconButton::new("confirm-button", IconName::Check)
274 .icon_size(IconSize::Small)
275 .icon_color(Color::Success)
276 .tooltip(Tooltip::text("Enter to Confirm"))
277 .on_click(move |_, window, cx| {
278 let Some(confirm) = confirm_for_button.as_ref() else {
279 return;
280 };
281 let Some(editor) = weak_editor_for_button.upgrade() else {
282 return;
283 };
284 let new_value =
285 editor.read_with(cx, |editor, cx| editor.text(cx));
286 let new_value =
287 (!new_value.is_empty()).then_some(new_value);
288 confirm(new_value, window, cx);
289 if clear_on_confirm_for_button {
290 editor.update(cx, |editor, cx| {
291 editor.set_text("", window, cx);
292 });
293 }
294 }),
295 )
296 },
297 )
298 .when_some(self.action_slot, |this, action| this.child(action)),
299 )
300 .when_some(self.confirm, |this, confirm| {
301 this.on_action::<menu::Confirm>({
302 move |_, window, cx| {
303 let Some(editor) = weak_editor.upgrade() else {
304 return;
305 };
306 let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
307 let new_value = (!new_value.is_empty()).then_some(new_value);
308 confirm(new_value, window, cx);
309 if clear_on_confirm {
310 editor.update(cx, |editor, cx| {
311 editor.set_text("", window, cx);
312 });
313 }
314 }
315 })
316 })
317 }
318}