1use gpui::{
2 AnyElement, App, Background, ElementId, InteractiveElement as _, IntoElement, ParentElement,
3 RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder, relative,
4};
5use smallvec::SmallVec;
6
7use crate::{ActiveTheme, StyledExt as _, v_flex};
8
9#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
11pub enum GroupBoxVariant {
12 #[default]
13 Normal,
14 Fill,
15 Outline,
16}
17
18pub trait GroupBoxVariants: Sized {
20 fn with_variant(self, variant: GroupBoxVariant) -> Self;
22 fn normal(mut self) -> Self {
24 self = self.with_variant(GroupBoxVariant::Normal);
25 self
26 }
27 fn fill(mut self) -> Self {
29 self = self.with_variant(GroupBoxVariant::Fill);
30 self
31 }
32 fn outline(mut self) -> Self {
34 self = self.with_variant(GroupBoxVariant::Outline);
35 self
36 }
37}
38
39impl GroupBoxVariant {
40 pub fn from_str(s: &str) -> Self {
42 match s.to_lowercase().as_str() {
43 "fill" => GroupBoxVariant::Fill,
44 "outline" => GroupBoxVariant::Outline,
45 _ => GroupBoxVariant::Normal,
46 }
47 }
48
49 pub fn as_str(&self) -> &str {
51 match self {
52 GroupBoxVariant::Normal => "normal",
53 GroupBoxVariant::Fill => "fill",
54 GroupBoxVariant::Outline => "outline",
55 }
56 }
57}
58
59#[derive(IntoElement)]
62pub struct GroupBox {
63 id: Option<ElementId>,
64 variant: GroupBoxVariant,
65 style: StyleRefinement,
66 title_style: StyleRefinement,
67 title: Option<AnyElement>,
68 content_style: StyleRefinement,
69 children: SmallVec<[AnyElement; 1]>,
70 footer: Option<AnyElement>,
71}
72
73impl GroupBox {
74 pub fn new() -> Self {
76 Self {
77 id: None,
78 variant: GroupBoxVariant::default(),
79 style: StyleRefinement::default(),
80 title_style: StyleRefinement::default(),
81 content_style: StyleRefinement::default(),
82 title: None,
83 children: SmallVec::new(),
84 footer: None,
85 }
86 }
87
88 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
90 self.id = Some(id.into());
91 self
92 }
93
94 pub fn title(mut self, title: impl IntoElement) -> Self {
96 self.title = Some(title.into_any_element());
97 self
98 }
99
100 pub fn title_style(mut self, style: StyleRefinement) -> Self {
102 self.title_style = style;
103 self
104 }
105
106 pub fn content_style(mut self, style: StyleRefinement) -> Self {
108 self.content_style = style;
109 self
110 }
111
112 pub fn footer(mut self, footer: impl IntoElement) -> Self {
117 self.footer = Some(footer.into_any_element());
118 self
119 }
120}
121
122impl ParentElement for GroupBox {
123 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
124 self.children.extend(elements);
125 }
126}
127
128impl Styled for GroupBox {
129 fn style(&mut self) -> &mut StyleRefinement {
130 &mut self.style
131 }
132}
133
134impl GroupBoxVariants for GroupBox {
135 fn with_variant(mut self, variant: GroupBoxVariant) -> Self {
136 self.variant = variant;
137 self
138 }
139}
140
141impl RenderOnce for GroupBox {
142 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
143 let (bg, border, has_paddings): (Option<Background>, _, _) = match self.variant {
144 GroupBoxVariant::Normal => (None, None, false),
145 GroupBoxVariant::Fill => (Some(cx.theme().tokens.group_box.into()), None, true),
146 GroupBoxVariant::Outline => (None, Some(cx.theme().border), true),
147 };
148
149 v_flex()
150 .id(self.id.unwrap_or("group-box".into()))
151 .w_full()
152 .when(has_paddings, |this| this.gap_3())
153 .when(!has_paddings, |this| this.gap_4())
154 .refine_style(&self.style)
155 .when_some(self.title, |this, title| {
156 this.child(
157 div()
158 .text_color(cx.theme().muted_foreground)
159 .line_height(relative(1.25))
160 .refine_style(&self.title_style)
161 .child(title),
162 )
163 })
164 .child(
165 v_flex()
168 .gap_2()
169 .child(
170 v_flex()
171 .when_some(bg, |this, bg| this.bg(bg))
172 .when_some(border, |this, border| this.border_color(border).border_1())
173 .text_color(cx.theme().group_box_foreground)
174 .when(has_paddings, |this| this.p_4())
175 .gap_4()
176 .rounded(cx.theme().radius)
177 .refine_style(&self.content_style)
178 .children(self.children),
179 )
180 .when_some(self.footer, |this, footer| {
181 this.child(
182 div()
183 .text_sm()
184 .text_color(cx.theme().muted_foreground)
185 .child(footer),
186 )
187 }),
188 )
189 }
190}
191
192#[cfg(test)]
193mod test {
194 #[test]
195 fn test_group_variant_from_str() {
196 use super::GroupBoxVariant;
197
198 assert_eq!(GroupBoxVariant::from_str("normal"), GroupBoxVariant::Normal);
199 assert_eq!(GroupBoxVariant::from_str("fill"), GroupBoxVariant::Fill);
200 assert_eq!(
201 GroupBoxVariant::from_str("outline"),
202 GroupBoxVariant::Outline
203 );
204 assert_eq!(GroupBoxVariant::from_str("other"), GroupBoxVariant::Normal);
205
206 assert_eq!(GroupBoxVariant::from_str("FILL"), GroupBoxVariant::Fill);
207 assert_eq!(
208 GroupBoxVariant::from_str("OutLine"),
209 GroupBoxVariant::Outline
210 );
211
212 assert_eq!(GroupBoxVariant::Normal.as_str(), "normal");
213 assert_eq!(GroupBoxVariant::Fill.as_str(), "fill");
214 assert_eq!(GroupBoxVariant::Outline.as_str(), "outline");
215 }
216}