Skip to main content

ui/components/
input_group.rs

1use crate::prelude::*;
2use gpui::{AnyElement, IntoElement, ParentElement, Styled};
3
4/// Wraps an input-like child (typically an `Entity<TextInput>`) with an
5/// optional leading and/or trailing slot (icon or button), presenting a
6/// single unified border/background around the whole group.
7///
8/// Note: `TextInput` (out of scope for this phase) draws its own
9/// border/background/focus-ring internally, so the wrapped input still shows
10/// its own inner border — this wrapper adds the *outer* shared border and
11/// slot dividers around it. A fully seamless single-border look would need a
12/// `borderless()` variant on `TextInput` (follow-up, not this phase).
13#[derive(IntoElement, RegisterComponent)]
14pub struct InputGroup {
15    content: AnyElement,
16    leading: Option<AnyElement>,
17    trailing: Option<AnyElement>,
18    invalid: bool,
19}
20
21impl InputGroup {
22    pub fn new(content: impl IntoElement) -> Self {
23        Self {
24            content: content.into_any_element(),
25            leading: None,
26            trailing: None,
27            invalid: false,
28        }
29    }
30
31    /// Sets the leading slot (rendered before the input, e.g. an icon).
32    pub fn leading(mut self, element: impl IntoElement) -> Self {
33        self.leading = Some(element.into_any_element());
34        self
35    }
36
37    /// Sets the trailing slot (rendered after the input, e.g. a button).
38    pub fn trailing(mut self, element: impl IntoElement) -> Self {
39        self.trailing = Some(element.into_any_element());
40        self
41    }
42
43    /// Marks the group as invalid, switching the outer border to the danger color.
44    pub fn invalid(mut self, invalid: bool) -> Self {
45        self.invalid = invalid;
46        self
47    }
48}
49
50impl RenderOnce for InputGroup {
51    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
52        let outer_border = if self.invalid {
53            palette::danger(500)
54        } else {
55            semantic::border(cx)
56        };
57        let divider = semantic::border_muted(cx);
58
59        h_flex()
60            .w_full()
61            .items_stretch()
62            .rounded_md()
63            .border_1()
64            .border_color(outer_border)
65            .bg(semantic::surface(cx))
66            .overflow_hidden()
67            .when_some(self.leading, |this, element| {
68                this.child(
69                    h_flex()
70                        .flex_none()
71                        .items_center()
72                        .px_3()
73                        .border_r_1()
74                        .border_color(divider)
75                        .child(element),
76                )
77            })
78            .child(div().flex_1().min_w_0().child(self.content))
79            .when_some(self.trailing, |this, element| {
80                this.child(
81                    h_flex()
82                        .flex_none()
83                        .items_center()
84                        .px_3()
85                        .border_l_1()
86                        .border_color(divider)
87                        .child(element),
88                )
89            })
90    }
91}
92
93impl Component for InputGroup {
94    fn scope() -> ComponentScope {
95        ComponentScope::Input
96    }
97
98    fn description() -> Option<&'static str> {
99        Some("Wraps an input with a leading/trailing slot under one unified border.")
100    }
101
102    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
103        Some(
104            v_flex()
105                .gap_4()
106                .child(
107                    InputGroup::new(Label::new("you@example.com").color(Color::Placeholder))
108                        .leading(
109                            Icon::new(IconName::AtSign)
110                                .size(IconSize::Small)
111                                .color(Color::Muted),
112                        )
113                        .into_any_element(),
114                )
115                .child(
116                    InputGroup::new(Label::new("https://example.com").color(Color::Placeholder))
117                        .trailing(Button::new("input-group-copy", "Copy").into_any_element())
118                        .into_any_element(),
119                )
120                .child(
121                    InputGroup::new(Label::new("Invalid value").color(Color::Placeholder))
122                        .invalid(true)
123                        .into_any_element(),
124                )
125                .into_any_element(),
126        )
127    }
128}