Skip to main content

tauri/menu/
submenu.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::sync::Arc;
6
7use super::run_item_main_thread;
8use super::Submenu;
9use super::{sealed::ContextMenuBase, IsMenuItem, MenuItemKind};
10use crate::menu::NativeIcon;
11use crate::menu::SubmenuInner;
12use crate::run_main_thread;
13use crate::{AppHandle, Manager, Position, Runtime, Window};
14use muda::{ContextMenu, Icon as MudaIcon, MenuId};
15
16impl<R: Runtime> super::ContextMenu for Submenu<R> {
17  #[cfg(target_os = "windows")]
18  fn hpopupmenu(&self) -> crate::Result<isize> {
19    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu())
20  }
21
22  fn popup<T: Runtime>(&self, window: Window<T>) -> crate::Result<()> {
23    self.popup_inner(window, None::<Position>)
24  }
25
26  fn popup_at<T: Runtime, P: Into<Position>>(
27    &self,
28    window: Window<T>,
29    position: P,
30  ) -> crate::Result<()> {
31    self.popup_inner(window, Some(position))
32  }
33}
34
35impl<R: Runtime> ContextMenuBase for Submenu<R> {
36  fn popup_inner<T: Runtime, P: Into<crate::Position>>(
37    &self,
38    window: crate::Window<T>,
39    position: Option<P>,
40  ) -> crate::Result<()> {
41    let position = position.map(Into::into);
42    run_item_main_thread!(self, move |self_: Self| {
43      #[cfg(target_os = "macos")]
44      if let Ok(view) = window.ns_view() {
45        unsafe {
46          self_
47            .inner()
48            .show_context_menu_for_nsview(view as _, position);
49        }
50      }
51
52      #[cfg(any(
53        target_os = "linux",
54        target_os = "dragonfly",
55        target_os = "freebsd",
56        target_os = "netbsd",
57        target_os = "openbsd"
58      ))]
59      if let Ok(w) = window.gtk_window() {
60        self_
61          .inner()
62          .show_context_menu_for_gtk_window(w.as_ref(), position);
63      }
64
65      #[cfg(windows)]
66      if let Ok(hwnd) = window.hwnd() {
67        unsafe {
68          self_
69            .inner()
70            .show_context_menu_for_hwnd(hwnd.0 as _, position);
71        }
72      }
73    })
74  }
75
76  fn inner_context(&self) -> &dyn muda::ContextMenu {
77    (*self.0).as_ref()
78  }
79
80  fn inner_context_owned(&self) -> Box<dyn muda::ContextMenu> {
81    Box::new((*self.0).as_ref().clone())
82  }
83}
84
85impl<R: Runtime> Submenu<R> {
86  /// Creates a new submenu.
87  pub fn new<M: Manager<R>, S: AsRef<str>>(
88    manager: &M,
89    text: S,
90    enabled: bool,
91  ) -> crate::Result<Self> {
92    let handle = manager.app_handle();
93    let app_handle = handle.clone();
94
95    let text = text.as_ref().to_owned();
96
97    let submenu = run_main_thread!(handle, || {
98      let submenu = muda::Submenu::new(text, enabled);
99      SubmenuInner {
100        id: submenu.id().clone(),
101        inner: Some(submenu),
102        app_handle,
103      }
104    })?;
105
106    Ok(Self(Arc::new(submenu)))
107  }
108
109  /// Create a new submenu with an icon.
110  pub fn new_with_icon<M: Manager<R>, S: AsRef<str>>(
111    manager: &M,
112    text: S,
113    enabled: bool,
114    icon: Option<crate::image::Image<'_>>,
115  ) -> crate::Result<Self> {
116    let handle = manager.app_handle();
117    let app_handle = handle.clone();
118    let text = text.as_ref().to_owned();
119    let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height()));
120    let submenu = run_main_thread!(handle, || {
121      let submenu = muda::Submenu::new(text, enabled);
122      if let Some((rgba, width, height)) = icon_data.clone() {
123        submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap()));
124      }
125      SubmenuInner {
126        id: submenu.id().clone(),
127        inner: Some(submenu),
128        app_handle,
129      }
130    })?;
131    Ok(Self(Arc::new(submenu)))
132  }
133
134  /// Create a new submenu with a native icon.
135  pub fn new_with_native_icon<M: Manager<R>, S: AsRef<str>>(
136    manager: &M,
137    text: S,
138    enabled: bool,
139    icon: Option<NativeIcon>,
140  ) -> crate::Result<Self> {
141    let handle = manager.app_handle();
142    let app_handle = handle.clone();
143    let text = text.as_ref().to_owned();
144    let submenu = run_main_thread!(handle, || {
145      let submenu = muda::Submenu::new(text, enabled);
146      if let Some(icon) = icon {
147        submenu.set_native_icon(Some(icon.into()));
148      }
149      SubmenuInner {
150        id: submenu.id().clone(),
151        inner: Some(submenu),
152        app_handle,
153      }
154    })?;
155    Ok(Self(Arc::new(submenu)))
156  }
157
158  /// Creates a new submenu with the specified id.
159  pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
160    manager: &M,
161    id: I,
162    text: S,
163    enabled: bool,
164  ) -> crate::Result<Self> {
165    let handle = manager.app_handle();
166    let app_handle = handle.clone();
167
168    let id = id.into();
169    let text = text.as_ref().to_owned();
170
171    let submenu = run_main_thread!(handle, || {
172      let submenu = muda::Submenu::with_id(id.clone(), text, enabled);
173      SubmenuInner {
174        id,
175        inner: Some(submenu),
176        app_handle,
177      }
178    })?;
179
180    Ok(Self(Arc::new(submenu)))
181  }
182
183  /// Create a new submenu with an id and an icon.
184  pub fn with_id_and_icon<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
185    manager: &M,
186    id: I,
187    text: S,
188    enabled: bool,
189    icon: Option<crate::image::Image<'_>>,
190  ) -> crate::Result<Self> {
191    let handle = manager.app_handle();
192    let app_handle = handle.clone();
193    let id = id.into();
194    let text = text.as_ref().to_owned();
195    let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height()));
196    let submenu = run_main_thread!(handle, || {
197      let submenu = muda::Submenu::with_id(id.clone(), text, enabled);
198      if let Some((rgba, width, height)) = icon_data.clone() {
199        submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap()));
200      }
201      SubmenuInner {
202        id,
203        inner: Some(submenu),
204        app_handle,
205      }
206    })?;
207    Ok(Self(Arc::new(submenu)))
208  }
209
210  /// Create a new submenu with an id and a native icon.
211  pub fn with_id_and_native_icon<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
212    manager: &M,
213    id: I,
214    text: S,
215    enabled: bool,
216    icon: Option<NativeIcon>,
217  ) -> crate::Result<Self> {
218    let handle = manager.app_handle();
219    let app_handle = handle.clone();
220    let id = id.into();
221    let text = text.as_ref().to_owned();
222    let submenu = run_main_thread!(handle, || {
223      let submenu = muda::Submenu::with_id(id.clone(), text, enabled);
224      if let Some(icon) = icon {
225        submenu.set_native_icon(Some(icon.into()));
226      }
227      SubmenuInner {
228        id,
229        inner: Some(submenu),
230        app_handle,
231      }
232    })?;
233    Ok(Self(Arc::new(submenu)))
234  }
235
236  /// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
237  pub fn with_items<M: Manager<R>, S: AsRef<str>>(
238    manager: &M,
239    text: S,
240    enabled: bool,
241    items: &[&dyn IsMenuItem<R>],
242  ) -> crate::Result<Self> {
243    let menu = Self::new(manager, text, enabled)?;
244    menu.append_items(items)?;
245    Ok(menu)
246  }
247
248  /// Creates a new menu with the specified id and given `items`.
249  /// It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
250  pub fn with_id_and_items<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
251    manager: &M,
252    id: I,
253    text: S,
254    enabled: bool,
255    items: &[&dyn IsMenuItem<R>],
256  ) -> crate::Result<Self> {
257    let menu = Self::with_id(manager, id, text, enabled)?;
258    menu.append_items(items)?;
259    Ok(menu)
260  }
261
262  pub(crate) fn inner(&self) -> &muda::Submenu {
263    (*self.0).as_ref()
264  }
265
266  /// The application handle associated with this type.
267  pub fn app_handle(&self) -> &AppHandle<R> {
268    &self.0.app_handle
269  }
270
271  /// Returns a unique identifier associated with this submenu.
272  pub fn id(&self) -> &MenuId {
273    &self.0.id
274  }
275
276  /// Add a menu item to the end of this submenu.
277  pub fn append(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
278    let kind = item.kind();
279    run_item_main_thread!(self, |self_: Self| {
280      (*self_.0).as_ref().append(kind.inner().inner_muda())
281    })?
282    .map_err(Into::into)
283  }
284
285  /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally.
286  pub fn append_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
287    for item in items {
288      self.append(*item)?
289    }
290
291    Ok(())
292  }
293
294  /// Add a menu item to the beginning of this submenu.
295  pub fn prepend(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
296    let kind = item.kind();
297    run_item_main_thread!(self, |self_: Self| {
298      (*self_.0).as_ref().prepend(kind.inner().inner_muda())
299    })?
300    .map_err(Into::into)
301  }
302
303  /// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally.
304  pub fn prepend_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
305    self.insert_items(items, 0)
306  }
307
308  /// Insert a menu item at the specified `position` in this submenu.
309  pub fn insert(&self, item: &dyn IsMenuItem<R>, position: usize) -> crate::Result<()> {
310    let kind = item.kind();
311    run_item_main_thread!(self, |self_: Self| {
312      (*self_.0)
313        .as_ref()
314        .insert(kind.inner().inner_muda(), position)
315    })?
316    .map_err(Into::into)
317  }
318
319  /// Insert menu items at the specified `position` in this submenu.
320  pub fn insert_items(&self, items: &[&dyn IsMenuItem<R>], position: usize) -> crate::Result<()> {
321    for (i, item) in items.iter().enumerate() {
322      self.insert(*item, position + i)?
323    }
324
325    Ok(())
326  }
327
328  /// Remove a menu item from this submenu.
329  pub fn remove(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
330    let kind = item.kind();
331    run_item_main_thread!(self, |self_: Self| {
332      (*self_.0).as_ref().remove(kind.inner().inner_muda())
333    })?
334    .map_err(Into::into)
335  }
336
337  /// Remove the menu item at the specified position from this submenu and returns it.
338  pub fn remove_at(&self, position: usize) -> crate::Result<Option<MenuItemKind<R>>> {
339    run_item_main_thread!(self, |self_: Self| {
340      (*self_.0)
341        .as_ref()
342        .remove_at(position)
343        .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
344    })
345  }
346
347  /// Retrieves the menu item matching the given identifier.
348  pub fn get<'a, I>(&self, id: &'a I) -> Option<MenuItemKind<R>>
349  where
350    I: ?Sized,
351    MenuId: PartialEq<&'a I>,
352  {
353    self
354      .items()
355      .unwrap_or_default()
356      .into_iter()
357      .find(|i| i.id() == &id)
358  }
359
360  /// Returns a list of menu items that has been added to this submenu.
361  pub fn items(&self) -> crate::Result<Vec<MenuItemKind<R>>> {
362    run_item_main_thread!(self, |self_: Self| {
363      (*self_.0)
364        .as_ref()
365        .items()
366        .into_iter()
367        .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
368        .collect::<Vec<_>>()
369    })
370  }
371
372  /// Get the text for this submenu.
373  pub fn text(&self) -> crate::Result<String> {
374    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
375  }
376
377  /// Set the text for this submenu. `text` could optionally contain
378  /// an `&` before a character to assign this character as the mnemonic
379  /// for this submenu. To display a `&` without assigning a mnemonic, use `&&`.
380  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
381    let text = text.as_ref().to_string();
382    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
383  }
384
385  /// Get whether this submenu is enabled or not.
386  pub fn is_enabled(&self) -> crate::Result<bool> {
387    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
388  }
389
390  /// Enable or disable this submenu.
391  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
392    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
393  }
394
395  /// Set this submenu as the Window menu for the application on macOS.
396  ///
397  /// This will cause macOS to automatically add window-switching items and
398  /// certain other items to the menu.
399  #[cfg(target_os = "macos")]
400  pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> {
401    run_item_main_thread!(self, |self_: Self| {
402      (*self_.0).as_ref().set_as_windows_menu_for_nsapp()
403    })?;
404    Ok(())
405  }
406
407  /// Set this submenu as the Help menu for the application on macOS.
408  ///
409  /// This will cause macOS to automatically add a search box to the menu.
410  ///
411  /// If no menu is set as the Help menu, macOS will automatically use any menu
412  /// which has a title matching the localized word "Help".
413  #[cfg(target_os = "macos")]
414  pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> {
415    run_item_main_thread!(self, |self_: Self| {
416      (*self_.0).as_ref().set_as_help_menu_for_nsapp()
417    })?;
418    Ok(())
419  }
420
421  /// Change this submenu icon or remove it.
422  pub fn set_icon(&self, icon: Option<crate::image::Image<'_>>) -> crate::Result<()> {
423    let icon = match icon {
424      Some(i) => Some(i.try_into()?),
425      None => None,
426    };
427    run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon))
428  }
429
430  /// Change this submenu icon to a native image or remove it.
431  ///
432  /// ## Platform-specific:
433  ///
434  /// - **Windows / Linux**: Unsupported.
435  pub fn set_native_icon(&self, _icon: Option<NativeIcon>) -> crate::Result<()> {
436    #[cfg(target_os = "macos")]
437    return run_item_main_thread!(self, |self_: Self| {
438      (*self_.0).as_ref().set_native_icon(_icon.map(Into::into))
439    });
440    #[allow(unreachable_code)]
441    Ok(())
442  }
443}