Skip to main content

guise/layout/
appshell.rs

1//! `AppShell` — the application frame: header, navbar, aside, and footer
2//! regions around a scrollable main area.
3//!
4//! Regions take a fixed px size plus a content closure that is re-invoked
5//! every render, so they show live data. The main area is the shell's
6//! children (`ParentElement`), laid out as a scrollable column. The shell
7//! fills its parent, so place it at the window root (or inside a sized box
8//! for a framed demo).
9//!
10//! ```ignore
11//! use guise::prelude::*;
12//!
13//! AppShell::new()
14//!     .header(48.0, |_window, _cx| Text::new("guise"))
15//!     .navbar(220.0, |_window, _cx| Text::new("nav links"))
16//!     .footer(28.0, |_window, _cx| Text::new("status").size(Size::Xs))
17//!     .child(Title::new("Main content").order(2))
18//! ```
19
20use gpui::prelude::*;
21use gpui::{div, px, AnyElement, App, IntoElement, Window};
22
23use crate::theme::theme;
24
25/// A region-content builder, re-invoked every render (mirrors
26/// `data::Content`, kept private to `layout`).
27type Content = Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
28
29/// The application frame. The Mantine `AppShell`.
30///
31/// Layout: header across the top, footer across the bottom, and a middle
32/// row of navbar | main | aside. Every region gets the theme surface
33/// background and a hairline border on its inner edge.
34#[derive(IntoElement)]
35pub struct AppShell {
36    header: Option<(f32, Content)>,
37    navbar: Option<(f32, Content)>,
38    aside: Option<(f32, Content)>,
39    footer: Option<(f32, Content)>,
40    children: Vec<AnyElement>,
41}
42
43impl AppShell {
44    pub fn new() -> Self {
45        AppShell {
46            header: None,
47            navbar: None,
48            aside: None,
49            footer: None,
50            children: Vec::new(),
51        }
52    }
53
54    /// Top region: `height` px tall, spanning the full width.
55    pub fn header<E>(
56        mut self,
57        height: f32,
58        content: impl Fn(&mut Window, &mut App) -> E + 'static,
59    ) -> Self
60    where
61        E: IntoElement,
62    {
63        self.header = Some((
64            height,
65            Box::new(move |window, cx| content(window, cx).into_any_element()),
66        ));
67        self
68    }
69
70    /// Left region: `width` px wide, between header and footer.
71    pub fn navbar<E>(
72        mut self,
73        width: f32,
74        content: impl Fn(&mut Window, &mut App) -> E + 'static,
75    ) -> Self
76    where
77        E: IntoElement,
78    {
79        self.navbar = Some((
80            width,
81            Box::new(move |window, cx| content(window, cx).into_any_element()),
82        ));
83        self
84    }
85
86    /// Right region: `width` px wide, between header and footer.
87    pub fn aside<E>(
88        mut self,
89        width: f32,
90        content: impl Fn(&mut Window, &mut App) -> E + 'static,
91    ) -> Self
92    where
93        E: IntoElement,
94    {
95        self.aside = Some((
96            width,
97            Box::new(move |window, cx| content(window, cx).into_any_element()),
98        ));
99        self
100    }
101
102    /// Bottom region: `height` px tall, spanning the full width.
103    pub fn footer<E>(
104        mut self,
105        height: f32,
106        content: impl Fn(&mut Window, &mut App) -> E + 'static,
107    ) -> Self
108    where
109        E: IntoElement,
110    {
111        self.footer = Some((
112            height,
113            Box::new(move |window, cx| content(window, cx).into_any_element()),
114        ));
115        self
116    }
117}
118
119impl Default for AppShell {
120    fn default() -> Self {
121        AppShell::new()
122    }
123}
124
125impl ParentElement for AppShell {
126    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
127        self.children.extend(elements);
128    }
129}
130
131impl RenderOnce for AppShell {
132    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
133        let t = theme(cx);
134        let body = t.body().hsla();
135        let surface = t.surface().hsla();
136        let border = t.border().hsla();
137
138        let mut root = div().size_full().flex().flex_col().bg(body);
139
140        if let Some((height, content)) = self.header {
141            root = root.child(
142                div()
143                    .flex_none()
144                    .w_full()
145                    .h(px(height))
146                    .flex()
147                    .flex_col()
148                    .overflow_hidden()
149                    .bg(surface)
150                    .border_b_1()
151                    .border_color(border)
152                    .child(content(window, cx)),
153            );
154        }
155
156        let mut middle = div().flex_1().min_h(px(0.0)).w_full().flex();
157
158        if let Some((width, content)) = self.navbar {
159            middle = middle.child(
160                div()
161                    .flex_none()
162                    .w(px(width))
163                    .flex()
164                    .flex_col()
165                    .overflow_hidden()
166                    .bg(surface)
167                    .border_r_1()
168                    .border_color(border)
169                    .child(content(window, cx)),
170            );
171        }
172
173        middle = middle.child(
174            div()
175                .id("guise-appshell-main")
176                .flex_1()
177                .min_w(px(0.0))
178                .flex()
179                .flex_col()
180                .overflow_y_scroll()
181                .children(self.children),
182        );
183
184        if let Some((width, content)) = self.aside {
185            middle = middle.child(
186                div()
187                    .flex_none()
188                    .w(px(width))
189                    .flex()
190                    .flex_col()
191                    .overflow_hidden()
192                    .bg(surface)
193                    .border_l_1()
194                    .border_color(border)
195                    .child(content(window, cx)),
196            );
197        }
198
199        root = root.child(middle);
200
201        if let Some((height, content)) = self.footer {
202            root = root.child(
203                div()
204                    .flex_none()
205                    .w_full()
206                    .h(px(height))
207                    .flex()
208                    .flex_col()
209                    .overflow_hidden()
210                    .bg(surface)
211                    .border_t_1()
212                    .border_color(border)
213                    .child(content(window, cx)),
214            );
215        }
216
217        root
218    }
219}