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