Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ objc2-app-kit = { version = "0.3", default-features = false, features = [
"NSEvent",
"NSGraphics",
"NSImage",
"NSMenu",
"NSOpenGLView",
"NSPasteboard",
"NSResponder",
Expand Down
18 changes: 17 additions & 1 deletion src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
dpi::{LogicalSize, Position},
event_loop::{EventLoop, EventLoopWindowTarget},
monitor::MonitorHandle,
platform_impl::{get_aux_state_mut, set_badge_label, set_dock_visibility, Parent},
platform_impl::{get_aux_state_mut, set_badge_label, set_dock_menu, set_dock_visibility, Parent},
window::{Window, WindowBuilder},
};

Expand Down Expand Up @@ -404,6 +404,18 @@ pub trait EventLoopWindowTargetExtMacOS {

/// Sets the badge label on macos dock
fn set_badge_label(&self, label: Option<String>);

/// Sets (or clears, with `None`) the menu shown when the user right-clicks
/// (Control-clicks / long-presses) the application's Dock icon. Pass an
/// [`objc2_app_kit::NSMenu`] built with any Cocoa-menu-construction crate
/// (e.g. `muda`'s [`Menu::ns_menu`](https://docs.rs/muda) exposes the
/// underlying `NSMenu`).
///
/// Can be called at any time after the event loop has started, including
/// from within a menu item's own click handler — e.g. to rebuild the menu
/// with an updated checkmark. The next right-click on the Dock icon picks
/// up the change; there is no need to re-set the menu on every click.
fn set_dock_menu(&self, menu: Option<objc2::rc::Retained<objc2_app_kit::NSMenu>>);
}

impl<T> EventLoopWindowTargetExtMacOS for EventLoopWindowTarget<T> {
Expand Down Expand Up @@ -455,4 +467,8 @@ impl<T> EventLoopWindowTargetExtMacOS for EventLoopWindowTarget<T> {
fn set_badge_label(&self, label: Option<String>) {
set_badge_label(label);
}

fn set_dock_menu(&self, menu: Option<objc2::rc::Retained<objc2_app_kit::NSMenu>>) {
set_dock_menu(menu);
}
}
29 changes: 29 additions & 0 deletions src/platform_impl/macos/app_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ use crate::{
platform::macos::ActivationPolicy,
platform_impl::platform::{
app_state::AppState,
dock_menu::get_dock_menu,
ffi::{id, BOOL, YES},
},
};

use objc2::rc::Retained;
use objc2::runtime::{
AnyClass as Class, AnyObject as Object, Bool, ClassBuilder as ClassDecl, Sel,
};
Expand Down Expand Up @@ -83,6 +85,10 @@ pub static APP_DELEGATE_CLASS: Lazy<AppDelegateClass> = Lazy::new(|| unsafe {
sel!(applicationSupportsSecureRestorableState:),
application_supports_secure_restorable_state as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(applicationDockMenu:),
application_dock_menu as extern "C" fn(_, _, _) -> id,
);
decl.add_ivar::<*mut c_void>(&CString::new(AUX_DELEGATE_STATE_NAME).unwrap());

AppDelegateClass(decl.register())
Expand Down Expand Up @@ -220,3 +226,26 @@ extern "C" fn application_supports_secure_restorable_state(_: &Object, _: Sel, _
trace!("Completed `applicationSupportsSecureRestorableState`");
YES
}

/// Returns the menu shown on a right-click / Control-click / long-press of
/// the application's Dock icon, as most recently registered via
/// [`crate::platform::macos::EventLoopWindowTargetExtMacOS::set_dock_menu`].
/// Returns `nil` (Cocoa's default minimal menu) if none was registered.
extern "C" fn application_dock_menu(_: &Object, _: Sel, _: id) -> id {
trace!("Triggered `applicationDockMenu`");
// `get_dock_menu` hands back a BORROWED pointer into the module's storage
// (still owned there) — we must retain+autorelease our own reference before
// handing it to Cocoa, matching the caller's expectation that the returned
// object stays valid for the (synchronous, main-thread) duration of the
// Dock-menu display, independent of whether `set_dock_menu` replaces the
// stored menu the very next moment.
let result = match get_dock_menu() {
Some(ptr) => unsafe {
let retained: Retained<objc2::runtime::NSObject> = Retained::retain(ptr.cast()).unwrap();
Retained::autorelease_return(retained) as id
},
None => std::ptr::null_mut(),
};
trace!("Completed `applicationDockMenu`");
result
}
60 changes: 60 additions & 0 deletions src/platform_impl/macos/dock_menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2014-2021 The winit contributors
// Copyright 2021-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0

use super::ffi::id;
use objc2::rc::Retained;
use objc2_app_kit::NSMenu;
use std::sync::Mutex;

/// The menu shown when the user right-clicks (or Control-clicks / long-presses)
/// the application's Dock icon. `None` means "no custom menu" — Cocoa falls
/// back to its default minimal menu (Show/Hide, Quit).
///
/// Stored as a raw pointer, not `Retained<NSMenu>`: Cocoa objects aren't
/// `Send`/`Sync`, and this is only ever touched from the main thread (same
/// invariant as the rest of this module's `id` usage, e.g. `app_state.rs`).
/// Retained manually (via `Retained::into_raw`/`from_raw`) rather than storing
/// a `Retained<NSMenu>` directly, because `applicationDockMenu:` must return a
/// value *synchronously* — there is no round-trip through `tao`'s event queue
/// like there is for e.g. `Event::Reopen`.
///
/// Wrapped in `MainThreadPtr` to satisfy `Sync` (raw pointers aren't
/// `Send`/`Sync` by default; see its doc comment for the actual invariant).
struct MainThreadPtr(Option<id>);
// SAFETY: `id` (an Objective-C object pointer) is only ever written or read
// from the main thread in this module — `set_dock_menu` takes a
// `Retained<NSMenu>`, which (like all Cocoa objects here) can only be
// constructed on the main thread, and `get_dock_menu` is only called from
// `application_dock_menu`, itself only ever invoked by Cocoa on the main
// thread. The `Mutex` exists solely to make the `static` legal, not for
// actual cross-thread coordination.
unsafe impl Send for MainThreadPtr {}
unsafe impl Sync for MainThreadPtr {}

static DOCK_MENU: Mutex<MainThreadPtr> = Mutex::new(MainThreadPtr(None));

/// Sets (or clears, with `None`) the application's Dock menu. Safe to call at
/// any point after the event loop has started, including from within a menu
/// item's own click handler (e.g. to rebuild the menu with updated
/// checkmarks) — the next right-click on the Dock icon picks up the change.
///
/// The previously registered menu (if any) is released.
pub fn set_dock_menu(menu: Option<Retained<NSMenu>>) {
let new_ptr = menu.map(|m| Retained::into_raw(m) as id);
let mut slot = DOCK_MENU.lock().unwrap();
let old_ptr = std::mem::replace(&mut slot.0, new_ptr);
if let Some(old_ptr) = old_ptr {
// SAFETY: `old_ptr` was produced by a prior `Retained::into_raw` call in
// this same function, so it's a valid, uniquely-owned +1 reference.
unsafe { drop(Retained::from_raw(old_ptr as *mut NSMenu)) };
}
}

/// Returns the currently registered Dock menu, if any, as a borrowed pointer
/// (still owned by this module — the caller must retain it before handing it
/// across an ownership boundary, e.g. back to Cocoa). Called from
/// `application_dock_menu` in `app_delegate.rs`.
pub(crate) fn get_dock_menu() -> Option<id> {
DOCK_MENU.lock().unwrap().0
}
2 changes: 2 additions & 0 deletions src/platform_impl/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod app_delegate;
mod app_state;
mod badge;
mod dock;
mod dock_menu;
mod event;
mod event_loop;
mod ffi;
Expand Down Expand Up @@ -37,6 +38,7 @@ use crate::{
};
pub(crate) use badge::set_badge_label;
pub(crate) use dock::set_dock_visibility;
pub(crate) use dock_menu::set_dock_menu;
pub(crate) use icon::PlatformIcon;

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down