zng_ext_window/
cmd.rs

1//! Commands that control the scoped window.
2
3use zng_app::{
4    event::{CommandHandle, CommandInfoExt, CommandNameExt, CommandScope, command},
5    hn,
6    shortcut::{CommandShortcutExt, shortcut},
7    view_process::VIEW_PROCESS,
8    window::WindowId,
9};
10
11use zng_layout::unit::{Dip, DipPoint, PxPoint, PxToDip as _};
12use zng_view_api::window::{WindowCapability, WindowState};
13use zng_wgt::{CommandIconExt as _, ICONS, wgt_fn};
14
15use crate::{IME_EVENT, ImeArgs, WINDOWS, WINDOWS_SV, WindowInstanceState, WindowVars};
16
17pub use zng_view_api::window::ResizeDirection;
18
19command! {
20    /// Represents the window **close** action.
21    pub static CLOSE_CMD {
22        l10n!: true,
23        name: "Close",
24        info: "Close the window",
25        shortcut: [shortcut!(ALT + F4), shortcut!(CTRL + 'W')],
26        icon: wgt_fn!(|_| ICONS.get(["window-close", "close"])),
27    };
28
29    /// Represents the window **minimize** action.
30    pub static MINIMIZE_CMD {
31        l10n!: true,
32        name: "Minimize",
33        info: "Minimize the window",
34        icon: wgt_fn!(|_| ICONS.get(["window-minimize"])),
35    };
36
37    /// Represents the window **maximize** action.
38    pub static MAXIMIZE_CMD {
39        l10n!: true,
40        name: "Maximize",
41        info: "Maximize the window",
42        icon: wgt_fn!(|_| ICONS.get(["window-maximize"])),
43    };
44
45    /// Represents the window **toggle fullscreen** action.
46    ///
47    /// # Behavior
48    ///
49    /// This command is about the *windowed* fullscreen state ([`WindowState::Fullscreen`]),
50    /// use the [`EXCLUSIVE_FULLSCREEN_CMD`] to toggle *exclusive* video mode fullscreen.
51    pub static FULLSCREEN_CMD {
52        l10n!: true,
53        name: "Fullscreen",
54        info: "Toggle fullscreen mode on the window",
55        shortcut: {
56            let a = if cfg!(target_os = "macos") {
57                shortcut!(CTRL | SHIFT + 'F')
58            } else {
59                shortcut!(F11)
60            };
61            [a, shortcut!(ZoomToggle)]
62        },
63        icon: wgt_fn!(|_| ICONS.get(["window-windowed-fullscreen", "window-fullscreen", "fullscreen"])),
64    };
65
66    /// Represents the window **toggle fullscreen** action.
67    ///
68    /// # Behavior
69    ///
70    /// This command is about the *exclusive* fullscreen state ([`WindowState::Exclusive`]),
71    /// use the [`FULLSCREEN_CMD`] to toggle *windowed* fullscreen.
72    pub static EXCLUSIVE_FULLSCREEN_CMD {
73        l10n!: true,
74        name: "Exclusive Fullscreen",
75        info: "Toggle exclusive fullscreen mode on the window",
76        icon: wgt_fn!(|_| ICONS.get(["window-exclusive-fullscreen", "window-fullscreen", "fullscreen"])),
77    };
78
79    /// Represents the window **restore** action.
80    ///
81    /// Restores the window to its previous non-minimized state or normal state.
82    pub static RESTORE_CMD {
83        l10n!: true,
84        name: "Restore",
85        info: "Restores the window to its previous non-minimized state or normal state",
86        icon: wgt_fn!(|_| ICONS.get(["window-restore"])),
87    };
88
89    /// Represents the **close IME** action.
90    ///
91    /// If any IME preview is active close it without committing.
92    pub static CANCEL_IME_CMD;
93
94    /// Represents the window **drag-move** and **drag-resize** actions.
95    ///
96    /// There's no guarantee that this will work unless the left mouse button was pressed immediately before this command is called.
97    ///
98    /// # Parameter
99    ///
100    /// If this command is called without parameter the window will drag-move, if it is called with a [`ResizeDirection`] the
101    /// window will drag-resize.
102    pub static DRAG_MOVE_RESIZE_CMD;
103
104    /// Represents the window **open title bar context menu** action.
105    ///
106    /// # Parameter
107    ///
108    /// This command supports an optional parameter, it can be a [`DipPoint`] or [`PxPoint`] that defines
109    /// the menu position.
110    ///
111    /// [`DipPoint`]: zng_layout::unit::DipPoint
112    /// [`PxPoint`]: zng_layout::unit::PxPoint
113    pub static OPEN_TITLE_BAR_CONTEXT_MENU_CMD;
114}
115
116pub(super) struct WindowCommands {
117    maximize_handle: CommandHandle,
118    minimize_handle: CommandHandle,
119    restore_handle: CommandHandle,
120
121    _fullscreen_handle: CommandHandle,
122    _exclusive_handle: CommandHandle,
123
124    _close_handle: CommandHandle,
125}
126impl WindowCommands {
127    /// Setup command handlers, handles live in the WindowVars hooks.
128    pub fn init(id: WindowId, vars: &WindowVars) {
129        let state = vars.state();
130        let restore_state = vars.restore_state();
131        let s = state.get();
132        let c = WindowCommands {
133            maximize_handle: MAXIMIZE_CMD.scoped(id).on_event(
134                !matches!(s, WindowState::Maximized),
135                true,
136                false,
137                hn!(state, |args| {
138                    args.propagation.stop();
139                    state.set(WindowState::Maximized);
140                }),
141            ),
142            minimize_handle: MINIMIZE_CMD.scoped(id).on_event(
143                !matches!(s, WindowState::Minimized),
144                true,
145                false,
146                hn!(state, |args| {
147                    args.propagation.stop();
148                    state.set(WindowState::Minimized);
149                }),
150            ),
151            restore_handle: RESTORE_CMD.scoped(id).on_event(
152                !matches!(s, WindowState::Normal),
153                true,
154                false,
155                hn!(state, restore_state, |args| {
156                    args.propagation.stop();
157                    state.set(restore_state.get());
158                }),
159            ),
160            _fullscreen_handle: FULLSCREEN_CMD.scoped(id).on_event(
161                true,
162                true,
163                false,
164                hn!(state, restore_state, |args| {
165                    if let WindowState::Fullscreen = state.get() {
166                        state.set(restore_state.get());
167                    } else {
168                        state.set(WindowState::Fullscreen);
169                    }
170                }),
171            ),
172            _exclusive_handle: EXCLUSIVE_FULLSCREEN_CMD.scoped(id).on_event(
173                true,
174                true,
175                false,
176                hn!(state, |args| {
177                    if let WindowState::Exclusive = state.get() {
178                        state.set(restore_state.get());
179                    } else {
180                        state.set(WindowState::Exclusive);
181                    }
182                }),
183            ),
184            _close_handle: CLOSE_CMD.scoped(id).on_event(
185                true,
186                true,
187                false,
188                hn!(|args| {
189                    args.propagation.stop();
190                    let _ = WINDOWS.close(id);
191                }),
192            ),
193        };
194        state
195            .hook(move |a| {
196                let state = *a.value();
197                let c = &c; // hold all handles
198                c.restore_handle.enabled().set(state != WindowState::Normal);
199                c.maximize_handle.enabled().set(state != WindowState::Maximized);
200                c.minimize_handle.enabled().set(state != WindowState::Minimized);
201                true
202            })
203            .perm();
204
205        fn can_open_ctx_menu(state: WindowInstanceState) -> bool {
206            matches!(state, WindowInstanceState::Loaded { has_view: true })
207                && VIEW_PROCESS.info().window.contains(WindowCapability::OPEN_TITLE_BAR_CONTEXT_MENU)
208        }
209        let handle = OPEN_TITLE_BAR_CONTEXT_MENU_CMD.scoped(id).on_event(
210            can_open_ctx_menu(vars.0.instance_state.get()),
211            true,
212            false,
213            hn!(|args| {
214                if let Some(w) = WINDOWS_SV.read().windows.get(&id)
215                    && let Some(vars) = &w.vars
216                    && let Some(r) = &w.root
217                    && let Some(v) = &r.view_window
218                {
219                    args.propagation.stop();
220
221                    let pos = if let Some(p) = args.param::<DipPoint>() {
222                        *p
223                    } else if let Some(p) = args.param::<PxPoint>() {
224                        p.to_dip(vars.0.scale_factor.get())
225                    } else {
226                        DipPoint::splat(Dip::new(24))
227                    };
228
229                    let _ = v.open_title_bar_context_menu(pos);
230                }
231            }),
232        );
233        vars.0
234            .instance_state
235            .hook(move |a| {
236                handle.enabled().set(can_open_ctx_menu(a.value().clone()));
237                true
238            })
239            .perm();
240
241        let handle = CANCEL_IME_CMD.scoped(id).on_event(
242            false,
243            false,
244            false,
245            hn!(|args| {
246                let s = WINDOWS_SV.read();
247                if let Some(w) = s.windows.get(&id)
248                    && let Some(r) = &w.root
249                    && let Some(v) = &r.view_window
250                    && let Some(f) = s.focused.get()
251                    && f.window_id() == id
252                    && (matches!(args.scope, CommandScope::Window(w) if w == id)
253                        || matches!(args.scope, CommandScope::Widget(w) if w == f.widget_id()))
254                {
255                    args.propagation.stop();
256
257                    let _ = v.set_ime_area(None);
258
259                    IME_EVENT.notify(ImeArgs::now(f, "", None));
260                }
261            }),
262        );
263        vars.0
264            .focused
265            .hook(move |a| {
266                handle.enabled().set(*a.value());
267                true
268            })
269            .perm();
270
271        let handle = DRAG_MOVE_RESIZE_CMD.scoped(id).on_event(
272            vars.0.resizable.get(),
273            true,
274            false,
275            hn!(|args| {
276                if let Some(w) = WINDOWS_SV.read().windows.get(&id)
277                    && let Some(r) = &w.root
278                    && let Some(v) = &r.view_window
279                {
280                    args.propagation.stop();
281                    let _ = match args.param::<crate::cmd::ResizeDirection>() {
282                        Some(r) => v.drag_resize(*r),
283                        None => v.drag_move(),
284                    };
285                }
286            }),
287        );
288        vars.0
289            .resizable
290            .hook(move |a| {
291                handle.enabled().set(*a.value());
292                true
293            })
294            .perm();
295    }
296}