From bd556b921d98892152eacd94591d04d9dce907bf Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Tue, 5 May 2026 14:11:31 +0200 Subject: [PATCH 1/2] fix(macos): clamp context-menu popup so it fits on-screen `popUpMenuPositioningItem:atLocation:inView:` does not auto-flip the menu when it would overflow the screen, so right-clicking near the bottom edge produces a menu with scroll arrows. Switch to `popUpContextMenu:withEvent:forView:`, which AppKit clamps and auto-flips against the screen's visible frame for free. The popup location is computed in the view's window's coordinate space and attached to a synthesized right-mouse-down `NSEvent` that drives the call, matching how AppKit's own contextual-menu path works. --- .changes/macos-popup-clamp.md | 5 +++ Cargo.toml | 2 + src/platform_impl/macos/mod.rs | 67 ++++++++++++++++++++++++++-------- 3 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 .changes/macos-popup-clamp.md diff --git a/.changes/macos-popup-clamp.md b/.changes/macos-popup-clamp.md new file mode 100644 index 00000000..18af0abc --- /dev/null +++ b/.changes/macos-popup-clamp.md @@ -0,0 +1,5 @@ +--- +"muda": patch +--- + +On macOS, fix context-menu popups overflowing off the bottom or right edge of the screen with scroll arrows by switching `show_context_menu_for_nsview` to `popUpContextMenu:withEvent:forView:`. AppKit clamps and auto-flips the menu against the active screen's visible frame for free, including menu bar / Dock margins and multi-monitor setups. diff --git a/Cargo.toml b/Cargo.toml index 2f21abc8..bdcff42c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ objc2-foundation = { version = "0.3.0", default-features = false, features = [ "std", "NSAttributedString", "NSData", + "NSDate", "NSDictionary", "NSGeometry", "NSString", @@ -68,6 +69,7 @@ objc2-app-kit = { version = "0.3.0", default-features = false, features = [ "NSApplication", "NSCell", "NSEvent", + "NSGraphicsContext", "NSImage", "NSMenu", "NSMenuItem", diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 4fe71efd..e00f9713 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -26,17 +26,18 @@ use objc2_app_kit::{ NSAboutPanelOptionApplicationIcon, NSAboutPanelOptionApplicationName, NSAboutPanelOptionApplicationVersion, NSAboutPanelOptionCredits, NSAboutPanelOptionVersion, NSApplication, NSControlStateValueOff, NSControlStateValueOn, NSEvent, NSEventModifierFlags, - NSImage, NSImageName, NSMenu, NSMenuDelegate, NSMenuItem, NSRunningApplication, NSView, + NSEventType, NSImage, NSImageName, NSMenu, NSMenuDelegate, NSMenuItem, NSRunningApplication, + NSView, }; use objc2_foundation::{ - ns_string, MainThreadMarker, NSAttributedString, NSDictionary, NSInteger, NSObject, NSPoint, - NSSize, NSString, + ns_string, MainThreadMarker, NSAttributedString, NSDate, NSDictionary, NSInteger, NSObject, + NSPoint, NSSize, NSString, }; use self::util::strip_mnemonic; use crate::{ accelerator::KeyAccelerator, - dpi::{LogicalPosition, Position}, + dpi::Position, icon::{Icon, NativeIcon}, items::*, util::{AddOp, Counter}, @@ -1197,21 +1198,55 @@ unsafe fn show_context_menu( let window = view.window().expect("view must be installed in a window"); let scale_factor = window.backingScaleFactor(); - let (location, in_view) = if let Some(pos) = position.map(|p| p.to_logical(scale_factor)) { - let view_rect = view.frame(); - let location = NSPoint::new(pos.x, view_rect.size.height - pos.y); - (location, Some(view)) - } else { - let mouse_location = unsafe { NSEvent::mouseLocation() }; - let pos = LogicalPosition { - x: mouse_location.x, - y: mouse_location.y, + + // Compute the popup location in `window`'s coordinate space (bottom-left + // origin). The synthesized event below carries this location, and AppKit + // interprets it relative to the event's window — which must be the same + // window the target view belongs to. + let location_in_window: NSPoint = + if let Some(pos) = position.map(|p| p.to_logical(scale_factor)) { + // `pos` is in the view's logical coordinate system with top-left + // origin (the rest of muda's API). Flip Y to AppKit's view coords + // (bottom-left), then convert to window coords. + let view_rect = view.frame(); + let view_point = NSPoint::new(pos.x, view_rect.size.height - pos.y); + unsafe { view.convertPoint_toView(view_point, None) } + } else { + // No explicit position → use the current mouse location. It is + // in screen coords; convert into the view's window's coords. + let mouse = unsafe { NSEvent::mouseLocation() }; + window.convertPointFromScreen(mouse) + }; + + // Synthesize a right-mouse-down event so we can call + // `popUpContextMenu:withEvent:forView:`, which (unlike + // `popUpMenuPositioningItem:atLocation:inView:`) auto-flips and clamps + // the menu against the screen's visible frame for free. + let timestamp = NSDate::timeIntervalSinceReferenceDate_class(); + let event = unsafe { + NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure( + NSEventType::RightMouseDown, + location_in_window, + NSEventModifierFlags(0), + timestamp, + window.windowNumber(), + None, + 0, + 1, + 1.0, + ) + }; + + let Some(event) = event else { + // Synthesizing an event can fail in pathological situations; fall + // back to the legacy positioning call so we still pop up something. + return unsafe { + ns_menu.popUpMenuPositioningItem_atLocation_inView(None, location_in_window, Some(view)) }; - let location = NSPoint::new(pos.x, pos.y); - (location, None) }; - unsafe { ns_menu.popUpMenuPositioningItem_atLocation_inView(None, location, in_view) } + unsafe { NSMenu::popUpContextMenu_withEvent_forView(ns_menu, &event, view) }; + true } impl NativeIcon { From 96dee27ec6f3cd4d73096c5e6874a8802ab00c5d Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Sat, 6 Jun 2026 22:54:11 +0200 Subject: [PATCH 2/2] fix(macos): don't let macOS inject plug-in items into the popup `popUpContextMenu:withEvent:forView:` automatically appends contextual-menu plug-in items (AutoFill, Services, third-party plug-ins) to the menu. The legacy positioning call never did that, so opt out via `setAllowsContextMenuPlugIns(false)` to keep the menu's contents exactly what the consumer built. --- src/platform_impl/macos/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index e00f9713..c7db0c10 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1245,6 +1245,12 @@ unsafe fn show_context_menu( }; }; + // `popUpContextMenu:withEvent:forView:` lets macOS append contextual-menu + // plug-in items (AutoFill, Services, third-party plug-ins) to the menu. + // The legacy positioning call never did that, so opt out to keep the + // menu's contents exactly what the consumer built. + ns_menu.setAllowsContextMenuPlugIns(false); + unsafe { NSMenu::popUpContextMenu_withEvent_forView(ns_menu, &event, view) }; true }