Compare commits

..

6 Commits

Author SHA1 Message Date
John Nunley
d5240c4650 Update to newest master
Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:14:17 -08:00
John Nunley
1c65f70d59 Update for 0.29.3 changes
Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:12:09 -08:00
John Nunley
4a30d0eadd Remove superseded TODO comment
Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:12:09 -08:00
John Nunley
39b5437951 Use XSetEventQueueOwner to indicate XCB
I don't know what this actually does, but it can't hurt, right?

Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:12:09 -08:00
John Nunley
9477514ad5 Use x11rb for event handling
Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:12:07 -08:00
John Nunley
5c9f1b2e9a Switch to using xim for IME
This is broken until we start using x11rb for event lookups.

Signed-off-by: John Nunley <dev@notgull.net>
2023-12-19 16:09:52 -08:00
98 changed files with 5115 additions and 4757 deletions

View File

@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
toolchain: [stable, nightly, '1.70.0']
toolchain: [stable, nightly, '1.65.0']
platform:
# Note: Make sure that we test all the `docs.rs` targets defined in Cargo.toml!
- { name: 'Windows 64bit MSVC', target: x86_64-pc-windows-msvc, os: windows-latest, }
@@ -43,10 +43,10 @@ jobs:
- { name: 'web', target: wasm32-unknown-unknown, os: ubuntu-latest, }
exclude:
# Android is tested on stable-3
- toolchain: '1.70.0'
- toolchain: '1.65.0'
platform: { name: 'Android', target: aarch64-linux-android, os: ubuntu-latest, options: '--package=winit --features=android-native-activity', cmd: 'apk --' }
include:
- toolchain: '1.70.0'
- toolchain: '1.69.0'
platform: { name: 'Android', target: aarch64-linux-android, os: ubuntu-latest, options: '--package=winit --features=android-native-activity', cmd: 'apk --' }
env:
@@ -120,7 +120,7 @@ jobs:
- name: Build tests
if: >
!contains(matrix.platform.target, 'redox') &&
matrix.toolchain != '1.70.0'
matrix.toolchain != '1.65.0'
run: cargo $CMD test --no-run $OPTIONS
- name: Run tests
@@ -129,7 +129,7 @@ jobs:
!contains(matrix.platform.target, 'ios') &&
!contains(matrix.platform.target, 'wasm32') &&
!contains(matrix.platform.target, 'redox') &&
matrix.toolchain != '1.70.0'
matrix.toolchain != '1.65.0'
run: cargo $CMD test $OPTIONS
- name: Lint with clippy
@@ -139,7 +139,7 @@ jobs:
- name: Build tests with serde enabled
if: >
!contains(matrix.platform.target, 'redox') &&
matrix.toolchain != '1.70.0'
matrix.toolchain != '1.65.0'
run: cargo $CMD test --no-run $OPTIONS --features serde
- name: Run tests with serde enabled
@@ -148,7 +148,7 @@ jobs:
!contains(matrix.platform.target, 'ios') &&
!contains(matrix.platform.target, 'wasm32') &&
!contains(matrix.platform.target, 'redox') &&
matrix.toolchain != '1.70.0'
matrix.toolchain != '1.65.0'
run: cargo $CMD test $OPTIONS --features serde
# See restore step above

View File

@@ -17,15 +17,6 @@ Unreleased` header.
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
- On macOS, add services menu.
- **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation.
- On Web, fix setting cursor icon overriding cursor visibility.
- On Web, fix context menu not being disabled by `with_prevent_default(true)`.
- **Breaking:** On Web, return `RawWindowHandle::WebCanvas` instead of `RawWindowHandle::Web`.
- **Breaking:** On Web, macOS and iOS, return `HandleError::Unavailable` when a window handle is not available.
- **Breaking:** Bump MSRV from `1.65` to `1.70`.
# 0.29.5
- On macOS, remove spurious error logging when handling `Fn`.
- On X11, fix an issue where floating point data from the server is
misinterpreted during a drag and drop operation.
@@ -34,8 +25,8 @@ Unreleased` header.
- On Wayland, disable Client Side Decorations when `wl_subcompositor` is not supported.
- On X11, fix `Xft.dpi` detection from Xresources.
- On Windows, fix consecutive calls to `window.set_fullscreen(Some(Fullscreen::Borderless(None)))` resulting in losing previous window state when eventually exiting fullscreen using `window.set_fullscreen(None)`.
- On Wayland, fix resize being sent on focus change.
- On Windows, fix `set_ime_cursor_area`.
- On Web, remove queuing fullscreen request in absence of transient activation.
- On Web, fix setting cursor icon overriding cursor visibility.
# 0.29.4

View File

@@ -20,7 +20,7 @@ your description of the issue as detailed as possible:
When making a code contribution to winit, before opening your pull request, please make sure that:
- your patch builds with Winit's minimal supported rust version - Rust 1.70.
- your patch builds with Winit's minimal supported rust version - Rust 1.65.
- you tested your modifications on all the platforms impacted, or if not possible, detail which platforms
were not tested, and what should be tested, so that a maintainer or another contributor can test them
- you updated any relevant documentation in winit

View File

@@ -1,6 +1,6 @@
[package]
name = "winit"
version = "0.29.5"
version = "0.29.4"
authors = ["The winit contributors", "Pierre Krieger <pierre.krieger1708@gmail.com>"]
description = "Cross-platform window creation library."
edition = "2021"
@@ -10,7 +10,7 @@ readme = "README.md"
repository = "https://github.com/rust-windowing/winit"
documentation = "https://docs.rs/winit"
categories = ["gui"]
rust-version = "1.70.0"
rust-version = "1.65.0"
[package.metadata.docs.rs]
features = [
@@ -43,7 +43,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"]
x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb", "xim"]
wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "wayland-protocols-plasma", "sctk", "ahash", "memmap2"]
wayland-dlopen = ["wayland-backend/dlopen"]
wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"]
@@ -86,13 +86,13 @@ ndk-sys = "0.5.0"
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
core-foundation = "0.9.3"
objc2 = "0.5.0"
objc2 = "0.4.1"
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.23.1"
[target.'cfg(target_os = "macos")'.dependencies.icrate]
version = "0.1.0"
version = "0.0.4"
features = [
"dispatch",
"Foundation",
@@ -105,31 +105,10 @@ features = [
"Foundation_NSProcessInfo",
"Foundation_NSThread",
"Foundation_NSNumber",
"AppKit",
"AppKit_NSAppearance",
"AppKit_NSApplication",
"AppKit_NSBitmapImageRep",
"AppKit_NSButton",
"AppKit_NSColor",
"AppKit_NSControl",
"AppKit_NSCursor",
"AppKit_NSEvent",
"AppKit_NSGraphicsContext",
"AppKit_NSImage",
"AppKit_NSImageRep",
"AppKit_NSMenu",
"AppKit_NSMenuItem",
"AppKit_NSPasteboard",
"AppKit_NSResponder",
"AppKit_NSScreen",
"AppKit_NSTextInputContext",
"AppKit_NSView",
"AppKit_NSWindow",
"AppKit_NSWindowTabGroup",
]
[target.'cfg(target_os = "ios")'.dependencies.icrate]
version = "0.1.0"
version = "0.0.4"
features = [
"dispatch",
"Foundation",
@@ -188,6 +167,7 @@ wayland-protocols = { version = "0.31.0", features = [ "staging"], optional = tr
wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optional = true }
x11-dl = { version = "2.18.5", optional = true }
x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true }
xim = { version = "0.3.0", features = ["x11rb-client", "client"], optional = true }
xkbcommon-dl = "0.4.0"
[target.'cfg(target_os = "redox")'.dependencies]
@@ -212,7 +192,6 @@ features = [
'FocusEvent',
'HtmlCanvasElement',
'HtmlElement',
'HtmlImageElement',
'ImageBitmap',
'ImageBitmapOptions',
'ImageBitmapRenderingContext',
@@ -253,3 +232,6 @@ web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
members = [
"run-wasm",
]
[patch.crates-io]
xim = { git = "https://github.com/forkgull/xim-rs", branch = "x11rb-13" }

View File

@@ -6,7 +6,7 @@
```toml
[dependencies]
winit = "0.29.5"
winit = "0.29.4"
```
## [Documentation](https://docs.rs/winit)
@@ -42,7 +42,7 @@ Winit provides the following features, which can be enabled in your `Cargo.toml`
## MSRV Policy
This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to
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
@@ -156,7 +156,7 @@ For more details, refer to these `android-activity` [example applications](https
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.5", features = [ "android-native-activity" ] }`
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).

View File

@@ -17,7 +17,7 @@ fn main() -> Result<(), impl std::error::Error> {
dpi::{LogicalPosition, LogicalSize, Position},
event::{ElementState, Event, KeyEvent, WindowEvent},
event_loop::{EventLoop, EventLoopWindowTarget},
raw_window_handle::HasWindowHandle,
raw_window_handle::HasRawWindowHandle,
window::{Window, WindowBuilder, WindowId},
};
@@ -26,13 +26,14 @@ fn main() -> Result<(), impl std::error::Error> {
event_loop: &EventLoopWindowTarget<()>,
windows: &mut HashMap<WindowId, Window>,
) {
let parent = parent.window_handle().unwrap();
let parent = parent.raw_window_handle().unwrap();
let mut builder = WindowBuilder::new()
.with_title("child window")
.with_inner_size(LogicalSize::new(200.0f32, 200.0f32))
.with_position(Position::Logical(LogicalPosition::new(0.0, 0.0)))
.with_visible(true);
builder = builder.with_parent_window(Some(parent));
// `with_parent_window` is unsafe. Parent window must be a valid window.
builder = unsafe { builder.with_parent_window(Some(parent)) };
let child_window = builder.build(event_loop).unwrap();
let id = child_window.id();

View File

@@ -4,19 +4,17 @@
use simple_logger::SimpleLogger;
use winit::{
event::{ElementState, Event, KeyEvent, WindowEvent},
event_loop::{EventLoop, EventLoopWindowTarget},
event_loop::EventLoop,
keyboard::Key,
window::{CustomCursor, WindowBuilder},
};
fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
fn decode_cursor(bytes: &[u8]) -> CustomCursor {
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
let samples = img.into_flat_samples();
let (_, w, h) = samples.extents();
let (w, h) = (w as u16, h as u16);
let builder = CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap();
builder.build(window_target)
CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
}
#[cfg(not(wasm_platform))]
@@ -45,8 +43,8 @@ fn main() -> Result<(), impl std::error::Error> {
let mut cursor_visible = true;
let custom_cursors = [
decode_cursor(include_bytes!("data/cross.png"), &event_loop),
decode_cursor(include_bytes!("data/cross2.png"), &event_loop),
decode_cursor(include_bytes!("data/cross.png")),
decode_cursor(include_bytes!("data/cross2.png")),
];
event_loop.run(move |event, _elwt| match event {

View File

@@ -1,10 +1,7 @@
use core::fmt;
use std::hash::Hasher;
use std::sync::Arc;
use std::{error::Error, hash::Hash};
use std::{error::Error, sync::Arc};
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::{self, PlatformCustomCursor, PlatformCustomCursorBuilder};
use crate::platform_impl::PlatformCustomCursor;
/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`].
pub const MAX_CURSOR_SIZE: u16 = 2048;
@@ -19,37 +16,23 @@ const PIXEL_SIZE: usize = 4;
///
/// # Example
///
/// ```no_run
/// use winit::{
/// event::{Event, WindowEvent},
/// event_loop::{ControlFlow, EventLoop},
/// window::{CustomCursor, Window},
/// };
///
/// let mut event_loop = EventLoop::new().unwrap();
/// ```
/// use winit::window::CustomCursor;
///
/// let w = 10;
/// let h = 10;
/// let rgba = vec![255; (w * h * 4) as usize];
///
/// #[cfg(not(target_family = "wasm"))]
/// let builder = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
/// let custom_cursor = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
///
/// #[cfg(target_family = "wasm")]
/// let builder = {
/// let custom_cursor_url = {
/// use winit::platform::web::CustomCursorExtWebSys;
/// CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0)
/// CustomCursor::from_url("http://localhost:3000/cursor.png", 0, 0).unwrap()
/// };
///
/// let custom_cursor = builder.build(&event_loop);
///
/// let window = Window::new(&event_loop).unwrap();
/// window.set_custom_cursor(&custom_cursor);
/// ```
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCursor {
/// Platforms should make sure this is cheap to clone.
pub(crate) inner: PlatformCustomCursor,
pub(crate) inner: Arc<PlatformCustomCursor>,
}
impl CustomCursor {
@@ -65,35 +48,20 @@ impl CustomCursor {
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<CustomCursorBuilder, BadImage> {
Ok(CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::from_rgba(
) -> Result<Self, BadImage> {
Ok(Self {
inner: PlatformCustomCursor::from_rgba(
rgba.into(),
width,
height,
hotspot_x,
hotspot_y,
)?,
)?
.into(),
})
}
}
/// Builds a [`CustomCursor`].
///
/// See [`CustomCursor`] for more details.
#[derive(Debug)]
pub struct CustomCursorBuilder {
pub(crate) inner: PlatformCustomCursorBuilder,
}
impl CustomCursorBuilder {
pub fn build<T>(self, window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
CustomCursor {
inner: PlatformCustomCursor::build(self.inner, &window_target.p),
}
}
}
/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments.
#[derive(Debug, Clone)]
pub enum BadImage {
@@ -152,54 +120,9 @@ impl fmt::Display for BadImage {
impl Error for BadImage {}
/// Platforms export this directly as `PlatformCustomCursorBuilder` if they need to only work with images.
#[derive(Debug)]
pub(crate) struct OnlyCursorImageBuilder(pub(crate) CursorImage);
#[allow(dead_code)]
impl OnlyCursorImageBuilder {
pub(crate) fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self)
}
}
/// Platforms export this directly as `PlatformCustomCursor` if they don't implement caching.
#[derive(Debug, Clone)]
pub(crate) struct OnlyCursorImage(pub(crate) Arc<CursorImage>);
impl Hash for OnlyCursorImage {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.0).hash(state);
}
}
impl PartialEq for OnlyCursorImage {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for OnlyCursorImage {}
#[allow(dead_code)]
impl OnlyCursorImage {
fn build<T>(
builder: OnlyCursorImageBuilder,
_: &platform_impl::EventLoopWindowTarget<T>,
) -> Self {
Self(Arc::new(builder.0))
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct CursorImage {
/// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CursorImage {
pub(crate) rgba: Vec<u8>,
pub(crate) width: u16,
pub(crate) height: u16,
@@ -207,8 +130,9 @@ pub(crate) struct CursorImage {
pub(crate) hotspot_y: u16,
}
#[allow(dead_code)]
impl CursorImage {
pub(crate) fn from_rgba(
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
@@ -256,12 +180,12 @@ impl CursorImage {
}
// Platforms that don't support cursors will export this as `PlatformCustomCursor`.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NoCustomCursor;
#[allow(dead_code)]
impl NoCustomCursor {
pub(crate) fn from_rgba(
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
@@ -271,8 +195,4 @@ impl NoCustomCursor {
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?;
Ok(Self)
}
fn build<T>(self, _: &platform_impl::EventLoopWindowTarget<T>) -> NoCustomCursor {
self
}
}

View File

@@ -1,6 +1,5 @@
use std::os::raw::c_void;
use icrate::Foundation::MainThreadMarker;
use objc2::rc::Id;
use crate::{
@@ -366,9 +365,7 @@ impl MonitorHandleExtMacOS for MonitorHandle {
}
fn ns_screen(&self) -> Option<*mut c_void> {
// SAFETY: We only use the marker to get a pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
self.inner.ns_screen(mtm).map(|s| Id::as_ptr(&s) as _)
self.inner.ns_screen().map(|s| Id::as_ptr(&s) as _)
}
}

View File

@@ -155,19 +155,19 @@ pub trait EventLoopExtPumpEvents {
/// - **Windows**: The implementation will use `PeekMessage` when checking for
/// window messages to avoid blocking your external event loop.
///
/// - **MacOS**: The implementation works in terms of stopping the global application
/// - **MacOS**: The implementation works in terms of stopping the global `NSApp`
/// whenever the application `RunLoop` indicates that it is preparing to block
/// and wait for new events.
///
/// This is very different to the polling APIs that are available on other
/// platforms (the lower level polling primitives on MacOS are private
/// implementation details for `NSApplication` which aren't accessible to
/// application developers)
/// implementation details for `NSApp` which aren't accessible to application
/// developers)
///
/// It's likely this will be less efficient than polling on other OSs and
/// it also means the `NSApplication` is stopped while outside of the Winit
/// it also means the `NSApp` is stopped while outside of the Winit
/// event loop - and that's observable (for example to crates like `rfd`)
/// because the `NSApplication` is global state.
/// because the `NSApp` is global state.
///
/// If you render outside of Winit you are likely to see window resizing artifacts
/// since MacOS expects applications to render synchronously during any `drawRect`

View File

@@ -27,12 +27,11 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
use crate::cursor::CustomCursorBuilder;
use crate::cursor::CustomCursor;
use crate::event::Event;
use crate::event_loop::EventLoop;
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursorBuilder;
use crate::window::CustomCursor;
use crate::platform_impl::PlatformCustomCursor;
use crate::window::{Window, WindowBuilder};
use crate::SendSyncWrapper;
@@ -210,17 +209,18 @@ pub trait CustomCursorExtWebSys {
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
///
/// [PNG]: https://en.wikipedia.org/wiki/PNG
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder;
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self;
}
impl CustomCursorExtWebSys for CustomCursor {
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder {
CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::Url {
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self {
Self {
inner: PlatformCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
},
}
.into(),
}
}
}

View File

@@ -18,6 +18,7 @@ use android_activity::{
use once_cell::sync::Lazy;
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error,
event::{self, Force, InnerSizeWriter, StartCause},
@@ -751,18 +752,6 @@ impl DeviceId {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct PlatformSpecificWindowBuilderAttributes;
#[derive(Debug, Clone)]
pub(crate) struct OwnedWindowHandle {}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(_handle: rwh_06::WindowHandle<'_>) -> Self {
// Parent windows are currently unsupported, though owned window
// handles would be implementable.
Self {}
}
}
pub(crate) struct Window {
app: AndroidApp,
redraw_requester: RedrawRequester,
@@ -918,7 +907,7 @@ impl Window {
pub fn set_cursor_icon(&self, _: window::CursorIcon) {}
pub(crate) fn set_custom_cursor(&self, _: PlatformCustomCursor) {}
pub fn set_custom_cursor(&self, _: CustomCursor) {}
pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> {
Err(error::ExternalError::NotSupported(
@@ -1046,7 +1035,6 @@ impl Display for OsError {
}
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursorBuilder;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]

View File

@@ -73,12 +73,11 @@ pub(crate) use self::{
EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes,
},
monitor::{MonitorHandle, VideoMode},
window::{OwnedWindowHandle, PlatformSpecificWindowBuilderAttributes, Window, WindowId},
window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId},
};
use self::uikit::UIScreen;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursorBuilder;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

View File

@@ -8,7 +8,6 @@ use std::{
use icrate::Foundation::{MainThreadBound, MainThreadMarker, NSInteger};
use objc2::mutability::IsRetainable;
use objc2::rc::Id;
use objc2::Message;
use super::uikit::{UIScreen, UIScreenMode};
use crate::{
@@ -21,15 +20,16 @@ use crate::{
#[derive(Debug)]
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Id<T>>);
impl<T: IsRetainable + Message> Clone for MainThreadBoundDelegateImpls<T> {
impl<T: IsRetainable> Clone for MainThreadBoundDelegateImpls<T> {
fn clone(&self) -> Self {
Self(MainThreadMarker::run_on_main(|mtm| {
MainThreadBound::new(Id::clone(self.0.get(mtm)), mtm)
}))
Self(
self.0
.get_on_main(|inner, mtm| MainThreadBound::new(Id::clone(inner), mtm)),
)
}
}
impl<T: IsRetainable + Message> hash::Hash for MainThreadBoundDelegateImpls<T> {
impl<T: IsRetainable> hash::Hash for MainThreadBoundDelegateImpls<T> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
@@ -37,7 +37,7 @@ impl<T: IsRetainable + Message> hash::Hash for MainThreadBoundDelegateImpls<T> {
}
}
impl<T: IsRetainable + Message> PartialEq for MainThreadBoundDelegateImpls<T> {
impl<T: IsRetainable> PartialEq for MainThreadBoundDelegateImpls<T> {
fn eq(&self, other: &Self) -> bool {
// SAFETY: Marker only used to get the pointer
let mtm = unsafe { MainThreadMarker::new_unchecked() };
@@ -45,7 +45,7 @@ impl<T: IsRetainable + Message> PartialEq for MainThreadBoundDelegateImpls<T> {
}
}
impl<T: IsRetainable + Message> Eq for MainThreadBoundDelegateImpls<T> {}
impl<T: IsRetainable> Eq for MainThreadBoundDelegateImpls<T> {}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct VideoMode {
@@ -100,9 +100,11 @@ pub struct MonitorHandle {
impl Clone for MonitorHandle {
fn clone(&self) -> Self {
MainThreadMarker::run_on_main(|mtm| Self {
ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm),
})
Self {
ui_screen: self
.ui_screen
.get_on_main(|inner, mtm| MainThreadBound::new(inner.clone(), mtm)),
}
}
}
@@ -166,16 +168,16 @@ impl MonitorHandle {
}
pub fn name(&self) -> Option<String> {
MainThreadMarker::run_on_main(|mtm| {
self.ui_screen.get_on_main(|ui_screen, mtm| {
let main = UIScreen::main(mtm);
if *self.ui_screen(mtm) == main {
if *ui_screen == main {
Some("Primary".to_string())
} else if *self.ui_screen(mtm) == main.mirroredScreen() {
} else if *ui_screen == main.mirroredScreen() {
Some("Mirrored".to_string())
} else {
UIScreen::screens(mtm)
.iter()
.position(|rhs| rhs == &**self.ui_screen(mtm))
.position(|rhs| rhs == &**ui_screen)
.map(|idx| idx.to_string())
}
})
@@ -184,32 +186,31 @@ impl MonitorHandle {
pub fn size(&self) -> PhysicalSize<u32> {
let bounds = self
.ui_screen
.get_on_main(|ui_screen| ui_screen.nativeBounds());
.get_on_main(|ui_screen, _| ui_screen.nativeBounds());
PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32)
}
pub fn position(&self) -> PhysicalPosition<i32> {
let bounds = self
.ui_screen
.get_on_main(|ui_screen| ui_screen.nativeBounds());
.get_on_main(|ui_screen, _| ui_screen.nativeBounds());
(bounds.origin.x as f64, bounds.origin.y as f64).into()
}
pub fn scale_factor(&self) -> f64 {
self.ui_screen
.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64
.get_on_main(|ui_screen, _| ui_screen.nativeScale()) as f64
}
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
Some(
self.ui_screen
.get_on_main(|ui_screen| refresh_rate_millihertz(ui_screen)),
.get_on_main(|ui_screen, _| refresh_rate_millihertz(ui_screen)),
)
}
pub fn video_modes(&self) -> impl Iterator<Item = VideoMode> {
MainThreadMarker::run_on_main(|mtm| {
let ui_screen = self.ui_screen(mtm);
self.ui_screen.get_on_main(|ui_screen, mtm| {
// Use Ord impl of RootVideoMode
let modes: BTreeSet<_> = ui_screen
@@ -229,12 +230,8 @@ impl MonitorHandle {
}
pub fn preferred_video_mode(&self) -> VideoMode {
MainThreadMarker::run_on_main(|mtm| {
VideoMode::new(
self.ui_screen(mtm).clone(),
self.ui_screen(mtm).preferredMode().unwrap(),
mtm,
)
self.ui_screen.get_on_main(|ui_screen, mtm| {
VideoMode::new(ui_screen.clone(), ui_screen.preferredMode().unwrap(), mtm)
})
}
}

View File

@@ -1,12 +1,12 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::Cell;
use std::ptr::NonNull;
use icrate::Foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSObjectProtocol, NSSet};
use objc2::declare::{Ivar, IvarDrop};
use objc2::rc::Id;
use objc2::runtime::AnyClass;
use objc2::{
declare_class, extern_methods, msg_send, msg_send_id, mutability, ClassType, DeclaredClass,
};
use objc2::{declare_class, extern_methods, msg_send, msg_send_id, mutability, ClassType};
use super::app_state::{self, EventWrapper};
use super::uikit::{
@@ -37,8 +37,6 @@ declare_class!(
const NAME: &'static str = "WinitUIView";
}
impl DeclaredClass for WinitView {}
unsafe impl WinitView {
#[method(drawRect:)]
fn draw_rect(&self, rect: CGRect) {
@@ -276,7 +274,11 @@ pub struct ViewControllerState {
}
declare_class!(
pub(crate) struct WinitViewController;
pub(crate) struct WinitViewController {
state: IvarDrop<Box<ViewControllerState>, "_state">,
}
mod view_controller_ivars;
unsafe impl ClassType for WinitViewController {
#[inherits(UIResponder, NSObject)]
@@ -285,8 +287,28 @@ declare_class!(
const NAME: &'static str = "WinitUIViewController";
}
impl DeclaredClass for WinitViewController {
type Ivars = ViewControllerState;
unsafe impl WinitViewController {
#[method(init)]
unsafe fn init(this: *mut Self) -> Option<NonNull<Self>> {
let this: Option<&mut Self> = msg_send![super(this), init];
this.map(|this| {
// These are set in WinitViewController::new, it's just to set them
// to _something_.
Ivar::write(
&mut this.state,
Box::new(ViewControllerState {
prefers_status_bar_hidden: Cell::new(false),
preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default),
prefers_home_indicator_auto_hidden: Cell::new(false),
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
preferred_screen_edges_deferring_system_gestures: Cell::new(
UIRectEdge::NONE,
),
}),
);
NonNull::from(this)
})
}
}
unsafe impl WinitViewController {
@@ -297,27 +319,27 @@ declare_class!(
#[method(prefersStatusBarHidden)]
fn prefers_status_bar_hidden(&self) -> bool {
self.ivars().prefers_status_bar_hidden.get()
self.state.prefers_status_bar_hidden.get()
}
#[method(preferredStatusBarStyle)]
fn preferred_status_bar_style(&self) -> UIStatusBarStyle {
self.ivars().preferred_status_bar_style.get()
self.state.preferred_status_bar_style.get()
}
#[method(prefersHomeIndicatorAutoHidden)]
fn prefers_home_indicator_auto_hidden(&self) -> bool {
self.ivars().prefers_home_indicator_auto_hidden.get()
self.state.prefers_home_indicator_auto_hidden.get()
}
#[method(supportedInterfaceOrientations)]
fn supported_orientations(&self) -> UIInterfaceOrientationMask {
self.ivars().supported_orientations.get()
self.state.supported_orientations.get()
}
#[method(preferredScreenEdgesDeferringSystemGestures)]
fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge {
self.ivars()
self.state
.preferred_screen_edges_deferring_system_gestures
.get()
}
@@ -326,17 +348,17 @@ declare_class!(
impl WinitViewController {
pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) {
self.ivars().prefers_status_bar_hidden.set(val);
self.state.prefers_status_bar_hidden.set(val);
self.setNeedsStatusBarAppearanceUpdate();
}
pub(crate) fn set_preferred_status_bar_style(&self, val: UIStatusBarStyle) {
self.ivars().preferred_status_bar_style.set(val);
self.state.preferred_status_bar_style.set(val);
self.setNeedsStatusBarAppearanceUpdate();
}
pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) {
self.ivars().prefers_home_indicator_auto_hidden.set(val);
self.state.prefers_home_indicator_auto_hidden.set(val);
let os_capabilities = app_state::os_capabilities();
if os_capabilities.home_indicator_hidden {
self.setNeedsUpdateOfHomeIndicatorAutoHidden();
@@ -346,7 +368,7 @@ impl WinitViewController {
}
pub(crate) fn set_preferred_screen_edges_deferring_system_gestures(&self, val: UIRectEdge) {
self.ivars()
self.state
.preferred_screen_edges_deferring_system_gestures
.set(val);
let os_capabilities = app_state::os_capabilities();
@@ -379,7 +401,7 @@ impl WinitViewController {
| UIInterfaceOrientationMask::PortraitUpsideDown
}
};
self.ivars().supported_orientations.set(mask);
self.state.supported_orientations.set(mask);
UIViewController::attemptRotationToDeviceOrientation();
}
@@ -389,15 +411,7 @@ impl WinitViewController {
platform_attributes: &PlatformSpecificWindowBuilderAttributes,
view: &UIView,
) -> Id<Self> {
// These are set properly below, we just to set them to something in the meantime.
let this = Self::alloc().set_ivars(ViewControllerState {
prefers_status_bar_hidden: Cell::new(false),
preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default),
prefers_home_indicator_auto_hidden: Cell::new(false),
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::NONE),
});
let this: Id<Self> = unsafe { msg_send_id![super(this), init] };
let this: Id<Self> = unsafe { msg_send_id![Self::alloc(), init] };
this.set_prefers_status_bar_hidden(platform_attributes.prefers_status_bar_hidden);
@@ -432,8 +446,6 @@ declare_class!(
const NAME: &'static str = "WinitUIWindow";
}
impl DeclaredClass for WinitUIWindow {}
unsafe impl WinitUIWindow {
#[method(becomeKeyWindow)]
fn become_key_window(&self) {
@@ -506,8 +518,6 @@ declare_class!(
const NAME: &'static str = "WinitApplicationDelegate";
}
impl DeclaredClass for WinitApplicationDelegate {}
// UIApplicationDelegate protocol
unsafe impl WinitApplicationDelegate {
#[method(application:didFinishLaunchingWithOptions:)]

View File

@@ -11,13 +11,14 @@ use super::app_state::EventWrapper;
use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation};
use super::view::{WinitUIWindow, WinitView, WinitViewController};
use crate::{
cursor::CustomCursor,
dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
event::{Event, WindowEvent},
icon::Icon,
platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations},
platform_impl::platform::{
app_state, monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, PlatformCustomCursor,
app_state, monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -25,19 +26,6 @@ use crate::{
},
};
#[derive(Debug, Clone)]
pub(crate) struct OwnedWindowHandle {}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(_handle: rwh_06::WindowHandle<'_>) -> Self {
// Parent windows are currently unsupported, though owned window
// handles would be implementable (would work similar to macOS).
warn!("parent windows are unsupported on iOS");
Self {}
}
}
pub struct Inner {
window: Id<WinitUIWindow>,
view_controller: Id<WinitViewController>,
@@ -190,7 +178,7 @@ impl Inner {
debug!("`Window::set_cursor_icon` ignored on iOS")
}
pub(crate) fn set_custom_cursor(&self, _: PlatformCustomCursor) {
pub fn set_custom_cursor(&self, _: CustomCursor) {
debug!("`Window::set_custom_cursor` ignored on iOS")
}
@@ -372,14 +360,14 @@ impl Inner {
}
#[cfg(feature = "rwh_06")]
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let mut window_handle = rwh_06::UiKitWindowHandle::new({
let ui_view = Id::as_ptr(&self.view) as _;
std::ptr::NonNull::new(ui_view).expect("Id<T> should never be null")
});
window_handle.ui_view_controller =
std::ptr::NonNull::new(Id::as_ptr(&self.view_controller) as _);
rwh_06::RawWindowHandle::UiKit(window_handle)
Ok(rwh_06::RawWindowHandle::UiKit(window_handle))
}
#[cfg(feature = "rwh_06")]
@@ -533,19 +521,7 @@ impl Window {
}
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Inner) -> R + Send) -> R {
self.inner.get_on_main(|inner| f(inner))
}
#[cfg(feature = "rwh_06")]
#[inline]
pub(crate) fn raw_window_handle_rwh_06(
&self,
) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
if let Some(mtm) = MainThreadMarker::new() {
Ok(self.inner.get(mtm).raw_window_handle_rwh_06())
} else {
Err(rwh_06::HandleError::Unavailable)
}
self.inner.get_on_main(|inner, _mtm| f(inner))
}
}

View File

@@ -14,6 +14,7 @@ use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex};
use once_cell::sync::Lazy;
use smol_str::SmolStr;
use crate::cursor::CustomCursor;
#[cfg(x11_platform)]
use crate::platform::x11::XlibErrorHook;
use crate::{
@@ -40,8 +41,7 @@ pub use x11::XNotSupported;
#[cfg(x11_platform)]
use x11::{util::WindowType as XWindowType, X11Error, XConnection, XError};
pub(crate) use crate::cursor::OnlyCursorImage as PlatformCustomCursor;
pub(crate) use crate::cursor::OnlyCursorImageBuilder as PlatformCustomCursorBuilder;
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;
@@ -141,43 +141,6 @@ impl fmt::Display for OsError {
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) enum OwnedWindowHandle {
#[cfg(x11_platform)]
X(x11rb::protocol::xproto::Window),
#[cfg(wayland_platform)]
Wayland,
}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(handle: rwh_06::WindowHandle<'_>) -> Self {
// TODO: Do we need to do something extra to extend the lifetime of
// the window lives beyond the passed-in handle?
match handle.as_raw() {
#[cfg(x11_platform)]
rwh_06::RawWindowHandle::Xlib(handle) => {
Self::X(handle.window as x11rb::protocol::xproto::Window)
}
#[cfg(x11_platform)]
rwh_06::RawWindowHandle::Xcb(handle) => Self::X(handle.window.get()),
#[cfg(wayland_platform)]
rwh_06::RawWindowHandle::Wayland(_handle) => {
// Wayland does not currently support parent windows, but it
// could support owned handles.
Self::Wayland
}
#[cfg(not(x11_platform))]
handle => panic!("invalid window handle {handle:?} on Wayland"),
#[cfg(not(wayland_platform))]
handle => panic!("invalid window handle {handle:?} on X11"),
#[cfg(all(x11_platform, wayland_platform))]
handle => panic!("invalid window handle {handle:?} on X11 or Wayland"),
}
}
}
pub(crate) enum Window {
#[cfg(x11_platform)]
X(x11::Window),
@@ -464,7 +427,7 @@ impl Window {
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
x11_or_wayland!(match self; Window(w) => w.set_custom_cursor(cursor))
}

View File

@@ -307,11 +307,7 @@ impl WindowHandler for WinitState {
&mut self.events_sink,
);
// NOTE: Only update when the value is `Some` to not override consequent configures with
// the same sizes.
if new_size.is_some() {
self.window_compositor_updates[pos].size = new_size;
}
self.window_compositor_updates[pos].size = Some(new_size);
}
}

View File

@@ -27,7 +27,7 @@ pub struct CustomCursor {
}
impl CustomCursor {
pub(crate) fn new(pool: &mut SlotPool, image: &CursorImage) -> Self {
pub fn new(pool: &mut SlotPool, image: &CursorImage) -> Self {
let (buffer, canvas) = pool
.create_buffer(
image.width as i32,

View File

@@ -15,13 +15,14 @@ use sctk::shell::xdg::window::Window as SctkWindow;
use sctk::shell::xdg::window::WindowDecorations;
use sctk::shell::WaylandSurface;
use crate::cursor::CustomCursor;
use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError};
use crate::event::{Ime, WindowEvent};
use crate::event_loop::AsyncRequestSerial;
use crate::platform_impl::{
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor,
PlatformIcon, PlatformSpecificWindowBuilderAttributes as PlatformAttributes,
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon,
PlatformSpecificWindowBuilderAttributes as PlatformAttributes,
};
use crate::window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -507,11 +508,8 @@ impl Window {
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
self.window_state
.lock()
.unwrap()
.set_custom_cursor(&cursor.0);
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
self.window_state.lock().unwrap().set_custom_cursor(cursor);
}
#[inline]

View File

@@ -28,7 +28,7 @@ use sctk::shm::Shm;
use sctk::subcompositor::SubcompositorState;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use crate::cursor::CursorImage;
use crate::cursor::CustomCursor as RootCustomCursor;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
use crate::error::{ExternalError, NotSupportedError};
use crate::event::WindowEvent;
@@ -262,7 +262,7 @@ impl WindowState {
shm: &Shm,
subcompositor: &Option<Arc<SubcompositorState>>,
event_sink: &mut EventSink,
) -> Option<LogicalSize<u32>> {
) -> LogicalSize<u32> {
// NOTE: when using fractional scaling or wl_compositor@v6 the scaling
// should be delivered before the first configure, thus apply it to
// properly scale the physical sizes provided by the users.
@@ -325,9 +325,14 @@ impl WindowState {
match configure.new_size {
(Some(width), Some(height)) => {
let (width, height) = frame.subtract_borders(width, height);
let width = width.map(|w| w.get()).unwrap_or(1);
let height = height.map(|h| h.get()).unwrap_or(1);
((width, height).into(), false)
(
(
width.map(|w| w.get()).unwrap_or(1),
height.map(|h| h.get()).unwrap_or(1),
)
.into(),
false,
)
}
(_, _) if stateless => (self.stateless_size, true),
_ => (self.size, true),
@@ -353,31 +358,13 @@ impl WindowState {
.unwrap_or(new_size.height);
}
let new_state = configure.state;
let old_state = self
.last_configure
.as_ref()
.map(|configure| configure.state);
let state_change_requires_resize = old_state
.map(|old_state| {
!old_state
.symmetric_difference(new_state)
.difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED)
.is_empty()
})
// NOTE: `None` is present for the initial configure, thus we must always resize.
.unwrap_or(true);
// NOTE: Set the configure before doing a resize, since we query it during it.
// XXX Set the configure before doing a resize.
self.last_configure = Some(configure);
if state_change_requires_resize || new_size != self.inner_size() {
self.resize(new_size);
Some(new_size)
} else {
None
}
// XXX Update the new size right away.
self.resize(new_size);
new_size
}
/// Compute the bounds for the inner size of the surface.
@@ -726,10 +713,10 @@ impl WindowState {
}
/// Set the custom cursor icon.
pub(crate) fn set_custom_cursor(&mut self, cursor: &CursorImage) {
pub fn set_custom_cursor(&mut self, cursor: RootCustomCursor) {
let cursor = {
let mut pool = self.custom_cursor_pool.lock().unwrap();
CustomCursor::new(&mut pool, cursor)
CustomCursor::new(&mut pool, &cursor.inner)
};
if self.cursor_visible {
@@ -983,7 +970,7 @@ impl WindowState {
/// Set the IME position.
pub fn set_ime_cursor_area(&self, position: LogicalPosition<u32>, size: LogicalSize<u32>) {
// FIXME: This won't fly unless user will have a way to request IME window per seat, since
// XXX This won't fly unless user will have a way to request IME window per seat, since
// the ime windows will be overlapping, but winit doesn't expose API to specify for
// which seat we're setting IME position.
let (x, y) = (position.x as i32, position.y as i32);
@@ -1014,7 +1001,7 @@ impl WindowState {
pub fn set_scale_factor(&mut self, scale_factor: f64) {
self.scale_factor = scale_factor;
// NOTE: When fractional scaling is not used update the buffer scale.
// XXX when fractional scaling is not used update the buffer scale.
if self.fractional_scale.is_none() {
let _ = self.window.set_buffer_scale(self.scale_factor as _);
}
@@ -1162,7 +1149,7 @@ impl From<ResizeDirection> for XdgResizeEdge {
}
}
// NOTE: Rust doesn't allow `From<Option<Theme>>`.
// XXX rust doesn't allow from `Option`.
#[cfg(feature = "sctk-adwaita")]
fn into_sctk_adwaita_config(theme: Option<Theme>) -> sctk_adwaita::FrameConfig {
match theme {

View File

@@ -40,7 +40,6 @@ atom_manager! {
WM_DELETE_WINDOW,
WM_PROTOCOLS,
WM_STATE,
XIM_SERVERS,
// Assorted ICCCM Atoms
_NET_WM_ICON,

View File

@@ -44,7 +44,7 @@ impl From<io::Error> for DndDataParseError {
pub(crate) struct Dnd {
xconn: Arc<XConnection>,
// Populated by XdndEnter event handler
pub version: Option<c_long>,
pub version: Option<u32>,
pub type_list: Option<Vec<xproto::Atom>>,
// Populated by XdndPosition event handler
pub source_window: Option<xproto::Window>,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,862 @@
//! IME handler, using the xim-rs crate.
use super::{X11Error, X11rbConnection, XConnection};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::Window;
use x11rb::protocol::Event;
use xim::x11rb::{HasConnection, X11rbClient};
use xim::{AttributeName, Client as _, ClientError, ClientHandler, InputStyle, Point};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
impl HasConnection for XConnection {
type Connection = X11rbConnection;
fn conn(&self) -> &Self::Connection {
self.xcb_connection()
}
}
/// A collection of the IME events that can occur.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ImeEvent {
Enabled,
Start,
Update(String, Option<usize>),
Commit(String),
End,
Disabled,
}
/// Invalid states that an IME client can enter.
#[derive(Debug, Clone)]
pub enum InvalidImeState {
/// The IME has no style information.
NoStyle,
/// No windows in the pending window queue.
NoWindows,
/// Invalid input context.
InvalidIc(u16),
}
impl fmt::Display for InvalidImeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidImeState::NoStyle => write!(f, "IME has no style information"),
InvalidImeState::NoWindows => {
write!(f, "IME has no windows in the pending window queue")
}
InvalidImeState::InvalidIc(ic) => write!(f, "IME has invalid input context {}", ic),
}
}
}
/// Request to control XIM handler from the window.
pub enum ImeRequest {
/// Set IME spot position for given `window_id`.
Position(Window, i16, i16),
/// Allow IME input for the given `window_id`.
Allow(Window, bool),
}
/// The IME data for winit.
pub(super) struct ImeData {
/// The XIM client manager.
client: X11rbClient<Arc<XConnection>>,
/// Relevant IME data.
handler: ImeHandler,
}
/// Inner IME handler.
struct ImeHandler {
/// Handle to the event queue.
event_queue: Rc<RefCell<VecDeque<Event>>>,
/// Whether IME is currently disconnected.
disconnected: bool,
/// IME events waiting to be read.
ime_events: VecDeque<(Window, ImeEvent)>,
/// Windows waiting to be assigned an input context.
pending_windows: VecDeque<WindowData>,
/// Currently registered input styles.
styles: Option<(Style, Style)>,
/// The input method for the display, if there is one.
input_method: Option<u16>,
/// Hash map between input contexts and their associated data.
input_contexts: HashMap<u16, IcData>,
/// Map between window IDs and their associated input contexts.
window_contexts: HashMap<Window, u16>,
}
/// Data relevant for each input context.
struct IcData {
/// Data associated with the window.
window: WindowData,
/// Newly set point for the context.
new_spot: Option<Point>,
/// The current preedit string.
///
/// We use a `Vec<char>` here instead of a string because the IME indices operate on chars,
/// not bytes.
text: Vec<char>,
/// The current cursor position in the preedit string.
cursor: usize,
}
/// Windows waiting for IME events.
struct WindowData {
/// The window ID.
id: Window,
/// The style of the window.
style: Style,
/// Current "spot" for the context.
spot: Point,
}
#[derive(Copy, Clone)]
enum Style {
Preedit,
Nothing,
None,
}
impl ImeData {
/// Creates the IME data for the display.
pub(super) fn new(
conn: &Arc<XConnection>,
screen: usize,
event_queue: &Rc<RefCell<VecDeque<Event>>>,
) -> Result<Self, X11Error> {
// IM servers to try, in order:
// - None, which defaults to the environment variable `XMODIFIERS` in xim's impl.
// - "local", which is the default for most IMEs.
// - empty string, which may work in some cases.
let input_methods = [None, Some("local"), Some("")];
let mut last_error = X11Error::Ime(ClientError::NoXimServer);
for im in input_methods {
// Try to initialize a client here.
match X11rbClient::init(conn.clone(), screen, im) {
Ok(client) => {
return Ok(Self {
client,
handler: ImeHandler {
event_queue: event_queue.clone(),
disconnected: true,
ime_events: VecDeque::new(),
pending_windows: VecDeque::new(),
styles: None,
input_method: None,
input_contexts: HashMap::new(),
window_contexts: HashMap::new(),
},
})
}
Err(err) => {
log::warn!("Failed to create XIM client for {:?}: {err}", ImData(im));
last_error = X11Error::Ime(err);
}
}
}
Err(last_error)
}
/// Filter an event.
pub(super) fn filter_event(&mut self, event: &Event) -> Result<bool, X11Error> {
self.client
.filter_event(event, &mut self.handler)
.map_err(X11Error::Ime)
}
/// Connection to the X server.
fn conn(&self) -> &X11rbConnection {
self.client.conn()
}
/// Get an IME event.
pub(super) fn next_ime_event(&mut self) -> Option<(Window, ImeEvent)> {
self.handler.ime_events.pop_front()
}
/// Create a new IME context for the provided window.
pub(super) fn create_context(
&mut self,
window: Window,
with_preedit: bool,
spot: Option<Point>,
) -> Result<bool, X11Error> {
// If we aren't connected, nothing can be done.
if self.handler.disconnected {
return Ok(false);
}
let method = match self.handler.input_method {
Some(im) => im,
None => return Ok(false),
};
// Get the current style.
let style = match (self.handler.styles, with_preedit) {
(None, _) => return Err(X11Error::InvalidImeState(InvalidImeState::NoStyle)),
(Some((preedit_style, _)), true) => preedit_style,
(Some((_, none_style)), false) => none_style,
};
// Setup IC attributes.
let ic_attributes = {
let mut ic_attributes = self
.client
.build_ic_attributes()
.push(AttributeName::ClientWindow, window);
let ic_style = match style {
Style::Preedit => InputStyle::PREEDIT_POSITION | InputStyle::STATUS_NOTHING,
Style::Nothing => InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING,
Style::None => InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE,
};
if let Some(spot) = spot.clone() {
ic_attributes = ic_attributes.push(AttributeName::SpotLocation, spot);
}
ic_attributes
.push(AttributeName::InputStyle, ic_style)
.build()
};
// Create the IC.
self.client.create_ic(method, ic_attributes)?;
// Add to the waiting window list.
self.handler.pending_windows.push_back(WindowData {
id: window,
style,
spot: spot.unwrap_or(Point { x: 0, y: 0 }),
});
Ok(true)
}
/// Remove an IME context for a window.
pub(super) fn remove_context(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = match self.handler.input_method {
Some(im) => im,
None => return Ok(false),
};
// Remove the pending window if it's still pending.
let mut removed = false;
self.handler.pending_windows.retain(|pending| {
if pending.id == window {
removed = true;
false
} else {
true
}
});
if removed {
return Ok(true);
}
// Remove the IC if it's already created.
if let Some(ic) = self.handler.window_contexts.remove(&window) {
self.handler.input_contexts.remove(&ic);
// Destroy the IC.
self.client.destroy_ic(method, ic)?;
}
Ok(false)
}
/// Focus an IME context.
pub(super) fn focus_window(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
self.client.set_focus(method, ic)?;
return Ok(true);
}
Ok(false)
}
/// Unfocus an IME context.
pub(super) fn unfocus_window(&mut self, window: Window) -> Result<bool, X11Error> {
if self.handler.disconnected {
return Ok(false);
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
self.client.unset_focus(method, ic)?;
return Ok(true);
}
Ok(false)
}
/// Set the spot for an IME context.
pub(super) fn set_spot(&mut self, window: Window, x: i16, y: i16) -> Result<(), X11Error> {
if self.handler.disconnected {
return Ok(());
}
let method = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
// If the IC is not available, or if the spot is the same, then we don't need to update.
let ic_data = match self.handler.input_contexts.get_mut(&ic) {
Some(ic_data) => ic_data,
None => return Ok(()),
};
let new_point = Point { x, y };
if !matches!(ic_data.window.style, Style::None) || ic_data.window.spot == new_point {
return Ok(());
}
let new_attrs = self
.client
.build_ic_attributes()
.push(AttributeName::SpotLocation, new_point.clone())
.build();
self.client.set_ic_values(method, ic, new_attrs)?;
// Indicate that we have a new spot.
debug_assert!(ic_data.new_spot.is_none());
ic_data.new_spot = Some(new_point);
}
Ok(())
}
pub(super) fn set_ime_allowed(
&mut self,
window: Window,
allowed: bool,
) -> Result<(), X11Error> {
if self.handler.disconnected {
return Ok(());
}
// Get the client info.
let _ = self.wait_for_method()?;
let ic = self.wait_for_context(window)?;
if let Some(ic) = ic {
let mut spot = None;
// See if we need to update the allowed state.
if let Some(ic_data) = self.handler.input_contexts.get(&ic) {
spot = Some(ic_data.window.spot.clone());
if matches!(ic_data.window.style, Style::None) != allowed {
return Ok(());
}
}
// Delete and re-install the IC.
self.remove_context(window)?;
self.create_context(window, allowed, spot)?;
}
Ok(())
}
/// Wait for the input method to be set.
fn wait_for_method(&mut self) -> Result<u16, X11Error> {
loop {
if let Some(im) = self.handler.input_method {
return Ok(im);
}
// Wait and hope the input method is set.
self.block_for_ime()?;
}
}
/// Wait for an input context to be set.
fn wait_for_context(&mut self, window: Window) -> Result<Option<u16>, X11Error> {
if let Some(cid) = self.handler.window_contexts.get(&window) {
return Ok(Some(*cid));
}
// If the window isn't in our pending windows queue, there's no way for it to get an IC.
if !self
.handler
.pending_windows
.iter()
.any(|WindowData { id, .. }| *id == window)
{
return Ok(None);
}
loop {
self.block_for_ime()?;
if let Some(cid) = self.handler.window_contexts.get(&window) {
return Ok(Some(*cid));
}
}
}
/// Wait until we've acted on an IME event.
fn block_for_ime(&mut self) -> Result<(), X11Error> {
let mut last_event = self.conn().poll_for_event()?;
loop {
if let Some(last_event) = last_event.as_ref() {
if self.filter_event(last_event)? {
return Ok(());
}
}
// This scope keeps track of the event queue handle.
{
// Check the event queue for events.
let event_queue = self.handler.event_queue.clone();
let mut event_queue = event_queue.borrow_mut();
// Check the event queue for events we can use.
let mut found_event = false;
let mut last_err = None;
event_queue.retain(|event| match self.filter_event(event) {
Ok(false) => {
found_event = true;
true
}
Ok(true) => false,
Err(err) => {
last_err = Some(err);
true
}
});
// Push our own event to the queue.
if let Some(last_event) = last_event.take() {
event_queue.push_back(last_event);
}
// Check for errors.
if let Some(err) = last_err {
return Err(err);
}
// If we found an event, then we're done.
if found_event {
return Ok(());
}
}
log::info!("Waiting for IME event");
last_event = Some(self.conn().wait_for_event()?);
}
}
}
impl<C: xim::Client> ClientHandler<C> for ImeHandler {
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
// We have been connected, now request a new input method for our current locale.
self.disconnected = false;
client.open(&locale())
}
fn handle_disconnect(&mut self) {
// We are now disconnected.
self.disconnected = true;
}
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
// We now have an input method.
debug_assert!(self.input_method.is_none());
self.input_method = Some(input_method_id);
// Ask for the IM's attributes.
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
}
fn handle_close(&mut self, _client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
// No more input method.
debug_assert_eq!(self.input_method, Some(input_method_id));
self.input_method = None;
Ok(())
}
fn handle_get_im_values(
&mut self,
_client: &mut C,
input_method_id: u16,
mut attributes: xim::AHashMap<xim::AttributeName, Vec<u8>>,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the input styles.
let mut preedit_style = None;
let mut none_style = None;
let styles = {
let style = attributes
.remove(&AttributeName::QueryInputStyle)
.expect("No query input style");
let mut result = vec![0u32; style.len() / 4];
bytemuck::cast_slice_mut::<u32, u8>(&mut result).copy_from_slice(&style);
result
};
{
// The styles that we're looking for.
let lu_preedit_style = InputStyle::PREEDIT_CALLBACKS | InputStyle::STATUS_NOTHING;
let lu_nothing_style = InputStyle::PREEDIT_NOTHING | InputStyle::STATUS_NOTHING;
let lu_none_style = InputStyle::PREEDIT_NONE | InputStyle::STATUS_NONE;
for style in styles {
let style = InputStyle::from_bits_truncate(style);
if style == lu_preedit_style {
preedit_style = Some(Style::Preedit);
} else if style == lu_nothing_style {
preedit_style = Some(Style::Nothing);
} else if style == lu_none_style {
none_style = Some(Style::None);
}
}
}
let (preedit_style, none_style) = match (preedit_style, none_style) {
(None, None) => {
log::error!("No supported input styles found");
return Ok(());
}
(Some(style), None) | (None, Some(style)) => (style, style),
(Some(preedit_style), Some(none_style)) => (preedit_style, none_style),
};
self.styles = Some((preedit_style, none_style));
Ok(())
}
fn handle_create_ic(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the window that wanted the IC context.
let window = self
.pending_windows
.pop_front()
.ok_or_else(|| invalid_state(InvalidImeState::NoWindows))?;
// Create the IC data.
let ic_data = IcData {
window,
new_spot: None,
text: Vec::new(),
cursor: 0,
};
// Store the context.
let (window, style) = (ic_data.window.id, ic_data.window.style);
self.input_contexts.insert(input_context_id, ic_data);
self.window_contexts.insert(window, input_context_id);
// Indicate our status.
let event = if matches!(style, Style::Nothing) {
ImeEvent::Disabled
} else {
ImeEvent::Enabled
};
self.ime_events.push_back((window, event));
Ok(())
}
fn handle_destroy_ic(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
) -> Result<(), ClientError> {
// This is already handled by the higher-level function.
Ok(())
}
fn handle_set_ic_values(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the IC data.
let ic_data = self
.input_contexts
.get_mut(&input_context_id)
.ok_or_else(|| invalid_state(InvalidImeState::InvalidIc(input_context_id)))?;
// Move up the new spot
if let Some(spot) = ic_data.new_spot.take() {
ic_data.window.spot = spot;
}
Ok(())
}
fn handle_preedit_start(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Start a pre-edit.
ic_data.text.clear();
ic_data.cursor = 0;
// Indicate the start.
self.ime_events
.push_back((ic_data.window.id, ImeEvent::Start));
}
Ok(())
}
fn handle_preedit_draw(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
caret: i32,
chg_first: i32,
chg_len: i32,
_status: xim::PreeditDrawStatus,
preedit_string: &str,
_feedbacks: Vec<xim::Feedback>,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Set the cursor.
ic_data.cursor = caret as usize;
// Figure out the range of text to change.
let change_range = chg_first as usize..(chg_first + chg_len) as usize;
// If the range doesn't fit our current text, warn and return.
if change_range.start > ic_data.text.len() || change_range.end > ic_data.text.len() {
warn!(
"Preedit draw range {}..{} doesn't fit text of length {}",
change_range.start,
change_range.end,
ic_data.text.len()
);
return Ok(());
}
// Update the text in the changed range.
{
let text = &mut ic_data.text;
let mut old_text_tail = text.split_off(change_range.end);
text.truncate(change_range.start);
text.extend(preedit_string.chars());
text.append(&mut old_text_tail);
}
// Send the event.
let cursor_byte_pos = calc_byte_position(&ic_data.text, ic_data.cursor);
let event = ImeEvent::Update(ic_data.text.iter().collect(), Some(cursor_byte_pos));
self.ime_events.push_back((ic_data.window.id, event));
}
Ok(())
}
fn handle_preedit_caret(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
position: &mut i32,
direction: xim::CaretDirection,
_style: xim::CaretStyle,
) -> Result<(), ClientError> {
// We only care about absolute position.
if matches!(direction, xim::CaretDirection::AbsolutePosition) {
debug_assert_eq!(self.input_method, Some(input_method_id));
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
ic_data.cursor = *position as usize;
// Send the event
let event =
ImeEvent::Update(ic_data.text.iter().collect(), Some(*position as usize));
self.ime_events.push_back((ic_data.window.id, event));
}
}
Ok(())
}
fn handle_preedit_done(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the client data.
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// We're done with a preedit.
ic_data.text.clear();
ic_data.cursor = 0;
// Send a message to the window.
let window = ic_data.window.id;
self.ime_events.push_back((window, ImeEvent::End));
}
Ok(())
}
fn handle_commit(
&mut self,
_client: &mut C,
input_method_id: u16,
input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
debug_assert_eq!(self.input_method, Some(input_method_id));
// Get the client data.
if let Some(ic_data) = self.input_contexts.get_mut(&input_context_id) {
// Send a message to the window.
let window = ic_data.window.id;
self.ime_events
.push_back((window, ImeEvent::Commit(text.to_owned())));
}
Ok(())
}
fn handle_query_extension(
&mut self,
_client: &mut C,
_extensions: &[xim::Extension],
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
fn handle_forward_event(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_flag: xim::ForwardEventFlag,
_xev: C::XEvent,
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
fn handle_set_event_mask(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_forward_event_mask: u32,
_synchronous_event_mask: u32,
) -> Result<(), ClientError> {
// Don't care.
Ok(())
}
}
#[inline(always)]
fn invalid_state(state: InvalidImeState) -> ClientError {
ClientError::Other(Box::new(X11Error::InvalidImeState(state)))
}
struct ImData(Option<&'static str>);
impl fmt::Debug for ImData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(name) => write!(f, "\"{}\"", name),
None => write!(f, "default input method"),
}
}
}
/// Get the current locale.
fn locale() -> String {
use std::ffi::CStr;
const EN_US: &str = "en_US.UTF-8";
// Get the pointer to the current locale.
let locale_ptr = unsafe { libc::setlocale(libc::LC_CTYPE, std::ptr::null()) };
// If locale_ptr is null, just default to en_US.UTF-8.
if locale_ptr.is_null() {
return EN_US.to_owned();
}
// Convert the pointer to a CStr.
let locale_cstr = unsafe { CStr::from_ptr(locale_ptr) };
// Convert the CStr to a String to prevent the result from getting clobbered.
locale_cstr.to_str().unwrap_or(EN_US).to_owned()
}
fn calc_byte_position(text: &[char], pos: usize) -> usize {
text.iter().take(pos).map(|c| c.len_utf8()).sum()
}

View File

@@ -1,215 +0,0 @@
use std::{collections::HashMap, os::raw::c_char, ptr, sync::Arc};
use super::{ffi, XConnection, XError};
use super::{
context::{ImeContext, ImeContextCreationError},
inner::{close_im, ImeInner},
input_method::PotentialInputMethods,
};
pub(crate) unsafe fn xim_set_callback(
xconn: &Arc<XConnection>,
xim: ffi::XIM,
field: *const c_char,
callback: *mut ffi::XIMCallback,
) -> Result<(), XError> {
// It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize
// access that isn't type-checked.
unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) };
xconn.check_errors()
}
// Set a callback for when an input method matching the current locale modifiers becomes
// available. Note that this has nothing to do with what input methods are open or able to be
// opened, and simply uses the modifiers that are set when the callback is set.
// * This is called per locale modifier, not per input method opened with that locale modifier.
// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt
// input contexts would always silently fail to use the input method.
pub(crate) unsafe fn set_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
unsafe {
(xconn.xlib.XRegisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
)
};
xconn.check_errors()
}
pub(crate) unsafe fn unset_instantiate_callback(
xconn: &Arc<XConnection>,
client_data: ffi::XPointer,
) -> Result<(), XError> {
unsafe {
(xconn.xlib.XUnregisterIMInstantiateCallback)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
Some(xim_instantiate_callback),
client_data,
)
};
xconn.check_errors()
}
pub(crate) unsafe fn set_destroy_callback(
xconn: &Arc<XConnection>,
im: ffi::XIM,
inner: &ImeInner,
) -> Result<(), XError> {
unsafe {
xim_set_callback(
xconn,
im,
ffi::XNDestroyCallback_0.as_ptr() as *const _,
&inner.destroy_callback as *const _ as *mut _,
)
}
}
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
enum ReplaceImError {
// Boxed to prevent large error type
MethodOpenFailed(Box<PotentialInputMethods>),
ContextCreationFailed(ImeContextCreationError),
SetDestroyCallbackFailed(XError),
}
// Attempt to replace current IM (which may or may not be presently valid) with a new one. This
// includes replacing all existing input contexts and free'ing resources as necessary. This only
// modifies existing state if all operations succeed.
unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
let xconn = unsafe { &(*inner).xconn };
let (new_im, is_fallback) = {
let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) };
let is_fallback = new_im.is_fallback();
(
new_im.ok().ok_or_else(|| {
ReplaceImError::MethodOpenFailed(Box::new(unsafe {
(*inner).potential_input_methods.clone()
}))
})?,
is_fallback,
)
};
// It's important to always set a destroy callback, since there's otherwise potential for us
// to try to use or free a resource that's already been destroyed on the server.
{
let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) };
if result.is_err() {
let _ = unsafe { close_im(xconn, new_im.im) };
}
result
}
.map_err(ReplaceImError::SetDestroyCallbackFailed)?;
let mut new_contexts = HashMap::new();
for (window, old_context) in unsafe { (*inner).contexts.iter() } {
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
// Check if the IME was allowed on that context.
let is_allowed = old_context
.as_ref()
.map(|old_context| old_context.is_allowed())
.unwrap_or_default();
// We can't use the style from the old context here, since it may change on reload, so
// pick style from the new XIM based on the old state.
let style = if is_allowed {
new_im.preedit_style
} else {
new_im.none_style
};
let new_context = {
let result = unsafe {
ImeContext::new(
xconn,
new_im.im,
style,
*window,
spot,
(*inner).event_sender.clone(),
)
};
if result.is_err() {
let _ = unsafe { close_im(xconn, new_im.im) };
}
result.map_err(ReplaceImError::ContextCreationFailed)?
};
new_contexts.insert(*window, Some(new_context));
}
// If we've made it this far, everything succeeded.
unsafe {
let _ = (*inner).destroy_all_contexts_if_necessary();
let _ = (*inner).close_im_if_necessary();
(*inner).im = Some(new_im);
(*inner).contexts = new_contexts;
(*inner).is_destroyed = false;
(*inner).is_fallback = is_fallback;
}
Ok(())
}
pub unsafe extern "C" fn xim_instantiate_callback(
_display: *mut ffi::Display,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
let xconn = unsafe { &(*inner).xconn };
match unsafe { replace_im(inner) } {
Ok(()) => unsafe {
let _ = unset_instantiate_callback(xconn, client_data);
(*inner).is_fallback = false;
},
Err(err) => unsafe {
if (*inner).is_destroyed {
// We have no usable input methods!
panic!("Failed to reopen input method: {err:?}");
}
},
}
}
}
// This callback is triggered when the input method is closed on the server end. When this
// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been
// free'd (attempting to do so causes our connection to freeze).
pub unsafe extern "C" fn xim_destroy_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
// This field is unsupplied.
_call_data: ffi::XPointer,
) {
let inner: *mut ImeInner = client_data as _;
if !inner.is_null() {
unsafe { (*inner).is_destroyed = true };
let xconn = unsafe { &(*inner).xconn };
if unsafe { !(*inner).is_fallback } {
let _ = unsafe { set_instantiate_callback(xconn, client_data) };
// Attempt to open fallback input method.
match unsafe { replace_im(inner) } {
Ok(()) => unsafe { (*inner).is_fallback = true },
Err(err) => {
// We have no usable input methods!
panic!("Failed to open fallback input method: {err:?}");
}
}
}
}
}

View File

@@ -1,385 +0,0 @@
use std::ffi::CStr;
use std::os::raw::c_short;
use std::sync::Arc;
use std::{mem, ptr};
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
use crate::platform_impl::platform::x11::ime::input_method::{Style, XIMStyle};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
use super::{ffi, util, XConnection, XError};
/// IME creation error.
#[derive(Debug)]
pub enum ImeContextCreationError {
/// Got the error from Xlib.
XError(XError),
/// Got null pointer from Xlib but without exact reason.
Null,
}
/// The callback used by XIM preedit functions.
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
/// Wrapper for creating XIM callbacks.
#[inline]
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
XIMCallback {
client_data,
callback: Some(callback),
}
}
/// The server started preedit.
extern "C" fn preedit_start_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) -> i32 {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
client_data.text.clear();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::Start))
.expect("failed to send preedit start event");
-1
}
/// Done callback is used when the preedit should be hidden.
extern "C" fn preedit_done_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
// Drop text buffer and reset cursor position on done.
client_data.text = Vec::new();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::End))
.expect("failed to send preedit end event");
}
fn calc_byte_position(text: &[char], pos: usize) -> usize {
text.iter()
.take(pos)
.fold(0, |byte_pos, text| byte_pos + text.len_utf8())
}
/// Preedit text information to be drawn inline by the client.
extern "C" fn preedit_draw_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
client_data.cursor_pos = call_data.caret as usize;
let chg_range =
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
warn!(
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
client_data.text.len(),
call_data.chg_first,
call_data.chg_length
);
return;
}
// NULL indicate text deletion
let mut new_chars = if call_data.text.is_null() {
Vec::new()
} else {
let xim_text = unsafe { &mut *(call_data.text) };
if xim_text.encoding_is_wchar > 0 {
return;
}
let new_text = unsafe { xim_text.string.multi_byte };
if new_text.is_null() {
return;
}
let new_text = unsafe { CStr::from_ptr(new_text) };
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
.chars()
.collect()
};
let mut old_text_tail = client_data.text.split_off(chg_range.end);
client_data.text.truncate(chg_range.start);
client_data.text.append(&mut new_chars);
client_data.text.append(&mut old_text_tail);
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
/// Handling of cursor movements in preedit text.
extern "C" fn preedit_caret_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
client_data.cursor_pos = call_data.position as usize;
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
}
/// Struct to simplify callback creation and latter passing into Xlib XIM.
struct PreeditCallbacks {
start_callback: ffi::XIMCallback,
done_callback: ffi::XIMCallback,
draw_callback: ffi::XIMCallback,
caret_callback: ffi::XIMCallback,
}
impl PreeditCallbacks {
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
let start_callback = create_xim_callback(client_data, unsafe {
mem::transmute(preedit_start_callback as usize)
});
let done_callback = create_xim_callback(client_data, preedit_done_callback);
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
PreeditCallbacks {
start_callback,
done_callback,
caret_callback,
draw_callback,
}
}
}
struct ImeContextClientData {
window: ffi::Window,
event_sender: ImeEventSender,
text: Vec<char>,
cursor_pos: usize,
}
// XXX: this struct doesn't destroy its XIC resource when dropped.
// This is intentional, as it doesn't have enough information to know whether or not the context
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
// through `ImeInner`.
pub struct ImeContext {
pub(crate) ic: ffi::XIC,
pub(crate) ic_spot: ffi::XPoint,
pub(crate) style: Style,
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
// there we keep the pointer to automatically deallocate it.
_client_data: Box<ImeContextClientData>,
}
impl ImeContext {
pub(crate) unsafe fn new(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: Style,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
event_sender: ImeEventSender,
) -> Result<Self, ImeContextCreationError> {
let client_data = Box::into_raw(Box::new(ImeContextClientData {
window,
event_sender,
text: Vec::new(),
cursor_pos: 0,
}));
let ic = match style as _ {
Style::Preedit(style) => unsafe {
ImeContext::create_preedit_ic(
xconn,
im,
style,
window,
client_data as ffi::XPointer,
)
},
Style::Nothing(style) => unsafe {
ImeContext::create_nothing_ic(xconn, im, style, window)
},
Style::None(style) => unsafe { ImeContext::create_none_ic(xconn, im, style, window) },
}
.ok_or(ImeContextCreationError::Null)?;
xconn
.check_errors()
.map_err(ImeContextCreationError::XError)?;
let mut context = ImeContext {
ic,
ic_spot: ffi::XPoint { x: 0, y: 0 },
style,
_client_data: unsafe { Box::from_raw(client_data) },
};
// Set the spot location, if it's present.
if let Some(ic_spot) = ic_spot {
context.set_spot(xconn, ic_spot.x, ic_spot.y)
}
Ok(context)
}
unsafe fn create_none_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
unsafe fn create_preedit_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
client_data: ffi::XPointer,
) -> Option<ffi::XIC> {
let preedit_callbacks = PreeditCallbacks::new(client_data);
let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe {
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
&(preedit_callbacks.start_callback) as *const _,
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
&(preedit_callbacks.done_callback) as *const _,
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
&(preedit_callbacks.caret_callback) as *const _,
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
&(preedit_callbacks.draw_callback) as *const _,
ptr::null_mut::<()>(),
)
})
.expect("XVaCreateNestedList returned NULL");
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
preedit_attr.ptr,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
unsafe fn create_nothing_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
style: XIMStyle,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = unsafe {
(xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
style,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
)
};
(!ic.is_null()).then_some(ic)
}
pub(crate) fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XSetICFocus)(self.ic);
}
xconn.check_errors()
}
pub(crate) fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
unsafe {
(xconn.xlib.XUnsetICFocus)(self.ic);
}
xconn.check_errors()
}
pub fn is_allowed(&self) -> bool {
!matches!(self.style, Style::None(_))
}
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
// are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
// window and couldn't be changed.
//
// For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
pub(crate) fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y {
return;
}
self.ic_spot = ffi::XPoint { x, y };
unsafe {
let preedit_attr = util::memory::XSmartPointer::new(
xconn,
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNSpotLocation_0.as_ptr(),
&self.ic_spot,
ptr::null_mut::<()>(),
),
)
.expect("XVaCreateNestedList returned NULL");
(xconn.xlib.XSetICValues)(
self.ic,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
preedit_attr.ptr,
ptr::null_mut::<()>(),
);
}
}
}

View File

@@ -1,75 +0,0 @@
use std::{collections::HashMap, mem, sync::Arc};
use super::{ffi, XConnection, XError};
use super::{
context::ImeContext,
input_method::{InputMethod, PotentialInputMethods},
};
use crate::platform_impl::platform::x11::ime::ImeEventSender;
pub(crate) unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
unsafe { (xconn.xlib.XCloseIM)(im) };
xconn.check_errors()
}
pub(crate) unsafe fn destroy_ic(xconn: &Arc<XConnection>, ic: ffi::XIC) -> Result<(), XError> {
unsafe { (xconn.xlib.XDestroyIC)(ic) };
xconn.check_errors()
}
pub(crate) struct ImeInner {
pub xconn: Arc<XConnection>,
pub im: Option<InputMethod>,
pub potential_input_methods: PotentialInputMethods,
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
// WARNING: this is initially zeroed!
pub destroy_callback: ffi::XIMCallback,
pub event_sender: ImeEventSender,
// Indicates whether or not the the input method was destroyed on the server end
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
pub is_destroyed: bool,
pub is_fallback: bool,
}
impl ImeInner {
pub(crate) fn new(
xconn: Arc<XConnection>,
potential_input_methods: PotentialInputMethods,
event_sender: ImeEventSender,
) -> Self {
ImeInner {
xconn,
im: None,
potential_input_methods,
contexts: HashMap::new(),
destroy_callback: unsafe { mem::zeroed() },
event_sender,
is_destroyed: false,
is_fallback: false,
}
}
pub unsafe fn close_im_if_necessary(&self) -> Result<bool, XError> {
if !self.is_destroyed && self.im.is_some() {
unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result<bool, XError> {
if !self.is_destroyed {
unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true)
} else {
Ok(false)
}
}
pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result<bool, XError> {
for context in self.contexts.values().flatten() {
unsafe { self.destroy_ic_if_necessary(context.ic)? };
}
Ok(!self.is_destroyed)
}
}

View File

@@ -1,370 +0,0 @@
use std::{
env,
ffi::{CStr, CString, IntoStringError},
fmt,
os::raw::{c_char, c_ulong, c_ushort},
ptr,
sync::{Arc, Mutex},
};
use super::{super::atoms::*, ffi, util, XConnection, XError};
use once_cell::sync::Lazy;
use x11rb::protocol::xproto;
static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(Default::default);
unsafe fn open_im(xconn: &Arc<XConnection>, locale_modifiers: &CStr) -> Option<ffi::XIM> {
let _lock = GLOBAL_LOCK.lock();
// XSetLocaleModifiers returns...
// * The current locale modifiers if it's given a NULL pointer.
// * The new locale modifiers if we succeeded in setting them.
// * NULL if the locale modifiers string is malformed or if the
// current locale is not supported by Xlib.
unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) };
let im = unsafe {
(xconn.xlib.XOpenIM)(
xconn.display,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if im.is_null() {
None
} else {
Some(im)
}
}
#[derive(Debug)]
pub struct InputMethod {
pub im: ffi::XIM,
pub preedit_style: Style,
pub none_style: Style,
_name: String,
}
impl InputMethod {
fn new(xconn: &Arc<XConnection>, im: ffi::XIM, name: String) -> Option<Self> {
let mut styles: *mut XIMStyles = std::ptr::null_mut();
// Query the styles supported by the XIM.
unsafe {
if !(xconn.xlib.XGetIMValues)(
im,
ffi::XNQueryInputStyle_0.as_ptr() as *const _,
(&mut styles) as *mut _,
std::ptr::null_mut::<()>(),
)
.is_null()
{
return None;
}
}
let mut preedit_style = None;
let mut none_style = None;
unsafe {
std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _)
.iter()
.for_each(|style| match *style {
XIM_PREEDIT_STYLE => {
preedit_style = Some(Style::Preedit(*style));
}
XIM_NOTHING_STYLE if preedit_style.is_none() => {
preedit_style = Some(Style::Nothing(*style))
}
XIM_NONE_STYLE => none_style = Some(Style::None(*style)),
_ => (),
});
(xconn.xlib.XFree)(styles.cast());
};
if preedit_style.is_none() && none_style.is_none() {
return None;
}
let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap());
let none_style = none_style.unwrap_or(preedit_style);
Some(InputMethod {
im,
_name: name,
preedit_style,
none_style,
})
}
}
const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle;
/// Style of the IME context.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style {
/// Preedit callbacks.
Preedit(XIMStyle),
/// Nothing.
Nothing(XIMStyle),
/// No IME.
None(XIMStyle),
}
impl Default for Style {
fn default() -> Self {
Style::None(XIM_NONE_STYLE)
}
}
#[repr(C)]
#[derive(Debug)]
struct XIMStyles {
count_styles: c_ushort,
supported_styles: *const XIMStyle,
}
pub(crate) type XIMStyle = c_ulong;
#[derive(Debug)]
pub enum InputMethodResult {
/// Input method used locale modifier from `XMODIFIERS` environment variable.
XModifiers(InputMethod),
/// Input method used internal fallback locale modifier.
Fallback(InputMethod),
/// Input method could not be opened using any locale modifier tried.
Failure,
}
impl InputMethodResult {
pub fn is_fallback(&self) -> bool {
matches!(self, InputMethodResult::Fallback(_))
}
pub fn ok(self) -> Option<InputMethod> {
use self::InputMethodResult::*;
match self {
XModifiers(im) | Fallback(im) => Some(im),
Failure => None,
}
}
}
#[derive(Debug, Clone)]
enum GetXimServersError {
XError(XError),
GetPropertyError(util::GetPropertyError),
InvalidUtf8(IntoStringError),
}
impl From<util::GetPropertyError> for GetXimServersError {
fn from(error: util::GetPropertyError) -> Self {
GetXimServersError::GetPropertyError(error)
}
}
// The root window has a property named XIM_SERVERS, which contains a list of atoms represeting
// the availabile XIM servers. For instance, if you're using ibus, it would contain an atom named
// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
// XMODIFIERS to `@server=ibus`?!?"
unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
let atoms = xconn.atoms();
let servers_atom = atoms[XIM_SERVERS];
let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) };
let mut atoms: Vec<ffi::Atom> = xconn
.get_property::<xproto::Atom>(
root as xproto::Window,
servers_atom,
xproto::Atom::from(xproto::AtomEnum::ATOM),
)
.map_err(GetXimServersError::GetPropertyError)?
.into_iter()
.map(ffi::Atom::from)
.collect::<Vec<_>>();
let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
unsafe {
(xconn.xlib.XGetAtomNames)(
xconn.display,
atoms.as_mut_ptr(),
atoms.len() as _,
names.as_mut_ptr() as _,
)
};
unsafe { names.set_len(atoms.len()) };
let mut formatted_names = Vec::with_capacity(names.len());
for name in names {
let string = unsafe { CStr::from_ptr(name) }
.to_owned()
.into_string()
.map_err(GetXimServersError::InvalidUtf8)?;
unsafe { (xconn.xlib.XFree)(name as _) };
formatted_names.push(string.replace("@server=", "@im="));
}
xconn.check_errors().map_err(GetXimServersError::XError)?;
Ok(formatted_names)
}
#[derive(Clone)]
struct InputMethodName {
c_string: CString,
string: String,
}
impl InputMethodName {
pub fn from_string(string: String) -> Self {
let c_string = CString::new(string.clone())
.expect("String used to construct CString contained null byte");
InputMethodName { c_string, string }
}
pub fn from_str(string: &str) -> Self {
let c_string =
CString::new(string).expect("String used to construct CString contained null byte");
InputMethodName {
c_string,
string: string.to_owned(),
}
}
}
impl fmt::Debug for InputMethodName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.string.fmt(f)
}
}
#[derive(Debug, Clone)]
struct PotentialInputMethod {
name: InputMethodName,
successful: Option<bool>,
}
impl PotentialInputMethod {
pub fn from_string(string: String) -> Self {
PotentialInputMethod {
name: InputMethodName::from_string(string),
successful: None,
}
}
pub fn from_str(string: &str) -> Self {
PotentialInputMethod {
name: InputMethodName::from_str(string),
successful: None,
}
}
pub fn reset(&mut self) {
self.successful = None;
}
pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
let im = unsafe { open_im(xconn, &self.name.c_string) };
self.successful = Some(im.is_some());
im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone()))
}
}
// By logging this struct, you get a sequential listing of every locale modifier tried, where it
// came from, and if it succeeded.
#[derive(Debug, Clone)]
pub(crate) struct PotentialInputMethods {
// On correctly configured systems, the XMODIFIERS environment variable tells us everything we
// need to know.
xmodifiers: Option<PotentialInputMethod>,
// We have some standard options at our disposal that should ostensibly always work. For users
// who only need compose sequences, this ensures that the program launches without a hitch
// For users who need more sophisticated IME features, this is more or less a silent failure.
// Logging features should be added in the future to allow both audiences to be effectively
// served.
fallbacks: [PotentialInputMethod; 2],
// For diagnostic purposes, we include the list of XIM servers that the server reports as
// being available.
_xim_servers: Result<Vec<String>, GetXimServersError>,
}
impl PotentialInputMethods {
pub fn new(xconn: &Arc<XConnection>) -> Self {
let xmodifiers = env::var("XMODIFIERS")
.ok()
.map(PotentialInputMethod::from_string);
PotentialInputMethods {
// Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
// XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
// running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
// defined in the profile (or parent environment) then that parent XMODIFIERS is used.
// If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
// XSetLocaleModifiers uses the default local input method. Note that defining
// XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
// that case, we get `None` and end up skipping ahead to the next method.
xmodifiers,
fallbacks: [
// This is a standard input method that supports compose sequences, which should
// always be available. `@im=none` appears to mean the same thing.
PotentialInputMethod::from_str("@im=local"),
// This explicitly specifies to use the implementation-dependent default, though
// that seems to be equivalent to just using the local input method.
PotentialInputMethod::from_str("@im="),
],
// The XIM_SERVERS property can have surprising values. For instance, when I exited
// ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
// that the fcitx input method could only be successfully opened using "@im=ibus".
// Presumably due to this quirk, it's actually possible to alternate between ibus and
// fcitx in a running application.
_xim_servers: unsafe { get_xim_servers(xconn) },
}
}
// This resets the `successful` field of every potential input method, ensuring we have
// accurate information when this struct is re-used by the destruction/instantiation callbacks.
fn reset(&mut self) {
if let Some(ref mut input_method) = self.xmodifiers {
input_method.reset();
}
for input_method in &mut self.fallbacks {
input_method.reset();
}
}
pub fn open_im(
&mut self,
xconn: &Arc<XConnection>,
callback: Option<&dyn Fn()>,
) -> InputMethodResult {
use self::InputMethodResult::*;
self.reset();
if let Some(ref mut input_method) = self.xmodifiers {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return XModifiers(im);
} else if let Some(ref callback) = callback {
callback();
}
}
for input_method in &mut self.fallbacks {
let im = input_method.open_im(xconn);
if let Some(im) = im {
return Fallback(im);
}
}
Failure
}
}

View File

@@ -1,249 +0,0 @@
// Important: all XIM calls need to happen from the same thread!
mod callbacks;
mod context;
mod inner;
mod input_method;
use std::sync::{
mpsc::{Receiver, Sender},
Arc,
};
use super::{ffi, util, XConnection, XError};
pub use self::context::ImeContextCreationError;
use self::{
callbacks::*,
context::ImeContext,
inner::{close_im, ImeInner},
input_method::{PotentialInputMethods, Style},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ImeEvent {
Enabled,
Start,
Update(String, usize),
End,
Disabled,
}
pub type ImeReceiver = Receiver<ImeRequest>;
pub type ImeSender = Sender<ImeRequest>;
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
/// Request to control XIM handler from the window.
pub enum ImeRequest {
/// Set IME spot position for given `window_id`.
Position(ffi::Window, i16, i16),
/// Allow IME input for the given `window_id`.
Allow(ffi::Window, bool),
}
#[derive(Debug)]
pub(crate) enum ImeCreationError {
// Boxed to prevent large error type
OpenFailure(Box<PotentialInputMethods>),
SetDestroyCallbackFailed(XError),
}
pub(crate) struct Ime {
xconn: Arc<XConnection>,
// The actual meat of this struct is boxed away, since it needs to have a fixed location in
// memory so we can pass a pointer to it around.
inner: Box<ImeInner>,
}
impl Ime {
pub fn new(
xconn: Arc<XConnection>,
event_sender: ImeEventSender,
) -> Result<Self, ImeCreationError> {
let potential_input_methods = PotentialInputMethods::new(&xconn);
let (mut inner, client_data) = {
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
let inner_ptr = Box::into_raw(inner);
let client_data = inner_ptr as _;
let destroy_callback = ffi::XIMCallback {
client_data,
callback: Some(xim_destroy_callback),
};
inner = unsafe { Box::from_raw(inner_ptr) };
inner.destroy_callback = destroy_callback;
(inner, client_data)
};
let xconn = Arc::clone(&inner.xconn);
let input_method = inner.potential_input_methods.open_im(
&xconn,
Some(&|| {
let _ = unsafe { set_instantiate_callback(&xconn, client_data) };
}),
);
let is_fallback = input_method.is_fallback();
if let Some(input_method) = input_method.ok() {
inner.is_fallback = is_fallback;
unsafe {
let result = set_destroy_callback(&xconn, input_method.im, &inner)
.map_err(ImeCreationError::SetDestroyCallbackFailed);
if result.is_err() {
let _ = close_im(&xconn, input_method.im);
}
result?;
}
inner.im = Some(input_method);
Ok(Ime { xconn, inner })
} else {
Err(ImeCreationError::OpenFailure(Box::new(
inner.potential_input_methods,
)))
}
}
pub fn is_destroyed(&self) -> bool {
self.inner.is_destroyed
}
// This pattern is used for various methods here:
// Ok(_) indicates that nothing went wrong internally
// Ok(true) indicates that the action was actually performed
// Ok(false) indicates that the action is not presently applicable
pub fn create_context(
&mut self,
window: ffi::Window,
with_preedit: bool,
) -> Result<bool, ImeContextCreationError> {
let context = if self.is_destroyed() {
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
None
} else {
let im = self.inner.im.as_ref().unwrap();
let style = if with_preedit {
im.preedit_style
} else {
im.none_style
};
let context = unsafe {
ImeContext::new(
&self.inner.xconn,
im.im,
style,
window,
None,
self.inner.event_sender.clone(),
)?
};
// Check the state on the context, since it could fail to enable or disable preedit.
let event = if matches!(style, Style::None(_)) {
if with_preedit {
debug!("failed to create IME context with preedit support.")
}
ImeEvent::Disabled
} else {
if !with_preedit {
debug!("failed to create IME context without preedit support.")
}
ImeEvent::Enabled
};
self.inner
.event_sender
.send((window, event))
.expect("Failed to send enabled event");
Some(context)
};
self.inner.contexts.insert(window, context);
Ok(!self.is_destroyed())
}
pub fn get_context(&self, window: ffi::Window) -> Option<ffi::XIC> {
if self.is_destroyed() {
return None;
}
if let Some(Some(context)) = self.inner.contexts.get(&window) {
Some(context.ic)
} else {
None
}
}
pub fn remove_context(&mut self, window: ffi::Window) -> Result<bool, XError> {
if let Some(Some(context)) = self.inner.contexts.remove(&window) {
unsafe {
self.inner.destroy_ic_if_necessary(context.ic)?;
}
Ok(true)
} else {
Ok(false)
}
}
pub fn focus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.focus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn unfocus(&mut self, window: ffi::Window) -> Result<bool, XError> {
if self.is_destroyed() {
return Ok(false);
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.unfocus(&self.xconn).map(|_| true)
} else {
Ok(false)
}
}
pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
context.set_spot(&self.xconn, x as _, y as _);
}
}
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
if allowed == context.is_allowed() {
return;
}
}
// Remove context for that window.
let _ = self.remove_context(window);
// Create new context supporting IME input.
let _ = self.create_context(window, allowed);
}
}
impl Drop for Ime {
fn drop(&mut self) {
unsafe {
let _ = self.inner.destroy_all_contexts_if_necessary();
let _ = self.inner.close_im_if_necessary();
}
}
}

View File

@@ -28,13 +28,11 @@ use std::{
collections::{HashMap, HashSet},
ffi::CStr,
fmt,
mem::MaybeUninit,
ops::Deref,
os::{
raw::*,
unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd},
},
ptr,
rc::Rc,
slice, str,
sync::mpsc::{Receiver, Sender, TryRecvError},
@@ -42,19 +40,14 @@ use std::{
time::{Duration, Instant},
};
use libc::{self, setlocale, LC_CTYPE};
use atoms::*;
use x11rb::x11_utils::X11Error as LogicalError;
use x11rb::{
connection::RequestConnection,
protocol::{
xinput::{self, ConnectionExt as _},
xkb,
xproto::{self, ConnectionExt as _},
},
use x11rb::protocol::{
xinput::{self, ConnectionExt as _},
xkb,
xproto::{self, ConnectionExt as _},
};
use x11rb::x11_utils::X11Error as LogicalError;
use x11rb::{
errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError},
xcb_ffi::ReplyOrIdError,
@@ -63,7 +56,6 @@ use x11rb::{
use self::{
dnd::{Dnd, DndState},
event_processor::EventProcessor,
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
};
use super::{common::xkb_state::KbdState, ControlFlow, OsError};
use crate::{
@@ -147,15 +139,18 @@ pub struct EventLoopWindowTarget<T> {
xconn: Arc<XConnection>,
wm_delete_window: xproto::Atom,
net_wm_ping: xproto::Atom,
ime_sender: ImeSender,
ime_sender: mpsc::Sender<ime::ImeRequest>,
control_flow: Cell<ControlFlow>,
exit: Cell<Option<i32>>,
root: xproto::Window,
ime: RefCell<Ime>,
windows: RefCell<HashMap<WindowId, Weak<UnownedWindow>>>,
redraw_sender: WakeSender<WindowId>,
activation_sender: WakeSender<ActivationToken>,
device_events: Cell<DeviceEvents>,
/// State of IME.
ime: Option<RefCell<ime::ImeData>>,
_marker: ::std::marker::PhantomData<T>,
}
@@ -201,57 +196,26 @@ impl<T: 'static> EventLoop<T> {
let wm_delete_window = atoms[WM_DELETE_WINDOW];
let net_wm_ping = atoms[_NET_WM_PING];
// Create an event queue.
let event_queue = Rc::new(RefCell::new(std::collections::VecDeque::with_capacity(4)));
let dnd = Dnd::new(Arc::clone(&xconn))
.expect("Failed to call XInternAtoms when initializing drag and drop");
let (ime_sender, ime_receiver) = mpsc::channel();
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
// Input methods will open successfully without setting the locale, but it won't be
// possible to actually commit pre-edit sequences.
unsafe {
// Remember default locale to restore it if target locale is unsupported
// by Xlib
let default_locale = setlocale(LC_CTYPE, ptr::null());
setlocale(LC_CTYPE, b"\0".as_ptr() as *const _);
// Check if set locale is supported by Xlib.
// If not, calls to some Xlib functions like `XSetLocaleModifiers`
// will fail.
let locale_supported = (xconn.xlib.XSupportsLocale)() == 1;
if !locale_supported {
let unsupported_locale = setlocale(LC_CTYPE, ptr::null());
warn!(
"Unsupported locale \"{}\". Restoring default locale \"{}\".",
CStr::from_ptr(unsupported_locale).to_string_lossy(),
CStr::from_ptr(default_locale).to_string_lossy()
);
// Restore default locale
setlocale(LC_CTYPE, default_locale);
let ime = match ime::ImeData::new(&xconn, xconn.default_screen_index(), &event_queue) {
Ok(ime) => Some(ime),
Err(e) => {
log::error!("Failed to open IME: {e}");
None
}
}
let ime = RefCell::new({
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
panic!("Failed to open input method: {state:#?}");
}
result.expect("Failed to set input method destruction callback")
});
};
let randr_event_offset = xconn
xconn
.select_xrandr_input(root)
.expect("Failed to query XRandR extension");
let xi2ext = xconn
.xcb_connection()
.extension_information(xinput::X11_EXTENSION_NAME)
.expect("Failed to query XInput extension")
.expect("X server missing XInput extension");
let xkbext = xconn
.xcb_connection()
.extension_information(xkb::X11_EXTENSION_NAME)
.expect("Failed to query XKB extension")
.expect("X server missing XKB extension");
// Check for XInput2 support.
xconn
.xcb_connection()
@@ -303,12 +267,12 @@ impl<T: 'static> EventLoop<T> {
KbdState::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap();
let window_target = EventLoopWindowTarget {
ime,
root,
control_flow: Cell::new(ControlFlow::default()),
exit: Cell::new(None),
windows: Default::default(),
_marker: ::std::marker::PhantomData,
ime: ime.map(RefCell::new),
ime_sender,
xconn,
wm_delete_window,
@@ -333,14 +297,11 @@ impl<T: 'static> EventLoop<T> {
});
let event_processor = EventProcessor {
event_queue,
target: target.clone(),
dnd,
devices: Default::default(),
randr_event_offset,
ime_receiver,
ime_event_receiver,
xi2ext,
xkbext,
ime_requests: ime_receiver,
kb_state,
num_touch: 0,
held_key_press: None,
@@ -623,12 +584,10 @@ impl<T: 'static> EventLoop<T> {
F: FnMut(Event<T>, &RootELW<T>),
{
let target = &self.target;
let mut xev = MaybeUninit::uninit();
let wt = get_xtarget(&self.target);
while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } {
let mut xev = unsafe { xev.assume_init() };
self.event_processor.process_event(&mut xev, |event| {
while let Some(event) = self.event_processor.poll_one_event() {
self.event_processor.process_event(event, |event| {
if let Event::WindowEvent {
window_id: crate::window::WindowId(wid),
event: WindowEvent::RedrawRequested,
@@ -880,8 +839,11 @@ pub enum X11Error {
/// The XID range has been exhausted.
XidsExhausted(IdsExhausted),
/// Got `null` from an Xlib function without a reason.
UnexpectedNull(&'static str),
/// An IME client error occurred.
Ime(xim::ClientError),
/// The IME client has entered an invalid state.
InvalidImeState(ime::InvalidImeState),
/// Got an invalid activation token.
InvalidActivationToken(Vec<u8>),
@@ -900,8 +862,9 @@ impl fmt::Display for X11Error {
X11Error::Connect(e) => write!(f, "X11 connection error: {}", e),
X11Error::Connection(e) => write!(f, "X11 connection error: {}", e),
X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e),
X11Error::Ime(e) => write!(f, "An IME error occurred: {}", e),
X11Error::InvalidImeState(e) => write!(f, "Invalid IME state: {}", e),
X11Error::X11(e) => write!(f, "X11 error: {:?}", e),
X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s),
X11Error::InvalidActivationToken(s) => write!(
f,
"Invalid activation token: {}",
@@ -964,15 +927,6 @@ impl From<ReplyError> for X11Error {
}
}
impl From<ime::ImeContextCreationError> for X11Error {
fn from(value: ime::ImeContextCreationError) -> Self {
match value {
ime::ImeContextCreationError::XError(e) => e.into(),
ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"),
}
}
}
impl From<ReplyOrIdError> for X11Error {
fn from(value: ReplyOrIdError) -> Self {
match value {
@@ -983,6 +937,18 @@ impl From<ReplyOrIdError> for X11Error {
}
}
impl From<xim::ClientError> for X11Error {
fn from(e: xim::ClientError) -> Self {
match e {
xim::ClientError::Other(other) => match other.downcast::<X11Error>() {
Ok(x11) => *x11,
Err(other) => X11Error::Ime(xim::ClientError::Other(other)),
},
e => X11Error::Ime(e),
}
}
}
/// The underlying x11rb connection that we are using.
type X11rbConnection = x11rb::xcb_ffi::XCBConnection;
@@ -1001,34 +967,6 @@ impl<'a, E: fmt::Debug> CookieResultExt for Result<VoidCookie<'a>, E> {
}
}
/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. This is a wrapper to
/// extract the cookie from a GenericEvent XEvent and release the cookie data once it has been processed
struct GenericEventCookie<'a> {
xconn: &'a XConnection,
cookie: ffi::XGenericEventCookie,
}
impl<'a> GenericEventCookie<'a> {
fn from_event(xconn: &XConnection, event: ffi::XEvent) -> Option<GenericEventCookie<'_>> {
unsafe {
let mut cookie: ffi::XGenericEventCookie = From::from(event);
if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == ffi::True {
Some(GenericEventCookie { xconn, cookie })
} else {
None
}
}
}
}
impl<'a> Drop for GenericEventCookie<'a> {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie);
}
}
}
fn mkwid(w: xproto::Window) -> crate::window::WindowId {
crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _))
}
@@ -1130,8 +1068,15 @@ impl Device {
}
}
/// Convert the raw X11 representation for a 32-bit floating point to a double.
/// Convert the raw X11 representation for a 32-bit fixed point to a double.
#[inline]
fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 {
(fp as f64) / ((1 << 16) as f64)
}
/// Conver the raw X11 representation for a 64-bit fixed point number to a double.
#[inline]
fn xinput_fp3232_to_float(fp: xinput::Fp3232) -> f64 {
let xinput::Fp3232 { integral, frac } = fp;
integral as f64 + (frac as f64 / (1u64 << 32) as f64)
}

View File

@@ -19,7 +19,7 @@ impl XConnection {
.expect("Failed to set cursor");
}
pub(crate) fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) {
pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) {
self.update_cursor(window, cursor.inner.cursor)
.expect("Failed to set cursor");
}

View File

@@ -1,4 +1,3 @@
use std::{slice, str};
use x11rb::protocol::{
xinput::{self, ConnectionExt as _},
xkb,
@@ -9,11 +8,6 @@ use super::*;
pub const VIRTUAL_CORE_POINTER: u16 = 2;
pub const VIRTUAL_CORE_KEYBOARD: u16 = 3;
// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to
// re-allocate (and make another round-trip) in the *vast* majority of cases.
// To test if `lookup_utf8` works correctly, set this to 1.
const TEXT_BUFFER_SIZE: usize = 1024;
impl XConnection {
pub fn select_xinput_events(
&self,
@@ -60,52 +54,4 @@ impl XConnection {
.reply()
.map_err(Into::into)
}
fn lookup_utf8_inner(
&self,
ic: ffi::XIC,
key_event: &mut ffi::XKeyEvent,
buffer: *mut u8,
size: usize,
) -> (ffi::KeySym, ffi::Status, c_int) {
let mut keysym: ffi::KeySym = 0;
let mut status: ffi::Status = 0;
let count = unsafe {
(self.xlib.Xutf8LookupString)(
ic,
key_event,
buffer as *mut c_char,
size as c_int,
&mut keysym,
&mut status,
)
};
(keysym, status, count)
}
pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String {
// `assume_init` is safe here because the array consists of `MaybeUninit` values,
// which do not require initialization.
let mut buffer: [MaybeUninit<u8>; TEXT_BUFFER_SIZE] =
unsafe { MaybeUninit::uninit().assume_init() };
// If the buffer overflows, we'll make a new one on the heap.
let mut vec;
let (_, status, count) =
self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len());
let bytes = if status == ffi::XBufferOverflow {
vec = Vec::with_capacity(count as usize);
let (_, _, new_count) =
self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity());
debug_assert_eq!(count, new_count);
unsafe { vec.set_len(count as usize) };
&vec[..count as usize]
} else {
unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) }
};
str::from_utf8(bytes).unwrap_or("").to_string()
}
}

View File

@@ -7,18 +7,6 @@ pub(crate) struct XSmartPointer<'a, T> {
pub ptr: *mut T,
}
impl<'a, T> XSmartPointer<'a, T> {
// You're responsible for only passing things to this that should be XFree'd.
// Returns None if ptr is null.
pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option<Self> {
if !ptr.is_null() {
Some(XSmartPointer { xconn, ptr })
} else {
None
}
}
}
impl<'a, T> Deref for XSmartPointer<'a, T> {
type Target = T;

View File

@@ -17,7 +17,6 @@ pub use self::{cursor::*, geometry::*, hint::*, input::*, window_property::*, wm
use std::{
mem::{self, MaybeUninit},
ops::BitAnd,
os::raw::*,
};
@@ -34,13 +33,6 @@ pub fn maybe_change<T: PartialEq>(field: &mut Option<T>, value: T) -> bool {
}
}
pub fn has_flag<T>(bitset: T, flag: T) -> bool
where
T: Copy + PartialEq + BitAnd<T, Output = T>,
{
bitset & flag == flag
}
impl XConnection {
// This is impoartant, so pay attention!
// Xlib has an output buffer, and tries to hide the async nature of X from you.

View File

@@ -4,9 +4,11 @@ use std::{
mem::replace,
os::raw::*,
path::Path,
sync::{Arc, Mutex, MutexGuard},
sync::{mpsc, Arc, Mutex, MutexGuard},
};
use crate::cursor::CustomCursor as RootCustomCursor;
use cursor_icon::CursorIcon;
use x11rb::{
connection::Connection,
@@ -30,9 +32,8 @@ use crate::{
atoms::*, xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender,
X11Error,
},
OwnedWindowHandle as PlatformOwnedWindowHandle,
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor,
PlatformIcon, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon,
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes,
@@ -42,9 +43,9 @@ use crate::{
use super::{
ffi,
ime::ImeRequest,
util::{self, CustomCursor, SelectedCursor},
CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId,
XConnection,
CookieResultExt, EventLoopWindowTarget, VoidCookie, WindowId, XConnection,
};
#[derive(Debug)]
@@ -134,7 +135,7 @@ pub(crate) struct UnownedWindow {
cursor_grabbed_mode: Mutex<CursorGrabMode>,
#[allow(clippy::mutex_atomic)]
cursor_visible: Mutex<bool>,
ime_sender: Mutex<ImeSender>,
ime_sender: Mutex<mpsc::Sender<ImeRequest>>,
pub shared_state: Mutex<SharedState>,
redraw_sender: WakeSender<WindowId>,
activation_sender: WakeSender<super::ActivationToken>,
@@ -158,12 +159,15 @@ impl UnownedWindow {
) -> Result<UnownedWindow, RootOsError> {
let xconn = &event_loop.xconn;
let atoms = xconn.atoms();
let root = match window_attrs.parent_window {
Some(PlatformOwnedWindowHandle::X(handle)) => handle,
#[cfg(wayland_platform)]
Some(handle) => panic!("invalid window handle {handle:?} on X11"),
#[cfg(feature = "rwh_06")]
let root = match window_attrs.parent_window.0 {
Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window,
Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(),
Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"),
None => event_loop.root,
};
#[cfg(not(feature = "rwh_06"))]
let root = event_loop.root;
let mut monitors = leap!(xconn.available_monitors());
let guessed_monitor = if monitors.is_empty() {
@@ -543,11 +547,10 @@ impl UnownedWindow {
.ignore_error();
{
let result = event_loop
.ime
.borrow_mut()
.create_context(window.xwindow as ffi::Window, false);
leap!(result);
if let Some(ime) = event_loop.ime.as_ref() {
let result = ime.borrow_mut().create_context(window.xwindow, false, None);
leap!(result);
}
}
// These properties must be set after mapping
@@ -1548,8 +1551,8 @@ impl UnownedWindow {
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
let new_cursor = unsafe { CustomCursor::new(&self.xconn, &cursor.0) };
pub fn set_custom_cursor(&self, cursor: RootCustomCursor) {
let new_cursor = unsafe { CustomCursor::new(&self.xconn, &cursor.inner) };
#[allow(clippy::mutex_atomic)]
if *self.cursor_visible.lock().unwrap() {
@@ -1788,11 +1791,11 @@ impl UnownedWindow {
#[inline]
pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) {
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position(
self.xwindow as ffi::Window,
x,
y,
));
let _ = self
.ime_sender
.lock()
.unwrap()
.send(ImeRequest::Position(self.xwindow, x, y));
}
#[inline]
@@ -1801,7 +1804,7 @@ impl UnownedWindow {
.ime_sender
.lock()
.unwrap()
.send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed));
.send(ImeRequest::Allow(self.xwindow, allowed));
}
#[inline]

View File

@@ -91,6 +91,14 @@ impl XConnection {
conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))?
};
// Make sure Xlib knows XCB is handling events.
unsafe {
(xlib_xcb.XSetEventQueueOwner)(
display,
x11_dl::xlib_xcb::XEventQueueOwner::XCBOwnsEventQueue,
);
}
// Get the default screen.
let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize;

View File

@@ -1,30 +1,23 @@
#![allow(clippy::unnecessary_cast)]
use icrate::AppKit::{
NSApplication, NSEvent, NSEventModifierFlagCommand, NSEventTypeKeyUp, NSEventTypeLeftMouseDown,
NSEventTypeLeftMouseDragged, NSEventTypeLeftMouseUp, NSEventTypeMouseMoved,
NSEventTypeOtherMouseDown, NSEventTypeOtherMouseDragged, NSEventTypeOtherMouseUp,
NSEventTypeRightMouseDown, NSEventTypeRightMouseDragged, NSEventTypeRightMouseUp, NSResponder,
};
use icrate::Foundation::NSObject;
use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass};
use objc2::{declare_class, msg_send, mutability, ClassType};
use super::event::flags_contains;
use super::appkit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder};
use super::{app_state::AppState, DEVICE_ID};
use crate::event::{DeviceEvent, ElementState, Event};
declare_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(super) struct WinitApplication;
unsafe impl ClassType for WinitApplication {
#[inherits(NSResponder, NSObject)]
type Super = NSApplication;
type Mutability = mutability::MainThreadOnly;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitApplication";
}
impl DeclaredClass for WinitApplication {}
unsafe impl WinitApplication {
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196)
@@ -34,13 +27,13 @@ declare_class!(
// For posterity, there are some undocumented event types
// (https://github.com/servo/cocoa-rs/issues/155)
// but that doesn't really matter here.
let event_type = unsafe { event.r#type() };
let modifier_flags = unsafe { event.modifierFlags() };
if event_type == NSEventTypeKeyUp
&& flags_contains(modifier_flags, NSEventModifierFlagCommand)
let event_type = event.type_();
let modifier_flags = event.modifierFlags();
if event_type == NSEventType::NSKeyUp
&& modifier_flags.contains(NSEventModifierFlags::NSCommandKeyMask)
{
if let Some(key_window) = self.keyWindow() {
key_window.sendEvent(event);
unsafe { key_window.sendEvent(event) };
}
} else {
maybe_dispatch_device_event(event);
@@ -51,15 +44,14 @@ declare_class!(
);
fn maybe_dispatch_device_event(event: &NSEvent) {
let event_type = unsafe { event.r#type() };
#[allow(non_upper_case_globals)]
let event_type = event.type_();
match event_type {
NSEventTypeMouseMoved
| NSEventTypeLeftMouseDragged
| NSEventTypeOtherMouseDragged
| NSEventTypeRightMouseDragged => {
let delta_x = unsafe { event.deltaX() } as f64;
let delta_y = unsafe { event.deltaY() } as f64;
NSEventType::NSMouseMoved
| NSEventType::NSLeftMouseDragged
| NSEventType::NSOtherMouseDragged
| NSEventType::NSRightMouseDragged => {
let delta_x = event.deltaX() as f64;
let delta_y = event.deltaY() as f64;
if delta_x != 0.0 {
queue_device_event(DeviceEvent::Motion {
@@ -81,15 +73,17 @@ fn maybe_dispatch_device_event(event: &NSEvent) {
});
}
}
NSEventTypeLeftMouseDown | NSEventTypeRightMouseDown | NSEventTypeOtherMouseDown => {
NSEventType::NSLeftMouseDown
| NSEventType::NSRightMouseDown
| NSEventType::NSOtherMouseDown => {
queue_device_event(DeviceEvent::Button {
button: unsafe { event.buttonNumber() } as u32,
button: event.buttonNumber() as u32,
state: ElementState::Pressed,
});
}
NSEventTypeLeftMouseUp | NSEventTypeRightMouseUp | NSEventTypeOtherMouseUp => {
NSEventType::NSLeftMouseUp | NSEventType::NSRightMouseUp | NSEventType::NSOtherMouseUp => {
queue_device_event(DeviceEvent::Button {
button: unsafe { event.buttonNumber() } as u32,
button: event.buttonNumber() as u32,
state: ElementState::Released,
});
}

View File

@@ -1,41 +1,54 @@
use icrate::AppKit::{NSApplicationActivationPolicy, NSApplicationDelegate};
use icrate::Foundation::{MainThreadMarker, NSObject, NSObjectProtocol};
use std::ptr::NonNull;
use icrate::Foundation::NSObject;
use objc2::declare::{IvarBool, IvarEncode};
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2::{declare_class, msg_send, msg_send_id, mutability, ClassType};
use super::app_state::AppState;
#[derive(Debug)]
pub(super) struct State {
activation_policy: NSApplicationActivationPolicy,
default_menu: bool,
activate_ignoring_other_apps: bool,
}
use super::appkit::NSApplicationActivationPolicy;
declare_class!(
pub(super) struct ApplicationDelegate;
#[derive(Debug)]
pub(super) struct ApplicationDelegate {
activation_policy: IvarEncode<NSApplicationActivationPolicy, "_activation_policy">,
default_menu: IvarBool<"_default_menu">,
activate_ignoring_other_apps: IvarBool<"_activate_ignoring_other_apps">,
}
mod ivars;
unsafe impl ClassType for ApplicationDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitApplicationDelegate";
}
impl DeclaredClass for ApplicationDelegate {
type Ivars = State;
}
unsafe impl ApplicationDelegate {
#[method(initWithActivationPolicy:defaultMenu:activateIgnoringOtherApps:)]
unsafe fn init(
this: *mut Self,
activation_policy: NSApplicationActivationPolicy,
default_menu: bool,
activate_ignoring_other_apps: bool,
) -> Option<NonNull<Self>> {
let this: Option<&mut Self> = unsafe { msg_send![super(this), init] };
this.map(|this| {
*this.activation_policy = activation_policy;
*this.default_menu = default_menu;
*this.activate_ignoring_other_apps = activate_ignoring_other_apps;
NonNull::from(this)
})
}
unsafe impl NSObjectProtocol for ApplicationDelegate {}
unsafe impl NSApplicationDelegate for ApplicationDelegate {
#[method(applicationDidFinishLaunching:)]
fn did_finish_launching(&self, _sender: Option<&AnyObject>) {
trace_scope!("applicationDidFinishLaunching:");
AppState::launched(
self.ivars().activation_policy,
self.ivars().default_menu,
self.ivars().activate_ignoring_other_apps,
*self.activation_policy,
*self.default_menu,
*self.activate_ignoring_other_apps,
);
}
@@ -50,16 +63,17 @@ declare_class!(
impl ApplicationDelegate {
pub(super) fn new(
mtm: MainThreadMarker,
activation_policy: NSApplicationActivationPolicy,
default_menu: bool,
activate_ignoring_other_apps: bool,
) -> Id<Self> {
let this = mtm.alloc().set_ivars(State {
activation_policy,
default_menu,
activate_ignoring_other_apps,
});
unsafe { msg_send_id![super(this), init] }
unsafe {
msg_send_id![
Self::alloc(),
initWithActivationPolicy: activation_policy,
defaultMenu: default_menu,
activateIgnoringOtherApps: activate_ignoring_other_apps,
]
}
}
}

View File

@@ -12,14 +12,13 @@ use std::{
};
use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp};
use icrate::AppKit::{NSApplication, NSApplicationActivationPolicy};
use icrate::Foundation::{is_main_thread, MainThreadMarker, NSSize};
use icrate::Foundation::{is_main_thread, NSSize};
use objc2::rc::{autoreleasepool, Id};
use once_cell::sync::Lazy;
use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent};
use super::{
event::dummy_event, event_loop::PanicInfo, menu, observer::EventLoopWaker, util::Never,
window::WinitWindow,
event_loop::PanicInfo, menu, observer::EventLoopWaker, util::Never, window::WinitWindow,
};
use crate::{
dpi::PhysicalSize,
@@ -58,14 +57,14 @@ impl<T> EventLoopHandler<T> {
where
F: FnOnce(&mut EventLoopHandler<T>, RefMut<'_, dyn FnMut(Event<T>, &RootWindowTarget<T>)>),
{
// `NSApplication` and our `HANDLER` are global state and so it's possible
// that we could get a delegate callback after the application has exit an
// The `NSApp` and our `HANDLER` are global state and so it's possible that
// we could get a delegate callback after the application has exit an
// `EventLoop`. If the loop has been exit then our weak `self.callback`
// will fail to upgrade.
//
// We don't want to panic or output any verbose logging if we fail to
// upgrade the weak reference since it might be valid that the application
// re-starts the `NSApplication` after exiting a Winit `EventLoop`
// re-starts the `NSApp` after exiting a Winit `EventLoop`
if let Some(callback) = self.callback.upgrade() {
let callback = callback.borrow_mut();
(f)(self, callback);
@@ -145,9 +144,9 @@ impl Handler {
/// `true` after `ApplicationDelegate::applicationDidFinishLaunching` called
///
/// NB: This is global / `NSApplication` state and since the app will only
/// be launched once but an `EventLoop` may be run more than once then only
/// the first `EventLoop` will observe the application before it is launched.
/// NB: This is global / `NSApp` state and since the app will only be launched
/// once but an `EventLoop` may be run more than once then only the first
/// `EventLoop` will observe the `NSApp` before it is launched.
fn is_launched(&self) -> bool {
self.launched.load(Ordering::Acquire)
}
@@ -159,8 +158,8 @@ impl Handler {
/// `true` if an `EventLoop` is currently running
///
/// NB: This is global / `NSApplication` state and may persist beyond the
/// lifetime of a running `EventLoop`.
/// NB: This is global / `NSApp` state and may persist beyond the lifetime of
/// a running `EventLoop`.
///
/// # Caveat
/// This is only intended to be called from the main thread
@@ -168,7 +167,7 @@ impl Handler {
self.running.load(Ordering::Relaxed)
}
/// Set when an `EventLoop` starts running, after the `NSApplication` is launched
/// Set when an `EventLoop` starts running, after the `NSApp` is launched
///
/// # Caveat
/// This is only intended to be called from the main thread
@@ -181,8 +180,8 @@ impl Handler {
/// Since an `EventLoop` may be run more than once we need make sure to reset the
/// `control_flow` state back to `Poll` each time the loop exits.
///
/// Note: that if the `NSApplication` has been launched then that state is preserved,
/// and we won't need to re-launch the app if subsequent EventLoops are run.
/// Note: that if the `NSApp` has been launched then that state is preserved, and we won't
/// need to re-launch the app if subsequent EventLoops are run.
///
/// # Caveat
/// This is only intended to be called from the main thread
@@ -393,7 +392,7 @@ impl AppState {
}
// If `pump_events` is called to progress the event loop then we bootstrap the event
// loop via `-[NSAppplication run]` but will use `CFRunLoopRunInMode` for subsequent calls to
// loop via `[NSApp run]` but will use `CFRunLoopRunInMode` for subsequent calls to
// `pump_events`
pub fn request_stop_on_launch() {
HANDLER.request_stop_app_on_launch();
@@ -460,15 +459,13 @@ impl AppState {
create_default_menu: bool,
activate_ignoring_other_apps: bool,
) {
let mtm = MainThreadMarker::new().unwrap();
let app = NSApplication::sharedApplication(mtm);
let app = NSApp();
// We need to delay setting the activation policy and activating the app
// until `applicationDidFinishLaunching` has been called. Otherwise the
// menu bar is initially unresponsive on macOS 10.15.
app.setActivationPolicy(activation_policy);
window_activation_hack(&app);
#[allow(deprecated)]
app.activateIgnoringOtherApps(activate_ignoring_other_apps);
HANDLER.set_launched();
@@ -476,22 +473,21 @@ impl AppState {
if create_default_menu {
// The menubar initialization should be before the `NewEvents` event, to allow
// overriding of the default menu even if it's created
menu::initialize(&app);
menu::initialize();
}
Self::start_running();
// If the application is being launched via `EventLoop::pump_events()` then we'll
// If the `NSApp` is being launched via `EventLoop::pump_events()` then we'll
// want to stop the app once it is launched (and return to the external loop)
//
// In this case we still want to consider Winit's `EventLoop` to be "running",
// so we call `start_running()` above.
if HANDLER.should_stop_app_on_launch() {
// Note: the original idea had been to only stop the underlying `RunLoop`
// for the app but that didn't work as expected (`-[NSApplication run]`
// effectively ignored the attempt to stop the RunLoop and re-started it).
//
// So we return from `pump_events` by stopping the application.
// for the app but that didn't work as expected (`[NSApp run]` effectively
// ignored the attempt to stop the RunLoop and re-started it.). So we
// return from `pump_events` by stopping the `NSApp`
Self::stop();
}
}
@@ -593,12 +589,11 @@ impl AppState {
}
pub fn stop() {
let mtm = MainThreadMarker::new().unwrap();
let app = NSApplication::sharedApplication(mtm);
let app = NSApp();
autoreleasepool(|_| {
app.stop(None);
// To stop event loop immediately, we need to post some event here.
app.postEvent_atStart(&dummy_event().unwrap(), true);
app.postEvent_atStart(&NSEvent::dummy(), true);
});
}

View File

@@ -0,0 +1,28 @@
use icrate::Foundation::{NSArray, NSObject, NSString};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSAppearance;
unsafe impl ClassType for NSAppearance {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
type NSAppearanceName = NSString;
extern_methods!(
unsafe impl NSAppearance {
#[method_id(appearanceNamed:)]
pub fn appearanceNamed(name: &NSAppearanceName) -> Id<Self>;
#[method_id(bestMatchFromAppearancesWithNames:)]
pub fn bestMatchFromAppearancesWithNames(
&self,
appearances: &NSArray<NSAppearanceName>,
) -> Id<NSAppearanceName>;
}
);

View File

@@ -0,0 +1,143 @@
use icrate::Foundation::{MainThreadMarker, NSArray, NSInteger, NSObject, NSUInteger};
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use objc2::{Encode, Encoding};
use super::{NSAppearance, NSEvent, NSMenu, NSResponder, NSWindow};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSApplication;
unsafe impl ClassType for NSApplication {
#[inherits(NSObject)]
type Super = NSResponder;
type Mutability = mutability::InteriorMutable;
}
);
pub(crate) fn NSApp() -> Id<NSApplication> {
// TODO: Only allow access from main thread
NSApplication::shared(unsafe { MainThreadMarker::new_unchecked() })
}
extern_methods!(
unsafe impl NSApplication {
/// This can only be called on the main thread since it may initialize
/// the application and since it's parameters may be changed by the main
/// thread at any time (hence it is only safe to access on the main thread).
pub fn shared(_mtm: MainThreadMarker) -> Id<Self> {
let app: Option<_> = unsafe { msg_send_id![Self::class(), sharedApplication] };
// SAFETY: `sharedApplication` always initializes the app if it isn't already
unsafe { app.unwrap_unchecked() }
}
#[method_id(currentEvent)]
pub fn currentEvent(&self) -> Option<Id<NSEvent>>;
#[method(postEvent:atStart:)]
pub fn postEvent_atStart(&self, event: &NSEvent, front_of_queue: bool);
#[method(presentationOptions)]
pub fn presentationOptions(&self) -> NSApplicationPresentationOptions;
#[method_id(windows)]
pub fn windows(&self) -> Id<NSArray<NSWindow>>;
#[method_id(keyWindow)]
pub fn keyWindow(&self) -> Option<Id<NSWindow>>;
// TODO: NSApplicationDelegate
#[method(setDelegate:)]
pub fn setDelegate(&self, delegate: &AnyObject);
#[method(setPresentationOptions:)]
pub fn setPresentationOptions(&self, options: NSApplicationPresentationOptions);
#[method(hide:)]
pub fn hide(&self, sender: Option<&AnyObject>);
#[method(orderFrontCharacterPalette:)]
#[allow(dead_code)]
pub fn orderFrontCharacterPalette(&self, sender: Option<&AnyObject>);
#[method(hideOtherApplications:)]
pub fn hideOtherApplications(&self, sender: Option<&AnyObject>);
#[method(stop:)]
pub fn stop(&self, sender: Option<&AnyObject>);
#[method(activateIgnoringOtherApps:)]
pub fn activateIgnoringOtherApps(&self, ignore: bool);
#[method(requestUserAttention:)]
pub fn requestUserAttention(&self, type_: NSRequestUserAttentionType) -> NSInteger;
#[method(setActivationPolicy:)]
pub fn setActivationPolicy(&self, policy: NSApplicationActivationPolicy) -> bool;
#[method(setMainMenu:)]
pub fn setMainMenu(&self, menu: &NSMenu);
#[method(setServicesMenu:)]
pub fn setServicesMenu(&self, menu: &NSMenu);
#[method_id(effectiveAppearance)]
pub fn effectiveAppearance(&self) -> Id<NSAppearance>;
#[method(setAppearance:)]
pub fn setAppearance(&self, appearance: Option<&NSAppearance>);
#[method(run)]
pub unsafe fn run(&self);
}
);
#[allow(dead_code)]
#[repr(isize)] // NSInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSApplicationActivationPolicy {
NSApplicationActivationPolicyRegular = 0,
NSApplicationActivationPolicyAccessory = 1,
NSApplicationActivationPolicyProhibited = 2,
NSApplicationActivationPolicyERROR = -1,
}
unsafe impl Encode for NSApplicationActivationPolicy {
const ENCODING: Encoding = NSInteger::ENCODING;
}
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct NSApplicationPresentationOptions: NSUInteger {
const NSApplicationPresentationDefault = 0;
const NSApplicationPresentationAutoHideDock = 1 << 0;
const NSApplicationPresentationHideDock = 1 << 1;
const NSApplicationPresentationAutoHideMenuBar = 1 << 2;
const NSApplicationPresentationHideMenuBar = 1 << 3;
const NSApplicationPresentationDisableAppleMenu = 1 << 4;
const NSApplicationPresentationDisableProcessSwitching = 1 << 5;
const NSApplicationPresentationDisableForceQuit = 1 << 6;
const NSApplicationPresentationDisableSessionTermination = 1 << 7;
const NSApplicationPresentationDisableHideApplication = 1 << 8;
const NSApplicationPresentationDisableMenuBarTransparency = 1 << 9;
const NSApplicationPresentationFullScreen = 1 << 10;
const NSApplicationPresentationAutoHideToolbar = 1 << 11;
}
}
unsafe impl Encode for NSApplicationPresentationOptions {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
#[repr(usize)] // NSUInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSRequestUserAttentionType {
NSCriticalRequest = 0,
NSInformationalRequest = 10,
}
unsafe impl Encode for NSRequestUserAttentionType {
const ENCODING: Encoding = NSUInteger::ENCODING;
}

View File

@@ -0,0 +1,56 @@
use std::ffi::c_uchar;
use icrate::Foundation::{NSInteger, NSObject, NSString};
use objc2::rc::Id;
use objc2::runtime::Bool;
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct NSImageRep;
unsafe impl ClassType for NSImageRep {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern "C" {
static NSDeviceRGBColorSpace: &'static NSString;
}
extern_class!(
// <https://developer.apple.com/documentation/appkit/nsbitmapimagerep?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSBitmapImageRep;
unsafe impl ClassType for NSBitmapImageRep {
type Super = NSImageRep;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSBitmapImageRep {
pub fn init_rgba(width: NSInteger, height: NSInteger) -> Id<Self> {
unsafe {
msg_send_id![Self::alloc(),
initWithBitmapDataPlanes: std::ptr::null_mut::<*mut c_uchar>(),
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8 as NSInteger,
samplesPerPixel: 4 as NSInteger,
hasAlpha: Bool::new(true),
isPlanar: Bool::new(false),
colorSpaceName: NSDeviceRGBColorSpace,
bytesPerRow: width * 4,
bitsPerPixel: 32 as NSInteger,
]
}
}
pub fn bitmap_data(&self) -> *mut u8 {
unsafe { msg_send![self, bitmapData] }
}
}
);

View File

@@ -0,0 +1,15 @@
use icrate::Foundation::NSObject;
use objc2::{extern_class, mutability, ClassType};
use super::{NSControl, NSResponder, NSView};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSButton;
unsafe impl ClassType for NSButton {
#[inherits(NSView, NSResponder, NSObject)]
type Super = NSControl;
type Mutability = mutability::InteriorMutable;
}
);

View File

@@ -0,0 +1,28 @@
use icrate::Foundation::NSObject;
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
extern_class!(
/// An object that stores color data and sometimes opacity (alpha value).
///
/// <https://developer.apple.com/documentation/appkit/nscolor?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSColor;
unsafe impl ClassType for NSColor {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// SAFETY: Documentation clearly states:
// > Color objects are immutable and thread-safe
unsafe impl Send for NSColor {}
unsafe impl Sync for NSColor {}
extern_methods!(
unsafe impl NSColor {
#[method_id(clearColor)]
pub fn clear() -> Id<Self>;
}
);

View File

@@ -0,0 +1,25 @@
use icrate::Foundation::NSObject;
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::{NSResponder, NSView};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSControl;
unsafe impl ClassType for NSControl {
#[inherits(NSResponder, NSObject)]
type Super = NSView;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSControl {
#[method(setEnabled:)]
pub fn setEnabled(&self, enabled: bool);
#[method(isEnabled)]
pub fn isEnabled(&self) -> bool;
}
);

View File

@@ -0,0 +1,259 @@
use once_cell::sync::Lazy;
use icrate::ns_string;
use icrate::Foundation::{
NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, NSString,
};
use objc2::rc::{DefaultId, Id};
use objc2::runtime::Sel;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType};
use super::{NSBitmapImageRep, NSImage};
use crate::cursor::CursorImage;
use crate::window::CursorIcon;
extern_class!(
/// <https://developer.apple.com/documentation/appkit/nscursor?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSCursor;
unsafe impl ClassType for NSCursor {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// SAFETY: NSCursor is immutable, stated here:
// https://developer.apple.com/documentation/appkit/nscursor/1527062-image?language=objc
unsafe impl Send for NSCursor {}
unsafe impl Sync for NSCursor {}
macro_rules! def_cursor {
{$(
$(#[$($m:meta)*])*
pub fn $name:ident();
)*} => {$(
$(#[$($m)*])*
pub fn $name() -> Id<Self> {
unsafe { msg_send_id![Self::class(), $name] }
}
)*};
}
macro_rules! def_undocumented_cursor {
{$(
$(#[$($m:meta)*])*
pub fn $name:ident();
)*} => {$(
$(#[$($m)*])*
pub fn $name() -> Id<Self> {
unsafe { Self::from_selector(sel!($name)).unwrap_or_else(|| Default::default()) }
}
)*};
}
extern_methods!(
/// Documented cursors
unsafe impl NSCursor {
def_cursor!(
pub fn arrowCursor();
pub fn pointingHandCursor();
pub fn openHandCursor();
pub fn closedHandCursor();
pub fn IBeamCursor();
pub fn IBeamCursorForVerticalLayout();
pub fn dragCopyCursor();
pub fn dragLinkCursor();
pub fn operationNotAllowedCursor();
pub fn contextualMenuCursor();
pub fn crosshairCursor();
pub fn resizeRightCursor();
pub fn resizeUpCursor();
pub fn resizeLeftCursor();
pub fn resizeDownCursor();
pub fn resizeLeftRightCursor();
pub fn resizeUpDownCursor();
);
// Creating cursors should be thread-safe, though using them for anything probably isn't.
pub fn new(image: &NSImage, hotSpot: NSPoint) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithImage: image, hotSpot: hotSpot] }
}
pub fn invisible() -> Id<Self> {
// 16x16 GIF data for invisible cursor
// You can reproduce this via ImageMagick.
// $ convert -size 16x16 xc:none cursor.gif
static CURSOR_BYTES: &[u8] = &[
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x84, 0x8F, 0xA9,
0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B,
];
static CURSOR: Lazy<Id<NSCursor>> = Lazy::new(|| {
// TODO: Consider using `dataWithBytesNoCopy:`
let data = NSData::with_bytes(CURSOR_BYTES);
let image = NSImage::new_with_data(&data);
NSCursor::new(&image, NSPoint::new(0.0, 0.0))
});
CURSOR.clone()
}
}
/// Undocumented cursors
unsafe impl NSCursor {
#[method(respondsToSelector:)]
fn class_responds_to(sel: Sel) -> bool;
#[method_id(performSelector:)]
unsafe fn from_selector_unchecked(sel: Sel) -> Id<Self>;
unsafe fn from_selector(sel: Sel) -> Option<Id<Self>> {
if Self::class_responds_to(sel) {
Some(unsafe { Self::from_selector_unchecked(sel) })
} else {
warn!("Cursor `{:?}` appears to be invalid", sel);
None
}
}
def_undocumented_cursor!(
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
pub fn _helpCursor();
pub fn _zoomInCursor();
pub fn _zoomOutCursor();
pub fn _windowResizeNorthEastCursor();
pub fn _windowResizeNorthWestCursor();
pub fn _windowResizeSouthEastCursor();
pub fn _windowResizeSouthWestCursor();
pub fn _windowResizeNorthEastSouthWestCursor();
pub fn _windowResizeNorthWestSouthEastCursor();
// While these two are available, the former just loads a white arrow,
// and the latter loads an ugly deflated beachball!
// pub fn _moveCursor();
// pub fn _waitCursor();
// An even more undocumented cursor...
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
pub fn busyButClickableCursor();
);
}
/// Webkit cursors
unsafe impl NSCursor {
// Note that loading `busybutclickable` with this code won't animate
// the frames; instead you'll just get them all in a column.
unsafe fn load_webkit_cursor(name: &NSString) -> Id<Self> {
// Snatch a cursor from WebKit; They fit the style of the native
// cursors, and will seem completely standard to macOS users.
//
// https://stackoverflow.com/a/21786835/5435443
let root = ns_string!("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors");
let cursor_path = root.stringByAppendingPathComponent(name);
let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf"));
let image = NSImage::new_by_referencing_file(&pdf_path);
// TODO: Handle PLists better
let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist"));
let info: Id<NSDictionary<NSObject, NSObject>> = unsafe {
msg_send_id![
<NSDictionary<NSObject, NSObject>>::class(),
dictionaryWithContentsOfFile: &*info_path,
]
};
let mut x = 0.0;
if let Some(n) = info.get(&*ns_string!("hotx")) {
if n.is_kind_of::<NSNumber>() {
let ptr: *const NSObject = n;
let ptr: *const NSNumber = ptr.cast();
x = unsafe { &*ptr }.as_cgfloat()
}
}
let mut y = 0.0;
if let Some(n) = info.get(&*ns_string!("hotx")) {
if n.is_kind_of::<NSNumber>() {
let ptr: *const NSObject = n;
let ptr: *const NSNumber = ptr.cast();
y = unsafe { &*ptr }.as_cgfloat()
}
}
let hotspot = NSPoint::new(x, y);
Self::new(&image, hotspot)
}
pub fn moveCursor() -> Id<Self> {
unsafe { Self::load_webkit_cursor(ns_string!("move")) }
}
pub fn cellCursor() -> Id<Self> {
unsafe { Self::load_webkit_cursor(ns_string!("cell")) }
}
}
);
impl NSCursor {
pub fn from_icon(icon: CursorIcon) -> Id<Self> {
match icon {
CursorIcon::Default => Default::default(),
CursorIcon::Pointer => Self::pointingHandCursor(),
CursorIcon::Grab => Self::openHandCursor(),
CursorIcon::Grabbing => Self::closedHandCursor(),
CursorIcon::Text => Self::IBeamCursor(),
CursorIcon::VerticalText => Self::IBeamCursorForVerticalLayout(),
CursorIcon::Copy => Self::dragCopyCursor(),
CursorIcon::Alias => Self::dragLinkCursor(),
CursorIcon::NotAllowed | CursorIcon::NoDrop => Self::operationNotAllowedCursor(),
CursorIcon::ContextMenu => Self::contextualMenuCursor(),
CursorIcon::Crosshair => Self::crosshairCursor(),
CursorIcon::EResize => Self::resizeRightCursor(),
CursorIcon::NResize => Self::resizeUpCursor(),
CursorIcon::WResize => Self::resizeLeftCursor(),
CursorIcon::SResize => Self::resizeDownCursor(),
CursorIcon::EwResize | CursorIcon::ColResize => Self::resizeLeftRightCursor(),
CursorIcon::NsResize | CursorIcon::RowResize => Self::resizeUpDownCursor(),
CursorIcon::Help => Self::_helpCursor(),
CursorIcon::ZoomIn => Self::_zoomInCursor(),
CursorIcon::ZoomOut => Self::_zoomOutCursor(),
CursorIcon::NeResize => Self::_windowResizeNorthEastCursor(),
CursorIcon::NwResize => Self::_windowResizeNorthWestCursor(),
CursorIcon::SeResize => Self::_windowResizeSouthEastCursor(),
CursorIcon::SwResize => Self::_windowResizeSouthWestCursor(),
CursorIcon::NeswResize => Self::_windowResizeNorthEastSouthWestCursor(),
CursorIcon::NwseResize => Self::_windowResizeNorthWestSouthEastCursor(),
// This is the wrong semantics for `Wait`, but it's the same as
// what's used in Safari and Chrome.
CursorIcon::Wait | CursorIcon::Progress => Self::busyButClickableCursor(),
CursorIcon::Move | CursorIcon::AllScroll => Self::moveCursor(),
CursorIcon::Cell => Self::cellCursor(),
_ => Default::default(),
}
}
pub fn from_image(cursor: &CursorImage) -> Id<Self> {
let width = cursor.width;
let height = cursor.height;
let bitmap = NSBitmapImageRep::init_rgba(width as isize, height as isize);
let bitmap_data =
unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), cursor.rgba.len()) };
bitmap_data.copy_from_slice(&cursor.rgba);
let image = NSImage::init_with_size(NSSize::new(width.into(), height.into()));
image.add_representation(&bitmap);
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
NSCursor::new(&image, hotspot)
}
}
impl DefaultId for NSCursor {
fn default_id() -> Id<Self> {
Self::arrowCursor()
}
}

View File

@@ -0,0 +1,308 @@
use std::os::raw::c_ushort;
use icrate::Foundation::{
CGFloat, NSCopying, NSInteger, NSObject, NSPoint, NSString, NSTimeInterval, NSUInteger,
};
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSEvent;
unsafe impl ClassType for NSEvent {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// > Safely handled only on the same thread, whether that be the main thread
// > or a secondary thread; otherwise you run the risk of having events get
// > out of sequence.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaFundamentals/AddingBehaviortoaCocoaProgram/AddingBehaviorCocoa.html#//apple_ref/doc/uid/TP40002974-CH5-SW47>
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123383>
extern_methods!(
unsafe impl NSEvent {
#[method_id(
otherEventWithType:
location:
modifierFlags:
timestamp:
windowNumber:
context:
subtype:
data1:
data2:
)]
unsafe fn otherEventWithType(
type_: NSEventType,
location: NSPoint,
flags: NSEventModifierFlags,
time: NSTimeInterval,
window_num: NSInteger,
context: Option<&NSObject>, // NSGraphicsContext
subtype: NSEventSubtype,
data1: NSInteger,
data2: NSInteger,
) -> Id<Self>;
pub fn dummy() -> Id<Self> {
unsafe {
Self::otherEventWithType(
NSEventType::NSApplicationDefined,
NSPoint::new(0.0, 0.0),
NSEventModifierFlags::empty(),
0.0,
0,
None,
NSEventSubtype::NSWindowExposedEventType,
0,
0,
)
}
}
#[method_id(
keyEventWithType:
location:
modifierFlags:
timestamp:
windowNumber:
context:
characters:
charactersIgnoringModifiers:
isARepeat:
keyCode:
)]
pub fn keyEventWithType(
type_: NSEventType,
location: NSPoint,
modifier_flags: NSEventModifierFlags,
timestamp: NSTimeInterval,
window_num: NSInteger,
context: Option<&NSObject>,
characters: &NSString,
characters_ignoring_modifiers: &NSString,
is_a_repeat: bool,
scancode: c_ushort,
) -> Id<Self>;
#[method(locationInWindow)]
pub fn locationInWindow(&self) -> NSPoint;
// TODO: MainThreadMarker
#[method(pressedMouseButtons)]
pub fn pressedMouseButtons() -> NSUInteger;
#[method(modifierFlags)]
pub fn modifierFlags(&self) -> NSEventModifierFlags;
#[method(type)]
pub fn type_(&self) -> NSEventType;
#[method(keyCode)]
pub fn key_code(&self) -> c_ushort;
#[method(magnification)]
pub fn magnification(&self) -> CGFloat;
#[method(phase)]
pub fn phase(&self) -> NSEventPhase;
#[method(momentumPhase)]
pub fn momentumPhase(&self) -> NSEventPhase;
#[method(deltaX)]
pub fn deltaX(&self) -> CGFloat;
#[method(deltaY)]
pub fn deltaY(&self) -> CGFloat;
#[method(buttonNumber)]
pub fn buttonNumber(&self) -> NSInteger;
#[method(scrollingDeltaX)]
pub fn scrollingDeltaX(&self) -> CGFloat;
#[method(scrollingDeltaY)]
pub fn scrollingDeltaY(&self) -> CGFloat;
#[method(hasPreciseScrollingDeltas)]
pub fn hasPreciseScrollingDeltas(&self) -> bool;
#[method(rotation)]
pub fn rotation(&self) -> f32;
#[method(pressure)]
pub fn pressure(&self) -> f32;
#[method(stage)]
pub fn stage(&self) -> NSInteger;
#[method(isARepeat)]
pub fn is_a_repeat(&self) -> bool;
#[method(windowNumber)]
pub fn window_number(&self) -> NSInteger;
#[method(timestamp)]
pub fn timestamp(&self) -> NSTimeInterval;
#[method_id(characters)]
pub fn characters(&self) -> Option<Id<NSString>>;
#[method_id(charactersIgnoringModifiers)]
pub fn charactersIgnoringModifiers(&self) -> Option<Id<NSString>>;
pub fn lshift_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICELSHIFTKEYMASK != 0
}
pub fn rshift_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICERSHIFTKEYMASK != 0
}
pub fn lctrl_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICELCTLKEYMASK != 0
}
pub fn rctrl_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICERCTLKEYMASK != 0
}
pub fn lalt_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICELALTKEYMASK != 0
}
pub fn ralt_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICERALTKEYMASK != 0
}
pub fn lcmd_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICELCMDKEYMASK != 0
}
pub fn rcmd_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICERCMDKEYMASK != 0
}
}
);
unsafe impl NSCopying for NSEvent {}
// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259
const NX_DEVICELCTLKEYMASK: u32 = 0x00000001;
const NX_DEVICELSHIFTKEYMASK: u32 = 0x00000002;
const NX_DEVICERSHIFTKEYMASK: u32 = 0x00000004;
const NX_DEVICELCMDKEYMASK: u32 = 0x00000008;
const NX_DEVICERCMDKEYMASK: u32 = 0x00000010;
const NX_DEVICELALTKEYMASK: u32 = 0x00000020;
const NX_DEVICERALTKEYMASK: u32 = 0x00000040;
const NX_DEVICERCTLKEYMASK: u32 = 0x00002000;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NSEventModifierFlags: NSUInteger {
const NSAlphaShiftKeyMask = 1 << 16;
const NSShiftKeyMask = 1 << 17;
const NSControlKeyMask = 1 << 18;
const NSAlternateKeyMask = 1 << 19;
const NSCommandKeyMask = 1 << 20;
const NSNumericPadKeyMask = 1 << 21;
const NSHelpKeyMask = 1 << 22;
const NSFunctionKeyMask = 1 << 23;
const NSDeviceIndependentModifierFlagsMask = 0xffff0000;
}
}
unsafe impl Encode for NSEventModifierFlags {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NSEventPhase: NSUInteger {
const NSEventPhaseNone = 0;
const NSEventPhaseBegan = 0x1 << 0;
const NSEventPhaseStationary = 0x1 << 1;
const NSEventPhaseChanged = 0x1 << 2;
const NSEventPhaseEnded = 0x1 << 3;
const NSEventPhaseCancelled = 0x1 << 4;
const NSEventPhaseMayBegin = 0x1 << 5;
}
}
unsafe impl Encode for NSEventPhase {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(i16)] // short
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSEventSubtype {
// TODO: Not sure what these values are
// NSMouseEventSubtype = NX_SUBTYPE_DEFAULT,
// NSTabletPointEventSubtype = NX_SUBTYPE_TABLET_POINT,
// NSTabletProximityEventSubtype = NX_SUBTYPE_TABLET_PROXIMITY
// NSTouchEventSubtype = NX_SUBTYPE_MOUSE_TOUCH,
NSWindowExposedEventType = 0,
NSApplicationActivatedEventType = 1,
NSApplicationDeactivatedEventType = 2,
NSWindowMovedEventType = 4,
NSScreenChangedEventType = 8,
NSAWTEventType = 16,
}
unsafe impl Encode for NSEventSubtype {
const ENCODING: Encoding = i16::ENCODING;
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(usize)] // NSUInteger
pub enum NSEventType {
NSLeftMouseDown = 1,
NSLeftMouseUp = 2,
NSRightMouseDown = 3,
NSRightMouseUp = 4,
NSMouseMoved = 5,
NSLeftMouseDragged = 6,
NSRightMouseDragged = 7,
NSMouseEntered = 8,
NSMouseExited = 9,
NSKeyDown = 10,
NSKeyUp = 11,
NSFlagsChanged = 12,
NSAppKitDefined = 13,
NSSystemDefined = 14,
NSApplicationDefined = 15,
NSPeriodic = 16,
NSCursorUpdate = 17,
NSScrollWheel = 22,
NSTabletPoint = 23,
NSTabletProximity = 24,
NSOtherMouseDown = 25,
NSOtherMouseUp = 26,
NSOtherMouseDragged = 27,
NSEventTypeGesture = 29,
NSEventTypeMagnify = 30,
NSEventTypeSwipe = 31,
NSEventTypeRotate = 18,
NSEventTypeBeginGesture = 19,
NSEventTypeEndGesture = 20,
NSEventTypePressure = 34,
}
unsafe impl Encode for NSEventType {
const ENCODING: Encoding = NSUInteger::ENCODING;
}

View File

@@ -0,0 +1,46 @@
use icrate::Foundation::{NSData, NSObject, NSSize, NSString};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType};
use super::NSBitmapImageRep;
extern_class!(
// TODO: Can this be mutable?
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSImage;
unsafe impl ClassType for NSImage {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// Documented Thread-Unsafe, but:
// > One thread can create an NSImage object, draw to the image buffer,
// > and pass it off to the main thread for drawing. The underlying image
// > cache is shared among all threads.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-126728>
//
// So really only unsafe to mutate on several threads.
unsafe impl Send for NSImage {}
unsafe impl Sync for NSImage {}
extern_methods!(
unsafe impl NSImage {
pub fn new_by_referencing_file(path: &NSString) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initByReferencingFile: path] }
}
pub fn new_with_data(data: &NSData) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithData: data] }
}
pub fn init_with_size(size: NSSize) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithSize: size] }
}
pub fn add_representation(&self, representation: &NSBitmapImageRep) {
unsafe { msg_send![self, addRepresentation: representation] }
}
}
);

View File

@@ -0,0 +1,25 @@
use icrate::Foundation::NSObject;
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::NSMenuItem;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSMenu;
unsafe impl ClassType for NSMenu {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSMenu {
#[method_id(new)]
pub fn new() -> Id<Self>;
#[method(addItem:)]
pub fn addItem(&self, item: &NSMenuItem);
}
);

View File

@@ -0,0 +1,47 @@
use icrate::Foundation::{NSObject, NSString};
use objc2::rc::Id;
use objc2::runtime::Sel;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType};
use super::{NSEventModifierFlags, NSMenu};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSMenuItem;
unsafe impl ClassType for NSMenuItem {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSMenuItem {
#[method_id(new)]
pub fn new() -> Id<Self>;
pub fn newWithTitle(
title: &NSString,
action: Option<Sel>,
key_equivalent: &NSString,
) -> Id<Self> {
unsafe {
msg_send_id![
Self::alloc(),
initWithTitle: title,
action: action,
keyEquivalent: key_equivalent,
]
}
}
#[method_id(separatorItem)]
pub fn separatorItem() -> Id<Self>;
#[method(setKeyEquivalentModifierMask:)]
pub fn setKeyEquivalentModifierMask(&self, mask: NSEventModifierFlags);
#[method(setSubmenu:)]
pub fn setSubmenu(&self, submenu: &NSMenu);
}
);

View File

@@ -0,0 +1,68 @@
//! Safe bindings for the AppKit framework.
//!
//! These are split out from the rest of `winit` to make safety easier to review.
//! In the future, these should probably live in another crate like `cacao`.
//!
//! TODO: Main thread safety.
// Objective-C methods have different conventions, and it's much easier to
// understand if we just use the same names
#![allow(non_snake_case)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::enum_variant_names)]
#![allow(non_upper_case_globals)]
mod appearance;
mod application;
mod bitmap_image_rep;
mod button;
mod color;
mod control;
mod cursor;
mod event;
mod image;
mod menu;
mod menu_item;
mod pasteboard;
mod responder;
mod screen;
mod tab_group;
mod text_input_client;
mod text_input_context;
mod version;
mod view;
mod window;
pub(crate) use self::appearance::NSAppearance;
pub(crate) use self::application::{
NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions,
NSRequestUserAttentionType,
};
pub(crate) use self::bitmap_image_rep::NSBitmapImageRep;
pub(crate) use self::button::NSButton;
pub(crate) use self::color::NSColor;
pub(crate) use self::control::NSControl;
pub(crate) use self::cursor::NSCursor;
#[allow(unused_imports)]
pub(crate) use self::event::{
NSEvent, NSEventModifierFlags, NSEventPhase, NSEventSubtype, NSEventType,
};
pub(crate) use self::image::NSImage;
pub(crate) use self::menu::NSMenu;
pub(crate) use self::menu_item::NSMenuItem;
pub(crate) use self::pasteboard::{NSFilenamesPboardType, NSPasteboard, NSPasteboardType};
pub(crate) use self::responder::NSResponder;
#[allow(unused_imports)]
pub(crate) use self::screen::{NSDeviceDescriptionKey, NSScreen};
pub(crate) use self::tab_group::NSWindowTabGroup;
pub(crate) use self::text_input_client::NSTextInputClient;
pub(crate) use self::text_input_context::NSTextInputContext;
pub(crate) use self::version::NSAppKitVersion;
pub(crate) use self::view::{NSTrackingRectTag, NSView};
pub(crate) use self::window::{
NSBackingStoreType, NSWindow, NSWindowButton, NSWindowLevel, NSWindowOcclusionState,
NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode,
NSWindowTitleVisibility,
};
#[link(name = "AppKit", kind = "framework")]
extern "C" {}

View File

@@ -0,0 +1,26 @@
use icrate::Foundation::{NSObject, NSString};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSPasteboard;
unsafe impl ClassType for NSPasteboard {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSPasteboard {
#[method_id(propertyListForType:)]
pub fn propertyListForType(&self, type_: &NSPasteboardType) -> Id<NSObject>;
}
);
pub type NSPasteboardType = NSString;
extern "C" {
pub static NSFilenamesPboardType: &'static NSPasteboardType;
}

View File

@@ -0,0 +1,23 @@
use icrate::Foundation::{NSArray, NSObject};
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::NSEvent;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct NSResponder;
unsafe impl ClassType for NSResponder {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// Documented as "Thread-Unsafe".
extern_methods!(
unsafe impl NSResponder {
#[method(interpretKeyEvents:)]
pub(crate) unsafe fn interpretKeyEvents(&self, events: &NSArray<NSEvent>);
}
);

View File

@@ -0,0 +1,65 @@
use icrate::ns_string;
use icrate::Foundation::{CGFloat, NSArray, NSDictionary, NSNumber, NSObject, NSRect, NSString};
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{extern_class, extern_methods, mutability, ClassType};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSScreen;
unsafe impl ClassType for NSScreen {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
// TODO: Main thread marker!
extern_methods!(
unsafe impl NSScreen {
/// The application object must have been created.
#[method_id(mainScreen)]
pub fn main() -> Option<Id<Self>>;
/// The application object must have been created.
#[method_id(screens)]
pub fn screens() -> Id<NSArray<Self>>;
#[method(frame)]
pub fn frame(&self) -> NSRect;
#[method(visibleFrame)]
pub fn visibleFrame(&self) -> NSRect;
#[method_id(deviceDescription)]
pub fn deviceDescription(&self) -> Id<NSDictionary<NSDeviceDescriptionKey, AnyObject>>;
pub fn display_id(&self) -> u32 {
let key = ns_string!("NSScreenNumber");
objc2::rc::autoreleasepool(|_| {
let device_description = self.deviceDescription();
// Retrieve the CGDirectDisplayID associated with this screen
//
// SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed
// to be an NSNumber. See documentation for `deviceDescription` for details:
// <https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc>
let obj = device_description
.get(key)
.expect("failed getting screen display id from device description");
let obj: *const AnyObject = obj;
let obj: *const NSNumber = obj.cast();
let obj: &NSNumber = unsafe { &*obj };
obj.as_u32()
})
}
#[method(backingScaleFactor)]
pub fn backingScaleFactor(&self) -> CGFloat;
}
);
pub type NSDeviceDescriptionKey = NSString;

View File

@@ -0,0 +1,31 @@
use icrate::Foundation::{NSArray, NSObject};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::NSWindow;
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSWindowTabGroup;
unsafe impl ClassType for NSWindowTabGroup {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSWindowTabGroup {
#[method(selectNextTab)]
pub fn selectNextTab(&self);
#[method(selectPreviousTab)]
pub fn selectPreviousTab(&self);
#[method_id(windows)]
pub fn tabbedWindows(&self) -> Option<Id<NSArray<NSWindow>>>;
#[method(setSelectedWindow:)]
pub fn setSelectedWindow(&self, window: &NSWindow);
}
);

View File

@@ -0,0 +1,9 @@
use objc2::{extern_protocol, ProtocolType};
extern_protocol!(
pub(crate) unsafe trait NSTextInputClient {
// TODO: Methods
}
unsafe impl ProtocolType for dyn NSTextInputClient {}
);

View File

@@ -0,0 +1,29 @@
use icrate::Foundation::{NSObject, NSString};
use objc2::rc::Id;
use objc2::{extern_class, extern_methods, mutability, ClassType};
type NSTextInputSourceIdentifier = NSString;
extern_class!(
/// Main-Thread-Only!
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSTextInputContext;
unsafe impl ClassType for NSTextInputContext {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl NSTextInputContext {
#[method(invalidateCharacterCoordinates)]
pub fn invalidateCharacterCoordinates(&self);
#[method(discardMarkedText)]
pub fn discardMarkedText(&self);
#[method_id(selectedKeyboardInputSource)]
pub fn selectedKeyboardInputSource(&self) -> Option<Id<NSTextInputSourceIdentifier>>;
}
);

View File

@@ -0,0 +1,62 @@
#[repr(transparent)]
#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
pub struct NSAppKitVersion(f64);
#[allow(dead_code)]
#[allow(non_upper_case_globals)]
impl NSAppKitVersion {
pub fn current() -> Self {
extern "C" {
static NSAppKitVersionNumber: NSAppKitVersion;
}
unsafe { NSAppKitVersionNumber }
}
pub fn floor(self) -> Self {
Self(self.0.floor())
}
pub const NSAppKitVersionNumber10_0: Self = Self(577.0);
pub const NSAppKitVersionNumber10_1: Self = Self(620.0);
pub const NSAppKitVersionNumber10_2: Self = Self(663.0);
pub const NSAppKitVersionNumber10_2_3: Self = Self(663.6);
pub const NSAppKitVersionNumber10_3: Self = Self(743.0);
pub const NSAppKitVersionNumber10_3_2: Self = Self(743.14);
pub const NSAppKitVersionNumber10_3_3: Self = Self(743.2);
pub const NSAppKitVersionNumber10_3_5: Self = Self(743.24);
pub const NSAppKitVersionNumber10_3_7: Self = Self(743.33);
pub const NSAppKitVersionNumber10_3_9: Self = Self(743.36);
pub const NSAppKitVersionNumber10_4: Self = Self(824.0);
pub const NSAppKitVersionNumber10_4_1: Self = Self(824.1);
pub const NSAppKitVersionNumber10_4_3: Self = Self(824.23);
pub const NSAppKitVersionNumber10_4_4: Self = Self(824.33);
pub const NSAppKitVersionNumber10_4_7: Self = Self(824.41);
pub const NSAppKitVersionNumber10_5: Self = Self(949.0);
pub const NSAppKitVersionNumber10_5_2: Self = Self(949.27);
pub const NSAppKitVersionNumber10_5_3: Self = Self(949.33);
pub const NSAppKitVersionNumber10_6: Self = Self(1038.0);
pub const NSAppKitVersionNumber10_7: Self = Self(1138.0);
pub const NSAppKitVersionNumber10_7_2: Self = Self(1138.23);
pub const NSAppKitVersionNumber10_7_3: Self = Self(1138.32);
pub const NSAppKitVersionNumber10_7_4: Self = Self(1138.47);
pub const NSAppKitVersionNumber10_8: Self = Self(1187.0);
pub const NSAppKitVersionNumber10_9: Self = Self(1265.0);
pub const NSAppKitVersionNumber10_10: Self = Self(1343.0);
pub const NSAppKitVersionNumber10_10_2: Self = Self(1344.0);
pub const NSAppKitVersionNumber10_10_3: Self = Self(1347.0);
pub const NSAppKitVersionNumber10_10_4: Self = Self(1348.0);
pub const NSAppKitVersionNumber10_10_5: Self = Self(1348.0);
pub const NSAppKitVersionNumber10_10_Max: Self = Self(1349.0);
pub const NSAppKitVersionNumber10_11: Self = Self(1404.0);
pub const NSAppKitVersionNumber10_11_1: Self = Self(1404.13);
pub const NSAppKitVersionNumber10_11_2: Self = Self(1404.34);
pub const NSAppKitVersionNumber10_11_3: Self = Self(1404.34);
pub const NSAppKitVersionNumber10_12: Self = Self(1504.0);
pub const NSAppKitVersionNumber10_12_1: Self = Self(1504.60);
pub const NSAppKitVersionNumber10_12_2: Self = Self(1504.76);
pub const NSAppKitVersionNumber10_13: Self = Self(1561.0);
pub const NSAppKitVersionNumber10_13_1: Self = Self(1561.1);
pub const NSAppKitVersionNumber10_13_2: Self = Self(1561.2);
pub const NSAppKitVersionNumber10_13_4: Self = Self(1561.4);
}

View File

@@ -0,0 +1,95 @@
use std::ffi::c_void;
use std::num::NonZeroIsize;
use std::ptr;
use icrate::Foundation::{NSObject, NSPoint, NSRect};
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::{NSCursor, NSResponder, NSTextInputContext, NSWindow};
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSView;
unsafe impl ClassType for NSView {
#[inherits(NSObject)]
type Super = NSResponder;
type Mutability = mutability::InteriorMutable;
}
);
// Documented as "Main Thread Only".
// > generally thread safe, although operations on views such as creating,
// > resizing, and moving should happen on the main thread.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaFundamentals/AddingBehaviortoaCocoaProgram/AddingBehaviorCocoa.html#//apple_ref/doc/uid/TP40002974-CH5-SW47>
//
// > If you want to use a thread to draw to a view, bracket all drawing code
// > between the lockFocusIfCanDraw and unlockFocus methods of NSView.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123351-BBCFIIEB>
extern_methods!(
/// Getter methods
unsafe impl NSView {
#[method(frame)]
pub fn frame(&self) -> NSRect;
#[method(bounds)]
pub fn bounds(&self) -> NSRect;
#[method_id(inputContext)]
pub fn inputContext(
&self,
// _mtm: MainThreadMarker,
) -> Option<Id<NSTextInputContext>>;
#[method(hasMarkedText)]
pub fn hasMarkedText(&self) -> bool;
#[method(convertPoint:fromView:)]
pub fn convertPoint_fromView(&self, point: NSPoint, view: Option<&NSView>) -> NSPoint;
#[method_id(window)]
pub fn window(&self) -> Option<Id<NSWindow>>;
}
unsafe impl NSView {
#[method(setWantsBestResolutionOpenGLSurface:)]
pub fn setWantsBestResolutionOpenGLSurface(&self, value: bool);
#[method(setWantsLayer:)]
pub fn setWantsLayer(&self, wants_layer: bool);
#[method(setPostsFrameChangedNotifications:)]
pub fn setPostsFrameChangedNotifications(&self, value: bool);
#[method(removeTrackingRect:)]
pub fn removeTrackingRect(&self, tag: NSTrackingRectTag);
#[method(addTrackingRect:owner:userData:assumeInside:)]
unsafe fn inner_addTrackingRect(
&self,
rect: NSRect,
owner: &AnyObject,
user_data: *mut c_void,
assume_inside: bool,
) -> Option<NSTrackingRectTag>;
pub fn add_tracking_rect(&self, rect: NSRect, assume_inside: bool) -> NSTrackingRectTag {
// SAFETY: The user data is NULL, so it is valid
unsafe { self.inner_addTrackingRect(rect, self, ptr::null_mut(), assume_inside) }
.expect("failed creating tracking rect")
}
#[method(addCursorRect:cursor:)]
// NSCursor safe to take by shared reference since it is already immutable
pub fn addCursorRect(&self, rect: NSRect, cursor: &NSCursor);
#[method(setHidden:)]
pub fn setHidden(&self, hidden: bool);
}
);
/// <https://developer.apple.com/documentation/appkit/nstrackingrecttag?language=objc>
pub type NSTrackingRectTag = NonZeroIsize; // NSInteger, but non-zero!

View File

@@ -0,0 +1,440 @@
use icrate::Foundation::{
CGFloat, NSArray, NSInteger, NSObject, NSPoint, NSRect, NSSize, NSString, NSUInteger,
};
use objc2::encode::{Encode, Encoding};
use objc2::rc::Id;
use objc2::runtime::AnyObject;
use objc2::{extern_class, extern_methods, mutability, ClassType};
use super::{
NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView, NSWindowTabGroup,
};
extern_class!(
/// Main-Thread-Only!
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct NSWindow;
unsafe impl ClassType for NSWindow {
#[inherits(NSObject)]
type Super = NSResponder;
type Mutability = mutability::InteriorMutable;
}
);
// Documented as "Main Thread Only", but:
// > Thread safe in that you can create and manage them on a secondary thread.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaFundamentals/AddingBehaviortoaCocoaProgram/AddingBehaviorCocoa.html#//apple_ref/doc/uid/TP40002974-CH5-SW47>
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123364>
//
// So could in theory be `Send`, and perhaps also `Sync` - but we would like
// interior mutability on windows, since that's just much easier, and in that
// case, they can't be!
extern_methods!(
unsafe impl NSWindow {
#[method(frame)]
pub(crate) fn frame(&self) -> NSRect;
#[method(windowNumber)]
pub(crate) fn windowNumber(&self) -> NSInteger;
#[method(backingScaleFactor)]
pub(crate) fn backingScaleFactor(&self) -> CGFloat;
#[method_id(contentView)]
pub(crate) fn contentView(&self) -> Id<NSView>;
#[method(setContentView:)]
pub(crate) fn setContentView(&self, view: &NSView);
#[method(setInitialFirstResponder:)]
pub(crate) fn setInitialFirstResponder(&self, view: &NSView);
#[method(makeFirstResponder:)]
#[must_use]
pub(crate) fn makeFirstResponder(&self, responder: Option<&NSResponder>) -> bool;
#[method(contentRectForFrameRect:)]
pub(crate) fn contentRectForFrameRect(&self, windowFrame: NSRect) -> NSRect;
#[method_id(screen)]
pub(crate) fn screen(&self) -> Option<Id<NSScreen>>;
#[method(setContentSize:)]
pub(crate) fn setContentSize(&self, contentSize: NSSize);
#[method(setFrameTopLeftPoint:)]
pub(crate) fn setFrameTopLeftPoint(&self, point: NSPoint);
#[method(setMinSize:)]
pub(crate) fn setMinSize(&self, minSize: NSSize);
#[method(setMaxSize:)]
pub(crate) fn setMaxSize(&self, maxSize: NSSize);
#[method(setResizeIncrements:)]
pub(crate) fn setResizeIncrements(&self, increments: NSSize);
#[method(contentResizeIncrements)]
pub(crate) fn contentResizeIncrements(&self) -> NSSize;
#[method(setContentResizeIncrements:)]
pub(crate) fn setContentResizeIncrements(&self, increments: NSSize);
#[method(setFrame:display:)]
pub(crate) fn setFrame_display(&self, frameRect: NSRect, flag: bool);
#[method(setMovable:)]
pub(crate) fn setMovable(&self, movable: bool);
#[method(setSharingType:)]
pub(crate) fn setSharingType(&self, sharingType: NSWindowSharingType);
#[method(setTabbingMode:)]
pub(crate) fn setTabbingMode(&self, tabbingMode: NSWindowTabbingMode);
#[method(setOpaque:)]
pub(crate) fn setOpaque(&self, opaque: bool);
#[method(hasShadow)]
pub(crate) fn hasShadow(&self) -> bool;
#[method(setHasShadow:)]
pub(crate) fn setHasShadow(&self, has_shadow: bool);
#[method(setIgnoresMouseEvents:)]
pub(crate) fn setIgnoresMouseEvents(&self, ignores: bool);
#[method(setBackgroundColor:)]
pub(crate) fn setBackgroundColor(&self, color: &NSColor);
#[method(styleMask)]
pub(crate) fn styleMask(&self) -> NSWindowStyleMask;
#[method(setStyleMask:)]
pub(crate) fn setStyleMask(&self, mask: NSWindowStyleMask);
#[method(registerForDraggedTypes:)]
pub(crate) fn registerForDraggedTypes(&self, types: &NSArray<NSPasteboardType>);
#[method(makeKeyAndOrderFront:)]
pub(crate) fn makeKeyAndOrderFront(&self, sender: Option<&AnyObject>);
#[method(orderFront:)]
pub(crate) fn orderFront(&self, sender: Option<&AnyObject>);
#[method(miniaturize:)]
pub(crate) fn miniaturize(&self, sender: Option<&AnyObject>);
#[method(deminiaturize:)]
pub(crate) fn deminiaturize(&self, sender: Option<&AnyObject>);
#[method(toggleFullScreen:)]
pub(crate) fn toggleFullScreen(&self, sender: Option<&AnyObject>);
#[method(orderOut:)]
pub(crate) fn orderOut(&self, sender: Option<&AnyObject>);
#[method(zoom:)]
pub(crate) fn zoom(&self, sender: Option<&AnyObject>);
#[method(selectNextKeyView:)]
pub(crate) fn selectNextKeyView(&self, sender: Option<&AnyObject>);
#[method(selectPreviousKeyView:)]
pub(crate) fn selectPreviousKeyView(&self, sender: Option<&AnyObject>);
#[method_id(firstResponder)]
pub(crate) fn firstResponder(&self) -> Option<Id<NSResponder>>;
#[method_id(standardWindowButton:)]
pub(crate) fn standardWindowButton(&self, kind: NSWindowButton) -> Option<Id<NSButton>>;
#[method(setTitle:)]
pub(crate) fn setTitle(&self, title: &NSString);
#[method_id(title)]
pub(crate) fn title_(&self) -> Id<NSString>;
#[method(setReleasedWhenClosed:)]
pub(crate) fn setReleasedWhenClosed(&self, val: bool);
#[method(setAcceptsMouseMovedEvents:)]
pub(crate) fn setAcceptsMouseMovedEvents(&self, val: bool);
#[method(setTitlebarAppearsTransparent:)]
pub(crate) fn setTitlebarAppearsTransparent(&self, val: bool);
#[method(setTitleVisibility:)]
pub(crate) fn setTitleVisibility(&self, visibility: NSWindowTitleVisibility);
#[method(setMovableByWindowBackground:)]
pub(crate) fn setMovableByWindowBackground(&self, val: bool);
#[method(setLevel:)]
pub(crate) fn setLevel(&self, level: NSWindowLevel);
#[method(setAllowsAutomaticWindowTabbing:)]
pub(crate) fn setAllowsAutomaticWindowTabbing(val: bool);
#[method(setTabbingIdentifier:)]
pub(crate) fn setTabbingIdentifier(&self, identifier: &NSString);
#[method(setDocumentEdited:)]
pub(crate) fn setDocumentEdited(&self, val: bool);
#[method(occlusionState)]
pub(crate) fn occlusionState(&self) -> NSWindowOcclusionState;
#[method(center)]
pub(crate) fn center(&self);
#[method(isResizable)]
pub(crate) fn isResizable(&self) -> bool;
#[method(isMiniaturizable)]
pub(crate) fn isMiniaturizable(&self) -> bool;
#[method(hasCloseBox)]
pub(crate) fn hasCloseBox(&self) -> bool;
#[method(isMiniaturized)]
pub(crate) fn isMiniaturized(&self) -> bool;
#[method(isVisible)]
pub(crate) fn isVisible(&self) -> bool;
#[method(isKeyWindow)]
pub(crate) fn isKeyWindow(&self) -> bool;
#[method(isZoomed)]
pub(crate) fn isZoomed(&self) -> bool;
#[method(allowsAutomaticWindowTabbing)]
pub(crate) fn allowsAutomaticWindowTabbing() -> bool;
#[method(selectNextTab)]
pub(crate) fn selectNextTab(&self);
#[method_id(tabbingIdentifier)]
pub(crate) fn tabbingIdentifier(&self) -> Id<NSString>;
#[method_id(tabGroup)]
pub(crate) fn tabGroup(&self) -> Option<Id<NSWindowTabGroup>>;
#[method(isDocumentEdited)]
pub(crate) fn isDocumentEdited(&self) -> bool;
#[method(close)]
pub(crate) fn close(&self);
#[method(performWindowDragWithEvent:)]
// TODO: Can this actually accept NULL?
pub(crate) fn performWindowDragWithEvent(&self, event: Option<&NSEvent>);
#[method(invalidateCursorRectsForView:)]
pub(crate) fn invalidateCursorRectsForView(&self, view: &NSView);
#[method(setDelegate:)]
pub(crate) fn setDelegate(&self, delegate: Option<&NSObject>);
#[method(sendEvent:)]
pub(crate) unsafe fn sendEvent(&self, event: &NSEvent);
#[method(addChildWindow:ordered:)]
pub(crate) unsafe fn addChildWindow(&self, child: &NSWindow, ordered: NSWindowOrderingMode);
}
);
#[allow(dead_code)]
#[repr(isize)] // NSInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSWindowTitleVisibility {
#[doc(alias = "NSWindowTitleVisible")]
Visible = 0,
#[doc(alias = "NSWindowTitleHidden")]
Hidden = 1,
}
unsafe impl Encode for NSWindowTitleVisibility {
const ENCODING: Encoding = NSInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(usize)] // NSUInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSWindowButton {
#[doc(alias = "NSWindowCloseButton")]
Close = 0,
#[doc(alias = "NSWindowMiniaturizeButton")]
Miniaturize = 1,
#[doc(alias = "NSWindowZoomButton")]
Zoom = 2,
#[doc(alias = "NSWindowToolbarButton")]
Toolbar = 3,
#[doc(alias = "NSWindowDocumentIconButton")]
DocumentIcon = 4,
#[doc(alias = "NSWindowDocumentVersionsButton")]
DocumentVersions = 6,
#[doc(alias = "NSWindowFullScreenButton")]
#[deprecated = "Deprecated since macOS 10.12"]
FullScreen = 7,
}
unsafe impl Encode for NSWindowButton {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
// CGWindowLevel.h
//
// Note: There are two different things at play in this header:
// `CGWindowLevel` and `CGWindowLevelKey`.
//
// It seems like there was a push towards using "key" values instead of the
// raw window level values, and then you were supposed to use
// `CGWindowLevelForKey` to get the actual level.
//
// But the values that `NSWindowLevel` has are compiled in, and as such has
// to remain ABI compatible, so they're safe for us to hardcode as well.
#[allow(dead_code)]
mod window_level {
const kCGNumReservedWindowLevels: i32 = 16;
const kCGNumReservedBaseWindowLevels: i32 = 5;
pub const kCGBaseWindowLevel: i32 = i32::MIN;
pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels;
pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels;
pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20;
pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20;
pub const kCGBackstopMenuLevel: i32 = -20;
pub const kCGNormalWindowLevel: i32 = 0;
pub const kCGFloatingWindowLevel: i32 = 3;
pub const kCGTornOffMenuWindowLevel: i32 = 3;
pub const kCGModalPanelWindowLevel: i32 = 8;
pub const kCGUtilityWindowLevel: i32 = 19;
pub const kCGDockWindowLevel: i32 = 20;
pub const kCGMainMenuWindowLevel: i32 = 24;
pub const kCGStatusWindowLevel: i32 = 25;
pub const kCGPopUpMenuWindowLevel: i32 = 101;
pub const kCGOverlayWindowLevel: i32 = 102;
pub const kCGHelpWindowLevel: i32 = 200;
pub const kCGDraggingWindowLevel: i32 = 500;
pub const kCGScreenSaverWindowLevel: i32 = 1000;
pub const kCGAssistiveTechHighWindowLevel: i32 = 1500;
pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1;
}
use window_level::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct NSWindowLevel(pub NSInteger);
#[allow(dead_code)]
impl NSWindowLevel {
#[doc(alias = "BelowNormalWindowLevel")]
pub const BELOW_NORMAL: Self = Self((kCGNormalWindowLevel - 1) as _);
#[doc(alias = "NSNormalWindowLevel")]
pub const Normal: Self = Self(kCGNormalWindowLevel as _);
#[doc(alias = "NSFloatingWindowLevel")]
pub const Floating: Self = Self(kCGFloatingWindowLevel as _);
#[doc(alias = "NSTornOffMenuWindowLevel")]
pub const TornOffMenu: Self = Self(kCGTornOffMenuWindowLevel as _);
#[doc(alias = "NSModalPanelWindowLevel")]
pub const ModalPanel: Self = Self(kCGModalPanelWindowLevel as _);
#[doc(alias = "NSMainMenuWindowLevel")]
pub const MainMenu: Self = Self(kCGMainMenuWindowLevel as _);
#[doc(alias = "NSStatusWindowLevel")]
pub const Status: Self = Self(kCGStatusWindowLevel as _);
#[doc(alias = "NSPopUpMenuWindowLevel")]
pub const PopUpMenu: Self = Self(kCGPopUpMenuWindowLevel as _);
#[doc(alias = "NSScreenSaverWindowLevel")]
pub const ScreenSaver: Self = Self(kCGScreenSaverWindowLevel as _);
}
unsafe impl Encode for NSWindowLevel {
const ENCODING: Encoding = NSInteger::ENCODING;
}
bitflags! {
#[derive(Clone, Copy)]
pub struct NSWindowOcclusionState: NSUInteger {
const NSWindowOcclusionStateVisible = 1 << 1;
}
}
unsafe impl Encode for NSWindowOcclusionState {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct NSWindowStyleMask: NSUInteger {
const NSBorderlessWindowMask = 0;
const NSTitledWindowMask = 1 << 0;
const NSClosableWindowMask = 1 << 1;
const NSMiniaturizableWindowMask = 1 << 2;
const NSResizableWindowMask = 1 << 3;
const NSTexturedBackgroundWindowMask = 1 << 8;
const NSUnifiedTitleAndToolbarWindowMask = 1 << 12;
const NSFullScreenWindowMask = 1 << 14;
const NSFullSizeContentViewWindowMask = 1 << 15;
}
}
unsafe impl Encode for NSWindowStyleMask {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(usize)] // NSUInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSBackingStoreType {
NSBackingStoreRetained = 0,
NSBackingStoreNonretained = 1,
NSBackingStoreBuffered = 2,
}
unsafe impl Encode for NSBackingStoreType {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(usize)] // NSUInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSWindowSharingType {
NSWindowSharingNone = 0,
NSWindowSharingReadOnly = 1,
NSWindowSharingReadWrite = 2,
}
unsafe impl Encode for NSWindowSharingType {
const ENCODING: Encoding = NSUInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(isize)] // NSInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSWindowOrderingMode {
NSWindowAbove = 1,
NSWindowBelow = -1,
NSWindowOut = 0,
}
unsafe impl Encode for NSWindowOrderingMode {
const ENCODING: Encoding = NSInteger::ENCODING;
}
#[allow(dead_code)]
#[repr(isize)] // NSInteger
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NSWindowTabbingMode {
NSWindowTabbingModeAutomatic = 0,
NSWindowTabbingModeDisallowed = 2,
NSWindowTabbingModePreferred = 1,
}
unsafe impl Encode for NSWindowTabbingMode {
const ENCODING: Encoding = NSInteger::ENCODING;
}

View File

@@ -1,228 +0,0 @@
use icrate::AppKit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
use icrate::Foundation::{
ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize,
NSString,
};
use objc2::rc::Id;
use objc2::runtime::Sel;
use objc2::{msg_send_id, sel, ClassType};
use once_cell::sync::Lazy;
use std::ffi::c_uchar;
use std::slice;
use super::EventLoopWindowTarget;
use crate::cursor::CursorImage;
use crate::cursor::OnlyCursorImageBuilder;
use crate::window::CursorIcon;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CustomCursor(pub(crate) Id<NSCursor>);
// SAFETY: NSCursor is immutable and thread-safe
// TODO(madsmtm): Put this logic in icrate itself
unsafe impl Send for CustomCursor {}
unsafe impl Sync for CustomCursor {}
impl CustomCursor {
pub(crate) fn build<T>(
cursor: OnlyCursorImageBuilder,
_: &EventLoopWindowTarget<T>,
) -> CustomCursor {
Self(cursor_from_image(&cursor.0))
}
}
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Id<NSCursor> {
let width = cursor.width;
let height = cursor.height;
let bitmap = unsafe {
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
std::ptr::null_mut::<*mut c_uchar>(),
width as isize,
height as isize,
8,
4,
true,
false,
NSDeviceRGBColorSpace,
width as isize * 4,
32,
).unwrap()
};
let bitmap_data = unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) };
bitmap_data.copy_from_slice(&cursor.rgba);
let image = unsafe {
NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into()))
};
unsafe { image.addRepresentation(&bitmap) };
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
}
pub(crate) fn default_cursor() -> Id<NSCursor> {
NSCursor::arrowCursor()
}
unsafe fn try_cursor_from_selector(sel: Sel) -> Option<Id<NSCursor>> {
let cls = NSCursor::class();
if cls.responds_to(sel) {
let cursor: Id<NSCursor> = unsafe { msg_send_id![cls, performSelector: sel] };
Some(cursor)
} else {
warn!("cursor `{sel}` appears to be invalid");
None
}
}
macro_rules! def_undocumented_cursor {
{$(
$(#[$($m:meta)*])*
fn $name:ident();
)*} => {$(
$(#[$($m)*])*
#[allow(non_snake_case)]
fn $name() -> Id<NSCursor> {
unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) }
}
)*};
}
def_undocumented_cursor!(
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
fn _helpCursor();
fn _zoomInCursor();
fn _zoomOutCursor();
fn _windowResizeNorthEastCursor();
fn _windowResizeNorthWestCursor();
fn _windowResizeSouthEastCursor();
fn _windowResizeSouthWestCursor();
fn _windowResizeNorthEastSouthWestCursor();
fn _windowResizeNorthWestSouthEastCursor();
// While these two are available, the former just loads a white arrow,
// and the latter loads an ugly deflated beachball!
// pub fn _moveCursor();
// pub fn _waitCursor();
// An even more undocumented cursor...
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
fn busyButClickableCursor();
);
// Note that loading `busybutclickable` with this code won't animate
// the frames; instead you'll just get them all in a column.
unsafe fn load_webkit_cursor(name: &NSString) -> Id<NSCursor> {
// Snatch a cursor from WebKit; They fit the style of the native
// cursors, and will seem completely standard to macOS users.
//
// https://stackoverflow.com/a/21786835/5435443
let root = ns_string!("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors");
let cursor_path = root.stringByAppendingPathComponent(name);
let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf"));
let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap();
// TODO: Handle PLists better
let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist"));
let info: Id<NSDictionary<NSObject, NSObject>> = unsafe {
msg_send_id![
<NSDictionary<NSObject, NSObject>>::class(),
dictionaryWithContentsOfFile: &*info_path,
]
};
let mut x = 0.0;
if let Some(n) = info.get(&*ns_string!("hotx")) {
if n.is_kind_of::<NSNumber>() {
let ptr: *const NSObject = n;
let ptr: *const NSNumber = ptr.cast();
x = unsafe { &*ptr }.as_cgfloat()
}
}
let mut y = 0.0;
if let Some(n) = info.get(&*ns_string!("hotx")) {
if n.is_kind_of::<NSNumber>() {
let ptr: *const NSObject = n;
let ptr: *const NSNumber = ptr.cast();
y = unsafe { &*ptr }.as_cgfloat()
}
}
let hotspot = NSPoint::new(x, y);
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
}
fn webkit_move() -> Id<NSCursor> {
unsafe { load_webkit_cursor(ns_string!("move")) }
}
fn webkit_cell() -> Id<NSCursor> {
unsafe { load_webkit_cursor(ns_string!("cell")) }
}
pub(crate) fn invisible_cursor() -> Id<NSCursor> {
// 16x16 GIF data for invisible cursor
// You can reproduce this via ImageMagick.
// $ convert -size 16x16 xc:none cursor.gif
static CURSOR_BYTES: &[u8] = &[
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00,
0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x84, 0x8F, 0xA9, 0xCB, 0xED, 0x0F,
0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B,
];
static CURSOR: Lazy<CustomCursor> = Lazy::new(|| {
// TODO: Consider using `dataWithBytesNoCopy:`
let data = NSData::with_bytes(CURSOR_BYTES);
let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap();
let hotspot = NSPoint::new(0.0, 0.0);
CustomCursor(NSCursor::initWithImage_hotSpot(
NSCursor::alloc(),
&image,
hotspot,
))
});
CURSOR.0.clone()
}
pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Id<NSCursor> {
match icon {
CursorIcon::Default => default_cursor(),
CursorIcon::Pointer => NSCursor::pointingHandCursor(),
CursorIcon::Grab => NSCursor::openHandCursor(),
CursorIcon::Grabbing => NSCursor::closedHandCursor(),
CursorIcon::Text => NSCursor::IBeamCursor(),
CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(),
CursorIcon::Copy => NSCursor::dragCopyCursor(),
CursorIcon::Alias => NSCursor::dragLinkCursor(),
CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(),
CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(),
CursorIcon::Crosshair => NSCursor::crosshairCursor(),
CursorIcon::EResize => NSCursor::resizeRightCursor(),
CursorIcon::NResize => NSCursor::resizeUpCursor(),
CursorIcon::WResize => NSCursor::resizeLeftCursor(),
CursorIcon::SResize => NSCursor::resizeDownCursor(),
CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(),
CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(),
CursorIcon::Help => _helpCursor(),
CursorIcon::ZoomIn => _zoomInCursor(),
CursorIcon::ZoomOut => _zoomOutCursor(),
CursorIcon::NeResize => _windowResizeNorthEastCursor(),
CursorIcon::NwResize => _windowResizeNorthWestCursor(),
CursorIcon::SeResize => _windowResizeSouthEastCursor(),
CursorIcon::SwResize => _windowResizeSouthWestCursor(),
CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(),
CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(),
// This is the wrong semantics for `Wait`, but it's the same as
// what's used in Safari and Chrome.
CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(),
CursorIcon::Move | CursorIcon::AllScroll => webkit_move(),
CursorIcon::Cell => webkit_cell(),
_ => default_cursor(),
}
}

View File

@@ -4,15 +4,10 @@ use core_foundation::{
base::CFRelease,
data::{CFDataGetBytePtr, CFDataRef},
};
use icrate::AppKit::{
NSEvent, NSEventModifierFlagCommand, NSEventModifierFlagControl, NSEventModifierFlagOption,
NSEventModifierFlagShift, NSEventModifierFlags, NSEventSubtypeWindowExposed,
NSEventTypeApplicationDefined,
};
use icrate::Foundation::{MainThreadMarker, NSPoint};
use objc2::rc::Id;
use icrate::Foundation::MainThreadMarker;
use smol_str::SmolStr;
use super::appkit::{NSEvent, NSEventModifierFlags};
use crate::{
event::{ElementState, KeyEvent, Modifiers},
keyboard::{
@@ -103,7 +98,8 @@ pub fn get_modifierless_char(scancode: u16) -> Key {
}
fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key {
let string = unsafe { ns_event.charactersIgnoringModifiers() }
let string = ns_event
.charactersIgnoringModifiers()
.map(|s| s.to_string())
.unwrap_or_default();
if string.is_empty() {
@@ -126,14 +122,15 @@ pub(crate) fn create_key_event(
use ElementState::{Pressed, Released};
let state = if is_press { Pressed } else { Released };
let scancode = unsafe { ns_event.keyCode() };
let scancode = ns_event.key_code();
let mut physical_key =
key_override.unwrap_or_else(|| PhysicalKey::from_scancode(scancode as u32));
let text_with_all_modifiers: Option<SmolStr> = if key_override.is_some() {
None
} else {
let characters = unsafe { ns_event.characters() }
let characters = ns_event
.characters()
.map(|s| s.to_string())
.unwrap_or_default();
if characters.is_empty() {
@@ -151,8 +148,8 @@ pub(crate) fn create_key_event(
let (logical_key, key_without_modifiers) = if matches!(key_from_code, Key::Unidentified(_)) {
let key_without_modifiers = get_modifierless_char(scancode);
let modifiers = unsafe { ns_event.modifierFlags() };
let has_ctrl = flags_contains(modifiers, NSEventModifierFlagControl);
let modifiers = NSEvent::modifierFlags(ns_event);
let has_ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let logical_key = match text_with_all_modifiers.as_ref() {
// Only checking for ctrl here, not checking for alt because we DO want to
@@ -314,107 +311,49 @@ pub fn extra_function_key_to_code(scancode: u16, string: &str) -> PhysicalKey {
}
}
// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259
const NX_DEVICELCTLKEYMASK: NSEventModifierFlags = 0x00000001;
const NX_DEVICELSHIFTKEYMASK: NSEventModifierFlags = 0x00000002;
const NX_DEVICERSHIFTKEYMASK: NSEventModifierFlags = 0x00000004;
const NX_DEVICELCMDKEYMASK: NSEventModifierFlags = 0x00000008;
const NX_DEVICERCMDKEYMASK: NSEventModifierFlags = 0x00000010;
const NX_DEVICELALTKEYMASK: NSEventModifierFlags = 0x00000020;
const NX_DEVICERALTKEYMASK: NSEventModifierFlags = 0x00000040;
const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = 0x00002000;
pub(super) fn flags_contains(flags: NSEventModifierFlags, value: NSEventModifierFlags) -> bool {
flags & value == value
}
pub(super) fn lalt_pressed(event: &NSEvent) -> bool {
flags_contains(unsafe { event.modifierFlags() }, NX_DEVICELALTKEYMASK)
}
pub(super) fn ralt_pressed(event: &NSEvent) -> bool {
flags_contains(unsafe { event.modifierFlags() }, NX_DEVICERALTKEYMASK)
}
pub(super) fn event_mods(event: &NSEvent) -> Modifiers {
let flags = unsafe { event.modifierFlags() };
let flags = event.modifierFlags();
let mut state = ModifiersState::empty();
let mut pressed_mods = ModifiersKeys::empty();
state.set(
ModifiersState::SHIFT,
flags_contains(flags, NSEventModifierFlagShift),
);
pressed_mods.set(
ModifiersKeys::LSHIFT,
flags_contains(flags, NX_DEVICELSHIFTKEYMASK),
);
pressed_mods.set(
ModifiersKeys::RSHIFT,
flags_contains(flags, NX_DEVICERSHIFTKEYMASK),
flags.contains(NSEventModifierFlags::NSShiftKeyMask),
);
pressed_mods.set(ModifiersKeys::LSHIFT, event.lshift_pressed());
pressed_mods.set(ModifiersKeys::RSHIFT, event.rshift_pressed());
state.set(
ModifiersState::CONTROL,
flags_contains(flags, NSEventModifierFlagControl),
);
pressed_mods.set(
ModifiersKeys::LCONTROL,
flags_contains(flags, NX_DEVICELCTLKEYMASK),
);
pressed_mods.set(
ModifiersKeys::RCONTROL,
flags_contains(flags, NX_DEVICERCTLKEYMASK),
flags.contains(NSEventModifierFlags::NSControlKeyMask),
);
pressed_mods.set(ModifiersKeys::LCONTROL, event.lctrl_pressed());
pressed_mods.set(ModifiersKeys::RCONTROL, event.rctrl_pressed());
state.set(
ModifiersState::ALT,
flags_contains(flags, NSEventModifierFlagOption),
);
pressed_mods.set(
ModifiersKeys::LALT,
flags_contains(flags, NX_DEVICELALTKEYMASK),
);
pressed_mods.set(
ModifiersKeys::RALT,
flags_contains(flags, NX_DEVICERALTKEYMASK),
flags.contains(NSEventModifierFlags::NSAlternateKeyMask),
);
pressed_mods.set(ModifiersKeys::LALT, event.lalt_pressed());
pressed_mods.set(ModifiersKeys::RALT, event.ralt_pressed());
state.set(
ModifiersState::SUPER,
flags_contains(flags, NSEventModifierFlagCommand),
);
pressed_mods.set(
ModifiersKeys::LSUPER,
flags_contains(flags, NX_DEVICELCMDKEYMASK),
);
pressed_mods.set(
ModifiersKeys::RSUPER,
flags_contains(flags, NX_DEVICERCMDKEYMASK),
flags.contains(NSEventModifierFlags::NSCommandKeyMask),
);
pressed_mods.set(ModifiersKeys::LSUPER, event.lcmd_pressed());
pressed_mods.set(ModifiersKeys::RSUPER, event.rcmd_pressed());
Modifiers {
state,
pressed_mods,
}
}
pub(super) fn dummy_event() -> Option<Id<NSEvent>> {
unsafe {
NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
NSEventTypeApplicationDefined,
NSPoint::new(0.0, 0.0),
0, // Empty NSEventModifierFlags
0.0,
0,
None,
NSEventSubtypeWindowExposed,
0,
0,
)
}
}
impl PhysicalKeyExtScancode for PhysicalKey {
fn to_scancode(self) -> Option<u32> {
let code = match self {

View File

@@ -17,18 +17,12 @@ use core_foundation::runloop::{
kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext,
CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
};
use icrate::AppKit::{
NSApplication, NSApplicationActivationPolicyAccessory, NSApplicationActivationPolicyProhibited,
NSApplicationActivationPolicyRegular, NSWindow,
};
use icrate::Foundation::{MainThreadMarker, NSObjectProtocol};
use icrate::Foundation::MainThreadMarker;
use objc2::rc::{autoreleasepool, Id};
use objc2::runtime::NSObjectProtocol;
use objc2::{msg_send_id, ClassType};
use objc2::{
rc::{autoreleasepool, Id},
runtime::ProtocolObject,
};
use super::event::dummy_event;
use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent, NSWindow};
use crate::{
error::EventLoopError,
event::Event,
@@ -129,19 +123,19 @@ impl<T: 'static> EventLoopWindowTarget<T> {
impl<T> EventLoopWindowTarget<T> {
pub(crate) fn hide_application(&self) {
NSApplication::sharedApplication(self.mtm).hide(None)
NSApplication::shared(self.mtm).hide(None)
}
pub(crate) fn hide_other_applications(&self) {
NSApplication::sharedApplication(self.mtm).hideOtherApplications(None)
NSApplication::shared(self.mtm).hideOtherApplications(None)
}
pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm)
NSWindow::setAllowsAutomaticWindowTabbing(enabled)
}
pub(crate) fn allows_automatic_window_tabbing(&self) -> bool {
NSWindow::allowsAutomaticWindowTabbing(self.mtm)
NSWindow::allowsAutomaticWindowTabbing()
}
}
@@ -202,20 +196,20 @@ impl<T> EventLoop<T> {
panic!("`winit` requires control over the principal class. You must create the event loop before other parts of your application initialize NSApplication");
}
use NSApplicationActivationPolicy::*;
let activation_policy = match attributes.activation_policy {
ActivationPolicy::Regular => NSApplicationActivationPolicyRegular,
ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory,
ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited,
};
let delegate = ApplicationDelegate::new(
mtm,
activation_policy,
attributes.default_menu,
attributes.activate_ignoring_other_apps,
);
autoreleasepool(|_| {
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
app.setDelegate(&delegate);
});
let panic_info: Rc<PanicInfo> = Default::default();
@@ -313,7 +307,7 @@ impl<T> EventLoop<T> {
// While the app is running it's possible that we catch a panic
// to avoid unwinding across an objective-c ffi boundary, which
// will lead to us stopping the `NSApplication` and saving the
// will lead to us stopping the `NSApp` and saving the
// `PanicInfo` so that we can resume the unwind at a controlled,
// safe point in time.
if let Some(panic) = self.panic_info.take() {
@@ -361,6 +355,8 @@ impl<T> EventLoop<T> {
self._callback = Some(Rc::clone(&callback));
autoreleasepool(|_| {
let app = NSApp();
// A bit of juggling with the callback references to make sure
// that `self.callback` is the only owner of the callback.
let weak_cb: Weak<_> = Rc::downgrade(&callback);
@@ -381,24 +377,25 @@ impl<T> EventLoop<T> {
// catch panics to make sure we can't unwind without clearing the set callback
// (which would leave the global `AppState` in an undefined, unsafe state)
let catch_result = catch_unwind(AssertUnwindSafe(|| {
// As a special case, if the application hasn't been launched yet then we at least run
// As a special case, if the `NSApp` hasn't been launched yet then we at least run
// the loop until it has fully launched.
if !AppState::is_launched() {
debug_assert!(!AppState::is_running());
AppState::request_stop_on_launch();
unsafe {
self.app.run();
app.run();
}
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application has launched
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched
} else if !AppState::is_running() {
// Even though the application may have been launched, it's possible we aren't running
// Even though the NSApp may have been launched, it's possible we aren't running
// if the `EventLoop` was run before and has since exited. This indicates that
// we just starting to re-run the same `EventLoop` again.
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
} else {
// Only run for as long as the given `Duration` allows so we don't block the external loop.
// Only run the NSApp for as long as the given `Duration` allows so we
// don't block the external loop.
match timeout {
Some(Duration::ZERO) => {
AppState::set_wait_timeout(None);
@@ -418,13 +415,13 @@ impl<T> EventLoop<T> {
}
AppState::set_stop_app_on_redraw_requested(true);
unsafe {
self.app.run();
app.run();
}
}
// While the app is running it's possible that we catch a panic
// to avoid unwinding across an objective-c ffi boundary, which
// will lead to us stopping the application and saving the
// will lead to us stopping the `NSApp` and saving the
// `PanicInfo` so that we can resume the unwind at a controlled,
// safe point in time.
if let Some(panic) = self.panic_info.take() {
@@ -462,7 +459,6 @@ impl<T> EventLoop<T> {
/// happens, stops the `sharedApplication`
#[inline]
pub fn stop_app_on_panic<F: FnOnce() -> R + UnwindSafe, R>(
mtm: MainThreadMarker,
panic_info: Weak<PanicInfo>,
f: F,
) -> Option<R> {
@@ -477,11 +473,11 @@ pub fn stop_app_on_panic<F: FnOnce() -> R + UnwindSafe, R>(
let panic_info = panic_info.upgrade().unwrap();
panic_info.set_panic(e);
}
let app = NSApplication::sharedApplication(mtm);
let app = NSApp();
app.stop(None);
// Posting a dummy event to get `stop` to take effect immediately.
// See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752
app.postEvent_atStart(&dummy_event().unwrap(), true);
app.postEvent_atStart(&NSEvent::dummy(), true);
None
}
}

View File

@@ -210,45 +210,3 @@ extern "C" {
unicodeString: *mut UniChar,
) -> OSStatus;
}
// CGWindowLevel.h
//
// Note: There are two different things at play in this header:
// `CGWindowLevel` and `CGWindowLevelKey`.
//
// It seems like there was a push towards using "key" values instead of the
// raw window level values, and then you were supposed to use
// `CGWindowLevelForKey` to get the actual level.
//
// But the values that `NSWindowLevel` has are compiled in, and as such has
// to remain ABI compatible, so they're safe for us to hardcode as well.
#[allow(dead_code, non_upper_case_globals)]
mod window_level {
const kCGNumReservedWindowLevels: i32 = 16;
const kCGNumReservedBaseWindowLevels: i32 = 5;
pub const kCGBaseWindowLevel: i32 = i32::MIN;
pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels;
pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels;
pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20;
pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20;
pub const kCGBackstopMenuLevel: i32 = -20;
pub const kCGNormalWindowLevel: i32 = 0;
pub const kCGFloatingWindowLevel: i32 = 3;
pub const kCGTornOffMenuWindowLevel: i32 = 3;
pub const kCGModalPanelWindowLevel: i32 = 8;
pub const kCGUtilityWindowLevel: i32 = 19;
pub const kCGDockWindowLevel: i32 = 20;
pub const kCGMainMenuWindowLevel: i32 = 24;
pub const kCGStatusWindowLevel: i32 = 25;
pub const kCGPopUpMenuWindowLevel: i32 = 101;
pub const kCGOverlayWindowLevel: i32 = 102;
pub const kCGHelpWindowLevel: i32 = 200;
pub const kCGDraggingWindowLevel: i32 = 500;
pub const kCGScreenSaverWindowLevel: i32 = 1000;
pub const kCGAssistiveTechHighWindowLevel: i32 = 1500;
pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1;
}
pub use window_level::*;

View File

@@ -1,47 +1,43 @@
use icrate::AppKit::{
NSApplication, NSEventModifierFlagCommand, NSEventModifierFlagOption, NSEventModifierFlags,
NSMenu, NSMenuItem,
};
use icrate::Foundation::{ns_string, MainThreadMarker, NSProcessInfo, NSString};
use icrate::ns_string;
use icrate::Foundation::{NSProcessInfo, NSString};
use objc2::rc::Id;
use objc2::runtime::Sel;
use objc2::sel;
use super::appkit::{NSApp, NSEventModifierFlags, NSMenu, NSMenuItem};
struct KeyEquivalent<'a> {
key: &'a NSString,
masks: Option<NSEventModifierFlags>,
}
pub fn initialize(app: &NSApplication) {
let mtm = MainThreadMarker::from(app);
let menubar = NSMenu::new(mtm);
let app_menu_item = NSMenuItem::new(mtm);
pub fn initialize() {
let menubar = NSMenu::new();
let app_menu_item = NSMenuItem::new();
menubar.addItem(&app_menu_item);
let app_menu = NSMenu::new(mtm);
let app_menu = NSMenu::new();
let process_name = NSProcessInfo::processInfo().processName();
// About menu item
let about_item_title = ns_string!("About ").stringByAppendingString(&process_name);
let about_item = menu_item(
mtm,
&about_item_title,
Some(sel!(orderFrontStandardAboutPanel:)),
None,
);
// Services menu item
let services_menu = NSMenu::new(mtm);
let services_item = menu_item(mtm, ns_string!("Services"), None, None);
services_item.setSubmenu(Some(&services_menu));
let services_menu = NSMenu::new();
let services_item = menu_item(ns_string!("Services"), None, None);
services_item.setSubmenu(&services_menu);
// Seperator menu item
let sep_first = NSMenuItem::separatorItem(mtm);
let sep_first = NSMenuItem::separatorItem();
// Hide application menu item
let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name);
let hide_item = menu_item(
mtm,
&hide_item_title,
Some(sel!(hide:)),
Some(KeyEquivalent {
@@ -53,31 +49,30 @@ pub fn initialize(app: &NSApplication) {
// Hide other applications menu item
let hide_others_item_title = ns_string!("Hide Others");
let hide_others_item = menu_item(
mtm,
hide_others_item_title,
Some(sel!(hideOtherApplications:)),
Some(KeyEquivalent {
key: ns_string!("h"),
masks: Some(NSEventModifierFlagOption | NSEventModifierFlagCommand),
masks: Some(
NSEventModifierFlags::NSAlternateKeyMask | NSEventModifierFlags::NSCommandKeyMask,
),
}),
);
// Show applications menu item
let show_all_item_title = ns_string!("Show All");
let show_all_item = menu_item(
mtm,
show_all_item_title,
Some(sel!(unhideAllApplications:)),
None,
);
// Seperator menu item
let sep = NSMenuItem::separatorItem(mtm);
let sep = NSMenuItem::separatorItem();
// Quit application menu item
let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name);
let quit_item = menu_item(
mtm,
&quit_item_title,
Some(sel!(terminate:)),
Some(KeyEquivalent {
@@ -94,14 +89,14 @@ pub fn initialize(app: &NSApplication) {
app_menu.addItem(&show_all_item);
app_menu.addItem(&sep);
app_menu.addItem(&quit_item);
app_menu_item.setSubmenu(Some(&app_menu));
app_menu_item.setSubmenu(&app_menu);
unsafe { app.setServicesMenu(Some(&services_menu)) };
app.setMainMenu(Some(&menubar));
let app = NSApp();
app.setServicesMenu(&services_menu);
app.setMainMenu(&menubar);
}
fn menu_item(
mtm: MainThreadMarker,
title: &NSString,
selector: Option<Sel>,
key_equivalent: Option<KeyEquivalent<'_>>,
@@ -110,9 +105,7 @@ fn menu_item(
Some(ke) => (ke.key, ke.masks),
None => (ns_string!(""), None),
};
let item = unsafe {
NSMenuItem::initWithTitle_action_keyEquivalent(mtm.alloc(), title, selector, key)
};
let item = NSMenuItem::newWithTitle(title, selector, key);
if let Some(masks) = masks {
item.setKeyEquivalentModifierMask(masks)
}

View File

@@ -4,7 +4,7 @@ mod util;
mod app;
mod app_delegate;
mod app_state;
mod cursor;
mod appkit;
mod event;
mod event_loop;
mod ffi;
@@ -27,9 +27,8 @@ pub(crate) use self::{
};
use crate::event::DeviceId as RootDeviceId;
pub(crate) use self::cursor::CustomCursor as PlatformCustomCursor;
pub(crate) use self::window::{OwnedWindowHandle, Window};
pub(crate) use crate::cursor::OnlyCursorImageBuilder as PlatformCustomCursorBuilder;
pub(crate) use self::window::Window;
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

View File

@@ -10,10 +10,9 @@ use core_foundation::{
use core_graphics::display::{
CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode,
};
use icrate::AppKit::NSScreen;
use icrate::Foundation::{ns_string, MainThreadMarker, NSNumber};
use objc2::{rc::Id, runtime::AnyObject};
use objc2::rc::Id;
use super::appkit::NSScreen;
use super::ffi;
use crate::dpi::{PhysicalPosition, PhysicalSize};
@@ -211,12 +210,10 @@ impl MonitorHandle {
}
pub fn scale_factor(&self) -> f64 {
MainThreadMarker::run_on_main(|mtm| {
match self.ns_screen(mtm) {
Some(screen) => screen.backingScaleFactor() as f64,
None => 1.0, // default to 1.0 when we can't find the screen
}
})
match self.ns_screen() {
Some(screen) => screen.backingScaleFactor() as f64,
None => 1.0, // default to 1.0 when we can't find the screen
}
}
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
@@ -306,10 +303,10 @@ impl MonitorHandle {
}
}
pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Id<NSScreen>> {
pub(crate) fn ns_screen(&self) -> Option<Id<NSScreen>> {
let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) };
NSScreen::screens(mtm).into_iter().find(|screen| {
let other_native_id = get_display_id(screen);
NSScreen::screens().into_iter().find(|screen| {
let other_native_id = screen.display_id();
let other_uuid = unsafe {
ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID)
};
@@ -317,25 +314,3 @@ impl MonitorHandle {
})
}
}
pub(crate) fn get_display_id(screen: &NSScreen) -> u32 {
let key = ns_string!("NSScreenNumber");
objc2::rc::autoreleasepool(|_| {
let device_description = screen.deviceDescription();
// Retrieve the CGDirectDisplayID associated with this screen
//
// SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed
// to be an NSNumber. See documentation for `deviceDescription` for details:
// <https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc>
let obj = device_description
.get(key)
.expect("failed getting screen display id from device description");
let obj: *const AnyObject = obj;
let obj: *const NSNumber = obj.cast();
let obj: &NSNumber = unsafe { &*obj };
obj.as_u32()
})
}

View File

@@ -21,7 +21,6 @@ use core_foundation::runloop::{
CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate,
CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate,
};
use icrate::Foundation::MainThreadMarker;
unsafe fn control_flow_handler<F>(panic_info: *mut c_void, f: F)
where
@@ -37,8 +36,7 @@ where
// However we want to keep that weak reference around after the function.
std::mem::forget(info_from_raw);
let mtm = MainThreadMarker::new().unwrap();
stop_app_on_panic(mtm, Weak::clone(&panic_info), move || {
stop_app_on_panic(Weak::clone(&panic_info), move || {
let _ = &panic_info;
f(panic_info.0)
});

View File

@@ -1,27 +1,25 @@
#![allow(clippy::unnecessary_cast)]
use std::boxed::Box;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::ptr;
use std::ptr::NonNull;
use icrate::AppKit::{
NSApplication, NSCursor, NSEvent, NSEventPhaseBegan, NSEventPhaseCancelled,
NSEventPhaseChanged, NSEventPhaseEnded, NSEventPhaseMayBegin, NSResponder, NSTextInputClient,
NSTrackingRectTag, NSView,
};
use icrate::Foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying,
NSMutableAttributedString, NSObject, NSObjectProtocol, NSPoint, NSRange, NSRect, NSSize,
NSString, NSUInteger,
NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString,
NSObject, NSObjectProtocol, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
};
use objc2::declare::{Ivar, IvarDrop};
use objc2::rc::{Id, WeakId};
use objc2::runtime::{AnyObject, Sel};
use objc2::{
class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
};
use objc2::{class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType};
use super::cursor::{default_cursor, invisible_cursor};
use super::event::{code_to_key, code_to_location};
use super::event::{lalt_pressed, ralt_pressed};
use super::{
appkit::{
NSApp, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingRectTag,
NSView,
},
event::{code_to_key, code_to_location},
};
use crate::{
dpi::{LogicalPosition, LogicalSize},
event::{
@@ -51,7 +49,7 @@ impl Default for CursorState {
fn default() -> Self {
Self {
visible: true,
cursor: default_cursor(),
cursor: Default::default(),
}
}
}
@@ -142,50 +140,93 @@ pub struct ViewState {
marked_text: RefCell<Id<NSMutableAttributedString>>,
accepts_first_mouse: bool,
// Weak reference because the window keeps a strong reference to the view
_ns_window: WeakId<WinitWindow>,
}
declare_class!(
pub(super) struct WinitView;
#[derive(Debug)]
#[allow(non_snake_case)]
pub(super) struct WinitView {
// Weak reference because the window keeps a strong reference to the view
_ns_window: IvarDrop<Box<WeakId<WinitWindow>>, "__ns_window">,
state: IvarDrop<Box<ViewState>, "_state">,
}
mod ivars;
unsafe impl ClassType for WinitView {
#[inherits(NSResponder, NSObject)]
type Super = NSView;
type Mutability = mutability::MainThreadOnly;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitView";
}
impl DeclaredClass for WinitView {
type Ivars = ViewState;
unsafe impl WinitView {
#[method(initWithId:acceptsFirstMouse:)]
unsafe fn init_with_id(
this: *mut Self,
window: &WinitWindow,
accepts_first_mouse: bool,
) -> Option<NonNull<Self>> {
let this: Option<&mut Self> = unsafe { msg_send![super(this), init] };
this.map(|this| {
let state = ViewState {
accepts_first_mouse,
..Default::default()
};
Ivar::write(
&mut this._ns_window,
Box::new(WeakId::new(&window.retain())),
);
Ivar::write(&mut this.state, Box::new(state));
this.setPostsFrameChangedNotifications(true);
let notification_center: &AnyObject =
unsafe { msg_send![class!(NSNotificationCenter), defaultCenter] };
// About frame change
let frame_did_change_notification_name =
NSString::from_str("NSViewFrameDidChangeNotification");
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![
notification_center,
addObserver: &*this,
selector: sel!(frameDidChange:),
name: &*frame_did_change_notification_name,
object: &*this,
];
}
*this.state.input_source.borrow_mut() = this.current_input_source();
NonNull::from(this)
})
}
}
unsafe impl WinitView {
#[method(viewDidMoveToWindow)]
fn view_did_move_to_window(&self) {
trace_scope!("viewDidMoveToWindow");
if let Some(tracking_rect) = self.ivars().tracking_rect.take() {
if let Some(tracking_rect) = self.state.tracking_rect.take() {
self.removeTrackingRect(tracking_rect);
}
let rect = self.frame();
let tracking_rect = unsafe { self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) };
assert_ne!(tracking_rect, 0, "failed adding tracking rect");
self.ivars().tracking_rect.set(Some(tracking_rect));
let tracking_rect = self.add_tracking_rect(rect, false);
self.state.tracking_rect.set(Some(tracking_rect));
}
#[method(frameDidChange:)]
fn frame_did_change(&self, _event: &NSEvent) {
trace_scope!("frameDidChange:");
if let Some(tracking_rect) = self.ivars().tracking_rect.take() {
if let Some(tracking_rect) = self.state.tracking_rect.take() {
self.removeTrackingRect(tracking_rect);
}
let rect = self.frame();
let tracking_rect = unsafe { self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) };
assert_ne!(tracking_rect, 0, "failed adding tracking rect");
self.ivars().tracking_rect.set(Some(tracking_rect));
let tracking_rect = self.add_tracking_rect(rect, false);
self.state.tracking_rect.set(Some(tracking_rect));
// Emit resize event here rather than from windowDidResize because:
// 1. When a new window is created as a tab, the frame size may change without a window resize occurring.
@@ -200,7 +241,7 @@ declare_class!(
trace_scope!("drawRect:");
// It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`.
if let Some(window) = self.ivars()._ns_window.load() {
if let Some(window) = self._ns_window.load() {
AppState::handle_redraw(WindowId(window.id()));
}
@@ -229,12 +270,12 @@ declare_class!(
fn reset_cursor_rects(&self) {
trace_scope!("resetCursorRects");
let bounds = self.bounds();
let cursor_state = self.ivars().cursor_state.borrow();
let cursor_state = self.state.cursor_state.borrow();
// We correctly invoke `addCursorRect` only from inside `resetCursorRects`
if cursor_state.visible {
self.addCursorRect_cursor(bounds, &cursor_state.cursor);
self.addCursorRect(bounds, &cursor_state.cursor);
} else {
self.addCursorRect_cursor(bounds, &invisible_cursor());
self.addCursorRect(bounds, &NSCursor::invisible());
}
}
}
@@ -243,13 +284,13 @@ declare_class!(
#[method(hasMarkedText)]
fn has_marked_text(&self) -> bool {
trace_scope!("hasMarkedText");
self.ivars().marked_text.borrow().length() > 0
self.state.marked_text.borrow().length() > 0
}
#[method(markedRange)]
fn marked_range(&self) -> NSRange {
trace_scope!("markedRange");
let length = self.ivars().marked_text.borrow().length();
let length = self.state.marked_text.borrow().length();
if length > 0 {
NSRange::new(0, length)
} else {
@@ -292,19 +333,19 @@ declare_class!(
};
// Update marked text.
*self.ivars().marked_text.borrow_mut() = marked_text;
*self.state.marked_text.borrow_mut() = marked_text;
// Notify IME is active if application still doesn't know it.
if self.ivars().ime_state.get() == ImeState::Disabled {
*self.ivars().input_source.borrow_mut() = self.current_input_source();
if self.state.ime_state.get() == ImeState::Disabled {
*self.state.input_source.borrow_mut() = self.current_input_source();
self.queue_event(WindowEvent::Ime(Ime::Enabled));
}
if unsafe { self.hasMarkedText() } {
self.ivars().ime_state.set(ImeState::Preedit);
if self.hasMarkedText() {
self.state.ime_state.set(ImeState::Preedit);
} else {
// In case the preedit was cleared, set IME into the Ground state.
self.ivars().ime_state.set(ImeState::Ground);
self.state.ime_state.set(ImeState::Ground);
}
// Empty string basically means that there's no preedit, so indicate that by sending
@@ -322,15 +363,15 @@ declare_class!(
#[method(unmarkText)]
fn unmark_text(&self) {
trace_scope!("unmarkText");
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
*self.state.marked_text.borrow_mut() = NSMutableAttributedString::new();
let input_context = self.inputContext().expect("input context");
input_context.discardMarkedText();
self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None)));
if self.is_ime_enabled() {
// Leave the Preedit self.ivars()
self.ivars().ime_state.set(ImeState::Ground);
// Leave the Preedit self.state
self.state.ime_state.set(ImeState::Ground);
} else {
warn!("Expected to have IME enabled when receiving unmarkText");
}
@@ -369,9 +410,9 @@ declare_class!(
let content_rect = window.contentRectForFrameRect(window.frame());
let base_x = content_rect.origin.x as f64;
let base_y = (content_rect.origin.y + content_rect.size.height) as f64;
let x = base_x + self.ivars().ime_position.get().x;
let y = base_y - self.ivars().ime_position.get().y;
let LogicalSize { width, height } = self.ivars().ime_size.get();
let x = base_x + self.state.ime_position.get().x;
let y = base_y - self.state.ime_position.get().y;
let LogicalSize { width, height } = self.state.ime_size.get();
NSRect::new(NSPoint::new(x as _, y as _), NSSize::new(width, height))
}
@@ -393,10 +434,10 @@ declare_class!(
let is_control = string.chars().next().map_or(false, |c| c.is_control());
// Commit only if we have marked text.
if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control {
if self.hasMarkedText() && self.is_ime_enabled() && !is_control {
self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None)));
self.queue_event(WindowEvent::Ime(Ime::Commit(string)));
self.ivars().ime_state.set(ImeState::Commited);
self.state.ime_state.set(ImeState::Commited);
}
}
@@ -408,15 +449,15 @@ declare_class!(
// We shouldn't forward any character from just commited text, since we'll end up sending
// it twice with some IMEs like Korean one. We'll also always send `Enter` in that case,
// which is not desired given it was used to confirm IME input.
if self.ivars().ime_state.get() == ImeState::Commited {
if self.state.ime_state.get() == ImeState::Commited {
return;
}
self.ivars().forward_key_to_app.set(true);
self.state.forward_key_to_app.set(true);
if unsafe { self.hasMarkedText() } && self.ivars().ime_state.get() == ImeState::Preedit {
if self.hasMarkedText() && self.state.ime_state.get() == ImeState::Preedit {
// Leave preedit so that we also report the key-up for this key.
self.ivars().ime_state.set(ImeState::Ground);
self.state.ime_state.set(ImeState::Ground);
}
}
}
@@ -426,19 +467,19 @@ declare_class!(
fn key_down(&self, event: &NSEvent) {
trace_scope!("keyDown:");
{
let mut prev_input_source = self.ivars().input_source.borrow_mut();
let mut prev_input_source = self.state.input_source.borrow_mut();
let current_input_source = self.current_input_source();
if *prev_input_source != current_input_source && self.is_ime_enabled() {
*prev_input_source = current_input_source;
drop(prev_input_source);
self.ivars().ime_state.set(ImeState::Disabled);
self.state.ime_state.set(ImeState::Disabled);
self.queue_event(WindowEvent::Ime(Ime::Disabled));
}
}
// Get the characters from the event.
let old_ime_state = self.ivars().ime_state.get();
self.ivars().forward_key_to_app.set(false);
let old_ime_state = self.state.ime_state.get();
self.state.forward_key_to_app.set(false);
let event = replace_event(event, self.window().option_as_alt());
// The `interpretKeyEvents` function might call
@@ -447,32 +488,32 @@ declare_class!(
// we must send the `KeyboardInput` event during IME if it triggered
// `doCommandBySelector`. (doCommandBySelector means that the keyboard input
// is not handled by IME and should be handled by the application)
if self.ivars().ime_allowed.get() {
if self.state.ime_allowed.get() {
let events_for_nsview = NSArray::from_slice(&[&*event]);
unsafe { self.interpretKeyEvents(&events_for_nsview) };
// If the text was commited we must treat the next keyboard event as IME related.
if self.ivars().ime_state.get() == ImeState::Commited {
if self.state.ime_state.get() == ImeState::Commited {
// Remove any marked text, so normal input can continue.
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
*self.state.marked_text.borrow_mut() = NSMutableAttributedString::new();
}
}
self.update_modifiers(&event, false);
let had_ime_input = match self.ivars().ime_state.get() {
let had_ime_input = match self.state.ime_state.get() {
ImeState::Commited => {
// Allow normal input after the commit.
self.ivars().ime_state.set(ImeState::Ground);
self.state.ime_state.set(ImeState::Ground);
true
}
ImeState::Preedit => true,
// `key_down` could result in preedit clear, so compare old and current state.
_ => old_ime_state != self.ivars().ime_state.get(),
_ => old_ime_state != self.state.ime_state.get(),
};
if !had_ime_input || self.ivars().forward_key_to_app.get() {
let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }, None);
if !had_ime_input || self.state.forward_key_to_app.get() {
let key_event = create_key_event(&event, true, event.is_a_repeat(), None);
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: key_event,
@@ -490,7 +531,7 @@ declare_class!(
// We want to send keyboard input when we are currently in the ground state.
if matches!(
self.ivars().ime_state.get(),
self.state.ime_state.get(),
ImeState::Ground | ImeState::Disabled
) {
self.queue_event(WindowEvent::KeyboardInput {
@@ -534,15 +575,14 @@ declare_class!(
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
#[method(cancelOperation:)]
fn cancel_operation(&self, _sender: Option<&AnyObject>) {
let mtm = MainThreadMarker::from(self);
trace_scope!("cancelOperation:");
let event = NSApplication::sharedApplication(mtm)
let event = NSApp()
.currentEvent()
.expect("could not find current event");
self.update_modifiers(&event, false);
let event = create_key_event(&event, true, unsafe { event.isARepeat() }, None);
let event = create_key_event(&event, true, event.is_a_repeat(), None);
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
@@ -639,8 +679,8 @@ declare_class!(
self.mouse_motion(event);
let delta = {
let (x, y) = unsafe { (event.scrollingDeltaX(), event.scrollingDeltaY()) };
if unsafe { event.hasPreciseScrollingDeltas() } {
let (x, y) = (event.scrollingDeltaX(), event.scrollingDeltaY());
if event.hasPreciseScrollingDeltas() {
let delta = LogicalPosition::new(x, y).to_physical(self.scale_factor());
MouseScrollDelta::PixelDelta(delta)
} else {
@@ -652,19 +692,18 @@ declare_class!(
// be mutually exclusive anyhow, which is why the API is rather incoherent). If no momentum
// phase is recorded (or rather, the started/ended cases of the momentum phase) then we
// report the touch phase.
#[allow(non_upper_case_globals)]
let phase = match unsafe { event.momentumPhase() } {
NSEventPhaseMayBegin | NSEventPhaseBegan => {
let phase = match event.momentumPhase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
TouchPhase::Started
}
NSEventPhaseEnded | NSEventPhaseCancelled => {
NSEventPhase::NSEventPhaseEnded | NSEventPhase::NSEventPhaseCancelled => {
TouchPhase::Ended
}
_ => match unsafe { event.phase() } {
NSEventPhaseMayBegin | NSEventPhaseBegan => {
_ => match event.phase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
TouchPhase::Started
}
NSEventPhaseEnded | NSEventPhaseCancelled => {
NSEventPhase::NSEventPhaseEnded | NSEventPhase::NSEventPhaseCancelled => {
TouchPhase::Ended
}
_ => TouchPhase::Moved,
@@ -685,18 +724,17 @@ declare_class!(
fn magnify_with_event(&self, event: &NSEvent) {
trace_scope!("magnifyWithEvent:");
#[allow(non_upper_case_globals)]
let phase = match unsafe { event.phase() } {
NSEventPhaseBegan => TouchPhase::Started,
NSEventPhaseChanged => TouchPhase::Moved,
NSEventPhaseCancelled => TouchPhase::Cancelled,
NSEventPhaseEnded => TouchPhase::Ended,
let phase = match event.phase() {
NSEventPhase::NSEventPhaseBegan => TouchPhase::Started,
NSEventPhase::NSEventPhaseChanged => TouchPhase::Moved,
NSEventPhase::NSEventPhaseCancelled => TouchPhase::Cancelled,
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
_ => return,
};
self.queue_event(WindowEvent::TouchpadMagnify {
device_id: DEVICE_ID,
delta: unsafe { event.magnification() },
delta: event.magnification(),
phase,
});
}
@@ -714,18 +752,17 @@ declare_class!(
fn rotate_with_event(&self, event: &NSEvent) {
trace_scope!("rotateWithEvent:");
#[allow(non_upper_case_globals)]
let phase = match unsafe { event.phase() } {
NSEventPhaseBegan => TouchPhase::Started,
NSEventPhaseChanged => TouchPhase::Moved,
NSEventPhaseCancelled => TouchPhase::Cancelled,
NSEventPhaseEnded => TouchPhase::Ended,
let phase = match event.phase() {
NSEventPhase::NSEventPhaseBegan => TouchPhase::Started,
NSEventPhase::NSEventPhaseChanged => TouchPhase::Moved,
NSEventPhase::NSEventPhaseCancelled => TouchPhase::Cancelled,
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
_ => return,
};
self.queue_event(WindowEvent::TouchpadRotate {
device_id: DEVICE_ID,
delta: unsafe { event.rotation() },
delta: event.rotation(),
phase,
});
}
@@ -738,8 +775,8 @@ declare_class!(
self.queue_event(WindowEvent::TouchpadPressure {
device_id: DEVICE_ID,
pressure: unsafe { event.pressure() },
stage: unsafe { event.stage() } as i64,
pressure: event.pressure(),
stage: event.stage() as i64,
});
}
@@ -755,41 +792,20 @@ declare_class!(
#[method(acceptsFirstMouse:)]
fn accepts_first_mouse(&self, _event: &NSEvent) -> bool {
trace_scope!("acceptsFirstMouse:");
self.ivars().accepts_first_mouse
self.state.accepts_first_mouse
}
}
);
impl WinitView {
pub(super) fn new(window: &WinitWindow, accepts_first_mouse: bool) -> Id<Self> {
let mtm = MainThreadMarker::from(window);
let this = mtm.alloc().set_ivars(ViewState {
accepts_first_mouse,
_ns_window: WeakId::new(&window.retain()),
..Default::default()
});
let this: Id<Self> = unsafe { msg_send_id![super(this), init] };
this.setPostsFrameChangedNotifications(true);
let notification_center: &AnyObject =
unsafe { msg_send![class!(NSNotificationCenter), defaultCenter] };
// About frame change
let frame_did_change_notification_name =
NSString::from_str("NSViewFrameDidChangeNotification");
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![
notification_center,
addObserver: &*this,
selector: sel!(frameDidChange:),
name: &*frame_did_change_notification_name,
object: &*this,
];
msg_send_id![
Self::alloc(),
initWithId: window,
acceptsFirstMouse: accepts_first_mouse,
]
}
*this.ivars().input_source.borrow_mut() = this.current_input_source();
this
}
fn window(&self) -> Id<WinitWindow> {
@@ -798,10 +814,7 @@ impl WinitView {
// (which is incompatible with `frameDidChange:`)
//
// unsafe { msg_send_id![self, window] }
self.ivars()
._ns_window
.load()
.expect("view to have a window")
self._ns_window.load().expect("view to have a window")
}
fn window_id(&self) -> WindowId {
@@ -829,7 +842,7 @@ impl WinitView {
}
fn is_ime_enabled(&self) -> bool {
!matches!(self.ivars().ime_state.get(), ImeState::Disabled)
!matches!(self.state.ime_state.get(), ImeState::Disabled)
}
fn current_input_source(&self) -> String {
@@ -841,7 +854,7 @@ impl WinitView {
}
pub(super) fn set_cursor_icon(&self, icon: Id<NSCursor>) {
let mut cursor_state = self.ivars().cursor_state.borrow_mut();
let mut cursor_state = self.state.cursor_state.borrow_mut();
cursor_state.cursor = icon;
}
@@ -849,7 +862,7 @@ impl WinitView {
///
/// Returns whether the state changed.
pub(super) fn set_cursor_visible(&self, visible: bool) -> bool {
let mut cursor_state = self.ivars().cursor_state.borrow_mut();
let mut cursor_state = self.state.cursor_state.borrow_mut();
if visible != cursor_state.visible {
cursor_state.visible = visible;
true
@@ -859,19 +872,19 @@ impl WinitView {
}
pub(super) fn set_ime_allowed(&self, ime_allowed: bool) {
if self.ivars().ime_allowed.get() == ime_allowed {
if self.state.ime_allowed.get() == ime_allowed {
return;
}
self.ivars().ime_allowed.set(ime_allowed);
if self.ivars().ime_allowed.get() {
self.state.ime_allowed.set(ime_allowed);
if self.state.ime_allowed.get() {
return;
}
// Clear markedText
*self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new();
*self.state.marked_text.borrow_mut() = NSMutableAttributedString::new();
if self.ivars().ime_state.get() != ImeState::Disabled {
self.ivars().ime_state.set(ImeState::Disabled);
if self.state.ime_state.get() != ImeState::Disabled {
self.state.ime_state.set(ImeState::Disabled);
self.queue_event(WindowEvent::Ime(Ime::Disabled));
}
}
@@ -881,17 +894,17 @@ impl WinitView {
position: LogicalPosition<f64>,
size: LogicalSize<f64>,
) {
self.ivars().ime_position.set(position);
self.ivars().ime_size.set(size);
self.state.ime_position.set(position);
self.state.ime_size.set(size);
let input_context = self.inputContext().expect("input context");
input_context.invalidateCharacterCoordinates();
}
/// Reset modifiers and emit a synthetic ModifiersChanged event if deemed necessary.
pub(super) fn reset_modifiers(&self) {
if !self.ivars().modifiers.get().state().is_empty() {
self.ivars().modifiers.set(Modifiers::default());
self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get()));
if !self.state.modifiers.get().state().is_empty() {
self.state.modifiers.set(Modifiers::default());
self.queue_event(WindowEvent::ModifiersChanged(self.state.modifiers.get()));
}
}
@@ -900,8 +913,8 @@ impl WinitView {
use ElementState::{Pressed, Released};
let current_modifiers = event_mods(ns_event);
let prev_modifiers = self.ivars().modifiers.get();
self.ivars().modifiers.set(current_modifiers);
let prev_modifiers = self.state.modifiers.get();
self.state.modifiers.set(current_modifiers);
// This function was called form the flagsChanged event, which is triggered
// when the user presses/releases a modifier even if the same kind of modifier
@@ -912,8 +925,8 @@ impl WinitView {
// later will work though, since the flags are attached to the event and contain valid
// information.
'send_event: {
if is_flags_changed_event && unsafe { ns_event.keyCode() } != 0 {
let scancode = unsafe { ns_event.keyCode() };
if is_flags_changed_event && ns_event.key_code() != 0 {
let scancode = ns_event.key_code();
let physical_key = PhysicalKey::from_scancode(scancode as u32);
// We'll correct the `is_press` later.
@@ -930,7 +943,7 @@ impl WinitView {
event.location = code_to_location(physical_key);
let location_mask = ModLocationMask::from_location(event.location);
let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut();
let mut phys_mod_state = self.state.phys_modifiers.borrow_mut();
let phys_mod = phys_mod_state
.entry(key)
.or_insert(ModLocationMask::empty());
@@ -1008,7 +1021,7 @@ impl WinitView {
return;
}
self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get()));
self.queue_event(WindowEvent::ModifiersChanged(self.state.modifiers.get()));
}
fn mouse_click(&self, event: &NSEvent, button_state: ElementState) {
@@ -1024,7 +1037,7 @@ impl WinitView {
}
fn mouse_motion(&self, event: &NSEvent) {
let window_point = unsafe { event.locationInWindow() };
let window_point = event.locationInWindow();
let view_point = self.convertPoint_fromView(window_point, None);
let view_rect = self.frame();
@@ -1033,7 +1046,7 @@ impl WinitView {
|| view_point.x > view_rect.size.width
|| view_point.y > view_rect.size.height
{
let mouse_buttons_down = unsafe { NSEvent::pressedMouseButtons() };
let mouse_buttons_down = NSEvent::pressedMouseButtons();
if mouse_buttons_down == 0 {
// Point is outside of the client area (view) and no buttons are pressed
return;
@@ -1060,7 +1073,7 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
// For the other events, it's always set to 0.
// MacOS only defines the left, right and middle buttons, 3..=31 are left as generic buttons,
// but 3 and 4 are very commonly used as Back and Forward by hardware vendors and applications.
match unsafe { event.buttonNumber() } {
match event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
@@ -1076,35 +1089,30 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id<NSEvent> {
let ev_mods = event_mods(event).state;
let ignore_alt_characters = match option_as_alt {
OptionAsAlt::OnlyLeft if lalt_pressed(event) => true,
OptionAsAlt::OnlyRight if ralt_pressed(event) => true,
OptionAsAlt::OnlyLeft if event.lalt_pressed() => true,
OptionAsAlt::OnlyRight if event.ralt_pressed() => true,
OptionAsAlt::Both if ev_mods.alt_key() => true,
_ => false,
} && !ev_mods.control_key()
&& !ev_mods.super_key();
if ignore_alt_characters {
let ns_chars = unsafe {
event
.charactersIgnoringModifiers()
.expect("expected characters to be non-null")
};
let ns_chars = event
.charactersIgnoringModifiers()
.expect("expected characters to be non-null");
unsafe {
NSEvent::keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode(
event.r#type(),
event.locationInWindow(),
event.modifierFlags(),
event.timestamp(),
event.windowNumber(),
None,
&ns_chars,
&ns_chars,
event.isARepeat(),
event.keyCode(),
)
.unwrap()
}
NSEvent::keyEventWithType(
event.type_(),
event.locationInWindow(),
event.modifierFlags(),
event.timestamp(),
event.window_number(),
None,
&ns_chars,
&ns_chars,
event.is_a_repeat(),
event.key_code(),
)
} else {
event.copy()
}

View File

@@ -3,8 +3,11 @@
use std::collections::VecDeque;
use std::f64;
use std::ops;
use std::os::raw::c_void;
use std::ptr::NonNull;
use std::sync::{Mutex, MutexGuard};
use crate::cursor::CustomCursor;
use crate::{
dpi::{
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical,
@@ -15,13 +18,14 @@ use crate::{
platform::macos::{OptionAsAlt, WindowExtMacOS},
platform_impl::platform::{
app_state::AppState,
appkit::NSWindowOrderingMode,
event_loop::EventLoopWindowTarget,
ffi,
monitor::{self, MonitorHandle, VideoMode},
util,
view::WinitView,
window_delegate::WinitWindowDelegate,
Fullscreen, OsError, PlatformCustomCursor,
Fullscreen, OsError,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -29,30 +33,22 @@ use crate::{
},
};
use core_graphics::display::{CGDisplay, CGPoint};
use icrate::AppKit::{
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication,
NSApplicationPresentationAutoHideDock, NSApplicationPresentationAutoHideMenuBar,
NSApplicationPresentationFullScreen, NSApplicationPresentationHideDock,
NSApplicationPresentationHideMenuBar, NSApplicationPresentationOptions, NSBackingStoreBuffered,
NSColor, NSCriticalRequest, NSFilenamesPboardType, NSInformationalRequest, NSResponder,
NSScreen, NSView, NSWindow, NSWindowAbove, NSWindowCloseButton, NSWindowFullScreenButton,
NSWindowLevel, NSWindowMiniaturizeButton, NSWindowSharingNone, NSWindowSharingReadOnly,
NSWindowStyleMask, NSWindowStyleMaskBorderless, NSWindowStyleMaskClosable,
NSWindowStyleMaskFullSizeContentView, NSWindowStyleMaskMiniaturizable,
NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, NSWindowTabbingModePreferred,
NSWindowTitleHidden, NSWindowZoomButton,
};
use icrate::Foundation::{
CGFloat, MainThreadBound, MainThreadMarker, NSArray, NSCopying, NSObject, NSPoint, NSRect,
NSSize, NSString,
CGFloat, MainThreadBound, MainThreadMarker, NSArray, NSCopying, NSInteger, NSObject, NSPoint,
NSRect, NSSize, NSString,
};
use objc2::declare::{Ivar, IvarDrop};
use objc2::rc::{autoreleasepool, Id};
use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass};
use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType};
use super::cursor::cursor_from_icon;
use super::ffi::kCGFloatingWindowLevel;
use super::ffi::{kCGNormalWindowLevel, CGSMainConnectionID, CGSSetWindowBackgroundBlurRadius};
use super::monitor::get_display_id;
use super::appkit::{
NSApp, NSAppKitVersion, NSAppearance, NSApplicationPresentationOptions, NSBackingStoreType,
NSColor, NSCursor, NSFilenamesPboardType, NSRequestUserAttentionType, NSResponder, NSScreen,
NSView, NSWindow, NSWindowButton, NSWindowLevel, NSWindowSharingType, NSWindowStyleMask,
NSWindowTabbingMode, NSWindowTitleVisibility,
};
use super::ffi::CGSMainConnectionID;
use super::ffi::CGSSetWindowBackgroundBlurRadius;
pub(crate) struct Window {
window: MainThreadBound<Id<WinitWindow>>,
@@ -63,7 +59,7 @@ pub(crate) struct Window {
impl Drop for Window {
fn drop(&mut self) {
self.window
.get_on_main(|window| autoreleasepool(|_| window.close()))
.get_on_main(|window, _| autoreleasepool(|_| window.close()))
}
}
@@ -75,8 +71,7 @@ impl Window {
) -> Result<Self, RootOsError> {
let mtm = MainThreadMarker::new()
.expect("windows can only be created on the main thread on macOS");
let (window, _delegate) =
autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs, mtm))?;
let (window, _delegate) = autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs))?;
Ok(Window {
window: MainThreadBound::new(window, mtm),
_delegate: MainThreadBound::new(_delegate, mtm),
@@ -92,19 +87,7 @@ impl Window {
&self,
f: impl FnOnce(&WinitWindow) -> R + Send,
) -> R {
self.window.get_on_main(|window| f(window))
}
#[cfg(feature = "rwh_06")]
#[inline]
pub(crate) fn raw_window_handle_rwh_06(
&self,
) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
if let Some(mtm) = MainThreadMarker::new() {
Ok(self.window.get(mtm).raw_window_handle_rwh_06())
} else {
Err(rwh_06::HandleError::Unavailable)
}
self.window.get_on_main(|window, _mtm| f(window))
}
}
@@ -129,54 +112,6 @@ impl From<u64> for WindowId {
}
}
#[derive(Debug)]
pub(crate) struct OwnedWindowHandle {
ns_view: MainThreadBound<Id<NSView>>,
}
impl Clone for OwnedWindowHandle {
fn clone(&self) -> Self {
Self {
ns_view: MainThreadMarker::run_on_main(|mtm| {
MainThreadBound::new(self.ns_view.get(mtm).clone(), mtm)
}),
}
}
}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(handle: rwh_06::WindowHandle<'_>) -> Self {
let mtm =
MainThreadMarker::new().expect("can only have handles on macOS on the main thread");
let ns_view = match handle.as_raw() {
rwh_06::RawWindowHandle::AppKit(handle) => {
// SAFETY: Taking `WindowHandle<'_>` ensures that the pointer is valid.
// Unwrap is fine, since the pointer comes from `NonNull`.
unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap()
}
handle => panic!("invalid window handle {handle:?} on macOS"),
};
Self {
ns_view: MainThreadBound::new(ns_view, mtm),
}
}
#[cfg(feature = "rwh_06")]
pub(crate) fn raw_window_handle(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
if let Some(mtm) = MainThreadMarker::new() {
let window_handle = rwh_06::AppKitWindowHandle::new({
let ptr = Id::as_ptr(self.ns_view.get(mtm)) as *mut _;
std::ptr::NonNull::new(ptr).expect("Id<T> should never be null")
});
let handle = rwh_06::RawWindowHandle::AppKit(window_handle);
Ok(handle)
} else {
Err(rwh_06::HandleError::Unavailable)
}
}
}
#[derive(Clone)]
pub struct PlatformSpecificWindowBuilderAttributes {
pub movable_by_window_background: bool,
@@ -213,17 +148,52 @@ impl Default for PlatformSpecificWindowBuilderAttributes {
declare_class!(
#[derive(Debug)]
pub struct WinitWindow;
pub struct WinitWindow {
// TODO: Fix unnecessary boxing here
shared_state: IvarDrop<Box<Mutex<SharedState>>, "_shared_state">,
}
mod ivars;
unsafe impl ClassType for WinitWindow {
#[inherits(NSResponder, NSObject)]
type Super = NSWindow;
type Mutability = mutability::MainThreadOnly;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitWindow";
}
impl DeclaredClass for WinitWindow {
type Ivars = Mutex<SharedState>;
unsafe impl WinitWindow {
#[method(initWithContentRect:styleMask:state:)]
unsafe fn init(
this: *mut Self,
frame: NSRect,
mask: NSWindowStyleMask,
state: *mut c_void,
) -> Option<NonNull<Self>> {
let this: Option<&mut Self> = unsafe {
msg_send![
super(this),
initWithContentRect: frame,
styleMask: mask,
backing: NSBackingStoreType::NSBackingStoreBuffered,
defer: false,
]
};
this.map(|this| {
// SAFETY: The pointer originally came from `Box::into_raw`.
Ivar::write(&mut this.shared_state, unsafe {
Box::from_raw(state as *mut Mutex<SharedState>)
});
// It is imperative to correct memory management that we
// disable the extra release that would otherwise happen when
// calling `clone` on the window.
this.setReleasedWhenClosed(false);
NonNull::from(this)
})
}
}
unsafe impl WinitWindow {
@@ -324,7 +294,6 @@ impl WinitWindow {
fn new(
attrs: WindowAttributes,
pl_attrs: PlatformSpecificWindowBuilderAttributes,
mtm: MainThreadMarker,
) -> Result<(Id<Self>, Id<WinitWindowDelegate>), RootOsError> {
trace_scope!("WinitWindow::new");
@@ -332,15 +301,15 @@ impl WinitWindow {
let screen = match attrs.fullscreen.0.clone().map(Into::into) {
Some(Fullscreen::Borderless(Some(monitor)))
| Some(Fullscreen::Exclusive(VideoMode { monitor, .. })) => {
monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm))
monitor.ns_screen().or_else(NSScreen::main)
}
Some(Fullscreen::Borderless(None)) => NSScreen::mainScreen(mtm),
Some(Fullscreen::Borderless(None)) => NSScreen::main(),
None => None,
};
let frame = match &screen {
Some(screen) => screen.frame(),
None => {
let scale_factor = NSScreen::mainScreen(mtm)
let scale_factor = NSScreen::main()
.map(|screen| screen.backingScaleFactor() as f64)
.unwrap_or(1.0);
let (width, height) = match attrs.inner_size {
@@ -370,31 +339,31 @@ impl WinitWindow {
// if decorations is set to false, ignore pl_attrs
//
// if the titlebar is hidden, ignore other pl_attrs
NSWindowStyleMaskBorderless
| NSWindowStyleMaskResizable
| NSWindowStyleMaskMiniaturizable
NSWindowStyleMask::NSBorderlessWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
} else {
// default case, resizable window with titlebar and titlebar buttons
NSWindowStyleMaskClosable
| NSWindowStyleMaskMiniaturizable
| NSWindowStyleMaskResizable
| NSWindowStyleMaskTitled
NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSTitledWindowMask
};
if !attrs.resizable {
masks &= !NSWindowStyleMaskResizable;
masks &= !NSWindowStyleMask::NSResizableWindowMask;
}
if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) {
masks &= !NSWindowStyleMaskMiniaturizable;
masks &= !NSWindowStyleMask::NSMiniaturizableWindowMask;
}
if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) {
masks &= !NSWindowStyleMaskClosable;
masks &= !NSWindowStyleMask::NSClosableWindowMask;
}
if pl_attrs.fullsize_content_view {
masks |= NSWindowStyleMaskFullSizeContentView;
masks |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
}
let state = SharedState {
@@ -404,23 +373,18 @@ impl WinitWindow {
..Default::default()
};
let this = mtm.alloc().set_ivars(Mutex::new(state));
// Pass the state through FFI to the method declared on the class
let state_ptr: *mut c_void = Box::into_raw(Box::new(Mutex::new(state))).cast();
let this: Option<Id<Self>> = unsafe {
msg_send_id![
super(this),
WinitWindow::alloc(),
initWithContentRect: frame,
styleMask: masks,
backing: NSBackingStoreBuffered,
defer: false,
state: state_ptr,
]
};
let this = this?;
// It is very important for correct memory management that we
// disable the extra release that would otherwise happen when
// calling `close` on the window.
unsafe { this.setReleasedWhenClosed(false) };
let resize_increments = match attrs
.resize_increments
.map(|i| i.to_logical::<f64>(this.scale_factor()))
@@ -438,26 +402,26 @@ impl WinitWindow {
if let Some(identifier) = pl_attrs.tabbing_identifier {
this.setTabbingIdentifier(&NSString::from_str(&identifier));
this.setTabbingMode(NSWindowTabbingModePreferred);
this.setTabbingMode(NSWindowTabbingMode::NSWindowTabbingModePreferred);
}
if attrs.content_protected {
this.setSharingType(NSWindowSharingNone);
this.setSharingType(NSWindowSharingType::NSWindowSharingNone);
}
if pl_attrs.titlebar_transparent {
this.setTitlebarAppearsTransparent(true);
}
if pl_attrs.title_hidden {
this.setTitleVisibility(NSWindowTitleHidden);
this.setTitleVisibility(NSWindowTitleVisibility::Hidden);
}
if pl_attrs.titlebar_buttons_hidden {
for titlebar_button in &[
#[allow(deprecated)]
NSWindowFullScreenButton,
NSWindowMiniaturizeButton,
NSWindowCloseButton,
NSWindowZoomButton,
NSWindowButton::FullScreen,
NSWindowButton::Miniaturize,
NSWindowButton::Close,
NSWindowButton::Zoom,
] {
if let Some(button) = this.standardWindowButton(*titlebar_button) {
button.setHidden(true);
@@ -469,7 +433,7 @@ impl WinitWindow {
}
if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) {
if let Some(button) = this.standardWindowButton(NSWindowZoomButton) {
if let Some(button) = this.standardWindowButton(NSWindowButton::Zoom) {
button.setEnabled(false);
}
}
@@ -487,16 +451,25 @@ impl WinitWindow {
})
.ok_or_else(|| os_error!(OsError::CreationError("Couldn't create `NSWindow`")))?;
if let Some(parent_window) = attrs.parent_window {
let parent = parent_window.ns_view.get(mtm).window().ok_or_else(|| {
os_error!(OsError::CreationError(
"parent view should be installed in a window"
))
})?;
#[cfg(feature = "rwh_06")]
match attrs.parent_window.0 {
Some(rwh_06::RawWindowHandle::AppKit(handle)) => {
// SAFETY: Caller ensures the pointer is valid or NULL
// Unwrap is fine, since the pointer comes from `NonNull`.
let parent_view: Id<NSView> =
unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap();
let parent = parent_view.window().ok_or_else(|| {
os_error!(OsError::CreationError(
"parent view should be installed in a window"
))
})?;
// SAFETY: We know that there are no parent -> child -> parent cycles since the only place in `winit`
// where we allow making a window a child window is right here, just after it's been created.
unsafe { parent.addChildWindow_ordered(&this, NSWindowAbove) };
// SAFETY: We know that there are no parent -> child -> parent cycles since the only place in `winit`
// where we allow making a window a child window is right here, just after it's been created.
unsafe { parent.addChildWindow(&this, NSWindowOrderingMode::NSWindowAbove) };
}
Some(raw) => panic!("Invalid raw window handle {raw:?} on macOS"),
None => (),
}
let view = WinitView::new(&this, pl_attrs.accepts_first_mouse);
@@ -504,7 +477,6 @@ impl WinitWindow {
// The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until
// macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid
// always the default system value in favour of the user's code
#[allow(deprecated)]
view.setWantsBestResolutionOpenGLSurface(!pl_attrs.disallow_hidpi);
// On Mojave, views automatically become layer-backed shortly after being added to
@@ -512,17 +484,17 @@ impl WinitWindow {
// the view and its associated OpenGL context. To work around this, on Mojave we
// explicitly make the view layer-backed up front so that AppKit doesn't do it
// itself and break the association with its context.
if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 {
if NSAppKitVersion::current().floor() > NSAppKitVersion::NSAppKitVersionNumber10_12 {
view.setWantsLayer(true);
}
// Configure the new view as the "key view" for the window
this.setContentView(Some(&view));
this.setInitialFirstResponder(Some(&view));
this.setContentView(&view);
this.setInitialFirstResponder(&view);
if attrs.transparent {
this.setOpaque(false);
this.setBackgroundColor(Some(unsafe { &NSColor::clearColor() }));
this.setBackgroundColor(&NSColor::clear());
}
if attrs.blur {
@@ -545,13 +517,13 @@ impl WinitWindow {
match attrs.preferred_theme {
Some(theme) => {
set_ns_theme(Some(theme), mtm);
set_ns_theme(Some(theme));
let mut state = this.lock_shared_state("WinitWindow::new");
state.current_theme = Some(theme);
}
None => {
let mut state = this.lock_shared_state("WinitWindow::new");
state.current_theme = Some(get_ns_theme(mtm));
state.current_theme = Some(get_ns_theme());
}
}
@@ -583,10 +555,9 @@ impl WinitWindow {
Ok((this, delegate))
}
#[track_caller]
pub(super) fn view(&self) -> Id<WinitView> {
// SAFETY: The view inside WinitWindow is always `WinitView`
unsafe { Id::cast(self.contentView().unwrap()) }
unsafe { Id::cast(self.contentView()) }
}
#[track_caller]
@@ -594,14 +565,14 @@ impl WinitWindow {
&self,
called_from_fn: &'static str,
) -> SharedStateMutexGuard<'_> {
SharedStateMutexGuard::new(self.ivars().lock().unwrap(), called_from_fn)
SharedStateMutexGuard::new(self.shared_state.lock().unwrap(), called_from_fn)
}
fn set_style_mask(&self, mask: NSWindowStyleMask) {
self.setStyleMask(mask);
// If we don't do this, key handling will break
// (at least until the window is clicked again/etc.)
let _ = self.makeFirstResponder(Some(&self.contentView().unwrap()));
let _ = self.makeFirstResponder(Some(&self.contentView()));
}
}
@@ -622,7 +593,7 @@ impl WinitWindow {
// NOTE: in general we want to specify the blur radius, but the choice of 80
// should be a reasonable default.
let radius = if blur { 80 } else { 0 };
let window_number = unsafe { self.windowNumber() };
let window_number = self.windowNumber();
unsafe {
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, radius);
}
@@ -675,7 +646,7 @@ impl WinitWindow {
#[inline]
pub fn inner_size(&self) -> PhysicalSize<u32> {
let frame = self.contentView().unwrap().frame();
let frame = self.contentView().frame();
let logical: LogicalSize<f64> = (frame.size.width as f64, frame.size.height as f64).into();
let scale_factor = self.scale_factor();
logical.to_physical(scale_factor)
@@ -797,9 +768,9 @@ impl WinitWindow {
if !fullscreen {
let mut mask = self.styleMask();
if resizable {
mask |= NSWindowStyleMaskResizable;
mask |= NSWindowStyleMask::NSResizableWindowMask;
} else {
mask &= !NSWindowStyleMaskResizable;
mask &= !NSWindowStyleMask::NSResizableWindowMask;
}
self.set_style_mask(mask);
}
@@ -816,15 +787,15 @@ impl WinitWindow {
let mut mask = self.styleMask();
if buttons.contains(WindowButtons::CLOSE) {
mask |= NSWindowStyleMaskClosable;
mask |= NSWindowStyleMask::NSClosableWindowMask;
} else {
mask &= !NSWindowStyleMaskClosable;
mask &= !NSWindowStyleMask::NSClosableWindowMask;
}
if buttons.contains(WindowButtons::MINIMIZE) {
mask |= NSWindowStyleMaskMiniaturizable;
mask |= NSWindowStyleMask::NSMiniaturizableWindowMask;
} else {
mask &= !NSWindowStyleMaskMiniaturizable;
mask &= !NSWindowStyleMask::NSMiniaturizableWindowMask;
}
// This must happen before the button's "enabled" status has been set,
@@ -834,7 +805,7 @@ impl WinitWindow {
// We edit the button directly instead of using `NSResizableWindowMask`,
// since that mask also affect the resizability of the window (which is
// controllable by other means in `winit`).
if let Some(button) = self.standardWindowButton(NSWindowZoomButton) {
if let Some(button) = self.standardWindowButton(NSWindowButton::Zoom) {
button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE));
}
}
@@ -846,7 +817,7 @@ impl WinitWindow {
buttons |= WindowButtons::MINIMIZE;
}
if self
.standardWindowButton(NSWindowZoomButton)
.standardWindowButton(NSWindowButton::Zoom)
.map(|b| b.isEnabled())
.unwrap_or(true)
{
@@ -860,14 +831,14 @@ impl WinitWindow {
pub fn set_cursor_icon(&self, icon: CursorIcon) {
let view = self.view();
view.set_cursor_icon(cursor_from_icon(icon));
view.set_cursor_icon(NSCursor::from_icon(icon));
self.invalidateCursorRectsForView(&view);
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let view = self.view();
view.set_cursor_icon(cursor.0.clone());
view.set_cursor_icon(NSCursor::from_image(&cursor.inner));
self.invalidateCursorRectsForView(&view);
}
@@ -920,11 +891,8 @@ impl WinitWindow {
#[inline]
pub fn drag_window(&self) -> Result<(), ExternalError> {
let mtm = MainThreadMarker::from(self);
let event = NSApplication::sharedApplication(mtm)
.currentEvent()
.unwrap();
self.performWindowDragWithEvent(&event);
let event = NSApp().currentEvent();
self.performWindowDragWithEvent(event.as_deref());
Ok(())
}
@@ -947,8 +915,9 @@ impl WinitWindow {
// we make it resizable temporalily.
let curr_mask = self.styleMask();
let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
let needs_temp_mask = !mask_contains(curr_mask, required);
let required =
NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
let needs_temp_mask = !curr_mask.contains(required);
if needs_temp_mask {
self.set_style_mask(required);
}
@@ -969,9 +938,9 @@ impl WinitWindow {
.take()
.unwrap_or_else(|| self.styleMask());
if shared_state.resizable {
base_mask | NSWindowStyleMaskResizable
base_mask | NSWindowStyleMask::NSResizableWindowMask
} else {
base_mask & !NSWindowStyleMaskResizable
base_mask & !NSWindowStyleMask::NSResizableWindowMask
}
}
@@ -1002,7 +971,7 @@ impl WinitWindow {
if minimized {
self.miniaturize(Some(self));
} else {
unsafe { self.deminiaturize(Some(self)) };
self.deminiaturize(Some(self));
}
}
@@ -1013,7 +982,6 @@ impl WinitWindow {
#[inline]
pub fn set_maximized(&self, maximized: bool) {
let mtm = MainThreadMarker::from(self);
let is_zoomed = self.is_zoomed();
if is_zoomed == maximized {
return;
@@ -1032,14 +1000,17 @@ impl WinitWindow {
return;
}
if mask_contains(self.styleMask(), NSWindowStyleMaskResizable) {
if self
.styleMask()
.contains(NSWindowStyleMask::NSResizableWindowMask)
{
drop(shared_state);
// Just use the native zoom if resizable
self.zoom(None);
} else {
// if it's not resizable, we set the frame directly
let new_rect = if maximized {
let screen = NSScreen::mainScreen(mtm).expect("no screen found");
let screen = NSScreen::main().expect("no screen found");
screen.visibleFrame()
} else {
shared_state.saved_standard_frame()
@@ -1062,9 +1033,6 @@ impl WinitWindow {
#[inline]
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
let mut shared_state_lock = self.lock_shared_state("set_fullscreen");
if shared_state_lock.is_simple_fullscreen {
return;
@@ -1096,7 +1064,7 @@ impl WinitWindow {
}
Fullscreen::Exclusive(video_mode) => video_mode.monitor(),
}
.ns_screen(mtm)
.ns_screen()
.unwrap();
let old_screen = self.screen().unwrap();
@@ -1127,6 +1095,7 @@ impl WinitWindow {
let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken;
if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) {
let app = NSApp();
let mut shared_state_lock = self.lock_shared_state("set_fullscreen");
shared_state_lock.save_presentation_opts = Some(app.presentationOptions());
}
@@ -1184,7 +1153,7 @@ impl WinitWindow {
// Window level must be restored from `CGShieldingWindowLevel()
// + 1` back to normal in order for `toggleFullScreen` to do
// anything
window.setLevel(kCGNormalWindowLevel as NSWindowLevel);
window.setLevel(NSWindowLevel::Normal);
window.toggleFullScreen(None);
}
@@ -1194,8 +1163,9 @@ impl WinitWindow {
// set a normal style temporarily. The previous state will be
// restored in `WindowDelegate::window_did_exit_fullscreen`.
let curr_mask = self.styleMask();
let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
if !mask_contains(curr_mask, required) {
let required = NSWindowStyleMask::NSTitledWindowMask
| NSWindowStyleMask::NSResizableWindowMask;
if !curr_mask.contains(required) {
self.set_style_mask(required);
self.lock_shared_state("set_fullscreen").saved_style = Some(curr_mask);
}
@@ -1224,27 +1194,30 @@ impl WinitWindow {
// of the menu bar, and this looks broken, so we must make sure
// that the menu bar is disabled. This is done in the window
// delegate in `window:willUseFullScreenPresentationOptions:`.
let app = NSApp();
self.lock_shared_state("set_fullscreen")
.save_presentation_opts = Some(app.presentationOptions());
let presentation_options = NSApplicationPresentationFullScreen
| NSApplicationPresentationHideDock
| NSApplicationPresentationHideMenuBar;
let presentation_options =
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar;
app.setPresentationOptions(presentation_options);
let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1;
let window_level =
NSWindowLevel(unsafe { ffi::CGShieldingWindowLevel() } as NSInteger + 1);
self.setLevel(window_level);
}
(Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => {
let presentation_options = self
.lock_shared_state("set_fullscreen")
.save_presentation_opts
.unwrap_or(
NSApplicationPresentationFullScreen
| NSApplicationPresentationAutoHideDock
| NSApplicationPresentationAutoHideMenuBar,
);
app.setPresentationOptions(presentation_options);
.unwrap_or_else(|| {
NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar
});
NSApp().setPresentationOptions(presentation_options);
unsafe {
ffi::CGRestorePermanentDisplayConfiguration();
@@ -1256,7 +1229,7 @@ impl WinitWindow {
// Restore the normal window level following the Borderless fullscreen
// `CGShieldingWindowLevel() + 1` hack.
self.setLevel(kCGNormalWindowLevel as NSWindowLevel);
self.setLevel(NSWindowLevel::Normal);
}
_ => {}
};
@@ -1284,15 +1257,15 @@ impl WinitWindow {
let new_mask = {
let mut new_mask = if decorations {
NSWindowStyleMaskClosable
| NSWindowStyleMaskMiniaturizable
| NSWindowStyleMaskResizable
| NSWindowStyleMaskTitled
NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSTitledWindowMask
} else {
NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable
NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask
};
if !resizable {
new_mask &= !NSWindowStyleMaskResizable;
new_mask &= !NSWindowStyleMask::NSResizableWindowMask;
}
new_mask
};
@@ -1307,9 +1280,9 @@ impl WinitWindow {
#[inline]
pub fn set_window_level(&self, level: WindowLevel) {
let level = match level {
WindowLevel::AlwaysOnTop => kCGFloatingWindowLevel as NSWindowLevel,
WindowLevel::AlwaysOnBottom => (kCGNormalWindowLevel - 1) as NSWindowLevel,
WindowLevel::Normal => kCGNormalWindowLevel as NSWindowLevel,
WindowLevel::AlwaysOnTop => NSWindowLevel::Floating,
WindowLevel::AlwaysOnBottom => NSWindowLevel::BELOW_NORMAL,
WindowLevel::Normal => NSWindowLevel::Normal,
};
self.setLevel(level);
}
@@ -1344,33 +1317,30 @@ impl WinitWindow {
#[inline]
pub fn focus_window(&self) {
let mtm = MainThreadMarker::from(self);
let is_minimized = self.isMiniaturized();
let is_visible = self.isVisible();
if !is_minimized && is_visible {
#[allow(deprecated)]
NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true);
NSApp().activateIgnoringOtherApps(true);
self.makeKeyAndOrderFront(None);
}
}
#[inline]
pub fn request_user_attention(&self, request_type: Option<UserAttentionType>) {
let mtm = MainThreadMarker::from(self);
let ns_request_type = request_type.map(|ty| match ty {
UserAttentionType::Critical => NSCriticalRequest,
UserAttentionType::Informational => NSInformationalRequest,
UserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest,
UserAttentionType::Informational => NSRequestUserAttentionType::NSInformationalRequest,
});
if let Some(ty) = ns_request_type {
NSApplication::sharedApplication(mtm).requestUserAttention(ty);
NSApp().requestUserAttention(ty);
}
}
#[inline]
// Allow directly accessing the current monitor internally without unwrapping.
pub(crate) fn current_monitor_inner(&self) -> Option<MonitorHandle> {
let display_id = get_display_id(&*self.screen()?);
let display_id = self.screen()?.display_id();
Some(MonitorHandle::new(display_id))
}
@@ -1395,7 +1365,7 @@ impl WinitWindow {
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
let mut window_handle = rwh_04::AppKitHandle::empty();
window_handle.ns_window = self as *const Self as *mut _;
window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _;
window_handle.ns_view = Id::as_ptr(&self.contentView()) as *mut _;
rwh_04::RawWindowHandle::AppKit(window_handle)
}
@@ -1404,7 +1374,7 @@ impl WinitWindow {
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
let mut window_handle = rwh_05::AppKitWindowHandle::empty();
window_handle.ns_window = self as *const Self as *mut _;
window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _;
window_handle.ns_view = Id::as_ptr(&self.contentView()) as *mut _;
rwh_05::RawWindowHandle::AppKit(window_handle)
}
@@ -1416,12 +1386,12 @@ impl WinitWindow {
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let window_handle = rwh_06::AppKitWindowHandle::new({
let ptr = Id::as_ptr(&self.contentView().unwrap()) as *mut _;
let ptr = Id::as_ptr(&self.contentView()) as *mut _;
std::ptr::NonNull::new(ptr).expect("Id<T> should never be null")
});
rwh_06::RawWindowHandle::AppKit(window_handle)
Ok(rwh_06::RawWindowHandle::AppKit(window_handle))
}
#[cfg(feature = "rwh_06")]
@@ -1454,24 +1424,21 @@ impl WinitWindow {
}
pub fn set_theme(&self, theme: Option<Theme>) {
let mtm = MainThreadMarker::from(self);
set_ns_theme(theme, mtm);
self.lock_shared_state("set_theme").current_theme =
theme.or_else(|| Some(get_ns_theme(mtm)));
set_ns_theme(theme);
self.lock_shared_state("set_theme").current_theme = theme.or_else(|| Some(get_ns_theme()));
}
#[inline]
pub fn set_content_protected(&self, protected: bool) {
self.setSharingType(if protected {
NSWindowSharingNone
NSWindowSharingType::NSWindowSharingNone
} else {
NSWindowSharingReadOnly
NSWindowSharingType::NSWindowSharingReadOnly
})
}
pub fn title(&self) -> String {
let window: &NSWindow = self;
window.title().to_string()
self.title_().to_string()
}
pub fn reset_dead_keys(&self) {
@@ -1488,10 +1455,9 @@ impl WindowExtMacOS for WinitWindow {
#[inline]
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
let mtm = MainThreadMarker::from(self);
let mut shared_state_lock = self.lock_shared_state("set_simple_fullscreen");
let app = NSApplication::sharedApplication(mtm);
let app = NSApp();
let is_native_fullscreen = shared_state_lock.fullscreen.is_some();
let is_simple_fullscreen = shared_state_lock.is_simple_fullscreen;
@@ -1519,19 +1485,20 @@ impl WindowExtMacOS for WinitWindow {
// Simulate pre-Lion fullscreen by hiding the dock and menu bar
let presentation_options =
NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar;
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar;
app.setPresentationOptions(presentation_options);
// Hide the titlebar
self.toggle_style_mask(NSWindowStyleMaskTitled, false);
self.toggle_style_mask(NSWindowStyleMask::NSTitledWindowMask, false);
// Set the window frame to the screen frame size
let screen = self.screen().expect("expected screen to be available");
self.setFrame_display(screen.frame(), true);
// Fullscreen windows can't be resized, minimized, or moved
self.toggle_style_mask(NSWindowStyleMaskMiniaturizable, false);
self.toggle_style_mask(NSWindowStyleMaskResizable, false);
self.toggle_style_mask(NSWindowStyleMask::NSMiniaturizableWindowMask, false);
self.toggle_style_mask(NSWindowStyleMask::NSResizableWindowMask, false);
self.setMovable(false);
true
@@ -1580,20 +1547,24 @@ impl WindowExtMacOS for WinitWindow {
#[inline]
fn select_next_tab(&self) {
self.selectNextTab(None)
if let Some(group) = self.tabGroup() {
group.selectNextTab();
}
}
#[inline]
fn select_previous_tab(&self) {
unsafe { self.selectPreviousTab(None) }
if let Some(group) = self.tabGroup() {
group.selectPreviousTab()
}
}
#[inline]
fn select_tab_at_index(&self, index: usize) {
if let Some(group) = self.tabGroup() {
if let Some(windows) = unsafe { self.tabbedWindows() } {
if let Some(windows) = group.tabbedWindows() {
if index < windows.len() {
group.setSelectedWindow(Some(&windows[index]));
group.setSelectedWindow(&windows[index]);
}
}
}
@@ -1601,7 +1572,8 @@ impl WindowExtMacOS for WinitWindow {
#[inline]
fn num_tabs(&self) -> usize {
unsafe { self.tabbedWindows() }
self.tabGroup()
.and_then(|group| group.tabbedWindows())
.map(|windows| windows.len())
.unwrap_or(1)
}
@@ -1625,31 +1597,25 @@ impl WindowExtMacOS for WinitWindow {
}
}
fn mask_contains(mask: NSWindowStyleMask, value: NSWindowStyleMask) -> bool {
mask & value == value
}
pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme {
let app = NSApplication::sharedApplication(mtm);
pub(super) fn get_ns_theme() -> Theme {
let app = NSApp();
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
if !has_theme {
return Theme::Light;
}
let appearance = app.effectiveAppearance();
let name = appearance
.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[
NSString::from_str("NSAppearanceNameAqua"),
NSString::from_str("NSAppearanceNameDarkAqua"),
]))
.unwrap();
let name = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[
NSString::from_str("NSAppearanceNameAqua"),
NSString::from_str("NSAppearanceNameDarkAqua"),
]));
match &*name.to_string() {
"NSAppearanceNameDarkAqua" => Theme::Dark,
_ => Theme::Light,
}
}
fn set_ns_theme(theme: Option<Theme>, mtm: MainThreadMarker) {
let app = NSApplication::sharedApplication(mtm);
fn set_ns_theme(theme: Option<Theme>) {
let app = NSApp();
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
if has_theme {
let appearance = theme.map(|t| {
@@ -1657,7 +1623,7 @@ fn set_ns_theme(theme: Option<Theme>, mtm: MainThreadMarker) {
Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"),
Theme::Light => NSString::from_str("NSAppearanceNameAqua"),
};
NSAppearance::appearanceNamed(&name).unwrap()
NSAppearance::appearanceNamed(&name)
});
app.setAppearance(appearance.as_ref().map(|a| a.as_ref()));
}

View File

@@ -1,19 +1,16 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::Cell;
use std::ptr;
use std::ptr::{self, NonNull};
use icrate::AppKit::{
NSApplicationPresentationFullScreen, NSApplicationPresentationHideDock,
NSApplicationPresentationHideMenuBar, NSApplicationPresentationOptions, NSDraggingDestination,
NSFilenamesPboardType, NSPasteboard, NSWindowDelegate, NSWindowOcclusionStateVisible,
};
use icrate::Foundation::{MainThreadMarker, NSArray, NSObject, NSObjectProtocol, NSSize, NSString};
use icrate::Foundation::{NSArray, NSObject, NSSize, NSString};
use objc2::declare::{Ivar, IvarDrop};
use objc2::rc::{autoreleasepool, Id};
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2::{
class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
};
use objc2::runtime::AnyObject;
use objc2::{class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType};
use super::appkit::{
NSApplicationPresentationOptions, NSFilenamesPboardType, NSPasteboard, NSWindowOcclusionState,
};
use super::{
app_state::AppState,
util,
@@ -27,9 +24,7 @@ use crate::{
};
#[derive(Debug)]
pub(crate) struct State {
window: Id<WinitWindow>,
pub struct State {
// This is set when WindowBuilder::with_fullscreen was set,
// see comments of `window_did_fail_to_enter_fullscreen`
initial_fullscreen: Cell<bool>,
@@ -42,21 +37,72 @@ pub(crate) struct State {
}
declare_class!(
pub(crate) struct WinitWindowDelegate;
#[derive(Debug)]
pub(crate) struct WinitWindowDelegate {
window: IvarDrop<Id<WinitWindow>, "_window">,
// TODO: It may be possible for delegate methods to be called
// asynchronously, causing data races panics?
// TODO: Remove unnecessary boxing here
state: IvarDrop<Box<State>, "_state">,
}
mod ivars;
unsafe impl ClassType for WinitWindowDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "WinitWindowDelegate";
}
impl DeclaredClass for WinitWindowDelegate {
type Ivars = State;
unsafe impl WinitWindowDelegate {
#[method(initWithWindow:initialFullscreen:)]
unsafe fn init_with_winit(
this: *mut Self,
window: &WinitWindow,
initial_fullscreen: bool,
) -> Option<NonNull<Self>> {
let this: Option<&mut Self> = unsafe { msg_send![super(this), init] };
this.map(|this| {
let scale_factor = window.scale_factor();
Ivar::write(&mut this.window, window.retain());
Ivar::write(
&mut this.state,
Box::new(State {
initial_fullscreen: Cell::new(initial_fullscreen),
previous_position: Cell::new(None),
previous_scale_factor: Cell::new(scale_factor),
}),
);
if scale_factor != 1.0 {
this.queue_static_scale_factor_changed_event();
}
this.window.setDelegate(Some(this));
// Enable theme change event
let notification_center: Id<AnyObject> =
unsafe { msg_send_id![class!(NSDistributedNotificationCenter), defaultCenter] };
let notification_name =
NSString::from_str("AppleInterfaceThemeChangedNotification");
let _: () = unsafe {
msg_send![
&notification_center,
addObserver: &*this
selector: sel!(effectiveAppearanceDidChange:)
name: &*notification_name
object: ptr::null::<AnyObject>()
]
};
NonNull::from(this)
})
}
}
unsafe impl NSObjectProtocol for WinitWindowDelegate {}
unsafe impl NSWindowDelegate for WinitWindowDelegate {
// NSWindowDelegate + NSDraggingDestination protocols
unsafe impl WinitWindowDelegate {
#[method(windowShouldClose:)]
fn window_should_close(&self, _: Option<&AnyObject>) -> bool {
trace_scope!("windowShouldClose:");
@@ -71,7 +117,7 @@ declare_class!(
autoreleasepool(|_| {
// Since El Capitan, we need to be careful that delegate methods can't
// be called after the window closes.
self.ivars().window.setDelegate(None);
self.window.setDelegate(None);
});
self.queue_event(WindowEvent::Destroyed);
}
@@ -88,19 +134,16 @@ declare_class!(
trace_scope!("windowWillStartLiveResize:");
let increments = self
.ivars()
.window
.lock_shared_state("window_will_enter_fullscreen")
.resize_increments;
self.ivars().window.set_resize_increments_inner(increments);
self.window.set_resize_increments_inner(increments);
}
#[method(windowDidEndLiveResize:)]
fn window_did_end_live_resize(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidEndLiveResize:");
self.ivars()
.window
.set_resize_increments_inner(NSSize::new(1., 1.));
self.window.set_resize_increments_inner(NSSize::new(1., 1.));
}
// This won't be triggered if the move was part of a resize.
@@ -134,183 +177,11 @@ declare_class!(
// NSWindowDelegate, and as a result a tracked modifiers state can quite
// easily fall out of synchrony with reality. This requires us to emit
// a synthetic ModifiersChanged event when we lose focus.
self.ivars().window.view().reset_modifiers();
self.window.view().reset_modifiers();
self.queue_event(WindowEvent::Focused(false));
}
/// Invoked when before enter fullscreen
#[method(windowWillEnterFullScreen:)]
fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillEnterFullScreen:");
let mut shared_state = self
.ivars()
.window
.lock_shared_state("window_will_enter_fullscreen");
shared_state.maximized = self.ivars().window.is_zoomed();
let fullscreen = shared_state.fullscreen.as_ref();
match fullscreen {
// Exclusive mode sets the state in `set_fullscreen` as the user
// can't enter exclusive mode by other means (like the
// fullscreen button on the window decorations)
Some(Fullscreen::Exclusive(_)) => (),
// `window_will_enter_fullscreen` was triggered and we're already
// in fullscreen, so we must've reached here by `set_fullscreen`
// as it updates the state
Some(Fullscreen::Borderless(_)) => (),
// Otherwise, we must've reached fullscreen by the user clicking
// on the green fullscreen button. Update state!
None => {
let current_monitor = self.ivars().window.current_monitor_inner();
shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor))
}
}
shared_state.in_fullscreen_transition = true;
}
/// Invoked when before exit fullscreen
#[method(windowWillExitFullScreen:)]
fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillExitFullScreen:");
let mut shared_state = self
.ivars()
.window
.lock_shared_state("window_will_exit_fullscreen");
shared_state.in_fullscreen_transition = true;
}
#[method(window:willUseFullScreenPresentationOptions:)]
fn window_will_use_fullscreen_presentation_options(
&self,
_: Option<&AnyObject>,
proposed_options: NSApplicationPresentationOptions,
) -> NSApplicationPresentationOptions {
trace_scope!("window:willUseFullScreenPresentationOptions:");
// Generally, games will want to disable the menu bar and the dock. Ideally,
// this would be configurable by the user. Unfortunately because of our
// `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is
// placed on top of the menu bar in exclusive fullscreen mode. This looks
// broken so we always disable the menu bar in exclusive fullscreen. We may
// still want to make this configurable for borderless fullscreen. Right now
// we don't, for consistency. If we do, it should be documented that the
// user-provided options are ignored in exclusive fullscreen.
let mut options = proposed_options;
let shared_state = self
.ivars()
.window
.lock_shared_state("window_will_use_fullscreen_presentation_options");
if let Some(Fullscreen::Exclusive(_)) = shared_state.fullscreen {
options = NSApplicationPresentationFullScreen
| NSApplicationPresentationHideDock
| NSApplicationPresentationHideMenuBar;
}
options
}
/// Invoked when entered fullscreen
#[method(windowDidEnterFullScreen:)]
fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidEnterFullScreen:");
self.ivars().initial_fullscreen.set(false);
let mut shared_state = self
.ivars()
.window
.lock_shared_state("window_did_enter_fullscreen");
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
self.ivars().window.set_fullscreen(target_fullscreen);
}
}
/// Invoked when exited fullscreen
#[method(windowDidExitFullScreen:)]
fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidExitFullScreen:");
self.ivars().window.restore_state_from_fullscreen();
let mut shared_state = self
.ivars()
.window
.lock_shared_state("window_did_exit_fullscreen");
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
self.ivars().window.set_fullscreen(target_fullscreen);
}
}
/// Invoked when fail to enter fullscreen
///
/// When this window launch from a fullscreen app (e.g. launch from VS Code
/// terminal), it creates a new virtual destkop and a transition animation.
/// This animation takes one second and cannot be disable without
/// elevated privileges. In this animation time, all toggleFullscreen events
/// will be failed. In this implementation, we will try again by using
/// performSelector:withObject:afterDelay: until window_did_enter_fullscreen.
/// It should be fine as we only do this at initialzation (i.e with_fullscreen
/// was set).
///
/// From Apple doc:
/// In some cases, the transition to enter full-screen mode can fail,
/// due to being in the midst of handling some other animation or user gesture.
/// This method indicates that there was an error, and you should clean up any
/// work you may have done to prepare to enter full-screen mode.
#[method(windowDidFailToEnterFullScreen:)]
fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidFailToEnterFullScreen:");
let mut shared_state = self
.ivars()
.window
.lock_shared_state("window_did_fail_to_enter_fullscreen");
shared_state.in_fullscreen_transition = false;
shared_state.target_fullscreen = None;
if self.ivars().initial_fullscreen.get() {
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![
&*self.ivars().window,
performSelector: sel!(toggleFullScreen:),
withObject: ptr::null::<AnyObject>(),
afterDelay: 0.5,
];
};
} else {
self.ivars().window.restore_state_from_fullscreen();
}
}
// Invoked when the occlusion state of the window changes
#[method(windowDidChangeOcclusionState:)]
fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeOcclusionState:");
let visible = self.ivars().window.occlusionState() & NSWindowOcclusionStateVisible
== NSWindowOcclusionStateVisible;
self.queue_event(WindowEvent::Occluded(!visible));
}
#[method(windowDidChangeScreen:)]
fn window_did_change_screen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeScreen:");
let is_simple_fullscreen = self
.ivars()
.window
.lock_shared_state("window_did_change_screen")
.is_simple_fullscreen;
if is_simple_fullscreen {
if let Some(screen) = self.ivars().window.screen() {
self.ivars().window.setFrame_display(screen.frame(), true);
}
}
}
}
unsafe impl NSDraggingDestination for WinitWindowDelegate {
/// Invoked when the dragged image enters destination bounds or frame
#[method(draggingEntered:)]
fn dragging_entered(&self, sender: &NSObject) -> bool {
@@ -319,9 +190,7 @@ declare_class!(
use std::path::PathBuf;
let pb: Id<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb
.propertyListForType(unsafe { NSFilenamesPboardType })
.unwrap();
let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType });
let filenames: Id<NSArray<NSString>> = unsafe { Id::cast(filenames) };
filenames.into_iter().for_each(|file| {
@@ -347,9 +216,7 @@ declare_class!(
use std::path::PathBuf;
let pb: Id<NSPasteboard> = unsafe { msg_send_id![sender, draggingPasteboard] };
let filenames = pb
.propertyListForType(unsafe { NSFilenamesPboardType })
.unwrap();
let filenames = pb.propertyListForType(unsafe { NSFilenamesPboardType });
let filenames: Id<NSArray<NSString>> = unsafe { Id::cast(filenames) };
filenames.into_iter().for_each(|file| {
@@ -372,9 +239,153 @@ declare_class!(
trace_scope!("draggingExited:");
self.queue_event(WindowEvent::HoveredFileCancelled);
}
}
unsafe impl WinitWindowDelegate {
/// Invoked when before enter fullscreen
#[method(windowWillEnterFullScreen:)]
fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillEnterFullScreen:");
let mut shared_state = self
.window
.lock_shared_state("window_will_enter_fullscreen");
shared_state.maximized = self.window.is_zoomed();
let fullscreen = shared_state.fullscreen.as_ref();
match fullscreen {
// Exclusive mode sets the state in `set_fullscreen` as the user
// can't enter exclusive mode by other means (like the
// fullscreen button on the window decorations)
Some(Fullscreen::Exclusive(_)) => (),
// `window_will_enter_fullscreen` was triggered and we're already
// in fullscreen, so we must've reached here by `set_fullscreen`
// as it updates the state
Some(Fullscreen::Borderless(_)) => (),
// Otherwise, we must've reached fullscreen by the user clicking
// on the green fullscreen button. Update state!
None => {
let current_monitor = self.window.current_monitor_inner();
shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor))
}
}
shared_state.in_fullscreen_transition = true;
}
/// Invoked when before exit fullscreen
#[method(windowWillExitFullScreen:)]
fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowWillExitFullScreen:");
let mut shared_state = self.window.lock_shared_state("window_will_exit_fullscreen");
shared_state.in_fullscreen_transition = true;
}
#[method(window:willUseFullScreenPresentationOptions:)]
fn window_will_use_fullscreen_presentation_options(
&self,
_: Option<&AnyObject>,
proposed_options: NSApplicationPresentationOptions,
) -> NSApplicationPresentationOptions {
trace_scope!("window:willUseFullScreenPresentationOptions:");
// Generally, games will want to disable the menu bar and the dock. Ideally,
// this would be configurable by the user. Unfortunately because of our
// `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is
// placed on top of the menu bar in exclusive fullscreen mode. This looks
// broken so we always disable the menu bar in exclusive fullscreen. We may
// still want to make this configurable for borderless fullscreen. Right now
// we don't, for consistency. If we do, it should be documented that the
// user-provided options are ignored in exclusive fullscreen.
let mut options = proposed_options;
let shared_state = self
.window
.lock_shared_state("window_will_use_fullscreen_presentation_options");
if let Some(Fullscreen::Exclusive(_)) = shared_state.fullscreen {
options = NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
| NSApplicationPresentationOptions::NSApplicationPresentationHideDock
| NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar;
}
options
}
/// Invoked when entered fullscreen
#[method(windowDidEnterFullScreen:)]
fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidEnterFullScreen:");
self.state.initial_fullscreen.set(false);
let mut shared_state = self.window.lock_shared_state("window_did_enter_fullscreen");
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
self.window.set_fullscreen(target_fullscreen);
}
}
/// Invoked when exited fullscreen
#[method(windowDidExitFullScreen:)]
fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidExitFullScreen:");
self.window.restore_state_from_fullscreen();
let mut shared_state = self.window.lock_shared_state("window_did_exit_fullscreen");
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
self.window.set_fullscreen(target_fullscreen);
}
}
/// Invoked when fail to enter fullscreen
///
/// When this window launch from a fullscreen app (e.g. launch from VS Code
/// terminal), it creates a new virtual destkop and a transition animation.
/// This animation takes one second and cannot be disable without
/// elevated privileges. In this animation time, all toggleFullscreen events
/// will be failed. In this implementation, we will try again by using
/// performSelector:withObject:afterDelay: until window_did_enter_fullscreen.
/// It should be fine as we only do this at initialzation (i.e with_fullscreen
/// was set).
///
/// From Apple doc:
/// In some cases, the transition to enter full-screen mode can fail,
/// due to being in the midst of handling some other animation or user gesture.
/// This method indicates that there was an error, and you should clean up any
/// work you may have done to prepare to enter full-screen mode.
#[method(windowDidFailToEnterFullScreen:)]
fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidFailToEnterFullScreen:");
let mut shared_state = self
.window
.lock_shared_state("window_did_fail_to_enter_fullscreen");
shared_state.in_fullscreen_transition = false;
shared_state.target_fullscreen = None;
if self.state.initial_fullscreen.get() {
#[allow(clippy::let_unit_value)]
unsafe {
let _: () = msg_send![
&*self.window,
performSelector: sel!(toggleFullScreen:),
withObject: ptr::null::<AnyObject>(),
afterDelay: 0.5,
];
};
} else {
self.window.restore_state_from_fullscreen();
}
}
// Invoked when the occlusion state of the window changes
#[method(windowDidChangeOcclusionState:)]
fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeOcclusionState:");
self.queue_event(WindowEvent::Occluded(
!self
.window
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible),
))
}
// Observe theme change
#[method(effectiveAppearanceDidChange:)]
fn effective_appearance_did_change(&self, sender: Option<&AnyObject>) {
@@ -391,10 +402,8 @@ declare_class!(
#[method(effectiveAppearanceDidChangedOnMainThread:)]
fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) {
let mtm = MainThreadMarker::from(self);
let theme = get_ns_theme(mtm);
let theme = get_ns_theme();
let mut shared_state = self
.ivars()
.window
.lock_shared_state("effective_appearance_did_change");
let current_theme = shared_state.current_theme;
@@ -404,80 +413,71 @@ declare_class!(
self.queue_event(WindowEvent::ThemeChanged(theme));
}
}
#[method(windowDidChangeScreen:)]
fn window_did_change_screen(&self, _: Option<&AnyObject>) {
trace_scope!("windowDidChangeScreen:");
let is_simple_fullscreen = self
.window
.lock_shared_state("window_did_change_screen")
.is_simple_fullscreen;
if is_simple_fullscreen {
if let Some(screen) = self.window.screen() {
self.window.setFrame_display(screen.frame(), true);
}
}
}
}
);
impl WinitWindowDelegate {
pub fn new(window: &WinitWindow, initial_fullscreen: bool) -> Id<Self> {
let mtm = MainThreadMarker::from(window);
let scale_factor = window.scale_factor();
let this = mtm.alloc().set_ivars(State {
window: window.retain(),
initial_fullscreen: Cell::new(initial_fullscreen),
previous_position: Cell::new(None),
previous_scale_factor: Cell::new(scale_factor),
});
let this: Id<Self> = unsafe { msg_send_id![super(this), init] };
if scale_factor != 1.0 {
this.queue_static_scale_factor_changed_event();
}
window.setDelegate(Some(ProtocolObject::from_ref(&*this)));
// Enable theme change event
let notification_center: Id<AnyObject> =
unsafe { msg_send_id![class!(NSDistributedNotificationCenter), defaultCenter] };
let notification_name = NSString::from_str("AppleInterfaceThemeChangedNotification");
let _: () = unsafe {
msg_send![
&notification_center,
addObserver: &*this
selector: sel!(effectiveAppearanceDidChange:)
name: &*notification_name
object: ptr::null::<AnyObject>()
unsafe {
msg_send_id![
Self::alloc(),
initWithWindow: window,
initialFullscreen: initial_fullscreen,
]
};
this
}
}
pub(crate) fn queue_event(&self, event: WindowEvent) {
let event = Event::WindowEvent {
window_id: WindowId(self.ivars().window.id()),
window_id: WindowId(self.window.id()),
event,
};
AppState::queue_event(event);
}
fn queue_static_scale_factor_changed_event(&self) {
let scale_factor = self.ivars().window.scale_factor();
if scale_factor == self.ivars().previous_scale_factor.get() {
let scale_factor = self.window.scale_factor();
if scale_factor == self.state.previous_scale_factor.get() {
return;
};
self.ivars().previous_scale_factor.set(scale_factor);
self.state.previous_scale_factor.set(scale_factor);
let suggested_size = self.view_size();
AppState::queue_static_scale_factor_changed_event(
self.ivars().window.clone(),
self.window.clone(),
suggested_size.to_physical(scale_factor),
scale_factor,
);
}
fn emit_move_event(&self) {
let rect = self.ivars().window.frame();
let rect = self.window.frame();
let x = rect.origin.x as f64;
let y = util::bottom_left_to_top_left(rect);
if self.ivars().previous_position.get() != Some((x, y)) {
self.ivars().previous_position.set(Some((x, y)));
let scale_factor = self.ivars().window.scale_factor();
if self.state.previous_position.get() != Some((x, y)) {
self.state.previous_position.set(Some((x, y)));
let scale_factor = self.window.scale_factor();
let physical_pos = LogicalPosition::<f64>::from((x, y)).to_physical(scale_factor);
self.queue_event(WindowEvent::Moved(physical_pos));
}
}
fn view_size(&self) -> LogicalSize<f64> {
let size = self.ivars().window.contentView().unwrap().frame().size;
let size = self.window.contentView().frame().size;
LogicalSize::new(size.width as f64, size.height as f64)
}
}

View File

@@ -9,7 +9,7 @@ use crate::dpi::{PhysicalPosition, PhysicalSize};
pub use self::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget};
mod event_loop;
pub(crate) use self::window::{OwnedWindowHandle, Window};
pub use self::window::Window;
mod window;
struct RedoxSocket {
@@ -194,7 +194,6 @@ impl Display for OsError {
}
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursorBuilder;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]

View File

@@ -4,6 +4,7 @@ use std::{
};
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error,
platform_impl::Fullscreen,
@@ -12,8 +13,8 @@ use crate::{
};
use super::{
EventLoopWindowTarget, MonitorHandle, PlatformCustomCursor,
PlatformSpecificWindowBuilderAttributes, RedoxSocket, TimeSocket, WindowId, WindowProperties,
EventLoopWindowTarget, MonitorHandle, PlatformSpecificWindowBuilderAttributes, RedoxSocket,
TimeSocket, WindowId, WindowProperties,
};
// These values match the values uses in the `window_new` function in orbital:
@@ -25,18 +26,6 @@ const ORBITAL_FLAG_BORDERLESS: char = 'l';
const ORBITAL_FLAG_RESIZABLE: char = 'r';
const ORBITAL_FLAG_TRANSPARENT: char = 't';
#[derive(Debug, Clone)]
pub(crate) struct OwnedWindowHandle {}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(_handle: rwh_06::WindowHandle<'_>) -> Self {
// Parent windows are currently unsupported, though owned window
// handles would be implementable.
Self {}
}
}
pub struct Window {
window_socket: Arc<RedoxSocket>,
redraws: Arc<Mutex<VecDeque<WindowId>>>,
@@ -364,7 +353,7 @@ impl Window {
#[inline]
pub fn set_cursor_icon(&self, _: window::CursorIcon) {}
pub(crate) fn set_custom_cursor(&self, _: PlatformCustomCursor) {}
pub fn set_custom_cursor(&self, _: CustomCursor) {}
#[inline]
pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> {

View File

@@ -6,67 +6,65 @@ use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError
use std::sync::{Arc, Mutex};
use std::task::Poll;
// NOTE: This channel doesn't wake up when all senders or receivers are
// dropped. This is acceptable as long as it's only used in `Dispatcher`, which
// has it's own `Drop` behavior.
pub fn channel<T>() -> (AsyncSender<T>, AsyncReceiver<T>) {
let (sender, receiver) = mpsc::channel();
let shared = Arc::new(Shared {
let sender = Arc::new(Mutex::new(sender));
let inner = Arc::new(Inner {
closed: AtomicBool::new(false),
waker: AtomicWaker::new(),
});
let sender = AsyncSender(Arc::new(SenderInner {
sender: Mutex::new(sender),
shared: Arc::clone(&shared),
}));
let sender = AsyncSender {
sender,
inner: Arc::clone(&inner),
};
let receiver = AsyncReceiver {
receiver: Rc::new(receiver),
shared,
inner,
};
(sender, receiver)
}
pub struct AsyncSender<T>(Arc<SenderInner<T>>);
struct SenderInner<T> {
pub struct AsyncSender<T> {
// We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't
// be accessed on the main thread, as it could block. Additionally we need
// to wrap `Sender` in an `Arc` to make it clonable on the main thread without
// to wrap it in an `Arc` to make it clonable on the main thread without
// having to block.
sender: Mutex<Sender<T>>,
shared: Arc<Shared>,
sender: Arc<Mutex<Sender<T>>>,
inner: Arc<Inner>,
}
impl<T> AsyncSender<T> {
pub fn send(&self, event: T) -> Result<(), SendError<T>> {
self.0.sender.lock().unwrap().send(event)?;
self.0.shared.waker.wake();
self.sender.lock().unwrap().send(event)?;
self.inner.waker.wake();
Ok(())
}
}
impl<T> SenderInner<T> {
fn close(&self) {
self.shared.closed.store(true, Ordering::Relaxed);
self.shared.waker.wake();
pub fn close(&self) {
self.inner.closed.store(true, Ordering::Relaxed);
self.inner.waker.wake()
}
}
impl<T> Clone for AsyncSender<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<T> Drop for SenderInner<T> {
fn drop(&mut self) {
self.close();
Self {
sender: Arc::clone(&self.sender),
inner: Arc::clone(&self.inner),
}
}
}
pub struct AsyncReceiver<T> {
receiver: Rc<Receiver<T>>,
shared: Arc<Shared>,
inner: Arc<Inner>,
}
impl<T> AsyncReceiver<T> {
@@ -74,12 +72,12 @@ impl<T> AsyncReceiver<T> {
future::poll_fn(|cx| match self.receiver.try_recv() {
Ok(event) => Poll::Ready(Ok(event)),
Err(TryRecvError::Empty) => {
self.shared.waker.register(cx.waker());
self.inner.waker.register(cx.waker());
match self.receiver.try_recv() {
Ok(event) => Poll::Ready(Ok(event)),
Err(TryRecvError::Empty) => {
if self.shared.closed.load(Ordering::Relaxed) {
if self.inner.closed.load(Ordering::Relaxed) {
Poll::Ready(Err(RecvError))
} else {
Poll::Pending
@@ -106,18 +104,12 @@ impl<T> Clone for AsyncReceiver<T> {
fn clone(&self) -> Self {
Self {
receiver: Rc::clone(&self.receiver),
shared: Arc::clone(&self.shared),
inner: Arc::clone(&self.inner),
}
}
}
impl<T> Drop for AsyncReceiver<T> {
fn drop(&mut self) {
self.shared.closed.store(true, Ordering::Relaxed);
}
}
struct Shared {
struct Inner {
closed: AtomicBool,
waker: AtomicWaker,
}

View File

@@ -82,6 +82,12 @@ impl<T> Dispatcher<T> {
}
}
impl<T> Drop for Dispatcher<T> {
fn drop(&mut self) {
self.0.with_sender_data(|sender| sender.close())
}
}
pub struct DispatchRunner<T: 'static> {
wrapper: Wrapper<true, T, AsyncSender<Closure<T>>, Closure<T>>,
receiver: AsyncReceiver<Closure<T>>,

View File

@@ -3,7 +3,7 @@ mod dispatcher;
mod waker;
mod wrapper;
pub use self::channel::{channel, AsyncReceiver, AsyncSender};
use self::channel::{channel, AsyncReceiver, AsyncSender};
pub use self::dispatcher::{DispatchRunner, Dispatcher};
pub use self::waker::{Waker, WakerSpawner};
use self::wrapper::Wrapper;

View File

@@ -1,33 +1,22 @@
use std::{
cell::RefCell,
future,
hash::{Hash, Hasher},
mem,
ops::DerefMut,
rc::{self, Rc},
sync::{self, Arc},
task::{Poll, Waker},
cell::{Cell, RefCell},
ops::Deref,
rc::{Rc, Weak},
};
use crate::{
cursor::{BadImage, CursorImage},
platform_impl::platform::r#async,
};
use crate::cursor::{BadImage, CursorImage};
use cursor_icon::CursorIcon;
use once_cell::sync::Lazy;
use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
Blob, Document, HtmlCanvasElement, HtmlImageElement, ImageBitmap, ImageBitmapOptions,
Blob, Document, HtmlCanvasElement, ImageBitmap, ImageBitmapOptions,
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
};
use self::thread_safe::ThreadSafe;
use super::backend::Style;
use super::{backend::Style, r#async::AsyncSender, EventLoopWindowTarget};
#[derive(Debug)]
pub(crate) enum CustomCursorBuilder {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebCustomCursor {
Image(CursorImage),
Url {
url: String,
@@ -36,185 +25,72 @@ pub(crate) enum CustomCursorBuilder {
},
}
impl CustomCursorBuilder {
impl WebCustomCursor {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<CustomCursorBuilder, BadImage> {
Ok(CustomCursorBuilder::Image(CursorImage::from_rgba(
) -> Result<Self, BadImage> {
Ok(Self::Image(CursorImage::from_rgba(
rgba, width, height, hotspot_x, hotspot_y,
)?))
}
}
#[derive(Clone, Debug)]
pub struct CustomCursor(Arc<Inner>);
pub(super) fn build(
&self,
window: &Window,
document: &Document,
style: &Style,
previous: SelectedCursor,
cursor_visible: Rc<Cell<bool>>,
) -> SelectedCursor {
let previous = previous.into();
impl Hash for CustomCursor {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.0).hash(state);
}
}
impl PartialEq for CustomCursor {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for CustomCursor {}
impl CustomCursor {
pub(crate) fn build<T>(
builder: CustomCursorBuilder,
window_target: &EventLoopWindowTarget<T>,
) -> Self {
Lazy::force(&DROP_HANDLER);
Self(match builder {
CustomCursorBuilder::Image(image) => ImageState::from_rgba(
window_target.runner.window(),
window_target.runner.document().clone(),
&image,
),
CustomCursorBuilder::Url {
match self {
WebCustomCursor::Image(image) => SelectedCursor::Image(CursorImageState::from_image(
window,
document.clone(),
style.clone(),
image,
previous,
cursor_visible,
)),
WebCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
} => ImageState::from_url(url, hotspot_x, hotspot_y),
})
}
}
} => {
let value = previous.style_with_url(url, *hotspot_x, *hotspot_y);
#[derive(Debug)]
struct Inner(Option<ThreadSafe<RefCell<ImageState>>>);
if cursor_visible.get() {
style.set("cursor", &value);
}
static DROP_HANDLER: Lazy<AsyncSender<ThreadSafe<RefCell<ImageState>>>> = Lazy::new(|| {
let (sender, receiver) = r#async::channel();
wasm_bindgen_futures::spawn_local(async move { while receiver.next().await.is_ok() {} });
sender
});
impl Inner {
fn new() -> Arc<Self> {
Arc::new(Inner(Some(ThreadSafe::new(RefCell::new(
ImageState::Loading(None),
)))))
}
fn get(&self) -> &RefCell<ImageState> {
self.0
.as_ref()
.expect("value has accidently already been dropped")
.get()
}
}
impl Drop for Inner {
fn drop(&mut self) {
let value = self
.0
.take()
.expect("value has accidently already been dropped");
if !value.in_origin_thread() {
DROP_HANDLER
.send(value)
.expect("sender dropped in main thread")
}
}
}
#[derive(Debug)]
pub struct CursorState(Rc<RefCell<State>>);
impl CursorState {
pub fn new(style: Style) -> Self {
Self(Rc::new(RefCell::new(State {
style,
visible: true,
cursor: SelectedCursor::default(),
})))
}
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
let mut this = self.0.borrow_mut();
if let SelectedCursor::ImageLoading { state, .. } = &this.cursor {
if let ImageState::Loading(state) = state.get().borrow_mut().deref_mut() {
state.take();
}
}
this.cursor = SelectedCursor::Named(cursor);
this.set_style();
}
pub(crate) fn set_custom_cursor(&self, cursor: CustomCursor) {
let mut this = self.0.borrow_mut();
match cursor.0.get().borrow_mut().deref_mut() {
ImageState::Loading(state) => {
this.cursor = SelectedCursor::ImageLoading {
state: cursor.0.clone(),
previous: mem::take(&mut this.cursor).into(),
};
*state = Some(Rc::downgrade(&self.0));
}
ImageState::Failed => log::error!("tried to load invalid cursor"),
ImageState::Ready(image) => {
this.cursor = SelectedCursor::ImageReady(image.clone());
this.set_style();
SelectedCursor::Url {
style: value,
previous,
url: url.clone(),
hotspot_x: *hotspot_x,
hotspot_y: *hotspot_y,
}
}
}
}
pub fn set_cursor_visible(&self, visible: bool) {
let mut state = self.0.borrow_mut();
if !visible && state.visible {
state.visible = false;
state.style.set("cursor", "none");
} else if visible && !state.visible {
state.visible = true;
state.set_style();
}
}
}
#[derive(Debug)]
struct State {
style: Style,
visible: bool,
cursor: SelectedCursor,
}
impl State {
pub fn set_style(&self) {
if self.visible {
let value = match &self.cursor {
SelectedCursor::Named(icon) => icon.name(),
SelectedCursor::ImageLoading { previous, .. } => previous.style(),
SelectedCursor::ImageReady(image) => &image.style,
};
self.style.set("cursor", value);
}
}
}
#[derive(Debug)]
enum SelectedCursor {
pub enum SelectedCursor {
Named(CursorIcon),
ImageLoading {
state: Arc<Inner>,
Url {
style: String,
previous: Previous,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
ImageReady(Rc<Image>),
Image(Rc<RefCell<Option<CursorImageState>>>),
}
impl Default for SelectedCursor {
@@ -223,26 +99,71 @@ impl Default for SelectedCursor {
}
}
impl From<Previous> for SelectedCursor {
fn from(previous: Previous) -> Self {
match previous {
Previous::Named(icon) => Self::Named(icon),
Previous::Image(image) => Self::ImageReady(image),
}
impl SelectedCursor {
pub fn set_style(&self, style: &Style) {
let value = match self {
SelectedCursor::Named(icon) => icon.name(),
SelectedCursor::Url { style, .. } => style,
SelectedCursor::Image(image) => {
let image = image.borrow();
let value = match image.deref().as_ref().unwrap() {
CursorImageState::Loading { previous, .. } => previous.style(),
CursorImageState::Failed(previous) => previous.style(),
CursorImageState::Ready { style, .. } => style,
};
return style.set("cursor", value);
}
};
style.set("cursor", value);
}
}
#[derive(Debug)]
pub enum Previous {
Named(CursorIcon),
Image(Rc<Image>),
Url {
style: String,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
Image {
style: String,
image: WebCursorImage,
},
}
impl Previous {
fn style(&self) -> &str {
match self {
Previous::Named(icon) => icon.name(),
Previous::Image(image) => &image.style,
Previous::Url { style: url, .. } => url,
Previous::Image { style, .. } => style,
}
}
fn style_with_url(&self, new_url: &str, new_hotspot_x: u16, new_hotspot_y: u16) -> String {
match self {
Previous::Named(icon) => format!("url({new_url}) {new_hotspot_x} {new_hotspot_y}, {}", icon.name()),
Previous::Url {
url,
hotspot_x,
hotspot_y,
..
}
| Previous::Image {
image:
WebCursorImage {
data_url: url,
hotspot_x,
hotspot_y,
..
},
..
} => format!(
"url({new_url}) {new_hotspot_x} {new_hotspot_y}, url({url}) {hotspot_x} {hotspot_y}, auto",
),
}
}
}
@@ -251,34 +172,66 @@ impl From<SelectedCursor> for Previous {
fn from(value: SelectedCursor) -> Self {
match value {
SelectedCursor::Named(icon) => Self::Named(icon),
SelectedCursor::ImageLoading { previous, .. } => previous,
SelectedCursor::ImageReady(image) => Self::Image(image),
SelectedCursor::Url {
style,
url,
hotspot_x,
hotspot_y,
..
} => Self::Url {
style,
url,
hotspot_x,
hotspot_y,
},
SelectedCursor::Image(image) => {
match Rc::try_unwrap(image).unwrap().into_inner().unwrap() {
CursorImageState::Loading { previous, .. } => previous,
CursorImageState::Failed(previous) => previous,
CursorImageState::Ready {
style,
image: current,
..
} => Self::Image {
style,
image: current,
},
}
}
}
}
}
#[derive(Debug)]
enum ImageState {
Loading(Option<rc::Weak<RefCell<State>>>),
Failed,
Ready(Rc<Image>),
pub enum CursorImageState {
Loading {
style: Style,
cursor_visible: Rc<Cell<bool>>,
previous: Previous,
hotspot_x: u16,
hotspot_y: u16,
},
Failed(Previous),
Ready {
style: String,
image: WebCursorImage,
previous: Previous,
},
}
impl ImageState {
fn from_rgba(window: &Window, document: Document, image: &CursorImage) -> Arc<Inner> {
// 1. Create an `ImageData` from the RGBA data.
// 2. Create an `ImageBitmap` from the `ImageData`.
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
// 4. Create a `Blob` from the `HTMLCanvasElement`.
// 5. Create an object URL from the `Blob`.
// 6. Decode the image on an `HTMLImageElement` from the URL.
// 7. Change the `CursorState` if queued.
// 1. Create an `ImageData` from the RGBA data.
impl CursorImageState {
fn from_image(
window: &Window,
document: Document,
style: Style,
image: &CursorImage,
previous: Previous,
cursor_visible: Rc<Cell<bool>>,
) -> Rc<RefCell<Option<Self>>> {
// Can't create array directly when backed by SharedArrayBuffer.
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
#[cfg(target_feature = "atomics")]
// Can't share `SharedArrayBuffer` with `ImageData`.
let result = {
let image_data = {
use js_sys::{Uint8Array, Uint8ClampedArray};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
@@ -297,283 +250,115 @@ impl ImageState {
ImageDataExt::new(array, image.width as u32)
.map(JsValue::from)
.map(ImageData::unchecked_from_js)
.unwrap()
};
#[cfg(not(target_feature = "atomics"))]
let result = ImageData::new_with_u8_clamped_array(
let image_data = ImageData::new_with_u8_clamped_array(
wasm_bindgen::Clamped(&image.rgba),
image.width as u32,
);
let image_data = result.expect("found wrong image size");
)
.unwrap();
// 2. Create an `ImageBitmap` from the `ImageData`.
//
// We call `createImageBitmap()` before spawning the future,
// to not have to clone the image buffer.
let mut options = ImageBitmapOptions::new();
options.premultiply_alpha(PremultiplyAlpha::None);
let bitmap = JsFuture::from(
window
.create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options)
.expect("unexpected exception in `createImageBitmap()`"),
.unwrap(),
);
let this = Inner::new();
let state = Rc::new(RefCell::new(Some(Self::Loading {
style,
cursor_visible,
previous,
hotspot_x: image.hotspot_x,
hotspot_y: image.hotspot_y,
})));
wasm_bindgen_futures::spawn_local({
let weak = Arc::downgrade(&this);
let CursorImage {
width,
height,
hotspot_x,
hotspot_y,
..
} = *image;
let weak = Rc::downgrade(&state);
let CursorImage { width, height, .. } = *image;
async move {
// Keep checking if all references are dropped between every `await` call.
if weak.strong_count() == 0 {
return;
}
let bitmap: ImageBitmap = bitmap
.await
.expect("found invalid state in `ImageData`")
.unchecked_into();
let bitmap: ImageBitmap = bitmap.await.unwrap().unchecked_into();
if weak.strong_count() == 0 {
return;
}
let canvas: HtmlCanvasElement = document
.create_element("canvas")
.expect("invalid tag name")
.unchecked_into();
let canvas: HtmlCanvasElement =
document.create_element("canvas").unwrap().unchecked_into();
#[allow(clippy::disallowed_methods)]
canvas.set_width(width as u32);
#[allow(clippy::disallowed_methods)]
canvas.set_height(height as u32);
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
let context: ImageBitmapRenderingContext = canvas
.get_context("bitmaprenderer")
.expect("unexpected exception in `HTMLCanvasElement.getContext()`")
.expect("`bitmaprenderer` context unsupported")
.unwrap()
.unwrap()
.unchecked_into();
context.transfer_from_image_bitmap(&bitmap);
// 4. Create a `Blob` from the `HTMLCanvasElement`.
//
// To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done,
// we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted,
// but it would increase complexity and isn't possible in this case.
// Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately.
let value = Rc::new(RefCell::new(None));
let waker = Rc::new(RefCell::<Option<Waker>>::new(None));
let callback = Closure::once({
let value = value.clone();
let waker = waker.clone();
move |blob: Option<Blob>| {
*value.borrow_mut() = Some(blob);
if let Some(waker) = waker.borrow_mut().take() {
waker.wake();
}
}
});
canvas
.to_blob(callback.as_ref().unchecked_ref())
.expect("failed with `SecurityError` despite only source coming from memory");
let blob = future::poll_fn(|cx| {
if let Some(blob) = value.borrow_mut().take() {
Poll::Ready(blob)
} else {
*waker.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
})
.await;
let url = {
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get().borrow_mut();
let Some(blob) = blob else {
log::error!("creating custom cursor failed");
let ImageState::Loading(state) = this.deref_mut() else {
unreachable!("found invalid state");
};
let state = state.take();
*this = ImageState::Failed;
if let Some(state) = state.and_then(|weak| weak.upgrade()) {
let mut state = state.borrow_mut();
let SelectedCursor::ImageLoading { previous, .. } =
mem::take(&mut state.cursor)
else {
unreachable!("found invalid state");
thread_local! {
static CURRENT_STATE: RefCell<Option<Weak<RefCell<Option<CursorImageState>>>>> = RefCell::new(None);
// `HTMLCanvasElement.toBlob()` can't be interrupted. So we have to use a
// `Closure` that doesn't need to be garbage-collected.
static CALLBACK: Closure<dyn Fn(Option<Blob>)> = Closure::new(|blob| {
CURRENT_STATE.with(|weak| {
let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else {
return;
};
let mut state = state.borrow_mut();
// Extract old state.
let CursorImageState::Loading { style, cursor_visible, previous, hotspot_x, hotspot_y, .. } = state.take().unwrap() else {
unreachable!("found invalid state")
};
state.cursor = previous.into();
}
return;
};
let Some(blob) = blob else {
*state = Some(CursorImageState::Failed(previous));
return;
};
let data_url = Url::create_object_url_with_blob(&blob).unwrap();
// 5. Create an object URL from the `Blob`.
Url::create_object_url_with_blob(&blob)
.expect("unexpected exception in `URL.createObjectURL()`")
};
let value = previous.style_with_url(&data_url, hotspot_x, hotspot_y);
Self::decode(weak, url, true, hotspot_x, hotspot_y).await;
if cursor_visible.get() {
style.set("cursor", &value);
}
*state = Some(
CursorImageState::Ready {
style: value,
image: WebCursorImage{ data_url, hotspot_x, hotspot_y },
previous,
});
});
});
}
CURRENT_STATE.with(|state| *state.borrow_mut() = Some(weak));
CALLBACK
.with(|callback| canvas.to_blob(callback.as_ref().unchecked_ref()).unwrap());
}
});
this
}
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Arc<Inner> {
let this = Inner::new();
wasm_bindgen_futures::spawn_local(Self::decode(
Arc::downgrade(&this),
url,
false,
hotspot_x,
hotspot_y,
));
this
}
async fn decode(
weak: sync::Weak<Inner>,
url: String,
object: bool,
hotspot_x: u16,
hotspot_y: u16,
) {
if weak.strong_count() == 0 {
return;
}
// 6. Decode the image on an `HTMLImageElement` from the URL.
let image =
HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
image.set_src(&url);
let result = JsFuture::from(image.decode()).await;
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get().borrow_mut();
let ImageState::Loading(state) = this.deref_mut() else {
unreachable!("found invalid state");
};
let state = state.take();
if let Err(error) = result {
log::error!("creating custom cursor failed: {error:?}");
*this = ImageState::Failed;
if let Some(state) = state.and_then(|weak| weak.upgrade()) {
let mut state = state.borrow_mut();
let SelectedCursor::ImageLoading { previous, .. } = mem::take(&mut state.cursor)
else {
unreachable!("found invalid state");
};
state.cursor = previous.into();
}
return;
}
let image = Image::new(url, object, image, hotspot_x, hotspot_y);
// 7. Change the `CursorState` if queued.
if let Some(state) = state.and_then(|weak| weak.upgrade()) {
let mut state = state.borrow_mut();
state.cursor = SelectedCursor::ImageReady(image.clone());
state.set_style();
}
*this = ImageState::Ready(image);
state
}
}
#[derive(Debug)]
pub struct Image {
style: String,
url: String,
object: bool,
_image: HtmlImageElement,
pub struct WebCursorImage {
data_url: String,
hotspot_x: u16,
hotspot_y: u16,
}
impl Drop for Image {
impl Drop for WebCursorImage {
fn drop(&mut self) {
if self.object {
Url::revoke_object_url(&self.url)
.expect("unexpected exception in `URL.revokeObjectURL()`");
}
Url::revoke_object_url(&self.data_url).unwrap();
}
}
impl Image {
fn new(
url: String,
object: bool,
image: HtmlImageElement,
hotspot_x: u16,
hotspot_y: u16,
) -> Rc<Self> {
let style = format!("url({url}) {hotspot_x} {hotspot_y}, auto");
Rc::new(Self {
style,
url,
object,
_image: image,
})
}
}
mod thread_safe {
use std::mem;
use std::thread::{self, ThreadId};
#[derive(Debug)]
pub struct ThreadSafe<T> {
origin_thread: ThreadId,
value: T,
}
impl<T> ThreadSafe<T> {
pub fn new(value: T) -> Self {
Self {
origin_thread: thread::current().id(),
value,
}
}
pub fn get(&self) -> &T {
if self.origin_thread == thread::current().id() {
&self.value
} else {
panic!("value not accessible outside its origin thread")
}
}
pub fn in_origin_thread(&self) -> bool {
self.origin_thread == thread::current().id()
}
}
impl<T> Drop for ThreadSafe<T> {
fn drop(&mut self) {
if mem::needs_drop::<T>() && self.origin_thread != thread::current().id() {
panic!("value can't be dropped outside its origin thread")
}
}
}
unsafe impl<T> Send for ThreadSafe<T> {}
unsafe impl<T> Sync for ThreadSafe<T> {}
}

View File

@@ -83,7 +83,6 @@ impl<T> EventLoopWindowTarget<T> {
) {
let canvas_clone = canvas.clone();
let mut canvas = canvas.borrow_mut();
#[cfg(any(feature = "rwh_04", feature = "rwh_05"))]
canvas.set_attribute("data-raw-handle", &id.0.to_string());
canvas.on_touch_start(prevent_default);
@@ -648,8 +647,6 @@ impl<T> EventLoopWindowTarget<T> {
let runner = self.runner.clone();
canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id)));
canvas.on_context_menu(prevent_default);
}
pub fn available_monitors(&self) -> VecDequeIter<MonitorHandle> {

View File

@@ -35,12 +35,9 @@ pub(crate) use self::event_loop::{
EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes,
};
pub use self::monitor::{MonitorHandle, VideoMode};
pub(crate) use self::window::{
OwnedWindowHandle, PlatformSpecificWindowBuilderAttributes, Window, WindowId,
};
pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId};
pub(crate) use self::keyboard::KeyEventExtra;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;
pub(crate) use cursor::CustomCursor as PlatformCustomCursor;
pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder;
pub(crate) use cursor::WebCustomCursor as PlatformCustomCursor;

View File

@@ -1,13 +1,11 @@
use std::cell::Cell;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use smol_str::SmolStr;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent,
PointerEvent, WheelEvent,
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent,
};
use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
@@ -43,15 +41,13 @@ pub struct Canvas {
on_intersect: Option<IntersectionObserverHandle>,
animation_frame_handler: AnimationFrameHandler,
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_context_menu: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
}
pub struct Common {
pub window: web_sys::Window,
pub document: Document,
/// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained.
/// Note: this is read-only because we use a pointer to this for [`WindowHandle`](rwh_06::WindowHandle).
raw: Rc<HtmlCanvasElement>,
pub raw: HtmlCanvasElement,
style: Style,
old_size: Rc<Cell<PhysicalSize<u32>>>,
current_size: Rc<Cell<PhysicalSize<u32>>>,
@@ -103,7 +99,7 @@ impl Canvas {
let common = Common {
window: window.clone(),
document: document.clone(),
raw: Rc::new(canvas.clone()),
raw: canvas.clone(),
style,
old_size: Rc::default(),
current_size: Rc::default(),
@@ -154,7 +150,6 @@ impl Canvas {
on_intersect: None,
animation_frame_handler: AnimationFrameHandler::new(window),
on_touch_end: None,
on_context_menu: None,
})
}
@@ -443,17 +438,6 @@ impl Canvas {
self.animation_frame_handler.on_animation_frame(f)
}
pub(crate) fn on_context_menu(&mut self, prevent_default: bool) {
self.on_context_menu = Some(self.common.add_event(
"contextmenu",
move |event: PointerEvent| {
if prevent_default {
event.prevent_default();
}
},
));
}
pub fn request_fullscreen(&self) {
fullscreen::request_fullscreen(self.document(), self.raw());
}
@@ -527,7 +511,6 @@ impl Canvas {
self.on_intersect = None;
self.animation_frame_handler.cancel();
self.on_touch_end = None;
self.on_context_menu = None;
}
}
@@ -541,11 +524,7 @@ impl Common {
E: 'static + AsRef<web_sys::Event> + wasm_bindgen::convert::FromWasmAbi,
F: 'static + FnMut(E),
{
EventListenerHandle::new(self.raw.deref().clone(), event_name, Closure::new(handler))
}
pub fn raw(&self) -> &HtmlCanvasElement {
&self.raw
EventListenerHandle::new(self.raw.clone(), event_name, Closure::new(handler))
}
}

View File

@@ -2,8 +2,8 @@ use crate::dpi::LogicalPosition;
use crate::event::{MouseButton, MouseScrollDelta};
use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey};
use once_cell::unsync::OnceCell;
use smol_str::SmolStr;
use std::cell::OnceCell;
use std::convert::TryInto;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};

View File

@@ -1,6 +1,5 @@
use std::cell::OnceCell;
use js_sys::Promise;
use once_cell::unsync::OnceCell;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};

View File

@@ -117,7 +117,7 @@ impl PointerHandler {
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
{
let window = canvas_common.window.clone();
let canvas = canvas_common.raw().clone();
let canvas = canvas_common.raw.clone();
self.on_pointer_press = Some(canvas_common.add_event(
"pointerdown",
move |event: PointerEvent| {
@@ -174,7 +174,7 @@ impl PointerHandler {
B: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, ButtonsState, MouseButton),
{
let window = canvas_common.window.clone();
let canvas = canvas_common.raw().clone();
let canvas = canvas_common.raw.clone();
self.on_cursor_move = Some(canvas_common.add_event(
"pointermove",
move |event: PointerEvent| {

View File

@@ -1,5 +1,5 @@
use js_sys::{Function, Object, Promise, Reflect};
use std::cell::OnceCell;
use once_cell::unsync::OnceCell;
use std::time::Duration;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;

View File

@@ -1,3 +1,4 @@
use crate::cursor::CustomCursor;
use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
use crate::icon::Icon;
@@ -7,28 +8,15 @@ use crate::window::{
};
use crate::SendSyncWrapper;
use super::cursor::CursorState;
use super::cursor::SelectedCursor;
use super::r#async::Dispatcher;
use super::PlatformCustomCursor;
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
use web_sys::HtmlCanvasElement;
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub(crate) struct OwnedWindowHandle {}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(_handle: rwh_06::WindowHandle<'_>) -> Self {
// Parent windows are currently unsupported, though owned window
// handles would be implementable.
Self {}
}
}
pub struct Window {
inner: Dispatcher<Inner>,
}
@@ -37,7 +25,8 @@ pub struct Inner {
id: WindowId,
pub window: web_sys::Window,
canvas: Rc<RefCell<backend::Canvas>>,
cursor: CursorState,
selected_cursor: RefCell<SelectedCursor>,
cursor_visible: Rc<Cell<bool>>,
destroy_fn: Option<Box<dyn FnOnce()>>,
}
@@ -56,7 +45,6 @@ impl Window {
let canvas =
backend::Canvas::create(id, window.clone(), document.clone(), &attr, platform_attr)?;
let canvas = Rc::new(RefCell::new(canvas));
let cursor = CursorState::new(canvas.borrow().style().clone());
target.register(&canvas, id, prevent_default);
@@ -67,7 +55,8 @@ impl Window {
id,
window: window.clone(),
canvas,
cursor,
selected_cursor: Default::default(),
cursor_visible: Rc::new(Cell::new(true)),
destroy_fn: Some(destroy_fn),
};
@@ -96,22 +85,6 @@ impl Window {
.value()
.map(|inner| inner.canvas.borrow().raw().clone())
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
self.inner
.value()
.map(|inner| {
let canvas = inner.canvas.borrow();
// SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid.
let canvas: &wasm_bindgen::JsValue = canvas.raw();
let window_handle =
rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast());
rwh_06::RawWindowHandle::WebCanvas(window_handle)
})
.ok_or(rwh_06::HandleError::Unavailable)
}
}
impl Inner {
@@ -225,12 +198,24 @@ impl Inner {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
self.cursor.set_cursor_icon(cursor)
*self.selected_cursor.borrow_mut() = SelectedCursor::Named(cursor);
if self.cursor_visible.get() {
self.canvas.borrow().style().set("cursor", cursor.name());
}
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
self.cursor.set_custom_cursor(cursor)
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let canvas = self.canvas.borrow();
let new_cursor = cursor.inner.build(
canvas.window(),
canvas.document(),
canvas.style(),
self.selected_cursor.take(),
self.cursor_visible.clone(),
);
*self.selected_cursor.borrow_mut() = new_cursor;
}
#[inline]
@@ -256,7 +241,15 @@ impl Inner {
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
self.cursor.set_cursor_visible(visible)
if !visible && self.cursor_visible.get() {
self.canvas.borrow().style().set("cursor", "none");
self.cursor_visible.set(false);
} else if visible && !self.cursor_visible.get() {
self.selected_cursor
.borrow()
.set_style(self.canvas.borrow().style());
self.cursor_visible.set(true);
}
}
#[inline]
@@ -405,6 +398,13 @@ impl Inner {
rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty())
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let window_handle = rwh_06::WebWindowHandle::new(self.id.0);
Ok(rwh_06::RawWindowHandle::Web(window_handle))
}
#[cfg(feature = "rwh_06")]
#[inline]
pub fn raw_display_handle_rwh_06(

View File

@@ -9,8 +9,8 @@ use windows_sys::Win32::{
},
UI::{
HiDpi::{
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE,
DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, MDT_EFFECTIVE_DPI,
PROCESS_PER_MONITOR_DPI_AWARE,
},
WindowsAndMessaging::IsProcessDPIAware,
},
@@ -21,6 +21,8 @@ use crate::platform_impl::platform::util::{
SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT,
};
const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2: DPI_AWARENESS_CONTEXT = -4;
pub fn become_dpi_aware() {
static ENABLE_DPI_AWARENESS: Once = Once::new();
ENABLE_DPI_AWARENESS.call_once(|| {

View File

View File

@@ -21,7 +21,7 @@ use once_cell::sync::Lazy;
use windows_sys::Win32::{
Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE,
Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM},
Foundation::{BOOL, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM},
Graphics::Gdi::{
GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient,
ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE,
@@ -35,9 +35,13 @@ use windows_sys::Win32::{
Input::{
Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW},
KeyboardAndMouse::{
ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT,
MapVirtualKeyW, ReleaseCapture, SetCapture, TrackMouseEvent, MAPVK_VK_TO_VSC_EX,
TME_LEAVE, TRACKMOUSEEVENT, VK_NUMLOCK, VK_SHIFT,
},
Pointer::{
POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, POINTER_INFO,
POINTER_PEN_INFO, POINTER_TOUCH_INFO,
},
Pointer::{POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE},
Touch::{
CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE,
TOUCHEVENTF_UP, TOUCHINPUT,
@@ -50,19 +54,20 @@ use windows_sys::Win32::{
RegisterClassExW, RegisterWindowMessageA, SetCursor, SetTimer, SetWindowPos,
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_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,
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,
},
},
};
@@ -75,8 +80,8 @@ use crate::{
WindowEvent,
},
event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW},
keyboard::ModifiersState,
platform::pump_events::PumpStatus,
keyboard::{KeyCode, ModifiersState, PhysicalKey},
platform::{pump_events::PumpStatus, scancode::PhysicalKeyExtScancode},
platform_impl::platform::{
dark_mode::try_theme,
dpi::{become_dpi_aware, dpi_to_scale_factor},
@@ -98,6 +103,37 @@ use self::runner::RunnerState;
use super::{window::set_skip_taskbar, SelectedCursor};
type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
entriesCount: *mut u32,
pointerCount: *mut u32,
pointerInfo: *mut POINTER_INFO,
) -> BOOL;
type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL;
type GetPointerDeviceRects = unsafe extern "system" fn(
device: HANDLE,
pointerDeviceRect: *mut RECT,
displayRect: *mut RECT,
) -> BOOL;
type GetPointerTouchInfo =
unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL;
type GetPointerPenInfo =
unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL;
static GET_POINTER_FRAME_INFO_HISTORY: Lazy<Option<GetPointerFrameInfoHistory>> =
Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory));
static SKIP_POINTER_FRAME_MESSAGES: Lazy<Option<SkipPointerFrameMessages>> =
Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages));
static GET_POINTER_DEVICE_RECTS: Lazy<Option<GetPointerDeviceRects>> =
Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects));
static GET_POINTER_TOUCH_INFO: Lazy<Option<GetPointerTouchInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo));
static GET_POINTER_PEN_INFO: Lazy<Option<GetPointerPenInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo));
pub(crate) struct WindowData<T: 'static> {
pub window_state: Arc<Mutex<WindowState>>,
pub event_loop_runner: EventLoopRunnerShared<T>,
@@ -1795,9 +1831,9 @@ unsafe fn public_window_callback_inner<T: 'static>(
Some(SkipPointerFrameMessages),
Some(GetPointerDeviceRects),
) = (
*util::GET_POINTER_FRAME_INFO_HISTORY,
*util::SKIP_POINTER_FRAME_MESSAGES,
*util::GET_POINTER_DEVICE_RECTS,
*GET_POINTER_FRAME_INFO_HISTORY,
*SKIP_POINTER_FRAME_MESSAGES,
*GET_POINTER_DEVICE_RECTS,
) {
let pointer_id = super::loword(wparam as u32) as u32;
let mut entries_count = 0u32;
@@ -1879,7 +1915,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
let force = match pointer_info.pointerType {
PT_TOUCH => {
let mut touch_info = mem::MaybeUninit::uninit();
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe {
GetPointerTouchInfo(
pointer_info.pointerId,
@@ -1895,7 +1931,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
}
PT_PEN => {
let mut pen_info = mem::MaybeUninit::uninit();
util::GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| {
GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| {
match unsafe {
GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr())
} {
@@ -2462,17 +2498,105 @@ unsafe fn handle_raw_input<T: 'static>(userdata: &ThreadMsgTargetData<T>, data:
return;
}
if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) {
let state = if pressed { Pressed } else { Released };
userdata.send_event(Event::DeviceEvent {
device_id,
event: Key(RawKeyEvent {
physical_key,
state,
}),
});
let state = if pressed { Pressed } else { Released };
let extension = {
if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) {
0xE000
} else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) {
0xE100
} else {
0x0000
}
};
let scancode = if keyboard.MakeCode == 0 {
// In some cases (often with media keys) the device reports a scancode of 0 but a
// valid virtual key. In these cases we obtain the scancode from the virtual key.
unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 }
} else {
keyboard.MakeCode | extension
};
if scancode == 0xE11D || scancode == 0xE02A {
// At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing
// Ctrl+NumLock.
// This equvalence means that if the user presses Pause, the keyboard will emit two
// subsequent keypresses:
// 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100)
// 2, 0x0045 - Which on its own can be interpreted as Pause
//
// There's another combination which isn't quite an equivalence:
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing
// PrtSc (print screen) produces the following sequence:
// 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000)
// 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on
// its own it can be interpreted as PrtSc
//
// For this reason, if we encounter the first keypress, we simply ignore it, trusting
// that there's going to be another event coming, from which we can extract the
// appropriate key.
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
return;
}
let physical_key = if keyboard.VKey == VK_NUMLOCK {
// Historically, the NumLock and the Pause key were one and the same physical key.
// The user could trigger Pause by pressing Ctrl+NumLock.
// Now these are often physically separate and the two keys can be differentiated by
// checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045.
//
// However in this event, both keys are reported as 0x0045 even on modern hardware.
// Therefore we use the virtual key instead to determine whether it's a NumLock and
// set the KeyCode accordingly.
//
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
PhysicalKey::Code(KeyCode::NumLock)
} else {
PhysicalKey::from_scancode(scancode as u32)
};
if keyboard.VKey == VK_SHIFT {
if let PhysicalKey::Code(code) = physical_key {
match code {
KeyCode::NumpadDecimal
| KeyCode::Numpad0
| KeyCode::Numpad1
| KeyCode::Numpad2
| KeyCode::Numpad3
| KeyCode::Numpad4
| KeyCode::Numpad5
| KeyCode::Numpad6
| KeyCode::Numpad7
| KeyCode::Numpad8
| KeyCode::Numpad9 => {
// On Windows, holding the Shift key makes numpad keys behave as if NumLock
// wasn't active. The way this is exposed to applications by the system is that
// the application receives a fake key release event for the shift key at the
// moment when the numpad key is pressed, just before receiving the numpad key
// as well.
//
// The issue is that in the raw device event (here), the fake shift release
// event reports the numpad key as the scancode. Unfortunately, the event doesn't
// have any information to tell whether it's the left shift or the right shift
// that needs to get the fake release (or press) event so we don't forward this
// event to the application at all.
//
// For more on this, read the article by Raymond Chen, titled:
// "The shift key overrides NumLock"
// https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953
return;
}
_ => (),
}
}
}
userdata.send_event(Event::DeviceEvent {
device_id,
event: Key(RawKeyEvent {
physical_key,
state,
}),
});
}
}

View File

@@ -194,7 +194,7 @@ impl WinCursor {
}
}
pub(crate) fn new(image: &CursorImage) -> Result<Self, io::Error> {
pub fn new(image: &CursorImage) -> Result<Self, io::Error> {
let mut bgra = image.rgba.clone();
bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2));

View File

@@ -10,9 +10,9 @@ use windows_sys::Win32::{
UI::{
Input::Ime::{
ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext,
ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED,
ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM,
GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT,
ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM,
CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN,
IACE_DEFAULT,
},
WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED},
},
@@ -124,7 +124,7 @@ impl ImeContext {
left: x,
top: y,
right: x + width,
bottom: y + height,
bottom: y - height,
};
let candidate_form = CANDIDATEFORM {
dwIndex: 0,
@@ -132,16 +132,8 @@ impl ImeContext {
ptCurrentPos: POINT { x, y },
rcArea: rc_area,
};
let composition_form = COMPOSITIONFORM {
dwStyle: CFS_POINT,
ptCurrentPos: POINT { x, y: y + height },
rcArea: rc_area,
};
unsafe {
ImmSetCompositionWindow(self.himc, &composition_form);
ImmSetCandidateWindow(self.himc, &candidate_form);
}
unsafe { ImmSetCandidateWindow(self.himc, &candidate_form) };
}
pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) {

View File

@@ -12,12 +12,11 @@ pub(crate) use self::{
},
icon::{SelectedCursor, WinIcon},
monitor::{MonitorHandle, VideoMode},
window::{OwnedWindowHandle, Window},
window::Window,
};
pub use self::icon::WinIcon as PlatformIcon;
pub(crate) use crate::cursor::OnlyCursorImage as PlatformCustomCursor;
pub(crate) use crate::cursor::OnlyCursorImageBuilder as PlatformCustomCursorBuilder;
pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor;
use crate::platform_impl::Fullscreen;
use crate::event::DeviceId as RootDeviceId;

View File

@@ -11,29 +11,21 @@ use windows_sys::Win32::{
UI::{
Input::{
GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList,
KeyboardAndMouse::{MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT},
RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST,
RAWINPUTHEADER, RAWKEYBOARD, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE,
RIDI_DEVICEINFO, RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID,
RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID,
RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
RAWINPUTHEADER, RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, RIDI_DEVICEINFO,
RIDI_DEVICENAME, RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD,
RID_DEVICE_INFO_MOUSE, RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
},
WindowsAndMessaging::{
RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP,
RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN,
RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP,
RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP,
RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, RI_MOUSE_BUTTON_2_DOWN,
RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP,
RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP, RI_MOUSE_BUTTON_5_DOWN,
RI_MOUSE_BUTTON_5_UP,
},
},
};
use crate::{
event::ElementState,
event_loop::DeviceEvents,
keyboard::{KeyCode, PhysicalKey},
platform::scancode::PhysicalKeyExtScancode,
platform_impl::platform::util,
};
use crate::{event::ElementState, event_loop::DeviceEvents, platform_impl::platform::util};
#[allow(dead_code)]
pub fn get_raw_input_device_list() -> Option<Vec<RAWINPUTDEVICELIST>> {
@@ -228,99 +220,3 @@ pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option<ElementState>; 5
button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP),
]
}
pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option<PhysicalKey> {
let extension = {
if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) {
0xE000
} else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) {
0xE100
} else {
0x0000
}
};
let scancode = if keyboard.MakeCode == 0 {
// In some cases (often with media keys) the device reports a scancode of 0 but a
// valid virtual key. In these cases we obtain the scancode from the virtual key.
unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 }
} else {
keyboard.MakeCode | extension
};
if scancode == 0xE11D || scancode == 0xE02A {
// At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing
// Ctrl+NumLock.
// This equvalence means that if the user presses Pause, the keyboard will emit two
// subsequent keypresses:
// 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100)
// 2, 0x0045 - Which on its own can be interpreted as Pause
//
// There's another combination which isn't quite an equivalence:
// PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing
// PrtSc (print screen) produces the following sequence:
// 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000)
// 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on
// its own it can be interpreted as PrtSc
//
// For this reason, if we encounter the first keypress, we simply ignore it, trusting
// that there's going to be another event coming, from which we can extract the
// appropriate key.
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
return None;
}
let physical_key = if keyboard.VKey == VK_NUMLOCK {
// Historically, the NumLock and the Pause key were one and the same physical key.
// The user could trigger Pause by pressing Ctrl+NumLock.
// Now these are often physically separate and the two keys can be differentiated by
// checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045.
//
// However in this event, both keys are reported as 0x0045 even on modern hardware.
// Therefore we use the virtual key instead to determine whether it's a NumLock and
// set the KeyCode accordingly.
//
// For more on this, read the article by Raymond Chen, titled:
// "Why does Ctrl+ScrollLock cancel dialogs?"
// https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503
PhysicalKey::Code(KeyCode::NumLock)
} else {
PhysicalKey::from_scancode(scancode as u32)
};
if keyboard.VKey == VK_SHIFT {
if let PhysicalKey::Code(code) = physical_key {
match code {
KeyCode::NumpadDecimal
| KeyCode::Numpad0
| KeyCode::Numpad1
| KeyCode::Numpad2
| KeyCode::Numpad3
| KeyCode::Numpad4
| KeyCode::Numpad5
| KeyCode::Numpad6
| KeyCode::Numpad7
| KeyCode::Numpad8
| KeyCode::Numpad9 => {
// On Windows, holding the Shift key makes numpad keys behave as if NumLock
// wasn't active. The way this is exposed to applications by the system is that
// the application receives a fake key release event for the shift key at the
// moment when the numpad key is pressed, just before receiving the numpad key
// as well.
//
// The issue is that in the raw device event (here), the fake shift release
// event reports the numpad key as the scancode. Unfortunately, the event doesn't
// have any information to tell whether it's the left shift or the right shift
// that needs to get the fake release (or press) event so we don't forward this
// event to the application at all.
//
// For more on this, read the article by Raymond Chen, titled:
// "The shift key overrides NumLock"
// https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953
return None;
}
_ => (),
}
}
}
Some(physical_key)
}

View File

@@ -13,7 +13,7 @@ use once_cell::sync::Lazy;
use windows_sys::{
core::{HRESULT, PCWSTR},
Win32::{
Foundation::{BOOL, HANDLE, HMODULE, HWND, RECT},
Foundation::{BOOL, HMODULE, HWND, RECT},
Graphics::Gdi::{ClientToScreen, HMONITOR},
System::{
LibraryLoader::{GetProcAddress, LoadLibraryA},
@@ -21,10 +21,7 @@ use windows_sys::{
},
UI::{
HiDpi::{DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS},
Input::{
KeyboardAndMouse::GetActiveWindow,
Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO},
},
Input::KeyboardAndMouse::GetActiveWindow,
WindowsAndMessaging::{
ClipCursor, GetClientRect, GetClipCursor, GetSystemMetrics, GetWindowPlacement,
GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS,
@@ -194,9 +191,7 @@ pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR {
}
}
// Helper function to dynamically load function pointer as some functions
// may not be available on all Windows platforms supported by winit.
//
// Helper function to dynamically load function pointer.
// `library` and `function` must be zero-terminated.
pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> {
assert_eq!(library.chars().last(), Some('\0'));
@@ -242,26 +237,6 @@ pub type AdjustWindowRectExForDpi = unsafe extern "system" fn(
dpi: u32,
) -> BOOL;
pub type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
entriesCount: *mut u32,
pointerCount: *mut u32,
pointerInfo: *mut POINTER_INFO,
) -> BOOL;
pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL;
pub type GetPointerDeviceRects = unsafe extern "system" fn(
device: HANDLE,
pointerDeviceRect: *mut RECT,
displayRect: *mut RECT,
) -> BOOL;
pub type GetPointerTouchInfo =
unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL;
pub type GetPointerPenInfo =
unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL;
pub static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
pub static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy<Option<AdjustWindowRectExForDpi>> =
@@ -276,13 +251,3 @@ pub static SET_PROCESS_DPI_AWARENESS: Lazy<Option<SetProcessDpiAwareness>> =
Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness));
pub static SET_PROCESS_DPI_AWARE: Lazy<Option<SetProcessDPIAware>> =
Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware));
pub static GET_POINTER_FRAME_INFO_HISTORY: Lazy<Option<GetPointerFrameInfoHistory>> =
Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory));
pub static SKIP_POINTER_FRAME_MESSAGES: Lazy<Option<SkipPointerFrameMessages>> =
Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages));
pub static GET_POINTER_DEVICE_RECTS: Lazy<Option<GetPointerDeviceRects>> =
Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects));
pub static GET_POINTER_TOUCH_INFO: Lazy<Option<GetPointerTouchInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo));
pub static GET_POINTER_PEN_INFO: Lazy<Option<GetPointerPenInfo>> =
Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo));

View File

@@ -55,6 +55,7 @@ use windows_sys::Win32::{
};
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
icon::Icon,
@@ -72,8 +73,7 @@ use crate::{
monitor::{self, MonitorHandle},
util,
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
Fullscreen, PlatformCustomCursor, PlatformSpecificWindowBuilderAttributes, SelectedCursor,
WindowId,
Fullscreen, PlatformSpecificWindowBuilderAttributes, SelectedCursor, WindowId,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@@ -81,27 +81,10 @@ use crate::{
},
};
#[derive(Debug, Clone)]
pub(crate) struct OwnedWindowHandle {
hwnd: HWND,
}
impl OwnedWindowHandle {
#[cfg(feature = "rwh_06")]
pub(crate) fn new_parent_window(handle: rwh_06::WindowHandle<'_>) -> Self {
// TODO: Do we need to do something to extend the lifetime of the window handle?
let hwnd = match handle.as_raw() {
rwh_06::RawWindowHandle::Win32(handle) => handle.hwnd.get() as HWND,
handle => panic!("invalid window handle {handle:?} on Windows"),
};
Self { hwnd }
}
}
/// The Win32 implementation of the main `Window` object.
pub(crate) struct Window {
/// Main handle for the window.
window: HWND,
window: WindowWrapper,
/// The current window state.
window_state: Arc<Mutex<WindowState>>,
@@ -145,11 +128,11 @@ impl Window {
}
pub fn set_transparent(&self, transparent: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::TRANSPARENT, transparent)
});
});
@@ -159,11 +142,11 @@ impl Window {
#[inline]
pub fn set_visible(&self, visible: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::VISIBLE, visible)
});
});
@@ -171,7 +154,7 @@ impl Window {
#[inline]
pub fn is_visible(&self) -> Option<bool> {
Some(unsafe { IsWindowVisible(self.window) == 1 })
Some(unsafe { IsWindowVisible(self.window.0) == 1 })
}
#[inline]
@@ -207,10 +190,10 @@ impl Window {
let (x, y): (i32, i32) = position.to_physical::<i32>(self.scale_factor()).into();
let window_state = Arc::clone(&self.window_state);
let window = self.window;
let window = self.window.clone();
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MAXIMIZED, false)
});
});
@@ -264,10 +247,10 @@ impl Window {
if physical_size != self.inner_size() {
let window_state = Arc::clone(&self.window_state);
let window = self.window;
let window = self.window.clone();
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MAXIMIZED, false)
});
});
@@ -302,12 +285,12 @@ impl Window {
#[inline]
pub fn set_resizable(&self, resizable: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::RESIZABLE, resizable)
});
});
@@ -321,12 +304,12 @@ impl Window {
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(
WindowFlags::MINIMIZABLE,
buttons.contains(WindowButtons::MINIMIZE),
@@ -360,14 +343,14 @@ impl Window {
/// Returns the `hwnd` of this window.
#[inline]
pub fn hwnd(&self) -> HWND {
self.window
self.window.0
}
#[cfg(feature = "rwh_04")]
#[inline]
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
let mut window_handle = rwh_04::Win32Handle::empty();
window_handle.hwnd = self.window as *mut _;
window_handle.hwnd = self.window.0 as *mut _;
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = hinstance as *mut _;
rwh_04::RawWindowHandle::Win32(window_handle)
@@ -377,7 +360,7 @@ impl Window {
#[inline]
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
let mut window_handle = rwh_05::Win32WindowHandle::empty();
window_handle.hwnd = self.window as *mut _;
window_handle.hwnd = self.window.0 as *mut _;
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = hinstance as *mut _;
rwh_05::RawWindowHandle::Win32(window_handle)
@@ -394,7 +377,8 @@ impl Window {
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe {
// SAFETY: Handle will never be zero.
std::num::NonZeroIsize::new_unchecked(self.window)
let window = self.window.0;
std::num::NonZeroIsize::new_unchecked(window)
});
let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) };
window_handle.hinstance = std::num::NonZeroIsize::new(hinstance);
@@ -421,8 +405,8 @@ impl Window {
}
#[inline]
pub(crate) fn set_custom_cursor(&self, cursor: PlatformCustomCursor) {
let new_cursor = match WinCursor::new(&cursor.0) {
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let new_cursor = match WinCursor::new(&cursor.inner) {
Ok(cursor) => cursor,
Err(err) => {
warn!("Failed to create custom cursor: {err}");
@@ -445,7 +429,7 @@ impl Window {
}
};
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
let (tx, rx) = channel();
@@ -455,7 +439,7 @@ impl Window {
.lock()
.unwrap()
.mouse
.set_cursor_flags(window, |f| f.set(CursorFlags::GRABBED, confine))
.set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, confine))
.map_err(|e| ExternalError::Os(os_error!(e)));
let _ = tx.send(result);
});
@@ -464,7 +448,7 @@ impl Window {
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
let (tx, rx) = channel();
@@ -474,7 +458,7 @@ impl Window {
.lock()
.unwrap()
.mouse
.set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible))
.set_cursor_flags(window.0, |f| f.set(CursorFlags::HIDDEN, !visible))
.map_err(|e| e.to_string());
let _ = tx.send(result);
});
@@ -504,7 +488,7 @@ impl Window {
}
unsafe fn handle_os_dragging(&self, wparam: WPARAM) {
let window = self.window;
let window = self.window.clone();
let window_state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || {
@@ -532,7 +516,7 @@ impl Window {
unsafe {
PostMessageW(
window,
window.0,
WM_NCLBUTTONDOWN,
wparam,
&points as *const _ as LPARAM,
@@ -647,10 +631,10 @@ impl Window {
#[inline]
pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest)
});
});
@@ -665,7 +649,7 @@ impl Window {
#[inline]
pub fn set_minimized(&self, minimized: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
let is_minimized = util::is_minimized(self.hwnd());
@@ -675,7 +659,7 @@ impl Window {
WindowState::set_window_flags_in_place(&mut window_state.lock().unwrap(), |f| {
f.set(WindowFlags::MINIMIZED, is_minimized)
});
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MINIMIZED, minimized)
});
});
@@ -688,12 +672,12 @@ impl Window {
#[inline]
pub fn set_maximized(&self, maximized: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MAXIMIZED, maximized)
});
});
@@ -713,7 +697,7 @@ impl Window {
#[inline]
pub fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
let mut window_state_lock = window_state.lock().unwrap();
@@ -724,7 +708,7 @@ impl Window {
_ if old_fullscreen == fullscreen => return,
// Return if saved Borderless(monitor) is the same as current monitor when requested fullscreen is Borderless(None)
(Some(Fullscreen::Borderless(Some(monitor))), Some(Fullscreen::Borderless(None)))
if *monitor == monitor::current_monitor(window) =>
if *monitor == monitor::current_monitor(window.0) =>
{
return
}
@@ -793,7 +777,7 @@ impl Window {
}
// Update window style
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(
WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN,
matches!(fullscreen, Some(Fullscreen::Exclusive(_))),
@@ -809,7 +793,7 @@ impl Window {
// this needs to be called before the below fullscreen SetWindowPos as this itself
// will generate WM_SIZE messages of the old window size that can race with what we set below
unsafe {
taskbar_mark_fullscreen(window, fullscreen.is_some());
taskbar_mark_fullscreen(window.0, fullscreen.is_some());
}
// Update window bounds
@@ -818,7 +802,7 @@ impl Window {
// Save window bounds before entering fullscreen
let placement = unsafe {
let mut placement = mem::zeroed();
GetWindowPlacement(window, &mut placement);
GetWindowPlacement(window.0, &mut placement);
placement
};
@@ -827,7 +811,7 @@ impl Window {
let monitor = match &fullscreen {
Fullscreen::Exclusive(video_mode) => video_mode.monitor(),
Fullscreen::Borderless(Some(monitor)) => monitor.clone(),
Fullscreen::Borderless(None) => monitor::current_monitor(window),
Fullscreen::Borderless(None) => monitor::current_monitor(window.0),
};
let position: (i32, i32) = monitor.position().into();
@@ -835,7 +819,7 @@ impl Window {
unsafe {
SetWindowPos(
window,
window.0,
0,
position.0,
position.1,
@@ -843,7 +827,7 @@ impl Window {
size.1 as i32,
SWP_ASYNCWINDOWPOS | SWP_NOZORDER,
);
InvalidateRgn(window, 0, false.into());
InvalidateRgn(window.0, 0, false.into());
}
}
None => {
@@ -851,8 +835,8 @@ impl Window {
if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() {
drop(window_state_lock);
unsafe {
SetWindowPlacement(window, &placement);
InvalidateRgn(window, 0, false.into());
SetWindowPlacement(window.0, &placement);
InvalidateRgn(window.0, 0, false.into());
}
}
}
@@ -862,12 +846,12 @@ impl Window {
#[inline]
pub fn set_decorations(&self, decorations: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MARKER_DECORATIONS, decorations)
});
});
@@ -883,12 +867,12 @@ impl Window {
#[inline]
pub fn set_window_level(&self, level: WindowLevel) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(
WindowFlags::ALWAYS_ON_TOP,
level == WindowLevel::AlwaysOnTop,
@@ -937,21 +921,21 @@ impl Window {
#[inline]
pub fn set_ime_cursor_area(&self, spot: Position, size: Size) {
let window = self.window;
let window = self.window.clone();
let state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || unsafe {
let scale_factor = state.lock().unwrap().scale_factor;
ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor);
ImeContext::current(window.0).set_ime_cursor_area(spot, size, scale_factor);
});
}
#[inline]
pub fn set_ime_allowed(&self, allowed: bool) {
let window = self.window;
let window = self.window.clone();
let state = self.window_state.clone();
self.thread_executor.execute_in_thread(move || unsafe {
state.lock().unwrap().ime_allowed = allowed;
ImeContext::set_ime_allowed(window, allowed);
ImeContext::set_ime_allowed(window.0, allowed);
})
}
@@ -960,13 +944,14 @@ impl Window {
#[inline]
pub fn request_user_attention(&self, request_type: Option<UserAttentionType>) {
let window = self.window;
let window = self.window.clone();
let active_window_handle = unsafe { GetActiveWindow() };
if window == active_window_handle {
if window.0 == active_window_handle {
return;
}
self.thread_executor.execute_in_thread(move || unsafe {
let _ = &window;
let (flags, count) = request_type
.map(|ty| match ty {
UserAttentionType::Critical => (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX),
@@ -976,7 +961,7 @@ impl Window {
let flash_info = FLASHWINFO {
cbSize: mem::size_of::<FLASHWINFO>() as u32,
hwnd: window,
hwnd: window.0,
dwFlags: flags,
uCount: count,
dwTimeout: 0,
@@ -987,7 +972,7 @@ impl Window {
#[inline]
pub fn set_theme(&self, theme: Option<Theme>) {
try_theme(self.window, theme);
try_theme(self.window.0, theme);
}
#[inline]
@@ -1002,9 +987,9 @@ impl Window {
}
pub fn title(&self) -> String {
let len = unsafe { GetWindowTextLengthW(self.window) } + 1;
let len = unsafe { GetWindowTextLengthW(self.window.0) } + 1;
let mut buf = vec![0; len as usize];
unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) };
unsafe { GetWindowTextW(self.window.0, buf.as_mut_ptr(), len) };
util::decode_wide(&buf).to_string_lossy().to_string()
}
@@ -1016,12 +1001,12 @@ impl Window {
#[inline]
pub fn set_undecorated_shadow(&self, shadow: bool) {
let window = self.window;
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow)
});
});
@@ -1029,14 +1014,15 @@ impl Window {
#[inline]
pub fn focus_window(&self) {
let window = self.window.clone();
let window_flags = self.window_state_lock().window_flags();
let is_visible = window_flags.contains(WindowFlags::VISIBLE);
let is_minimized = util::is_minimized(self.hwnd());
let is_foreground = self.window == unsafe { GetForegroundWindow() };
let is_foreground = window.0 == unsafe { GetForegroundWindow() };
if is_visible && !is_minimized && !is_foreground {
unsafe { force_window_active(self.window) };
unsafe { force_window_active(window.0) };
}
}
@@ -1086,6 +1072,18 @@ impl Drop for Window {
}
}
/// A simple non-owning wrapper around a window.
#[doc(hidden)]
#[derive(Clone)]
pub struct WindowWrapper(HWND);
// Send and Sync are not implemented for HWND and HDC, we have to wrap it and implement them manually.
// For more info see:
// https://github.com/retep998/winapi-rs/issues/360
// https://github.com/retep998/winapi-rs/issues/396
unsafe impl Sync for WindowWrapper {}
unsafe impl Send for WindowWrapper {}
pub(super) struct InitData<'a, T: 'static> {
// inputs
pub event_loop: &'a EventLoopWindowTarget<T>,
@@ -1133,7 +1131,7 @@ impl<'a, T: 'static> InitData<'a, T> {
unsafe { ImeContext::set_ime_allowed(window, false) };
Window {
window,
window: WindowWrapper(window),
window_state,
thread_executor: self.event_loop.create_thread_executor(),
}
@@ -1156,7 +1154,7 @@ impl<'a, T: 'static> InitData<'a, T> {
let file_drop_runner = self.event_loop.runner_shared.clone();
let file_drop_handler = FileDropHandler::new(
win.window,
win.window.0,
Box::new(move |event| {
if let Ok(e) = event.map_nonuser_event() {
file_drop_runner.send_event(e)
@@ -1168,7 +1166,7 @@ impl<'a, T: 'static> InitData<'a, T> {
unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void };
assert_eq!(
unsafe { RegisterDragDrop(win.window, handler_interface_ptr) },
unsafe { RegisterDragDrop(win.window.0, handler_interface_ptr) },
S_OK
);
Some(file_drop_handler)
@@ -1245,7 +1243,7 @@ impl<'a, T: 'static> InitData<'a, T> {
if attributes.fullscreen.0.is_some() {
win.set_fullscreen(attributes.fullscreen.0.map(Into::into));
unsafe { force_window_active(win.window) };
unsafe { force_window_active(win.window.0) };
} else {
let size = attributes
.inner_size
@@ -1318,25 +1316,33 @@ where
// so the diffing later can work.
window_flags.set(WindowFlags::CLOSABLE, true);
let parent = if let Some(parent_window) = &attributes.parent_window {
window_flags.set(WindowFlags::CHILD, true);
if pl_attribs.menu.is_some() {
warn!("Setting a menu on a child window is unsupported");
let mut fallback_parent = || match pl_attribs.owner {
Some(parent) => {
window_flags.set(WindowFlags::POPUP, true);
Some(parent)
}
Some(parent_window.hwnd)
} else {
match pl_attribs.owner {
Some(parent) => {
window_flags.set(WindowFlags::POPUP, true);
Some(parent)
}
None => {
window_flags.set(WindowFlags::ON_TASKBAR, true);
None
}
None => {
window_flags.set(WindowFlags::ON_TASKBAR, true);
None
}
};
#[cfg(feature = "rwh_06")]
let parent = match attributes.parent_window.0 {
Some(rwh_06::RawWindowHandle::Win32(handle)) => {
window_flags.set(WindowFlags::CHILD, true);
if pl_attribs.menu.is_some() {
warn!("Setting a menu on a child window is unsupported");
}
Some(handle.hwnd.get() as HWND)
}
Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"),
None => fallback_parent(),
};
#[cfg(not(feature = "rwh_06"))]
let parent = fallback_parent();
let mut initdata = InitData {
event_loop,
attributes,

View File

@@ -9,7 +9,7 @@ use crate::{
platform_impl, SendSyncWrapper,
};
pub use crate::cursor::{BadImage, CustomCursor, CustomCursorBuilder, MAX_CURSOR_SIZE};
pub use crate::cursor::{BadImage, CustomCursor, MAX_CURSOR_SIZE};
pub use crate::icon::{BadIcon, Icon};
#[doc(inline)]
@@ -153,7 +153,8 @@ pub struct WindowAttributes {
pub content_protected: bool,
pub window_level: WindowLevel,
pub active: bool,
pub(crate) parent_window: Option<platform_impl::OwnedWindowHandle>,
#[cfg(feature = "rwh_06")]
pub(crate) parent_window: SendSyncWrapper<Option<rwh_06::RawWindowHandle>>,
pub(crate) fullscreen: SendSyncWrapper<Option<Fullscreen>>,
}
@@ -179,7 +180,8 @@ impl Default for WindowAttributes {
preferred_theme: None,
resize_increments: None,
content_protected: false,
parent_window: None,
#[cfg(feature = "rwh_06")]
parent_window: SendSyncWrapper(None),
active: true,
}
}
@@ -188,8 +190,8 @@ impl Default for WindowAttributes {
impl WindowAttributes {
/// Get the parent window stored on the attributes.
#[cfg(feature = "rwh_06")]
pub fn parent_window(&self) -> Option<Result<rwh_06::RawWindowHandle, rwh_06::HandleError>> {
Some(self.parent_window.as_ref()?.raw_window_handle())
pub fn parent_window(&self) -> Option<&rwh_06::RawWindowHandle> {
self.parent_window.0.as_ref()
}
/// Get `Fullscreen` option stored on the attributes.
@@ -474,6 +476,10 @@ impl WindowBuilder {
///
/// The default is `None`.
///
/// ## Safety
///
/// `parent_window` must be a valid window handle.
///
/// ## Platform-specific
///
/// - **Windows** : A child window has the WS_CHILD style and is confined
@@ -483,9 +489,11 @@ impl WindowBuilder {
/// - **Android / iOS / Wayland / Web:** Unsupported.
#[cfg(feature = "rwh_06")]
#[inline]
pub fn with_parent_window(mut self, parent_window: Option<rwh_06::WindowHandle<'_>>) -> Self {
self.window.parent_window =
parent_window.map(platform_impl::OwnedWindowHandle::new_parent_window);
pub unsafe fn with_parent_window(
mut self,
parent_window: Option<rwh_06::RawWindowHandle>,
) -> Self {
self.window.parent_window = SendSyncWrapper(parent_window);
self
}
@@ -1347,7 +1355,7 @@ impl Window {
/// - **iOS / Android / Orbital:** Unsupported.
#[inline]
pub fn set_custom_cursor(&self, cursor: &CustomCursor) {
let cursor = cursor.inner.clone();
let cursor = cursor.clone();
self.window
.maybe_queue_on_main(move |w| w.set_custom_cursor(cursor))
}
@@ -1481,6 +1489,10 @@ impl Window {
/// Returns the monitor on which the window currently resides.
///
/// Returns `None` if current monitor can't be detected.
///
/// ## Platform-specific
///
/// **iOS:** Can only be called on the main thread.
#[inline]
pub fn current_monitor(&self) -> Option<MonitorHandle> {
self.window
@@ -1491,6 +1503,10 @@ impl Window {
///
/// This is the same as [`EventLoopWindowTarget::available_monitors`], and is provided for convenience.
///
/// ## Platform-specific
///
/// **iOS:** Can only be called on the main thread.
///
/// [`EventLoopWindowTarget::available_monitors`]: crate::event_loop::EventLoopWindowTarget::available_monitors
#[inline]
pub fn available_monitors(&self) -> impl Iterator<Item = MonitorHandle> {
@@ -1509,7 +1525,8 @@ impl Window {
///
/// ## Platform-specific
///
/// **Wayland / Web:** Always returns `None`.
/// **iOS:** Can only be called on the main thread.
/// **Wayland:** Always returns `None`.
///
/// [`EventLoopWindowTarget::primary_monitor`]: crate::event_loop::EventLoopWindowTarget::primary_monitor
#[inline]
@@ -1522,10 +1539,12 @@ impl Window {
#[cfg(feature = "rwh_06")]
impl rwh_06::HasWindowHandle for Window {
fn window_handle(&self) -> Result<rwh_06::WindowHandle<'_>, rwh_06::HandleError> {
let raw = self.window.raw_window_handle_rwh_06()?;
let raw = self
.window
.maybe_wait_on_main(|w| w.raw_window_handle_rwh_06().map(SendSyncWrapper))?
.0;
// SAFETY: The window handle will never be deallocated while the window is alive,
// and the main thread safety requirements are upheld internally by each platform.
// SAFETY: The window handle will never be deallocated while the window is alive.
Ok(unsafe { rwh_06::WindowHandle::borrow_raw(raw) })
}
}

View File

@@ -31,6 +31,5 @@ fn ids_send() {
#[test]
fn custom_cursor_send() {
needs_send::<winit::window::CustomCursorBuilder>();
needs_send::<winit::window::CustomCursor>();
}

View File

@@ -14,6 +14,5 @@ fn window_builder_sync() {
#[test]
fn custom_cursor_sync() {
needs_sync::<winit::window::CustomCursorBuilder>();
needs_sync::<winit::window::CustomCursor>();
}