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