input_field.rs

  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}