Skip to main content

gpui_component/
group_box.rs

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/// The variant of the GroupBox.
10#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
11pub enum GroupBoxVariant {
12    #[default]
13    Normal,
14    Fill,
15    Outline,
16}
17
18/// Trait to add GroupBox variant methods to elements.
19pub trait GroupBoxVariants: Sized {
20    /// Set the variant of the [`GroupBox`].
21    fn with_variant(self, variant: GroupBoxVariant) -> Self;
22    /// Set to use [`GroupBoxVariant::Normal`] to GroupBox.
23    fn normal(mut self) -> Self {
24        self = self.with_variant(GroupBoxVariant::Normal);
25        self
26    }
27    /// Set to use [`GroupBoxVariant::Fill`] to GroupBox.
28    fn fill(mut self) -> Self {
29        self = self.with_variant(GroupBoxVariant::Fill);
30        self
31    }
32    /// Set to use [`GroupBoxVariant::Outline`] to GroupBox.
33    fn outline(mut self) -> Self {
34        self = self.with_variant(GroupBoxVariant::Outline);
35        self
36    }
37}
38
39impl GroupBoxVariant {
40    /// Create a GroupBoxVariant from a string.
41    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    /// Convert the GroupBoxVariant to a string.
50    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/// GroupBox is a styled container element that with
60/// an optional title to groups related content together.
61#[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    /// Create a new GroupBox.
75    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    /// Set the id of the group box, default is None.
89    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
90        self.id = Some(id.into());
91        self
92    }
93
94    /// Set the title of the group box, default is None.
95    pub fn title(mut self, title: impl IntoElement) -> Self {
96        self.title = Some(title.into_any_element());
97        self
98    }
99
100    /// Set the style of the title of the group box to override the default style, default is None.
101    pub fn title_style(mut self, style: StyleRefinement) -> Self {
102        self.title_style = style;
103        self
104    }
105
106    /// Set the style of the content of the group box to override the default style, default is None.
107    pub fn content_style(mut self, style: StyleRefinement) -> Self {
108        self.content_style = style;
109        self
110    }
111
112    /// Set supporting content below the group's filled or outlined surface.
113    ///
114    /// The footer shares the title's leading edge, sits 8 px under the
115    /// surface, and renders as small muted text like a description.
116    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                // The footer sits inside the surface's slot so its 8 px gap is
166                // independent of the root gap between the title and surface.
167                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}