Skip to repository content273 lines · 9.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:32:42.406Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
input_field.rs
1use component::{example_group, single_example};
2
3use gpui::{App, FocusHandle, Focusable, Hsla, Length};
4use std::sync::Arc;
5
6use ui::Tooltip;
7use ui::prelude::*;
8
9use crate::ErasedEditor;
10
11pub struct InputFieldStyle {
12 text_color: Hsla,
13 background_color: Hsla,
14 border_color: Hsla,
15}
16
17/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
18///
19/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
20#[derive(RegisterComponent)]
21pub struct InputField {
22 /// An optional label for the text field.
23 ///
24 /// Its position is determined by the [`FieldLabelLayout`].
25 label: Option<SharedString>,
26 /// The size of the label text.
27 label_size: LabelSize,
28 /// The placeholder text for the text field.
29 placeholder: SharedString,
30
31 editor: Arc<dyn ErasedEditor>,
32 /// An optional icon that is displayed at the start of the text field.
33 ///
34 /// For example, a magnifying glass icon in a search field.
35 start_icon: Option<IconName>,
36 /// The minimum width of for the input
37 min_width: Length,
38 /// The tab index for keyboard navigation order.
39 tab_index: Option<isize>,
40 /// Whether this field is a tab stop (can be focused via Tab key).
41 tab_stop: bool,
42 /// Whether the field content is masked (for sensitive fields like passwords or API keys).
43 masked: Option<bool>,
44 /// An optional validation error. When set, the field's border turns red
45 /// and the message is shown as hint subtext below the field.
46 error: Option<SharedString>,
47}
48
49impl Focusable for InputField {
50 fn focus_handle(&self, cx: &App) -> FocusHandle {
51 self.editor.focus_handle(cx)
52 }
53}
54
55impl InputField {
56 pub fn new(window: &mut Window, cx: &mut App, placeholder_text: &str) -> Self {
57 let editor_factory = crate::ERASED_EDITOR_FACTORY
58 .get()
59 .expect("ErasedEditorFactory to be initialized");
60 let editor = (editor_factory)(window, cx);
61 editor.set_placeholder_text(placeholder_text, window, cx);
62
63 Self {
64 label: None,
65 label_size: LabelSize::Small,
66 placeholder: SharedString::new(placeholder_text),
67 editor,
68 start_icon: None,
69 min_width: px(192.).into(),
70 tab_index: None,
71 tab_stop: true,
72 masked: None,
73 error: None,
74 }
75 }
76
77 pub fn start_icon(mut self, icon: IconName) -> Self {
78 self.start_icon = Some(icon);
79 self
80 }
81
82 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
83 self.label = Some(label.into());
84 self
85 }
86
87 pub fn label_size(mut self, size: LabelSize) -> Self {
88 self.label_size = size;
89 self
90 }
91
92 pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
93 self.min_width = width.into();
94 self
95 }
96
97 pub fn tab_index(mut self, index: isize) -> Self {
98 self.tab_index = Some(index);
99 self
100 }
101
102 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
103 self.tab_stop = tab_stop;
104 self
105 }
106
107 /// Sets this field as a masked/sensitive input (e.g., for passwords or API keys).
108 pub fn masked(mut self, masked: bool) -> Self {
109 self.masked = Some(masked);
110 self
111 }
112
113 /// Sets a validation error message, turning the field's border red and
114 /// showing the message as hint subtext below the field. Pass `None` to
115 /// clear the error.
116 pub fn set_error(&mut self, error: Option<impl Into<SharedString>>, cx: &mut Context<Self>) {
117 self.error = error.map(Into::into);
118 cx.notify();
119 }
120
121 pub fn is_empty(&self, cx: &App) -> bool {
122 self.editor().text(cx).trim().is_empty()
123 }
124
125 pub fn editor(&self) -> &Arc<dyn ErasedEditor> {
126 &self.editor
127 }
128
129 pub fn text(&self, cx: &App) -> String {
130 self.editor().text(cx)
131 }
132
133 pub fn clear(&self, window: &mut Window, cx: &mut App) {
134 self.editor().clear(window, cx)
135 }
136
137 pub fn set_text(&self, text: &str, window: &mut Window, cx: &mut App) {
138 self.editor().set_text(text, window, cx)
139 }
140
141 pub fn set_masked(&self, masked: bool, window: &mut Window, cx: &mut App) {
142 self.editor().set_masked(masked, window, cx)
143 }
144}
145
146impl Render for InputField {
147 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
148 let editor = self.editor.clone();
149
150 if let Some(masked) = self.masked {
151 self.editor.set_masked(masked, window, cx);
152 }
153
154 let theme_color = cx.theme().colors();
155
156 let style = InputFieldStyle {
157 text_color: theme_color.text,
158 background_color: theme_color.editor_background,
159 border_color: theme_color.border_variant,
160 };
161
162 let focus_handle = self.editor.focus_handle(cx);
163
164 let has_error = self.error.is_some();
165 let error_border = cx.theme().status().error_border;
166
167 let configured_handle = if let Some(tab_index) = self.tab_index {
168 focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
169 } else if !self.tab_stop {
170 focus_handle.tab_stop(false)
171 } else {
172 focus_handle
173 };
174
175 v_flex()
176 .id(self.placeholder.clone())
177 .w_full()
178 .gap_1()
179 .when_some(self.label.clone(), |this, label| {
180 this.child(Label::new(label).size(self.label_size))
181 })
182 .child(
183 h_flex()
184 .track_focus(&configured_handle)
185 .min_w(self.min_width)
186 .min_h_8()
187 .w_full()
188 .px_2()
189 .py_1p5()
190 .flex_grow_1()
191 .text_color(style.text_color)
192 .rounded_md()
193 .bg(style.background_color)
194 .border_1()
195 .border_color(style.border_color)
196 .when(
197 editor.focus_handle(cx).contains_focused(window, cx),
198 |this| this.border_color(theme_color.border_focused),
199 )
200 .when(has_error, |this| this.border_color(error_border))
201 .when_some(self.start_icon, |this, icon| {
202 this.gap_1()
203 .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
204 })
205 .child(self.editor.render(window, cx))
206 .when_some(self.masked, |this, is_masked| {
207 this.child(
208 IconButton::new(
209 "toggle-masked",
210 if is_masked {
211 IconName::Eye
212 } else {
213 IconName::EyeOff
214 },
215 )
216 .icon_size(IconSize::Small)
217 .icon_color(Color::Muted)
218 .tooltip(Tooltip::text(if is_masked { "Show" } else { "Hide" }))
219 .on_click(cx.listener(
220 |this, _, window, cx| {
221 if let Some(ref mut masked) = this.masked {
222 *masked = !*masked;
223 this.editor.set_masked(*masked, window, cx);
224 cx.notify();
225 }
226 },
227 )),
228 )
229 }),
230 )
231 .when_some(self.error.clone(), |this, error| {
232 this.child(Label::new(error).size(LabelSize::Small).color(Color::Error))
233 })
234 }
235}
236
237impl Component for InputField {
238 fn scope() -> ComponentScope {
239 ComponentScope::Input
240 }
241
242 fn description() -> &'static str {
243 "A single-line text field used for search inputs, \
244 form fields, and similar inputs, supporting labels, placeholders, \
245 leading icons, and masked content."
246 }
247
248 fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
249 let input_small =
250 cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
251
252 let input_regular = cx.new(|cx| {
253 InputField::new(window, cx, "placeholder")
254 .label("Regular Label")
255 .label_size(LabelSize::Default)
256 });
257
258 v_flex()
259 .gap_6()
260 .children(vec![example_group(vec![
261 single_example(
262 "Small Label (Default)",
263 div().child(input_small).into_any_element(),
264 ),
265 single_example(
266 "Regular Label",
267 div().child(input_regular).into_any_element(),
268 ),
269 ])])
270 .into_any_element()
271 }
272}
273