diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a1baafc..600d28396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,29 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +### Features +- **ribir**: Introduced `AppRunGuard` to allow app and window configuration prior to app startup. (#565, @M-Adoo) + Previously, to configure the app and window before startup, `App::run` couldn't be used: + ``` rust + unsafe { + AppCtx::set_app_theme(material::purple::light()); + } + + App::new_window(root, None).set_title("Counter"); + App::exec(); + ``` + Now, with AppRunGuard, you can use `App::run` and chain the configuration methods: + + ``` rust + App::run(root) + .set_app_theme(material::purple::light()) + .on_window(|wnd| wnd.set_title("Counter")); + ``` + +### Breaking + +- **ribir**: `App::new_window` not accept window size as the second parameter. (#565, @M-Adoo) + ## [0.3.0-alpha.4] - 2024-04-17 ## [0.3.0-alpha.3] - 2024-04-10 diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 99b8f2c80..2b3072e63 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -65,8 +65,7 @@ impl Overlay { /// @{ Label::new("Click to show overlay") } /// } /// }; - /// App::new_window(w, None); - /// App::exec(); + /// App::run(w); /// ``` pub fn new(widget: M) -> Self where @@ -104,8 +103,8 @@ impl Overlay { /// @{ Label::new("Click to show overlay") } /// } /// }; - /// App::new_window(w, Some(Size::new(200., 200.))); - /// App::exec(); + /// + /// App::run(w).on_window(|wnd| wnd.request_resize(Size::new(200., 200.))); /// ``` pub fn new_with_handle(builder: M) -> Self where @@ -168,8 +167,7 @@ impl Overlay { /// @{ Label::new("Click to show overlay") } /// } /// }; - /// App::new_window(w, None); - /// App::exec(); + /// App::run(w); /// ``` pub fn show_map(&self, f: F, wnd: Rc) where diff --git a/core/src/window.rs b/core/src/window.rs index 81d7935d7..b5fac6cf0 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -300,13 +300,14 @@ impl Window { window } - pub fn set_content_widget(&self, root: impl WidgetBuilder) { + pub fn set_content_widget(&self, root: impl WidgetBuilder) -> &Self { let build_ctx = BuildCtx::new(None, &self.widget_tree); let root = root.build(&build_ctx); self .widget_tree .borrow_mut() - .set_content(root.consume()) + .set_content(root.consume()); + self } #[inline] @@ -320,37 +321,51 @@ impl Window { /// device. pub fn device_pixel_ratio(&self) -> f32 { self.shell_wnd.borrow().device_pixel_ratio() } - pub fn set_title(&self, title: &str) { self.shell_wnd.borrow_mut().set_title(title); } + pub fn set_title(&self, title: &str) -> &Self { + self.shell_wnd.borrow_mut().set_title(title); + self + } - pub fn set_icon(&self, icon: &PixelImage) { self.shell_wnd.borrow_mut().set_icon(icon); } + pub fn set_icon(&self, icon: &PixelImage) -> &Self { + self.shell_wnd.borrow_mut().set_icon(icon); + self + } /// Returns the cursor icon of the window. pub fn get_cursor(&self) -> CursorIcon { self.shell_wnd.borrow().cursor() } /// Modifies the cursor icon of the window. - pub fn set_cursor(&self, cursor: CursorIcon) { self.shell_wnd.borrow_mut().set_cursor(cursor); } + pub fn set_cursor(&self, cursor: CursorIcon) -> &Self { + self.shell_wnd.borrow_mut().set_cursor(cursor); + self + } /// Sets location of IME candidate box in window global coordinates relative /// to the top left. - pub fn set_ime_cursor_area(&self, rect: &Rect) { + pub fn set_ime_cursor_area(&self, rect: &Rect) -> &Self { self .shell_wnd .borrow_mut() .set_ime_cursor_area(rect); + self } - pub fn set_ime_allowed(&self, allowed: bool) { + pub fn set_ime_allowed(&self, allowed: bool) -> &Self { self .shell_wnd .borrow_mut() .set_ime_allowed(allowed); + self } pub fn request_resize(&self, size: Size) { self.shell_wnd.borrow_mut().request_resize(size) } pub fn size(&self) -> Size { self.shell_wnd.borrow().inner_size() } - pub fn set_min_size(&self, size: Size) { self.shell_wnd.borrow_mut().set_min_size(size); } + pub fn set_min_size(&self, size: Size) -> &Self { + self.shell_wnd.borrow_mut().set_min_size(size); + self + } pub fn shell_wnd(&self) -> &RefCell> { &self.shell_wnd } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index b5fb382ed..5f7f06784 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -3,12 +3,9 @@ use counter::counter; use ribir::prelude::*; fn main() { - unsafe { - AppCtx::set_app_theme(material::purple::light()); - } - - App::new_window(counter(), None).set_title("Counter"); - App::exec(); + App::run(counter()) + .set_app_theme(material::purple::light()) + .on_window(|wnd| wnd.set_title("Counter")); } #[cfg(test)] diff --git a/examples/messages/src/main.rs b/examples/messages/src/main.rs index 67a495789..dd9c96008 100644 --- a/examples/messages/src/main.rs +++ b/examples/messages/src/main.rs @@ -3,12 +3,9 @@ use messages::messages; use ribir::prelude::*; fn main() { - unsafe { - AppCtx::set_app_theme(material::purple::light()); - } - - App::new_window(messages(), None).set_title("Messages"); - App::exec(); + App::run(messages()) + .set_app_theme(material::purple::light()) + .on_window(|wnd| wnd.set_title("Messages")); } #[cfg(test)] diff --git a/examples/storybook/src/main.rs b/examples/storybook/src/main.rs index b381a5756..0cf755d0f 100644 --- a/examples/storybook/src/main.rs +++ b/examples/storybook/src/main.rs @@ -3,12 +3,13 @@ use ribir::prelude::*; use storybook::storybook; fn main() { - unsafe { - AppCtx::set_app_theme(material::purple::light()); - } - - App::new_window(storybook(), Some(Size::new(1024., 768.))).set_title("Storybook"); - App::exec(); + App::run(storybook()) + .set_app_theme(material::purple::light()) + .on_window(|wnd| { + wnd + .set_title("Storybook") + .request_resize(Size::new(1024., 768.)) + }); } #[cfg(test)] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index b92d1db87..bb5994a9a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -4,12 +4,9 @@ mod ui; use ui::todos; fn main() { - unsafe { - AppCtx::set_app_theme(material::purple::light()); - } - - App::new_window(todos(), Some(Size::new(400., 640.))).set_title("Todos"); - App::exec(); + App::run(todos()) + .set_app_theme(material::purple::light()) + .on_window(|wnd| wnd.set_title("Todos")); } #[cfg(test)] diff --git a/examples/wordle_game/src/main.rs b/examples/wordle_game/src/main.rs index 4efa3250d..f1bbd8ffb 100644 --- a/examples/wordle_game/src/main.rs +++ b/examples/wordle_game/src/main.rs @@ -5,12 +5,9 @@ mod ui; mod wordle; fn main() { - unsafe { - AppCtx::set_app_theme(material::purple::light()); - } - - App::new_window(wordle_game(), Some(Size::new(700., 620.))).set_title("Messages"); - App::exec(); + App::run(wordle_game()) + .set_app_theme(material::purple::light()) + .on_window(|wnd| wnd.set_title("Wordle Game")); } #[cfg(test)] diff --git a/ribir/src/app.rs b/ribir/src/app.rs index bfceeacb6..42b657e6e 100644 --- a/ribir/src/app.rs +++ b/ribir/src/app.rs @@ -161,14 +161,20 @@ impl App { } } +/// A guard return by the `App::run`, this help you to config the application +/// before it's run. +/// +/// This will call `App::exec` when it's dropped. +pub struct AppRunGuard(std::rc::Rc); + impl App { /// Start an application with the `root` widget, this will use the default /// theme to create an application and use the `root` widget to create a /// window, then run the application. #[track_caller] - pub fn run(root: impl WidgetBuilder + 'static) { - Self::new_window(root, None); - App::exec() + pub fn run(root: impl WidgetBuilder) -> AppRunGuard { + let wnd = Self::new_window(root); + AppRunGuard::new(wnd) } /// Get a event sender of the application event loop, you can use this to send @@ -192,12 +198,12 @@ impl App { /// create a new window with the `root` widget #[track_caller] - pub fn new_window(root: impl WidgetBuilder, size: Option) -> std::rc::Rc { + pub fn new_window(root: impl WidgetBuilder) -> std::rc::Rc { let app = unsafe { App::shared_mut() }; let event_loop = app.event_loop.as_ref().expect( " Event loop consumed. You can't create window after `App::exec` called in Web platform.", ); - let shell_wnd = WinitShellWnd::new(size, event_loop); + let shell_wnd = WinitShellWnd::new(event_loop); let wnd = AppCtx::new_window(Box::new(shell_wnd), root); #[cfg(not(target_family = "wasm"))] @@ -290,6 +296,28 @@ impl App { } } +impl AppRunGuard { + fn new(wnd: std::rc::Rc) -> Self { + static ONCE: std::sync::Once = std::sync::Once::new(); + assert!(!ONCE.is_completed(), "App::run can only be called once."); + Self(wnd) + } + + /// Set the application theme, this will apply to whole application. + pub fn set_app_theme(&self, theme: FullTheme) -> &Self { + // Safety: AppRunGuard is only created once and should be always used + // before the application startup. + unsafe { AppCtx::set_app_theme(theme) } + self + } + + /// Config the current window with the `f` function. + pub fn on_window<'a, R: 'a>(&'a self, f: impl FnOnce(&'a Window) -> R) -> R { f(&self.0) } +} + +impl Drop for AppRunGuard { + fn drop(&mut self) { App::exec() } +} impl EventSender { pub fn send(&self, e: AppEvent) { if let Err(err) = self.0.send_event(e) { diff --git a/ribir/src/winit_shell_wnd.rs b/ribir/src/winit_shell_wnd.rs index e9229eda5..f569c55b6 100644 --- a/ribir/src/winit_shell_wnd.rs +++ b/ribir/src/winit_shell_wnd.rs @@ -191,51 +191,48 @@ impl WinitShellWnd { Self::inner_wnd(wnd) } - pub(crate) fn new(size: Option, window_target: &EventLoopWindowTarget) -> Self { - let mut winit_wnd = winit::window::WindowBuilder::new(); - if let Some(size) = size { - winit_wnd = winit_wnd.with_inner_size(LogicalSize::new(size.width, size.height)); - } - - // A canvas need configure in web platform. - #[cfg(target_family = "wasm")] - { - use winit::platform::web::WindowBuilderExtWebSys; - const RIBIR_CANVAS: &str = "ribir_canvas"; - const RIBIR_CANVAS_USED: &str = "ribir_canvas_used"; - - use web_sys::wasm_bindgen::JsCast; - let document = web_sys::window().unwrap().document().unwrap(); - let elems = document.get_elements_by_class_name(RIBIR_CANVAS); - - let len = elems.length(); - for idx in 0..len { - if let Some(elem) = elems.get_with_index(idx) { - let mut classes_name = elem.class_name(); - if !classes_name - .split(" ") - .any(|v| v == RIBIR_CANVAS_USED) - { - let mut canvas = elem - .clone() - .dyn_into::(); - if canvas.is_err() { - let child = document.create_element("canvas").unwrap(); - if elem.append_child(&child).is_ok() { - canvas = child.dyn_into::(); - } - } - if let Ok(canvas) = canvas { - classes_name.push_str(&format!(" {}", RIBIR_CANVAS_USED)); - elem.set_class_name(&classes_name); - winit_wnd = winit_wnd.with_canvas(Some(canvas)); - } + #[cfg(target_family = "wasm")] + pub(crate) fn new(window_target: &EventLoopWindowTarget) -> Self { + const RIBIR_CANVAS: &str = "ribir_canvas"; + const RIBIR_CANVAS_USED: &str = "ribir_canvas_used"; + + use web_sys::{wasm_bindgen::JsCast, HtmlCanvasElement}; + let document = web_sys::window().unwrap().document().unwrap(); + let elems = document.get_elements_by_class_name(RIBIR_CANVAS); + + let mut canvas = None; + let len = elems.length(); + for idx in 0..len { + if let Some(elem) = elems.get_with_index(idx) { + let mut classes_name = elem.class_name(); + if !classes_name + .split(" ") + .any(|v| v == RIBIR_CANVAS_USED) + { + if let Ok(c) = elem.clone().dyn_into::() { + classes_name.push_str(&format!(" {}", RIBIR_CANVAS_USED)); + elem.set_class_name(&classes_name); + canvas = Some(c); + } else { + let child = document.create_element("canvas").unwrap(); + elem.append_child(&child).unwrap(); + canvas = Some(child.dyn_into::().unwrap()) } + break; } } } - let wnd = winit_wnd.build(window_target).unwrap(); + let canvas = canvas.expect("No unused 'ribir_canvas' class element found."); + + return Self::new_with_canvas(canvas, window_target); + } + + #[cfg(not(target_family = "wasm"))] + pub(crate) fn new(window_target: &EventLoopWindowTarget) -> Self { + let wnd = winit::window::WindowBuilder::new() + .build(window_target) + .unwrap(); Self::inner_wnd(wnd) }