Skip to main content

liora_components/
container.rs

1use 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        // Header
126        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        // Body: aside + main
142        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        // Footer
177        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}