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 <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2023-07-27 14:56:01 +03:00 committed by GitHub
parent 5fbe39e995
commit c7ec320738
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 625 additions and 161 deletions

View file

@ -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.

View file

@ -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]

View file

@ -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)

View file

@ -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<Position>) {
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 {

View file

@ -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<Position>) {
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 {

View file

@ -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#"
<html>
<body>
<style>
main {
main {{
width: 100vw;
height: 100vh;
}
}}
</style>
<main>
<h4> WRYYYYYYYYYYYYYYYYYYYYYY! </h4>
@ -164,41 +169,66 @@ fn main() -> wry::Result<()> {
<button> Hi </button>
</main>
<script>
window.addEventListener('contextmenu', (e) => {
window.addEventListener('contextmenu', (e) => {{
e.preventDefault();
// if e.button is -1 on chromuim or e.buttons is 0 (as a fallback on webkit2gtk) then this event was fired by keyboard
if (e.button === -1 || e.buttons === 0) {
window.ipc.postMessage(`showContextMenu:${e.clientX},${e.clientY}`);
}
})
window.addEventListener('mouseup', (e) => {
if (e.button === 2) {
window.ipc.postMessage(`showContextMenu:${e.clientX},${e.clientY}`);
}
})
console.log(e)
// contextmenu was requested from keyboard
if ({condition}) {{
window.ipc.postMessage(`showContextMenuPos:${{e.clientX}},${{e.clientY}}`);
}}
}})
let x = true;
window.addEventListener('mouseup', (e) => {{
if (e.button === 2) {{
if (x) {{
window.ipc.postMessage(`showContextMenuPos:${{e.clientX}},${{e.clientY}}`);
}} else {{
window.ipc.postMessage(`showContextMenu`);
}}
x = !x;
}}
}})
</script>
</body>
</html>
"#;
"#,
);
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::<f64>().unwrap(), y.parse::<f64>().unwrap()))
.map(|(x, y)| (x.parse::<i32>().unwrap(), y.parse::<i32>().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<muda::Position>) {
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 {

233
src/dpi.rs Normal file
View file

@ -0,0 +1,233 @@
pub trait Pixel: Copy + Into<f64> {
fn from_f64(f: f64) -> Self;
fn cast<P: Pixel>(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<P> {
pub x: P,
pub y: P,
}
impl<P> LogicalPosition<P> {
#[inline]
pub const fn new(x: P, y: P) -> Self {
LogicalPosition { x, y }
}
}
impl<P: Pixel> LogicalPosition<P> {
#[inline]
pub fn from_physical<T: Into<PhysicalPosition<X>>, X: Pixel>(
physical: T,
scale_factor: f64,
) -> Self {
physical.into().to_logical(scale_factor)
}
#[inline]
pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<X> {
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<X: Pixel>(&self) -> LogicalPosition<X> {
LogicalPosition {
x: self.x.cast(),
y: self.y.cast(),
}
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalPosition<P> {
fn from((x, y): (X, X)) -> LogicalPosition<P> {
LogicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalPosition<P>> for (X, X) {
fn from(p: LogicalPosition<P>) -> (X, X) {
(p.x.cast(), p.y.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalPosition<P> {
fn from([x, y]: [X; 2]) -> LogicalPosition<P> {
LogicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<LogicalPosition<P>> for [X; 2] {
fn from(p: LogicalPosition<P>) -> [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<P> {
pub x: P,
pub y: P,
}
impl<P> PhysicalPosition<P> {
#[inline]
pub const fn new(x: P, y: P) -> Self {
PhysicalPosition { x, y }
}
}
impl<P: Pixel> PhysicalPosition<P> {
#[inline]
pub fn from_logical<T: Into<LogicalPosition<X>>, X: Pixel>(
logical: T,
scale_factor: f64,
) -> Self {
logical.into().to_physical(scale_factor)
}
#[inline]
pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalPosition<X> {
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<X: Pixel>(&self) -> PhysicalPosition<X> {
PhysicalPosition {
x: self.x.cast(),
y: self.y.cast(),
}
}
}
impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalPosition<P> {
fn from((x, y): (X, X)) -> PhysicalPosition<P> {
PhysicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalPosition<P>> for (X, X) {
fn from(p: PhysicalPosition<P>) -> (X, X) {
(p.x.cast(), p.y.cast())
}
}
impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalPosition<P> {
fn from([x, y]: [X; 2]) -> PhysicalPosition<P> {
PhysicalPosition::new(x.cast(), y.cast())
}
}
impl<P: Pixel, X: Pixel> From<PhysicalPosition<P>> for [X; 2] {
fn from(p: PhysicalPosition<P>) -> [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<i32>),
Logical(LogicalPosition<f64>),
}
impl Position {
pub fn new<S: Into<Position>>(position: S) -> Position {
position.into()
}
pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> LogicalPosition<P> {
match *self {
Position::Physical(position) => position.to_logical(scale_factor),
Position::Logical(position) => position.cast(),
}
}
pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<P> {
match *self {
Position::Physical(position) => position.cast(),
Position::Logical(position) => position.to_physical(scale_factor),
}
}
}
impl<P: Pixel> From<PhysicalPosition<P>> for Position {
#[inline]
fn from(position: PhysicalPosition<P>) -> Position {
Position::Physical(position.cast())
}
}
impl<P: Pixel> From<LogicalPosition<P>> for Position {
#[inline]
fn from(position: LogicalPosition<P>) -> Position {
Position::Logical(position.cast())
}
}

View file

@ -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<Position>) {
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: &gtk::ApplicationWindow, x: f64, y: f64) {
fn show_context_menu_for_gtk_window(
&self,
w: &gtk::ApplicationWindow,
position: Option<Position>,
) {
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<Position>) {
self.0
.borrow_mut()
.show_context_menu_for_nsview(view, position)
}
#[cfg(target_os = "macos")]

View file

@ -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(&gtk_window, x, y);
//! menu.show_context_menu_for_gtk_window(&gtk_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<Position>);
/// 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: &gtk::ApplicationWindow, x: f64, y: f64);
fn show_context_menu_for_gtk_window(
&self,
w: &gtk::ApplicationWindow,
position: Option<Position>,
);
/// 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<Position>);
/// Get the underlying NSMenu reserved for context menus.
#[cfg(target_os = "macos")]

View file

@ -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<W>(self, window: &W) -> Option<gtk::MenuBar>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
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<Position>) {
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: &gtk::ApplicationWindow, x: f64, y: f64) {
fn show_context_menu_for_gtk_window(
&self,
window: &gtk::ApplicationWindow,
position: Option<Position>,
) {
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<Position>) {
self.0
.borrow_mut()
.show_context_menu_for_nsview(view, position)
}
#[cfg(target_os = "macos")]

View file

@ -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<gtk::Widget>, 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(&gtk_item);
pub fn gtk_menubar_for_gtk_window<W>(&self, window: &W) -> Option<gtk::MenuBar>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.gtk_menubars.get(&(window.as_ptr() as u32)).cloned()
}
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<gtk::Widget>,
position: Option<Position>,
) {
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<gtk::Widget>, 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(&gtk_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<gtk::Widget>,
position: Option<Position>,
) {
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<gtk::Widget>,
position: Option<Position>,
) {
let (pos, window) = if let Some(pos) = position {
let window = widget.window();
(
pos.to_logical::<i32>(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 {

View file

@ -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<Position>) {
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<Position>) {
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<NativeIcon>) {
}
}
fn show_context_menu(ns_menu: id, view: id, position: Option<Position>) {
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 {

View file

@ -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<Position>) {
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<Position>) {
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<Rc<RefCell<MenuChild>>>) -> Option<Rc<RefC
None
}
fn show_context_menu(hwnd: HWND, hmenu: HMENU, x: f64, y: f64) {
fn show_context_menu(hwnd: HWND, hmenu: HMENU, position: Option<Position>) {
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::<i32>(scale_factor);
let mut pt = POINT {
x: pos.x as _,
y: pos.y as _,
};
ClientToScreen(hwnd, &mut point);
TrackPopupMenu(
hmenu,
TPM_LEFTALIGN,
point.x,
point.y,
0,
hwnd,
std::ptr::null(),
);
ClientToScreen(hwnd, &mut pt);
pt
} else {
let mut pt = POINT { x: 0, y: 0 };
GetCursorPos(&mut pt);
pt
};
TrackPopupMenu(hmenu, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, std::ptr::null());
}
}

View file

@ -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<S: AsRef<std::ffi::OsStr>>(string: S) -> Vec<u16> {
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<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
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
}
}
}