gpui_component/sidebar/
menu.rs

1use crate::{h_flex, v_flex, ActiveTheme as _, Collapsible, Icon, IconName, StyledExt};
2use gpui::{
3    div, percentage, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, ElementId,
4    InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
5    StatefulInteractiveElement as _, Styled as _, Window,
6};
7use std::rc::Rc;
8
9/// Menu for the [`super::Sidebar`]
10#[derive(IntoElement)]
11pub struct SidebarMenu {
12    collapsed: bool,
13    items: Vec<SidebarMenuItem>,
14}
15
16impl SidebarMenu {
17    /// Create a new SidebarMenu
18    pub fn new() -> Self {
19        Self {
20            items: Vec::new(),
21            collapsed: false,
22        }
23    }
24
25    /// Add a [`SidebarMenuItem`] child menu item to the sidebar menu.
26    ///
27    /// See also [`SidebarMenu::children`].
28    pub fn child(mut self, child: impl Into<SidebarMenuItem>) -> Self {
29        self.items.push(child.into());
30        self
31    }
32
33    /// Add multiple [`SidebarMenuItem`] child menu items to the sidebar menu.
34    pub fn children(
35        mut self,
36        children: impl IntoIterator<Item = impl Into<SidebarMenuItem>>,
37    ) -> Self {
38        self.items = children.into_iter().map(Into::into).collect();
39        self
40    }
41}
42
43impl Collapsible for SidebarMenu {
44    fn is_collapsed(&self) -> bool {
45        self.collapsed
46    }
47
48    fn collapsed(mut self, collapsed: bool) -> Self {
49        self.collapsed = collapsed;
50        self
51    }
52}
53
54impl RenderOnce for SidebarMenu {
55    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
56        v_flex().gap_2().children(
57            self.items
58                .into_iter()
59                .enumerate()
60                .map(|(ix, item)| item.id(ix).collapsed(self.collapsed)),
61        )
62    }
63}
64
65/// Menu item for the [`SidebarMenu`]
66#[derive(IntoElement)]
67pub struct SidebarMenuItem {
68    id: ElementId,
69    icon: Option<Icon>,
70    label: SharedString,
71    handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
72    active: bool,
73    collapsed: bool,
74    children: Vec<Self>,
75    suffix: Option<AnyElement>,
76}
77
78impl SidebarMenuItem {
79    /// Create a new [`SidebarMenuItem`] with a label.
80    pub fn new(label: impl Into<SharedString>) -> Self {
81        Self {
82            id: ElementId::Integer(0),
83            icon: None,
84            label: label.into(),
85            handler: Rc::new(|_, _, _| {}),
86            active: false,
87            collapsed: false,
88            children: Vec::new(),
89            suffix: None,
90        }
91    }
92
93    /// Set the icon for the menu item
94    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
95        self.icon = Some(icon.into());
96        self
97    }
98
99    /// Set the active state of the menu item
100    pub fn active(mut self, active: bool) -> Self {
101        self.active = active;
102        self
103    }
104
105    /// Add a click handler to the menu item
106    pub fn on_click(
107        mut self,
108        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
109    ) -> Self {
110        self.handler = Rc::new(handler);
111        self
112    }
113
114    /// Set the collapsed state of the menu item
115    pub fn collapsed(mut self, collapsed: bool) -> Self {
116        self.collapsed = collapsed;
117        self
118    }
119
120    pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Self>>) -> Self {
121        self.children = children.into_iter().map(Into::into).collect();
122        self
123    }
124
125    /// Set the suffix for the menu item.
126    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
127        self.suffix = Some(suffix.into_any_element());
128        self
129    }
130
131    /// Set id to the menu item.
132    fn id(mut self, id: impl Into<ElementId>) -> Self {
133        self.id = id.into();
134        self
135    }
136
137    fn is_submenu(&self) -> bool {
138        self.children.len() > 0
139    }
140
141    fn is_open(&self) -> bool {
142        if self.is_submenu() {
143            self.active
144        } else {
145            false
146        }
147    }
148}
149
150impl RenderOnce for SidebarMenuItem {
151    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
152        let handler = self.handler.clone();
153        let is_collapsed = self.collapsed;
154        let is_active = self.active;
155        let is_open = self.is_open();
156        let is_submenu = self.is_submenu();
157
158        div()
159            .id(self.id.clone())
160            .w_full()
161            .child(
162                h_flex()
163                    .size_full()
164                    .id("item")
165                    .overflow_x_hidden()
166                    .flex_shrink_0()
167                    .p_2()
168                    .gap_x_2()
169                    .rounded(cx.theme().radius)
170                    .text_sm()
171                    .hover(|this| {
172                        if is_active {
173                            return this;
174                        }
175
176                        this.bg(cx.theme().sidebar_accent.opacity(0.8))
177                            .text_color(cx.theme().sidebar_accent_foreground)
178                    })
179                    .when(is_active && !is_submenu, |this| {
180                        this.font_medium()
181                            .bg(cx.theme().sidebar_accent)
182                            .text_color(cx.theme().sidebar_accent_foreground)
183                    })
184                    .when_some(self.icon.clone(), |this, icon| this.child(icon))
185                    .when(is_collapsed, |this| {
186                        this.justify_center().when(is_active, |this| {
187                            this.bg(cx.theme().sidebar_accent)
188                                .text_color(cx.theme().sidebar_accent_foreground)
189                        })
190                    })
191                    .when(!is_collapsed, |this| {
192                        this.h_7()
193                            .child(
194                                h_flex()
195                                    .flex_1()
196                                    .gap_x_2()
197                                    .justify_between()
198                                    .overflow_x_hidden()
199                                    .child(
200                                        h_flex()
201                                            .flex_1()
202                                            .overflow_x_hidden()
203                                            .child(self.label.clone()),
204                                    )
205                                    .when_some(self.suffix, |this, suffix| this.child(suffix)),
206                            )
207                            .when(is_submenu, |this| {
208                                this.child(
209                                    Icon::new(IconName::ChevronRight)
210                                        .size_4()
211                                        .when(is_open, |this| this.rotate(percentage(90. / 360.))),
212                                )
213                            })
214                    })
215                    .on_click(move |ev, window, cx| handler(ev, window, cx)),
216            )
217            .when(is_submenu && is_open && !is_collapsed, |this| {
218                this.child(
219                    v_flex()
220                        .id("submenu")
221                        .border_l_1()
222                        .border_color(cx.theme().sidebar_border)
223                        .gap_1()
224                        .ml_3p5()
225                        .pl_2p5()
226                        .py_0p5()
227                        .children(
228                            self.children
229                                .into_iter()
230                                .enumerate()
231                                .map(|(ix, item)| item.id(ix)),
232                        ),
233                )
234            })
235    }
236}