Skip to main content

ragrig_gpui/
side_panel.rs

1//! A titled side panel with a scrollable body.
2
3use gpui_kit::{
4    AnyElement, ElementId, FontWeight, InteractiveElement as _, IntoElement, ParentElement,
5    RenderOnce, SharedString, Styled as _, Window,
6    component::{ActiveTheme as _, h_flex, scroll::ScrollableElement as _, v_flex},
7    div,
8    prelude::FluentBuilder as _,
9    px,
10};
11
12/// A reusable left/right dock surface.
13///
14/// The panel draws a header (title, optional subtitle, action buttons), a
15/// vertically scrollable body, and an optional footer. It owns no state, so
16/// hosts can rebuild it freely each frame.
17#[derive(IntoElement)]
18pub struct SidePanel {
19    id: SharedString,
20    title: SharedString,
21    subtitle: Option<SharedString>,
22    actions: Vec<AnyElement>,
23    toolbar: Option<AnyElement>,
24    children: Vec<AnyElement>,
25    footer: Option<AnyElement>,
26}
27
28impl SidePanel {
29    /// A panel with an element-ID prefix and a title.
30    ///
31    /// The ID must be unique in the window; it separates the scroll state of
32    /// panels that otherwise look alike.
33    pub fn new(id: impl Into<SharedString>, title: impl Into<SharedString>) -> Self {
34        Self {
35            id: id.into(),
36            title: title.into(),
37            subtitle: None,
38            actions: Vec::new(),
39            toolbar: None,
40            children: Vec::new(),
41            footer: None,
42        }
43    }
44
45    /// Add a muted second line under the title.
46    pub fn subtitle(mut self, subtitle: impl Into<SharedString>) -> Self {
47        self.subtitle = Some(subtitle.into());
48        self
49    }
50
51    /// Add an action to the header's trailing edge.
52    pub fn action(mut self, action: impl IntoElement) -> Self {
53        self.actions.push(action.into_any_element());
54        self
55    }
56
57    /// Add a fixed toolbar between the header and the scrollable body.
58    ///
59    /// Unlike [`Self::child`], the toolbar does not scroll with the body, so
60    /// pane-level controls stay reachable above a long list.
61    pub fn toolbar(mut self, toolbar: impl IntoElement) -> Self {
62        self.toolbar = Some(toolbar.into_any_element());
63        self
64    }
65
66    /// Add a footer below the scrollable body.
67    pub fn footer(mut self, footer: impl IntoElement) -> Self {
68        self.footer = Some(footer.into_any_element());
69        self
70    }
71}
72
73impl ParentElement for SidePanel {
74    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
75        self.children.extend(elements);
76    }
77}
78
79impl RenderOnce for SidePanel {
80    fn render(self, _: &mut Window, cx: &mut gpui_kit::App) -> impl IntoElement {
81        let theme = cx.theme();
82        let body_id = ElementId::Name(format!("{}-body", self.id).into());
83
84        let header = v_flex()
85            .gap_1()
86            .px_3()
87            .py_3()
88            .border_b_1()
89            .border_color(theme.border)
90            .child(
91                h_flex()
92                    .justify_between()
93                    .gap_2()
94                    .child(
95                        div()
96                            .text_lg()
97                            .font_weight(FontWeight::SEMIBOLD)
98                            .child(self.title),
99                    )
100                    .child(h_flex().gap_1().children(self.actions)),
101            )
102            .when_some(self.subtitle, |header, subtitle| {
103                header.child(
104                    div()
105                        .text_sm()
106                        .text_color(theme.muted_foreground)
107                        .child(subtitle),
108                )
109            });
110
111        let body = v_flex()
112            .id(body_id)
113            .flex_1()
114            .min_h(px(0.))
115            .gap_2()
116            .px_3()
117            .py_3()
118            .overflow_y_scrollbar()
119            .children(self.children);
120
121        v_flex()
122            .size_full()
123            .bg(theme.background)
124            .child(header)
125            .when_some(self.toolbar, |panel, toolbar| {
126                panel.child(
127                    div()
128                        .px_3()
129                        .py_2()
130                        .border_b_1()
131                        .border_color(theme.border)
132                        .child(toolbar),
133                )
134            })
135            .child(body)
136            .when_some(self.footer, |panel, footer| {
137                panel.child(
138                    div()
139                        .px_3()
140                        .py_2()
141                        .border_t_1()
142                        .border_color(theme.border)
143                        .child(footer),
144                )
145            })
146    }
147}