diff --git a/CHANGELOG.md b/CHANGELOG.md index 626e447d02..fb2b5a70a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa - Timer events will only be delivered to the widgets that requested them. ([#831] by [@sjoshid]) - `Event::Wheel` now contains a `MouseEvent` structure. ([#895] by [@teddemunnik]) - `AppDelegate::command` now receives a `Target` instead of a `&Target`. ([#909] by [@xStrom]) +- `SHOW_WINDOW` and `OPEN_WINDOW` no longer require a `WindowId` as payload, but they must `Target` the correct window. ([#???] by [@finnerale]) ### Deprecated diff --git a/druid/examples/blocking_function.rs b/druid/examples/blocking_function.rs index 0caa288dc5..2700de6fd4 100644 --- a/druid/examples/blocking_function.rs +++ b/druid/examples/blocking_function.rs @@ -23,9 +23,9 @@ use druid::{ use druid::widget::{Button, Either, Flex, Label}; -const START_SLOW_FUNCTION: Selector = Selector::new("start_slow_function"); +const START_SLOW_FUNCTION: Selector = Selector::new("start_slow_function"); -const FINISH_SLOW_FUNCTION: Selector = Selector::new("finish_slow_function"); +const FINISH_SLOW_FUNCTION: Selector = Selector::new("finish_slow_function"); struct Delegate { eventsink: ExtEventSink, @@ -61,20 +61,15 @@ impl AppDelegate for Delegate { data: &mut AppState, _env: &Env, ) -> bool { - match cmd.selector { - START_SLOW_FUNCTION => { - data.processing = true; - wrapped_slow_function(self.eventsink.clone(), data.value); - true - } - FINISH_SLOW_FUNCTION => { - data.processing = false; - let number = cmd.get_object::().expect("api violation"); - data.value = *number; - true - } - _ => true, + if cmd.is(START_SLOW_FUNCTION) { + data.processing = true; + wrapped_slow_function(self.eventsink.clone(), data.value); } + if let Some(number) = cmd.get(FINISH_SLOW_FUNCTION) { + data.processing = false; + data.value = *number; + } + true } } diff --git a/druid/examples/ext_event.rs b/druid/examples/ext_event.rs index 6ef2c9450a..ab57e80916 100644 --- a/druid/examples/ext_event.rs +++ b/druid/examples/ext_event.rs @@ -22,7 +22,7 @@ use druid::kurbo::RoundedRect; use druid::widget::prelude::*; use druid::{AppLauncher, Color, Data, LocalizedString, Rect, Selector, WidgetExt, WindowDesc}; -const SET_COLOR: Selector = Selector::new("event-example.set-color"); +const SET_COLOR: Selector = Selector::new("event-example.set-color"); /// A widget that displays a color. struct ColorWell; @@ -53,8 +53,8 @@ impl ColorWell { impl Widget for ColorWell { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut MyColor, _env: &Env) { match event { - Event::Command(cmd) if cmd.selector == SET_COLOR => { - data.0 = cmd.get_object::().unwrap().clone(); + Event::Command(cmd) if cmd.is(SET_COLOR) => { + data.0 = cmd.get(SET_COLOR).unwrap().clone(); ctx.request_paint(); } _ => (), diff --git a/druid/examples/identity.rs b/druid/examples/identity.rs index 3c8c863eee..7c080b3344 100644 --- a/druid/examples/identity.rs +++ b/druid/examples/identity.rs @@ -40,7 +40,7 @@ use druid::{ const CYCLE_DURATION: Duration = Duration::from_millis(100); -const FREEZE_COLOR: Selector = Selector::new("identity-example.freeze-color"); +const FREEZE_COLOR: Selector = Selector::new("identity-example.freeze-color"); const UNFREEZE_COLOR: Selector = Selector::new("identity-example.unfreeze-color"); /// Honestly: it's just a color in fancy clothing. @@ -114,15 +114,10 @@ impl Widget for ColorWell { self.token = ctx.request_timer(CYCLE_DURATION); } - Event::Command(cmd) if cmd.selector == FREEZE_COLOR => { - self.frozen = cmd - .get_object::() - .ok() - .cloned() - .expect("payload is always a Color") - .into(); + Event::Command(cmd) if cmd.is(FREEZE_COLOR) => { + self.frozen = cmd.get(FREEZE_COLOR).cloned(); } - Event::Command(cmd) if cmd.selector == UNFREEZE_COLOR => self.frozen = None, + Event::Command(cmd) if cmd.is(UNFREEZE_COLOR) => self.frozen = None, _ => (), } } diff --git a/druid/examples/multiwin.rs b/druid/examples/multiwin.rs index 46094c4e8b..dda2b01be6 100644 --- a/druid/examples/multiwin.rs +++ b/druid/examples/multiwin.rs @@ -23,7 +23,7 @@ use druid::{ use log::info; -const MENU_COUNT_ACTION: Selector = Selector::new("menu-count-action"); +const MENU_COUNT_ACTION: Selector = Selector::new("menu-count-action"); const MENU_INCREMENT_ACTION: Selector = Selector::new("menu-increment-action"); const MENU_DECREMENT_ACTION: Selector = Selector::new("menu-decrement-action"); const MENU_SWITCH_GLOW_ACTION: Selector = Selector::new("menu-switch-glow"); @@ -56,7 +56,7 @@ trait EventCtxExt { impl EventCtxExt for EventCtx<'_> { fn set_menu(&mut self, menu: MenuDesc) { - let cmd = Command::new(druid::commands::SET_MENU, menu); + let cmd = Command::new(druid::commands::SET_MENU, menu.into_app_state_menu_desc()); let target = self.window_id(); self.submit_command(cmd, target); } @@ -155,7 +155,10 @@ impl AppDelegate for Delegate { match event { Event::MouseDown(ref mouse) if mouse.button.is_right() => { let menu = ContextMenu::new(make_context_menu::(), mouse.pos); - let cmd = Command::new(druid::commands::SHOW_CONTEXT_MENU, menu); + let cmd = Command::new( + druid::commands::SHOW_CONTEXT_MENU, + menu.into_app_state_context_menu(), + ); ctx.submit_command(cmd, Target::Window(window_id)); None } @@ -171,39 +174,40 @@ impl AppDelegate for Delegate { data: &mut State, _env: &Env, ) -> bool { - match (target, &cmd.selector) { - (_, &sys_cmds::NEW_FILE) => { - let new_win = WindowDesc::new(ui_builder) + match target { + _ if cmd.is(sys_cmds::NEW_FILE) => { + let new_win = WindowDesc::::new(ui_builder) .menu(make_menu(data)) .window_size((data.selected as f64 * 100.0 + 300.0, 500.0)); - let command = Command::one_shot(sys_cmds::NEW_WINDOW, new_win); + let command = + Command::one_shot(sys_cmds::NEW_WINDOW, new_win.into_app_state_menu_desc()); ctx.submit_command(command, Target::Global); false } - (Target::Window(id), &MENU_COUNT_ACTION) => { - data.selected = *cmd.get_object().unwrap(); + Target::Window(id) if cmd.is(MENU_COUNT_ACTION) => { + data.selected = *cmd.get(MENU_COUNT_ACTION).unwrap(); let menu = make_menu::(data); - let cmd = Command::new(druid::commands::SET_MENU, menu); + let cmd = Command::new(sys_cmds::SET_MENU, menu.into_app_state_menu_desc()); ctx.submit_command(cmd, id); false } // wouldn't it be nice if a menu (like a button) could just mutate state // directly if desired? - (Target::Window(id), &MENU_INCREMENT_ACTION) => { + Target::Window(id) if cmd.is(MENU_INCREMENT_ACTION) => { data.menu_count += 1; let menu = make_menu::(data); - let cmd = Command::new(druid::commands::SET_MENU, menu); + let cmd = Command::new(sys_cmds::SET_MENU, menu.into_app_state_menu_desc()); ctx.submit_command(cmd, id); false } - (Target::Window(id), &MENU_DECREMENT_ACTION) => { + Target::Window(id) if cmd.is(MENU_DECREMENT_ACTION) => { data.menu_count = data.menu_count.saturating_sub(1); let menu = make_menu::(data); - let cmd = Command::new(druid::commands::SET_MENU, menu); + let cmd = Command::new(sys_cmds::SET_MENU, menu.into_app_state_menu_desc()); ctx.submit_command(cmd, id); false } - (_, &MENU_SWITCH_GLOW_ACTION) => { + _ if cmd.is(MENU_SWITCH_GLOW_ACTION) => { data.glow_hot = !data.glow_hot; false } @@ -220,6 +224,7 @@ impl AppDelegate for Delegate { ) { info!("Window added, id: {:?}", id); } + fn window_removed( &mut self, id: WindowId, diff --git a/druid/examples/open_save.rs b/druid/examples/open_save.rs index 571a3a1557..82b9a067e3 100644 --- a/druid/examples/open_save.rs +++ b/druid/examples/open_save.rs @@ -14,7 +14,7 @@ use druid::widget::{Align, Button, Flex, TextBox}; use druid::{ - AppDelegate, AppLauncher, Command, DelegateCtx, Env, FileDialogOptions, FileInfo, FileSpec, + commands, AppDelegate, AppLauncher, Command, DelegateCtx, Env, FileDialogOptions, FileSpec, LocalizedString, Target, Widget, WindowDesc, }; @@ -77,30 +77,24 @@ impl AppDelegate for Delegate { data: &mut String, _env: &Env, ) -> bool { - match cmd.selector { - druid::commands::SAVE_FILE => { - if let Ok(file_info) = cmd.get_object::() { - if let Err(e) = std::fs::write(file_info.path(), &data[..]) { - println!("Error writing file: {}", e); - } - } - true + if let Some(Some(file_info)) = cmd.get(commands::SAVE_FILE) { + if let Err(e) = std::fs::write(file_info.path(), &data[..]) { + println!("Error writing file: {}", e); } - druid::commands::OPEN_FILE => { - if let Ok(file_info) = cmd.get_object::() { - match std::fs::read_to_string(file_info.path()) { - Ok(s) => { - let first_line = s.lines().next().unwrap_or(""); - *data = first_line.to_owned(); - } - Err(e) => { - println!("Error opening file: {}", e); - } - } + return true; + } + if let Some(file_info) = cmd.get(commands::OPEN_FILE) { + match std::fs::read_to_string(file_info.path()) { + Ok(s) => { + let first_line = s.lines().next().unwrap_or(""); + *data = first_line.to_owned(); + } + Err(e) => { + println!("Error opening file: {}", e); } - true } - _ => false, + return true; } + false } } diff --git a/druid/src/app.rs b/druid/src/app.rs index db6c5e8cb4..4ca266ac38 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -21,8 +21,10 @@ use crate::widget::LabelText; use crate::win_handler::{AppHandler, AppState}; use crate::window::WindowId; use crate::{ - theme, AppDelegate, Data, DruidHandler, Env, LocalizedString, MenuDesc, Widget, WidgetExt, + command::AppStateTypeError, theme, AppDelegate, Data, DruidHandler, Env, LocalizedString, + MenuDesc, Widget, WidgetExt, }; +use std::any::{self, Any}; /// A function that modifies the initial environment. type EnvSetupFn = dyn FnOnce(&mut Env, &T); @@ -54,6 +56,43 @@ pub struct WindowDesc { pub id: WindowId, } +/// A description of a window to be instantiated. The user has to guarantee that this +/// represents a `WindowDesc` where `T` is the users `AppState`. +/// +/// This includes a function that can build the root widget, as well as other +/// window properties such as the title. +pub struct AppStateWindowDesc { + inner: Box, + type_name: &'static str, +} + +impl WindowDesc { + /// This turns a typed `WindowDesc` into an untyped `AppStateWindowDesc`. + /// Doing so allows sending `WindowDesc` through `Command`s. + /// It is up to you, to ensure that this `T` represents your application + /// state that you passed to `AppLauncher::launch`. + pub fn into_app_state_menu_desc(self) -> AppStateWindowDesc { + AppStateWindowDesc { + inner: Box::new(self), + type_name: any::type_name::(), + } + } +} + +impl AppStateWindowDesc { + pub(crate) fn realize(self) -> Result, AppStateTypeError> { + let inner: Result>, _> = self.inner.downcast(); + if let Ok(inner) = inner { + Ok(*inner) + } else { + Err(AppStateTypeError::new( + any::type_name::>(), + self.type_name, + )) + } + } +} + impl AppLauncher { /// Create a new `AppLauncher` with the provided window. pub fn with_window(window: WindowDesc) -> Self { diff --git a/druid/src/command.rs b/druid/src/command.rs index 2e2c516a53..048fe72f7b 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -15,10 +15,35 @@ //! Custom commands. use std::any::Any; -use std::sync::{Arc, Mutex}; +use std::{ + marker::PhantomData, + sync::{Arc, Mutex}, +}; use crate::{WidgetId, WindowId}; +/// An untyped identifier for a `Selector`. +pub type SelectorSymbol = &'static str; + +/// An identifier for a particular command. +/// +/// This should be a unique string identifier. Certain `Selector`s are defined +/// by druid, and have special meaning to the framework; these are listed in the +/// [`druid::commands`] module. +/// +/// [`druid::commands`]: commands/index.html +#[derive(Debug, PartialEq, Eq)] +pub struct Selector(SelectorSymbol, PhantomData<*const T>); + +// This has do be done explicitly, to avoid the Copy bound on `T`. +// See https://doc.rust-lang.org/std/marker/trait.Copy.html#how-can-i-implement-copy . +impl Copy for Selector {} +impl Clone for Selector { + fn clone(&self) -> Self { + *self + } +} + /// An identifier for a particular command. /// /// This should be a unique string identifier. Certain `Selector`s are defined @@ -26,8 +51,33 @@ use crate::{WidgetId, WindowId}; /// [`druid::commands`] module. /// /// [`druid::commands`]: commands/index.html -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Selector(&'static str); +#[derive(Debug, PartialEq, Eq)] +pub struct OneShotSelector(SelectorSymbol, PhantomData<*const T>); + +// This has do be done explicitly, to avoid the Copy bound on `T`. +// See https://doc.rust-lang.org/std/marker/trait.Copy.html#how-can-i-implement-copy . +impl Copy for OneShotSelector {} +impl Clone for OneShotSelector { + fn clone(&self) -> Self { + *self + } +} + +pub trait AnySelector { + fn symbol(self) -> SelectorSymbol; +} + +impl AnySelector for Selector { + fn symbol(self) -> SelectorSymbol { + self.0 + } +} + +impl AnySelector for OneShotSelector { + fn symbol(self) -> SelectorSymbol { + self.0 + } +} /// An arbitrary command. /// @@ -54,7 +104,7 @@ pub struct Selector(&'static str); /// let rows = vec![1, 3, 10, 12]; /// let command = Command::new(selector, rows); /// -/// assert_eq!(command.get_object(), Ok(&vec![1, 3, 10, 12])); +/// assert_eq!(command.get(selector), Some(&vec![1, 3, 10, 12])); /// ``` /// /// [`Command::new`]: #method.new @@ -62,9 +112,8 @@ pub struct Selector(&'static str); /// [`Selector`]: struct.Selector.html #[derive(Debug, Clone)] pub struct Command { - /// The command's `Selector`. - pub selector: Selector, - object: Option, + selector: SelectorSymbol, + object: Arg, } #[derive(Debug, Clone)] @@ -73,19 +122,42 @@ enum Arg { OneShot(Arc>>>), } -/// Errors that can occur when attempting to retrieve the a command's argument. +/// Errors that can occur when attempting to retrieve the `OneShotCommand`s argument. #[derive(Debug, Clone, PartialEq)] pub enum ArgumentError { - /// The command did not have an argument. - NoArgument, - /// The argument was expected to be reusable and wasn't, or vice-versa. - WrongVariant, - /// The argument could not be downcast to the specified type. - IncorrectType, + /// The command represented a different selector. + WrongSelector, /// The one-shot argument has already been taken. Consumed, } +/// This error can occur when wrongly promising that a type erased +/// variant of some generic item represents the application state. +/// Examples are `MenuDesc` and `AppStateMenuDesc`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AppStateTypeError { + expected: &'static str, + found: &'static str, +} + +impl AppStateTypeError { + pub(crate) fn new(expected: &'static str, found: &'static str) -> Self { + Self { expected, found } + } +} + +impl std::fmt::Display for AppStateTypeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Promise to represent the app state was not meet. Expected {} but got {}.", + self.expected, self.found + ) + } +} + +impl std::error::Error for AppStateTypeError {} + /// The target of a command. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Target { @@ -104,7 +176,12 @@ pub enum Target { /// /// [`Command`]: ../struct.Command.html pub mod sys { - use super::Selector; + use super::{OneShotSelector, Selector}; + use crate::{ + app::AppStateWindowDesc, + menu::{AppStateContextMenu, AppStateMenuDesc}, + FileDialogOptions, FileInfo, + }; /// Quit the running application. This command is handled by the druid library. pub const QUIT_APP: Selector = Selector::new("druid-builtin.quit-app"); @@ -116,10 +193,14 @@ pub mod sys { pub const HIDE_OTHERS: Selector = Selector::new("druid-builtin.menu-hide-others"); /// The selector for a command to create a new window. - pub const NEW_WINDOW: Selector = Selector::new("druid-builtin.new-window"); + pub const NEW_WINDOW: OneShotSelector = + OneShotSelector::new("druid-builtin.new-window"); - /// The selector for a command to close a window. The command's argument - /// should be the id of the window to close. + /// The selector for a command to close a window. + /// + /// The command must target a specific window. + /// When calling `submit_command` on a `Widget`s context, passing `None` as target + /// will automatically target the window containing the widget. pub const CLOSE_WINDOW: Selector = Selector::new("druid-builtin.close-window"); /// Close all windows. @@ -127,20 +208,23 @@ pub mod sys { /// The selector for a command to bring a window to the front, and give it focus. /// - /// The command's argument should be the id of the target window. + /// The command must target a specific window. + /// When calling `submit_command` on a `Widget`s context, passing `None` as target + /// will automatically target the window containing the widget. pub const SHOW_WINDOW: Selector = Selector::new("druid-builtin.show-window"); - /// Display a context (right-click) menu. The argument must be the [`ContextMenu`]. - /// object to be displayed. + /// Display a context (right-click) menu. + /// An `AppStateContextMenu` can be obtained using `ContextMenu::into_app_state_context_menu`. /// /// [`ContextMenu`]: ../struct.ContextMenu.html - pub const SHOW_CONTEXT_MENU: Selector = Selector::new("druid-builtin.show-context-menu"); + pub const SHOW_CONTEXT_MENU: Selector = + Selector::new("druid-builtin.show-context-menu"); - /// The selector for a command to set the window's menu. The argument should - /// be a [`MenuDesc`] object. + /// The selector for a command to set the window's menu. + /// An `AppStateMenuDesc` can be obtained using `MenuDesc::into_app_state_menu_desc`. /// /// [`MenuDesc`]: ../struct.MenuDesc.html - pub const SET_MENU: Selector = Selector::new("druid-builtin.set-menu"); + pub const SET_MENU: Selector = Selector::new("druid-builtin.set-menu"); /// Show the application preferences. pub const SHOW_PREFERENCES: Selector = Selector::new("druid-builtin.menu-show-preferences"); @@ -157,33 +241,30 @@ pub mod sys { /// System command. A file picker dialog will be shown to the user, and an /// [`OPEN_FILE`] command will be sent if a file is chosen. /// - /// The argument should be a [`FileDialogOptions`] struct. - /// /// [`OPEN_FILE`]: constant.OPEN_FILE.html /// [`FileDialogOptions`]: ../struct.FileDialogOptions.html - pub const SHOW_OPEN_PANEL: Selector = Selector::new("druid-builtin.menu-file-open"); + pub const SHOW_OPEN_PANEL: Selector = + Selector::new("druid-builtin.menu-file-open"); - /// Open a file. - /// - /// The argument must be a [`FileInfo`] object for the file to be opened. + /// Commands to open a file, must be handled by the application. /// /// [`FileInfo`]: ../struct.FileInfo.html - pub const OPEN_FILE: Selector = Selector::new("druid-builtin.open-file-path"); + pub const OPEN_FILE: Selector = Selector::new("druid-builtin.open-file-path"); - /// Special command. When issued, the system will show the 'save as' panel, + /// Special command. When issued by the application, the system will show the 'save as' panel, /// and if a path is selected the system will issue a [`SAVE_FILE`] command /// with the selected path as the argument. /// - /// The argument should be a [`FileDialogOptions`] object. - /// /// [`SAVE_FILE`]: constant.SAVE_FILE.html /// [`FileDialogOptions`]: ../struct.FileDialogOptions.html - pub const SHOW_SAVE_PANEL: Selector = Selector::new("druid-builtin.menu-file-save-as"); + pub const SHOW_SAVE_PANEL: Selector = + Selector::new("druid-builtin.menu-file-save-as"); - /// Save the current file. + /// Commands to save a file, must be handled by the application. /// - /// The argument, if present, should be the path where the file should be saved. - pub const SAVE_FILE: Selector = Selector::new("druid-builtin.menu-file-save"); + /// If it carries `Some`, then the application should save to that file and store the `FileInfo` for future use. + /// If it carries `None`, the application should have received `Some` before and use the stored `FileInfo`. + pub const SAVE_FILE: Selector> = Selector::new("druid-builtin.menu-file-save"); /// Show the print-setup window. pub const PRINT_SETUP: Selector = Selector::new("druid-builtin.menu-file-print-setup"); @@ -210,23 +291,37 @@ pub mod sys { pub const REDO: Selector = Selector::new("druid-builtin.menu-redo"); } -impl Selector { +impl Selector { /// A selector that does nothing. - pub const NOOP: Selector = Selector::new(""); + pub const fn noop() -> Self { + Selector::new("") + } /// Create a new `Selector` with the given string. - pub const fn new(s: &'static str) -> Selector { - Selector(s) + pub const fn new(s: &'static str) -> Self { + Selector(s, PhantomData) + } +} + +impl OneShotSelector { + /// A selector that does nothing. + pub const fn noop() -> Self { + OneShotSelector::new("") + } + + /// Create a new `Selector` with the given string. + pub const fn new(s: &'static str) -> Self { + OneShotSelector(s, PhantomData) } } impl Command { /// Create a new `Command` with an argument. If you do not need /// an argument, `Selector` implements `Into`. - pub fn new(selector: Selector, arg: impl Any) -> Self { + pub fn new(selector: Selector, arg: T) -> Self { Command { - selector, - object: Some(Arg::Reusable(Arc::new(arg))), + selector: selector.symbol(), + object: Arg::Reusable(Arc::new(arg)), } } @@ -237,85 +332,106 @@ impl Command { /// [`take_object`]. /// /// [`take_object`]: #method.take_object - pub fn one_shot(selector: Selector, arg: impl Any) -> Self { + pub fn one_shot(selector: OneShotSelector, arg: T) -> Self { Command { - selector, - object: Some(Arg::OneShot(Arc::new(Mutex::new(Some(Box::new(arg)))))), + selector: selector.symbol(), + object: Arg::OneShot(Arc::new(Mutex::new(Some(Box::new(arg))))), } } /// Used to create a command from the types sent via an `ExtEventSink`. - pub(crate) fn from_ext(selector: Selector, object: Option>) -> Self { - let object: Option> = object.map(|obj| obj as Box); - let object = object.map(|o| Arg::Reusable(o.into())); + pub(crate) fn from_ext(selector: SelectorSymbol, object: Box) -> Self { + let object: Box = object; + let object = Arg::Reusable(object.into()); Command { selector, object } } + pub fn is(&self, selector: impl AnySelector) -> bool { + self.selector == selector.symbol() + } + /// Return a reference to this `Command`'s object, if it has one. /// /// This only works for 'reusable' commands; it does not work for commands /// created with [`one_shot`]. /// /// [`one_shot`]: #method.one_shot - pub fn get_object(&self) -> Result<&T, ArgumentError> { - match self.object.as_ref() { - Some(Arg::Reusable(o)) => o.downcast_ref().ok_or(ArgumentError::IncorrectType), - Some(Arg::OneShot(_)) => Err(ArgumentError::WrongVariant), - None => Err(ArgumentError::NoArgument), + pub fn get(&self, selector: Selector) -> Option<&T> { + if self.selector != selector.symbol() { + return None; + } + match &self.object { + Arg::Reusable(obj) => Some( + obj.downcast_ref() + .expect("Reusable command had wrong payload type."), + ), + Arg::OneShot(_) => panic!("Reusable command {} carried OneShot argument.", selector), } } /// Attempt to take the object of a [`one-shot`] command. /// /// [`one-shot`]: #method.one_shot - pub fn take_object(&self) -> Result, ArgumentError> { - match self.object.as_ref() { - Some(Arg::Reusable(_)) => Err(ArgumentError::WrongVariant), - Some(Arg::OneShot(inner)) => { + pub fn take(&self, selector: OneShotSelector) -> Result, ArgumentError> { + if self.selector != selector.symbol() { + return Err(ArgumentError::WrongSelector); + } + match &self.object { + Arg::Reusable(_) => panic!("OneShot command {} carried Reusable argument.", selector), + Arg::OneShot(inner) => { let obj = inner .lock() .unwrap() .take() .ok_or(ArgumentError::Consumed)?; + #[allow(clippy::match_wild_err_arm)] match obj.downcast::() { Ok(obj) => Ok(obj), - Err(obj) => { - inner.lock().unwrap().replace(obj); - Err(ArgumentError::IncorrectType) + Err(_) => { + panic!("OneShot command had wrong payload type."); } } } - None => Err(ArgumentError::NoArgument), } } } -impl From for Command { - fn from(selector: Selector) -> Command { +impl From> for Command { + fn from(selector: Selector<()>) -> Command { Command { - selector, - object: None, + selector: selector.symbol(), + object: Arg::Reusable(Arc::new(())), } } } -impl std::fmt::Display for Selector { +impl std::fmt::Display for Selector { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Selector('{}')", self.0) + write!( + f, + "Selector(\"{}\", {})", + self.0, + std::any::type_name::() + ) + } +} + +impl std::fmt::Display for OneShotSelector { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "OneShotSelector(\"{}\", {})", + self.0, + std::any::type_name::() + ) } } impl std::fmt::Display for ArgumentError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - ArgumentError::NoArgument => write!(f, "Command has no argument"), - ArgumentError::IncorrectType => write!(f, "Downcast failed: wrong concrete type"), + ArgumentError::WrongSelector => write!(f, "Command had wrong selector"), ArgumentError::Consumed => write!(f, "One-shot command arguemnt already consumed"), - ArgumentError::WrongVariant => write!( - f, - "Incorrect access method for argument type; \ - check Command::one_shot docs for more detail." - ), } } } @@ -349,11 +465,13 @@ impl Into> for WidgetId { #[cfg(test)] mod tests { use super::*; + #[test] fn get_object() { - let sel = Selector::new("my-selector"); + let sel: Selector> = Selector::new("my-selector"); let objs = vec![0, 1, 2]; + // TODO: find out why this now wants a `.clone()` even tho `Selector` implements `Copy`. let command = Command::new(sel, objs); - assert_eq!(command.get_object(), Ok(&vec![0, 1, 2])); + assert_eq!(command.get(sel), Some(&vec![0, 1, 2])); } } diff --git a/druid/src/ext_event.rs b/druid/src/ext_event.rs index 3d1361b121..943f5bcb8e 100644 --- a/druid/src/ext_event.rs +++ b/druid/src/ext_event.rs @@ -20,9 +20,12 @@ use std::sync::{Arc, Mutex}; use crate::shell::IdleHandle; use crate::win_handler::EXT_EVENT_IDLE_TOKEN; -use crate::{Command, Selector, Target, WindowId}; +use crate::{ + command::{AnySelector, SelectorSymbol}, + Command, Selector, Target, WindowId, +}; -pub(crate) type ExtCommand = (Selector, Option>, Option); +pub(crate) type ExtCommand = (SelectorSymbol, Box, Option); /// A thing that can move into other threads and be used to submit commands back /// to the running application. @@ -103,19 +106,19 @@ impl ExtEventSink { /// [`Selector`]: struct.Selector.html pub fn submit_command( &self, - sel: Selector, - obj: impl Into>, + sel: Selector, + obj: T, target: impl Into>, ) -> Result<(), ExtEventError> { let target = target.into(); - let obj = obj.into().map(|o| Box::new(o) as Box); + let obj = Box::new(obj) as Box; if let Some(handle) = self.handle.lock().unwrap().as_mut() { handle.schedule_idle(EXT_EVENT_IDLE_TOKEN); } self.queue .lock() .map_err(|_| ExtEventError)? - .push_back((sel, obj, target)); + .push_back((sel.symbol(), obj, target)); Ok(()) } } diff --git a/druid/src/menu.rs b/druid/src/menu.rs index a66922330f..144a638ac5 100644 --- a/druid/src/menu.rs +++ b/druid/src/menu.rs @@ -105,11 +105,16 @@ //! [`Selector`]: ../struct.Selector.html //! [`SET_MENU`]: ../struct.Selector.html#associatedconstant.SET_MENU -use std::num::NonZeroU32; +use std::{ + any::{self, Any}, + num::NonZeroU32, +}; use crate::kurbo::Point; use crate::shell::{HotKey, KeyCompare, Menu as PlatformMenu, RawMods, SysMods}; -use crate::{commands, Command, Data, Env, KeyCode, LocalizedString, Selector}; +use crate::{ + command::AppStateTypeError, commands, Command, Data, Env, KeyCode, LocalizedString, Selector, +}; /// A platform-agnostic description of an application, window, or context /// menu. @@ -120,6 +125,35 @@ pub struct MenuDesc { items: Vec>, } +/// A platform-agnostic description of an application, window, or context +/// menu. The user has to guarantee that this represents a `MenuDesc` where `T` is the users +/// `AppState`. +pub struct AppStateMenuDesc { + inner: Box, + type_name: &'static str, +} + +impl MenuDesc { + /// This turns a typed `MenuDesc` into an untyped `AppStateMenuDesc`. + /// Doing so allows sending `MenuDesc` through `Command`s. + /// It is up to you, to ensure that this `T` represents your application + /// state that you passed to `AppLauncher::launch`. + pub fn into_app_state_menu_desc(self) -> AppStateMenuDesc { + AppStateMenuDesc { + inner: Box::new(self), + type_name: any::type_name::(), + } + } +} + +impl AppStateMenuDesc { + pub(crate) fn realize(&self) -> Result<&MenuDesc, AppStateTypeError> { + self.inner + .downcast_ref() + .ok_or_else(|| AppStateTypeError::new(any::type_name::>(), self.type_name)) + } +} + /// An item in a menu, which may be a normal item, a submenu, or a separator. #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] @@ -159,6 +193,35 @@ pub struct ContextMenu { pub(crate) location: Point, } +/// A platform-agnostic description of a context menu. +/// The user has to guarantee that this represents a `ContextMenu` where `T` is the users +/// `AppState`. +pub struct AppStateContextMenu { + inner: Box, + type_name: &'static str, +} + +impl ContextMenu { + /// This turns a typed `ContextMenu` into an untyped `AppStateContextMenu`. + /// Doing so allows sending `ContextMenu` through `Command`s. + /// It is up to you, to ensure that this `T` represents your application + /// state that you passed to `AppLauncher::launch`. + pub fn into_app_state_context_menu(self) -> AppStateContextMenu { + AppStateContextMenu { + inner: Box::new(self), + type_name: any::type_name::(), + } + } +} + +impl AppStateContextMenu { + pub(crate) fn realize(&self) -> Result<&ContextMenu, AppStateTypeError> { + self.inner.downcast_ref().ok_or_else(|| { + AppStateTypeError::new(any::type_name::>(), self.type_name) + }) + } +} + /// Uniquely identifies a menu item. /// /// On the druid-shell side, the id is represented as a u32. @@ -238,7 +301,7 @@ impl MenuDesc { /// Create a new menu with the given title. pub fn new(title: LocalizedString) -> Self { - let item = MenuItem::new(title, Selector::NOOP); + let item = MenuItem::new(title, Selector::noop()); MenuDesc { item, items: Vec::new(), @@ -268,9 +331,9 @@ impl MenuDesc { /// use druid::{Command, LocalizedString, MenuDesc, MenuItem, Selector}; /// /// let num_items: usize = 4; - /// const MENU_COUNT_ACTION: Selector = Selector::new("menu-count-action"); + /// const MENU_COUNT_ACTION: Selector = Selector::new("menu-count-action"); /// - /// let my_menu: MenuDesc = MenuDesc::empty() + /// let my_menu: MenuDesc = MenuDesc::empty() /// .append_iter(|| (0..num_items).map(|i| { /// MenuItem::new( /// LocalizedString::new("hello-counter").with_arg("count", move |_, _| i.into()), @@ -541,7 +604,7 @@ pub mod sys { pub fn open() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-open"), - commands::SHOW_OPEN_PANEL, + Command::new(commands::SHOW_OPEN_PANEL, Default::default()), ) .hotkey(RawMods::Ctrl, "o") } @@ -558,16 +621,16 @@ pub mod sys { pub fn save() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-save"), - commands::SAVE_FILE, + Command::new(commands::SAVE_FILE, None), ) .hotkey(RawMods::Ctrl, "s") } - /// The 'Save' menu item. + /// The 'Save...' menu item. pub fn save_ellipsis() -> MenuItem { MenuItem::new( - LocalizedString::new("common-menu-file-save"), - commands::SAVE_FILE, + LocalizedString::new("common-menu-file-save-ellipsis"), + Command::new(commands::SHOW_OPEN_PANEL, Default::default()), ) .hotkey(RawMods::Ctrl, "s") } @@ -576,7 +639,7 @@ pub mod sys { pub fn save_as() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-save-as"), - commands::SHOW_SAVE_PANEL, + Command::new(commands::SHOW_SAVE_PANEL, Default::default()), ) .hotkey(RawMods::CtrlShift, "s") } @@ -742,7 +805,7 @@ pub mod sys { pub fn open_file() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-open"), - commands::OPEN_FILE, + Command::new(commands::SHOW_OPEN_PANEL, Default::default()), ) .hotkey(RawMods::Meta, "o") } @@ -760,7 +823,7 @@ pub mod sys { pub fn save() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-save"), - commands::SAVE_FILE, + Command::new(commands::SAVE_FILE, None), ) .hotkey(RawMods::Meta, "s") } @@ -771,7 +834,7 @@ pub mod sys { pub fn save_ellipsis() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-save-ellipsis"), - commands::SAVE_FILE, + Command::new(commands::SHOW_SAVE_PANEL, Default::default()), ) .hotkey(RawMods::Meta, "s") } @@ -780,7 +843,7 @@ pub mod sys { pub fn save_as() -> MenuItem { MenuItem::new( LocalizedString::new("common-menu-file-save-as"), - commands::SHOW_SAVE_PANEL, + Command::new(commands::SHOW_SAVE_PANEL, Default::default()), ) .hotkey(RawMods::MetaShift, "s") } diff --git a/druid/src/tests/helpers.rs b/druid/src/tests/helpers.rs index 19d382fdbb..b3ef3899e8 100644 --- a/druid/src/tests/helpers.rs +++ b/druid/src/tests/helpers.rs @@ -214,7 +214,7 @@ impl ReplaceChild { impl Widget for ReplaceChild { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { if let Event::Command(cmd) = event { - if cmd.selector == REPLACE_CHILD { + if cmd.is(REPLACE_CHILD) { self.inner = WidgetPod::new((self.replacer)()); ctx.children_changed(); return; diff --git a/druid/src/tests/mod.rs b/druid/src/tests/mod.rs index 7552295f8c..2f6240039f 100644 --- a/druid/src/tests/mod.rs +++ b/druid/src/tests/mod.rs @@ -158,7 +158,7 @@ fn take_focus() { ModularWidget::new(inner) .event_fn(|_, ctx, event, _data, _env| { if let Event::Command(cmd) = event { - if cmd.selector == TAKE_FOCUS { + if cmd.is(TAKE_FOCUS) { ctx.request_focus(); } } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 08e9962abe..6a0987b428 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -53,7 +53,8 @@ pub struct TextBox { impl TextBox { /// Perform an `EditAction`. The payload *must* be an `EditAction`. - pub const PERFORM_EDIT: Selector = Selector::new("druid-builtin.textbox.perform-edit"); + pub const PERFORM_EDIT: Selector = + Selector::new("druid-builtin.textbox.perform-edit"); /// Create a new TextBox widget pub fn new() -> TextBox { @@ -281,22 +282,19 @@ impl Widget for TextBox { } Event::Command(ref cmd) if ctx.is_focused() - && (cmd.selector == crate::commands::COPY - || cmd.selector == crate::commands::CUT) => + && (cmd.is(crate::commands::COPY) || cmd.is(crate::commands::CUT)) => { if let Some(text) = data.slice(self.selection.range()) { Application::global().clipboard().put_string(text); } - if !self.selection.is_caret() && cmd.selector == crate::commands::CUT { + if !self.selection.is_caret() && cmd.is(crate::commands::CUT) { edit_action = Some(EditAction::Delete); } ctx.set_handled(); } - Event::Command(cmd) if cmd.selector == RESET_BLINK => self.reset_cursor_blink(ctx), - Event::Command(cmd) if cmd.selector == TextBox::PERFORM_EDIT => { - let edit = cmd - .get_object::() - .expect("PERFORM_EDIT contained non-edit payload"); + Event::Command(cmd) if cmd.is(RESET_BLINK) => self.reset_cursor_blink(ctx), + Event::Command(cmd) if cmd.is(TextBox::PERFORM_EDIT) => { + let edit = cmd.get(TextBox::PERFORM_EDIT).unwrap(); self.do_edit_action(edit.to_owned(), data); } Event::Paste(ref item) => { diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 6513bdbb48..6d5881871d 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -21,9 +21,7 @@ use std::rc::Rc; use crate::kurbo::{Rect, Size}; use crate::piet::Piet; -use crate::shell::{ - Application, FileDialogOptions, IdleToken, MouseEvent, WinHandler, WindowHandle, -}; +use crate::shell::{Application, IdleToken, MouseEvent, WinHandler, WindowHandle}; use crate::app_delegate::{AppDelegate, DelegateCtx}; use crate::core::CommandQueue; @@ -313,10 +311,11 @@ impl Inner { match target { Target::Window(id) => { // first handle special window-level events - match cmd.selector { - sys_cmd::SET_MENU => return self.set_menu(id, &cmd), - sys_cmd::SHOW_CONTEXT_MENU => return self.show_context_menu(id, &cmd), - _ => (), + if cmd.is(sys_cmd::SET_MENU) { + return self.set_menu(id, &cmd); + } + if cmd.is(sys_cmd::SHOW_CONTEXT_MENU) { + return self.show_context_menu(id, &cmd); } if let Some(w) = self.windows.get_mut(id) { let event = Event::Command(cmd); @@ -368,20 +367,24 @@ impl Inner { fn set_menu(&mut self, window_id: WindowId, cmd: &Command) { if let Some(win) = self.windows.get_mut(window_id) { - match cmd.get_object::>() { - Ok(menu) => win.set_menu(menu.to_owned(), &self.data, &self.env), - Err(e) => log::warn!("set-menu object error: '{}'", e), + if let Some(menu) = cmd.get(sys_cmd::SET_MENU) { + match menu.realize() { + Ok(menu) => win.set_menu(menu.to_owned(), &self.data, &self.env), + Err(e) => log::error!("set_menu: {}", e), + } } } } fn show_context_menu(&mut self, window_id: WindowId, cmd: &Command) { if let Some(win) = self.windows.get_mut(window_id) { - match cmd.get_object::>() { - Ok(ContextMenu { menu, location }) => { - win.show_context_menu(menu.to_owned(), *location, &self.data, &self.env) + if let Some(menu) = cmd.get(sys_cmd::SHOW_CONTEXT_MENU) { + match menu.realize() { + Ok(ContextMenu { menu, location }) => { + win.show_context_menu(menu.to_owned(), *location, &self.data, &self.env) + } + Err(e) => log::error!("show_context_menu: {}", e), } - Err(e) => log::warn!("show-context-menu object error: '{}'", e), } } } @@ -523,31 +526,37 @@ impl AppState { /// windows) have their logic here; other commands are passed to the window. fn handle_cmd(&mut self, target: Target, cmd: Command) { use Target as T; - match (target, &cmd.selector) { + match target { // these are handled the same no matter where they come from - (_, &sys_cmd::QUIT_APP) => self.quit(), - (_, &sys_cmd::HIDE_APPLICATION) => self.hide_app(), - (_, &sys_cmd::HIDE_OTHERS) => self.hide_others(), - (_, &sys_cmd::NEW_WINDOW) => { + _ if cmd.is(sys_cmd::QUIT_APP) => self.quit(), + _ if cmd.is(sys_cmd::HIDE_APPLICATION) => self.hide_app(), + _ if cmd.is(sys_cmd::HIDE_OTHERS) => self.hide_others(), + _ if cmd.is(sys_cmd::NEW_WINDOW) => { if let Err(e) = self.new_window(cmd) { log::error!("failed to create window: '{}'", e); } } - (_, &sys_cmd::CLOSE_ALL_WINDOWS) => self.request_close_all_windows(), + _ if cmd.is(sys_cmd::CLOSE_ALL_WINDOWS) => self.request_close_all_windows(), // these should come from a window // FIXME: we need to be able to open a file without a window handle - (T::Window(id), &sys_cmd::SHOW_OPEN_PANEL) => self.show_open_panel(cmd, id), - (T::Window(id), &sys_cmd::SHOW_SAVE_PANEL) => self.show_save_panel(cmd, id), - (T::Window(id), &sys_cmd::CLOSE_WINDOW) => self.request_close_window(cmd, id), - (T::Window(_), &sys_cmd::SHOW_WINDOW) => self.show_window(cmd), - (T::Window(id), &sys_cmd::PASTE) => self.do_paste(id), - _sel => self.inner.borrow_mut().dispatch_cmd(target, cmd), + T::Window(id) if cmd.is(sys_cmd::SHOW_OPEN_PANEL) => self.show_open_panel(cmd, id), + T::Window(id) if cmd.is(sys_cmd::SHOW_SAVE_PANEL) => self.show_save_panel(cmd, id), + T::Window(id) if cmd.is(sys_cmd::CLOSE_WINDOW) => self.request_close_window(id), + T::Window(id) if cmd.is(sys_cmd::SHOW_WINDOW) => self.show_window(id), + T::Window(id) if cmd.is(sys_cmd::PASTE) => self.do_paste(id), + _ if cmd.is(sys_cmd::CLOSE_WINDOW) => { + log::warn!("CLOSE_WINDOW command must target a window.") + } + _ if cmd.is(sys_cmd::SHOW_WINDOW) => { + log::warn!("SHOW_WINDOW command must target a window.") + } + _ => self.inner.borrow_mut().dispatch_cmd(target, cmd), } } fn show_open_panel(&mut self, cmd: Command, window_id: WindowId) { let options = cmd - .get_object::() + .get(sys_cmd::SHOW_OPEN_PANEL) .map(|opts| opts.to_owned()) .unwrap_or_default(); //FIXME: this is blocking; if we hold `borrow_mut` we are likely to cause @@ -569,7 +578,7 @@ impl AppState { fn show_save_panel(&mut self, cmd: Command, window_id: WindowId) { let options = cmd - .get_object::() + .get(sys_cmd::SHOW_SAVE_PANEL) .map(|opts| opts.to_owned()) .unwrap_or_default(); let handle = self @@ -580,32 +589,29 @@ impl AppState { .map(|w| w.handle.clone()); let result = handle.and_then(|mut handle| handle.save_as_sync(options)); if let Some(info) = result { - let cmd = Command::new(sys_cmd::SAVE_FILE, info); + let cmd = Command::new(sys_cmd::SAVE_FILE, Some(info)); self.inner.borrow_mut().dispatch_cmd(window_id.into(), cmd); } } fn new_window(&mut self, cmd: Command) -> Result<(), Box> { - let desc = cmd.take_object::>()?; + let desc = cmd.take(sys_cmd::NEW_WINDOW)?; + let desc = desc.realize()?; let window = desc.build_native(self)?; window.show(); Ok(()) } - fn request_close_window(&mut self, cmd: Command, window_id: WindowId) { - let id = cmd.get_object().unwrap_or(&window_id); - self.inner.borrow_mut().request_close_window(*id); + fn request_close_window(&mut self, window_id: WindowId) { + self.inner.borrow_mut().request_close_window(window_id); } fn request_close_all_windows(&mut self) { self.inner.borrow_mut().request_close_all_windows(); } - fn show_window(&mut self, cmd: Command) { - let id: WindowId = *cmd - .get_object() - .expect("show window selector missing window id"); - self.inner.borrow_mut().show_window(id); + fn show_window(&mut self, window_id: WindowId) { + self.inner.borrow_mut().show_window(window_id); } fn do_paste(&mut self, window_id: WindowId) {