virtual_desktop_manager/
quick_switch.rs

1use core::ops::Range;
2use std::collections::BTreeMap;
3
4#[derive(Default)]
5pub struct QuickSwitchMenu {
6    /// Menu items that map to specific virtual desktops.
7    tray_virtual_desktops_quick: Vec<(nwg::MenuItem, u32)>,
8
9    tray_separators: Vec<nwg::MenuSeparator>,
10
11    /// [`nwg::Menu`] won't be automatically removed from their parent when
12    /// dropped so make sure to use [`crate::nwg_ext::remove_menu`] to manually
13    /// remove them.
14    tray_submenus: Vec<(nwg::Menu, u32)>,
15
16    /// Support shortcut access keys to specific virtual desktop indexes. This
17    /// keys should not be numbers since those access keys are already in use by
18    /// the quick switch menu.
19    pub shortcuts: BTreeMap<String, u32>,
20    /// Only show the shortcut menu items in the root of the quick switch menu.
21    pub shortcuts_only_in_root: bool,
22
23    /// Used to limit recursion in order to prevent stack overflow, should be
24    /// empty when not inside a [`Self::create_quick_switch_menu`] call.
25    ///
26    /// We could have used a `VecDeque` here to more faithfully remember the
27    /// call order but its actually more performant to not do that since the
28    /// normal call order is breadth first which means we would store more items
29    /// in this queue.
30    call_queue: Vec<(nwg::ControlHandle, u32, u32, u32)>,
31    /// `true` if we are in a recursive call of
32    /// [`Self::create_quick_switch_menu`].
33    is_recursive_call: bool,
34}
35impl QuickSwitchMenu {
36    /// Get the virtual desktop index that should be selected when a specific
37    /// context menu item is pressed.
38    pub fn get_clicked_desktop_index(&self, handle: nwg::ControlHandle) -> Option<usize> {
39        if !matches!(handle, nwg::ControlHandle::MenuItem { .. }) {
40            return None;
41        }
42        self.tray_virtual_desktops_quick
43            .iter()
44            .find(|(d, _)| d.handle == handle)
45            .map(|(_, ix)| *ix as usize)
46    }
47    /// Get the number that would be generated by the keyboard shortcuts needed
48    /// to open the specified submenu.
49    pub fn get_desktop_index_so_far(&self, submenu_handle: isize) -> Option<usize> {
50        self.tray_submenus
51            .iter()
52            .find(|(d, _)| matches!(d.handle.hmenu(), Some((_, handle)) if handle as isize == submenu_handle))
53            .map(|(_, ix)| *ix as usize)
54    }
55    /// Find first item/menu inside a submenu.
56    pub fn first_item_in_submenu(&self, submenu_handle: isize) -> Option<nwg::ControlHandle> {
57        // Submenus come before menu items:
58        self.tray_submenus
59            .iter()
60            .find_map(|(d, _)| {
61                let (parent, _) = d.handle.hmenu()?;
62                (parent as isize == submenu_handle).then_some(d.handle)
63            })
64            .or_else(|| {
65                self.tray_virtual_desktops_quick
66                    .iter()
67                    .find_map(|(item, _)| {
68                        let (parent, _) = item.handle.hmenu_item()?;
69                        (parent as isize == submenu_handle).then_some(item.handle)
70                    })
71            })
72    }
73
74    pub fn clear(&mut self) {
75        self.tray_virtual_desktops_quick.clear();
76        self.tray_separators.clear();
77        self.tray_submenus
78            .drain(..)
79            .rev() // Remove deepest nested submenus first
80            .for_each(|(menu, _)| crate::nwg_ext::menu_remove(&menu));
81    }
82
83    #[allow(dead_code)]
84    pub fn has_submenu(&self, menu_handle: isize) -> bool {
85        self.tray_submenus.iter().any(|(menu, _)| {
86            menu.handle
87                .hmenu()
88                .map_or(false, |(_, id)| id as isize == menu_handle)
89        })
90    }
91
92    /// Extra menu items that don't use numbers as access keys. These allow the
93    /// user to give specific keys as shortcuts for frequently visited virtual
94    /// desktops.
95    fn create_shortcut_items(&mut self, parent: nwg::ControlHandle) {
96        // Start with a separator:
97        let mut separator = Default::default();
98        nwg::MenuSeparator::builder()
99            .parent(parent)
100            .build(&mut separator)
101            .expect("Failed to build separator for quick switch menu's shortcut section");
102        self.tray_separators.push(separator);
103
104        for (shortcut, &virtual_desktop_ix) in &self.shortcuts {
105            let mut item = Default::default();
106            nwg::MenuItem::builder()
107                .text(&format!(
108                    "Virtual desktop {}\t&{shortcut}",
109                    virtual_desktop_ix.saturating_add(1)
110                ))
111                .parent(parent)
112                .build(&mut item)
113                .expect("Failed to build \"shortcut\" menu item for quick switch menu");
114            self.tray_virtual_desktops_quick
115                .push((item, virtual_desktop_ix));
116        }
117    }
118    /// If there are 10 items or less in a submenu then each one could get a
119    /// unique access key. This function will only create context menu items if
120    /// that is the case.
121    fn try_create_leaf_items(&mut self, parent: nwg::ControlHandle, range: Range<u32>) -> bool {
122        if range.len() <= 10 {
123            // Check if each item can have its own access key:
124            let mut last_digit = [0_u32; 10];
125            for ix in range.clone() {
126                last_digit[(ix % 10) as usize] += 1;
127            }
128
129            if last_digit.iter().all(|&x| x <= 1) {
130                // All last digits are unique, so can make a leaf menu
131                for ix in range {
132                    let tens = ix / 10;
133                    let ones = ix % 10;
134                    let mut item = Default::default();
135                    nwg::MenuItem::builder()
136                        .text(&format!(
137                            "Virtual desktop {}&{ones}",
138                            if tens == 0 {
139                                String::new()
140                            } else {
141                                tens.to_string()
142                            }
143                        ))
144                        .parent(parent)
145                        .build(&mut item)
146                        .expect("Failed to build \"leaf\" menu item for quick switch menu");
147
148                    // Note: we store 0-based index but show 1 based
149                    // index in UI
150                    self.tray_virtual_desktops_quick.push((item, ix - 1));
151                }
152                return true;
153            }
154        }
155        false
156    }
157    /// If there are too many items to assign each a unique access key then we
158    /// create up to 10 submenus that divide the range into smaller parts.
159    ///
160    /// Returns part of the range for the first created submenu, but changes its
161    /// start value so that it doesn't overlap with the submenus' access keys.
162    fn create_submenus(
163        &mut self,
164        parent: nwg::ControlHandle,
165        desktop_count: u32,
166        prefix: u32,
167        remaining_digits: u32,
168        start_digit: u32,
169    ) -> usize {
170        if remaining_digits == 0 {
171            return 0;
172        }
173        // remaining_digits: 2, unit: 100
174        let unit = 10_u32.pow(remaining_digits - 1);
175        let start = ((prefix * 10 + start_digit) * unit).max(1);
176        let end = ((prefix + 1) * unit * 10).min(desktop_count);
177        let end_digit = (end - 1) / unit % 10;
178        let max_digits = if prefix == 0 { 0 } else { prefix.ilog10() + 1 } + remaining_digits;
179
180        let get_range_for = |digit: u32| {
181            let start = if digit == start_digit {
182                start
183            } else {
184                // digit: 2, Start digit: 200
185                (prefix * 10 * unit) + digit * unit
186            };
187            let end = if digit == end_digit {
188                end
189            } else {
190                // digit: 2, Exclusive End digit: 300
191                (prefix * 10 * unit) + (digit + 1) * unit
192            };
193            (start, end)
194        };
195        for digit in start_digit..end_digit + 1 {
196            let (start, end) = get_range_for(digit);
197            let mut menu = Default::default();
198            let start_prefix = start / (unit * 10);
199            let start_suffix = start % (unit * 10);
200
201            let prefix_width = max_digits.saturating_sub(remaining_digits) as usize;
202            nwg::Menu::builder()
203                .text(&format!(
204                    "{}&{start_suffix:0suffix_width$} - {:0width$}",
205                    if prefix_width == 0 {
206                        debug_assert_eq!(start_prefix, 0);
207                        String::new()
208                    } else {
209                        format!("{start_prefix:0prefix_width$}")
210                    },
211                    end - 1,
212                    suffix_width = remaining_digits as usize,
213                    width = max_digits as usize
214                ))
215                .parent(parent)
216                .build(&mut menu)
217                .expect("Failed to build submenu for quick switch menu");
218
219            let handle = menu.handle;
220            // Note: we want to be taken to the number that the user has entered
221            // on their keypad (when this menu is selected that includes the
222            // first digit in the suffix).
223            self.tray_submenus.push((
224                menu,
225                (start_prefix * 10 + start_suffix % 10)
226                    // one-based to zero-based index:
227                    .saturating_sub(1),
228            ));
229            self.call_queue.push((
230                handle,
231                desktop_count,
232                prefix * 10 + digit,
233                remaining_digits - 1,
234            ));
235        }
236        (start_digit..end_digit + 1).len()
237    }
238    /// Create a menu that is easily navigable with keyboard access keys.
239    ///
240    /// Here is an example of what it can look like:
241    ///
242    /// ```text
243    /// quick -> &00 - 09 ------------> 0&1
244    ///          &10 - 19 --> 1&0       0&2
245    ///          &20 - 29     1&1       0&3
246    ///          &30 - 39     1&2       0&4
247    ///          &40 - 49     1&3       0&5
248    ///          &50 - 59     1&4       0&6
249    ///         ----------    1%5       0&7
250    ///          &6           1&6       0&8
251    ///          &7           1&7       0&9
252    ///          &8           1&8
253    ///          &9           1&9
254    /// ```
255    pub fn create_quick_switch_menu(&mut self, parent: nwg::ControlHandle, desktop_count: u32) {
256        self._create_quick_switch_menu(parent, desktop_count, 0, desktop_count.ilog10() + 1);
257    }
258    fn _create_quick_switch_menu(
259        &mut self,
260        parent: nwg::ControlHandle,
261        desktop_count: u32,
262        prefix: u32,
263        remaining_digits: u32,
264    ) {
265        let is_root = !self.is_recursive_call;
266        if remaining_digits == 0 {
267            return;
268        }
269        if remaining_digits == 1 {
270            let start = (prefix * 10).max(1);
271            let end = ((prefix + 1) * 10).min(desktop_count);
272            let _did_create = self.try_create_leaf_items(parent, start..end);
273            debug_assert!(
274                _did_create,
275                "Should have created leaf items, otherwise our range was incorrect, used the range: {:?}",
276                start..end
277            );
278            if !self.shortcuts_only_in_root || is_root {
279                self.create_shortcut_items(parent);
280            }
281            return;
282        }
283
284        /// Handles recursive calls.
285        struct Guard<'a> {
286            this: &'a mut QuickSwitchMenu,
287            /// `true` if we are in a recursive call.
288            is_recursive_call: bool,
289        }
290        impl<'a> Guard<'a> {
291            fn new(this: &'a mut QuickSwitchMenu) -> Self {
292                let is_recursive_call = this.is_recursive_call;
293                this.is_recursive_call = true;
294                Self {
295                    this,
296                    is_recursive_call,
297                }
298            }
299        }
300        impl Drop for Guard<'_> {
301            fn drop(&mut self) {
302                if !self.is_recursive_call {
303                    while let Some((parent, desktop_count, prefix, remaining_digits)) =
304                        self.this.call_queue.pop()
305                    {
306                        self.this._create_quick_switch_menu(
307                            parent,
308                            desktop_count,
309                            prefix,
310                            remaining_digits,
311                        );
312                    }
313                }
314                // Restore this after all recursive calls:
315                self.this.is_recursive_call = self.is_recursive_call;
316            }
317        }
318        let guard = Guard::new(self);
319        let this = &mut *guard.this;
320
321        let created_menus =
322            this.create_submenus(parent, desktop_count, prefix, remaining_digits, 0);
323        if created_menus >= 10 && prefix != 0 {
324            // All access keys already in use!
325            return;
326        }
327
328        // We can use any remaining digits after `end_digit` (the last used access
329        // key) as shortcuts for the items inside the first `0001-0999` range (or
330        // 01-09 more commonly).
331        let mut separator = Default::default();
332        nwg::MenuSeparator::builder()
333            .parent(parent)
334            .build(&mut separator)
335            .expect("Failed to build separator for quick switch menu");
336        this.tray_separators.push(separator);
337
338        if remaining_digits == 2 {
339            let start = (prefix * 100 + created_menus as u32).max(1);
340            let end = ((prefix * 10 + 1) * 10).min(desktop_count);
341            let _did_create = this.try_create_leaf_items(parent, start..end);
342            debug_assert!(
343                _did_create,
344                "Should have created trailing leaf items, otherwise our range was incorrect, used the range: {:?}",
345                start..end
346            );
347        } else {
348            this.create_submenus(
349                parent,
350                desktop_count,
351                prefix * 10,
352                remaining_digits - 1,
353                created_menus as u32,
354            );
355        }
356
357        if !this.shortcuts_only_in_root || is_root {
358            this.create_shortcut_items(parent);
359        }
360
361        // Handle recursive calls if we are the first call (not recursive
362        // ourself):
363        drop(guard);
364    }
365}
366impl Drop for QuickSwitchMenu {
367    fn drop(&mut self) {
368        self.clear();
369    }
370}