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