diff --git a/Cargo.toml b/Cargo.toml index 8cfd23c14c..19eadb8ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,13 @@ [package] -name = "winit" +name = "floem-winit" version = "0.29.4" authors = ["The winit contributors", "Pierre Krieger "] -description = "Cross-platform window creation library." +description = "Unofficial winit fork for Floem" edition = "2021" -keywords = ["windowing"] license = "Apache-2.0" readme = "README.md" -repository = "https://github.com/rust-windowing/winit" +repository = "https://github.com/lapce/winit" documentation = "https://docs.rs/winit" -categories = ["gui"] rust-version = "1.65.0" [package.metadata.docs.rs] diff --git a/README.md b/README.md index 1071920c13..c954281bcd 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,3 @@ -# winit - Cross-platform window creation and management in Rust +You are probably looking for the [real winit crate](https://crates.io/crates/winit). -[![Crates.io](https://img.shields.io/crates/v/winit.svg)](https://crates.io/crates/winit) -[![Docs.rs](https://docs.rs/winit/badge.svg)](https://docs.rs/winit) -[![CI Status](https://github.com/rust-windowing/winit/workflows/CI/badge.svg)](https://github.com/rust-windowing/winit/actions) - -```toml -[dependencies] -winit = "0.29.4" -``` - -## [Documentation](https://docs.rs/winit) - -For features _within_ the scope of winit, see [FEATURES.md](FEATURES.md). - -For features _outside_ the scope of winit, see [Are we GUI Yet?](https://areweguiyet.com/) and [Are we game yet?](https://arewegameyet.rs/), depending on what kind of project you're looking to do. - -## Contact Us - -Join us in any of these: - -[![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) -[![Libera.Chat](https://img.shields.io/badge/libera.chat-%23winit-red.svg)](https://web.libera.chat/#winit) - -## Usage - -Winit is a window creation and management library. It can create windows and lets you handle -events (for example: the window being resized, a key being pressed, a mouse movement, etc.) -produced by the window. - -Winit is designed to be a low-level brick in a hierarchy of libraries. Consequently, in order to -show something on the window you need to use the platform-specific getters provided by winit, or -another library. - -### Cargo Features - -Winit provides the following features, which can be enabled in your `Cargo.toml` file: -* `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). -* `x11` (enabled by default): On Unix platform, compiles with the X11 backend -* `wayland` (enabled by default): On Unix platform, compiles with the Wayland backend -* `mint`: Enables mint (math interoperability standard types) conversions. - -## MSRV Policy - -This crate's Minimum Supported Rust Version (MSRV) is **1.65**. Changes to -the MSRV will be accompanied by a minor version bump. - -As a **tentative** policy, the upper bound of the MSRV is given by the following -formula: - -``` -min(sid, stable - 3) -``` - -Where `sid` is the current version of `rustc` provided by [Debian Sid], and -`stable` is the latest stable version of Rust. This bound may be broken in case of a major ecosystem shift or a security vulnerability. - -[Debian Sid]: https://packages.debian.org/sid/rustc - -The exception is for the Android platform, where a higher Rust version -must be used for certain Android features. In this case, the MSRV will be -capped at the latest stable version of Rust minus three. This inconsistency is -not reflected in Cargo metadata, as it is not powerful enough to expose this -restriction. - -All crates in the [`rust-windowing`] organizations have the -same MSRV policy. - -[`rust-windowing`]: https://github.com/rust-windowing - -### Platform-specific usage - -#### Wayland - -Note that windows don't appear on Wayland until you draw/present to them. - -#### WebAssembly - -To run the web example: `cargo run-wasm --example web` - -Winit supports compiling to the `wasm32-unknown-unknown` target with `web-sys`. - -On the web platform, a Winit window is backed by a `` element. You can -either [provide Winit with a `` element][web with_canvas], or [let Winit -create a `` element which you can then retrieve][web canvas getter] and -insert it into the DOM yourself. - -For the example code using Winit with WebAssembly, check out the [web example]. For -information on using Rust on WebAssembly, check out the [Rust and WebAssembly -book]. - -[web with_canvas]: https://docs.rs/winit/latest/wasm32-unknown-unknown/winit/platform/web/trait.WindowBuilderExtWebSys.html#tymethod.with_canvas -[web canvas getter]: https://docs.rs/winit/latest/wasm32-unknown-unknown/winit/platform/web/trait.WindowExtWebSys.html#tymethod.canvas -[web example]: ./examples/web.rs -[Rust and WebAssembly book]: https://rustwasm.github.io/book/ - -#### Android - -The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/latest/ndk/) crate. - -Native Android applications need some form of "glue" crate that is responsible -for defining the main entry point for your Rust application as well as tracking -various life-cycle events and synchronizing with the main JVM thread. - -Winit uses the [android-activity](https://github.com/rib/android-activity) as a -glue crate (prior to `0.28` it used -[ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue)). - -The version of the glue crate that your application depends on _must_ match the -version that Winit depends on because the glue crate is responsible for your -application's main entry point. If Cargo resolves multiple versions, they will -clash. - -`winit` glue compatibility table: - -| winit | ndk-glue | -| :---: | :--------------------------: | -| 0.29 | `android-activity = "0.5"` | -| 0.28 | `android-activity = "0.4"` | -| 0.27 | `ndk-glue = "0.7"` | -| 0.26 | `ndk-glue = "0.5"` | -| 0.25 | `ndk-glue = "0.3"` | -| 0.24 | `ndk-glue = "0.2"` | - -The recommended way to avoid a conflict with the glue version is to avoid explicitly -depending on the `android-activity` crate, and instead consume the API that -is re-exported by Winit under `winit::platform::android::activity::*` - -Running on an Android device needs a dynamic system library. Add this to Cargo.toml: - -```toml -[lib] -name = "main" -crate-type = ["cdylib"] -``` - -All Android applications are based on an `Activity` subclass, and the -`android-activity` crate is designed to support different choices for this base -class. Your application _must_ specify the base class it needs via a feature flag: - -| Base Class | Feature Flag | Notes | -| :--------------: | :---------------: | :-----: | -| `NativeActivity` | `android-native-activity` | Built-in to Android - it is possible to use without compiling any Java or Kotlin code. Java or Kotlin code may be needed to subclass `NativeActivity` to access some platform features. It does not derive from the [`AndroidAppCompat`] base class.| -| [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`], a defacto standard `Activity` base class that helps support a wider range of Android versions. Requires a build system that can compile Java or Kotlin and fetch Android dependencies from a [Maven repository][agdk_jetpack] (or link with an embedded [release][agdk_releases] of [`GameActivity`]) | - -[`GameActivity`]: https://developer.android.com/games/agdk/game-activity -[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input -[`AndroidAppCompat`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity -[agdk_jetpack]: https://developer.android.com/jetpack/androidx/releases/games -[agdk_releases]: https://developer.android.com/games/agdk/download#agdk-libraries -[Gradle]: https://developer.android.com/studio/build - -For more details, refer to these `android-activity` [example applications](https://github.com/rib/android-activity/tree/main/examples). - -##### Converting from `ndk-glue` to `android-activity` - -If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk`, then the minimal changes would be: -1. Remove `ndk-glue` from your `Cargo.toml` -2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.29.4", features = [ "android-native-activity" ] }` -3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize logging as above). -4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your event loop (as shown above). - -#### MacOS - -A lot of functionality expects the application to be ready before you start -doing anything; this includes creating windows, fetching monitors, drawing, -and so on, see issues [#2238], [#2051] and [#2087]. - -If you encounter problems, you should try doing your initialization inside -`Event::Resumed`. - -#### iOS - -Similar to macOS, iOS's main `UIApplicationMain` does some init work that's required -by all UI-related code (see issue [#1705]). It would be best to consider creating your windows -inside `Event::Resumed`. - - -[#2238]: https://github.com/rust-windowing/winit/issues/2238 -[#2051]: https://github.com/rust-windowing/winit/issues/2051 -[#2087]: https://github.com/rust-windowing/winit/issues/2087 -[#1705]: https://github.com/rust-windowing/winit/issues/1705 - -#### Redox OS - -Redox OS has some functionality not yet present that will be implemented when -its orbital display server provides it. +Floem depends on winit changes that haven't been upstreamed. To make Floem publishable on crates.io, all its dependencies have to be published there - hence this unofficial crate. diff --git a/src/event.rs b/src/event.rs index 6a4b33cf7f..d20c02e961 100644 --- a/src/event.rs +++ b/src/event.rs @@ -581,6 +581,9 @@ pub enum WindowEvent { /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform Occluded(bool), + /// A menu item was pressed + MenuAction(usize), + /// Emitted when a window should be redrawn. /// /// This gets triggered in two scenarios: diff --git a/src/lib.rs b/src/lib.rs index 03af6f4898..1103414710 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,6 +176,7 @@ pub mod event; pub mod event_loop; mod icon; pub mod keyboard; +pub mod menu; pub mod monitor; mod platform_impl; pub mod window; diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000000..65cb26624a --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,41 @@ +use crate::platform_impl; + +pub struct Menu(pub(crate) platform_impl::Menu); + +impl Menu { + /// Create a new empty window or application menu. + pub fn new() -> Menu { + Menu(platform_impl::Menu::new()) + } + + /// Create a new empty context menu. + pub fn new_for_popup() -> Menu { + Menu(platform_impl::Menu::new_for_popup()) + } + + /// Consume this `Menu`, returning the platform menu object. + pub(crate) fn into_inner(self) -> platform_impl::Menu { + self.0 + } + + /// Add the provided `Menu` as a submenu of self, with the provided title. + pub fn add_dropdown(&mut self, menu: Menu, text: &str, enabled: bool) { + self.0.add_dropdown(menu.0, text, enabled) + } + + /// Add an item to this menu. + pub fn add_item(&mut self, id: u32, text: &str, selected: Option, enabled: bool) { + self.0.add_item(id, text, selected, enabled) + } + + /// Add a separator to the menu. + pub fn add_separator(&mut self) { + self.0.add_separator() + } +} + +impl Default for Menu { + fn default() -> Self { + Self::new() + } +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index d543c299d8..6a9dc32065 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -3,7 +3,9 @@ use std::os::raw::c_void; use objc2::rc::Id; use crate::{ + dpi::Position, event_loop::{EventLoopBuilder, EventLoopWindowTarget}, + menu::Menu, monitor::MonitorHandle, window::{Window, WindowBuilder}, }; @@ -79,6 +81,8 @@ pub trait WindowExtMacOS { /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. fn option_as_alt(&self) -> OptionAsAlt; + + fn show_context_menu(&self, menu: Menu, position: Option); } impl WindowExtMacOS for Window { @@ -157,6 +161,11 @@ impl WindowExtMacOS for Window { fn option_as_alt(&self) -> OptionAsAlt { self.window.maybe_wait_on_main(|w| w.option_as_alt()) } + + fn show_context_menu(&self, menu: Menu, position: Option) { + self.window + .maybe_queue_on_main(move |w| w.show_context_menu(menu, position)) + } } /// Corresponds to `NSApplicationActivationPolicy`. @@ -196,6 +205,8 @@ pub trait WindowBuilderExtMacOS { fn with_fullsize_content_view(self, fullsize_content_view: bool) -> Self; fn with_disallow_hidpi(self, disallow_hidpi: bool) -> Self; fn with_has_shadow(self, has_shadow: bool) -> Self; + fn with_movable(self, movable: bool) -> Self; + fn with_traffic_lights_offset(self, x: f64, y: f64) -> Self; /// Window accepts click-through mouse events. fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> Self; /// Defines the window tabbing identifier. @@ -239,6 +250,18 @@ impl WindowBuilderExtMacOS for WindowBuilder { self } + #[inline] + fn with_movable(mut self, movable: bool) -> Self { + self.platform_specific.movable = movable; + self + } + + #[inline] + fn with_traffic_lights_offset(mut self, x: f64, y: f64) -> Self { + self.platform_specific.traffic_lights_offset = Some((x, y)); + self + } + #[inline] fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self { self.platform_specific.fullsize_content_view = fullsize_content_view; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 8e10157944..46f19b5226 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,10 +1,11 @@ use std::{ffi::c_void, path::Path}; use crate::{ - dpi::PhysicalSize, + dpi::{PhysicalSize, Position}, event::{DeviceId, KeyEvent}, event_loop::EventLoopBuilder, keyboard::Key, + menu::Menu, monitor::MonitorHandle, platform::modifier_supplement::KeyEventExtModifierSupplement, platform_impl::WinIcon, @@ -136,6 +137,8 @@ pub trait WindowExtWindows { /// /// Enabling the shadow causes a thin 1px line to appear on the top of the window. fn set_undecorated_shadow(&self, shadow: bool); + + fn show_context_menu(&self, menu: Menu, position: Option); } impl WindowExtWindows for Window { @@ -158,6 +161,10 @@ impl WindowExtWindows for Window { fn set_undecorated_shadow(&self, shadow: bool) { self.window.set_undecorated_shadow(shadow) } + + fn show_context_menu(&self, menu: Menu, position: Option) { + self.window.show_context_menu(menu, position); + } } /// Additional methods on `WindowBuilder` that are specific to Windows. diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index a6068bc762..c7d2aa138a 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -960,3 +960,21 @@ fn is_main_thread() -> bool { fn is_main_thread() -> bool { std::thread::current().name() == Some("main") } + +pub struct Menu(usize); + +impl Menu { + pub fn new() -> Menu { + Menu(0) + } + + pub fn new_for_popup() -> Menu { + Menu(0) + } + + pub fn add_dropdown(&mut self, menu: Menu, text: &str, enabled: bool) {} + + pub fn add_item(&mut self, id: u32, text: &str, selected: Option, enabled: bool) {} + + pub fn add_separator(&mut self) {} +} diff --git a/src/platform_impl/macos/appkit/menu.rs b/src/platform_impl/macos/appkit/menu.rs index e03ec95945..a4776c757d 100644 --- a/src/platform_impl/macos/appkit/menu.rs +++ b/src/platform_impl/macos/appkit/menu.rs @@ -1,8 +1,9 @@ -use icrate::Foundation::NSObject; +use icrate::Foundation::{NSObject, NSPoint, NSString}; +use objc2::ffi::BOOL; use objc2::rc::Id; use objc2::{extern_class, extern_methods, mutability, ClassType}; -use super::NSMenuItem; +use super::{NSMenuItem, NSView}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -14,6 +15,9 @@ extern_class!( } ); +unsafe impl Send for NSMenu {} +unsafe impl Sync for NSMenu {} + extern_methods!( unsafe impl NSMenu { #[method_id(new)] @@ -21,5 +25,16 @@ extern_methods!( #[method(addItem:)] pub fn addItem(&self, item: &NSMenuItem); + + #[method(setTitle:)] + pub fn setTitle(&self, title: &NSString); + + #[method(popUpMenuPositioningItem:atLocation:inView:)] + pub fn popUpMenuPositioningItem( + &self, + item: *mut NSMenuItem, + location: NSPoint, + inView: *mut NSView, + ) -> BOOL; } ); diff --git a/src/platform_impl/macos/appkit/menu_item.rs b/src/platform_impl/macos/appkit/menu_item.rs index c406529139..3fcb680516 100644 --- a/src/platform_impl/macos/appkit/menu_item.rs +++ b/src/platform_impl/macos/appkit/menu_item.rs @@ -43,5 +43,17 @@ extern_methods!( #[method(setSubmenu:)] pub fn setSubmenu(&self, submenu: &NSMenu); + + #[method(setEnabled:)] + pub fn setEnabled(&self, enabled: bool); + + #[method(setTag:)] + pub fn setTag(&self, tag: isize); + + #[method(setState:)] + pub fn setState(&self, state: isize); + + #[method(setTitle:)] + pub fn setTitle(&self, title: &NSString); } ); diff --git a/src/platform_impl/macos/appkit/view.rs b/src/platform_impl/macos/appkit/view.rs index d6720ec529..211fc2642c 100644 --- a/src/platform_impl/macos/appkit/view.rs +++ b/src/platform_impl/macos/appkit/view.rs @@ -2,7 +2,7 @@ use std::ffi::c_void; use std::num::NonZeroIsize; use std::ptr; -use icrate::Foundation::{NSObject, NSPoint, NSRect}; +use icrate::Foundation::{CGPoint, NSObject, NSPoint, NSRect}; use objc2::rc::Id; use objc2::runtime::AnyObject; use objc2::{extern_class, extern_methods, mutability, ClassType}; @@ -20,6 +20,9 @@ extern_class!( } ); +unsafe impl Sync for NSView {} +unsafe impl Send for NSView {} + // Documented as "Main Thread Only". // > generally thread safe, although operations on views such as creating, // > resizing, and moving should happen on the main thread. @@ -88,6 +91,12 @@ extern_methods!( #[method(setHidden:)] pub fn setHidden(&self, hidden: bool); + + #[method(setFrame:)] + pub fn setFrame(&self, rect: NSRect); + + #[method(setFrameOrigin:)] + pub fn setFrameOrigin(&self, origin: CGPoint); } ); diff --git a/src/platform_impl/macos/appkit/window.rs b/src/platform_impl/macos/appkit/window.rs index 5f5d8f179f..094d9b937e 100644 --- a/src/platform_impl/macos/appkit/window.rs +++ b/src/platform_impl/macos/appkit/window.rs @@ -42,6 +42,9 @@ extern_methods!( #[method(backingScaleFactor)] pub(crate) fn backingScaleFactor(&self) -> CGFloat; + #[method(mouseLocationOutsideOfEventStream)] + pub(crate) fn mouseLocationOutsideOfEventStream(&self) -> NSPoint; + #[method_id(contentView)] pub(crate) fn contentView(&self) -> Id; diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index 978e0bff5a..77c1a244f9 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -6,6 +6,61 @@ use objc2::sel; use super::appkit::{NSApp, NSEventModifierFlags, NSMenu, NSMenuItem}; +pub struct Menu(pub(crate) Id); + +fn make_menu_item(id: u32, text: &str, selected: Option, enabled: bool) -> Id { + let item = NSMenuItem::newWithTitle( + &NSString::from_str(text), + Some(sel!(handleMenuItem:)), + ns_string!(""), + ); + + item.setTag(id as isize); + + if !enabled { + item.setEnabled(false); + } + + if let Some(true) = selected { + item.setState(1_isize); + } + item +} + +impl Menu { + pub fn new() -> Menu { + let menu = NSMenu::new(); + // let () = msg_send![menu, setAutoenablesItems: NO]; + Menu(menu) + } + + pub fn new_for_popup() -> Menu { + // mac doesn't distinguish between application and context menu types. + Menu::new() + } + + pub fn add_dropdown(&mut self, menu: Menu, text: &str, enabled: bool) { + let menu_item = NSMenuItem::new(); + let title = NSString::from_str(text); + menu.0.setTitle(&title); + menu_item.setTitle(&title); + if !enabled { + menu_item.setEnabled(false); + } + menu_item.setSubmenu(&menu.0); + self.0.addItem(&menu_item); + } + + pub fn add_item(&mut self, id: u32, text: &str, selected: Option, enabled: bool) { + let menu_item = make_menu_item(id, text, selected, enabled); + self.0.addItem(&menu_item); + } + + pub fn add_separator(&mut self) { + self.0.addItem(&NSMenuItem::separatorItem()); + } +} + struct KeyEquivalent<'a> { key: &'a NSString, masks: Option, diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 7169ca9dbf..94c988aff0 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -27,6 +27,7 @@ pub(crate) use self::{ }; use crate::event::DeviceId as RootDeviceId; +pub(crate) use self::menu::Menu; pub(crate) use self::window::Window; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index bac77f9e2b..4abbb36cb0 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -13,6 +13,7 @@ use objc2::rc::{Id, WeakId}; use objc2::runtime::{AnyObject, Sel}; use objc2::{class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType}; +use super::appkit::NSMenu; use super::{ appkit::{ NSApp, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingRectTag, @@ -20,6 +21,7 @@ use super::{ }, event::{code_to_key, code_to_location}, }; +use crate::platform_impl::platform::window::position_traffic_lights; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{ @@ -118,6 +120,14 @@ fn get_left_modifier_code(key: &Key) -> KeyCode { } } +pub struct MenuItemAction(Box); + +impl std::fmt::Debug for MenuItemAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("MenuItemAction") + } +} + #[derive(Debug, Default)] pub struct ViewState { cursor_state: RefCell, @@ -138,6 +148,8 @@ pub struct ViewState { /// to the application, even during IME forward_key_to_app: Cell, + context_menu: RefCell, NSPoint)>>, + marked_text: RefCell>, accepts_first_mouse: bool, } @@ -145,7 +157,7 @@ pub struct ViewState { declare_class!( #[derive(Debug)] #[allow(non_snake_case)] - pub(super) struct WinitView { + pub(crate) struct WinitView { // Weak reference because the window keeps a strong reference to the view _ns_window: IvarDrop>, "__ns_window">, state: IvarDrop, "_state">, @@ -234,6 +246,7 @@ declare_class!( let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); let size = logical_size.to_physical::(self.scale_factor()); self.queue_event(WindowEvent::Resized(size)); + position_traffic_lights(&self.window()); } #[method(drawRect:)] @@ -278,6 +291,25 @@ declare_class!( self.addCursorRect(bounds, &NSCursor::invisible()); } } + + #[method(showContextMenu:)] + fn show_context_menu(&self, _n: &AnyObject) { + if let Some((menu, position)) = self.state.context_menu.borrow_mut().take() { + menu.popUpMenuPositioningItem( + std::ptr::null::() as *mut _, + position, + self as *const Self as *mut _, + ); + } + } + + #[method(handleMenuItem:)] + fn handle_menu_item(&self, item: &AnyObject) { + unsafe { + let tag: isize = msg_send![item, tag]; + self.queue_event(WindowEvent::MenuAction(tag as usize)); + } + } } unsafe impl NSTextInputClient for WinitView { @@ -858,6 +890,10 @@ impl WinitView { cursor_state.cursor = icon; } + pub(super) fn set_context_menu(&self, menu: Id, position: NSPoint) { + *self.state.context_menu.borrow_mut() = Some((menu, position)); + } + /// Set whether the cursor should be visible or not. /// /// Returns whether the state changed. diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7e7d6f383e..9c8ecb2ac3 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -7,6 +7,9 @@ use std::os::raw::c_void; use std::ptr::NonNull; use std::sync::{Mutex, MutexGuard}; +use objc2::ffi::NO; +use objc2::runtime::AnyObject; + use crate::{ dpi::{ LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, @@ -118,12 +121,14 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub title_hidden: bool, pub titlebar_hidden: bool, pub titlebar_buttons_hidden: bool, + pub movable: bool, pub fullsize_content_view: bool, pub disallow_hidpi: bool, pub has_shadow: bool, pub accepts_first_mouse: bool, pub tabbing_identifier: Option, pub option_as_alt: OptionAsAlt, + pub traffic_lights_offset: Option<(f64, f64)>, } impl Default for PlatformSpecificWindowBuilderAttributes { @@ -136,11 +141,13 @@ impl Default for PlatformSpecificWindowBuilderAttributes { titlebar_hidden: false, titlebar_buttons_hidden: false, fullsize_content_view: false, + movable: true, disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, tabbing_identifier: None, option_as_alt: Default::default(), + traffic_lights_offset: None, } } } @@ -241,6 +248,8 @@ pub struct SharedState { pub(crate) option_as_alt: OptionAsAlt, decorations: bool, + + traffic_lights_offset: Option<(f64, f64)>, } impl SharedState { @@ -369,6 +378,7 @@ impl WinitWindow { resizable: attrs.resizable, maximized: attrs.maximized, decorations: attrs.decorations, + traffic_lights_offset: pl_attrs.traffic_lights_offset, ..Default::default() }; @@ -414,6 +424,9 @@ impl WinitWindow { if pl_attrs.title_hidden { this.setTitleVisibility(NSWindowTitleVisibility::Hidden); } + if !pl_attrs.movable { + this.setMovable(false); + } if pl_attrs.titlebar_buttons_hidden { for titlebar_button in &[ #[allow(deprecated)] @@ -550,6 +563,7 @@ impl WinitWindow { if attrs.maximized { this.set_maximized(attrs.maximized); } + position_traffic_lights(&this); Ok((this, delegate)) } @@ -581,7 +595,8 @@ impl WinitWindow { } pub fn set_title(&self, title: &str) { - self.setTitle(&NSString::from_str(title)) + self.setTitle(&NSString::from_str(title)); + position_traffic_lights(self); } pub fn set_transparent(&self, transparent: bool) { @@ -1587,6 +1602,27 @@ impl WindowExtMacOS for WinitWindow { let shared_state_lock = self.lock_shared_state("option_as_alt"); shared_state_lock.option_as_alt } + + fn show_context_menu(&self, menu: crate::menu::Menu, position: Option) { + unsafe { + let scale = self.scale_factor(); + let position = if let Some(pos) = position.map(|p| p.to_logical(scale)) { + let view_rect: NSRect = self.view().frame(); + NSPoint::new(pos.x, view_rect.size.height - pos.y) + } else { + let mouse_location = self.mouseLocationOutsideOfEventStream(); + let pos = Position::Logical(LogicalPosition { + x: mouse_location.x, + y: mouse_location.y, + }); + let pos = pos.to_logical(scale); + NSPoint::new(pos.x, pos.y) + }; + self.view().set_context_menu(menu.into_inner().0, position); + let view: *mut WinitView = Id::as_ptr(&self.view()) as *mut _; + let () = msg_send![view as *mut _, performSelectorOnMainThread: sel!(showContextMenu:) withObject: std::ptr::null::() waitUntilDone: NO]; + } + } } pub(super) fn get_ns_theme() -> Theme { @@ -1620,3 +1656,31 @@ fn set_ns_theme(theme: Option) { app.setAppearance(appearance.as_ref().map(|a| a.as_ref())); } } + +pub(super) fn position_traffic_lights(window: &WinitWindow) -> Option<()> { + let offset = { window.shared_state.lock().ok()?.traffic_lights_offset? }; + + let close = window.standardWindowButton(NSWindowButton::Close)?; + let miniaturize = window.standardWindowButton(NSWindowButton::Miniaturize)?; + let zoom = window.standardWindowButton(NSWindowButton::Zoom)?; + + let superview: &mut NSView = unsafe { msg_send![&close, superview] }; + let title_bar_container_view: &mut NSView = unsafe { msg_send![superview, superview] }; + + let close_rect = close.frame(); + let title_bar_frame_height = close_rect.size.height + offset.1; + let mut title_bar_rect = NSView::frame(title_bar_container_view); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height; + title_bar_container_view.setFrame(title_bar_rect); + + let space_between = miniaturize.frame().origin.x - close_rect.origin.x; + let window_buttons = vec![close, miniaturize, zoom]; + + for (i, button) in window_buttons.into_iter().enumerate() { + let mut rect = button.frame(); + rect.origin.x = offset.0 + (i as f64 * space_between); + button.setFrameOrigin(rect.origin); + } + Some(()) +} diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index fb6df1b451..de1473a440 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -17,6 +17,7 @@ use super::{ window::{get_ns_theme, WinitWindow}, Fullscreen, }; +use crate::platform_impl::platform::window::position_traffic_lights; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{Event, WindowEvent}, @@ -333,6 +334,7 @@ declare_class!( if let Some(target_fullscreen) = target_fullscreen { self.window.set_fullscreen(target_fullscreen); } + position_traffic_lights(&self.window); } /// Invoked when fail to enter fullscreen diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index d4263920af..8d04cba3ce 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -55,19 +55,19 @@ use windows_sys::Win32::{ TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, PT_TOUCH, RI_KEY_E0, RI_KEY_E1, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, - SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, - WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, - WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, - WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, - WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, - WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, - WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, - WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, - WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, - WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, - WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, - WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, - WS_VISIBLE, + SC_RESTORE, SIZE_MAXIMIZED, SIZE_RESTORED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CLOSE, WM_COMMAND, + WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, + WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, + WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, + WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, + WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, + WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, + WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, + WM_SETTINGCHANGE, WM_SIZE, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, + WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, + WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, + WS_POPUP, WS_VISIBLE, }, }, }; @@ -1286,28 +1286,33 @@ unsafe fn public_window_callback_inner( } WM_SIZE => { - use crate::event::WindowEvent::Resized; - let w = super::loword(lparam as u32) as u32; - let h = super::hiword(lparam as u32) as u32; - - let physical_size = PhysicalSize::new(w, h); - let event = Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: Resized(physical_size), - }; + // handle spurious WM_SIZE messages + // see https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-size#parameters + // and https://devblogs.microsoft.com/oldnewthing/20050210-00/?p=36483 + if wparam == SIZE_RESTORED as usize || wparam == SIZE_MAXIMIZED as usize { + use crate::event::WindowEvent::Resized; + let w = super::loword(lparam as u32) as u32; + let h = super::hiword(lparam as u32) as u32; + + let physical_size = PhysicalSize::new(w, h); + let event = Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Resized(physical_size), + }; - { - let mut w = userdata.window_state_lock(); - // See WindowFlags::MARKER_RETAIN_STATE_ON_SIZE docs for info on why this `if` check exists. - if !w - .window_flags() - .contains(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE) { - let maximized = wparam == SIZE_MAXIMIZED as usize; - w.set_window_flags_in_place(|f| f.set(WindowFlags::MAXIMIZED, maximized)); + let mut w = userdata.window_state_lock(); + // See WindowFlags::MARKER_RETAIN_STATE_ON_SIZE docs for info on why this `if` check exists. + if !w + .window_flags() + .contains(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE) + { + let maximized = wparam == SIZE_MAXIMIZED as usize; + w.set_window_flags_in_place(|f| f.set(WindowFlags::MAXIMIZED, maximized)); + } } + userdata.send_event(event); } - userdata.send_event(event); result = ProcResult::Value(0); } @@ -1595,12 +1600,37 @@ unsafe fn public_window_callback_inner( } WM_LBUTTONDOWN => { - use crate::event::{ElementState::Pressed, MouseButton::Left, WindowEvent::MouseInput}; + use crate::event::{ + ElementState::Pressed, MouseButton::Left, WindowEvent::CursorMoved, + WindowEvent::MouseInput, + }; unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); + let x = super::get_x_lparam(lparam as u32) as f64; + let y = super::get_y_lparam(lparam as u32) as f64; + let position = PhysicalPosition::new(x, y); + let cursor_moved; + { + // handle spurious WM_MOUSEMOVE messages + // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 + // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html + let mut w = userdata.window_state_lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + if cursor_moved { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { + device_id: DEVICE_ID, + position, + }, + }); + } + userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { @@ -1634,13 +1664,36 @@ unsafe fn public_window_callback_inner( WM_RBUTTONDOWN => { use crate::event::{ - ElementState::Pressed, MouseButton::Right, WindowEvent::MouseInput, + ElementState::Pressed, MouseButton::Right, WindowEvent::CursorMoved, + WindowEvent::MouseInput, }; unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; update_modifiers(window, userdata); + let x = super::get_x_lparam(lparam as u32) as f64; + let y = super::get_y_lparam(lparam as u32) as f64; + let position = PhysicalPosition::new(x, y); + let cursor_moved; + { + // handle spurious WM_MOUSEMOVE messages + // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 + // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html + let mut w = userdata.window_state_lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + if cursor_moved { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { + device_id: DEVICE_ID, + position, + }, + }); + } + userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { @@ -2295,6 +2348,15 @@ unsafe fn public_window_callback_inner( result = ProcResult::DefWindowProc(wparam); } + WM_COMMAND => { + use crate::event::WindowEvent::MenuAction; + let id = super::loword(wparam as u32) as u32; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MenuAction(id as usize), + }); + } + _ => { if msg == DESTROY_MSG_ID.get() { unsafe { DestroyWindow(window) }; diff --git a/src/platform_impl/windows/menu.rs b/src/platform_impl/windows/menu.rs new file mode 100644 index 0000000000..b70e6aaa35 --- /dev/null +++ b/src/platform_impl/windows/menu.rs @@ -0,0 +1,105 @@ +use std::{collections::HashMap, ptr::null}; + +use windows_sys::Win32::UI::WindowsAndMessaging::{ + AppendMenuW, CreateMenu, CreatePopupMenu, DestroyMenu, ACCEL, HMENU, MF_CHECKED, MF_GRAYED, + MF_POPUP, MF_SEPARATOR, MF_STRING, +}; + +/// A menu object, which can be either a top-level menubar or a +/// submenu. +pub struct Menu { + hmenu: HMENU, + accels: HashMap, +} + +impl Drop for Menu { + fn drop(&mut self) { + unsafe { + DestroyMenu(self.hmenu); + } + } +} + +impl Menu { + /// Create a new menu for a window. + pub fn new() -> Menu { + unsafe { + let hmenu = CreateMenu(); + Menu { + hmenu, + accels: HashMap::default(), + } + } + } + + /// Create a new popup (context / right-click) menu. + pub fn new_for_popup() -> Menu { + unsafe { + let hmenu = CreatePopupMenu(); + Menu { + hmenu, + accels: HashMap::default(), + } + } + } + + pub fn into_hmenu(self) -> HMENU { + let hmenu = self.hmenu; + std::mem::forget(self); + hmenu + } + + /// Add a dropdown menu. This takes the menu by ownership, but we'll + /// probably want to change that so we can manipulate it later. + /// + /// The `text` field has all the fun behavior of winapi CreateMenu. + pub fn add_dropdown(&mut self, mut menu: Menu, text: &str, enabled: bool) { + let child_accels = std::mem::take(&mut menu.accels); + self.accels.extend(child_accels); + + unsafe { + let mut flags = MF_POPUP; + if !enabled { + flags |= MF_GRAYED; + } + let text = text + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + AppendMenuW(self.hmenu, flags, menu.into_hmenu() as usize, text.as_ptr()); + } + } + + /// Add an item to the menu. + pub fn add_item(&mut self, id: u32, text: &str, selected: Option, enabled: bool) { + unsafe { + let mut flags = MF_STRING; + if !enabled { + flags |= MF_GRAYED; + } + if let Some(true) = selected { + flags |= MF_CHECKED; + } + let text = text + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + AppendMenuW(self.hmenu, flags, id as usize, text.as_ptr()); + } + } + + /// Add a separator to the menu. + pub fn add_separator(&mut self) { + unsafe { + AppendMenuW(self.hmenu, MF_SEPARATOR, 0, null()); + } + } + + /// Get the accels table + pub fn accels(&self) -> Option> { + if self.accels.is_empty() { + return None; + } + Some(self.accels.values().cloned().collect()) + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index bc6caeb6f5..567baaee46 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -174,6 +174,8 @@ unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: }; } +pub use menu::Menu; + #[macro_use] mod util; mod dark_mode; @@ -185,6 +187,7 @@ mod icon; mod ime; mod keyboard; mod keyboard_layout; +mod menu; mod monitor; mod raw_input; mod window; diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 7d5bc6d746..30b779f2d2 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -5,7 +5,8 @@ use std::{ ffi::c_void, io, mem::{self, MaybeUninit}, - panic, ptr, + panic, + ptr::{self, null}, sync::{mpsc::channel, Arc, Mutex, MutexGuard}, }; @@ -37,8 +38,8 @@ use windows_sys::Win32::{ Touch::{RegisterTouchWindow, TWF_WANTPALM}, }, WindowsAndMessaging::{ - CreateWindowExW, EnableMenuItem, FlashWindowEx, GetClientRect, GetCursorPos, - GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, + CreateWindowExW, DestroyMenu, EnableMenuItem, FlashWindowEx, GetClientRect, + GetCursorPos, GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, GetWindowTextW, IsWindowVisible, LoadCursorW, PeekMessageW, PostMessageW, RegisterClassExW, SetCursor, SetCursorPos, SetForegroundWindow, SetMenuDefaultItem, SetWindowDisplayAffinity, SetWindowPlacement, SetWindowPos, @@ -58,6 +59,7 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, icon::Icon, + menu::Menu, platform_impl::platform::{ dark_mode::try_theme, definitions::{ @@ -1033,6 +1035,28 @@ impl Window { ); } } + + pub fn show_context_menu(&self, menu: Menu, position: Option) { + let hmenu = menu.into_inner().into_hmenu(); + unsafe { + let pt = if let Some(pos) = position { + let scale_factor = self.scale_factor(); + let pos = pos.to_physical::(scale_factor); + let mut pt = POINT { + x: pos.x as _, + y: pos.y as _, + }; + ClientToScreen(self.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, self.hwnd(), null()); + DestroyMenu(hmenu); + } + } } impl Drop for Window {