Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:42.406Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

input_field.rs

273 lines · 9.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.