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    children: Vec<AnyElement>,
24    footer: Option<AnyElement>,
25}
26
27impl SidePanel {
28    /// A panel with an element-ID prefix and a title.
29    ///
30    /// The ID must be unique in the window; it separates the scroll state of
31    /// panels that otherwise look alike.
32    pub fn new(id: impl Into<SharedString>, title: impl Into<SharedString>) -> Self {
33        Self {
34            id: id.into(),
35            title: title.into(),
36            subtitle: None,
37            actions: Vec::new(),
38            children: Vec::new(),
39            footer: None,
40        }
41    }
42
43    /// Add a muted second line under the title.
44    pub fn subtitle(mut self, subtitle: impl Into<SharedString>) -> Self {
45        self.subtitle = Some(subtitle.into());
46        self
47    }
48
49    /// Add an action to the header's trailing edge.
50    pub fn action(mut self, action: impl IntoElement) -> Self {
51        self.actions.push(action.into_any_element());
52        self
53    }
54
55    /// Add a footer below the scrollable body.
56    pub fn footer(mut self, footer: impl IntoElement) -> Self {
57        self.footer = Some(footer.into_any_element());
58        self
59    }
60}
61
62impl ParentElement for SidePanel {
63    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
64        self.children.extend(elements);
65    }
66}
67
68impl RenderOnce for SidePanel {
69    fn render(self, _: &mut Window, cx: &mut gpui_kit::App) -> impl IntoElement {
70        let theme = cx.theme();
71        let body_id = ElementId::Name(format!("{}-body", self.id).into());
72
73        let header = v_flex()
74            .gap_1()
75            .px_3()
76            .py_3()
77            .border_b_1()
78            .border_color(theme.border)
79            .child(
80                h_flex()
81                    .justify_between()
82                    .gap_2()
83                    .child(
84                        div()
85                            .text_lg()
86                            .font_weight(FontWeight::SEMIBOLD)
87                            .child(self.title),
88                    )
89                    .child(h_flex().gap_1().children(self.actions)),
90            )
91            .when_some(self.subtitle, |header, subtitle| {
92                header.child(
93                    div()
94                        .text_sm()
95                        .text_color(theme.muted_foreground)
96                        .child(subtitle),
97                )
98            });
99
100        let body = v_flex()
101            .id(body_id)
102            .flex_1()
103            .min_h(px(0.))
104            .gap_2()
105            .px_3()
106            .py_3()
107            .overflow_y_scrollbar()
108            .children(self.children);
109
110        v_flex()
111            .size_full()
112            .bg(theme.background)
113            .child(header)
114            .child(body)
115            .when_some(self.footer, |panel, footer| {
116                panel.child(
117                    div()
118                        .px_3()
119                        .py_2()
120                        .border_t_1()
121                        .border_color(theme.border)
122                        .child(footer),
123                )
124            })
125    }
126}