From c7ec3207388947b5572847e589eb494d0222373d Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Thu, 27 Jul 2023 14:56:01 +0300 Subject: [PATCH] refactor!: use optional `Position` type and fallback to cursor pos (#78) * refactor!: use optional `Position` type and fallback to cursor pos * impl gtk & change to use screen coords * impl macos * revert back to client coordinates * fix build * fix macos impl * enhance examples * fix serde feature * fix tests * lint --------- Co-authored-by: Lucas Nogueira --- .changes/context-menu-pos.md | 5 + Cargo.toml | 6 +- README.md | 1 + examples/tao.rs | 40 +++-- examples/winit.rs | 36 +++-- examples/wry.rs | 83 +++++++---- src/dpi.rs | 233 ++++++++++++++++++++++++++++++ src/items/submenu.rs | 22 ++- src/lib.rs | 27 ++-- src/menu.rs | 30 +++- src/platform_impl/gtk/mod.rs | 105 +++++++++----- src/platform_impl/macos/mod.rs | 47 +++--- src/platform_impl/windows/mod.rs | 53 +++---- src/platform_impl/windows/util.rs | 98 ++++++++++++- 14 files changed, 625 insertions(+), 161 deletions(-) create mode 100644 .changes/context-menu-pos.md create mode 100644 src/dpi.rs diff --git a/.changes/context-menu-pos.md b/.changes/context-menu-pos.md new file mode 100644 index 0000000..69c5fd3 --- /dev/null +++ b/.changes/context-menu-pos.md @@ -0,0 +1,5 @@ +--- +"muda": "minor" +--- + +**Breaking Change**: `ContextMenu::show_context_menu_for_hwnd`, `ContextMenu::show_context_menu_for_gtk_window` and `ContextMenu::show_context_menu_for_nsview` has been changed to take an optional `Position` type instead of `x` and `y` and if `None` is provided, it will use the current cursor position. diff --git a/Cargo.toml b/Cargo.toml index 0aefcfd..b25d5c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,14 @@ categories = [ "gui" ] default = [ "libxdo" ] libxdo = [ "dep:libxdo" ] common-controls-v6 = [ "windows-sys/Win32_UI_Controls" ] +serde = [ "dep:serde" ] [dependencies] crossbeam-channel = "0.5" keyboard-types = "0.6" once_cell = "1" thiserror = "1" +serde = { version = "1", optional = true } [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.48" @@ -30,7 +32,9 @@ features = [ "Win32_UI_Shell", "Win32_Globalization", "Win32_UI_Input_KeyboardAndMouse", - "Win32_System_SystemServices" + "Win32_System_SystemServices", + "Win32_UI_HiDpi", + "Win32_System_LibraryLoader" ] [target."cfg(target_os = \"linux\")".dependencies] diff --git a/README.md b/README.md index 43a760a..772540e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ muda is a Menu Utilities library for Desktop Applications. - `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog. - `libxdo`: Enables linking to `libxdo` on Linux which is used for the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu item. +- `serde`: Enables de/serializing the dpi types. ## Dependencies (Linux Only) diff --git a/examples/tao.rs b/examples/tao.rs index f536b4f..6e08217 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -6,7 +6,7 @@ use muda::{ accelerator::{Accelerator, Code, Modifiers}, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuEvent, MenuItem, - PredefinedMenuItem, Submenu, + PhysicalPosition, Position, PredefinedMenuItem, Submenu, }; #[cfg(target_os = "macos")] use tao::platform::macos::WindowExtMacOS; @@ -147,9 +147,9 @@ fn main() { } let menu_channel = MenuEvent::receiver(); + let mut window_cursor_position = PhysicalPosition { x: 0., y: 0. }; + let mut use_window_pos = false; - let mut x = 0_f64; - let mut y = 0_f64; event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; @@ -163,24 +163,33 @@ fn main() { window_id, .. } => { - if window_id == window2.id() { - x = position.x; - y = position.y; - } + window_cursor_position.x = position.x; + window_cursor_position.y = position.y; } Event::WindowEvent { event: WindowEvent::MouseInput { - state: ElementState::Pressed, + state: ElementState::Released, button: MouseButton::Right, .. }, window_id, .. } => { - if window_id == window2.id() { - show_context_menu(&window2, &file_m, x, y); - } + show_context_menu( + if window_id == window.id() { + &window + } else { + &window2 + }, + &file_m, + if use_window_pos { + Some(window_cursor_position.into()) + } else { + None + }, + ); + use_window_pos = !use_window_pos; } Event::MainEventsCleared => { window.request_redraw(); @@ -199,13 +208,14 @@ fn main() { }) } -fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) { +fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option) { + println!("Show context menu at position {position:?}"); #[cfg(target_os = "windows")] - menu.show_context_menu_for_hwnd(window.hwnd() as _, x, y); + menu.show_context_menu_for_hwnd(window.hwnd() as _, position); #[cfg(target_os = "linux")] - menu.show_context_menu_for_gtk_window(window.gtk_window(), x, y); + menu.show_context_menu_for_gtk_window(window.gtk_window(), position); #[cfg(target_os = "macos")] - menu.show_context_menu_for_nsview(window.ns_view() as _, x, y); + menu.show_context_menu_for_nsview(window.ns_view() as _, position); } fn load_icon(path: &std::path::Path) -> muda::icon::Icon { diff --git a/examples/winit.rs b/examples/winit.rs index 06494b1..691e10e 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -6,7 +6,7 @@ use muda::{ accelerator::{Accelerator, Code, Modifiers}, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuEvent, MenuItem, - PredefinedMenuItem, Submenu, + PhysicalPosition, Position, PredefinedMenuItem, Submenu, }; #[cfg(target_os = "macos")] use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS}; @@ -137,9 +137,9 @@ fn main() { } let menu_channel = MenuEvent::receiver(); + let mut window_cursor_position = PhysicalPosition { x: 0., y: 0. }; + let mut use_window_pos = false; - let mut x = 0_f64; - let mut y = 0_f64; event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; @@ -153,10 +153,8 @@ fn main() { window_id, .. } => { - if window_id == window2.id() { - x = position.x; - y = position.y; - } + window_cursor_position.x = position.x; + window_cursor_position.y = position.y; } Event::WindowEvent { event: @@ -168,9 +166,20 @@ fn main() { window_id, .. } => { - if window_id == window2.id() { - show_context_menu(&window2, &file_m, x, y); - } + show_context_menu( + if window_id == window.id() { + &window + } else { + &window2 + }, + &file_m, + if use_window_pos { + Some(window_cursor_position.into()) + } else { + None + }, + ); + use_window_pos = !use_window_pos; } Event::MainEventsCleared => { window.request_redraw(); @@ -187,11 +196,12 @@ fn main() { }) } -fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) { +fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option) { + println!("Show context menu at position {position:?}"); #[cfg(target_os = "windows")] - menu.show_context_menu_for_hwnd(window.hwnd() as _, x, y); + menu.show_context_menu_for_hwnd(window.hwnd() as _, position); #[cfg(target_os = "macos")] - menu.show_context_menu_for_nsview(window.ns_view() as _, x, y); + menu.show_context_menu_for_nsview(window.ns_view() as _, position); } fn load_icon(path: &std::path::Path) -> muda::icon::Icon { diff --git a/examples/wry.rs b/examples/wry.rs index a6293f5..7c79c6c 100644 --- a/examples/wry.rs +++ b/examples/wry.rs @@ -149,14 +149,19 @@ fn main() -> wry::Result<()> { window_m.set_windows_menu_for_nsapp(); } - const HTML: &str = r#" + #[cfg(windows)] + let condition = "e.button !== 2"; + #[cfg(not(windows))] + let condition = "e.button == 2 && e.buttons === 0"; + let html: String = format!( + r#"

WRYYYYYYYYYYYYYYYYYYYYYY!

@@ -164,41 +169,66 @@ fn main() -> wry::Result<()> {
- "#; + "#, + ); + let file_m_c = file_m.clone(); let handler = move |window: &Window, req: String| { - if let Some(rest) = req.strip_prefix("showContextMenu:") { - let (x, y) = rest + if &req == "showContextMenu" { + show_context_menu(window, &file_m_c, None) + } else if let Some(rest) = req.strip_prefix("showContextMenuPos:") { + let (x, mut y) = rest .split_once(',') - .map(|(x, y)| (x.parse::().unwrap(), y.parse::().unwrap())) + .map(|(x, y)| (x.parse::().unwrap(), y.parse::().unwrap())) .unwrap(); - if window.id() == window2_id { - show_context_menu(window, &window_m, x, y) + + #[cfg(target_os = "linux")] + { + if let Some(menu_bar) = menu_bar + .clone() + .gtk_menubar_for_gtk_window(window.gtk_window()) + { + use gtk::prelude::*; + y += menu_bar.allocated_height(); + } } + + show_context_menu( + window, + &file_m_c, + Some(muda::Position::Logical((x, y).into())), + ) } }; let webview = WebViewBuilder::new(window)? - .with_html(HTML)? + .with_html(&html)? .with_ipc_handler(handler.clone()) .build()?; let webview2 = WebViewBuilder::new(window2)? - .with_html(HTML)? + .with_html(html)? .with_ipc_handler(handler) .build()?; @@ -226,13 +256,14 @@ fn main() -> wry::Result<()> { }) } -fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) { +fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option) { + println!("Show context menu at position {position:?}"); #[cfg(target_os = "windows")] - menu.show_context_menu_for_hwnd(window.hwnd() as _, x, y); + menu.show_context_menu_for_hwnd(window.hwnd() as _, position); #[cfg(target_os = "linux")] - menu.show_context_menu_for_gtk_window(window.gtk_window(), x, y); + menu.show_context_menu_for_gtk_window(window.gtk_window(), position); #[cfg(target_os = "macos")] - menu.show_context_menu_for_nsview(window.ns_view() as _, x, y); + menu.show_context_menu_for_nsview(window.ns_view() as _, position); } fn load_icon(path: &std::path::Path) -> muda::icon::Icon { diff --git a/src/dpi.rs b/src/dpi.rs new file mode 100644 index 0000000..2431f90 --- /dev/null +++ b/src/dpi.rs @@ -0,0 +1,233 @@ +pub trait Pixel: Copy + Into { + fn from_f64(f: f64) -> Self; + fn cast(self) -> P { + P::from_f64(self.into()) + } +} + +impl Pixel for u8 { + fn from_f64(f: f64) -> Self { + f.round() as u8 + } +} +impl Pixel for u16 { + fn from_f64(f: f64) -> Self { + f.round() as u16 + } +} +impl Pixel for u32 { + fn from_f64(f: f64) -> Self { + f.round() as u32 + } +} +impl Pixel for i8 { + fn from_f64(f: f64) -> Self { + f.round() as i8 + } +} +impl Pixel for i16 { + fn from_f64(f: f64) -> Self { + f.round() as i16 + } +} +impl Pixel for i32 { + fn from_f64(f: f64) -> Self { + f.round() as i32 + } +} +impl Pixel for f32 { + fn from_f64(f: f64) -> Self { + f as f32 + } +} +impl Pixel for f64 { + fn from_f64(f: f64) -> Self { + f + } +} + +/// Checks that the scale factor is a normal positive `f64`. +/// +/// All functions that take a scale factor assert that this will return `true`. If you're sourcing scale factors from +/// anywhere other than winit, it's recommended to validate them using this function before passing them to winit; +/// otherwise, you risk panics. +#[inline] +pub fn validate_scale_factor(scale_factor: f64) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +} + +/// A position represented in logical pixels. +/// +/// The position is stored as floats, so please be careful. Casting floats to integers truncates the +/// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>` +/// implementation is provided which does the rounding for you. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct LogicalPosition

{ + pub x: P, + pub y: P, +} + +impl

LogicalPosition

{ + #[inline] + pub const fn new(x: P, y: P) -> Self { + LogicalPosition { x, y } + } +} + +impl LogicalPosition

{ + #[inline] + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) + } + + #[inline] + pub fn to_physical(&self, scale_factor: f64) -> PhysicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() * scale_factor; + let y = self.y.into() * scale_factor; + PhysicalPosition::new(x, y).cast() + } + + #[inline] + pub fn cast(&self) -> LogicalPosition { + LogicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } + } +} + +impl From<(X, X)> for LogicalPosition

{ + fn from((x, y): (X, X)) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(p: LogicalPosition

) -> (X, X) { + (p.x.cast(), p.y.cast()) + } +} + +impl From<[X; 2]> for LogicalPosition

{ + fn from([x, y]: [X; 2]) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(p: LogicalPosition

) -> [X; 2] { + [p.x.cast(), p.y.cast()] + } +} + +/// A position represented in physical pixels. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct PhysicalPosition

{ + pub x: P, + pub y: P, +} + +impl

PhysicalPosition

{ + #[inline] + pub const fn new(x: P, y: P) -> Self { + PhysicalPosition { x, y } + } +} + +impl PhysicalPosition

{ + #[inline] + pub fn from_logical>, X: Pixel>( + logical: T, + scale_factor: f64, + ) -> Self { + logical.into().to_physical(scale_factor) + } + + #[inline] + pub fn to_logical(&self, scale_factor: f64) -> LogicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() / scale_factor; + let y = self.y.into() / scale_factor; + LogicalPosition::new(x, y).cast() + } + + #[inline] + pub fn cast(&self) -> PhysicalPosition { + PhysicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } + } +} + +impl From<(X, X)> for PhysicalPosition

{ + fn from((x, y): (X, X)) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(p: PhysicalPosition

) -> (X, X) { + (p.x.cast(), p.y.cast()) + } +} + +impl From<[X; 2]> for PhysicalPosition

{ + fn from([x, y]: [X; 2]) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(p: PhysicalPosition

) -> [X; 2] { + [p.x.cast(), p.y.cast()] + } +} + +/// A position that's either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Position { + Physical(PhysicalPosition), + Logical(LogicalPosition), +} + +impl Position { + pub fn new>(position: S) -> Position { + position.into() + } + + pub fn to_logical(&self, scale_factor: f64) -> LogicalPosition

{ + match *self { + Position::Physical(position) => position.to_logical(scale_factor), + Position::Logical(position) => position.cast(), + } + } + + pub fn to_physical(&self, scale_factor: f64) -> PhysicalPosition

{ + match *self { + Position::Physical(position) => position.cast(), + Position::Logical(position) => position.to_physical(scale_factor), + } + } +} + +impl From> for Position { + #[inline] + fn from(position: PhysicalPosition

) -> Position { + Position::Physical(position.cast()) + } +} + +impl From> for Position { + #[inline] + fn from(position: LogicalPosition

) -> Position { + Position::Logical(position.cast()) + } +} diff --git a/src/items/submenu.rs b/src/items/submenu.rs index a129f7e..8b98519 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -4,7 +4,7 @@ use std::{cell::RefCell, rc::Rc}; -use crate::{util::AddOp, ContextMenu, IsMenuItem, MenuItemType}; +use crate::{util::AddOp, ContextMenu, IsMenuItem, MenuItemType, Position}; /// A menu that can be added to a [`Menu`] or another [`Submenu`]. /// @@ -155,8 +155,10 @@ impl ContextMenu for Submenu { } #[cfg(target_os = "windows")] - fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { - self.0.borrow_mut().show_context_menu_for_hwnd(hwnd, x, y) + fn show_context_menu_for_hwnd(&self, hwnd: isize, position: Option) { + self.0 + .borrow_mut() + .show_context_menu_for_hwnd(hwnd, position) } #[cfg(target_os = "windows")] @@ -170,10 +172,14 @@ impl ContextMenu for Submenu { } #[cfg(target_os = "linux")] - fn show_context_menu_for_gtk_window(&self, w: >k::ApplicationWindow, x: f64, y: f64) { + fn show_context_menu_for_gtk_window( + &self, + w: >k::ApplicationWindow, + position: Option, + ) { self.0 .borrow_mut() - .show_context_menu_for_gtk_window(w, x, y) + .show_context_menu_for_gtk_window(w, position) } #[cfg(target_os = "linux")] @@ -182,8 +188,10 @@ impl ContextMenu for Submenu { } #[cfg(target_os = "macos")] - fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) { - self.0.borrow_mut().show_context_menu_for_nsview(view, x, y) + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, position: Option) { + self.0 + .borrow_mut() + .show_context_menu_for_nsview(view, position) } #[cfg(target_os = "macos")] diff --git a/src/lib.rs b/src/lib.rs index 9c08f9c..bdf0f08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,14 +100,13 @@ //! # #[cfg(target_os = "macos")] //! # let nsview = 0 as *mut objc::runtime::Object; //! // --snip-- -//! let x = 100.0; -//! let y = 120.0; +//! let position = muda::PhysicalPosition { x: 100., y: 120. }; //! #[cfg(target_os = "windows")] -//! menu.show_context_menu_for_hwnd(window_hwnd, x, y); +//! menu.show_context_menu_for_hwnd(window_hwnd, Some(position.into())); //! #[cfg(target_os = "linux")] -//! menu.show_context_menu_for_gtk_window(>k_window, x, y); +//! menu.show_context_menu_for_gtk_window(>k_window, Some(position.into())); //! #[cfg(target_os = "macos")] -//! menu.show_context_menu_for_nsview(nsview, x, y); +//! menu.show_context_menu_for_nsview(nsview, Some(position.into())); //! ``` //! # Processing menu events //! @@ -133,6 +132,7 @@ use once_cell::sync::{Lazy, OnceCell}; mod about_metadata; pub mod accelerator; pub mod builders; +mod dpi; mod error; mod items; mod menu; @@ -144,6 +144,7 @@ mod util; extern crate objc; pub use about_metadata::AboutMetadata; +pub use dpi::*; pub use error::*; pub use items::*; pub use menu::Menu; @@ -250,9 +251,9 @@ pub trait ContextMenu { /// Shows this menu as a context menu inside a win32 window. /// - /// `x` and `y` are relative to the window's top-left corner. + /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. #[cfg(target_os = "windows")] - fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64); + fn show_context_menu_for_hwnd(&self, hwnd: isize, position: Option); /// Attach the menu subclass handler to the given hwnd /// so you can recieve events from that window using [MenuEvent::receiver] @@ -267,9 +268,13 @@ pub trait ContextMenu { /// Shows this menu as a context menu inside a [`gtk::ApplicationWindow`] /// - /// `x` and `y` are relative to the window's top-left corner. + /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. #[cfg(target_os = "linux")] - fn show_context_menu_for_gtk_window(&self, w: >k::ApplicationWindow, x: f64, y: f64); + fn show_context_menu_for_gtk_window( + &self, + w: >k::ApplicationWindow, + position: Option, + ); /// Get the underlying gtk menu reserved for context menus. #[cfg(target_os = "linux")] @@ -277,9 +282,9 @@ pub trait ContextMenu { /// Shows this menu as a context menu for the specified `NSView`. /// - /// `x` and `y` are relative to the window's top-left corner. + /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. #[cfg(target_os = "macos")] - fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64); + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, position: Option); /// Get the underlying NSMenu reserved for context menus. #[cfg(target_os = "macos")] diff --git a/src/menu.rs b/src/menu.rs index f529cc9..fcdcecf 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -4,7 +4,7 @@ use std::{cell::RefCell, rc::Rc}; -use crate::{util::AddOp, ContextMenu, IsMenuItem}; +use crate::{util::AddOp, ContextMenu, IsMenuItem, Position}; /// A root menu that can be added to a Window on Windows and Linux /// and used as the app global menu on macOS. @@ -243,6 +243,16 @@ impl Menu { self.0.borrow().is_visible_on_gtk_window(window) } + #[cfg(target_os = "linux")] + /// Returns the [`gtk::MenuBar`] that is associated with this window if it exists. + /// This is useful to get information about the menubar for example its height. + pub fn gtk_menubar_for_gtk_window(self, window: &W) -> Option + where + W: gtk::prelude::IsA, + { + self.0.borrow().gtk_menubar_for_gtk_window(window) + } + /// Returns whether this menu visible on a on a win32 window #[cfg(target_os = "windows")] pub fn is_visible_on_hwnd(&self, hwnd: isize) -> bool { @@ -269,8 +279,8 @@ impl ContextMenu for Menu { } #[cfg(target_os = "windows")] - fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { - self.0.borrow().show_context_menu_for_hwnd(hwnd, x, y) + fn show_context_menu_for_hwnd(&self, hwnd: isize, position: Option) { + self.0.borrow().show_context_menu_for_hwnd(hwnd, position) } #[cfg(target_os = "windows")] @@ -284,10 +294,14 @@ impl ContextMenu for Menu { } #[cfg(target_os = "linux")] - fn show_context_menu_for_gtk_window(&self, window: >k::ApplicationWindow, x: f64, y: f64) { + fn show_context_menu_for_gtk_window( + &self, + window: >k::ApplicationWindow, + position: Option, + ) { self.0 .borrow_mut() - .show_context_menu_for_gtk_window(window, x, y) + .show_context_menu_for_gtk_window(window, position) } #[cfg(target_os = "linux")] @@ -296,8 +310,10 @@ impl ContextMenu for Menu { } #[cfg(target_os = "macos")] - fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) { - self.0.borrow_mut().show_context_menu_for_nsview(view, x, y) + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, position: Option) { + self.0 + .borrow_mut() + .show_context_menu_for_nsview(view, position) } #[cfg(target_os = "macos")] diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index ae2f65f..752cd20 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -12,7 +12,7 @@ use crate::{ icon::{Icon, NativeIcon}, items::*, util::{AddOp, Counter}, - MenuEvent, MenuItemType, + MenuEvent, MenuItemType, Position, }; use accelerator::{from_gtk_mnemonic, parse_accelerator, to_gtk_mnemonic}; use gtk::{prelude::*, Orientation}; @@ -314,24 +314,19 @@ impl Menu { .unwrap_or(false) } - pub fn show_context_menu_for_gtk_window(&self, window: &impl IsA, x: f64, y: f64) { - if let Some(window) = window.window() { - let gtk_menu = gtk::Menu::new(); + pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option + where + W: gtk::prelude::IsA, + { + self.gtk_menubars.get(&(window.as_ptr() as u32)).cloned() + } - for item in self.items() { - let gtk_item = item.make_gtk_menu_item(0, None, false).unwrap(); - gtk_menu.append(>k_item); - } - gtk_menu.show_all(); - - gtk_menu.popup_at_rect( - &window, - &gdk::Rectangle::new(x as _, y as _, 0, 0), - gdk::Gravity::NorthWest, - gdk::Gravity::NorthWest, - None, - ); - } + pub fn show_context_menu_for_gtk_window( + &mut self, + widget: &impl IsA, + position: Option, + ) { + show_context_menu(self.gtk_context_menu(), widget, position) } pub fn gtk_context_menu(&mut self) -> gtk::Menu { @@ -775,24 +770,12 @@ impl MenuChild { .collect() } - pub fn show_context_menu_for_gtk_window(&self, window: &impl IsA, x: f64, y: f64) { - if let Some(window) = window.window() { - let gtk_menu = gtk::Menu::new(); - - for item in self.items() { - let gtk_item = item.make_gtk_menu_item(0, None, false).unwrap(); - gtk_menu.append(>k_item); - } - gtk_menu.show_all(); - - gtk_menu.popup_at_rect( - &window, - &gdk::Rectangle::new(x as _, y as _, 0, 0), - gdk::Gravity::NorthWest, - gdk::Gravity::NorthWest, - None, - ); - } + pub fn show_context_menu_for_gtk_window( + &mut self, + widget: &impl IsA, + position: Option, + ) { + show_context_menu(self.gtk_context_menu(), widget, position) } pub fn gtk_context_menu(&mut self) -> gtk::Menu { @@ -1197,6 +1180,56 @@ impl dyn crate::IsMenuItem + '_ { } } +fn show_context_menu( + gtk_menu: gtk::Menu, + widget: &impl IsA, + position: Option, +) { + let (pos, window) = if let Some(pos) = position { + let window = widget.window(); + ( + pos.to_logical::(window.as_ref().map(|w| w.scale_factor()).unwrap_or(1) as _) + .into(), + window, + ) + } else { + let window = widget.screen().and_then(|s| s.root_window()); + ( + window + .as_ref() + .and_then(|w| { + w.display() + .default_seat() + .and_then(|s| s.pointer()) + .map(|s| { + let p = s.position(); + (p.1, p.2) + }) + }) + .unwrap_or_default(), + window, + ) + }; + + if let Some(window) = window { + let mut event = gdk::Event::new(gdk::EventType::ButtonPress); + event.set_device( + window + .display() + .default_seat() + .and_then(|d| d.pointer()) + .as_ref(), + ); + gtk_menu.popup_at_rect( + &window, + &gdk::Rectangle::new(pos.0, pos.1, 0, 0), + gdk::Gravity::NorthWest, + gdk::Gravity::NorthWest, + Some(&event), + ); + } +} + impl PredfinedMenuItemType { #[cfg(feature = "libxdo")] fn xdo_keys(&self) -> &str { diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 383ea5f..4fda2f5 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -28,7 +28,7 @@ use crate::{ icon::{Icon, NativeIcon}, items::*, util::{AddOp, Counter}, - IsMenuItem, MenuEvent, MenuItemType, + IsMenuItem, LogicalPosition, MenuEvent, MenuItemType, Position, }; static COUNTER: Counter = Counter::new(); @@ -153,15 +153,8 @@ impl Menu { unsafe { NSApp().setMainMenu_(nil) } } - pub fn show_context_menu_for_nsview(&self, view: id, x: f64, y: f64) { - unsafe { - let window: id = msg_send![view, window]; - let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; - let view_point = NSPoint::new(x / scale_factor, y / scale_factor); - let view_rect: NSRect = msg_send![view, frame]; - let location = NSPoint::new(view_point.x, view_rect.size.height - view_point.y); - msg_send![self.ns_menu, popUpMenuPositioningItem: nil atLocation: location inView: view] - } + pub fn show_context_menu_for_nsview(&self, view: id, position: Option) { + show_context_menu(self.ns_menu, view, position) } pub fn ns_menu(&self) -> *mut std::ffi::c_void { @@ -532,15 +525,8 @@ impl MenuChild { .collect() } - pub fn show_context_menu_for_nsview(&self, view: id, x: f64, y: f64) { - unsafe { - let window: id = msg_send![view, window]; - let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; - let view_point = NSPoint::new(x / scale_factor, y / scale_factor); - let view_rect: NSRect = msg_send![view, frame]; - let location = NSPoint::new(view_point.x, view_rect.size.height - view_point.y); - msg_send![self.ns_menu.1, popUpMenuPositioningItem: nil atLocation: location inView: view] - } + pub fn show_context_menu_for_nsview(&self, view: id, position: Option) { + show_context_menu(self.ns_menu.1, view, position) } pub fn set_windows_menu_for_nsapp(&self) { @@ -977,6 +963,29 @@ fn menuitem_set_native_icon(menuitem: id, icon: Option) { } } +fn show_context_menu(ns_menu: id, view: id, position: Option) { + unsafe { + let window: id = msg_send![view, window]; + let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; + let (location, in_view) = if let Some(pos) = position.map(|p| p.to_logical(scale_factor)) { + let view_rect: NSRect = msg_send![view, frame]; + let location = NSPoint::new(pos.x, view_rect.size.height - pos.y); + (location, view) + } else { + let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation]; + let pos = Position::Logical(LogicalPosition { + x: mouse_location.x, + y: mouse_location.y, + }); + let pos = pos.to_logical(scale_factor); + let location = NSPoint::new(pos.x, pos.y); + (location, nil) + }; + + msg_send![ns_menu, popUpMenuPositioningItem: nil atLocation: location inView: in_view] + } +} + impl NativeIcon { unsafe fn named_img(self) -> id { match self { diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 88de8d9..02b4e53 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -14,7 +14,7 @@ use crate::{ items::PredfinedMenuItemType, util::{AddOp, Counter}, AboutMetadata, CheckMenuItem, IconMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemType, - PredefinedMenuItem, Submenu, + Position, PredefinedMenuItem, Submenu, }; use std::{ cell::{RefCell, RefMut}, @@ -31,12 +31,13 @@ use windows_sys::Win32::{ Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass}, WindowsAndMessaging::{ AppendMenuW, CreateAcceleratorTableW, CreateMenu, CreatePopupMenu, - DestroyAcceleratorTable, DrawMenuBar, EnableMenuItem, GetMenu, GetMenuItemInfoW, - InsertMenuW, PostQuitMessage, RemoveMenu, SendMessageW, SetMenu, SetMenuItemInfoW, - ShowWindow, TrackPopupMenu, HACCEL, HMENU, MENUITEMINFOW, MFS_CHECKED, MFS_DISABLED, - MF_BYCOMMAND, MF_BYPOSITION, MF_CHECKED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, - MF_SEPARATOR, MF_STRING, MF_UNCHECKED, MIIM_BITMAP, MIIM_STATE, MIIM_STRING, SW_HIDE, - SW_MAXIMIZE, SW_MINIMIZE, TPM_LEFTALIGN, WM_CLOSE, WM_COMMAND, WM_DESTROY, + DestroyAcceleratorTable, DrawMenuBar, EnableMenuItem, GetCursorPos, GetMenu, + GetMenuItemInfoW, InsertMenuW, PostQuitMessage, RemoveMenu, SendMessageW, SetMenu, + SetMenuItemInfoW, ShowWindow, TrackPopupMenu, HACCEL, HMENU, MENUITEMINFOW, + MFS_CHECKED, MFS_DISABLED, MF_BYCOMMAND, MF_BYPOSITION, MF_CHECKED, MF_DISABLED, + MF_ENABLED, MF_GRAYED, MF_POPUP, MF_SEPARATOR, MF_STRING, MF_UNCHECKED, MIIM_BITMAP, + MIIM_STATE, MIIM_STRING, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, TPM_LEFTALIGN, WM_CLOSE, + WM_COMMAND, WM_DESTROY, }, }, }; @@ -360,8 +361,8 @@ impl Menu { .unwrap_or(false) } - pub fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { - show_context_menu(hwnd, self.hpopupmenu, x, y) + pub fn show_context_menu_for_hwnd(&self, hwnd: isize, position: Option) { + show_context_menu(hwnd, self.hpopupmenu, position) } } @@ -779,8 +780,8 @@ impl MenuChild { .collect() } - pub fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { - show_context_menu(hwnd, self.hpopupmenu, x, y) + pub fn show_context_menu_for_hwnd(&self, hwnd: isize, position: Option) { + show_context_menu(hwnd, self.hpopupmenu, position) } pub fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) { @@ -826,22 +827,24 @@ fn find_by_id(id: u32, children: &Vec>>) -> Option) { unsafe { - let mut point = POINT { - x: x as _, - y: y as _, + let pt = if let Some(pos) = position { + let dpi = util::hwnd_dpi(hwnd); + let scale_factor = util::dpi_to_scale_factor(dpi); + let pos = pos.to_physical::(scale_factor); + let mut pt = POINT { + x: pos.x as _, + y: pos.y as _, + }; + ClientToScreen(hwnd, &mut pt); + pt + } else { + let mut pt = POINT { x: 0, y: 0 }; + GetCursorPos(&mut pt); + pt }; - ClientToScreen(hwnd, &mut point); - TrackPopupMenu( - hmenu, - TPM_LEFTALIGN, - point.x, - point.y, - 0, - hwnd, - std::ptr::null(), - ); + TrackPopupMenu(hmenu, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, std::ptr::null()); } } diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 77df4d7..f691ff7 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -4,7 +4,21 @@ use std::ops::{Deref, DerefMut}; -use windows_sys::Win32::UI::WindowsAndMessaging::ACCEL; +use once_cell::sync::Lazy; +use windows_sys::{ + core::HRESULT, + Win32::{ + Foundation::{FARPROC, HWND, S_OK}, + Graphics::Gdi::{ + GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST, + }, + System::LibraryLoader::{GetProcAddress, LoadLibraryW}, + UI::{ + HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE}, + WindowsAndMessaging::{IsProcessDPIAware, ACCEL}, + }, + }, +}; pub fn encode_wide>(string: S) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) @@ -68,3 +82,85 @@ pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE { unsafe { &__ImageBase as *const _ as _ } } + +fn get_function_impl(library: &str, function: &str) -> FARPROC { + let library = encode_wide(library); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryW(library.as_ptr()) }; + if module == 0 { + return None; + } + + unsafe { GetProcAddress(module, function.as_ptr()) } +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl( + $lib, + concat!(stringify!($func), '\0'), + ) + .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) + }; +} + +pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; +pub type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, +) -> HRESULT; + +static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); +static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); + +pub const BASE_DPI: u32 = 96; +pub fn dpi_to_scale_factor(dpi: u32) -> f64 { + dpi as f64 / BASE_DPI as f64 +} + +#[allow(non_snake_case)] +pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + let hdc = GetDC(hwnd); + if hdc == 0 { + panic!("[tao] `GetDC` returned null!"); + } + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match GetDpiForWindow(hwnd) { + 0 => BASE_DPI, // 0 is returned if hwnd is invalid + dpi => dpi as u32, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if monitor == 0 { + return BASE_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK { + dpi_x as u32 + } else { + BASE_DPI + } + } else { + // We are on Vista or later. + if IsProcessDPIAware() == 1 { + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + GetDeviceCaps(hdc, LOGPIXELSX) as u32 + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + BASE_DPI + } + } +}