Skip to main content

fluent_app/
app_builder.rs

1use std::sync::Arc;
2
3use fluent_core::Theme;
4use fluent_layout::{modal::ModalStack, ToastStack};
5use gpui::{
6    prelude::*, px, size, App, Application, AssetSource, Bounds, Context, Entity, IntoElement,
7    Render, SharedString, TitlebarOptions, Window, WindowBounds, WindowDecorations, WindowOptions,
8};
9
10use crate::{
11    assets::FluentAssets, chrome::wrap_with_chrome, title_bar::TitleBar, window_title::WindowTitle,
12};
13
14const DEFAULT_W: f32 = 1280.0;
15const DEFAULT_H: f32 = 800.0;
16
17type WindowShouldCloseHandler = Box<dyn Fn(&mut Window, &mut App) -> bool>;
18
19struct ThemeRoot<V: Render + 'static> {
20    content: Entity<V>,
21    last_window_title: SharedString,
22}
23
24impl<V: Render + 'static> ThemeRoot<V> {
25    fn new(cx: &mut Context<Self>, content: Entity<V>) -> Self {
26        let themed_content = content.clone();
27        cx.observe_global::<Theme>(move |_, cx| {
28            themed_content.update(cx, |_, cx| cx.notify());
29            cx.notify();
30        })
31        .detach();
32
33        cx.observe_global::<WindowTitle>(|_, cx| cx.notify())
34            .detach();
35
36        let last_window_title = cx
37            .try_global::<WindowTitle>()
38            .map(|title| title.title().clone())
39            .unwrap_or_default();
40
41        Self {
42            content,
43            last_window_title,
44        }
45    }
46}
47
48impl<V: Render + 'static> Render for ThemeRoot<V> {
49    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
50        if let Some(title) = cx.try_global::<WindowTitle>() {
51            let next_title = title.title().clone();
52            if self.last_window_title != next_title {
53                window.set_window_title(next_title.as_ref());
54                self.last_window_title = next_title;
55            }
56        }
57
58        self.content.clone()
59    }
60}
61
62/// Builder for a FluentGUI application.
63///
64/// ```ignore
65/// FluentApp::new("MyApp")
66///     .window_size(1440.0, 900.0)
67///     .run(|cx| {
68///         cx.new(|_| Workspace::new()...)
69///     });
70/// ```
71pub struct FluentApp {
72    title: SharedString,
73    window_w: f32,
74    window_h: f32,
75    window_min_size: Option<(f32, f32)>,
76    app_id: Option<String>,
77    window_status: Option<SharedString>,
78    on_window_should_close: Option<WindowShouldCloseHandler>,
79    dark: bool,
80    assets: Option<Arc<dyn AssetSource>>,
81}
82
83impl FluentApp {
84    pub fn new(title: impl Into<SharedString>) -> Self {
85        Self {
86            title: title.into(),
87            window_w: DEFAULT_W,
88            window_h: DEFAULT_H,
89            window_min_size: None,
90            app_id: None,
91            window_status: None,
92            on_window_should_close: None,
93            dark: true,
94            assets: None,
95        }
96    }
97
98    pub fn window_size(mut self, w: f32, h: f32) -> Self {
99        self.window_w = w;
100        self.window_h = h;
101        self
102    }
103
104    pub fn window_min_size(mut self, w: f32, h: f32) -> Self {
105        self.window_min_size = Some((w, h));
106        self
107    }
108
109    /// Set the desktop application id used by platforms that group windows.
110    pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
111        self.app_id = Some(app_id.into());
112        self
113    }
114
115    /// Set the initial generic status associated with the window title.
116    pub fn window_status(mut self, status: impl Into<SharedString>) -> Self {
117        self.window_status = Some(status.into());
118        self
119    }
120
121    /// Register a generic close guard for app lifecycle integrations.
122    ///
123    /// Return `false` to prevent the platform close request.
124    pub fn on_window_should_close(
125        mut self,
126        handler: impl Fn(&mut Window, &mut App) -> bool + 'static,
127    ) -> Self {
128        self.on_window_should_close = Some(Box::new(handler));
129        self
130    }
131
132    pub fn dark_theme(mut self) -> Self {
133        self.dark = true;
134        self
135    }
136
137    pub fn light_theme(mut self) -> Self {
138        self.dark = false;
139        self
140    }
141
142    pub fn assets(mut self, assets: impl AssetSource) -> Self {
143        self.assets = Some(Arc::new(assets));
144        self
145    }
146
147    /// Launch the application.
148    ///
149    /// The `build` closure runs on the main thread with `&mut App` and must
150    /// return the root `Entity<V>` to display as the window's content.
151    pub fn run<V: Render + 'static>(self, build: impl FnOnce(&mut App) -> Entity<V> + 'static) {
152        let title = self.title.clone();
153        let w = self.window_w;
154        let h = self.window_h;
155        let min_size = self.window_min_size;
156        let app_id = self.app_id;
157        let window_status = self.window_status;
158        let on_window_should_close = self.on_window_should_close;
159        let dark = self.dark;
160        let assets = self.assets;
161
162        Application::new()
163            .with_assets(FluentAssets::new(assets))
164            .run(move |cx: &mut App| {
165                if dark {
166                    Theme::init(cx);
167                } else {
168                    cx.set_global(Theme::light());
169                }
170                ModalStack::init(cx);
171                ToastStack::init(cx);
172                let mut window_title = WindowTitle::new(title.clone());
173                if let Some(status) = window_status.clone() {
174                    window_title = window_title.with_status(status);
175                }
176                cx.set_global(window_title);
177
178                let bounds = Bounds::centered(None, size(px(w), px(h)), cx);
179
180                cx.open_window(
181                    WindowOptions {
182                        window_bounds: Some(WindowBounds::Windowed(bounds)),
183                        window_min_size: min_size.map(|(w, h)| size(px(w), px(h))),
184                        app_id: app_id.clone(),
185                        titlebar: Some(TitlebarOptions {
186                            title: Some(title.clone()),
187                            appears_transparent: true,
188                            traffic_light_position: None,
189                        }),
190                        window_decorations: Some(WindowDecorations::Client),
191                        ..Default::default()
192                    },
193                    move |window, cx: &mut App| {
194                        if let Some(app_id) = app_id.as_deref() {
195                            window.set_app_id(app_id);
196                        }
197                        if let Some(handler) = on_window_should_close {
198                            window
199                                .on_window_should_close(cx, move |window, cx| handler(window, cx));
200                        }
201                        let root = build(cx);
202                        let chromed = wrap_with_chrome(root, cx);
203                        cx.new(|cx| ThemeRoot::new(cx, chromed))
204                    },
205                )
206                .unwrap();
207
208                cx.activate(true);
209            });
210    }
211}
212
213/// Create a `TitleBar` entity — include as the first child of your `Workspace`.
214pub fn title_bar(title: impl Into<SharedString>, cx: &mut App) -> Entity<TitleBar> {
215    let t = title.into();
216    cx.new(|cx| TitleBar::new(cx, t))
217}
218
219/// Create a `TitleBar` entity whose title/status follow the global
220/// [`WindowTitle`] state.
221pub fn window_title_bar(cx: &mut App) -> Entity<TitleBar> {
222    cx.new(TitleBar::from_window_title)
223}
224
225#[cfg(test)]
226mod tests {
227    use super::FluentApp;
228
229    #[test]
230    fn builder_stores_window_min_size() {
231        let app = FluentApp::new("App").window_min_size(800.0, 600.0);
232
233        assert_eq!(app.window_min_size, Some((800.0, 600.0)));
234    }
235
236    #[test]
237    fn builder_stores_app_id_and_status() {
238        let app = FluentApp::new("App")
239            .app_id("org.example.App")
240            .window_status("Ready");
241
242        assert_eq!(app.app_id.as_deref(), Some("org.example.App"));
243        assert_eq!(
244            app.window_status.as_ref().map(|s| s.as_ref()),
245            Some("Ready")
246        );
247    }
248}