gpui_ui_kit/src-app/
miniapp.rs

1//! MiniApp - A minimal application template for GPUI examples and showcases
2//!
3//! Provides a reusable application shell with:
4//! - Standard menu bar with Quit option (Cmd+Q on macOS)
5//! - Configurable window title and size
6//! - Extensible for additional default features
7//!
8//! # Example
9//!
10//! ```ignore
11//! use gpui::*;
12//! use gpui_ui_kit::miniapp::{MiniApp, MiniAppConfig};
13//!
14//! struct MyDemo;
15//!
16//! impl Render for MyDemo {
17//!     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
18//!         div().child("Hello from MiniApp!")
19//!     }
20//! }
21//!
22//! fn main() {
23//!     MiniApp::run(MiniAppConfig::new("My Demo"), |cx| cx.new(|_| MyDemo));
24//! }
25//! ```
26
27use gpui::*;
28
29/// Configuration for a MiniApp instance
30#[derive(Clone)]
31pub struct MiniAppConfig {
32    /// Window title
33    pub title: SharedString,
34    /// Window width in pixels
35    pub width: f32,
36    /// Window height in pixels
37    pub height: f32,
38    /// Application name shown in menu bar
39    pub app_name: SharedString,
40    /// Enable vertical scrollbar for content
41    pub scrollable: bool,
42}
43
44impl MiniAppConfig {
45    /// Create a new configuration with the given title
46    ///
47    /// Uses default window size of 900x700 pixels.
48    pub fn new(title: impl Into<SharedString>) -> Self {
49        let title = title.into();
50        Self {
51            title: title.clone(),
52            width: 900.0,
53            height: 700.0,
54            app_name: title,
55            scrollable: true,
56        }
57    }
58
59    /// Set the window size
60    pub fn size(mut self, width: f32, height: f32) -> Self {
61        self.width = width;
62        self.height = height;
63        self
64    }
65
66    /// Set the application name shown in the menu bar
67    ///
68    /// By default, this is the same as the window title.
69    pub fn app_name(mut self, name: impl Into<SharedString>) -> Self {
70        self.app_name = name.into();
71        self
72    }
73
74    /// Enable or disable vertical scrollbar for content
75    ///
76    /// By default, scrolling is enabled.
77    pub fn scrollable(mut self, scrollable: bool) -> Self {
78        self.scrollable = scrollable;
79        self
80    }
81}
82
83impl Default for MiniAppConfig {
84    fn default() -> Self {
85        Self::new("MiniApp")
86    }
87}
88
89// Define the Quit action for the menu
90actions!(miniapp, [Quit]);
91
92/// A wrapper view that adds vertical scrolling to its content
93struct ScrollableWrapper {
94    inner: AnyView,
95}
96
97impl Render for ScrollableWrapper {
98    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
99        div()
100            .id("miniapp-scroll-container")
101            .size_full()
102            .overflow_y_scroll()
103            .child(self.inner.clone())
104    }
105}
106
107/// MiniApp provides a minimal application shell for GPUI examples and showcases
108///
109/// It handles:
110/// - Application lifecycle
111/// - Standard menu bar with Quit option
112/// - Window creation with configurable size
113/// - Keyboard shortcut binding (Cmd+Q to quit)
114pub struct MiniApp;
115
116impl MiniApp {
117    /// Run a MiniApp with the given configuration and view builder
118    ///
119    /// The `build_view` closure receives a `&mut Context<V>` and should return
120    /// a `V` instance that implements `Render`.
121    ///
122    /// # Example
123    ///
124    /// ```ignore
125    /// use gpui::*;
126    /// use gpui_ui_kit::MiniApp;
127    ///
128    /// struct MyView;
129    /// impl Render for MyView {
130    ///     fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
131    ///         div().child("Hello!")
132    ///     }
133    /// }
134    ///
135    /// MiniApp::run(MiniAppConfig::new("Demo"), |cx| cx.new(MyView::new));
136    /// ```
137    pub fn run<V, F>(config: MiniAppConfig, build_view: F)
138    where
139        V: Render + 'static,
140        F: FnOnce(&mut App) -> Entity<V> + 'static,
141    {
142        let config_clone = config.clone();
143
144        Application::new().run(move |cx: &mut App| {
145            // Register quit action
146            cx.on_action::<Quit>(|_action, cx| {
147                cx.quit();
148            });
149
150            // Set up menu bar with application name
151            let quit_label: SharedString = format!("Quit {}", config_clone.app_name).into();
152            cx.set_menus(vec![Menu {
153                name: config_clone.app_name.clone(),
154                items: vec![MenuItem::action(quit_label, Quit)],
155            }]);
156
157            // Bind Cmd+Q to quit
158            cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
159
160            // Create window
161            let bounds = Bounds::centered(
162                None,
163                size(px(config_clone.width), px(config_clone.height)),
164                cx,
165            );
166
167            if config_clone.scrollable {
168                cx.open_window(
169                    WindowOptions {
170                        window_bounds: Some(WindowBounds::Windowed(bounds)),
171                        titlebar: Some(TitlebarOptions {
172                            title: Some(config_clone.title.clone()),
173                            ..Default::default()
174                        }),
175                        ..Default::default()
176                    },
177                    move |_, cx| {
178                        let inner_view = build_view(cx);
179                        cx.new(|_| ScrollableWrapper {
180                            inner: inner_view.into(),
181                        })
182                    },
183                )
184                .unwrap();
185            } else {
186                cx.open_window(
187                    WindowOptions {
188                        window_bounds: Some(WindowBounds::Windowed(bounds)),
189                        titlebar: Some(TitlebarOptions {
190                            title: Some(config_clone.title.clone()),
191                            ..Default::default()
192                        }),
193                        ..Default::default()
194                    },
195                    |_, cx| build_view(cx),
196                )
197                .unwrap();
198            }
199
200            cx.activate(true);
201        });
202    }
203
204    /// Run a MiniApp with default configuration
205    ///
206    /// Uses "MiniApp" as the default title and 900x700 window size.
207    pub fn run_default<V, F>(build_view: F)
208    where
209        V: Render + 'static,
210        F: FnOnce(&mut App) -> Entity<V> + 'static,
211    {
212        Self::run(MiniAppConfig::default(), build_view);
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::MiniAppConfig;
219
220    #[test]
221    fn test_config_new() {
222        let config = MiniAppConfig::new("Test App");
223        assert_eq!(config.title.as_ref(), "Test App");
224        assert_eq!(config.app_name.as_ref(), "Test App");
225        assert_eq!(config.width, 900.0);
226        assert_eq!(config.height, 700.0);
227    }
228
229    #[test]
230    fn test_config_size() {
231        let config = MiniAppConfig::new("Test").size(1200.0, 800.0);
232        assert_eq!(config.width, 1200.0);
233        assert_eq!(config.height, 800.0);
234    }
235
236    #[test]
237    fn test_config_app_name() {
238        let config = MiniAppConfig::new("Window Title").app_name("Menu Name");
239        assert_eq!(config.title.as_ref(), "Window Title");
240        assert_eq!(config.app_name.as_ref(), "Menu Name");
241    }
242
243    #[test]
244    fn test_config_default() {
245        let config = MiniAppConfig::default();
246        assert_eq!(config.title.as_ref(), "MiniApp");
247    }
248
249    #[test]
250    fn test_config_builder_chain() {
251        let config = MiniAppConfig::new("Demo")
252            .size(1000.0, 600.0)
253            .app_name("My Demo App");
254
255        assert_eq!(config.title.as_ref(), "Demo");
256        assert_eq!(config.width, 1000.0);
257        assert_eq!(config.height, 600.0);
258        assert_eq!(config.app_name.as_ref(), "My Demo App");
259    }
260}