diff --git a/Cargo.toml b/Cargo.toml index 9c91f99a3..f4a365810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = dioxus-hooks = { path = "packages/hooks", version = "0.5.0-alpha.2" } dioxus-web = { path = "packages/web", version = "0.5.0-alpha.2" } dioxus-ssr = { path = "packages/ssr", version = "0.5.0-alpha.2", default-features = false } -dioxus-desktop = { path = "packages/desktop", version = "0.5.0-alpha.2" } +dioxus-desktop = { path = "packages/desktop", version = "0.5.0-alpha.2", default-features = false } dioxus-mobile = { path = "packages/mobile", version = "0.5.0-alpha.2" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.5.0-alpha.2" } dioxus-liveview = { path = "packages/liveview", version = "0.5.0-alpha.2" } diff --git a/examples/custom_menu.rs b/examples/custom_menu.rs new file mode 100644 index 000000000..73d35c352 --- /dev/null +++ b/examples/custom_menu.rs @@ -0,0 +1,35 @@ +//! This example shows how to use a custom menu bar with Dioxus desktop. +//! This example is not supported on the mobile or web renderers. + +use dioxus::desktop::muda::*; +use dioxus::prelude::*; + +fn main() { + // Create a menu bar that only contains the edit menu + let menu = Menu::new(); + let edit_menu = Submenu::new("Edit", true); + + edit_menu + .append_items(&[ + &PredefinedMenuItem::undo(None), + &PredefinedMenuItem::redo(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::cut(None), + &PredefinedMenuItem::copy(None), + &PredefinedMenuItem::paste(None), + &PredefinedMenuItem::select_all(None), + ]) + .unwrap(); + + menu.append(&edit_menu).unwrap(); + + // Create a desktop config that overrides the default menu with the custom menu + let config = dioxus::desktop::Config::new().with_menu(menu); + + // Launch the app with the custom menu + LaunchBuilder::new().with_cfg(config).launch(app) +} + +fn app() -> Element { + rsx! {"Hello World!"} +} diff --git a/examples/window_focus.rs b/examples/window_focus.rs index 447544077..e3a037276 100644 --- a/examples/window_focus.rs +++ b/examples/window_focus.rs @@ -8,12 +8,12 @@ use dioxus::desktop::tao::event::Event as WryEvent; use dioxus::desktop::tao::event::WindowEvent; use dioxus::desktop::use_wry_event_handler; -use dioxus::desktop::{Config, WindowCloseBehaviour}; +use dioxus::desktop::{Config, WindowCloseBehavior}; use dioxus::prelude::*; fn main() { LaunchBuilder::desktop() - .with_cfg(Config::new().with_close_behaviour(WindowCloseBehaviour::CloseWindow)) + .with_cfg(Config::new().with_close_behaviour(WindowCloseBehavior::CloseWindow)) .launch(app) } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 3b09f4ad5..442cccea4 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, WindowCloseBehaviour}, + config::{Config, WindowCloseBehavior}, element::DesktopElement, event_handlers::WindowEventHandlers, file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest}, @@ -33,7 +33,7 @@ pub(crate) struct App { // Stuff we need mutable access to pub(crate) control_flow: ControlFlow, pub(crate) is_visible_before_start: bool, - pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) window_behavior: WindowCloseBehavior, pub(crate) webviews: HashMap, /// This single blob of state is shared between all the windows so they have access to the runtime state @@ -43,8 +43,6 @@ pub(crate) struct App { } /// A bundle of state shared between all the windows, providing a way for us to communicate with running webview. -/// -/// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell pub(crate) struct SharedContext { pub(crate) event_handlers: WindowEventHandlers, pub(crate) pending_webviews: RefCell>, @@ -58,7 +56,7 @@ impl App { let event_loop = EventLoopBuilder::::with_user_event().build(); let app = Self { - window_behavior: cfg.last_window_close_behaviour, + window_behavior: cfg.last_window_close_behavior, is_visible_before_start: true, webviews: HashMap::new(), control_flow: ControlFlow::Wait, @@ -128,7 +126,7 @@ impl App { } pub fn handle_close_requested(&mut self, id: WindowId) { - use WindowCloseBehaviour::*; + use WindowCloseBehavior::*; match self.window_behavior { LastWindowExitsApp => { @@ -156,7 +154,7 @@ impl App { if matches!( self.window_behavior, - WindowCloseBehaviour::LastWindowExitsApp + WindowCloseBehavior::LastWindowExitsApp ) && self.webviews.is_empty() { self.control_flow = ControlFlow::Exit diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 6178dbf93..6b2bb3331 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -3,9 +3,11 @@ use std::path::PathBuf; use tao::window::{Icon, WindowBuilder}; use wry::http::{Request as HttpRequest, Response as HttpResponse}; +use crate::menubar::{default_menu_bar, DioxusMenu}; + /// The behaviour of the application when the last window is closed. #[derive(Copy, Clone, Eq, PartialEq)] -pub enum WindowCloseBehaviour { +pub enum WindowCloseBehavior { /// Default behaviour, closing the last window exits the app LastWindowExitsApp, /// Closing the last window will not actually close it, just hide it @@ -17,6 +19,7 @@ pub enum WindowCloseBehaviour { /// The configuration for the desktop application. pub struct Config { pub(crate) window: WindowBuilder, + pub(crate) menu: Option, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) disable_context_menu: bool, @@ -26,8 +29,7 @@ pub struct Config { pub(crate) custom_index: Option, pub(crate) root_name: String, pub(crate) background_color: Option<(u8, u8, u8, u8)>, - pub(crate) last_window_close_behaviour: WindowCloseBehaviour, - pub(crate) enable_default_menu_bar: bool, + pub(crate) last_window_close_behavior: WindowCloseBehavior, } pub(crate) type WryProtocol = ( @@ -48,6 +50,7 @@ impl Config { Self { window, + menu: Some(default_menu_bar()), protocols: Vec::new(), pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), @@ -57,19 +60,10 @@ impl Config { custom_index: None, root_name: "main".to_string(), background_color: None, - last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp, - enable_default_menu_bar: true, + last_window_close_behavior: WindowCloseBehavior::LastWindowExitsApp, } } - /// Set whether the default menu bar should be enabled. - /// - /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`. - pub fn with_default_menu_bar(mut self, enable: bool) -> Self { - self.enable_default_menu_bar = enable; - self - } - /// set the directory from which assets will be searched in release mode pub fn with_resource_directory(mut self, path: impl Into) -> Self { self.resource_dir = Some(path.into()); @@ -105,8 +99,8 @@ impl Config { } /// Sets the behaviour of the application when the last window is closed. - pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self { - self.last_window_close_behaviour = behaviour; + pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehavior) -> Self { + self.last_window_close_behavior = behaviour; self } @@ -146,7 +140,7 @@ impl Config { /// Set the name of the element that Dioxus will use as the root. /// - /// This is akint to calling React.render() on the element with the specified name. + /// This is akin to calling React.render() on the element with the specified name. pub fn with_root_name(mut self, name: impl Into) -> Self { self.root_name = name.into(); self @@ -159,6 +153,18 @@ impl Config { self.background_color = Some(color); self } + + /// Sets the menu the window will use. This will override the default menu bar. + /// + /// > Note: A default menu bar will be enabled unless the menu is overridden or set to `None`. + #[allow(unused)] + pub fn with_menu(mut self, menu: impl Into>) -> Self { + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + self.menu = menu.into(); + } + self + } } impl Default for Config { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 6305b2729..71d41ed37 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -35,10 +35,13 @@ pub use tao::dpi::{LogicalPosition, LogicalSize}; pub use tao::event::WindowEvent; pub use tao::window::WindowBuilder; pub use wry; +// Reexport muda only if we are on desktop platforms that support menus +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub use muda; // Public exports pub use assets::AssetRequest; -pub use config::{Config, WindowCloseBehaviour}; +pub use config::{Config, WindowCloseBehavior}; pub use desktop_context::{window, DesktopContext, DesktopService}; pub use event_handlers::WryEventHandler; pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs index 731fb6a68..eb02cce4b 100644 --- a/packages/desktop/src/menubar.rs +++ b/packages/desktop/src/menubar.rs @@ -1,31 +1,39 @@ -use std::any::Any; - use tao::window::Window; +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub type DioxusMenu = muda::Menu; +#[cfg(any(target_os = "ios", target_os = "android"))] +pub type DioxusMenu = (); + +/// Initializes the menu bar for the window. #[allow(unused)] -pub fn build_menu(window: &Window, default_menu_bar: bool) -> Option> { +pub fn init_menu_bar(menu: &DioxusMenu, window: &Window) { #[cfg(not(any(target_os = "ios", target_os = "android")))] { - return Some(Box::new(impl_::build_menu_bar(default_menu_bar, window)) as Box); + desktop_platforms::init_menu_bar(menu, window); } +} - None +/// Creates a standard menu bar depending on the users platform. It may be used as a starting point +/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). +/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. +/// > The menu bar differs per platform because of constraints introduced +/// > by [`MenuItem`](tao::menu::MenuItem). +#[allow(unused)] +pub fn default_menu_bar() -> DioxusMenu { + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + desktop_platforms::default_menu_bar() + } } #[cfg(not(any(target_os = "ios", target_os = "android")))] -mod impl_ { +mod desktop_platforms { use super::*; use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; - /// Builds a standard menu bar depending on the users platform. It may be used as a starting point - /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). - /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. - /// > The menu bar differs per platform because of constraints introduced - /// > by [`MenuItem`](tao::menu::MenuItem). #[allow(unused)] - pub fn build_menu_bar(default: bool, window: &Window) -> Menu { - let menu = Menu::new(); - + pub fn init_menu_bar(menu: &Menu, window: &Window) { #[cfg(target_os = "windows")] { use tao::platform::windows::WindowExtWindows; @@ -44,53 +52,54 @@ mod impl_ { use tao::platform::macos::WindowExtMacOS; menu.init_for_nsapp(); } + } - if default { - // since it is uncommon on windows to have an "application menu" - // we add a "window" menu to be more consistent across platforms with the standard menu - let window_menu = Submenu::new("Window", true); - window_menu - .append_items(&[ - &PredefinedMenuItem::fullscreen(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::hide(None), - &PredefinedMenuItem::hide_others(None), - &PredefinedMenuItem::show_all(None), - &PredefinedMenuItem::maximize(None), - &PredefinedMenuItem::minimize(None), - &PredefinedMenuItem::close_window(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::quit(None), - ]) - .unwrap(); + pub fn default_menu_bar() -> Menu { + let menu = Menu::new(); + // since it is uncommon on windows to have an "application menu" + // we add a "window" menu to be more consistent across platforms with the standard menu + let window_menu = Submenu::new("Window", true); + window_menu + .append_items(&[ + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::close_window(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]) + .unwrap(); - let edit_menu = Submenu::new("Edit", true); - edit_menu - .append_items(&[ - &PredefinedMenuItem::undo(None), - &PredefinedMenuItem::redo(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::cut(None), - &PredefinedMenuItem::copy(None), - &PredefinedMenuItem::paste(None), - &PredefinedMenuItem::separator(), - &PredefinedMenuItem::select_all(None), - ]) - .unwrap(); + let edit_menu = Submenu::new("Edit", true); + edit_menu + .append_items(&[ + &PredefinedMenuItem::undo(None), + &PredefinedMenuItem::redo(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::cut(None), + &PredefinedMenuItem::copy(None), + &PredefinedMenuItem::paste(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::select_all(None), + ]) + .unwrap(); - let help_menu = Submenu::new("Help", true); - help_menu - .append_items(&[&MenuItem::new("Toggle Developer Tools", true, None)]) - .unwrap(); + let help_menu = Submenu::new("Help", true); + help_menu + .append_items(&[&MenuItem::new("Toggle Developer Tools", true, None)]) + .unwrap(); - menu.append_items(&[&window_menu, &edit_menu, &help_menu]) - .unwrap(); + menu.append_items(&[&window_menu, &edit_menu, &help_menu]) + .unwrap(); - #[cfg(target_os = "macos")] - { - window_menu.set_as_windows_menu_for_nsapp(); - help_menu.set_as_help_menu_for_nsapp(); - } + #[cfg(target_os = "macos")] + { + window_menu.set_as_windows_menu_for_nsapp(); + help_menu.set_as_help_menu_for_nsapp(); } menu diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index baf3479ff..d32bfe255 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,3 +1,4 @@ +use crate::menubar::DioxusMenu; use crate::{ app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider, file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config, @@ -6,7 +7,7 @@ use crate::{ use dioxus_core::{ScopeId, VirtualDom}; use dioxus_html::prelude::EvalProvider; use futures_util::{pin_mut, FutureExt}; -use std::{any::Any, rc::Rc, task::Waker}; +use std::{rc::Rc, task::Waker}; use wry::{RequestAsyncResponder, WebContext, WebViewBuilder}; pub(crate) struct WebviewInstance { @@ -19,11 +20,11 @@ pub(crate) struct WebviewInstance { _web_context: WebContext, // Same with the menu. - // Currently it's a box because 1) we don't touch it and 2) we support a number of platforms + // Currently it's a DioxusMenu because 1) we don't touch it and 2) we support a number of platforms // like ios where muda does not give us a menu type. It sucks but alas. // // This would be a good thing for someone looking to contribute to fix. - _menu: Option>, + _menu: Option, } impl WebviewInstance { @@ -165,9 +166,11 @@ impl WebviewInstance { let webview = webview.build().unwrap(); - // TODO: allow users to specify their own menubars, again :/ let menu = if cfg!(not(any(target_os = "android", target_os = "ios"))) { - crate::menubar::build_menu(&window, cfg.enable_default_menu_bar) + if let Some(menu) = &cfg.menu { + crate::menubar::init_menu_bar(menu, &window); + } + cfg.menu } else { None }; @@ -221,7 +224,7 @@ impl WebviewInstance { } } - #[allow(unused)] + #[cfg(all(feature = "hot-reload", debug_assertions))] pub fn kick_stylsheets(&self) { // run eval in the webview to kick the stylesheets by appending a query string // we should do something less clunky than this diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 56d98481b..2e8c665ef 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -20,14 +20,14 @@ dioxus-signals = { workspace = true, optional = true } dioxus-router = { workspace = true, optional = true } dioxus-web = { workspace = true, optional = true } dioxus-mobile = { workspace = true, optional = true } -dioxus-desktop = { workspace = true, optional = true } +dioxus-desktop = { workspace = true, default-features = true, optional = true } dioxus-fullstack = { workspace = true, optional = true } dioxus-liveview = { workspace = true, optional = true } dioxus-ssr ={ workspace = true, optional = true } serde = { version = "1.0.136", optional = true } -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +[target.'cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))'.dependencies] dioxus-hot-reload = { workspace = true, optional = true } [features] @@ -44,7 +44,7 @@ router = ["dioxus-router"] # Platforms fullstack = ["dioxus-fullstack", "dioxus-config-macro/fullstack", "serde", "dioxus-router?/fullstack"] desktop = ["dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"] -mobile = ["dioxus-mobile", "dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"] +mobile = ["dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"] web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus-router?/web"] ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"] liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 5fcbddd64..dfd199bdd 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -151,7 +151,7 @@ mod current_platform { pub use dioxus_desktop::launch::*; #[cfg(all(feature = "mobile", not(feature = "fullstack")))] - pub use dioxus_desktop::launch::*; + pub use dioxus_mobile::launch::*; #[cfg(feature = "fullstack")] pub use dioxus_fullstack::launch::*; diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index b7afd3be0..188a30311 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -63,7 +63,10 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(feature = "html")))] pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes}; - #[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))] + #[cfg(all( + not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")), + feature = "hot-reload" + ))] #[cfg_attr(docsrs, doc(cfg(feature = "hot-reload")))] pub use dioxus_hot_reload::{self, hot_reload_init}; @@ -100,7 +103,7 @@ pub use dioxus_desktop as desktop; #[cfg(feature = "mobile")] #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))] -pub use dioxus_desktop as mobile; +pub use dioxus_mobile as mobile; #[cfg(feature = "liveview")] #[cfg_attr(docsrs, doc(cfg(feature = "liveview")))]