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
62pub 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 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 pub fn window_status(mut self, status: impl Into<SharedString>) -> Self {
117 self.window_status = Some(status.into());
118 self
119 }
120
121 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 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
213pub 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
219pub 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}