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}