Skip to main content

zng_ext_window/
cmd.rs

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