1use std::rc::Rc;
2
3use editor::Editor;
4use gpui::{AnyElement, ElementId, Focusable, TextStyleRefinement};
5use settings::Settings as _;
6use theme::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 action_slot: Option<AnyElement>,
20 color: Option<Color>,
21}
22
23impl SettingsInputField {
24 pub fn new() -> Self {
25 Self {
26 id: None,
27 initial_text: None,
28 placeholder: None,
29 confirm: None,
30 tab_index: None,
31 use_buffer_font: false,
32 display_confirm_button: false,
33 display_clear_button: false,
34 action_slot: None,
35 color: None,
36 }
37 }
38
39 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
40 self.id = Some(id.into());
41 self
42 }
43
44 pub fn with_initial_text(mut self, initial_text: String) -> Self {
45 self.initial_text = Some(initial_text);
46 self
47 }
48
49 pub fn with_placeholder(mut self, placeholder: &'static str) -> Self {
50 self.placeholder = Some(placeholder);
51 self
52 }
53
54 pub fn on_confirm(
55 mut self,
56 confirm: impl Fn(Option<String>, &mut Window, &mut App) + 'static,
57 ) -> Self {
58 self.confirm = Some(Rc::new(confirm));
59 self
60 }
61
62 pub fn display_confirm_button(mut self) -> Self {
63 self.display_confirm_button = true;
64 self
65 }
66
67 pub fn display_clear_button(mut self) -> Self {
68 self.display_clear_button = true;
69 self
70 }
71
72 pub fn action_slot(mut self, action: impl IntoElement) -> Self {
73 self.action_slot = Some(action.into_any_element());
74 self
75 }
76
77 pub(crate) fn tab_index(mut self, arg: isize) -> Self {
78 self.tab_index = Some(arg);
79 self
80 }
81
82 pub fn with_buffer_font(mut self) -> Self {
83 self.use_buffer_font = true;
84 self
85 }
86
87 pub fn color(mut self, color: Color) -> Self {
88 self.color = Some(color);
89 self
90 }
91}
92
93impl RenderOnce for SettingsInputField {
94 fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
95 let settings = ThemeSettings::get_global(cx);
96 let use_buffer_font = self.use_buffer_font;
97 let color = self.color.map(|c| c.color(cx));
98 let styles = TextStyleRefinement {
99 font_family: use_buffer_font.then(|| settings.buffer_font.family.clone()),
100 font_size: use_buffer_font.then(|| rems(0.75).into()),
101 color,
102 ..Default::default()
103 };
104
105 let editor = if let Some(id) = self.id {
106 window.use_keyed_state(id, cx, {
107 let initial_text = self.initial_text.clone();
108 let placeholder = self.placeholder;
109 move |window, cx| {
110 let mut editor = Editor::single_line(window, cx);
111 if let Some(text) = initial_text {
112 editor.set_text(text, window, cx);
113 }
114
115 if let Some(placeholder) = placeholder {
116 editor.set_placeholder_text(placeholder, window, cx);
117 }
118 editor.set_text_style_refinement(styles);
119 editor
120 }
121 })
122 } else {
123 window.use_state(cx, {
124 let initial_text = self.initial_text.clone();
125 let placeholder = self.placeholder;
126 move |window, cx| {
127 let mut editor = Editor::single_line(window, cx);
128 if let Some(text) = initial_text {
129 editor.set_text(text, window, cx);
130 }
131
132 if let Some(placeholder) = placeholder {
133 editor.set_placeholder_text(placeholder, window, cx);
134 }
135 editor.set_text_style_refinement(styles);
136 editor
137 }
138 })
139 };
140
141 let weak_editor = editor.downgrade();
142 let weak_editor_for_button = editor.downgrade();
143 let weak_editor_for_clear = editor.downgrade();
144
145 let theme_colors = cx.theme().colors();
146
147 let display_confirm_button = self.display_confirm_button;
148 let display_clear_button = self.display_clear_button;
149 let confirm_for_button = self.confirm.clone();
150 let is_editor_empty = editor.read(cx).text(cx).trim().is_empty();
151 let is_editor_focused = editor.read(cx).is_focused(window);
152
153 h_flex()
154 .group("settings-input-field-editor")
155 .relative()
156 .py_1()
157 .px_2()
158 .h_8()
159 .min_w_64()
160 .rounded_md()
161 .border_1()
162 .border_color(theme_colors.border)
163 .bg(theme_colors.editor_background)
164 .when_some(self.tab_index, |this, tab_index| {
165 let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true);
166 this.track_focus(&focus_handle)
167 .focus(|s| s.border_color(theme_colors.border_focused))
168 })
169 .child(editor)
170 .child(
171 h_flex()
172 .absolute()
173 .top_1()
174 .right_1()
175 .invisible()
176 .when(is_editor_focused, |this| this.visible())
177 .group_hover("settings-input-field-editor", |this| this.visible())
178 .when(
179 display_clear_button && !is_editor_empty && is_editor_focused,
180 |this| {
181 this.child(
182 IconButton::new("clear-button", IconName::Close)
183 .icon_size(IconSize::Small)
184 .icon_color(Color::Muted)
185 .tooltip(Tooltip::text("Clear"))
186 .on_click(move |_, window, cx| {
187 let Some(editor) = weak_editor_for_clear.upgrade() else {
188 return;
189 };
190 editor.update(cx, |editor, cx| {
191 editor.set_text("", window, cx);
192 });
193 }),
194 )
195 },
196 )
197 .when(
198 display_confirm_button && !is_editor_empty && is_editor_focused,
199 |this| {
200 this.child(
201 IconButton::new("confirm-button", IconName::Check)
202 .icon_size(IconSize::Small)
203 .icon_color(Color::Success)
204 .tooltip(Tooltip::text("Enter to Confirm"))
205 .on_click(move |_, window, cx| {
206 let Some(confirm) = confirm_for_button.as_ref() else {
207 return;
208 };
209 let Some(editor) = weak_editor_for_button.upgrade() else {
210 return;
211 };
212 let new_value =
213 editor.read_with(cx, |editor, cx| editor.text(cx));
214 let new_value =
215 (!new_value.is_empty()).then_some(new_value);
216 confirm(new_value, window, cx);
217 }),
218 )
219 },
220 )
221 .when_some(self.action_slot, |this, action| this.child(action)),
222 )
223 .when_some(self.confirm, |this, confirm| {
224 this.on_action::<menu::Confirm>({
225 move |_, window, cx| {
226 let Some(editor) = weak_editor.upgrade() else {
227 return;
228 };
229 let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
230 let new_value = (!new_value.is_empty()).then_some(new_value);
231 confirm(new_value, window, cx);
232 }
233 })
234 })
235 }
236}