liora_components/
container.rs1use gpui::{AnyElement, App, Component, IntoElement, Pixels, RenderOnce, Window, prelude::*, px};
2use liora_core::stable_unique_id;
3
4pub struct Container {
5 header: Option<AnyElement>,
6 aside: Option<AnyElement>,
7 aside_right: bool,
8 footer: Option<AnyElement>,
9 main: Vec<AnyElement>,
10 overlays: Vec<AnyElement>,
11 header_height: Pixels,
12 footer_height: Pixels,
13 aside_width: Pixels,
14 aside_scroll: bool,
15 main_scroll: bool,
16 main_padding: Option<Pixels>,
17}
18
19impl Container {
20 pub fn new() -> Self {
21 Self {
22 header: None,
23 aside: None,
24 aside_right: false,
25 footer: None,
26 main: vec![],
27 overlays: vec![],
28 header_height: px(48.0),
29 footer_height: px(48.0),
30 aside_width: px(200.0),
31 aside_scroll: false,
32 main_scroll: false,
33 main_padding: None,
34 }
35 }
36
37 pub fn header(mut self, el: impl IntoElement) -> Self {
38 self.header = Some(el.into_any_element());
39 self
40 }
41 pub fn aside(mut self, el: impl IntoElement) -> Self {
42 self.aside = Some(el.into_any_element());
43 self
44 }
45 pub fn aside_right(mut self) -> Self {
46 self.aside_right = true;
47 self
48 }
49 pub fn footer(mut self, el: impl IntoElement) -> Self {
50 self.footer = Some(el.into_any_element());
51 self
52 }
53 pub fn child(mut self, el: impl IntoElement) -> Self {
54 self.main.push(el.into_any_element());
55 self
56 }
57 pub fn overlay(mut self, el: impl IntoElement) -> Self {
58 self.overlays.push(el.into_any_element());
59 self
60 }
61
62 pub fn header_height(mut self, height: impl Into<Pixels>) -> Self {
63 self.header_height = height.into();
64 self
65 }
66
67 pub fn header_height_lg(self) -> Self {
68 self.header_height(px(84.0))
69 }
70
71 pub fn footer_height(mut self, height: impl Into<Pixels>) -> Self {
72 self.footer_height = height.into();
73 self
74 }
75
76 pub fn aside_width(mut self, width: impl Into<Pixels>) -> Self {
77 self.aside_width = width.into();
78 self
79 }
80
81 pub fn aside_width_lg(self) -> Self {
82 self.aside_width(px(280.0))
83 }
84
85 pub fn aside_scroll(mut self) -> Self {
86 self.aside_scroll = true;
87 self
88 }
89
90 pub fn main_scroll(mut self) -> Self {
91 self.main_scroll = true;
92 self
93 }
94
95 pub fn main_padding(mut self, padding: impl Into<Pixels>) -> Self {
96 self.main_padding = Some(padding.into());
97 self
98 }
99
100 pub fn main_padding_xl(self) -> Self {
101 self.main_padding(px(32.0))
102 }
103}
104
105impl RenderOnce for Container {
106 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
107 let aside_id = stable_unique_id("container-aside-scroll", "aside", window, cx);
108 let main_id = stable_unique_id("container-main-scroll", "main", window, cx);
109 let theme = cx.global::<liora_core::Config>().theme.clone();
110 let aside_right = self.aside_right;
111 let main_children = self.main;
112 let overlays = self.overlays;
113 let aside_width = self.aside_width;
114 let aside_scroll = self.aside_scroll;
115 let main_scroll = self.main_scroll;
116 let main_padding = self.main_padding;
117
118 let mut page = gpui::div()
119 .flex()
120 .flex_col()
121 .size_full()
122 .relative()
123 .bg(theme.neutral.body);
124
125 if let Some(h) = self.header {
127 page = page.child(
128 gpui::div()
129 .flex_none()
130 .h(self.header_height)
131 .w_full()
132 .border_b_1()
133 .border_color(theme.neutral.border)
134 .px(px(16.0))
135 .flex()
136 .items_center()
137 .child(h),
138 );
139 }
140
141 let main = gpui::div()
143 .flex_1()
144 .min_h_0()
145 .flex()
146 .flex_col()
147 .h_full()
148 .id(main_id)
149 .when(main_scroll, |s| s.overflow_y_scroll())
150 .when_some(main_padding, |s, padding| s.p(padding))
151 .children(main_children);
152
153 let mut body = gpui::div().flex().flex_1().min_h_0().flex_row();
154 if let Some(a) = self.aside {
155 let aside_el = gpui::div()
156 .flex_none()
157 .w(aside_width)
158 .h_full()
159 .border_r_1()
160 .border_color(theme.neutral.border)
161 .id(aside_id)
162 .when(aside_scroll, |s| s.overflow_y_scroll())
163 .child(a);
164 if aside_right {
165 body = body.child(main);
166 body = body.child(aside_el);
167 } else {
168 body = body.child(aside_el);
169 body = body.child(main);
170 }
171 } else {
172 body = body.child(main);
173 }
174 page = page.child(body);
175
176 if let Some(f) = self.footer {
178 page = page.child(
179 gpui::div()
180 .flex_none()
181 .h(self.footer_height)
182 .w_full()
183 .border_t_1()
184 .border_color(theme.neutral.border)
185 .px(px(16.0))
186 .flex()
187 .items_center()
188 .child(f),
189 );
190 }
191
192 page.children(overlays)
193 }
194}
195
196impl IntoElement for Container {
197 type Element = Component<Self>;
198 fn into_element(self) -> Self::Element {
199 Component::new(self)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn container_gallery_shell_helpers_track_layout_state() {
209 let container = Container::new()
210 .header_height_lg()
211 .aside_width_lg()
212 .aside_scroll()
213 .main_scroll()
214 .main_padding_xl()
215 .overlay("portal");
216
217 assert_eq!(container.header_height, px(84.0));
218 assert_eq!(container.aside_width, px(280.0));
219 assert!(container.aside_scroll);
220 assert!(container.main_scroll);
221 assert_eq!(container.main_padding, Some(px(32.0)));
222 assert_eq!(container.overlays.len(), 1);
223 }
224
225 #[test]
226 fn container_scroll_regions_use_distinct_stable_id_keys() {
227 let production = include_str!("container.rs")
228 .split("#[cfg(test)]")
229 .next()
230 .unwrap();
231
232 assert!(
233 production.contains(r#"stable_unique_id("container-aside-scroll""#),
234 "aside scroll region needs its own stable key"
235 );
236 assert!(
237 production.contains(r#"stable_unique_id("container-main-scroll""#),
238 "main scroll region needs its own stable key"
239 );
240 assert!(
241 !production.contains(r#"stable_unique_id("container", "aside""#),
242 "aside/main scroll regions must not share the same keyed state"
243 );
244 }
245
246 #[test]
247 fn container_main_scroll_region_is_height_constrained() {
248 let production = include_str!("container.rs")
249 .split("#[cfg(test)]")
250 .next()
251 .unwrap();
252
253 assert!(
254 production.contains(".h_full()\n .id(main_id)"),
255 "main scroll region needs h_full before overflow_y_scroll so it forms a bounded viewport"
256 );
257 }
258}