Skip to main content

gpui_component/setting/
group.rs

1use std::rc::Rc;
2
3use gpui::{
4    AnyElement, App, IntoElement, ParentElement as _, SharedString, StyleRefinement, Styled,
5    Window, prelude::FluentBuilder as _,
6};
7
8use crate::{
9    ActiveTheme, StyledExt,
10    group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants},
11    label::Label,
12    setting::{RenderOptions, SettingItem},
13    v_flex,
14};
15
16/// A setting group that can contain multiple setting items.
17#[derive(Clone)]
18pub struct SettingGroup {
19    style: StyleRefinement,
20    footer: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
21    variant: Option<GroupBoxVariant>,
22
23    pub(super) title: Option<SharedString>,
24    pub(super) description: Option<SharedString>,
25    pub(super) items: Vec<SettingItem>,
26}
27
28impl Styled for SettingGroup {
29    fn style(&mut self) -> &mut StyleRefinement {
30        &mut self.style
31    }
32}
33
34impl SettingGroup {
35    /// Create a new setting group.
36    pub fn new() -> Self {
37        Self {
38            style: StyleRefinement::default(),
39            footer: None,
40            variant: None,
41            title: None,
42            description: None,
43            items: Vec::new(),
44        }
45    }
46
47    /// Set the label of the setting group, default is None.
48    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
49        self.title = Some(title.into());
50        self
51    }
52
53    /// Set the description of the setting group, default is None.
54    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
55        self.description = Some(description.into());
56        self
57    }
58
59    /// Set the variant of the group surface, overriding the variant set via
60    /// `Settings::with_group_variant` for this group, default is None (use the
61    /// settings-level variant).
62    ///
63    /// For example, `GroupBoxVariant::Normal` presents the items directly,
64    /// without the card surface a global `Outline` or `Fill` default draws
65    /// around the group.
66    pub fn variant(mut self, variant: GroupBoxVariant) -> Self {
67        self.variant = Some(variant);
68        self
69    }
70
71    /// Render supporting content below, and outside, the group's surface.
72    ///
73    /// The footer aligns with the group title and renders as small muted text,
74    /// like a description. It scrolls with the group and follows its search
75    /// visibility; it does not add an independently searchable item or a
76    /// sidebar entry, and a group needs at least one item to be shown.
77    pub fn footer<F, E>(mut self, footer: F) -> Self
78    where
79        E: IntoElement,
80        F: Fn(&mut Window, &mut App) -> E + 'static,
81    {
82        self.footer = Some(Rc::new(move |window, cx| {
83            footer(window, cx).into_any_element()
84        }));
85        self
86    }
87
88    /// Add a setting item to the group.
89    pub fn item(mut self, item: SettingItem) -> Self {
90        self.items.push(item);
91        self
92    }
93
94    /// Add multiple setting items to the group.
95    pub fn items<I>(mut self, items: I) -> Self
96    where
97        I: IntoIterator<Item = SettingItem>,
98    {
99        self.items.extend(items);
100        self
101    }
102
103    /// Return true if any of the setting items in the group match the given query.
104    pub(super) fn is_match(&self, query: &str, cx: &App) -> bool {
105        self.items.iter().any(|item| item.is_match(query, cx))
106    }
107
108    pub(super) fn is_resettable(&self, query: &str, cx: &App) -> bool {
109        self.items
110            .iter()
111            .any(|item| item.is_match(query, cx) && item.is_resettable(cx))
112    }
113
114    pub(crate) fn render(
115        self,
116        query: &str,
117        options: &RenderOptions,
118        window: &mut Window,
119        cx: &mut App,
120    ) -> impl IntoElement {
121        GroupBox::new()
122            .id(SharedString::from(format!("group-{}", options.group_ix())))
123            .with_variant(self.variant.unwrap_or(options.group_variant()))
124            .when_some(self.title.clone(), |this, title| {
125                this.title(v_flex().gap_1().child(title).when_some(
126                    self.description.clone(),
127                    |this, description| {
128                        this.child(
129                            Label::new(description)
130                                .text_sm()
131                                .text_color(cx.theme().muted_foreground),
132                        )
133                    },
134                ))
135            })
136            .gap_4()
137            .children(self.items.iter().enumerate().filter_map(|(item_ix, item)| {
138                if item.is_match(&query, cx) {
139                    Some(
140                        item.clone()
141                            .render_item(&options.with_item_ix(item_ix), window, cx),
142                    )
143                } else {
144                    None
145                }
146            }))
147            .when_some(self.footer, |this, footer| this.footer(footer(window, cx)))
148            .refine_style(&self.style)
149    }
150
151    pub(crate) fn reset(&self, query: &str, window: &mut Window, cx: &mut App) {
152        for item in &self.items {
153            if item.is_match(query, cx) {
154                item.reset(window, cx);
155            }
156        }
157    }
158}