zng_ext_input/focus/
cmd.rs

1//! Commands that control focus and [`Command`] extensions.
2//!
3//! [`Command`]: zng_app::event::Command
4
5use zng_app::{
6    event::{Command, CommandHandle, CommandInfoExt, CommandNameExt, CommandScope, command},
7    hn,
8    shortcut::{CommandShortcutExt, shortcut},
9    widget::info::WidgetInfo,
10};
11use zng_ext_window::WINDOWS;
12use zng_var::{Var, merge_var};
13
14use super::*;
15
16command! {
17    /// Represents the **focus next** action.
18    pub static FOCUS_NEXT_CMD {
19        l10n!: true,
20        name: "Focus Next",
21        info: "Focus next focusable",
22        shortcut: shortcut!(Tab),
23    };
24
25    /// Represents the **focus previous** action.
26    pub static FOCUS_PREV_CMD {
27        l10n!: true,
28        name: "Focus Previous",
29        info: "Focus previous focusable",
30        shortcut: shortcut!(SHIFT + Tab),
31    };
32
33    /// Represents the **focus/escape alt** action.
34    pub static FOCUS_ALT_CMD {
35        l10n!: true,
36        name: "Focus Alt",
37        info: "Focus alt scope",
38        shortcut: shortcut!(Alt),
39    };
40
41    /// Represents the **focus enter** action.
42    pub static FOCUS_ENTER_CMD {
43        l10n!: true,
44        name: "Focus Enter",
45        info: "Focus child focusable",
46        shortcut: [shortcut!(Enter), shortcut!(ALT + Enter)],
47    };
48
49    /// Represents the **focus exit** action.
50    ///
51    /// An optional command parameter of type `bool` will set the `recursive_alt` parameter.
52    pub static FOCUS_EXIT_CMD {
53        l10n!: true,
54        name: "Focus Exit",
55        info: "Focus parent focusable, or return focus",
56        shortcut: [shortcut!(Escape), shortcut!(ALT + Escape)],
57    };
58
59    /// Represents the **focus up** action.
60    pub static FOCUS_UP_CMD {
61        l10n!: true,
62        name: "Focus Up",
63        info: "Focus closest focusable up",
64        shortcut: [shortcut!(ArrowUp), shortcut!(ALT + ArrowUp)],
65    };
66
67    /// Represents the **focus down** action.
68    pub static FOCUS_DOWN_CMD {
69        l10n!: true,
70        name: "Focus Down",
71        info: "Focus closest focusable down",
72        shortcut: [shortcut!(ArrowDown), shortcut!(ALT + ArrowDown)],
73    };
74
75    /// Represents the **focus left** action.
76    pub static FOCUS_LEFT_CMD {
77        l10n!: true,
78        name: "Focus Left",
79        info: "Focus closest focusable left",
80        shortcut: [shortcut!(ArrowLeft), shortcut!(ALT + ArrowLeft)],
81    };
82
83    /// Represents the **focus right** action.
84    pub static FOCUS_RIGHT_CMD {
85        l10n!: true,
86        name: "Focus Right",
87        info: "Focus closest focusable right",
88        shortcut: [shortcut!(ArrowRight), shortcut!(ALT + ArrowRight)],
89    };
90
91    /// Represents a [`FocusRequest`] action.
92    ///
93    /// If this command parameter is a [`FocusRequest`] the request is made.
94    pub static FOCUS_CMD;
95}
96
97pub(super) struct FocusCommands {
98    next_handle: CommandHandle,
99    prev_handle: CommandHandle,
100
101    alt_handle: CommandHandle,
102
103    up_handle: CommandHandle,
104    down_handle: CommandHandle,
105    left_handle: CommandHandle,
106    right_handle: CommandHandle,
107
108    exit_handle: CommandHandle,
109    enter_handle: CommandHandle,
110
111    _focus_handle: CommandHandle,
112}
113impl FocusCommands {
114    pub fn new() -> Self {
115        macro_rules! handle {
116            ($($CMD:ident($handle:ident) => $method:ident,)+) => {Self {
117                $($handle: $CMD.on_event_with_enabled(false, true, false, hn!(|a| {
118                    let (args, enabled) = a;
119                    if args.param.is_some() {
120                        return;
121                    }
122                    args.propagation.stop();
123                    if enabled.get() {
124                        FOCUS.$method();
125                    } else {
126                        FOCUS.highlight_within_auto();
127                    }
128                })),)+
129                exit_handle: FOCUS_EXIT_CMD.on_event(false, true, false, hn!(|args| {
130                    if let Some(recursive_alt) = args.param::<bool>() {
131                        args.propagation.stop();
132                        FOCUS.focus_exit(*recursive_alt);
133                    } else if args.param.is_none() {
134                        args.propagation.stop();
135                        FOCUS.focus_exit(false);
136                    }
137                })),
138                _focus_handle: FOCUS_CMD.on_event(true, true, false, hn!(|args| {
139                    if let Some(req) = args.param::<FocusRequest>() {
140                        args.propagation.stop();
141                        FOCUS.focus(*req);
142                    }
143                })),
144            }};
145        }
146
147        #[rustfmt::skip] // for zng fmt
148        handle! {
149            FOCUS_NEXT_CMD(next_handle) => focus_next,
150            FOCUS_PREV_CMD(prev_handle) => focus_prev,
151            FOCUS_ALT_CMD(alt_handle) => focus_alt,
152            FOCUS_UP_CMD(up_handle) => focus_up,
153            FOCUS_DOWN_CMD(down_handle) => focus_down,
154            FOCUS_LEFT_CMD(left_handle) => focus_left,
155            FOCUS_RIGHT_CMD(right_handle) => focus_right,
156            FOCUS_ENTER_CMD(enter_handle) => focus_enter,
157        }
158    }
159
160    pub fn update_enabled(&mut self, nav: FocusNavAction) {
161        self.next_handle.enabled().set(nav.contains(FocusNavAction::NEXT));
162        self.prev_handle.enabled().set(nav.contains(FocusNavAction::PREV));
163
164        self.alt_handle.enabled().set(nav.contains(FocusNavAction::ALT));
165
166        self.up_handle.enabled().set(nav.contains(FocusNavAction::UP));
167        self.down_handle.enabled().set(nav.contains(FocusNavAction::DOWN));
168        self.left_handle.enabled().set(nav.contains(FocusNavAction::LEFT));
169        self.right_handle.enabled().set(nav.contains(FocusNavAction::RIGHT));
170
171        self.exit_handle.enabled().set(nav.contains(FocusNavAction::EXIT));
172        self.enter_handle.enabled().set(nav.contains(FocusNavAction::ENTER));
173    }
174}
175
176/// Focus extension methods for commands.
177pub trait CommandFocusExt {
178    /// Gets a command variable with `self` scoped to the focused (non-alt) widget or app.
179    ///
180    /// The scope is [`alt_return`] if is set, otherwise it is [`focused`], otherwise the
181    /// command is not scoped (app scope). This means that you can bind the command variable to
182    /// a menu or toolbar button inside an *alt-scope* without losing track of the intended target
183    /// of the command.
184    ///
185    /// [`alt_return`]: FOCUS::alt_return
186    /// [`focused`]: FOCUS::focused
187    ///
188    /// # Examples
189    ///
190    /// ```no_run
191    /// # zng_app::command! { pub static PASTE_CMD; }
192    /// # use zng_ext_input::focus::cmd::CommandFocusExt as _;
193    /// # use zng_var::*;
194    /// # fn main() {
195    /// let paste_in_focused_cmd = PASTE_CMD.focus_scoped();
196    /// let is_enabled = paste_in_focused_cmd.flat_map(|c| c.is_enabled());
197    /// paste_in_focused_cmd.get().notify();
198    /// # }
199    /// ```
200    fn focus_scoped(self) -> Var<Command>;
201
202    /// Gets a command variable with `self` scoped to the output of `map`.
203    ///
204    /// The `map` closure is called every time the non-alt focused widget changes, that is the [`alt_return`] or
205    /// the [`focused`]. The closure input is the [`WidgetInfo`] for the focused widget and the output must be
206    /// a [`CommandScope`] for the command.
207    ///
208    /// [`alt_return`]: FOCUS::alt_return
209    /// [`focused`]: FOCUS::focused
210    /// [`WidgetInfo`]: zng_app::widget::info::WidgetInfo
211    /// [`CommandScope`]: zng_app::event::CommandScope
212    fn focus_scoped_with(self, map: impl FnMut(Option<WidgetInfo>) -> CommandScope + Send + 'static) -> Var<Command>;
213}
214
215impl CommandFocusExt for Command {
216    fn focus_scoped(self) -> Var<Command> {
217        let cmd = self.scoped(CommandScope::App);
218        merge_var!(FOCUS.alt_return(), FOCUS.focused(), move |alt, f| {
219            match alt.as_ref().or(f.as_ref()) {
220                Some(p) => cmd.scoped(p.widget_id()),
221                None => cmd,
222            }
223        })
224    }
225
226    fn focus_scoped_with(self, mut map: impl FnMut(Option<WidgetInfo>) -> CommandScope + Send + 'static) -> Var<Command> {
227        let cmd = self.scoped(CommandScope::App);
228        merge_var!(FOCUS.alt_return(), FOCUS.focused(), |alt, f| {
229            match alt.as_ref().or(f.as_ref()) {
230                Some(p) => WINDOWS.widget_tree(p.window_id())?.get(p.widget_id()),
231                None => None,
232            }
233        })
234        .map(move |w| cmd.scoped(map(w.clone())))
235    }
236}