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