From 6cabab4d5a4a78e0b6ab24cf75a81e1451058474 Mon Sep 17 00:00:00 2001 From: Samuel Guerra Date: Thu, 11 Jan 2024 21:50:21 -0300 Subject: [PATCH] Renamed `map_to_text` to `map_to_txt`. Finished button docs. Implemented `Command::on_event` that subscribes to the command too, not just the command event. --- TODO/_current.md | 3 +- examples/config.rs | 2 +- examples/text.rs | 2 +- examples/window.rs | 4 +- zero-ui-app/src/event/command.rs | 88 ++++++++++++++++++- zero-ui-var/src/lib.rs | 2 +- zero-ui-wgt-button/Cargo.toml | 4 +- zero-ui-wgt-button/src/lib.rs | 81 +++++++++++++++++- zero-ui-wgt-text_input/src/selectable.rs | 7 +- zero-ui-wgt-text_input/src/text_input.rs | 2 +- zero-ui-wgt-tooltip/src/lib.rs | 1 + zero-ui-wgt-view/src/lib.rs | 2 +- zero-ui/src/button.rs | 104 +++++++++++++---------- zero-ui/src/style.rs | 4 + 14 files changed, 246 insertions(+), 60 deletions(-) diff --git a/TODO/_current.md b/TODO/_current.md index cfec54bfc..98cb158e2 100644 --- a/TODO/_current.md +++ b/TODO/_current.md @@ -1,6 +1,7 @@ * Test touch context menu. * Add description tooltip (with shortcut) for command button. -* Finish documenting button module. +* `StyleMix` does not capture `extend_style`/`replace_style` on the same widget, so it ends-up ignored. Need + to promote this pattern. # Documentation diff --git a/examples/config.rs b/examples/config.rs index a07b73df3..ffcfe7a88 100644 --- a/examples/config.rs +++ b/examples/config.rs @@ -123,7 +123,7 @@ fn app_main() { Window! { title = if std::env::var("MOVE-TO").is_err() { "Config Example" } else { "Config Example - Other Process" }; widget::background = Text! { - txt = CONFIG.status().map_to_text(); + txt = CONFIG.status().map_to_txt(); margin = 10; font_family = "monospace"; align = Align::TOP_LEFT; diff --git a/examples/text.rs b/examples/text.rs index 1f66910ee..c1a73717d 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -437,7 +437,7 @@ fn text_editor_window(is_open: ArcVar) -> WindowRoot { child_bottom = Text! { margin = (0, 4); align = Align::RIGHT; - txt = editor.caret_status.map_to_text(); + txt = editor.caret_status.map_to_txt(); }, 0; } } diff --git a/examples/window.rs b/examples/window.rs index 55e976201..ad2a306b1 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -127,7 +127,7 @@ fn background_color_example(color: impl Var) -> impl UiNode { background_color = c.clone(); size = (16, 16); }, - Text!(c.map_to_text()), + Text!(c.map_to_txt()), ]; }; } @@ -403,7 +403,7 @@ fn exclusive_mode() -> impl UiNode { tooltip = Tip!(Text!("Exclusive video mode")); child = Text! { - txt = WINDOW.vars().video_mode().map_to_text(); + txt = WINDOW.vars().video_mode().map_to_txt(); txt_align = Align::CENTER; padding = 2; }; diff --git a/zero-ui-app/src/event/command.rs b/zero-ui-app/src/event/command.rs index 49366b83a..d7665a741 100644 --- a/zero-ui-app/src/event/command.rs +++ b/zero-ui-app/src/event/command.rs @@ -1,7 +1,7 @@ use std::{ any::TypeId, collections::{hash_map, HashMap}, - mem, + mem, ops, }; use crate::{shortcut::CommandShortcutExt, update::UpdatesTrace, widget::info::WidgetInfo, window::WindowId, APP}; @@ -424,6 +424,40 @@ impl Command { )) } + /// Creates a preview event handler for the command. + /// + /// This is similar to [`Event::on_pre_event`], but `handler` is only called if the command + /// scope matches and a command subscription exists for the lifetime of the handler. + /// + /// The `enabled` parameter defines the initial state of the command subscription, the subscription + /// handle is available in the handler args. + pub fn on_pre_event(&self, enabled: bool, handler: H) -> EventHandle + where + H: AppHandler, + { + self.event().on_pre_event(CmdAppHandler { + handler, + handle: Arc::new(self.subscribe(enabled)), + }) + } + + /// Creates an event handler for the command. + /// + /// This is similar to [`Event::on_event`], but `handler` is only called if the command + /// scope matches and a command subscription exists for the lifetime of the handler. + /// + /// The `enabled` parameter defines the initial state of the command subscription, the subscription + /// handle is available in the handler args. + pub fn on_event(&self, enabled: bool, handler: H) -> EventHandle + where + H: AppHandler, + { + self.event().on_event(CmdAppHandler { + handler, + handle: Arc::new(self.subscribe(enabled)), + }) + } + /// Update state vars, returns if the command must be retained. #[must_use] pub(crate) fn update_state(&self) -> bool { @@ -474,6 +508,20 @@ impl PartialEq for Command { } impl Eq for Command {} +struct CmdAppHandler { + handler: H, + handle: Arc, +} +impl> AppHandler for CmdAppHandler { + fn event(&mut self, args: &CommandArgs, handler_args: &AppHandlerArgs) { + let args = AppCommandArgs { + args: args.clone(), + handle: self.handle.clone(), + }; + self.handler.event(&args, handler_args); + } +} + /// Represents the scope of a [`Command`]. /// /// The command scope defines the targets of its event and the context of its metadata. @@ -578,6 +626,44 @@ impl CommandArgs { } } +/// Arguments for [`Command::on_event`]. +#[derive(Debug, Clone)] +pub struct AppCommandArgs { + /// The command args. + pub args: CommandArgs, + /// The command handle held by the event handler. + pub handle: Arc, +} +impl ops::Deref for AppCommandArgs { + type Target = CommandArgs; + + fn deref(&self) -> &Self::Target { + &self.args + } +} +impl AnyEventArgs for AppCommandArgs { + fn clone_any(&self) -> Box { + Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn timestamp(&self) -> Instant { + self.args.timestamp() + } + + fn delivery_list(&self, list: &mut UpdateDeliveryList) { + self.args.delivery_list(list) + } + + fn propagation(&self) -> &EventPropagationHandle { + self.args.propagation() + } +} +impl EventArgs for AppCommandArgs {} + /// A handle to a [`Command`]. /// /// Holding the command handle indicates that the command is relevant in the current app state. diff --git a/zero-ui-var/src/lib.rs b/zero-ui-var/src/lib.rs index ae58f5bbc..51fbd81ca 100644 --- a/zero-ui-var/src/lib.rs +++ b/zero-ui-var/src/lib.rs @@ -1416,7 +1416,7 @@ pub trait Var: IntoVar + AnyVar + Clone { /// [`map`]: Var::map /// [`Txt`]: Txt /// [`ToTxt`]: ToTxt - fn map_to_text(&self) -> Self::Map + fn map_to_txt(&self) -> Self::Map where T: ToTxt, { diff --git a/zero-ui-wgt-button/Cargo.toml b/zero-ui-wgt-button/Cargo.toml index 9bf456701..d4ea17b31 100644 --- a/zero-ui-wgt-button/Cargo.toml +++ b/zero-ui-wgt-button/Cargo.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" [dependencies] zero-ui-var = { path = "../zero-ui-var" } zero-ui-app = { path = "../zero-ui-app" } +zero-ui-ext-font = { path = "../zero-ui-ext-font" } zero-ui-wgt = { path = "../zero-ui-wgt" } zero-ui-wgt-container = { path = "../zero-ui-wgt-container" } zero-ui-wgt-style = { path = "../zero-ui-wgt-style" } @@ -15,4 +16,5 @@ zero-ui-wgt-input = { path = "../zero-ui-wgt-input" } zero-ui-wgt-access = { path = "../zero-ui-wgt-access" } zero-ui-wgt-fill = { path = "../zero-ui-wgt-fill" } zero-ui-wgt-filter = { path = "../zero-ui-wgt-filter" } -zero-ui-wgt-text = { path = "../zero-ui-wgt-text" } \ No newline at end of file +zero-ui-wgt-text = { path = "../zero-ui-wgt-text" } +zero-ui-wgt-tooltip = { path = "../zero-ui-wgt-tooltip" } \ No newline at end of file diff --git a/zero-ui-wgt-button/src/lib.rs b/zero-ui-wgt-button/src/lib.rs index 23c730ee9..8bca400d7 100644 --- a/zero-ui-wgt-button/src/lib.rs +++ b/zero-ui-wgt-button/src/lib.rs @@ -5,6 +5,8 @@ zero_ui_wgt::enable_widget_macros!(); +use std::ops; + use zero_ui_app::event::CommandParam; use zero_ui_var::ReadOnlyContextVar; use zero_ui_wgt::{border, corner_radius, is_disabled, prelude::*}; @@ -21,6 +23,8 @@ use zero_ui_wgt_input::{ CursorIcon, }; use zero_ui_wgt_style::{Style, StyleFn, StyleMix}; +use zero_ui_wgt_text::Text; +use zero_ui_wgt_tooltip::{tooltip, tooltip_fn, Tip, TooltipArgs}; /// A clickable container. /// @@ -67,7 +71,8 @@ impl Button { let on_click = wgt.property(property_id!(Self::on_click)).is_none(); let on_disabled_click = wgt.property(property_id!(on_disabled_click)).is_none(); - if on_click || on_disabled_click { + let tooltip = wgt.property(property_id!(tooltip)).is_none() && wgt.property(property_id!(tooltip_fn)).is_none(); + if on_click || on_disabled_click || tooltip { wgt.push_intrinsic( NestGroup::EVENT, "cmd-event", @@ -106,6 +111,19 @@ impl Button { ) .boxed(); } + if tooltip { + child = self::tooltip_fn( + child, + merge_var!(cmd, CMD_TOOLTIP_FN_VAR, |cmd, tt_fn| { + if tt_fn.is_nil() { + WidgetFn::nil() + } else { + wgt_fn!(cmd, tt_fn, |tooltip| { tt_fn(CmdTooltipArgs { tooltip, cmd }) }) + } + }), + ) + .boxed(); + } child }), ); @@ -142,12 +160,60 @@ context_var! { /// Widget function used when `cmd` is set and `child` is not. pub static CMD_CHILD_FN_VAR: WidgetFn = WidgetFn::new(default_cmd_child_fn); + /// Widget function used when `cmd` is set and `tooltip_fn`, `tooltip` are not set. + pub static CMD_TOOLTIP_FN_VAR: WidgetFn = WidgetFn::new(default_cmd_tooltip_fn); + static CMD_VAR: Option = None; } +/// Arguments for [`cmd_tooltip_fn`]. +/// +/// [`cmd_tooltip_fn`]: fn@cmd_tooltip_fn +#[derive(Clone)] +pub struct CmdTooltipArgs { + /// The tooltip arguments. + pub tooltip: TooltipArgs, + /// The command. + pub cmd: Command, +} +impl ops::Deref for CmdTooltipArgs { + type Target = TooltipArgs; + + fn deref(&self) -> &Self::Target { + &self.tooltip + } +} + /// Default [`CMD_CHILD_FN_VAR`]. pub fn default_cmd_child_fn(cmd: Command) -> impl UiNode { - zero_ui_wgt_text::Text!(cmd.name()) + Text!(cmd.name()) +} + +/// Default [`CMD_TOOLTIP_FN_VAR`]. +pub fn default_cmd_tooltip_fn(args: CmdTooltipArgs) -> impl UiNode { + let info = args.cmd.info(); + let has_info = info.map(|s| !s.is_empty()); + let shortcut = args.cmd.shortcut().map(|s| match s.first() { + Some(s) => s.to_txt(), + None => Txt::from(""), + }); + let has_shortcut = shortcut.map(|s| !s.is_empty()); + Tip! { + child = Text! { + zero_ui_wgt::visibility = has_info.map_into(); + txt = info; + }; + child_bottom = { + insert: Text! { + font_weight = zero_ui_ext_font::FontWeight::BOLD; + zero_ui_wgt::visibility = has_shortcut.map_into(); + txt = shortcut; + }, + spacing: 4, + }; + + zero_ui_wgt::visibility = expr_var!((*#{has_info} || *#{has_shortcut}).into()) + } } /// Sets the [`Command`] the button represents. @@ -155,12 +221,15 @@ pub fn default_cmd_child_fn(cmd: Command) -> impl UiNode { /// When this is set the button widget sets these properties if they are not set: /// /// * [`child`]: Set to an widget produced by [`cmd_child_fn`](fn@cmd_child_fn), by default is `Text!(cmd.name())`. +/// * [`tooltip_fn`]: Set to a widget function provided by [`cmd_tooltip_fn`](fn@cmd_tooltip_fn), by default it +/// shows the command info and first shortcut. /// * [`enabled`]: Set to `cmd.is_enabled()`. /// * [`visibility`]: Set to `cmd.has_handlers().into()`. /// * [`on_click`]: Set to a handler that notifies the command if `cmd.is_enabled()`. /// * [`on_disabled_click`]: Set to a handler that notifies the command if `!cmd.is_enabled()`. /// /// [`child`]: struct@Button#child +/// [`tooltip_fn`]: fn@tooltip_fn /// [`Command`]: zero_ui_app::event::Command /// [`enabled`]: fn@zero_ui_wgt::enabled /// [`visibility`]: fn@zero_ui_wgt::visibility @@ -186,6 +255,14 @@ pub fn cmd_child_fn(child: impl UiNode, cmd_child: impl IntoVar>) -> impl UiNode { + with_context_var(child, CMD_TOOLTIP_FN_VAR, cmd_tooltip) +} + /// Sets the [`BASE_COLORS_VAR`] that is used to compute all background and border colors in the button style. #[property(CONTEXT, default(BASE_COLORS_VAR), widget_impl(DefaultStyle))] pub fn base_colors(child: impl UiNode, color: impl IntoVar) -> impl UiNode { diff --git a/zero-ui-wgt-text_input/src/selectable.rs b/zero-ui-wgt-text_input/src/selectable.rs index 05e7773cb..2755f9f2d 100644 --- a/zero-ui-wgt-text_input/src/selectable.rs +++ b/zero-ui-wgt-text_input/src/selectable.rs @@ -2,6 +2,7 @@ use zero_ui_ext_clipboard::COPY_CMD; use zero_ui_wgt::prelude::*; +use zero_ui_wgt_button::Button; use zero_ui_wgt_input::focus::FocusableMix; use zero_ui_wgt_menu::{ self as menu, @@ -9,7 +10,6 @@ use zero_ui_wgt_menu::{ }; use zero_ui_wgt_style::{Style, StyleFn, StyleMix}; use zero_ui_wgt_text::{self as text, *}; -use zero_ui_wgt_button::Button; /// Styleable read-only text widget that can be selected and copied to clipboard. #[widget($crate::selectable::SelectableText)] @@ -70,10 +70,7 @@ impl DefaultStyle { /// [`DefaultStyle!`]: struct@DefaultStyle pub fn default_context_menu(args: menu::context::ContextMenuArgs) -> impl UiNode { let id = args.anchor_id; - ContextMenu!(ui_vec![ - Button!(COPY_CMD.scoped(id)), - Button!(text::cmd::SELECT_ALL_CMD.scoped(id)), - ]) + ContextMenu!(ui_vec![Button!(COPY_CMD.scoped(id)), Button!(text::cmd::SELECT_ALL_CMD.scoped(id)),]) } /// Selection toolbar set by the [`DefaultStyle!`]. diff --git a/zero-ui-wgt-text_input/src/text_input.rs b/zero-ui-wgt-text_input/src/text_input.rs index 54dc2721f..ac3793f5c 100644 --- a/zero-ui-wgt-text_input/src/text_input.rs +++ b/zero-ui-wgt-text_input/src/text_input.rs @@ -1,6 +1,7 @@ use zero_ui_ext_clipboard::{COPY_CMD, CUT_CMD, PASTE_CMD}; use zero_ui_wgt::{align, is_disabled, margin, prelude::*}; use zero_ui_wgt_access::{access_role, AccessRole}; +use zero_ui_wgt_button::Button; use zero_ui_wgt_data::{DataNoteLevel, DataNotes, DATA}; use zero_ui_wgt_fill::foreground_highlight; use zero_ui_wgt_filter::{child_opacity, saturate}; @@ -18,7 +19,6 @@ use zero_ui_wgt_size_offset::{offset, y}; use zero_ui_wgt_style::{Style, StyleFn, StyleMix}; use zero_ui_wgt_text::{self as text, *}; use zero_ui_wgt_undo::{undo_scope, UndoMix}; -use zero_ui_wgt_button::Button; /// Simple text editor widget. /// diff --git a/zero-ui-wgt-tooltip/src/lib.rs b/zero-ui-wgt-tooltip/src/lib.rs index d78fcdb3c..feab16897 100644 --- a/zero-ui-wgt-tooltip/src/lib.rs +++ b/zero-ui-wgt-tooltip/src/lib.rs @@ -433,6 +433,7 @@ pub fn access_tooltip_duration(child: impl UiNode, duration: impl IntoVar ViewArgs { /// Text! { /// font_size = 28; /// // bind data, same view will be used for all n > 0 values. -/// txt = a.data().map_to_text(); +/// txt = a.data().map_to_txt(); /// } /// } else { /// // finished view diff --git a/zero-ui/src/button.rs b/zero-ui/src/button.rs index 22768e569..6f6eff292 100644 --- a/zero-ui/src/button.rs +++ b/zero-ui/src/button.rs @@ -2,16 +2,16 @@ //! //! A simple clickable container widget, it can be used by directly handling the click events or by setting it to //! operate a [`Command`]. -//! +//! //! [`Command`]: crate::event::Command -//! +//! //! # Click Events -//! +//! //! The button widget implements the [`gesture::on_click`] event so you can use it directly, but like any //! other widget all events can be set. The example below demonstrates both ways of setting events. -//! +//! //! [`gesture::on_click`]: fn@crate::gesture::on_click -//! +//! //! ``` //! use zero_ui::prelude::*; //! @@ -36,16 +36,16 @@ //! } //! # ; //! ``` -//! +//! //! # Command -//! +//! //! Instead of handling events directly the button widget can be set to represents a command. //! If the [`cmd`](struct@Button#cmd) property is set the button widget will automatically set properties //! from command metadata, you can manually set some of these properties to override the command default. -//! +//! //! ``` //! use zero_ui::prelude::*; -//! +//! //! # let _scope = APP.defaults(); //! # let _ = //! Stack!(left_to_right, 5, ui_vec![ @@ -59,48 +59,66 @@ //! ]) //! # ; //! ``` -//! -//! The properties a command button sets are documented in the [`cmd`](struct@Button#cmd) property docs. //! -//!
-//! Equivalent command button. -//! -//! This example shows an equivalent command button implementation, for a single command. -//! There are some differences, the real `cmd` is a variable so commands can dynamically change and -//! the handlers also pass on the[`cmd_param`](struct@Button#cmd_param) if set. -//! +//! The properties a command button sets are documented in the [`cmd`](struct@Button#cmd) property docs. +//! Of particular importance is the [`widget::visibility`], it is set so that the button is only visible if +//! the command has any handlers, enabled or disabled, this is done because commands are considered irrelevant +//! in the current context if they don't even have a disabled handler. The example above will only be +//! visible if you set handlers for those commands. +//! //! ``` //! # use zero_ui::prelude::*; -//! let cmd = zero_ui::clipboard::COPY_CMD; -//! # let _scope = APP.defaults(); let _ = -//! Button! { -//! child = Text!(cmd.name()); -//! widget::enabled = cmd.is_enabled(); -//! widget::visibility = cmd.has_handlers().map_into(); -//! on_click = hn!(|args: &gesture::ClickArgs| { -//! if cmd.is_enabled_value() { -//! args.propagation().stop(); -//! cmd.notify(); -//! } -//! }); -//! gesture::on_disabled_click = hn!(|args: &gesture::ClickArgs| { -//! if !cmd.is_enabled_value() { -//! args.propagation().stop(); -//! cmd.notify(); -//! } -//! }); +//! # let _scope = APP.defaults(); +//! # fn cmd_btn_example() -> impl UiNode { widget::node::NilUiNode } +//! # let _ = +//! zero_ui::clipboard::COPY_CMD.on_event(true, app_hn!(|_, _| { println!("copy") })).perm(); +//! zero_ui::clipboard::PASTE_CMD.on_event(true, app_hn!(|_, _| { println!("paste") })).perm(); +//! Window! { +//! child = cmd_btn_example(); //! } //! # ; //! ``` -//! -//!
-//! +//! +//! [`widget::visibility`]: fn@crate::widget::visibility +//! //! # Style -//! -//! TODO, also mention BUTTON.cmd. -//! +//! +//! The button widget is styleable and implements the [extend/replace] pattern, the [`extend_style`] property can be +//! set in any parent widget or the button itself to add to the button style, the [`replace_style`] property +//! can be set to fully replace the style. +//! +//! [extend/replace]: crate::style#extend-replace +//! [`extend_style`]: fn@extend_style +//! [`replace_style`]: fn@replace_style +//! //! ## Base Colors -//! +//! +//! The default style derive all colors from the [`base_colors`](fn@base_colors), so if you +//! only want to change color of buttons you can use this property. +//! +//! The example below extends the button style to change the button color to red when it represents +//! an specific command. +//! +//! ``` +//! use zero_ui::prelude::*; +//! use zero_ui::button; +//! +//! # let _scope = APP.defaults(); let _ = +//! Window! { +//! button::extend_style = Style! { +//! when *#{button::BUTTON.cmd()} == Some(window::cmd::CLOSE_CMD) { +//! button::base_colors = color::ColorPair { +//! // dark theme base +//! dark: colors::BLACK.with_alpha(80.pct()).mix_normal(colors::RED), +//! // light theme base +//! light: colors::WHITE.with_alpha(80.pct()).mix_normal(colors::RED), +//! }; +//! } +//! }; +//! } +//! # ; +//! ``` +//! //! # Full API //! //! See [`zero_ui_wgt_button`] for the full widget API. diff --git a/zero-ui/src/style.rs b/zero-ui/src/style.rs index 6baf21565..2d0d9315d 100644 --- a/zero-ui/src/style.rs +++ b/zero-ui/src/style.rs @@ -1,3 +1,7 @@ //! Style mix-in and types. +//! +//! # Extend/Replace +//! +//! A common pattern implemented by styleable widgets is to declare two properties, `extend_style` and `replace_style`. pub use zero_ui_wgt_style::{style_fn, with_style_extension, Style, StyleArgs, StyleBuilder, StyleFn, StyleMix};