Add a menu bar option to the desktop config (#2107)

* add an option to set a custom menu in the desktop config

* Fix rename issue

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan Almloff 2024-03-19 00:36:47 -05:00 committed by GitHub
parent 10d361a44e
commit d442dac168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 152 additions and 95 deletions

View file

@ -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" }

35
examples/custom_menu.rs Normal file
View file

@ -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!"}
}

View file

@ -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)
}

View file

@ -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<WindowId, WebviewInstance>,
/// 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<Vec<WebviewInstance>>,
@ -58,7 +56,7 @@ impl App {
let event_loop = EventLoopBuilder::<UserWindowEvent>::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

View file

@ -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<DioxusMenu>,
pub(crate) protocols: Vec<WryProtocol>,
pub(crate) pre_rendered: Option<String>,
pub(crate) disable_context_menu: bool,
@ -26,8 +29,7 @@ pub struct Config {
pub(crate) custom_index: Option<String>,
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<PathBuf>) -> 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<String>) -> 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<Option<DioxusMenu>>) -> Self {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
self.menu = menu.into();
}
self
}
}
impl Default for Config {

View file

@ -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};

View file

@ -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<Box<dyn Any>> {
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<dyn Any>);
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

View file

@ -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<dyn any> 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<Box<dyn Any>>,
_menu: Option<DioxusMenu>,
}
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

View file

@ -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"]

View file

@ -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::*;

View file

@ -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")))]