Trying to clena the mess opened by plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s

This commit is contained in:
2026-02-21 01:03:55 +01:00
parent ac0ddc7fb9
commit 75a8fd4401
67 changed files with 1246 additions and 69 deletions

View File

@@ -0,0 +1,34 @@
[package]
name = "baseview"
version = "0.1.0"
edition = "2018"
license = "MIT OR Apache-2.0"
[features]
default = []
opengl = ["uuid", "x11/glx"]
[dependencies]
keyboard-types = { version = "0.6.1", default-features = false }
raw-window-handle = "0.5"
[target.'cfg(target_os="linux")'.dependencies]
x11rb = { version = "0.13.0", features = ["cursor", "resource_manager", "allow-unsafe-code"] }
x11 = { version = "2.21", features = ["xlib", "xlib_xcb"] }
nix = "0.22.0"
[target.'cfg(target_os="windows")'.dependencies]
winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] }
uuid = { version = "0.8", features = ["v4"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
cocoa = "0.24.0"
core-foundation = "0.9.1"
objc = "0.2.7"
uuid = { version = "0.8", features = ["v4"] }
[lints.clippy]
missing-safety-doc = "allow"
[lints.rust]
unexpected_cfgs = "allow"

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,10 @@
#[cfg(target_os = "macos")]
use crate::macos as platform;
#[cfg(target_os = "windows")]
use crate::win as platform;
#[cfg(target_os = "linux")]
use crate::x11 as platform;
pub fn copy_to_clipboard(data: &str) {
platform::copy_to_clipboard(data)
}

View File

@@ -0,0 +1,170 @@
use std::path::PathBuf;
use keyboard_types::{KeyboardEvent, Modifiers};
use crate::{Point, WindowInfo};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum MouseButton {
Left,
Middle,
Right,
Back,
Forward,
Other(u8),
}
/// A scroll movement.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScrollDelta {
/// A line-based scroll movement
Lines {
/// The number of horizontal lines scrolled
x: f32,
/// The number of vertical lines scrolled
y: f32,
},
/// A pixel-based scroll movement
Pixels {
/// The number of horizontal pixels scrolled
x: f32,
/// The number of vertical pixels scrolled
y: f32,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum MouseEvent {
/// The mouse cursor was moved
CursorMoved {
/// The logical coordinates of the mouse position
position: Point,
/// The screen-absolute logical coordinates of the mouse position
screen_position: Point,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
},
/// A mouse button was pressed.
ButtonPressed {
/// The button that was pressed.
button: MouseButton,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
},
/// A mouse button was released.
ButtonReleased {
/// The button that was released.
button: MouseButton,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
},
/// The mouse wheel was scrolled.
WheelScrolled {
/// How much was scrolled, in factional lines.
delta: ScrollDelta,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
},
/// The mouse cursor entered the window.
///
/// May not be available on all platforms.
CursorEntered,
/// The mouse cursor left the window.
///
/// May not be available on all platforms.
CursorLeft,
DragEntered {
/// The logical coordinates of the mouse position
position: Point,
/// The screen-absolute logical coordinates of the mouse position
screen_position: Point,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
/// Data being dragged
data: DropData,
},
DragMoved {
/// The logical coordinates of the mouse position
position: Point,
/// The screen-absolute logical coordinates of the mouse position
screen_position: Point,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
/// Data being dragged
data: DropData,
},
DragLeft,
DragDropped {
/// The logical coordinates of the mouse position
position: Point,
/// The screen-absolute logical coordinates of the mouse position
screen_position: Point,
/// The modifiers that were held down just before the event.
modifiers: Modifiers,
/// Data being dragged
data: DropData,
},
}
#[derive(Debug, Clone)]
pub enum WindowEvent {
Resized(WindowInfo),
Focused,
Unfocused,
WillClose,
}
#[derive(Debug, Clone)]
pub enum Event {
Mouse(MouseEvent),
Keyboard(KeyboardEvent),
Window(WindowEvent),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DropEffect {
Copy,
Move,
Link,
Scroll,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DropData {
None,
Files(Vec<PathBuf>),
}
/// Return value for [WindowHandler::on_event](`crate::WindowHandler::on_event()`),
/// indicating whether the event was handled by your window or should be passed
/// back to the platform.
///
/// For most event types, this value won't have any effect. This is the case
/// when there is no clear meaning of passing back the event to the platform,
/// or it isn't obviously useful. Currently, only [`Event::Keyboard`] variants
/// are supported.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EventStatus {
/// Event was handled by your window and will not be sent back to the
/// platform for further processing.
Captured,
/// Event was **not** handled by your window, so pass it back to the
/// platform. For parented windows, this usually means that the parent
/// window will receive the event. This is useful for cases such as using
/// DAW functionality for playing piano keys with the keyboard while a
/// plugin window is in focus.
Ignored,
/// We are prepared to handle the data in the drag and dropping will
/// result in [DropEffect]
AcceptDrop(DropEffect),
}

View File

@@ -0,0 +1,155 @@
// This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125
// Eventually we should migrate to the objc2 crate and remove this.
#![allow(unexpected_cfgs)]
use std::ffi::c_void;
use std::str::FromStr;
use raw_window_handle::RawWindowHandle;
use cocoa::appkit::{
NSOpenGLContext, NSOpenGLContextParameter, NSOpenGLPFAAccelerated, NSOpenGLPFAAlphaSize,
NSOpenGLPFAColorSize, NSOpenGLPFADepthSize, NSOpenGLPFADoubleBuffer, NSOpenGLPFAMultisample,
NSOpenGLPFAOpenGLProfile, NSOpenGLPFASampleBuffers, NSOpenGLPFASamples, NSOpenGLPFAStencilSize,
NSOpenGLPixelFormat, NSOpenGLProfileVersion3_2Core, NSOpenGLProfileVersion4_1Core,
NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView,
};
use cocoa::base::{id, nil, YES};
use cocoa::foundation::NSSize;
use core_foundation::base::TCFType;
use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName};
use core_foundation::string::CFString;
use objc::{msg_send, sel, sel_impl};
use super::{GlConfig, GlError, Profile};
pub type CreationFailedError = ();
pub struct GlContext {
view: id,
context: id,
}
impl GlContext {
pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result<GlContext, GlError> {
let handle = if let RawWindowHandle::AppKit(handle) = parent {
handle
} else {
return Err(GlError::InvalidWindowHandle);
};
if handle.ns_view.is_null() {
return Err(GlError::InvalidWindowHandle);
}
let parent_view = handle.ns_view as id;
let version = if config.version < (3, 2) && config.profile == Profile::Compatibility {
NSOpenGLProfileVersionLegacy
} else if config.version == (3, 2) && config.profile == Profile::Core {
NSOpenGLProfileVersion3_2Core
} else if config.version > (3, 2) && config.profile == Profile::Core {
NSOpenGLProfileVersion4_1Core
} else {
return Err(GlError::VersionNotSupported);
};
#[rustfmt::skip]
let mut attrs = vec![
NSOpenGLPFAOpenGLProfile as u32, version as u32,
NSOpenGLPFAColorSize as u32, (config.red_bits + config.blue_bits + config.green_bits) as u32,
NSOpenGLPFAAlphaSize as u32, config.alpha_bits as u32,
NSOpenGLPFADepthSize as u32, config.depth_bits as u32,
NSOpenGLPFAStencilSize as u32, config.stencil_bits as u32,
NSOpenGLPFAAccelerated as u32,
];
if let Some(samples) = config.samples {
#[rustfmt::skip]
attrs.extend_from_slice(&[
NSOpenGLPFAMultisample as u32,
NSOpenGLPFASampleBuffers as u32, 1,
NSOpenGLPFASamples as u32, samples as u32,
]);
}
if config.double_buffer {
attrs.push(NSOpenGLPFADoubleBuffer as u32);
}
attrs.push(0);
let pixel_format = NSOpenGLPixelFormat::alloc(nil).initWithAttributes_(&attrs);
if pixel_format == nil {
return Err(GlError::CreationFailed(()));
}
let view =
NSOpenGLView::alloc(nil).initWithFrame_pixelFormat_(parent_view.frame(), pixel_format);
if view == nil {
return Err(GlError::CreationFailed(()));
}
view.setWantsBestResolutionOpenGLSurface_(YES);
NSOpenGLView::display_(view);
parent_view.addSubview_(view);
let context: id = msg_send![view, openGLContext];
let () = msg_send![context, retain];
context.setValues_forParameter_(
&(config.vsync as i32),
NSOpenGLContextParameter::NSOpenGLCPSwapInterval,
);
let () = msg_send![pixel_format, release];
Ok(GlContext { view, context })
}
pub unsafe fn make_current(&self) {
self.context.makeCurrentContext();
}
pub unsafe fn make_not_current(&self) {
NSOpenGLContext::clearCurrentContext(self.context);
}
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
let symbol_name = CFString::from_str(symbol).unwrap();
let framework_name = CFString::from_str("com.apple.opengl").unwrap();
let framework =
unsafe { CFBundleGetBundleWithIdentifier(framework_name.as_concrete_TypeRef()) };
unsafe { CFBundleGetFunctionPointerForName(framework, symbol_name.as_concrete_TypeRef()) }
}
pub fn swap_buffers(&self) {
unsafe {
self.context.flushBuffer();
let () = msg_send![self.view, setNeedsDisplay: YES];
}
}
/// On macOS the `NSOpenGLView` needs to be resized separtely from our main view.
pub(crate) fn resize(&self, size: NSSize) {
unsafe { NSView::setFrameSize(self.view, size) };
unsafe {
let _: () = msg_send![self.context, update];
let _: () = msg_send![self.view, setNeedsDisplay: YES];
}
}
}
impl Drop for GlContext {
fn drop(&mut self) {
unsafe {
let () = msg_send![self.context, release];
let () = msg_send![self.view, release];
}
}
}

View File

@@ -0,0 +1,115 @@
use std::ffi::c_void;
use std::marker::PhantomData;
// On X11 creating the context is a two step process
#[cfg(not(target_os = "linux"))]
use raw_window_handle::RawWindowHandle;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
use win as platform;
// We need to use this directly within the X11 window creation to negotiate the correct visual
#[cfg(target_os = "linux")]
pub(crate) mod x11;
#[cfg(target_os = "linux")]
pub(crate) use self::x11 as platform;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos as platform;
#[derive(Clone, Debug)]
pub struct GlConfig {
pub version: (u8, u8),
pub profile: Profile,
pub red_bits: u8,
pub blue_bits: u8,
pub green_bits: u8,
pub alpha_bits: u8,
pub depth_bits: u8,
pub stencil_bits: u8,
pub samples: Option<u8>,
pub srgb: bool,
pub double_buffer: bool,
pub vsync: bool,
}
impl Default for GlConfig {
fn default() -> Self {
GlConfig {
version: (3, 2),
profile: Profile::Core,
red_bits: 8,
blue_bits: 8,
green_bits: 8,
alpha_bits: 8,
depth_bits: 24,
stencil_bits: 8,
samples: None,
srgb: true,
double_buffer: true,
vsync: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Profile {
Compatibility,
Core,
}
#[derive(Debug)]
pub enum GlError {
InvalidWindowHandle,
VersionNotSupported,
CreationFailed(platform::CreationFailedError),
}
pub struct GlContext {
context: platform::GlContext,
phantom: PhantomData<*mut ()>,
}
impl GlContext {
#[cfg(not(target_os = "linux"))]
pub(crate) unsafe fn create(
parent: &RawWindowHandle, config: GlConfig,
) -> Result<GlContext, GlError> {
platform::GlContext::create(parent, config)
.map(|context| GlContext { context, phantom: PhantomData })
}
/// The X11 version needs to be set up in a different way compared to the Windows and macOS
/// versions. So the platform-specific versions should be used to construct the context within
/// baseview, and then this object can be passed to the user.
#[cfg(target_os = "linux")]
pub(crate) fn new(context: platform::GlContext) -> GlContext {
GlContext { context, phantom: PhantomData }
}
pub unsafe fn make_current(&self) {
self.context.make_current();
}
pub unsafe fn make_not_current(&self) {
self.context.make_not_current();
}
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
self.context.get_proc_address(symbol)
}
pub fn swap_buffers(&self) {
self.context.swap_buffers();
}
/// On macOS the `NSOpenGLView` needs to be resized separtely from our main view.
#[cfg(target_os = "macos")]
pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) {
self.context.resize(size);
}
}

View File

@@ -0,0 +1,308 @@
use std::ffi::{c_void, CString, OsStr};
use std::os::windows::ffi::OsStrExt;
use raw_window_handle::RawWindowHandle;
use winapi::shared::minwindef::{HINSTANCE, HMODULE};
use winapi::shared::ntdef::WCHAR;
use winapi::shared::windef::{HDC, HGLRC, HWND};
use winapi::um::libloaderapi::{FreeLibrary, GetProcAddress, LoadLibraryA};
use winapi::um::wingdi::{
wglCreateContext, wglDeleteContext, wglGetProcAddress, wglMakeCurrent, ChoosePixelFormat,
DescribePixelFormat, SetPixelFormat, SwapBuffers, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW,
PFD_MAIN_PLANE, PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR,
};
use winapi::um::winnt::IMAGE_DOS_HEADER;
use winapi::um::winuser::{
CreateWindowExW, DefWindowProcW, DestroyWindow, GetDC, RegisterClassW, ReleaseDC,
UnregisterClassW, CS_OWNDC, CW_USEDEFAULT, WNDCLASSW,
};
use super::{GlConfig, GlError, Profile};
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt
type WglCreateContextAttribsARB = extern "system" fn(HDC, HGLRC, *const i32) -> HGLRC;
const WGL_CONTEXT_MAJOR_VERSION_ARB: i32 = 0x2091;
const WGL_CONTEXT_MINOR_VERSION_ARB: i32 = 0x2092;
const WGL_CONTEXT_PROFILE_MASK_ARB: i32 = 0x9126;
const WGL_CONTEXT_CORE_PROFILE_BIT_ARB: i32 = 0x00000001;
const WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB: i32 = 0x00000002;
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt
type WglChoosePixelFormatARB =
extern "system" fn(HDC, *const i32, *const f32, u32, *mut i32, *mut u32) -> i32;
const WGL_DRAW_TO_WINDOW_ARB: i32 = 0x2001;
const WGL_ACCELERATION_ARB: i32 = 0x2003;
const WGL_SUPPORT_OPENGL_ARB: i32 = 0x2010;
const WGL_DOUBLE_BUFFER_ARB: i32 = 0x2011;
const WGL_PIXEL_TYPE_ARB: i32 = 0x2013;
const WGL_RED_BITS_ARB: i32 = 0x2015;
const WGL_GREEN_BITS_ARB: i32 = 0x2017;
const WGL_BLUE_BITS_ARB: i32 = 0x2019;
const WGL_ALPHA_BITS_ARB: i32 = 0x201B;
const WGL_DEPTH_BITS_ARB: i32 = 0x2022;
const WGL_STENCIL_BITS_ARB: i32 = 0x2023;
const WGL_FULL_ACCELERATION_ARB: i32 = 0x2027;
const WGL_TYPE_RGBA_ARB: i32 = 0x202B;
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_multisample.txt
const WGL_SAMPLE_BUFFERS_ARB: i32 = 0x2041;
const WGL_SAMPLES_ARB: i32 = 0x2042;
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt
const WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20A9;
// See https://www.khronos.org/registry/OpenGL/extensions/EXT/WGL_EXT_swap_control.txt
type WglSwapIntervalEXT = extern "system" fn(i32) -> i32;
pub type CreationFailedError = ();
pub struct GlContext {
hwnd: HWND,
hdc: HDC,
hglrc: HGLRC,
gl_library: HMODULE,
}
extern "C" {
static __ImageBase: IMAGE_DOS_HEADER;
}
impl GlContext {
pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result<GlContext, GlError> {
let handle = if let RawWindowHandle::Win32(handle) = parent {
handle
} else {
return Err(GlError::InvalidWindowHandle);
};
if handle.hwnd.is_null() {
return Err(GlError::InvalidWindowHandle);
}
// Create temporary window and context to load function pointers
let class_name_str = format!("raw-gl-context-window-{}", uuid::Uuid::new_v4().to_simple());
let mut class_name: Vec<WCHAR> = OsStr::new(&class_name_str).encode_wide().collect();
class_name.push(0);
let hinstance = &__ImageBase as *const IMAGE_DOS_HEADER as HINSTANCE;
let wnd_class = WNDCLASSW {
style: CS_OWNDC,
lpfnWndProc: Some(DefWindowProcW),
hInstance: hinstance,
lpszClassName: class_name.as_ptr(),
..std::mem::zeroed()
};
let class = RegisterClassW(&wnd_class);
if class == 0 {
return Err(GlError::CreationFailed(()));
}
let hwnd_tmp = CreateWindowExW(
0,
class as *const WCHAR,
[0].as_ptr(),
0,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
std::ptr::null_mut(),
std::ptr::null_mut(),
hinstance,
std::ptr::null_mut(),
);
if hwnd_tmp.is_null() {
return Err(GlError::CreationFailed(()));
}
let hdc_tmp = GetDC(hwnd_tmp);
let pfd_tmp = PIXELFORMATDESCRIPTOR {
nSize: std::mem::size_of::<PIXELFORMATDESCRIPTOR>() as u16,
nVersion: 1,
dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
iPixelType: PFD_TYPE_RGBA,
cColorBits: 32,
cAlphaBits: 8,
cDepthBits: 24,
cStencilBits: 8,
iLayerType: PFD_MAIN_PLANE,
..std::mem::zeroed()
};
SetPixelFormat(hdc_tmp, ChoosePixelFormat(hdc_tmp, &pfd_tmp), &pfd_tmp);
let hglrc_tmp = wglCreateContext(hdc_tmp);
if hglrc_tmp.is_null() {
ReleaseDC(hwnd_tmp, hdc_tmp);
UnregisterClassW(class as *const WCHAR, hinstance);
DestroyWindow(hwnd_tmp);
return Err(GlError::CreationFailed(()));
}
wglMakeCurrent(hdc_tmp, hglrc_tmp);
#[allow(non_snake_case)]
let wglCreateContextAttribsARB: Option<WglCreateContextAttribsARB> = {
let symbol = CString::new("wglCreateContextAttribsARB").unwrap();
let addr = wglGetProcAddress(symbol.as_ptr());
if !addr.is_null() {
#[allow(clippy::missing_transmute_annotations)]
Some(std::mem::transmute(addr))
} else {
None
}
};
#[allow(non_snake_case)]
let wglChoosePixelFormatARB: Option<WglChoosePixelFormatARB> = {
let symbol = CString::new("wglChoosePixelFormatARB").unwrap();
let addr = wglGetProcAddress(symbol.as_ptr());
if !addr.is_null() {
#[allow(clippy::missing_transmute_annotations)]
Some(std::mem::transmute(addr))
} else {
None
}
};
#[allow(non_snake_case)]
let wglSwapIntervalEXT: Option<WglSwapIntervalEXT> = {
let symbol = CString::new("wglSwapIntervalEXT").unwrap();
let addr = wglGetProcAddress(symbol.as_ptr());
if !addr.is_null() {
#[allow(clippy::missing_transmute_annotations)]
Some(std::mem::transmute(addr))
} else {
None
}
};
wglMakeCurrent(hdc_tmp, std::ptr::null_mut());
wglDeleteContext(hglrc_tmp);
ReleaseDC(hwnd_tmp, hdc_tmp);
UnregisterClassW(class as *const WCHAR, hinstance);
DestroyWindow(hwnd_tmp);
// Create actual context
let hwnd = handle.hwnd as HWND;
let hdc = GetDC(hwnd);
#[rustfmt::skip]
let pixel_format_attribs = [
WGL_DRAW_TO_WINDOW_ARB, 1,
WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
WGL_SUPPORT_OPENGL_ARB, 1,
WGL_DOUBLE_BUFFER_ARB, config.double_buffer as i32,
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
WGL_RED_BITS_ARB, config.red_bits as i32,
WGL_GREEN_BITS_ARB, config.green_bits as i32,
WGL_BLUE_BITS_ARB, config.blue_bits as i32,
WGL_ALPHA_BITS_ARB, config.alpha_bits as i32,
WGL_DEPTH_BITS_ARB, config.depth_bits as i32,
WGL_STENCIL_BITS_ARB, config.stencil_bits as i32,
WGL_SAMPLE_BUFFERS_ARB, config.samples.is_some() as i32,
WGL_SAMPLES_ARB, config.samples.unwrap_or(0) as i32,
WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32,
0,
];
let mut pixel_format = 0;
let mut num_formats = 0;
wglChoosePixelFormatARB.unwrap()(
hdc,
pixel_format_attribs.as_ptr(),
std::ptr::null(),
1,
&mut pixel_format,
&mut num_formats,
);
let mut pfd: PIXELFORMATDESCRIPTOR = std::mem::zeroed();
DescribePixelFormat(
hdc,
pixel_format,
std::mem::size_of::<PIXELFORMATDESCRIPTOR>() as u32,
&mut pfd,
);
SetPixelFormat(hdc, pixel_format, &pfd);
let profile_mask = match config.profile {
Profile::Core => WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
Profile::Compatibility => WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
};
#[rustfmt::skip]
let ctx_attribs = [
WGL_CONTEXT_MAJOR_VERSION_ARB, config.version.0 as i32,
WGL_CONTEXT_MINOR_VERSION_ARB, config.version.1 as i32,
WGL_CONTEXT_PROFILE_MASK_ARB, profile_mask,
0
];
let hglrc =
wglCreateContextAttribsARB.unwrap()(hdc, std::ptr::null_mut(), ctx_attribs.as_ptr());
if hglrc.is_null() {
return Err(GlError::CreationFailed(()));
}
let gl_library_name = CString::new("opengl32.dll").unwrap();
let gl_library = LoadLibraryA(gl_library_name.as_ptr());
wglMakeCurrent(hdc, hglrc);
wglSwapIntervalEXT.unwrap()(config.vsync as i32);
wglMakeCurrent(hdc, std::ptr::null_mut());
Ok(GlContext { hwnd, hdc, hglrc, gl_library })
}
pub unsafe fn make_current(&self) {
wglMakeCurrent(self.hdc, self.hglrc);
}
pub unsafe fn make_not_current(&self) {
wglMakeCurrent(self.hdc, std::ptr::null_mut());
}
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
let symbol = CString::new(symbol).unwrap();
let addr = unsafe { wglGetProcAddress(symbol.as_ptr()) as *const c_void };
if !addr.is_null() {
addr
} else {
unsafe { GetProcAddress(self.gl_library, symbol.as_ptr()) as *const c_void }
}
}
pub fn swap_buffers(&self) {
unsafe {
SwapBuffers(self.hdc);
}
}
}
impl Drop for GlContext {
fn drop(&mut self) {
unsafe {
wglMakeCurrent(std::ptr::null_mut(), std::ptr::null_mut());
wglDeleteContext(self.hglrc);
ReleaseDC(self.hwnd, self.hdc);
FreeLibrary(self.gl_library);
}
}
}

View File

@@ -0,0 +1,249 @@
use std::ffi::{c_void, CString};
use std::os::raw::{c_int, c_ulong};
use x11::glx;
use x11::xlib;
use super::{GlConfig, GlError, Profile};
mod errors;
#[derive(Debug)]
pub enum CreationFailedError {
InvalidFBConfig,
NoVisual,
GetProcAddressFailed,
MakeCurrentFailed,
ContextCreationFailed,
X11Error(errors::XLibError),
}
impl From<errors::XLibError> for GlError {
fn from(e: errors::XLibError) -> Self {
GlError::CreationFailed(CreationFailedError::X11Error(e))
}
}
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/GLX_ARB_create_context.txt
type GlXCreateContextAttribsARB = unsafe extern "C" fn(
dpy: *mut xlib::Display,
fbc: glx::GLXFBConfig,
share_context: glx::GLXContext,
direct: xlib::Bool,
attribs: *const c_int,
) -> glx::GLXContext;
// See https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt
type GlXSwapIntervalEXT =
unsafe extern "C" fn(dpy: *mut xlib::Display, drawable: glx::GLXDrawable, interval: i32);
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt
const GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20B2;
fn get_proc_address(symbol: &str) -> *const c_void {
let symbol = CString::new(symbol).unwrap();
unsafe { glx::glXGetProcAddress(symbol.as_ptr() as *const u8).unwrap() as *const c_void }
}
pub struct GlContext {
window: c_ulong,
display: *mut xlib::_XDisplay,
context: glx::GLXContext,
}
/// The frame buffer configuration along with the general OpenGL configuration to somewhat minimize
/// misuse.
pub struct FbConfig {
gl_config: GlConfig,
fb_config: *mut glx::__GLXFBConfigRec,
}
/// The configuration a window should be created with after calling
/// [GlContext::get_fb_config_and_visual].
pub struct WindowConfig {
pub depth: u8,
pub visual: u32,
}
impl GlContext {
/// Creating an OpenGL context under X11 works slightly different. Different OpenGL
/// configurations require different framebuffer configurations, and to be able to use that
/// context with a window the window needs to be created with a matching visual. This means that
/// you need to decide on the framebuffer config before creating the window, ask the X11 server
/// for a matching visual for that framebuffer config, crate the window with that visual, and
/// only then create the OpenGL context.
///
/// Use [Self::get_fb_config_and_visual] to create both of these things.
pub unsafe fn create(
window: c_ulong, display: *mut xlib::_XDisplay, config: FbConfig,
) -> Result<GlContext, GlError> {
if display.is_null() {
return Err(GlError::InvalidWindowHandle);
}
errors::XErrorHandler::handle(display, |error_handler| {
#[allow(non_snake_case)]
let glXCreateContextAttribsARB: GlXCreateContextAttribsARB = {
let addr = get_proc_address("glXCreateContextAttribsARB");
if addr.is_null() {
return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed));
} else {
#[allow(clippy::missing_transmute_annotations)]
std::mem::transmute(addr)
}
};
#[allow(non_snake_case)]
let glXSwapIntervalEXT: GlXSwapIntervalEXT = {
let addr = get_proc_address("glXSwapIntervalEXT");
if addr.is_null() {
return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed));
} else {
#[allow(clippy::missing_transmute_annotations)]
std::mem::transmute(addr)
}
};
error_handler.check()?;
let profile_mask = match config.gl_config.profile {
Profile::Core => glx::arb::GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
Profile::Compatibility => glx::arb::GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
};
#[rustfmt::skip]
let ctx_attribs = [
glx::arb::GLX_CONTEXT_MAJOR_VERSION_ARB, config.gl_config.version.0 as i32,
glx::arb::GLX_CONTEXT_MINOR_VERSION_ARB, config.gl_config.version.1 as i32,
glx::arb::GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask,
0,
];
let context = glXCreateContextAttribsARB(
display,
config.fb_config,
std::ptr::null_mut(),
1,
ctx_attribs.as_ptr(),
);
error_handler.check()?;
if context.is_null() {
return Err(GlError::CreationFailed(CreationFailedError::ContextCreationFailed));
}
let res = glx::glXMakeCurrent(display, window, context);
error_handler.check()?;
if res == 0 {
return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed));
}
glXSwapIntervalEXT(display, window, config.gl_config.vsync as i32);
error_handler.check()?;
if glx::glXMakeCurrent(display, 0, std::ptr::null_mut()) == 0 {
error_handler.check()?;
return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed));
}
Ok(GlContext { window, display, context })
})
}
/// Find a matching framebuffer config and window visual for the given OpenGL configuration.
/// This needs to be passed to [Self::create] along with a handle to a window that was created
/// using the visual also returned from this function.
pub unsafe fn get_fb_config_and_visual(
display: *mut xlib::_XDisplay, config: GlConfig,
) -> Result<(FbConfig, WindowConfig), GlError> {
errors::XErrorHandler::handle(display, |error_handler| {
let screen = xlib::XDefaultScreen(display);
#[rustfmt::skip]
let fb_attribs = [
glx::GLX_X_RENDERABLE, 1,
glx::GLX_X_VISUAL_TYPE, glx::GLX_TRUE_COLOR,
glx::GLX_DRAWABLE_TYPE, glx::GLX_WINDOW_BIT,
glx::GLX_RENDER_TYPE, glx::GLX_RGBA_BIT,
glx::GLX_RED_SIZE, config.red_bits as i32,
glx::GLX_GREEN_SIZE, config.green_bits as i32,
glx::GLX_BLUE_SIZE, config.blue_bits as i32,
glx::GLX_ALPHA_SIZE, config.alpha_bits as i32,
glx::GLX_DEPTH_SIZE, config.depth_bits as i32,
glx::GLX_STENCIL_SIZE, config.stencil_bits as i32,
glx::GLX_DOUBLEBUFFER, config.double_buffer as i32,
glx::GLX_SAMPLE_BUFFERS, config.samples.is_some() as i32,
glx::GLX_SAMPLES, config.samples.unwrap_or(0) as i32,
GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32,
0,
];
let mut n_configs = 0;
let fb_config =
glx::glXChooseFBConfig(display, screen, fb_attribs.as_ptr(), &mut n_configs);
error_handler.check()?;
if n_configs <= 0 || fb_config.is_null() {
return Err(GlError::CreationFailed(CreationFailedError::InvalidFBConfig));
}
// Now that we have a matching framebuffer config, we need to know which visual matches
// thsi config so the window is compatible with the OpenGL context we're about to create
let fb_config = *fb_config;
let visual = glx::glXGetVisualFromFBConfig(display, fb_config);
if visual.is_null() {
return Err(GlError::CreationFailed(CreationFailedError::NoVisual));
}
Ok((
FbConfig { fb_config, gl_config: config },
WindowConfig { depth: (*visual).depth as u8, visual: (*visual).visualid as u32 },
))
})
}
pub unsafe fn make_current(&self) {
errors::XErrorHandler::handle(self.display, |error_handler| {
let res = glx::glXMakeCurrent(self.display, self.window, self.context);
error_handler.check().unwrap();
if res == 0 {
panic!("make_current failed")
}
})
}
pub unsafe fn make_not_current(&self) {
errors::XErrorHandler::handle(self.display, |error_handler| {
let res = glx::glXMakeCurrent(self.display, 0, std::ptr::null_mut());
error_handler.check().unwrap();
if res == 0 {
panic!("make_not_current failed")
}
})
}
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
get_proc_address(symbol)
}
pub fn swap_buffers(&self) {
unsafe {
errors::XErrorHandler::handle(self.display, |error_handler| {
glx::glXSwapBuffers(self.display, self.window);
error_handler.check().unwrap();
})
}
}
}
impl Drop for GlContext {
fn drop(&mut self) {
unsafe {
glx::glXDestroyContext(self.display, self.context);
}
}
}

View File

@@ -0,0 +1,166 @@
use std::ffi::CStr;
use std::fmt::{Debug, Display, Formatter};
use x11::xlib;
use std::cell::RefCell;
use std::error::Error;
use std::os::raw::{c_int, c_uchar, c_ulong};
use std::panic::AssertUnwindSafe;
thread_local! {
/// Used as part of [`XErrorHandler::handle()`]. When an X11 error occurs during this function,
/// the error gets copied to this RefCell after which the program is allowed to resume. The
/// error can then be converted to a regular Rust Result value afterward.
static CURRENT_X11_ERROR: RefCell<Option<XLibError>> = const { RefCell::new(None) };
}
/// A helper struct for safe X11 error handling
pub struct XErrorHandler<'a> {
display: *mut xlib::Display,
error: &'a RefCell<Option<XLibError>>,
}
impl<'a> XErrorHandler<'a> {
/// Syncs and checks if any previous X11 calls from the given display returned an error
pub fn check(&mut self) -> Result<(), XLibError> {
// Flush all possible previous errors
unsafe {
xlib::XSync(self.display, 0);
}
let error = self.error.borrow_mut().take();
match error {
None => Ok(()),
Some(inner) => Err(inner),
}
}
/// Sets up a temporary X11 error handler for the duration of the given closure, and allows
/// that closure to check on the latest X11 error at any time.
///
/// # Safety
///
/// The given display pointer *must* be and remain valid for the duration of this function, as
/// well as for the duration of the given `handler` closure.
pub unsafe fn handle<T, F: FnOnce(&mut XErrorHandler) -> T>(
display: *mut xlib::Display, handler: F,
) -> T {
/// # Safety
/// The given display and error pointers *must* be valid for the duration of this function.
unsafe extern "C" fn error_handler(
_dpy: *mut xlib::Display, err: *mut xlib::XErrorEvent,
) -> i32 {
// SAFETY: the error pointer should be safe to access
let err = &*err;
CURRENT_X11_ERROR.with(|error| {
let mut error = error.borrow_mut();
match error.as_mut() {
// If multiple errors occur, keep the first one since that's likely going to be the
// cause of the other errors
Some(_) => 1,
None => {
*error = Some(XLibError::from_event(err));
0
}
}
})
}
// Flush all possible previous errors
unsafe {
xlib::XSync(display, 0);
}
CURRENT_X11_ERROR.with(|error| {
// Make sure to clear any errors from the last call to this function
{
*error.borrow_mut() = None;
}
let old_handler = unsafe { xlib::XSetErrorHandler(Some(error_handler)) };
let panic_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let mut h = XErrorHandler { display, error };
handler(&mut h)
}));
// Whatever happened, restore old error handler
unsafe { xlib::XSetErrorHandler(old_handler) };
match panic_result {
Ok(v) => v,
Err(e) => std::panic::resume_unwind(e),
}
})
}
}
pub struct XLibError {
type_: c_int,
resourceid: xlib::XID,
serial: c_ulong,
error_code: c_uchar,
request_code: c_uchar,
minor_code: c_uchar,
display_name: Box<str>,
}
impl XLibError {
/// # Safety
/// The display pointer inside error must be valid for the duration of this call
unsafe fn from_event(error: &xlib::XErrorEvent) -> Self {
Self {
type_: error.type_,
resourceid: error.resourceid,
serial: error.serial,
error_code: error.error_code,
request_code: error.request_code,
minor_code: error.minor_code,
display_name: Self::get_display_name(error),
}
}
/// # Safety
/// The display pointer inside error must be valid for the duration of this call
unsafe fn get_display_name(error: &xlib::XErrorEvent) -> Box<str> {
let mut buf = [0; 255];
unsafe {
xlib::XGetErrorText(
error.display,
error.error_code.into(),
buf.as_mut_ptr().cast(),
(buf.len() - 1) as i32,
);
}
*buf.last_mut().unwrap() = 0;
// SAFETY: whatever XGetErrorText did or not, we guaranteed there is a nul byte at the end of the buffer
let cstr = unsafe { CStr::from_ptr(buf.as_mut_ptr().cast()) };
cstr.to_string_lossy().into()
}
}
impl Debug for XLibError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XLibError")
.field("error_code", &self.error_code)
.field("error_message", &self.display_name)
.field("minor_code", &self.minor_code)
.field("request_code", &self.request_code)
.field("type", &self.type_)
.field("resource_id", &self.resourceid)
.field("serial", &self.serial)
.finish()
}
}
impl Display for XLibError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "XLib error: {} (error code {})", &self.display_name, self.error_code)
}
}
impl Error for XLibError {}

View File

@@ -0,0 +1,55 @@
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Baseview modifications to druid code:
// - only keep code_to_location function
//! Keyboard types.
#[cfg(any(target_os = "linux", target_os = "macos"))]
use keyboard_types::{Code, Location};
#[cfg(any(target_os = "linux", target_os = "macos"))]
/// Map key code to location.
///
/// The logic for this is adapted from InitKeyEvent in TextInputHandler (in the Mozilla
/// mac port).
///
/// Note: in the original, this is based on kVK constants, but since we don't have those
/// readily available, we use the mapping to code (which should be effectively lossless).
pub fn code_to_location(code: Code) -> Location {
match code {
Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left,
Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right,
Code::Numpad0
| Code::Numpad1
| Code::Numpad2
| Code::Numpad3
| Code::Numpad4
| Code::Numpad5
| Code::Numpad6
| Code::Numpad7
| Code::Numpad8
| Code::Numpad9
| Code::NumpadAdd
| Code::NumpadComma
| Code::NumpadDecimal
| Code::NumpadDivide
| Code::NumpadEnter
| Code::NumpadEqual
| Code::NumpadMultiply
| Code::NumpadSubtract => Location::Numpad,
_ => Location::Standard,
}
}

View File

@@ -0,0 +1,24 @@
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "linux")]
mod x11;
mod clipboard;
mod event;
mod keyboard;
mod mouse_cursor;
mod window;
mod window_info;
mod window_open_options;
#[cfg(feature = "opengl")]
pub mod gl;
pub use clipboard::*;
pub use event::*;
pub use mouse_cursor::MouseCursor;
pub use window::*;
pub use window_info::*;
pub use window_open_options::*;

View File

@@ -0,0 +1,357 @@
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Baseview modifications to druid code:
// - move from_nsstring function to this file
// - update imports, paths etc
//! Conversion of platform keyboard event into cross-platform event.
use std::cell::Cell;
use cocoa::appkit::{NSEvent, NSEventModifierFlags, NSEventType};
use cocoa::base::id;
use cocoa::foundation::NSString;
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Modifiers};
use objc::{msg_send, sel, sel_impl};
use crate::keyboard::code_to_location;
pub(crate) fn from_nsstring(s: id) -> String {
unsafe {
let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len());
let result = std::str::from_utf8_unchecked(slice);
result.into()
}
}
/// State for processing of keyboard events.
///
/// This needs to be stateful for proper processing of dead keys. The current
/// implementation is somewhat primitive and is not based on IME; in the future
/// when IME is implemented, it will need to be redone somewhat, letting the IME
/// be the authoritative source of truth for Unicode string values of keys.
///
/// Most of the logic in this module is adapted from Mozilla, and in particular
/// TextInputHandler.mm.
pub(crate) struct KeyboardState {
last_mods: Cell<NSEventModifierFlags>,
}
/// Convert a macOS platform key code (keyCode field of NSEvent).
///
/// The primary source for this mapping is:
/// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
///
/// It should also match up with CODE_MAP_MAC bindings in
/// NativeKeyToDOMCodeName.h.
fn key_code_to_code(key_code: u16) -> Code {
match key_code {
0x00 => Code::KeyA,
0x01 => Code::KeyS,
0x02 => Code::KeyD,
0x03 => Code::KeyF,
0x04 => Code::KeyH,
0x05 => Code::KeyG,
0x06 => Code::KeyZ,
0x07 => Code::KeyX,
0x08 => Code::KeyC,
0x09 => Code::KeyV,
0x0a => Code::IntlBackslash,
0x0b => Code::KeyB,
0x0c => Code::KeyQ,
0x0d => Code::KeyW,
0x0e => Code::KeyE,
0x0f => Code::KeyR,
0x10 => Code::KeyY,
0x11 => Code::KeyT,
0x12 => Code::Digit1,
0x13 => Code::Digit2,
0x14 => Code::Digit3,
0x15 => Code::Digit4,
0x16 => Code::Digit6,
0x17 => Code::Digit5,
0x18 => Code::Equal,
0x19 => Code::Digit9,
0x1a => Code::Digit7,
0x1b => Code::Minus,
0x1c => Code::Digit8,
0x1d => Code::Digit0,
0x1e => Code::BracketRight,
0x1f => Code::KeyO,
0x20 => Code::KeyU,
0x21 => Code::BracketLeft,
0x22 => Code::KeyI,
0x23 => Code::KeyP,
0x24 => Code::Enter,
0x25 => Code::KeyL,
0x26 => Code::KeyJ,
0x27 => Code::Quote,
0x28 => Code::KeyK,
0x29 => Code::Semicolon,
0x2a => Code::Backslash,
0x2b => Code::Comma,
0x2c => Code::Slash,
0x2d => Code::KeyN,
0x2e => Code::KeyM,
0x2f => Code::Period,
0x30 => Code::Tab,
0x31 => Code::Space,
0x32 => Code::Backquote,
0x33 => Code::Backspace,
0x34 => Code::NumpadEnter,
0x35 => Code::Escape,
0x36 => Code::MetaRight,
0x37 => Code::MetaLeft,
0x38 => Code::ShiftLeft,
0x39 => Code::CapsLock,
// Note: in the linked source doc, this is "OSLeft"
0x3a => Code::AltLeft,
0x3b => Code::ControlLeft,
0x3c => Code::ShiftRight,
// Note: in the linked source doc, this is "OSRight"
0x3d => Code::AltRight,
0x3e => Code::ControlRight,
0x3f => Code::Fn, // No events fired
//0x40 => Code::F17,
0x41 => Code::NumpadDecimal,
0x43 => Code::NumpadMultiply,
0x45 => Code::NumpadAdd,
0x47 => Code::NumLock,
0x48 => Code::AudioVolumeUp,
0x49 => Code::AudioVolumeDown,
0x4a => Code::AudioVolumeMute,
0x4b => Code::NumpadDivide,
0x4c => Code::NumpadEnter,
0x4e => Code::NumpadSubtract,
//0x4f => Code::F18,
//0x50 => Code::F19,
0x51 => Code::NumpadEqual,
0x52 => Code::Numpad0,
0x53 => Code::Numpad1,
0x54 => Code::Numpad2,
0x55 => Code::Numpad3,
0x56 => Code::Numpad4,
0x57 => Code::Numpad5,
0x58 => Code::Numpad6,
0x59 => Code::Numpad7,
//0x5a => Code::F20,
0x5b => Code::Numpad8,
0x5c => Code::Numpad9,
0x5d => Code::IntlYen,
0x5e => Code::IntlRo,
0x5f => Code::NumpadComma,
0x60 => Code::F5,
0x61 => Code::F6,
0x62 => Code::F7,
0x63 => Code::F3,
0x64 => Code::F8,
0x65 => Code::F9,
0x66 => Code::Lang2,
0x67 => Code::F11,
0x68 => Code::Lang1,
// Note: this is listed as F13, but in testing with a standard
// USB kb, this the code produced by PrtSc.
0x69 => Code::PrintScreen,
//0x6a => Code::F16,
//0x6b => Code::F14,
0x6d => Code::F10,
0x6e => Code::ContextMenu,
0x6f => Code::F12,
//0x71 => Code::F15,
0x72 => Code::Help,
0x73 => Code::Home,
0x74 => Code::PageUp,
0x75 => Code::Delete,
0x76 => Code::F4,
0x77 => Code::End,
0x78 => Code::F2,
0x79 => Code::PageDown,
0x7a => Code::F1,
0x7b => Code::ArrowLeft,
0x7c => Code::ArrowRight,
0x7d => Code::ArrowDown,
0x7e => Code::ArrowUp,
_ => Code::Unidentified,
}
}
/// Convert code to key.
///
/// On macOS, for non-printable keys, the keyCode we get from the event serves is
/// really more of a key than a physical scan code.
///
/// When this function returns None, the code can be considered printable.
///
/// The logic for this function is derived from KEY_MAP_COCOA bindings in
/// NativeKeyToDOMKeyName.h.
fn code_to_key(code: Code) -> Option<Key> {
Some(match code {
Code::Escape => Key::Escape,
Code::ShiftLeft | Code::ShiftRight => Key::Shift,
Code::AltLeft | Code::AltRight => Key::Alt,
Code::MetaLeft | Code::MetaRight => Key::Meta,
Code::ControlLeft | Code::ControlRight => Key::Control,
Code::CapsLock => Key::CapsLock,
// kVK_ANSI_KeypadClear
Code::NumLock => Key::Clear,
Code::Fn => Key::Fn,
Code::F1 => Key::F1,
Code::F2 => Key::F2,
Code::F3 => Key::F3,
Code::F4 => Key::F4,
Code::F5 => Key::F5,
Code::F6 => Key::F6,
Code::F7 => Key::F7,
Code::F8 => Key::F8,
Code::F9 => Key::F9,
Code::F10 => Key::F10,
Code::F11 => Key::F11,
Code::F12 => Key::F12,
Code::Pause => Key::Pause,
Code::ScrollLock => Key::ScrollLock,
Code::PrintScreen => Key::PrintScreen,
Code::Insert => Key::Insert,
Code::Delete => Key::Delete,
Code::Tab => Key::Tab,
Code::Backspace => Key::Backspace,
Code::ContextMenu => Key::ContextMenu,
// kVK_JIS_Kana
Code::Lang1 => Key::KanjiMode,
// kVK_JIS_Eisu
Code::Lang2 => Key::Eisu,
Code::Home => Key::Home,
Code::End => Key::End,
Code::PageUp => Key::PageUp,
Code::PageDown => Key::PageDown,
Code::ArrowLeft => Key::ArrowLeft,
Code::ArrowRight => Key::ArrowRight,
Code::ArrowUp => Key::ArrowUp,
Code::ArrowDown => Key::ArrowDown,
Code::Enter => Key::Enter,
Code::NumpadEnter => Key::Enter,
Code::Help => Key::Help,
_ => return None,
})
}
fn is_valid_key(s: &str) -> bool {
match s.chars().next() {
None => false,
Some(c) => c >= ' ' && c != '\x7f' && !('\u{e000}'..'\u{f900}').contains(&c),
}
}
fn is_modifier_code(code: Code) -> bool {
matches!(
code,
Code::ShiftLeft
| Code::ShiftRight
| Code::AltLeft
| Code::AltRight
| Code::ControlLeft
| Code::ControlRight
| Code::MetaLeft
| Code::MetaRight
| Code::CapsLock
| Code::Help
)
}
impl KeyboardState {
pub(crate) fn new() -> KeyboardState {
let last_mods = Cell::new(NSEventModifierFlags::empty());
KeyboardState { last_mods }
}
pub(crate) fn last_mods(&self) -> NSEventModifierFlags {
self.last_mods.get()
}
pub(crate) fn process_native_event(&self, event: id) -> Option<KeyboardEvent> {
unsafe {
let event_type = event.eventType();
let key_code = event.keyCode();
let code = key_code_to_code(key_code);
let location = code_to_location(code);
let raw_mods = event.modifierFlags();
let modifiers = make_modifiers(raw_mods);
let state = match event_type {
NSEventType::NSKeyDown => KeyState::Down,
NSEventType::NSKeyUp => KeyState::Up,
NSEventType::NSFlagsChanged => {
// We use `bits` here because we want to distinguish the
// device dependent bits (when both left and right keys
// may be pressed, for example).
let any_down = raw_mods.bits() & !self.last_mods.get().bits();
self.last_mods.set(raw_mods);
if is_modifier_code(code) {
if any_down == 0 {
KeyState::Up
} else {
KeyState::Down
}
} else {
// HandleFlagsChanged has some logic for this; it might
// happen when an app is deactivated by Command-Tab. In
// that case, the best thing to do is synthesize the event
// from the modifiers. But a challenge there is that we
// might get multiple events.
return None;
}
}
_ => unreachable!(),
};
let is_composing = false;
let repeat: bool = event_type == NSEventType::NSKeyDown && msg_send![event, isARepeat];
let key = if let Some(key) = code_to_key(code) {
key
} else {
let characters = from_nsstring(event.characters());
if is_valid_key(&characters) {
Key::Character(characters)
} else {
let chars_ignoring = from_nsstring(event.charactersIgnoringModifiers());
if is_valid_key(&chars_ignoring) {
Key::Character(chars_ignoring)
} else {
// There may be more heroic things we can do here.
Key::Unidentified
}
}
};
let event =
KeyboardEvent { code, key, location, modifiers, state, is_composing, repeat };
Some(event)
}
}
}
const MODIFIER_MAP: &[(NSEventModifierFlags, Modifiers)] = &[
(NSEventModifierFlags::NSShiftKeyMask, Modifiers::SHIFT),
(NSEventModifierFlags::NSAlternateKeyMask, Modifiers::ALT),
(NSEventModifierFlags::NSControlKeyMask, Modifiers::CONTROL),
(NSEventModifierFlags::NSCommandKeyMask, Modifiers::META),
(NSEventModifierFlags::NSAlphaShiftKeyMask, Modifiers::CAPS_LOCK),
];
pub(crate) fn make_modifiers(raw: NSEventModifierFlags) -> Modifiers {
let mut modifiers = Modifiers::empty();
for &(flags, mods) in MODIFIER_MAP {
if raw.contains(flags) {
modifiers |= mods;
}
}
modifiers
}

View File

@@ -0,0 +1,21 @@
// This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125
// Eventually we should migrate to the objc2 crate and remove this.
#![allow(unexpected_cfgs)]
mod keyboard;
mod view;
mod window;
pub use window::*;
#[allow(non_upper_case_globals)]
mod consts {
use cocoa::foundation::NSUInteger;
pub const NSDragOperationNone: NSUInteger = 0;
pub const NSDragOperationCopy: NSUInteger = 1;
pub const NSDragOperationLink: NSUInteger = 2;
pub const NSDragOperationGeneric: NSUInteger = 4;
pub const NSDragOperationMove: NSUInteger = 16;
}
use consts::*;

View File

@@ -0,0 +1,617 @@
use std::ffi::c_void;
use cocoa::appkit::{NSEvent, NSFilenamesPboardType, NSView, NSWindow};
use cocoa::base::{id, nil, BOOL, NO, YES};
use cocoa::foundation::{NSArray, NSPoint, NSRect, NSSize, NSUInteger};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use uuid::Uuid;
use crate::MouseEvent::{ButtonPressed, ButtonReleased};
use crate::{
DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, Point, ScrollDelta, Size,
WindowEvent, WindowInfo, WindowOpenOptions,
};
use super::keyboard::{from_nsstring, make_modifiers};
use super::window::WindowState;
use super::{
NSDragOperationCopy, NSDragOperationGeneric, NSDragOperationLink, NSDragOperationMove,
NSDragOperationNone,
};
/// Name of the field used to store the `WindowState` pointer.
pub(super) const BASEVIEW_STATE_IVAR: &str = "baseview_state";
#[link(name = "AppKit", kind = "framework")]
extern "C" {
static NSWindowDidBecomeKeyNotification: id;
static NSWindowDidResignKeyNotification: id;
}
macro_rules! add_simple_mouse_class_method {
($class:ident, $sel:ident, $event:expr) => {
#[allow(non_snake_case)]
extern "C" fn $sel(this: &Object, _: Sel, _: id){
let state = unsafe { WindowState::from_view(this) };
state.trigger_event(Event::Mouse($event));
}
$class.add_method(
sel!($sel:),
$sel as extern "C" fn(&Object, Sel, id),
);
};
}
/// Similar to [add_simple_mouse_class_method!], but this creates its own event object for the
/// press/release event and adds the active modifier keys to that event.
macro_rules! add_mouse_button_class_method {
($class:ident, $sel:ident, $event_ty:ident, $button:expr) => {
#[allow(non_snake_case)]
extern "C" fn $sel(this: &Object, _: Sel, event: id){
let state = unsafe { WindowState::from_view(this) };
let modifiers = unsafe { NSEvent::modifierFlags(event) };
state.trigger_event(Event::Mouse($event_ty {
button: $button,
modifiers: make_modifiers(modifiers),
}));
}
$class.add_method(
sel!($sel:),
$sel as extern "C" fn(&Object, Sel, id),
);
};
}
macro_rules! add_simple_keyboard_class_method {
($class:ident, $sel:ident) => {
#[allow(non_snake_case)]
extern "C" fn $sel(this: &Object, _: Sel, event: id){
let state = unsafe { WindowState::from_view(this) };
if let Some(key_event) = state.process_native_key_event(event){
let status = state.trigger_event(Event::Keyboard(key_event));
if let EventStatus::Ignored = status {
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), $sel:event];
}
}
}
}
$class.add_method(
sel!($sel:),
$sel as extern "C" fn(&Object, Sel, id),
);
};
}
unsafe fn register_notification(observer: id, notification_name: id, object: id) {
let notification_center: id = msg_send![class!(NSNotificationCenter), defaultCenter];
let _: () = msg_send![
notification_center,
addObserver:observer
selector:sel!(handleNotification:)
name:notification_name
object:object
];
}
pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id {
let class = create_view_class();
let view: id = msg_send![class, alloc];
let size = window_options.size;
view.initWithFrame_(NSRect::new(NSPoint::new(0., 0.), NSSize::new(size.width, size.height)));
register_notification(view, NSWindowDidBecomeKeyNotification, nil);
register_notification(view, NSWindowDidResignKeyNotification, nil);
let _: id = msg_send![
view,
registerForDraggedTypes: NSArray::arrayWithObjects(nil, &[NSFilenamesPboardType])
];
view
}
unsafe fn create_view_class() -> &'static Class {
// Use unique class names so that there are no conflicts between different
// instances. The class is deleted when the view is released. Previously,
// the class was stored in a OnceCell after creation. This way, we didn't
// have to recreate it each time a view was opened, but now we don't leave
// any class definitions lying around when the plugin is closed.
let class_name = format!("BaseviewNSView_{}", Uuid::new_v4().to_simple());
let mut class = ClassDecl::new(&class_name, class!(NSView)).unwrap();
class.add_method(
sel!(acceptsFirstResponder),
property_yes as extern "C" fn(&Object, Sel) -> BOOL,
);
class.add_method(
sel!(becomeFirstResponder),
become_first_responder as extern "C" fn(&Object, Sel) -> BOOL,
);
class.add_method(
sel!(resignFirstResponder),
resign_first_responder as extern "C" fn(&Object, Sel) -> BOOL,
);
class.add_method(sel!(isFlipped), property_yes as extern "C" fn(&Object, Sel) -> BOOL);
class.add_method(
sel!(preservesContentInLiveResize),
property_no as extern "C" fn(&Object, Sel) -> BOOL,
);
class.add_method(
sel!(acceptsFirstMouse:),
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(
sel!(windowShouldClose:),
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel));
class.add_method(
sel!(viewWillMoveToWindow:),
view_will_move_to_window as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(updateTrackingAreas:),
update_tracking_areas as extern "C" fn(&Object, Sel, id),
);
class.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(&Object, Sel, id));
class.add_method(sel!(mouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
class.add_method(sel!(rightMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
class.add_method(sel!(otherMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(&Object, Sel, id));
class.add_method(
sel!(viewDidChangeBackingProperties:),
view_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(setFrameSize:),
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
);
class.add_method(
sel!(draggingEntered:),
dragging_entered as extern "C" fn(&Object, Sel, id) -> NSUInteger,
);
class.add_method(
sel!(prepareForDragOperation:),
prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(
sel!(performDragOperation:),
perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(
sel!(draggingUpdated:),
dragging_updated as extern "C" fn(&Object, Sel, id) -> NSUInteger,
);
class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id));
class.add_method(
sel!(handleNotification:),
handle_notification as extern "C" fn(&Object, Sel, id),
);
add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left);
add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left);
add_mouse_button_class_method!(class, rightMouseDown, ButtonPressed, MouseButton::Right);
add_mouse_button_class_method!(class, rightMouseUp, ButtonReleased, MouseButton::Right);
add_mouse_button_class_method!(class, otherMouseDown, ButtonPressed, MouseButton::Middle);
add_mouse_button_class_method!(class, otherMouseUp, ButtonReleased, MouseButton::Middle);
add_simple_mouse_class_method!(class, mouseEntered, MouseEvent::CursorEntered);
add_simple_mouse_class_method!(class, mouseExited, MouseEvent::CursorLeft);
add_simple_keyboard_class_method!(class, keyDown);
add_simple_keyboard_class_method!(class, keyUp);
add_simple_keyboard_class_method!(class, flagsChanged);
class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR);
class.register()
}
extern "C" fn property_yes(_this: &Object, _sel: Sel) -> BOOL {
YES
}
extern "C" fn property_no(_this: &Object, _sel: Sel) -> BOOL {
NO
}
extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL {
YES
}
extern "C" fn become_first_responder(this: &Object, _sel: Sel) -> BOOL {
let state = unsafe { WindowState::from_view(this) };
let is_key_window = unsafe {
let window: id = msg_send![this, window];
if window != nil {
let is_key_window: BOOL = msg_send![window, isKeyWindow];
is_key_window == YES
} else {
false
}
};
if is_key_window {
state.trigger_deferrable_event(Event::Window(WindowEvent::Focused));
}
YES
}
extern "C" fn resign_first_responder(this: &Object, _sel: Sel) -> BOOL {
let state = unsafe { WindowState::from_view(this) };
state.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused));
YES
}
extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL {
let state = unsafe { WindowState::from_view(this) };
state.trigger_event(Event::Window(WindowEvent::WillClose));
state.window_inner.close();
NO
}
extern "C" fn dealloc(this: &mut Object, _sel: Sel) {
unsafe {
let class = msg_send![this, class];
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), dealloc];
// Delete class
::objc::runtime::objc_disposeClassPair(class);
}
}
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) {
unsafe {
let ns_window: *mut Object = msg_send![this, window];
let scale_factor: f64 =
if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) };
let state = WindowState::from_view(this);
let bounds: NSRect = msg_send![this, bounds];
let new_window_info = WindowInfo::from_logical_size(
Size::new(bounds.size.width, bounds.size.height),
scale_factor,
);
let window_info = state.window_info.get();
// Only send the event when the window's size has actually changed to be in line with the
// other platform implementations
if new_window_info.physical_size() != window_info.physical_size() {
state.window_info.set(new_window_info);
state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info)));
}
}
}
/// Init/reinit tracking area
///
/// Info:
/// https://developer.apple.com/documentation/appkit/nstrackingarea
/// https://developer.apple.com/documentation/appkit/nstrackingarea/options
/// https://developer.apple.com/documentation/appkit/nstrackingareaoptions
unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object) {
let options: usize = {
let mouse_entered_and_exited = 0x01;
let tracking_mouse_moved = 0x02;
let tracking_cursor_update = 0x04;
let tracking_active_in_active_app = 0x40;
let tracking_in_visible_rect = 0x200;
let tracking_enabled_during_mouse_drag = 0x400;
mouse_entered_and_exited
| tracking_mouse_moved
| tracking_cursor_update
| tracking_active_in_active_app
| tracking_in_visible_rect
| tracking_enabled_during_mouse_drag
};
let bounds: NSRect = msg_send![this, bounds];
*tracking_area = msg_send![tracking_area,
initWithRect:bounds
options:options
owner:this
userInfo:nil
];
}
extern "C" fn view_will_move_to_window(this: &Object, _self: Sel, new_window: id) {
unsafe {
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
let tracking_area_count = NSArray::count(tracking_areas);
if new_window == nil {
if tracking_area_count != 0 {
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
let _: () = msg_send![this, removeTrackingArea: tracking_area];
let _: () = msg_send![tracking_area, release];
}
} else {
if tracking_area_count == 0 {
let class = Class::get("NSTrackingArea").unwrap();
let tracking_area: *mut Object = msg_send![class, alloc];
reinit_tracking_area(this, tracking_area);
let _: () = msg_send![this, addTrackingArea: tracking_area];
}
let _: () = msg_send![new_window, setAcceptsMouseMovedEvents: YES];
let _: () = msg_send![new_window, makeFirstResponder: this];
}
}
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), viewWillMoveToWindow: new_window];
}
}
extern "C" fn update_tracking_areas(this: &Object, _self: Sel, _: id) {
unsafe {
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
reinit_tracking_area(this, tracking_area);
}
}
extern "C" fn set_frame_size(this: &Object, _: Sel, new_size: NSSize) {
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), setFrameSize: new_size];
}
let state_ptr: *const c_void = unsafe { *this.get_ivar(BASEVIEW_STATE_IVAR) };
if state_ptr.is_null() {
return;
}
let state = unsafe { WindowState::from_view(this) };
let scale_factor = unsafe {
let ns_window: *mut Object = msg_send![this, window];
if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) }
};
let new_window_info = WindowInfo::from_logical_size(
Size::new(new_size.width, new_size.height),
scale_factor,
);
let old_info = state.window_info.get();
if new_window_info.physical_size() != old_info.physical_size() {
state.window_info.set(new_window_info);
#[cfg(feature = "opengl")]
if let Some(gl_context) = &state.window_inner.gl_context {
gl_context.resize(new_size);
}
state.trigger_deferrable_event(Event::Window(WindowEvent::Resized(new_window_info)));
}
}
fn get_screen_position() -> Point {
unsafe {
let screen_point: NSPoint = msg_send![class!(NSEvent), mouseLocation];
let main_screen: id = msg_send![class!(NSScreen), mainScreen];
let screen_frame: NSRect = msg_send![main_screen, frame];
Point {
x: screen_point.x,
y: screen_frame.size.height - screen_point.y,
}
}
}
extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) {
let state = unsafe { WindowState::from_view(this) };
let point: NSPoint = unsafe {
let point = NSEvent::locationInWindow(event);
msg_send![this, convertPoint:point fromView:nil]
};
let modifiers = unsafe { NSEvent::modifierFlags(event) };
let position = Point { x: point.x, y: point.y };
let screen_position = get_screen_position();
state.trigger_event(Event::Mouse(MouseEvent::CursorMoved {
position,
screen_position,
modifiers: make_modifiers(modifiers),
}));
}
extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) {
let state = unsafe { WindowState::from_view(this) };
let delta = unsafe {
let x = NSEvent::scrollingDeltaX(event) as f32;
let y = NSEvent::scrollingDeltaY(event) as f32;
if NSEvent::hasPreciseScrollingDeltas(event) != NO {
ScrollDelta::Pixels { x, y }
} else {
ScrollDelta::Lines { x, y }
}
};
let modifiers = unsafe { NSEvent::modifierFlags(event) };
state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled {
delta,
modifiers: make_modifiers(modifiers),
}));
}
fn get_drag_position(sender: id) -> (Point, Point) {
let point: NSPoint = unsafe { msg_send![sender, draggingLocation] };
(Point::new(point.x, point.y), get_screen_position())
}
fn get_drop_data(sender: id) -> DropData {
if sender == nil {
return DropData::None;
}
unsafe {
let pasteboard: id = msg_send![sender, draggingPasteboard];
let file_list: id = msg_send![pasteboard, propertyListForType: NSFilenamesPboardType];
if file_list == nil {
return DropData::None;
}
let mut files = vec![];
for i in 0..NSArray::count(file_list) {
let data = NSArray::objectAtIndex(file_list, i);
files.push(from_nsstring(data).into());
}
DropData::Files(files)
}
}
fn on_event(window_state: &WindowState, event: MouseEvent) -> NSUInteger {
let event_status = window_state.trigger_event(Event::Mouse(event));
match event_status {
EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperationCopy,
EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperationMove,
EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperationLink,
EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperationGeneric,
_ => NSDragOperationNone,
}
}
extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteger {
let state = unsafe { WindowState::from_view(this) };
let modifiers = state.keyboard_state().last_mods();
let drop_data = get_drop_data(sender);
let (position, screen_position) = get_drag_position(sender);
let event = MouseEvent::DragEntered {
position,
screen_position,
modifiers: make_modifiers(modifiers),
data: drop_data,
};
on_event(&state, event)
}
extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteger {
let state = unsafe { WindowState::from_view(this) };
let modifiers = state.keyboard_state().last_mods();
let drop_data = get_drop_data(sender);
let (position, screen_position) = get_drag_position(sender);
let event = MouseEvent::DragMoved {
position,
screen_position,
modifiers: make_modifiers(modifiers),
data: drop_data,
};
on_event(&state, event)
}
extern "C" fn prepare_for_drag_operation(_this: &Object, _sel: Sel, _sender: id) -> BOOL {
// Always accept drag operation if we get this far
// This function won't be called unless dragging_entered/updated
// has returned an acceptable operation
YES
}
extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BOOL {
let state = unsafe { WindowState::from_view(this) };
let modifiers = state.keyboard_state().last_mods();
let drop_data = get_drop_data(sender);
let (position, screen_position) = get_drag_position(sender);
let event = MouseEvent::DragDropped {
position,
screen_position,
modifiers: make_modifiers(modifiers),
data: drop_data,
};
let event_status = state.trigger_event(Event::Mouse(event));
match event_status {
EventStatus::AcceptDrop(_) => YES,
_ => NO,
}
}
extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) {
let state = unsafe { WindowState::from_view(this) };
on_event(&state, MouseEvent::DragLeft);
}
extern "C" fn handle_notification(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let state = WindowState::from_view(this);
// The subject of the notication, in this case an NSWindow object.
let notification_object: id = msg_send![notification, object];
// The NSWindow object associated with our NSView.
let window: id = msg_send![this, window];
let first_responder: id = msg_send![window, firstResponder];
// Only trigger focus events if the NSWindow that's being notified about is our window,
// and if the window's first responder is our NSView.
// If the first responder isn't our NSView, the focus events will instead be triggered
// by the becomeFirstResponder and resignFirstResponder methods on the NSView itself.
if notification_object == window && std::ptr::eq(first_responder, this) {
let is_key_window: BOOL = msg_send![window, isKeyWindow];
state.trigger_event(Event::Window(if is_key_window == YES {
WindowEvent::Focused
} else {
WindowEvent::Unfocused
}));
}
}
}

View File

@@ -0,0 +1,484 @@
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::ffi::c_void;
use std::ptr;
use std::rc::Rc;
use cocoa::appkit::{
NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered,
NSPasteboard, NSView, NSWindow, NSWindowStyleMask,
};
use cocoa::base::{id, nil, BOOL, NO, YES};
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString};
use core_foundation::runloop::{
CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, __CFRunLoopTimer, kCFRunLoopDefaultMode,
};
use keyboard_types::KeyboardEvent;
use objc::class;
use objc::{msg_send, runtime::Object, sel, sel_impl};
use raw_window_handle::{
AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle,
RawDisplayHandle, RawWindowHandle,
};
use crate::{
Event, EventStatus, MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions,
WindowScalePolicy,
};
use super::keyboard::KeyboardState;
use super::view::{create_view, BASEVIEW_STATE_IVAR};
#[cfg(feature = "opengl")]
use crate::gl::{GlConfig, GlContext};
pub struct WindowHandle {
state: Rc<WindowState>,
}
impl WindowHandle {
pub fn close(&mut self) {
self.state.window_inner.close();
}
pub fn is_open(&self) -> bool {
self.state.window_inner.open.get()
}
}
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
self.state.window_inner.raw_window_handle()
}
}
pub(super) struct WindowInner {
open: Cell<bool>,
/// Only set if we created the parent window, i.e. we are running in
/// parentless mode
ns_app: Cell<Option<id>>,
/// Only set if we created the parent window, i.e. we are running in
/// parentless mode
ns_window: Cell<Option<id>>,
/// Our subclassed NSView
ns_view: id,
#[cfg(feature = "opengl")]
pub(super) gl_context: Option<GlContext>,
}
impl WindowInner {
pub(super) fn close(&self) {
if self.open.get() {
self.open.set(false);
unsafe {
// Take back ownership of the NSView's Rc<WindowState>
let state_ptr: *const c_void = *(*self.ns_view).get_ivar(BASEVIEW_STATE_IVAR);
let window_state = Rc::from_raw(state_ptr as *mut WindowState);
// Cancel the frame timer
if let Some(frame_timer) = window_state.frame_timer.take() {
CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode);
}
// Deregister NSView from NotificationCenter.
let notification_center: id =
msg_send![class!(NSNotificationCenter), defaultCenter];
let () = msg_send![notification_center, removeObserver:self.ns_view];
drop(window_state);
// Close the window if in non-parented mode
if let Some(ns_window) = self.ns_window.take() {
ns_window.close();
}
// Ensure that the NSView is detached from the parent window
self.ns_view.removeFromSuperview();
let () = msg_send![self.ns_view as id, release];
// If in non-parented mode, we want to also quit the app altogether
let app = self.ns_app.take();
if let Some(app) = app {
app.stop_(app);
}
}
}
}
fn raw_window_handle(&self) -> RawWindowHandle {
if self.open.get() {
let ns_window = self.ns_window.get().unwrap_or(ptr::null_mut()) as *mut c_void;
let mut handle = AppKitWindowHandle::empty();
handle.ns_window = ns_window;
handle.ns_view = self.ns_view as *mut c_void;
return RawWindowHandle::AppKit(handle);
}
RawWindowHandle::AppKit(AppKitWindowHandle::empty())
}
}
pub struct Window<'a> {
inner: &'a WindowInner,
}
impl<'a> Window<'a> {
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
where
P: HasRawWindowHandle,
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let pool = unsafe { NSAutoreleasePool::new(nil) };
let scaling = match options.scale {
WindowScalePolicy::ScaleFactor(scale) => scale,
WindowScalePolicy::SystemScaleFactor => 1.0,
};
let window_info = WindowInfo::from_logical_size(options.size, scaling);
let handle = if let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() {
handle
} else {
panic!("Not a macOS window");
};
let ns_view = unsafe { create_view(&options) };
let window_inner = WindowInner {
open: Cell::new(true),
ns_app: Cell::new(None),
ns_window: Cell::new(None),
ns_view,
#[cfg(feature = "opengl")]
gl_context: options
.gl_config
.map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)),
};
let window_handle = Self::init(window_inner, window_info, build);
unsafe {
let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view];
let () = msg_send![pool, drain];
}
window_handle
}
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let pool = unsafe { NSAutoreleasePool::new(nil) };
// It seems prudent to run NSApp() here before doing other
// work. It runs [NSApplication sharedApplication], which is
// what is run at the very start of the Xcode-generated main
// function of a cocoa app according to:
// https://developer.apple.com/documentation/appkit/nsapplication
let app = unsafe { NSApp() };
unsafe {
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
}
let scaling = match options.scale {
WindowScalePolicy::ScaleFactor(scale) => scale,
WindowScalePolicy::SystemScaleFactor => 1.0,
};
let window_info = WindowInfo::from_logical_size(options.size, scaling);
let rect = NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(window_info.logical_size().width, window_info.logical_size().height),
);
let ns_window = unsafe {
let ns_window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_(
rect,
NSWindowStyleMask::NSTitledWindowMask
| NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask,
NSBackingStoreBuffered,
NO,
);
ns_window.center();
let title = NSString::alloc(nil).init_str(&options.title).autorelease();
ns_window.setTitle_(title);
ns_window.makeKeyAndOrderFront_(nil);
ns_window
};
let ns_view = unsafe { create_view(&options) };
let window_inner = WindowInner {
open: Cell::new(true),
ns_app: Cell::new(Some(app)),
ns_window: Cell::new(Some(ns_window)),
ns_view,
#[cfg(feature = "opengl")]
gl_context: options
.gl_config
.map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)),
};
let _ = Self::init(window_inner, window_info, build);
unsafe {
ns_window.setContentView_(ns_view);
ns_window.setDelegate_(ns_view);
let () = msg_send![pool, drain];
app.run();
}
}
fn init<H, B>(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let mut window = crate::Window::new(Window { inner: &window_inner });
let window_handler = Box::new(build(&mut window));
let ns_view = window_inner.ns_view;
let window_state = Rc::new(WindowState {
window_inner,
window_handler: RefCell::new(window_handler),
keyboard_state: KeyboardState::new(),
frame_timer: Cell::new(None),
window_info: Cell::new(window_info),
deferred_events: RefCell::default(),
});
let window_state_ptr = Rc::into_raw(Rc::clone(&window_state));
unsafe {
(*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void);
WindowState::setup_timer(window_state_ptr);
}
WindowHandle { state: window_state }
}
pub fn close(&mut self) {
self.inner.close();
}
pub fn has_focus(&mut self) -> bool {
unsafe {
let view = self.inner.ns_view.as_mut().unwrap();
let window: id = msg_send![view, window];
if window == nil {
return false;
};
let first_responder: id = msg_send![window, firstResponder];
let is_key_window: BOOL = msg_send![window, isKeyWindow];
let is_focused: BOOL = msg_send![view, isEqual: first_responder];
is_key_window == YES && is_focused == YES
}
}
pub fn focus(&mut self) {
unsafe {
let view = self.inner.ns_view.as_mut().unwrap();
let window: id = msg_send![view, window];
if window != nil {
msg_send![window, makeFirstResponder:view]
}
}
}
pub fn resize(&mut self, size: Size) {
if self.inner.open.get() {
let size = NSSize::new(size.width.round(), size.height.round());
unsafe { NSView::setFrameSize(self.inner.ns_view, size) };
if let Some(ns_window) = self.inner.ns_window.get() {
unsafe { NSWindow::setContentSize_(ns_window, size) };
}
}
}
pub fn physical_size(&self) -> crate::PhySize {
unsafe {
let frame: NSRect = NSView::frame(self.inner.ns_view);
let ns_window: *mut Object = msg_send![self.inner.ns_view, window];
let scale: f64 = if ns_window.is_null() {
1.0
} else {
NSWindow::backingScaleFactor(ns_window)
};
crate::PhySize {
width: (frame.size.width * scale).round() as u32,
height: (frame.size.height * scale).round() as u32,
}
}
}
pub fn set_mouse_cursor(&mut self, _mouse_cursor: MouseCursor) {
todo!()
}
#[cfg(feature = "opengl")]
pub fn gl_context(&self) -> Option<&GlContext> {
self.inner.gl_context.as_ref()
}
#[cfg(feature = "opengl")]
fn create_gl_context(ns_window: Option<id>, ns_view: id, config: GlConfig) -> GlContext {
let mut handle = AppKitWindowHandle::empty();
handle.ns_window = ns_window.unwrap_or(ptr::null_mut()) as *mut c_void;
handle.ns_view = ns_view as *mut c_void;
let handle = RawWindowHandle::AppKit(handle);
unsafe { GlContext::create(&handle, config).expect("Could not create OpenGL context") }
}
}
pub(super) struct WindowState {
pub(super) window_inner: WindowInner,
window_handler: RefCell<Box<dyn WindowHandler>>,
keyboard_state: KeyboardState,
frame_timer: Cell<Option<CFRunLoopTimer>>,
/// The last known window info for this window.
pub window_info: Cell<WindowInfo>,
/// Events that will be triggered at the end of `window_handler`'s borrow.
deferred_events: RefCell<VecDeque<Event>>,
}
impl WindowState {
/// Gets the `WindowState` held by a given `NSView`.
///
/// This method returns a cloned `Rc<WindowState>` rather than just a `&WindowState`, since the
/// original `Rc<WindowState>` owned by the `NSView` can be dropped at any time
/// (including during an event handler).
pub(super) unsafe fn from_view(view: &Object) -> Rc<WindowState> {
let state_ptr: *const c_void = *view.get_ivar(BASEVIEW_STATE_IVAR);
let state_rc = Rc::from_raw(state_ptr as *const WindowState);
let state = Rc::clone(&state_rc);
let _ = Rc::into_raw(state_rc);
state
}
/// Trigger the event immediately and return the event status.
/// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`).
pub(super) fn trigger_event(&self, event: Event) -> EventStatus {
let mut window = crate::Window::new(Window { inner: &self.window_inner });
let mut window_handler = self.window_handler.borrow_mut();
let status = window_handler.on_event(&mut window, event);
self.send_deferred_events(window_handler.as_mut());
status
}
/// Trigger the event immediately if `window_handler` can be borrowed mutably,
/// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends.
/// As this method might result in the event triggering asynchronously, it can't reliably return the event status.
pub(super) fn trigger_deferrable_event(&self, event: Event) {
if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() {
let mut window = crate::Window::new(Window { inner: &self.window_inner });
window_handler.on_event(&mut window, event);
self.send_deferred_events(window_handler.as_mut());
} else {
self.deferred_events.borrow_mut().push_back(event);
}
}
pub(super) fn trigger_frame(&self) {
let mut window = crate::Window::new(Window { inner: &self.window_inner });
let mut window_handler = self.window_handler.borrow_mut();
window_handler.on_frame(&mut window);
self.send_deferred_events(window_handler.as_mut());
}
pub(super) fn keyboard_state(&self) -> &KeyboardState {
&self.keyboard_state
}
pub(super) fn process_native_key_event(&self, event: *mut Object) -> Option<KeyboardEvent> {
self.keyboard_state.process_native_event(event)
}
unsafe fn setup_timer(window_state_ptr: *const WindowState) {
extern "C" fn timer_callback(_: *mut __CFRunLoopTimer, window_state_ptr: *mut c_void) {
unsafe {
let window_state = &*(window_state_ptr as *const WindowState);
window_state.trigger_frame();
}
}
let mut timer_context = CFRunLoopTimerContext {
version: 0,
info: window_state_ptr as *mut c_void,
retain: None,
release: None,
copyDescription: None,
};
let timer = CFRunLoopTimer::new(0.0, 0.015, 0, 0, timer_callback, &mut timer_context);
CFRunLoop::get_current().add_timer(&timer, kCFRunLoopDefaultMode);
(*window_state_ptr).frame_timer.set(Some(timer));
}
fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) {
let mut window = crate::Window::new(Window { inner: &self.window_inner });
loop {
let next_event = self.deferred_events.borrow_mut().pop_front();
if let Some(event) = next_event {
window_handler.on_event(&mut window, event);
} else {
break;
}
}
}
}
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
fn raw_window_handle(&self) -> RawWindowHandle {
self.inner.raw_window_handle()
}
}
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::AppKit(AppKitDisplayHandle::empty())
}
}
pub fn copy_to_clipboard(string: &str) {
unsafe {
let pb = NSPasteboard::generalPasteboard(nil);
let ns_str = NSString::alloc(nil).init_str(string);
pb.clearContents();
pb.setString_forType(ns_str, cocoa::appkit::NSPasteboardTypeString);
}
}

View File

@@ -0,0 +1,44 @@
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash)]
pub enum MouseCursor {
#[default]
Default,
Hand,
HandGrabbing,
Help,
Hidden,
Text,
VerticalText,
Working,
PtrWorking,
NotAllowed,
PtrNotAllowed,
ZoomIn,
ZoomOut,
Alias,
Copy,
Move,
AllScroll,
Cell,
Crosshair,
EResize,
NResize,
NeResize,
NwResize,
SResize,
SeResize,
SwResize,
WResize,
EwResize,
NsResize,
NwseResize,
NeswResize,
ColResize,
RowResize,
}

View File

@@ -0,0 +1,54 @@
use crate::MouseCursor;
use winapi::{
shared::ntdef::LPCWSTR,
um::winuser::{
IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL,
IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT,
},
};
pub fn cursor_to_lpcwstr(cursor: MouseCursor) -> LPCWSTR {
match cursor {
MouseCursor::Default => IDC_ARROW,
MouseCursor::Hand => IDC_HAND,
MouseCursor::HandGrabbing => IDC_SIZEALL,
MouseCursor::Help => IDC_HELP,
// an empty LPCWSTR results in the cursor being hidden
MouseCursor::Hidden => std::ptr::null(),
MouseCursor::Text => IDC_IBEAM,
MouseCursor::VerticalText => IDC_IBEAM,
MouseCursor::Working => IDC_WAIT,
MouseCursor::PtrWorking => IDC_APPSTARTING,
MouseCursor::NotAllowed => IDC_NO,
MouseCursor::PtrNotAllowed => IDC_NO,
MouseCursor::ZoomIn => IDC_ARROW,
MouseCursor::ZoomOut => IDC_ARROW,
MouseCursor::Alias => IDC_ARROW,
MouseCursor::Copy => IDC_ARROW,
MouseCursor::Move => IDC_SIZEALL,
MouseCursor::AllScroll => IDC_SIZEALL,
MouseCursor::Cell => IDC_CROSS,
MouseCursor::Crosshair => IDC_CROSS,
MouseCursor::EResize => IDC_SIZEWE,
MouseCursor::NResize => IDC_SIZENS,
MouseCursor::NeResize => IDC_SIZENESW,
MouseCursor::NwResize => IDC_SIZENWSE,
MouseCursor::SResize => IDC_SIZENS,
MouseCursor::SeResize => IDC_SIZENWSE,
MouseCursor::SwResize => IDC_SIZENESW,
MouseCursor::WResize => IDC_SIZEWE,
MouseCursor::EwResize => IDC_SIZEWE,
MouseCursor::NsResize => IDC_SIZENS,
MouseCursor::NwseResize => IDC_SIZENWSE,
MouseCursor::NeswResize => IDC_SIZENESW,
MouseCursor::ColResize => IDC_SIZEWE,
MouseCursor::RowResize => IDC_SIZENS,
}
}

View File

@@ -0,0 +1,288 @@
use std::ffi::OsString;
use std::mem::transmute;
use std::os::windows::prelude::OsStringExt;
use std::ptr::null_mut;
use std::rc::{Rc, Weak};
use winapi::shared::guiddef::{IsEqualIID, REFIID};
use winapi::shared::minwindef::{DWORD, WPARAM};
use winapi::shared::ntdef::{HRESULT, ULONG};
use winapi::shared::windef::{POINT, POINTL};
use winapi::shared::winerror::{E_NOINTERFACE, E_UNEXPECTED, S_OK};
use winapi::shared::wtypes::DVASPECT_CONTENT;
use winapi::um::objidl::{IDataObject, FORMATETC, STGMEDIUM, TYMED_HGLOBAL};
use winapi::um::oleidl::{
IDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE,
DROPEFFECT_NONE, DROPEFFECT_SCROLL,
};
use winapi::um::shellapi::{DragQueryFileW, HDROP};
use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl};
use winapi::um::winuser::{ScreenToClient, CF_HDROP};
use winapi::Interface;
use crate::{DropData, DropEffect, Event, EventStatus, MouseEvent, PhyPoint, Point};
use super::WindowState;
// These function pointers have to be stored in a (const) variable before they can be transmuted
// Transmuting is needed because winapi has a bug where the pt parameter has an incorrect
// type `*const POINTL`
#[allow(non_snake_case)]
const DRAG_ENTER_PTR: unsafe extern "system" fn(
this: *mut IDropTarget,
pDataObj: *const IDataObject,
grfKeyState: DWORD,
pt: POINTL,
pdwEffect: *mut DWORD,
) -> HRESULT = DropTarget::drag_enter;
#[allow(non_snake_case)]
const DRAG_OVER_PTR: unsafe extern "system" fn(
this: *mut IDropTarget,
grfKeyState: DWORD,
pt: POINTL,
pdwEffect: *mut DWORD,
) -> HRESULT = DropTarget::drag_over;
#[allow(non_snake_case)]
const DROP_PTR: unsafe extern "system" fn(
this: *mut IDropTarget,
pDataObj: *const IDataObject,
grfKeyState: DWORD,
pt: POINTL,
pdwEffect: *mut DWORD,
) -> HRESULT = DropTarget::drop;
#[allow(clippy::missing_transmute_annotations)]
const DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl {
parent: IUnknownVtbl {
QueryInterface: DropTarget::query_interface,
AddRef: DropTarget::add_ref,
Release: DropTarget::release,
},
DragEnter: unsafe { transmute(DRAG_ENTER_PTR) },
DragOver: unsafe { transmute(DRAG_OVER_PTR) },
DragLeave: DropTarget::drag_leave,
Drop: unsafe { transmute(DROP_PTR) },
};
#[repr(C)]
pub(super) struct DropTarget {
base: IDropTarget,
window_state: Weak<WindowState>,
// These are cached since DragOver and DragLeave callbacks don't provide them,
// and handling drag move events gets awkward on the client end otherwise
drag_position: Point,
drag_screen_position: Point,
drop_data: DropData,
}
impl DropTarget {
pub(super) fn new(window_state: Weak<WindowState>) -> Self {
Self {
base: IDropTarget { lpVtbl: &DROP_TARGET_VTBL },
window_state,
drag_position: Point::new(0.0, 0.0),
drag_screen_position: Point::new(0.0, 0.0),
drop_data: DropData::None,
}
}
#[allow(non_snake_case)]
fn on_event(&self, pdwEffect: Option<*mut DWORD>, event: MouseEvent) {
let Some(window_state) = self.window_state.upgrade() else {
return;
};
unsafe {
let mut window = crate::Window::new(window_state.create_window());
let event = Event::Mouse(event);
let event_status =
window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event);
if let Some(pdwEffect) = pdwEffect {
match event_status {
EventStatus::AcceptDrop(DropEffect::Copy) => *pdwEffect = DROPEFFECT_COPY,
EventStatus::AcceptDrop(DropEffect::Move) => *pdwEffect = DROPEFFECT_MOVE,
EventStatus::AcceptDrop(DropEffect::Link) => *pdwEffect = DROPEFFECT_LINK,
EventStatus::AcceptDrop(DropEffect::Scroll) => *pdwEffect = DROPEFFECT_SCROLL,
_ => *pdwEffect = DROPEFFECT_NONE,
}
}
}
}
fn parse_coordinates(&mut self, pt: POINTL) {
let Some(window_state) = self.window_state.upgrade() else {
return;
};
self.drag_screen_position = Point::new(pt.x as f64, pt.y as f64);
let mut pt = POINT { x: pt.x, y: pt.y };
unsafe { ScreenToClient(window_state.hwnd, &mut pt as *mut POINT) };
let phy_point = PhyPoint::new(pt.x, pt.y);
self.drag_position = phy_point.to_logical(&window_state.window_info());
}
fn parse_drop_data(&mut self, data_object: &IDataObject) {
let format = FORMATETC {
cfFormat: CF_HDROP as u16,
ptd: null_mut(),
dwAspect: DVASPECT_CONTENT,
lindex: -1,
tymed: TYMED_HGLOBAL,
};
let mut medium = STGMEDIUM { tymed: 0, u: null_mut(), pUnkForRelease: null_mut() };
unsafe {
let hresult = data_object.GetData(&format, &mut medium);
if hresult != S_OK {
self.drop_data = DropData::None;
return;
}
let hdrop = *(*medium.u).hGlobal() as HDROP;
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0);
if item_count == 0 {
self.drop_data = DropData::None;
return;
}
let mut paths = Vec::with_capacity(item_count as usize);
for i in 0..item_count {
let characters = DragQueryFileW(hdrop, i, null_mut(), 0);
let buffer_size = characters as usize + 1;
let mut buffer = vec![0u16; buffer_size];
DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32);
paths.push(OsString::from_wide(&buffer[..characters as usize]).into())
}
self.drop_data = DropData::Files(paths);
}
}
#[allow(non_snake_case)]
unsafe extern "system" fn query_interface(
this: *mut IUnknown, riid: REFIID, ppvObject: *mut *mut winapi::ctypes::c_void,
) -> HRESULT {
if IsEqualIID(&*riid, &IUnknown::uuidof()) || IsEqualIID(&*riid, &IDropTarget::uuidof()) {
Self::add_ref(this);
*ppvObject = this as *mut winapi::ctypes::c_void;
return S_OK;
}
E_NOINTERFACE
}
unsafe extern "system" fn add_ref(this: *mut IUnknown) -> ULONG {
let arc = Rc::from_raw(this);
let result = Rc::strong_count(&arc) + 1;
let _ = Rc::into_raw(arc);
Rc::increment_strong_count(this);
result as ULONG
}
unsafe extern "system" fn release(this: *mut IUnknown) -> ULONG {
let arc = Rc::from_raw(this);
let result = Rc::strong_count(&arc) - 1;
let _ = Rc::into_raw(arc);
Rc::decrement_strong_count(this);
result as ULONG
}
#[allow(non_snake_case)]
unsafe extern "system" fn drag_enter(
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
pdwEffect: *mut DWORD,
) -> HRESULT {
let drop_target = &mut *(this as *mut DropTarget);
let Some(window_state) = drop_target.window_state.upgrade() else {
return E_UNEXPECTED;
};
let modifiers =
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
drop_target.parse_coordinates(pt);
drop_target.parse_drop_data(&*pDataObj);
let event = MouseEvent::DragEntered {
position: drop_target.drag_position,
screen_position: drop_target.drag_screen_position,
modifiers,
data: drop_target.drop_data.clone(),
};
drop_target.on_event(Some(pdwEffect), event);
S_OK
}
#[allow(non_snake_case)]
unsafe extern "system" fn drag_over(
this: *mut IDropTarget, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD,
) -> HRESULT {
let drop_target = &mut *(this as *mut DropTarget);
let Some(window_state) = drop_target.window_state.upgrade() else {
return E_UNEXPECTED;
};
let modifiers =
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
drop_target.parse_coordinates(pt);
let event = MouseEvent::DragMoved {
position: drop_target.drag_position,
screen_position: drop_target.drag_screen_position,
modifiers,
data: drop_target.drop_data.clone(),
};
drop_target.on_event(Some(pdwEffect), event);
S_OK
}
unsafe extern "system" fn drag_leave(this: *mut IDropTarget) -> HRESULT {
let drop_target = &mut *(this as *mut DropTarget);
drop_target.on_event(None, MouseEvent::DragLeft);
S_OK
}
#[allow(non_snake_case)]
unsafe extern "system" fn drop(
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
pdwEffect: *mut DWORD,
) -> HRESULT {
let drop_target = &mut *(this as *mut DropTarget);
let Some(window_state) = drop_target.window_state.upgrade() else {
return E_UNEXPECTED;
};
let modifiers =
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
drop_target.parse_coordinates(pt);
drop_target.parse_drop_data(&*pDataObj);
let event = MouseEvent::DragDropped {
position: drop_target.drag_position,
screen_position: drop_target.drag_screen_position,
modifiers,
data: drop_target.drop_data.clone(),
};
drop_target.on_event(Some(pdwEffect), event);
S_OK
}
}

View File

@@ -0,0 +1,142 @@
use std::{
collections::HashSet,
ffi::c_int,
ptr,
sync::{LazyLock, RwLock},
};
use winapi::{
shared::{
minwindef::{LPARAM, WPARAM},
windef::{HHOOK, HWND, POINT},
},
um::{
libloaderapi::GetModuleHandleW,
processthreadsapi::GetCurrentThreadId,
winuser::{
CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE,
WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
WM_USER,
},
},
};
use crate::win::wnd_proc;
// track all windows opened by this instance of baseview
// we use an RwLock here since the vast majority of uses (event interceptions)
// will only need to read from the HashSet
static HOOK_STATE: LazyLock<RwLock<KeyboardHookState>> = LazyLock::new(|| RwLock::default());
pub(crate) struct KeyboardHookHandle(HWNDWrapper);
#[derive(Default)]
struct KeyboardHookState {
hook: Option<HHOOK>,
open_windows: HashSet<HWNDWrapper>,
}
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
struct HWNDWrapper(HWND);
// SAFETY: it's a pointer behind an RwLock. we'll live
unsafe impl Send for KeyboardHookState {}
unsafe impl Sync for KeyboardHookState {}
// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet entry
unsafe impl Send for HWNDWrapper {}
unsafe impl Sync for HWNDWrapper {}
impl Drop for KeyboardHookHandle {
fn drop(&mut self) {
deinit_keyboard_hook(self.0);
}
}
// initialize keyboard hook
// some DAWs (particularly Ableton) intercept incoming keyboard messages,
// but we're naughty so we intercept them right back
pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle {
let state = &mut *HOOK_STATE.write().unwrap();
// register hwnd to global window set
state.open_windows.insert(HWNDWrapper(hwnd));
if state.hook.is_some() {
// keyboard hook already exists, just return handle
KeyboardHookHandle(HWNDWrapper(hwnd))
} else {
// keyboard hook doesn't exist (no windows open before this), create it
let new_hook = unsafe {
SetWindowsHookExW(
WH_GETMESSAGE,
Some(keyboard_hook_callback),
GetModuleHandleW(ptr::null()),
GetCurrentThreadId(),
)
};
state.hook = Some(new_hook);
KeyboardHookHandle(HWNDWrapper(hwnd))
}
}
fn deinit_keyboard_hook(hwnd: HWNDWrapper) {
let state = &mut *HOOK_STATE.write().unwrap();
state.open_windows.remove(&hwnd);
if state.open_windows.is_empty() {
if let Some(hhook) = state.hook {
unsafe {
UnhookWindowsHookEx(hhook);
}
state.hook = None;
}
}
}
unsafe extern "system" fn keyboard_hook_callback(
n_code: c_int, wparam: WPARAM, lparam: LPARAM,
) -> isize {
let msg = lparam as *mut MSG;
if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) {
*msg = MSG {
hwnd: ptr::null_mut(),
message: WM_USER,
wParam: 0,
lParam: 0,
time: 0,
pt: POINT { x: 0, y: 0 },
};
0
} else {
CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam)
}
}
// check if `msg` is a keyboard message addressed to a window
// in KeyboardHookState::open_windows, and intercept it if so
unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool {
let msg = &*msg;
// if this isn't a keyboard message, ignore it
match msg.message {
WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {}
_ => return false,
}
// check if this is one of our windows. if so, intercept it
if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) {
let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
return true;
}
false
}

View File

@@ -0,0 +1,704 @@
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Baseview modifications to druid code:
// - update imports, paths etc
//! Key event handling.
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::mem;
use std::ops::RangeInclusive;
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
use winapi::shared::minwindef::{HKL, INT, LPARAM, UINT, WPARAM};
use winapi::shared::ntdef::SHORT;
use winapi::shared::windef::HWND;
use winapi::um::winuser::{
GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, MAPVK_VK_TO_CHAR,
MAPVK_VSC_TO_VK_EX, MK_CONTROL, MK_SHIFT, PM_NOREMOVE, VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN,
VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME,
VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CANCEL, VK_CAPITAL, VK_CLEAR,
VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF,
VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, VK_F2, VK_F3, VK_F4, VK_F5,
VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI,
VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT,
VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK,
VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK,
VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7,
VK_NUMPAD8, VK_NUMPAD9, VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR,
VK_PROCESSKEY, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL,
VK_SELECT, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN,
VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP,
WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
};
const VK_ABNT_C2: INT = 0xc2;
/// A (non-extended) virtual key code.
type VkCode = u8;
// This is really bitfields.
type ShiftState = u8;
const SHIFT_STATE_SHIFT: ShiftState = 1;
const SHIFT_STATE_ALTGR: ShiftState = 2;
const N_SHIFT_STATE: ShiftState = 4;
/// Per-window keyboard state.
pub(super) struct KeyboardState {
hkl: HKL,
// A map from (vk, is_shifted) to string val
key_vals: HashMap<(VkCode, ShiftState), String>,
dead_keys: HashSet<(VkCode, ShiftState)>,
has_altgr: bool,
stash_vk: Option<VkCode>,
stash_utf16: Vec<u16>,
}
/// Virtual key codes that are considered printable.
///
/// This logic is borrowed from KeyboardLayout::GetKeyIndex
/// in Mozilla.
const PRINTABLE_VKS: &[RangeInclusive<VkCode>] = &[
0x20..=0x20,
0x30..=0x39,
0x41..=0x5A,
0x60..=0x6B,
0x6D..=0x6F,
0xBA..=0xC2,
0xDB..=0xDF,
0xE1..=0xE4,
];
/// Bits of lparam indicating scan code, including extended bit.
const SCAN_MASK: LPARAM = 0x1ff_0000;
/// Determine whether there are more messages in the queue for this key event.
///
/// When this function returns `false`, there is another message in the queue
/// with a matching scan code, therefore it is reasonable to stash the data
/// from this message and defer til later to actually produce the event.
unsafe fn is_last_message(hwnd: HWND, msg: UINT, lparam: LPARAM) -> bool {
let expected_msg = match msg {
WM_KEYDOWN | WM_CHAR => WM_CHAR,
WM_SYSKEYDOWN | WM_SYSCHAR => WM_SYSCHAR,
_ => unreachable!(),
};
let mut msg = mem::zeroed();
let avail = PeekMessageW(&mut msg, hwnd, expected_msg, expected_msg, PM_NOREMOVE);
avail == 0 || msg.lParam & SCAN_MASK != lparam & SCAN_MASK
}
const MODIFIER_MAP: &[(INT, Modifiers, SHORT)] = &[
(VK_MENU, Modifiers::ALT, 0x80),
(VK_CAPITAL, Modifiers::CAPS_LOCK, 0x1),
(VK_CONTROL, Modifiers::CONTROL, 0x80),
(VK_NUMLOCK, Modifiers::NUM_LOCK, 0x1),
(VK_SCROLL, Modifiers::SCROLL_LOCK, 0x1),
(VK_SHIFT, Modifiers::SHIFT, 0x80),
];
/// Convert scan code to W3C standard code.
///
/// It's hard to get an authoritative source for this; it's mostly based
/// on NativeKeyToDOMCodeName.h in Mozilla.
fn scan_to_code(scan_code: u32) -> Code {
use Code::*;
match scan_code {
0x1 => Escape,
0x2 => Digit1,
0x3 => Digit2,
0x4 => Digit3,
0x5 => Digit4,
0x6 => Digit5,
0x7 => Digit6,
0x8 => Digit7,
0x9 => Digit8,
0xA => Digit9,
0xB => Digit0,
0xC => Minus,
0xD => Equal,
0xE => Backspace,
0xF => Tab,
0x10 => KeyQ,
0x11 => KeyW,
0x12 => KeyE,
0x13 => KeyR,
0x14 => KeyT,
0x15 => KeyY,
0x16 => KeyU,
0x17 => KeyI,
0x18 => KeyO,
0x19 => KeyP,
0x1A => BracketLeft,
0x1B => BracketRight,
0x1C => Enter,
0x1D => ControlLeft,
0x1E => KeyA,
0x1F => KeyS,
0x20 => KeyD,
0x21 => KeyF,
0x22 => KeyG,
0x23 => KeyH,
0x24 => KeyJ,
0x25 => KeyK,
0x26 => KeyL,
0x27 => Semicolon,
0x28 => Quote,
0x29 => Backquote,
0x2A => ShiftLeft,
0x2B => Backslash,
0x2C => KeyZ,
0x2D => KeyX,
0x2E => KeyC,
0x2F => KeyV,
0x30 => KeyB,
0x31 => KeyN,
0x32 => KeyM,
0x33 => Comma,
0x34 => Period,
0x35 => Slash,
0x36 => ShiftRight,
0x37 => NumpadMultiply,
0x38 => AltLeft,
0x39 => Space,
0x3A => CapsLock,
0x3B => F1,
0x3C => F2,
0x3D => F3,
0x3E => F4,
0x3F => F5,
0x40 => F6,
0x41 => F7,
0x42 => F8,
0x43 => F9,
0x44 => F10,
0x45 => Pause,
0x46 => ScrollLock,
0x47 => Numpad7,
0x48 => Numpad8,
0x49 => Numpad9,
0x4A => NumpadSubtract,
0x4B => Numpad4,
0x4C => Numpad5,
0x4D => Numpad6,
0x4E => NumpadAdd,
0x4F => Numpad1,
0x50 => Numpad2,
0x51 => Numpad3,
0x52 => Numpad0,
0x53 => NumpadDecimal,
0x54 => PrintScreen,
0x56 => IntlBackslash,
0x57 => F11,
0x58 => F12,
0x59 => NumpadEqual,
0x70 => KanaMode,
0x71 => Lang2,
0x72 => Lang1,
0x73 => IntlRo,
0x79 => Convert,
0x7B => NonConvert,
0x7D => IntlYen,
0x7E => NumpadComma,
0x110 => MediaTrackPrevious,
0x119 => MediaTrackNext,
0x11C => NumpadEnter,
0x11D => ControlRight,
0x120 => AudioVolumeMute,
0x121 => LaunchApp2,
0x122 => MediaPlayPause,
0x124 => MediaStop,
0x12E => AudioVolumeDown,
0x130 => AudioVolumeUp,
0x132 => BrowserHome,
0x135 => NumpadDivide,
0x137 => PrintScreen,
0x138 => AltRight,
0x145 => NumLock,
0x147 => Home,
0x148 => ArrowUp,
0x149 => PageUp,
0x14B => ArrowLeft,
0x14D => ArrowRight,
0x14F => End,
0x150 => ArrowDown,
0x151 => PageDown,
0x152 => Insert,
0x153 => Delete,
0x15B => MetaLeft,
0x15C => MetaRight,
0x15D => ContextMenu,
0x15E => Power,
0x165 => BrowserSearch,
0x166 => BrowserFavorites,
0x167 => BrowserRefresh,
0x168 => BrowserStop,
0x169 => BrowserForward,
0x16A => BrowserBack,
0x16B => LaunchApp1,
0x16C => LaunchMail,
0x16D => MediaSelect,
0x1F1 => Lang2,
0x1F2 => Lang1,
_ => Unidentified,
}
}
fn vk_to_key(vk: VkCode) -> Option<Key> {
Some(match vk as INT {
VK_CANCEL => Key::Cancel,
VK_BACK => Key::Backspace,
VK_TAB => Key::Tab,
VK_CLEAR => Key::Clear,
VK_RETURN => Key::Enter,
VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Key::Shift,
VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Key::Control,
VK_MENU | VK_LMENU | VK_RMENU => Key::Alt,
VK_PAUSE => Key::Pause,
VK_CAPITAL => Key::CapsLock,
// TODO: disambiguate kana and hangul? same vk
VK_KANA => Key::KanaMode,
VK_JUNJA => Key::JunjaMode,
VK_FINAL => Key::FinalMode,
VK_KANJI => Key::KanjiMode,
VK_ESCAPE => Key::Escape,
VK_NONCONVERT => Key::NonConvert,
VK_ACCEPT => Key::Accept,
VK_PRIOR => Key::PageUp,
VK_NEXT => Key::PageDown,
VK_END => Key::End,
VK_HOME => Key::Home,
VK_LEFT => Key::ArrowLeft,
VK_UP => Key::ArrowUp,
VK_RIGHT => Key::ArrowRight,
VK_DOWN => Key::ArrowDown,
VK_SELECT => Key::Select,
VK_PRINT => Key::Print,
VK_EXECUTE => Key::Execute,
VK_SNAPSHOT => Key::PrintScreen,
VK_INSERT => Key::Insert,
VK_DELETE => Key::Delete,
VK_HELP => Key::Help,
VK_LWIN | VK_RWIN => Key::Meta,
VK_APPS => Key::ContextMenu,
VK_SLEEP => Key::Standby,
VK_F1 => Key::F1,
VK_F2 => Key::F2,
VK_F3 => Key::F3,
VK_F4 => Key::F4,
VK_F5 => Key::F5,
VK_F6 => Key::F6,
VK_F7 => Key::F7,
VK_F8 => Key::F8,
VK_F9 => Key::F9,
VK_F10 => Key::F10,
VK_F11 => Key::F11,
VK_F12 => Key::F12,
VK_NUMLOCK => Key::NumLock,
VK_SCROLL => Key::ScrollLock,
VK_BROWSER_BACK => Key::BrowserBack,
VK_BROWSER_FORWARD => Key::BrowserForward,
VK_BROWSER_REFRESH => Key::BrowserRefresh,
VK_BROWSER_STOP => Key::BrowserStop,
VK_BROWSER_SEARCH => Key::BrowserSearch,
VK_BROWSER_FAVORITES => Key::BrowserFavorites,
VK_BROWSER_HOME => Key::BrowserHome,
VK_VOLUME_MUTE => Key::AudioVolumeMute,
VK_VOLUME_DOWN => Key::AudioVolumeDown,
VK_VOLUME_UP => Key::AudioVolumeUp,
VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext,
VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious,
VK_MEDIA_STOP => Key::MediaStop,
VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause,
VK_LAUNCH_MAIL => Key::LaunchMail,
VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer,
VK_LAUNCH_APP1 => Key::LaunchApplication1,
VK_LAUNCH_APP2 => Key::LaunchApplication2,
VK_OEM_ATTN => Key::Alphanumeric,
VK_CONVERT => Key::Convert,
VK_MODECHANGE => Key::ModeChange,
VK_PROCESSKEY => Key::Process,
VK_ATTN => Key::Attn,
VK_CRSEL => Key::CrSel,
VK_EXSEL => Key::ExSel,
VK_EREOF => Key::EraseEof,
VK_PLAY => Key::Play,
VK_ZOOM => Key::ZoomToggle,
VK_OEM_CLEAR => Key::Clear,
_ => return None,
})
}
fn code_unit_to_key(code_unit: u32) -> Key {
match code_unit {
0x8 | 0x7F => Key::Backspace,
0x9 => Key::Tab,
0xA | 0xD => Key::Enter,
0x1B => Key::Escape,
_ if code_unit >= 0x20 => {
if let Some(c) = std::char::from_u32(code_unit) {
Key::Character(c.to_string())
} else {
// UTF-16 error, very unlikely
Key::Unidentified
}
}
_ => Key::Unidentified,
}
}
/// Get location from virtual key code.
///
/// This logic is based on NativeKey::GetKeyLocation from Mozilla.
fn vk_to_location(vk: VkCode, is_extended: bool) -> Location {
match vk as INT {
VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => Location::Left,
VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => Location::Right,
VK_RETURN if is_extended => Location::Numpad,
VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT
| VK_HOME | VK_UP | VK_PRIOR => {
if is_extended {
Location::Standard
} else {
Location::Numpad
}
}
VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5
| VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE
| VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_ABNT_C2 => Location::Numpad,
_ => Location::Standard,
}
}
impl KeyboardState {
/// Create a new keyboard state.
///
/// There should be one of these per window. It loads the current keyboard
/// layout and retains some mapping information from it.
pub(crate) fn new() -> KeyboardState {
unsafe {
let hkl = GetKeyboardLayout(0);
let key_vals = HashMap::new();
let dead_keys = HashSet::new();
let stash_vk = None;
let stash_utf16 = Vec::new();
let has_altgr = false;
let mut result =
KeyboardState { hkl, key_vals, dead_keys, has_altgr, stash_vk, stash_utf16 };
result.load_keyboard_layout();
result
}
}
/// Process one message from the platform.
///
/// This is the main interface point for generating cooked keyboard events
/// from raw platform messages. It should be called for each relevant message,
/// which comprises: `WM_KEYDOWN`, `WM_KEYUP`, `WM_CHAR`, `WM_SYSKEYDOWN`,
/// `WM_SYSKEYUP`, `WM_SYSCHAR`, and `WM_INPUTLANGCHANGE`.
///
/// As a general theory, many keyboard events generate a sequence of platform
/// messages. In these cases, we stash information from all messages but the
/// last, and generate the event from the last (using `PeekMessage` to detect
/// that case). Mozilla handling is slightly different; it actually tries to
/// do the processing on the first message, fetching the subsequent messages
/// from the queue. We believe our handling is simpler and more robust.
///
/// A simple example of a multi-message sequence is the key "=". In a US layout,
/// we'd expect `WM_KEYDOWN` with `wparam = VK_OEM_PLUS` and lparam encoding the
/// keycode that translates into `Code::Equal`, followed by a `WM_CHAR` with
/// `wparam = b"="` and the same scancode.
///
/// A more complex example of a multi-message sequence is the second press of
/// that key in a German layout, where it's mapped to the dead key for accent
/// acute. Then we expect `WM_KEYDOWN` with `wparam = VK_OEM_6` followed by
/// two `WM_CHAR` with `wparam = 0xB4` (corresponding to U+00B4 = acute accent).
/// In this case, the result (produced on the final message in the sequence) is
/// a key event with `key = Key::Character("´´")`, which also matches browser
/// behavior.
///
/// # Safety
///
/// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must be
/// a valid `HKL` reference in the `WM_INPUTLANGCHANGE` message. Actual danger
/// is likely low, though.
pub(crate) unsafe fn process_message(
&mut self, hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
) -> Option<KeyboardEvent> {
match msg {
WM_KEYDOWN | WM_SYSKEYDOWN => {
//println!("keydown wparam {:x} lparam {:x}", wparam, lparam);
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
let vk = self.refine_vk(wparam as u8, scan_code);
if is_last_message(hwnd, msg, lparam) {
let modifiers = self.get_modifiers();
let code = scan_to_code(scan_code);
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
let repeat = (lparam & 0x4000_0000) != 0;
let is_extended = (lparam & 0x100_0000) != 0;
let location = vk_to_location(vk, is_extended);
let state = KeyState::Down;
let event = KeyboardEvent {
state,
modifiers,
code,
key,
is_composing: false,
location,
repeat,
};
Some(event)
} else {
self.stash_vk = Some(vk);
None
}
}
WM_KEYUP | WM_SYSKEYUP => {
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
let vk = self.refine_vk(wparam as u8, scan_code);
let modifiers = self.get_modifiers();
let code = scan_to_code(scan_code);
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
let repeat = false;
let is_extended = (lparam & 0x100_0000) != 0;
let location = vk_to_location(vk, is_extended);
let state = KeyState::Up;
let event = KeyboardEvent {
state,
modifiers,
code,
key,
is_composing: false,
location,
repeat,
};
Some(event)
}
WM_CHAR | WM_SYSCHAR => {
//println!("char wparam {:x} lparam {:x}", wparam, lparam);
if is_last_message(hwnd, msg, lparam) {
let stash_vk = self.stash_vk.take();
let modifiers = self.get_modifiers();
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
let vk = self.refine_vk(stash_vk.unwrap_or(0), scan_code);
let code = scan_to_code(scan_code);
let key = if self.stash_utf16.is_empty() && wparam < 0x20 {
vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers))
} else {
self.stash_utf16.push(wparam as u16);
if let Ok(s) = String::from_utf16(&self.stash_utf16) {
Key::Character(s)
} else {
Key::Unidentified
}
};
self.stash_utf16.clear();
let repeat = (lparam & 0x4000_0000) != 0;
let is_extended = (lparam & 0x100_0000) != 0;
let location = vk_to_location(vk, is_extended);
let state = KeyState::Down;
let event = KeyboardEvent {
state,
modifiers,
code,
key,
is_composing: false,
location,
repeat,
};
Some(event)
} else {
self.stash_utf16.push(wparam as u16);
None
}
}
WM_INPUTLANGCHANGE => {
self.hkl = lparam as HKL;
self.load_keyboard_layout();
None
}
_ => None,
}
}
/// Get the modifier state.
///
/// This function is designed to be called from a message handler, and
/// gives the modifier state at the time of the message (ie is the
/// synchronous variant). See [`GetKeyState`] for more context.
///
/// The interpretation of modifiers depends on the keyboard layout, as
/// some layouts have [AltGr] and others do not.
///
/// [`GetKeyState`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate
/// [AltGr]: https://en.wikipedia.org/wiki/AltGr_key
pub(crate) fn get_modifiers(&self) -> Modifiers {
unsafe {
let mut modifiers = Modifiers::empty();
for &(vk, modifier, mask) in MODIFIER_MAP {
if GetKeyState(vk) & mask != 0 {
modifiers |= modifier;
}
}
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
modifiers |= Modifiers::ALT_GRAPH;
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
}
modifiers
}
}
/// The same as [Self::get_modifiers()], but it reads the Ctrl and Shift state from a mouse
/// event's wParam parameter. Saves two calls to [GetKeyState()].
pub(crate) fn get_modifiers_from_mouse_wparam(&self, wparam: WPARAM) -> Modifiers {
unsafe {
let mut modifiers = Modifiers::empty();
for &(vk, modifier, mask) in MODIFIER_MAP {
let modifier_active = match modifier {
Modifiers::CONTROL => wparam & MK_CONTROL != 0,
Modifiers::SHIFT => wparam & MK_SHIFT != 0,
_ => GetKeyState(vk) & mask != 0,
};
if modifier_active {
modifiers |= modifier;
}
}
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
modifiers |= Modifiers::ALT_GRAPH;
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
}
modifiers
}
}
/// Load a keyboard layout.
///
/// We need to retain a map of virtual key codes in various modifier
/// states, because it's not practical to query that at keyboard event
/// time (the main culprit is that `ToUnicodeEx` is stateful).
///
/// The logic is based on Mozilla KeyboardLayout::LoadLayout but is
/// considerably simplified.
fn load_keyboard_layout(&mut self) {
unsafe {
self.key_vals.clear();
self.dead_keys.clear();
self.has_altgr = false;
let mut key_state = [0u8; 256];
let mut uni_chars = [0u16; 5];
// Right now, we're only getting the values for base and shifted
// variants. Mozilla goes through 16 mod states.
for shift_state in 0..N_SHIFT_STATE {
let has_shift = shift_state & SHIFT_STATE_SHIFT != 0;
let has_altgr = shift_state & SHIFT_STATE_ALTGR != 0;
key_state[VK_SHIFT as usize] = if has_shift { 0x80 } else { 0 };
key_state[VK_CONTROL as usize] = if has_altgr { 0x80 } else { 0 };
key_state[VK_LCONTROL as usize] = if has_altgr { 0x80 } else { 0 };
key_state[VK_MENU as usize] = if has_altgr { 0x80 } else { 0 };
key_state[VK_RMENU as usize] = if has_altgr { 0x80 } else { 0 };
#[allow(clippy::iter_overeager_cloned)]
for vk in PRINTABLE_VKS.iter().cloned().flatten() {
let ret = ToUnicodeEx(
vk as UINT,
0,
key_state.as_ptr(),
uni_chars.as_mut_ptr(),
uni_chars.len() as _,
0,
self.hkl,
);
match ret.cmp(&0) {
Ordering::Greater => {
let utf16_slice = &uni_chars[..ret as usize];
if let Ok(strval) = String::from_utf16(utf16_slice) {
self.key_vals.insert((vk, shift_state), strval);
}
// If the AltGr version of the key has a different string than
// the base, then the layout has AltGr. Note that Mozilla also
// checks dead keys for change.
if has_altgr
&& !self.has_altgr
&& self.key_vals.get(&(vk, shift_state))
!= self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR))
{
self.has_altgr = true;
}
}
Ordering::Less => {
// It's a dead key, press it again to reset the state.
self.dead_keys.insert((vk, shift_state));
let _ = ToUnicodeEx(
vk as UINT,
0,
key_state.as_ptr(),
uni_chars.as_mut_ptr(),
uni_chars.len() as _,
0,
self.hkl,
);
}
_ => (),
}
}
}
}
}
fn get_base_key(&self, vk: VkCode, modifiers: Modifiers) -> Key {
let mut shift_state = 0;
if modifiers.contains(Modifiers::SHIFT) {
shift_state |= SHIFT_STATE_SHIFT;
}
if modifiers.contains(Modifiers::ALT_GRAPH) {
shift_state |= SHIFT_STATE_ALTGR;
}
if let Some(s) = self.key_vals.get(&(vk, shift_state)) {
Key::Character(s.clone())
} else {
let mapped = self.map_vk(vk);
if mapped >= (1 << 31) {
Key::Dead
} else {
code_unit_to_key(mapped)
}
}
}
/// Map a virtual key code to a code unit, also indicate if dead key.
///
/// Bit 31 is set if the mapping is to a dead key. The bottom bits contain the code unit.
fn map_vk(&self, vk: VkCode) -> u32 {
unsafe { MapVirtualKeyExW(vk as _, MAPVK_VK_TO_CHAR, self.hkl) }
}
/// Refine a virtual key code to distinguish left and right.
///
/// This only does the mapping if the original code is ambiguous, as otherwise the
/// virtual key code reported in `wparam` is more reliable.
fn refine_vk(&self, vk: VkCode, mut scan_code: u32) -> VkCode {
match vk as INT {
0 | VK_SHIFT | VK_CONTROL | VK_MENU => {
if scan_code >= 0x100 {
scan_code += 0xE000 - 0x100;
}
unsafe { MapVirtualKeyExW(scan_code, MAPVK_VSC_TO_VK_EX, self.hkl) as u8 }
}
_ => vk,
}
}
}

View File

@@ -0,0 +1,7 @@
mod cursor;
mod drop_target;
mod hook;
mod keyboard;
mod window;
pub use window::*;

View File

@@ -0,0 +1,867 @@
use winapi::shared::guiddef::GUID;
use winapi::shared::minwindef::{ATOM, FALSE, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
use winapi::shared::windef::{HWND, POINT, RECT};
use winapi::um::combaseapi::CoCreateGuid;
use winapi::um::ole2::{OleInitialize, RegisterDragDrop, RevokeDragDrop};
use winapi::um::oleidl::LPDROPTARGET;
use winapi::um::winuser::{
AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
GetCursorPos, GetDpiForWindow, GetFocus, GetMessageW, GetWindowLongPtrW, LoadCursorW,
PostMessageW, RegisterClassW, ReleaseCapture, SetCapture, SetCursor, SetFocus,
SetProcessDpiAwarenessContext, SetTimer, SetWindowLongPtrW, SetWindowPos, TrackMouseEvent,
TranslateMessage, UnregisterClassW,
CS_OWNDC, GET_XBUTTON_WPARAM, GWLP_USERDATA, HTCLIENT, IDC_ARROW, MSG, SWP_NOMOVE,
SWP_NOZORDER, TRACKMOUSEEVENT, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DPICHANGED,
WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSELEAVE, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCDESTROY,
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SHOWWINDOW, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN,
WM_SYSKEYUP, WM_TIMER, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WS_CAPTION, WS_CHILD,
WS_CLIPSIBLINGS, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_POPUPWINDOW, WS_SIZEBOX, WS_VISIBLE,
XBUTTON1, XBUTTON2,
};
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::collections::VecDeque;
use std::ffi::{c_void, OsStr};
use std::os::windows::ffi::OsStrExt;
use std::ptr::null_mut;
use std::rc::Rc;
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, Win32WindowHandle,
WindowsDisplayHandle,
};
const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1;
use crate::win::hook::{self, KeyboardHookHandle};
use crate::{
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, Point, ScrollDelta, Size,
WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
};
use super::cursor::cursor_to_lpcwstr;
use super::drop_target::DropTarget;
use super::keyboard::KeyboardState;
#[cfg(feature = "opengl")]
use crate::gl::GlContext;
unsafe fn generate_guid() -> String {
let mut guid: GUID = std::mem::zeroed();
CoCreateGuid(&mut guid);
format!(
"{:0X}-{:0X}-{:0X}-{:0X}{:0X}-{:0X}{:0X}{:0X}{:0X}{:0X}{:0X}\0",
guid.Data1,
guid.Data2,
guid.Data3,
guid.Data4[0],
guid.Data4[1],
guid.Data4[2],
guid.Data4[3],
guid.Data4[4],
guid.Data4[5],
guid.Data4[6],
guid.Data4[7]
)
}
const WIN_FRAME_TIMER: usize = 4242;
pub struct WindowHandle {
hwnd: Option<HWND>,
is_open: Rc<Cell<bool>>,
}
impl WindowHandle {
pub fn close(&mut self) {
if let Some(hwnd) = self.hwnd.take() {
unsafe {
PostMessageW(hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
}
}
}
pub fn is_open(&self) -> bool {
self.is_open.get()
}
}
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
if let Some(hwnd) = self.hwnd {
let mut handle = Win32WindowHandle::empty();
handle.hwnd = hwnd as *mut c_void;
RawWindowHandle::Win32(handle)
} else {
RawWindowHandle::Win32(Win32WindowHandle::empty())
}
}
}
struct ParentHandle {
is_open: Rc<Cell<bool>>,
}
impl ParentHandle {
pub fn new(hwnd: HWND) -> (Self, WindowHandle) {
let is_open = Rc::new(Cell::new(true));
let handle = WindowHandle { hwnd: Some(hwnd), is_open: Rc::clone(&is_open) };
(Self { is_open }, handle)
}
}
impl Drop for ParentHandle {
fn drop(&mut self) {
self.is_open.set(false);
}
}
pub(crate) unsafe extern "system" fn wnd_proc(
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
) -> LRESULT {
if msg == WM_CREATE {
PostMessageW(hwnd, WM_SHOWWINDOW, 0, 0);
return 0;
}
let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
if !window_state_ptr.is_null() {
let result = wnd_proc_inner(hwnd, msg, wparam, lparam, &*window_state_ptr);
// If any of the above event handlers caused tasks to be pushed to the deferred tasks list,
// then we'll try to handle them now
loop {
// NOTE: This is written like this instead of using a `while let` loop to avoid exending
// the borrow of `window_state.deferred_tasks` into the call of
// `window_state.handle_deferred_task()` since that may also generate additional
// messages.
let task = match (*window_state_ptr).deferred_tasks.borrow_mut().pop_front() {
Some(task) => task,
None => break,
};
(*window_state_ptr).handle_deferred_task(task);
}
// NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above
if msg == WM_NCDESTROY {
RevokeDragDrop(hwnd);
unregister_wnd_class((*window_state_ptr).window_class);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
drop(Rc::from_raw(window_state_ptr));
}
// The actual custom window proc has been moved to another function so we can always handle
// the deferred tasks regardless of whether the custom window proc returns early or not
if let Some(result) = result {
return result;
}
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}
/// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after
/// handling any deferred tasks. otherwise the default window procedure is invoked.
unsafe fn wnd_proc_inner(
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState,
) -> Option<LRESULT> {
match msg {
WM_MOUSEMOVE => {
let mut window = crate::Window::new(window_state.create_window());
let mut mouse_was_outside_window = window_state.mouse_was_outside_window.borrow_mut();
if *mouse_was_outside_window {
// this makes Windows track whether the mouse leaves the window.
// When the mouse leaves it results in a `WM_MOUSELEAVE` event.
let mut track_mouse = TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: winapi::um::winuser::TME_LEAVE,
hwndTrack: hwnd,
dwHoverTime: winapi::um::winuser::HOVER_DEFAULT,
};
// Couldn't find a good way to track whether the mouse enters,
// but if `WM_MOUSEMOVE` happens, the mouse must have entered.
TrackMouseEvent(&mut track_mouse);
*mouse_was_outside_window = false;
let enter_event = Event::Mouse(MouseEvent::CursorEntered);
window_state
.handler
.borrow_mut()
.as_mut()
.unwrap()
.on_event(&mut window, enter_event);
}
let x = (lparam & 0xFFFF) as i16 as i32;
let y = ((lparam >> 16) & 0xFFFF) as i16 as i32;
let physical_pos = PhyPoint { x, y };
let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow());
let mut screen_pt = POINT { x: 0, y: 0 };
GetCursorPos(&mut screen_pt);
let screen_position = Point::new(screen_pt.x as f64, screen_pt.y as f64);
let move_event = Event::Mouse(MouseEvent::CursorMoved {
position: logical_pos,
screen_position,
modifiers: window_state
.keyboard_state
.borrow()
.get_modifiers_from_mouse_wparam(wparam),
});
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, move_event);
Some(0)
}
WM_MOUSELEAVE => {
let mut window = crate::Window::new(window_state.create_window());
let event = Event::Mouse(MouseEvent::CursorLeft);
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
*window_state.mouse_was_outside_window.borrow_mut() = true;
Some(0)
}
WM_MOUSEWHEEL | WM_MOUSEHWHEEL => {
let mut window = crate::Window::new(window_state.create_window());
let value = (wparam >> 16) as i16;
let value = value as i32;
let value = value as f32 / WHEEL_DELTA as f32;
let event = Event::Mouse(MouseEvent::WheelScrolled {
delta: if msg == WM_MOUSEWHEEL {
ScrollDelta::Lines { x: 0.0, y: value }
} else {
ScrollDelta::Lines { x: value, y: 0.0 }
},
modifiers: window_state
.keyboard_state
.borrow()
.get_modifiers_from_mouse_wparam(wparam),
});
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
Some(0)
}
WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN
| WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => {
let mut window = crate::Window::new(window_state.create_window());
let mut mouse_button_counter = window_state.mouse_button_counter.get();
let button = match msg {
WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left),
WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle),
WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right),
WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) {
XBUTTON1 => Some(MouseButton::Back),
XBUTTON2 => Some(MouseButton::Forward),
_ => None,
},
_ => None,
};
if let Some(button) = button {
let event = match msg {
WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => {
// Capture the mouse cursor on button down
mouse_button_counter = mouse_button_counter.saturating_add(1);
SetCapture(hwnd);
MouseEvent::ButtonPressed {
button,
modifiers: window_state
.keyboard_state
.borrow()
.get_modifiers_from_mouse_wparam(wparam),
}
}
WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => {
// Release the mouse cursor capture when all buttons are released
mouse_button_counter = mouse_button_counter.saturating_sub(1);
if mouse_button_counter == 0 {
ReleaseCapture();
}
MouseEvent::ButtonReleased {
button,
modifiers: window_state
.keyboard_state
.borrow()
.get_modifiers_from_mouse_wparam(wparam),
}
}
_ => {
unreachable!()
}
};
window_state.mouse_button_counter.set(mouse_button_counter);
window_state
.handler
.borrow_mut()
.as_mut()
.unwrap()
.on_event(&mut window, Event::Mouse(event));
}
None
}
WM_TIMER => {
let mut window = crate::Window::new(window_state.create_window());
if wparam == WIN_FRAME_TIMER {
window_state.handler.borrow_mut().as_mut().unwrap().on_frame(&mut window);
}
Some(0)
}
WM_CLOSE => {
// Make sure to release the borrow before the DefWindowProc call
{
let mut window = crate::Window::new(window_state.create_window());
window_state
.handler
.borrow_mut()
.as_mut()
.unwrap()
.on_event(&mut window, Event::Window(WindowEvent::WillClose));
}
// DestroyWindow(hwnd);
// Some(0)
Some(DefWindowProcW(hwnd, msg, wparam, lparam))
}
WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP
| WM_INPUTLANGCHANGE => {
let mut window = crate::Window::new(window_state.create_window());
let opt_event =
window_state.keyboard_state.borrow_mut().process_message(hwnd, msg, wparam, lparam);
if let Some(event) = opt_event {
window_state
.handler
.borrow_mut()
.as_mut()
.unwrap()
.on_event(&mut window, Event::Keyboard(event));
}
if msg != WM_SYSKEYDOWN {
Some(0)
} else {
None
}
}
WM_SIZE => {
let mut window = crate::Window::new(window_state.create_window());
let width = (lparam & 0xFFFF) as u16 as u32;
let height = ((lparam >> 16) & 0xFFFF) as u16 as u32;
let new_window_info = {
let mut window_info = window_state.window_info.borrow_mut();
let new_window_info =
WindowInfo::from_physical_size(PhySize { width, height }, window_info.scale());
// Only send the event if anything changed
if window_info.physical_size() == new_window_info.physical_size() {
return None;
}
*window_info = new_window_info;
new_window_info
};
window_state
.handler
.borrow_mut()
.as_mut()
.unwrap()
.on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info)));
None
}
WM_DPICHANGED => {
// To avoid weirdness with the realtime borrow checker.
let new_rect = {
if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy {
let dpi = (wparam & 0xFFFF) as u16 as u32;
let scale_factor = dpi as f64 / 96.0;
let mut window_info = window_state.window_info.borrow_mut();
*window_info =
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
Some((
RECT {
left: 0,
top: 0,
// todo: check if usize fits into i32
right: window_info.physical_size().width as i32,
bottom: window_info.physical_size().height as i32,
},
window_state.dw_style,
))
} else {
None
}
};
if let Some((mut new_rect, dw_style)) = new_rect {
// Convert this desired "client rectangle" size to the actual "window rectangle"
// size (Because of course you have to do that).
AdjustWindowRectEx(&mut new_rect, dw_style, 0, 0);
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
// which we can then send the user the new scale factor.
SetWindowPos(
hwnd,
hwnd,
new_rect.left,
new_rect.top,
new_rect.right - new_rect.left,
new_rect.bottom - new_rect.top,
SWP_NOZORDER | SWP_NOMOVE,
);
}
None
}
// If WM_SETCURSOR returns `None`, WM_SETCURSOR continues to get handled by the outer window(s),
// If it returns `Some(1)`, the current window decides what the cursor is
WM_SETCURSOR => {
let low_word = LOWORD(lparam as u32) as isize;
let mouse_in_window = low_word == HTCLIENT;
if mouse_in_window {
// Here we need to set the cursor back to what the state says, since it can have changed when outside the window
let cursor =
LoadCursorW(null_mut(), cursor_to_lpcwstr(window_state.cursor_icon.get()));
unsafe {
SetCursor(cursor);
}
Some(1)
} else {
// Cursor is being changed by some other window, e.g. when having mouse on the borders to resize it
None
}
}
// NOTE: `WM_NCDESTROY` is handled in the outer function because this deallocates the window
// state
BV_WINDOW_MUST_CLOSE => {
DestroyWindow(hwnd);
Some(0)
}
_ => None,
}
}
unsafe fn register_wnd_class() -> ATOM {
// We generate a unique name for the new window class to prevent name collisions
let class_name_str = format!("Baseview-{}", generate_guid());
let mut class_name: Vec<u16> = OsStr::new(&class_name_str).encode_wide().collect();
class_name.push(0);
let wnd_class = WNDCLASSW {
style: CS_OWNDC,
lpfnWndProc: Some(wnd_proc),
hInstance: null_mut(),
lpszClassName: class_name.as_ptr(),
cbClsExtra: 0,
cbWndExtra: 0,
hIcon: null_mut(),
hCursor: LoadCursorW(null_mut(), IDC_ARROW),
hbrBackground: null_mut(),
lpszMenuName: null_mut(),
};
RegisterClassW(&wnd_class)
}
unsafe fn unregister_wnd_class(wnd_class: ATOM) {
UnregisterClassW(wnd_class as _, null_mut());
}
/// All data associated with the window. This uses internal mutability so the outer struct doesn't
/// need to be mutably borrowed. Mutably borrowing the entire `WindowState` can be problematic
/// because of the Windows message loops' reentrant nature. Care still needs to be taken to prevent
/// `handler` from indirectly triggering other events that would also need to be handled using
/// `handler`.
pub(super) struct WindowState {
/// The HWND belonging to this window. The window's actual state is stored in the `WindowState`
/// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd,
/// GWLP_USERDATA) } as *const WindowState`.
pub hwnd: HWND,
window_class: ATOM,
window_info: RefCell<WindowInfo>,
_parent_handle: Option<ParentHandle>,
keyboard_state: RefCell<KeyboardState>,
mouse_button_counter: Cell<usize>,
mouse_was_outside_window: RefCell<bool>,
cursor_icon: Cell<MouseCursor>,
// Initialized late so the `Window` can hold a reference to this `WindowState`
handler: RefCell<Option<Box<dyn WindowHandler>>>,
_drop_target: RefCell<Option<Rc<DropTarget>>>,
scale_policy: WindowScalePolicy,
dw_style: u32,
// handle to the win32 keyboard hook
// we don't need to read from this, just carry it around so the Drop impl can run
#[allow(dead_code)]
kb_hook: KeyboardHookHandle,
/// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably
/// borrowing the fields from `WindowState` more than once. For instance, when the window
/// handler requests a resize in response to a keyboard event, the window state will already be
/// borrowed in `wnd_proc`. So the `resize()` function below cannot also mutably borrow that
/// window state at the same time.
pub deferred_tasks: RefCell<VecDeque<WindowTask>>,
#[cfg(feature = "opengl")]
pub gl_context: Option<GlContext>,
}
impl WindowState {
pub(super) fn create_window(&self) -> Window<'_> {
Window { state: self }
}
pub(super) fn window_info(&self) -> Ref<'_, WindowInfo> {
self.window_info.borrow()
}
pub(super) fn keyboard_state(&self) -> Ref<'_, KeyboardState> {
self.keyboard_state.borrow()
}
pub(super) fn handler_mut(&self) -> RefMut<'_, Option<Box<dyn WindowHandler>>> {
self.handler.borrow_mut()
}
/// Handle a deferred task as described in [`Self::deferred_tasks`].
pub(self) fn handle_deferred_task(&self, task: WindowTask) {
match task {
WindowTask::Resize(size) => {
// `self.window_info` will be modified in response to the `WM_SIZE` event that
// follows the `SetWindowPos()` call
let scaling = self.window_info.borrow().scale();
let window_info = WindowInfo::from_logical_size(size, scaling);
// If the window is a standalone window then the size needs to include the window
// decorations
let mut rect = RECT {
left: 0,
top: 0,
right: window_info.physical_size().width as i32,
bottom: window_info.physical_size().height as i32,
};
unsafe {
AdjustWindowRectEx(&mut rect, self.dw_style, 0, 0);
SetWindowPos(
self.hwnd,
self.hwnd,
0,
0,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOMOVE,
)
};
}
}
}
}
/// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState`
/// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information.
#[derive(Debug, Clone)]
pub(super) enum WindowTask {
/// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied
/// automatically.
Resize(Size),
}
pub struct Window<'a> {
state: &'a WindowState,
}
impl Window<'_> {
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
where
P: HasRawWindowHandle,
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let parent = match parent.raw_window_handle() {
RawWindowHandle::Win32(h) => h.hwnd as HWND,
h => panic!("unsupported parent handle {:?}", h),
};
let (window_handle, _) = Self::open(true, parent, options, build);
window_handle
}
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let (_, hwnd) = Self::open(false, null_mut(), options, build);
unsafe {
let mut msg: MSG = std::mem::zeroed();
loop {
let status = GetMessageW(&mut msg, hwnd, 0, 0);
if status == -1 {
break;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
fn open<H, B>(
parented: bool, parent: HWND, options: WindowOpenOptions, build: B,
) -> (WindowHandle, HWND)
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
unsafe {
let mut title: Vec<u16> = OsStr::new(&options.title[..]).encode_wide().collect();
title.push(0);
let window_class = register_wnd_class();
// todo: manage error ^
let scaling = match options.scale {
WindowScalePolicy::SystemScaleFactor => 1.0,
WindowScalePolicy::ScaleFactor(scale) => scale,
};
let window_info = WindowInfo::from_logical_size(options.size, scaling);
let mut rect = RECT {
left: 0,
top: 0,
// todo: check if usize fits into i32
right: window_info.physical_size().width as i32,
bottom: window_info.physical_size().height as i32,
};
let flags = if parented {
WS_CHILD | WS_VISIBLE
} else {
WS_POPUPWINDOW
| WS_CAPTION
| WS_VISIBLE
| WS_SIZEBOX
| WS_MINIMIZEBOX
| WS_MAXIMIZEBOX
| WS_CLIPSIBLINGS
};
if !parented {
AdjustWindowRectEx(&mut rect, flags, FALSE, 0);
}
let hwnd = CreateWindowExW(
0,
window_class as _,
title.as_ptr(),
flags,
0,
0,
rect.right - rect.left,
rect.bottom - rect.top,
parent as *mut _,
null_mut(),
null_mut(),
null_mut(),
);
// todo: manage error ^
let kb_hook = hook::init_keyboard_hook(hwnd);
#[cfg(feature = "opengl")]
let gl_context: Option<GlContext> = options.gl_config.map(|gl_config| {
let mut handle = Win32WindowHandle::empty();
handle.hwnd = hwnd as *mut c_void;
let handle = RawWindowHandle::Win32(handle);
GlContext::create(&handle, gl_config).expect("Could not create OpenGL context")
});
let (parent_handle, window_handle) = ParentHandle::new(hwnd);
let parent_handle = if parented { Some(parent_handle) } else { None };
let window_state = Rc::new(WindowState {
hwnd,
window_class,
window_info: RefCell::new(window_info),
_parent_handle: parent_handle,
keyboard_state: RefCell::new(KeyboardState::new()),
mouse_button_counter: Cell::new(0),
mouse_was_outside_window: RefCell::new(true),
cursor_icon: Cell::new(MouseCursor::Default),
// The Window refers to this `WindowState`, so this `handler` needs to be
// initialized later
handler: RefCell::new(None),
_drop_target: RefCell::new(None),
scale_policy: options.scale,
dw_style: flags,
deferred_tasks: RefCell::new(VecDeque::with_capacity(4)),
kb_hook,
#[cfg(feature = "opengl")]
gl_context,
});
let handler = {
let mut window = crate::Window::new(window_state.create_window());
build(&mut window)
};
*window_state.handler.borrow_mut() = Some(Box::new(handler));
// Only works on Windows 10 unfortunately.
SetProcessDpiAwarenessContext(
winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,
);
// Now we can get the actual dpi of the window.
let new_rect = if let WindowScalePolicy::SystemScaleFactor = options.scale {
// Only works on Windows 10 unfortunately.
let dpi = GetDpiForWindow(hwnd);
let scale_factor = dpi as f64 / 96.0;
let mut window_info = window_state.window_info.borrow_mut();
if window_info.scale() != scale_factor {
*window_info =
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
Some(RECT {
left: 0,
top: 0,
// todo: check if usize fits into i32
right: window_info.physical_size().width as i32,
bottom: window_info.physical_size().height as i32,
})
} else {
None
}
} else {
None
};
let drop_target = Rc::new(DropTarget::new(Rc::downgrade(&window_state)));
*window_state._drop_target.borrow_mut() = Some(drop_target.clone());
OleInitialize(null_mut());
RegisterDragDrop(hwnd, Rc::as_ptr(&drop_target) as LPDROPTARGET);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::into_raw(window_state) as *const _ as _);
SetTimer(hwnd, WIN_FRAME_TIMER, 15, None);
if let Some(mut new_rect) = new_rect {
// Convert this desired"client rectangle" size to the actual "window rectangle"
// size (Because of course you have to do that).
AdjustWindowRectEx(&mut new_rect, flags, 0, 0);
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
// which we can then send the user the new scale factor.
SetWindowPos(
hwnd,
hwnd,
new_rect.left,
new_rect.top,
new_rect.right - new_rect.left,
new_rect.bottom - new_rect.top,
SWP_NOZORDER | SWP_NOMOVE,
);
}
(window_handle, hwnd)
}
}
pub fn close(&mut self) {
unsafe {
PostMessageW(self.state.hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
}
}
pub fn has_focus(&mut self) -> bool {
let focused_window = unsafe { GetFocus() };
focused_window == self.state.hwnd
}
pub fn focus(&mut self) {
unsafe {
SetFocus(self.state.hwnd);
}
}
pub fn resize(&mut self, size: Size) {
// To avoid reentrant event handler calls we'll defer the actual resizing until after the
// event has been handled
let task = WindowTask::Resize(size);
self.state.deferred_tasks.borrow_mut().push_back(task);
}
pub fn physical_size(&self) -> PhySize {
self.state.window_info().physical_size()
}
pub fn set_mouse_cursor(&mut self, mouse_cursor: MouseCursor) {
self.state.cursor_icon.set(mouse_cursor);
unsafe {
let cursor = LoadCursorW(null_mut(), cursor_to_lpcwstr(mouse_cursor));
SetCursor(cursor);
}
}
#[cfg(feature = "opengl")]
pub fn gl_context(&self) -> Option<&GlContext> {
self.state.gl_context.as_ref()
}
}
unsafe impl HasRawWindowHandle for Window<'_> {
fn raw_window_handle(&self) -> RawWindowHandle {
let mut handle = Win32WindowHandle::empty();
handle.hwnd = self.state.hwnd as *mut c_void;
RawWindowHandle::Win32(handle)
}
}
unsafe impl HasRawDisplayHandle for Window<'_> {
fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::Windows(WindowsDisplayHandle::empty())
}
}
pub fn copy_to_clipboard(_data: &str) {
todo!()
}

View File

@@ -0,0 +1,136 @@
use std::marker::PhantomData;
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle,
};
use crate::event::{Event, EventStatus};
use crate::window_open_options::WindowOpenOptions;
use crate::{MouseCursor, PhySize, Size};
#[cfg(target_os = "macos")]
use crate::macos as platform;
#[cfg(target_os = "windows")]
use crate::win as platform;
#[cfg(target_os = "linux")]
use crate::x11 as platform;
pub struct WindowHandle {
window_handle: platform::WindowHandle,
// so that WindowHandle is !Send on all platforms
phantom: PhantomData<*mut ()>,
}
impl WindowHandle {
fn new(window_handle: platform::WindowHandle) -> Self {
Self { window_handle, phantom: PhantomData }
}
/// Close the window
pub fn close(&mut self) {
self.window_handle.close();
}
/// Returns `true` if the window is still open, and returns `false`
/// if the window was closed/dropped.
pub fn is_open(&self) -> bool {
self.window_handle.is_open()
}
}
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
self.window_handle.raw_window_handle()
}
}
pub trait WindowHandler {
fn on_frame(&mut self, window: &mut Window);
fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus;
}
pub struct Window<'a> {
window: platform::Window<'a>,
// so that Window is !Send on all platforms
phantom: PhantomData<*mut ()>,
}
impl<'a> Window<'a> {
#[cfg(target_os = "windows")]
pub(crate) fn new(window: platform::Window<'a>) -> Window<'a> {
Window { window, phantom: PhantomData }
}
#[cfg(not(target_os = "windows"))]
pub(crate) fn new(window: platform::Window) -> Window {
Window { window, phantom: PhantomData }
}
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
where
P: HasRawWindowHandle,
H: WindowHandler + 'static,
B: FnOnce(&mut Window) -> H,
B: Send + 'static,
{
let window_handle = platform::Window::open_parented::<P, H, B>(parent, options, build);
WindowHandle::new(window_handle)
}
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
where
H: WindowHandler + 'static,
B: FnOnce(&mut Window) -> H,
B: Send + 'static,
{
platform::Window::open_blocking::<H, B>(options, build)
}
/// Close the window
pub fn close(&mut self) {
self.window.close();
}
/// Returns the current physical size of the window by querying the OS directly.
pub fn physical_size(&self) -> PhySize {
self.window.physical_size()
}
/// Resize the window to the given size. The size is always in logical pixels. DPI scaling will
/// automatically be accounted for.
pub fn resize(&mut self, size: Size) {
self.window.resize(size);
}
pub fn set_mouse_cursor(&mut self, cursor: MouseCursor) {
self.window.set_mouse_cursor(cursor);
}
pub fn has_focus(&mut self) -> bool {
self.window.has_focus()
}
pub fn focus(&mut self) {
self.window.focus()
}
/// If provided, then an OpenGL context will be created for this window. You'll be able to
/// access this context through [crate::Window::gl_context].
#[cfg(feature = "opengl")]
pub fn gl_context(&self) -> Option<&crate::gl::GlContext> {
self.window.gl_context()
}
}
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
fn raw_window_handle(&self) -> RawWindowHandle {
self.window.raw_window_handle()
}
}
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
fn raw_display_handle(&self) -> RawDisplayHandle {
self.window.raw_display_handle()
}
}

View File

@@ -0,0 +1,144 @@
/// The info about the window
#[derive(Debug, Copy, Clone)]
pub struct WindowInfo {
logical_size: Size,
physical_size: PhySize,
scale: f64,
scale_recip: f64,
}
impl WindowInfo {
pub fn from_logical_size(logical_size: Size, scale: f64) -> Self {
let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale };
let physical_size = PhySize {
width: (logical_size.width * scale).round() as u32,
height: (logical_size.height * scale).round() as u32,
};
Self { logical_size, physical_size, scale, scale_recip }
}
pub fn from_physical_size(physical_size: PhySize, scale: f64) -> Self {
let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale };
let logical_size = Size {
width: f64::from(physical_size.width) * scale_recip,
height: f64::from(physical_size.height) * scale_recip,
};
Self { logical_size, physical_size, scale, scale_recip }
}
/// The logical size of the window
pub fn logical_size(&self) -> Size {
self.logical_size
}
/// The physical size of the window
pub fn physical_size(&self) -> PhySize {
self.physical_size
}
/// The scale factor of the window
pub fn scale(&self) -> f64 {
self.scale
}
/// The reciprocal of the scale factor of the window
pub fn scale_recip(&self) -> f64 {
self.scale_recip
}
}
/// A point in logical coordinates
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
/// Create a new point in logical coordinates
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
/// Convert to actual physical coordinates
#[inline]
pub fn to_physical(&self, window_info: &WindowInfo) -> PhyPoint {
PhyPoint {
x: (self.x * window_info.scale()).round() as i32,
y: (self.y * window_info.scale()).round() as i32,
}
}
}
/// A point in actual physical coordinates
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PhyPoint {
pub x: i32,
pub y: i32,
}
impl PhyPoint {
/// Create a new point in actual physical coordinates
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
/// Convert to logical coordinates
#[inline]
pub fn to_logical(&self, window_info: &WindowInfo) -> Point {
Point {
x: f64::from(self.x) * window_info.scale_recip(),
y: f64::from(self.y) * window_info.scale_recip(),
}
}
}
/// A size in logical coordinates
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Size {
pub width: f64,
pub height: f64,
}
impl Size {
/// Create a new size in logical coordinates
pub fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
/// Convert to actual physical size
#[inline]
pub fn to_physical(&self, window_info: &WindowInfo) -> PhySize {
PhySize {
width: (self.width * window_info.scale()).round() as u32,
height: (self.height * window_info.scale()).round() as u32,
}
}
}
/// An actual size in physical coordinates
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PhySize {
pub width: u32,
pub height: u32,
}
impl PhySize {
/// Create a new size in actual physical coordinates
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
/// Convert to logical size
#[inline]
pub fn to_logical(&self, window_info: &WindowInfo) -> Size {
Size {
width: f64::from(self.width) * window_info.scale_recip(),
height: f64::from(self.height) * window_info.scale_recip(),
}
}
}

View File

@@ -0,0 +1,29 @@
use crate::Size;
/// The dpi scaling policy of the window
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowScalePolicy {
/// Use the system's dpi scale factor
SystemScaleFactor,
/// Use the given dpi scale factor (e.g. `1.0` = 96 dpi)
ScaleFactor(f64),
}
/// The options for opening a new window
pub struct WindowOpenOptions {
pub title: String,
/// The logical size of the window.
///
/// These dimensions will be scaled by the scaling policy specified in `scale`. Mouse
/// position will be passed back as logical coordinates.
pub size: Size,
/// The dpi scaling policy
pub scale: WindowScalePolicy,
/// If provided, then an OpenGL context will be created for this window. You'll be able to
/// access this context through [crate::Window::gl_context].
#[cfg(feature = "opengl")]
pub gl_config: Option<crate::gl::GlConfig>,
}

View File

@@ -0,0 +1,100 @@
use std::error::Error;
use x11rb::connection::Connection;
use x11rb::cursor::Handle as CursorHandle;
use x11rb::protocol::xproto::{ConnectionExt as _, Cursor};
use x11rb::xcb_ffi::XCBConnection;
use crate::MouseCursor;
fn create_empty_cursor(conn: &XCBConnection, screen: usize) -> Result<Cursor, Box<dyn Error>> {
let cursor_id = conn.generate_id()?;
let pixmap_id = conn.generate_id()?;
let root_window = conn.setup().roots[screen].root;
conn.create_pixmap(1, pixmap_id, root_window, 1, 1)?;
conn.create_cursor(cursor_id, pixmap_id, pixmap_id, 0, 0, 0, 0, 0, 0, 0, 0)?;
conn.free_pixmap(pixmap_id)?;
Ok(cursor_id)
}
fn load_cursor(
conn: &XCBConnection, cursor_handle: &CursorHandle, name: &str,
) -> Result<Option<Cursor>, Box<dyn Error>> {
let cursor = cursor_handle.load_cursor(conn, name)?;
if cursor != x11rb::NONE {
Ok(Some(cursor))
} else {
Ok(None)
}
}
fn load_first_existing_cursor(
conn: &XCBConnection, cursor_handle: &CursorHandle, names: &[&str],
) -> Result<Option<Cursor>, Box<dyn Error>> {
for name in names {
let cursor = load_cursor(conn, cursor_handle, name)?;
if cursor.is_some() {
return Ok(cursor);
}
}
Ok(None)
}
pub(super) fn get_xcursor(
conn: &XCBConnection, screen: usize, cursor_handle: &CursorHandle, cursor: MouseCursor,
) -> Result<Cursor, Box<dyn Error>> {
let load = |name: &str| load_cursor(conn, cursor_handle, name);
let loadn = |names: &[&str]| load_first_existing_cursor(conn, cursor_handle, names);
let cursor = match cursor {
MouseCursor::Default => None, // catch this in the fallback case below
MouseCursor::Hand => loadn(&["hand2", "hand1"])?,
MouseCursor::HandGrabbing => loadn(&["closedhand", "grabbing"])?,
MouseCursor::Help => load("question_arrow")?,
MouseCursor::Hidden => Some(create_empty_cursor(conn, screen)?),
MouseCursor::Text => loadn(&["text", "xterm"])?,
MouseCursor::VerticalText => load("vertical-text")?,
MouseCursor::Working => load("watch")?,
MouseCursor::PtrWorking => load("left_ptr_watch")?,
MouseCursor::NotAllowed => load("crossed_circle")?,
MouseCursor::PtrNotAllowed => loadn(&["no-drop", "crossed_circle"])?,
MouseCursor::ZoomIn => load("zoom-in")?,
MouseCursor::ZoomOut => load("zoom-out")?,
MouseCursor::Alias => load("link")?,
MouseCursor::Copy => load("copy")?,
MouseCursor::Move => load("move")?,
MouseCursor::AllScroll => load("all-scroll")?,
MouseCursor::Cell => load("plus")?,
MouseCursor::Crosshair => load("crosshair")?,
MouseCursor::EResize => load("right_side")?,
MouseCursor::NResize => load("top_side")?,
MouseCursor::NeResize => load("top_right_corner")?,
MouseCursor::NwResize => load("top_left_corner")?,
MouseCursor::SResize => load("bottom_side")?,
MouseCursor::SeResize => load("bottom_right_corner")?,
MouseCursor::SwResize => load("bottom_left_corner")?,
MouseCursor::WResize => load("left_side")?,
MouseCursor::EwResize => load("h_double_arrow")?,
MouseCursor::NsResize => load("v_double_arrow")?,
MouseCursor::NwseResize => loadn(&["bd_double_arrow", "size_bdiag"])?,
MouseCursor::NeswResize => loadn(&["fd_double_arrow", "size_fdiag"])?,
MouseCursor::ColResize => loadn(&["split_h", "h_double_arrow"])?,
MouseCursor::RowResize => loadn(&["split_v", "v_double_arrow"])?,
};
if let Some(cursor) = cursor {
Ok(cursor)
} else {
Ok(load("left_ptr")?.unwrap_or(x11rb::NONE))
}
}

View File

@@ -0,0 +1,305 @@
use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods};
use crate::x11::{ParentHandle, Window, WindowInner};
use crate::{
Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler,
WindowInfo,
};
use std::error::Error;
use std::os::fd::AsRawFd;
use std::time::{Duration, Instant};
use x11rb::connection::Connection;
use x11rb::protocol::Event as XEvent;
pub(super) struct EventLoop {
handler: Box<dyn WindowHandler>,
window: WindowInner,
parent_handle: Option<ParentHandle>,
new_physical_size: Option<PhySize>,
frame_interval: Duration,
event_loop_running: bool,
}
impl EventLoop {
pub fn new(
window: WindowInner, handler: impl WindowHandler + 'static,
parent_handle: Option<ParentHandle>,
) -> Self {
Self {
window,
handler: Box::new(handler),
parent_handle,
frame_interval: Duration::from_millis(15),
event_loop_running: false,
new_physical_size: None,
}
}
#[inline]
fn drain_xcb_events(&mut self) -> Result<(), Box<dyn Error>> {
// the X server has a tendency to send spurious/extraneous configure notify events when a
// window is resized, and we need to batch those together and just send one resize event
// when they've all been coalesced.
self.new_physical_size = None;
while let Some(event) = self.window.xcb_connection.conn.poll_for_event()? {
self.handle_xcb_event(event);
}
if let Some(size) = self.new_physical_size.take() {
self.window.window_info =
WindowInfo::from_physical_size(size, self.window.window_info.scale());
let window_info = self.window.window_info;
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Window(WindowEvent::Resized(window_info)),
);
}
Ok(())
}
// Event loop
// FIXME: poll() acts fine on linux, sometimes funky on *BSD. XCB upstream uses a define to
// switch between poll() and select() (the latter of which is fine on *BSD), and we should do
// the same.
pub fn run(&mut self) -> Result<(), Box<dyn Error>> {
use nix::poll::*;
let xcb_fd = self.window.xcb_connection.conn.as_raw_fd();
let mut last_frame = Instant::now();
self.event_loop_running = true;
while self.event_loop_running {
// We'll try to keep a consistent frame pace. If the last frame couldn't be processed in
// the expected frame time, this will throttle down to prevent multiple frames from
// being queued up. The conditional here is needed because event handling and frame
// drawing is interleaved. The `poll()` function below will wait until the next frame
// can be drawn, or until the window receives an event. We thus need to manually check
// if it's already time to draw a new frame.
let next_frame = last_frame + self.frame_interval;
if Instant::now() >= next_frame {
self.handler.on_frame(&mut crate::Window::new(Window { inner: &self.window }));
last_frame = Instant::max(next_frame, Instant::now() - self.frame_interval);
}
let mut fds = [PollFd::new(xcb_fd, PollFlags::POLLIN)];
// Check for any events in the internal buffers
// before going to sleep:
self.drain_xcb_events()?;
// FIXME: handle errors
poll(&mut fds, next_frame.duration_since(Instant::now()).subsec_millis() as i32)
.unwrap();
if let Some(revents) = fds[0].revents() {
if revents.contains(PollFlags::POLLERR) {
panic!("xcb connection poll error");
}
if revents.contains(PollFlags::POLLIN) {
self.drain_xcb_events()?;
}
}
// Check if the parents's handle was dropped (such as when the host
// requested the window to close)
if let Some(parent_handle) = &self.parent_handle {
if parent_handle.parent_did_drop() {
self.handle_must_close();
self.window.close_requested.set(false);
}
}
// Check if the user has requested the window to close
if self.window.close_requested.get() {
self.handle_must_close();
self.window.close_requested.set(false);
}
}
Ok(())
}
fn handle_xcb_event(&mut self, event: XEvent) {
// For all the keyboard and mouse events, you can fetch
// `x`, `y`, `detail`, and `state`.
// - `x` and `y` are the position inside the window where the cursor currently is
// when the event happened.
// - `detail` will tell you which keycode was pressed/released (for keyboard events)
// or which mouse button was pressed/released (for mouse events).
// For mouse events, here's what the value means (at least on my current mouse):
// 1 = left mouse button
// 2 = middle mouse button (scroll wheel)
// 3 = right mouse button
// 4 = scroll wheel up
// 5 = scroll wheel down
// 8 = lower side button ("back" button)
// 9 = upper side button ("forward" button)
// Note that you *will* get a "button released" event for even the scroll wheel
// events, which you can probably ignore.
// - `state` will tell you the state of the main three mouse buttons and some of
// the keyboard modifier keys at the time of the event.
// http://rtbo.github.io/rust-xcb/src/xcb/ffi/xproto.rs.html#445
match event {
////
// window
////
XEvent::ClientMessage(event) => {
if event.format == 32
&& event.data.as_data32()[0]
== self.window.xcb_connection.atoms.WM_DELETE_WINDOW
{
self.handle_close_requested();
}
}
XEvent::ConfigureNotify(event) => {
let new_physical_size = PhySize::new(event.width as u32, event.height as u32);
if self.new_physical_size.is_some()
|| new_physical_size != self.window.window_info.physical_size()
{
self.new_physical_size = Some(new_physical_size);
}
}
////
// mouse
////
XEvent::MotionNotify(event) => {
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
let logical_pos = physical_pos.to_logical(&self.window.window_info);
let screen_physical = PhyPoint::new(event.root_x as i32, event.root_y as i32);
let screen_position = screen_physical.to_logical(&self.window.window_info);
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::CursorMoved {
position: logical_pos,
screen_position,
modifiers: key_mods(event.state),
}),
);
}
XEvent::EnterNotify(event) => {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::CursorEntered),
);
// since no `MOTION_NOTIFY` event is generated when `ENTER_NOTIFY` is generated,
// we generate a CursorMoved as well, so the mouse position from here isn't lost
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
let logical_pos = physical_pos.to_logical(&self.window.window_info);
let screen_physical = PhyPoint::new(event.root_x as i32, event.root_y as i32);
let screen_position = screen_physical.to_logical(&self.window.window_info);
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::CursorMoved {
position: logical_pos,
screen_position,
modifiers: key_mods(event.state),
}),
);
}
XEvent::LeaveNotify(_) => {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::CursorLeft),
);
}
XEvent::ButtonPress(event) => match event.detail {
4..=7 => {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::WheelScrolled {
delta: match event.detail {
4 => ScrollDelta::Lines { x: 0.0, y: 1.0 },
5 => ScrollDelta::Lines { x: 0.0, y: -1.0 },
6 => ScrollDelta::Lines { x: -1.0, y: 0.0 },
7 => ScrollDelta::Lines { x: 1.0, y: 0.0 },
_ => unreachable!(),
},
modifiers: key_mods(event.state),
}),
);
}
detail => {
let button_id = mouse_id(detail);
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::ButtonPressed {
button: button_id,
modifiers: key_mods(event.state),
}),
);
}
},
XEvent::ButtonRelease(event) => {
if !(4..=7).contains(&event.detail) {
let button_id = mouse_id(event.detail);
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Mouse(MouseEvent::ButtonReleased {
button: button_id,
modifiers: key_mods(event.state),
}),
);
}
}
////
// keys
////
XEvent::KeyPress(event) => {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Keyboard(convert_key_press_event(&event)),
);
}
XEvent::KeyRelease(event) => {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Keyboard(convert_key_release_event(&event)),
);
}
_ => {}
}
}
fn handle_close_requested(&mut self) {
// FIXME: handler should decide whether window stays open or not
self.handle_must_close();
}
fn handle_must_close(&mut self) {
self.handler.on_event(
&mut crate::Window::new(Window { inner: &self.window }),
Event::Window(WindowEvent::WillClose),
);
self.event_loop_running = false;
}
}
fn mouse_id(id: u8) -> MouseButton {
match id {
1 => MouseButton::Left,
2 => MouseButton::Middle,
3 => MouseButton::Right,
8 => MouseButton::Back,
9 => MouseButton::Forward,
id => MouseButton::Other(id),
}
}

View File

@@ -0,0 +1,406 @@
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Baseview modifications to druid code:
// - collect functions from various files
// - update imports, paths etc
//! X11 keyboard handling
use x11rb::protocol::xproto::{KeyButMask, KeyPressEvent, KeyReleaseEvent};
use keyboard_types::*;
use crate::keyboard::code_to_location;
/// Convert a hardware scan code to a key.
///
/// Note: this is a hardcoded layout. We need to detect the user's
/// layout from the system and apply it.
fn code_to_key(code: Code, m: Modifiers) -> Key {
fn a(s: &str) -> Key {
Key::Character(s.into())
}
fn s(mods: Modifiers, base: &str, shifted: &str) -> Key {
if mods.contains(Modifiers::SHIFT) {
Key::Character(shifted.into())
} else {
Key::Character(base.into())
}
}
fn n(mods: Modifiers, base: Key, num: &str) -> Key {
if mods.contains(Modifiers::NUM_LOCK) != mods.contains(Modifiers::SHIFT) {
Key::Character(num.into())
} else {
base
}
}
match code {
Code::KeyA => s(m, "a", "A"),
Code::KeyB => s(m, "b", "B"),
Code::KeyC => s(m, "c", "C"),
Code::KeyD => s(m, "d", "D"),
Code::KeyE => s(m, "e", "E"),
Code::KeyF => s(m, "f", "F"),
Code::KeyG => s(m, "g", "G"),
Code::KeyH => s(m, "h", "H"),
Code::KeyI => s(m, "i", "I"),
Code::KeyJ => s(m, "j", "J"),
Code::KeyK => s(m, "k", "K"),
Code::KeyL => s(m, "l", "L"),
Code::KeyM => s(m, "m", "M"),
Code::KeyN => s(m, "n", "N"),
Code::KeyO => s(m, "o", "O"),
Code::KeyP => s(m, "p", "P"),
Code::KeyQ => s(m, "q", "Q"),
Code::KeyR => s(m, "r", "R"),
Code::KeyS => s(m, "s", "S"),
Code::KeyT => s(m, "t", "T"),
Code::KeyU => s(m, "u", "U"),
Code::KeyV => s(m, "v", "V"),
Code::KeyW => s(m, "w", "W"),
Code::KeyX => s(m, "x", "X"),
Code::KeyY => s(m, "y", "Y"),
Code::KeyZ => s(m, "z", "Z"),
Code::Digit0 => s(m, "0", ")"),
Code::Digit1 => s(m, "1", "!"),
Code::Digit2 => s(m, "2", "@"),
Code::Digit3 => s(m, "3", "#"),
Code::Digit4 => s(m, "4", "$"),
Code::Digit5 => s(m, "5", "%"),
Code::Digit6 => s(m, "6", "^"),
Code::Digit7 => s(m, "7", "&"),
Code::Digit8 => s(m, "8", "*"),
Code::Digit9 => s(m, "9", "("),
Code::Backquote => s(m, "`", "~"),
Code::Minus => s(m, "-", "_"),
Code::Equal => s(m, "=", "+"),
Code::BracketLeft => s(m, "[", "{"),
Code::BracketRight => s(m, "]", "}"),
Code::Backslash => s(m, "\\", "|"),
Code::Semicolon => s(m, ";", ":"),
Code::Quote => s(m, "'", "\""),
Code::Comma => s(m, ",", "<"),
Code::Period => s(m, ".", ">"),
Code::Slash => s(m, "/", "?"),
Code::Space => a(" "),
Code::Escape => Key::Escape,
Code::Backspace => Key::Backspace,
Code::Tab => Key::Tab,
Code::Enter => Key::Enter,
Code::ControlLeft => Key::Control,
Code::ShiftLeft => Key::Shift,
Code::ShiftRight => Key::Shift,
Code::NumpadMultiply => a("*"),
Code::AltLeft => Key::Alt,
Code::CapsLock => Key::CapsLock,
Code::F1 => Key::F1,
Code::F2 => Key::F2,
Code::F3 => Key::F3,
Code::F4 => Key::F4,
Code::F5 => Key::F5,
Code::F6 => Key::F6,
Code::F7 => Key::F7,
Code::F8 => Key::F8,
Code::F9 => Key::F9,
Code::F10 => Key::F10,
Code::NumLock => Key::NumLock,
Code::ScrollLock => Key::ScrollLock,
Code::Numpad0 => n(m, Key::Insert, "0"),
Code::Numpad1 => n(m, Key::End, "1"),
Code::Numpad2 => n(m, Key::ArrowDown, "2"),
Code::Numpad3 => n(m, Key::PageDown, "3"),
Code::Numpad4 => n(m, Key::ArrowLeft, "4"),
Code::Numpad5 => n(m, Key::Clear, "5"),
Code::Numpad6 => n(m, Key::ArrowRight, "6"),
Code::Numpad7 => n(m, Key::Home, "7"),
Code::Numpad8 => n(m, Key::ArrowUp, "8"),
Code::Numpad9 => n(m, Key::PageUp, "9"),
Code::NumpadSubtract => a("-"),
Code::NumpadAdd => a("+"),
Code::NumpadDecimal => n(m, Key::Delete, "."),
Code::IntlBackslash => s(m, "\\", "|"),
Code::F11 => Key::F11,
Code::F12 => Key::F12,
// This mapping is based on the picture in the w3c spec.
Code::IntlRo => a("\\"),
Code::Convert => Key::Convert,
Code::KanaMode => Key::KanaMode,
Code::NonConvert => Key::NonConvert,
Code::NumpadEnter => Key::Enter,
Code::ControlRight => Key::Control,
Code::NumpadDivide => a("/"),
Code::PrintScreen => Key::PrintScreen,
Code::AltRight => Key::Alt,
Code::Home => Key::Home,
Code::ArrowUp => Key::ArrowUp,
Code::PageUp => Key::PageUp,
Code::ArrowLeft => Key::ArrowLeft,
Code::ArrowRight => Key::ArrowRight,
Code::End => Key::End,
Code::ArrowDown => Key::ArrowDown,
Code::PageDown => Key::PageDown,
Code::Insert => Key::Insert,
Code::Delete => Key::Delete,
Code::AudioVolumeMute => Key::AudioVolumeMute,
Code::AudioVolumeDown => Key::AudioVolumeDown,
Code::AudioVolumeUp => Key::AudioVolumeUp,
Code::NumpadEqual => a("="),
Code::Pause => Key::Pause,
Code::NumpadComma => a(","),
Code::Lang1 => Key::HangulMode,
Code::Lang2 => Key::HanjaMode,
Code::IntlYen => a("¥"),
Code::MetaLeft => Key::Meta,
Code::MetaRight => Key::Meta,
Code::ContextMenu => Key::ContextMenu,
Code::BrowserStop => Key::BrowserStop,
Code::Again => Key::Again,
Code::Props => Key::Props,
Code::Undo => Key::Undo,
Code::Select => Key::Select,
Code::Copy => Key::Copy,
Code::Open => Key::Open,
Code::Paste => Key::Paste,
Code::Find => Key::Find,
Code::Cut => Key::Cut,
Code::Help => Key::Help,
Code::LaunchApp2 => Key::LaunchApplication2,
Code::WakeUp => Key::WakeUp,
Code::LaunchApp1 => Key::LaunchApplication1,
Code::LaunchMail => Key::LaunchMail,
Code::BrowserFavorites => Key::BrowserFavorites,
Code::BrowserBack => Key::BrowserBack,
Code::BrowserForward => Key::BrowserForward,
Code::Eject => Key::Eject,
Code::MediaTrackNext => Key::MediaTrackNext,
Code::MediaPlayPause => Key::MediaPlayPause,
Code::MediaTrackPrevious => Key::MediaTrackPrevious,
Code::MediaStop => Key::MediaStop,
Code::MediaSelect => Key::LaunchMediaPlayer,
Code::BrowserHome => Key::BrowserHome,
Code::BrowserRefresh => Key::BrowserRefresh,
Code::BrowserSearch => Key::BrowserSearch,
_ => Key::Unidentified,
}
}
#[cfg(target_os = "linux")]
/// Map hardware keycode to code.
///
/// In theory, the hardware keycode is device dependent, but in
/// practice it's probably pretty reliable.
///
/// The logic is based on NativeKeyToDOMCodeName.h in Mozilla.
fn hardware_keycode_to_code(hw_keycode: u16) -> Code {
match hw_keycode {
0x0009 => Code::Escape,
0x000A => Code::Digit1,
0x000B => Code::Digit2,
0x000C => Code::Digit3,
0x000D => Code::Digit4,
0x000E => Code::Digit5,
0x000F => Code::Digit6,
0x0010 => Code::Digit7,
0x0011 => Code::Digit8,
0x0012 => Code::Digit9,
0x0013 => Code::Digit0,
0x0014 => Code::Minus,
0x0015 => Code::Equal,
0x0016 => Code::Backspace,
0x0017 => Code::Tab,
0x0018 => Code::KeyQ,
0x0019 => Code::KeyW,
0x001A => Code::KeyE,
0x001B => Code::KeyR,
0x001C => Code::KeyT,
0x001D => Code::KeyY,
0x001E => Code::KeyU,
0x001F => Code::KeyI,
0x0020 => Code::KeyO,
0x0021 => Code::KeyP,
0x0022 => Code::BracketLeft,
0x0023 => Code::BracketRight,
0x0024 => Code::Enter,
0x0025 => Code::ControlLeft,
0x0026 => Code::KeyA,
0x0027 => Code::KeyS,
0x0028 => Code::KeyD,
0x0029 => Code::KeyF,
0x002A => Code::KeyG,
0x002B => Code::KeyH,
0x002C => Code::KeyJ,
0x002D => Code::KeyK,
0x002E => Code::KeyL,
0x002F => Code::Semicolon,
0x0030 => Code::Quote,
0x0031 => Code::Backquote,
0x0032 => Code::ShiftLeft,
0x0033 => Code::Backslash,
0x0034 => Code::KeyZ,
0x0035 => Code::KeyX,
0x0036 => Code::KeyC,
0x0037 => Code::KeyV,
0x0038 => Code::KeyB,
0x0039 => Code::KeyN,
0x003A => Code::KeyM,
0x003B => Code::Comma,
0x003C => Code::Period,
0x003D => Code::Slash,
0x003E => Code::ShiftRight,
0x003F => Code::NumpadMultiply,
0x0040 => Code::AltLeft,
0x0041 => Code::Space,
0x0042 => Code::CapsLock,
0x0043 => Code::F1,
0x0044 => Code::F2,
0x0045 => Code::F3,
0x0046 => Code::F4,
0x0047 => Code::F5,
0x0048 => Code::F6,
0x0049 => Code::F7,
0x004A => Code::F8,
0x004B => Code::F9,
0x004C => Code::F10,
0x004D => Code::NumLock,
0x004E => Code::ScrollLock,
0x004F => Code::Numpad7,
0x0050 => Code::Numpad8,
0x0051 => Code::Numpad9,
0x0052 => Code::NumpadSubtract,
0x0053 => Code::Numpad4,
0x0054 => Code::Numpad5,
0x0055 => Code::Numpad6,
0x0056 => Code::NumpadAdd,
0x0057 => Code::Numpad1,
0x0058 => Code::Numpad2,
0x0059 => Code::Numpad3,
0x005A => Code::Numpad0,
0x005B => Code::NumpadDecimal,
0x005E => Code::IntlBackslash,
0x005F => Code::F11,
0x0060 => Code::F12,
0x0061 => Code::IntlRo,
0x0064 => Code::Convert,
0x0065 => Code::KanaMode,
0x0066 => Code::NonConvert,
0x0068 => Code::NumpadEnter,
0x0069 => Code::ControlRight,
0x006A => Code::NumpadDivide,
0x006B => Code::PrintScreen,
0x006C => Code::AltRight,
0x006E => Code::Home,
0x006F => Code::ArrowUp,
0x0070 => Code::PageUp,
0x0071 => Code::ArrowLeft,
0x0072 => Code::ArrowRight,
0x0073 => Code::End,
0x0074 => Code::ArrowDown,
0x0075 => Code::PageDown,
0x0076 => Code::Insert,
0x0077 => Code::Delete,
0x0079 => Code::AudioVolumeMute,
0x007A => Code::AudioVolumeDown,
0x007B => Code::AudioVolumeUp,
0x007D => Code::NumpadEqual,
0x007F => Code::Pause,
0x0081 => Code::NumpadComma,
0x0082 => Code::Lang1,
0x0083 => Code::Lang2,
0x0084 => Code::IntlYen,
0x0085 => Code::MetaLeft,
0x0086 => Code::MetaRight,
0x0087 => Code::ContextMenu,
0x0088 => Code::BrowserStop,
0x0089 => Code::Again,
0x008A => Code::Props,
0x008B => Code::Undo,
0x008C => Code::Select,
0x008D => Code::Copy,
0x008E => Code::Open,
0x008F => Code::Paste,
0x0090 => Code::Find,
0x0091 => Code::Cut,
0x0092 => Code::Help,
0x0094 => Code::LaunchApp2,
0x0097 => Code::WakeUp,
0x0098 => Code::LaunchApp1,
// key to right of volume controls on T430s produces 0x9C
// but no documentation of what it should map to :/
0x00A3 => Code::LaunchMail,
0x00A4 => Code::BrowserFavorites,
0x00A6 => Code::BrowserBack,
0x00A7 => Code::BrowserForward,
0x00A9 => Code::Eject,
0x00AB => Code::MediaTrackNext,
0x00AC => Code::MediaPlayPause,
0x00AD => Code::MediaTrackPrevious,
0x00AE => Code::MediaStop,
0x00B3 => Code::MediaSelect,
0x00B4 => Code::BrowserHome,
0x00B5 => Code::BrowserRefresh,
0x00E1 => Code::BrowserSearch,
_ => Code::Unidentified,
}
}
// Extracts the keyboard modifiers from, e.g., the `state` field of
// `x11rb::protocol::xproto::ButtonPressEvent`
pub(super) fn key_mods(mods: KeyButMask) -> Modifiers {
let mut ret = Modifiers::default();
let key_masks = [
(KeyButMask::SHIFT, Modifiers::SHIFT),
(KeyButMask::CONTROL, Modifiers::CONTROL),
// X11's mod keys are configurable, but this seems
// like a reasonable default for US keyboards, at least,
// where the "windows" key seems to be MOD_MASK_4.
(KeyButMask::MOD1, Modifiers::ALT),
(KeyButMask::MOD2, Modifiers::NUM_LOCK),
(KeyButMask::MOD4, Modifiers::META),
(KeyButMask::LOCK, Modifiers::CAPS_LOCK),
];
for (mask, modifiers) in &key_masks {
if mods.contains(*mask) {
ret |= *modifiers;
}
}
ret
}
pub(super) fn convert_key_press_event(key_press: &KeyPressEvent) -> KeyboardEvent {
let hw_keycode = key_press.detail;
let code = hardware_keycode_to_code(hw_keycode.into());
let modifiers = key_mods(key_press.state);
let key = code_to_key(code, modifiers);
let location = code_to_location(code);
let state = KeyState::Down;
KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false }
}
pub(super) fn convert_key_release_event(key_release: &KeyReleaseEvent) -> KeyboardEvent {
let hw_keycode = key_release.detail;
let code = hardware_keycode_to_code(hw_keycode.into());
let modifiers = key_mods(key_release.state);
let key = code_to_key(code, modifiers);
let location = code_to_location(code);
let state = KeyState::Up;
KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false }
}

View File

@@ -0,0 +1,10 @@
mod xcb_connection;
use xcb_connection::XcbConnection;
mod window;
pub use window::*;
mod cursor;
mod event_loop;
mod keyboard;
mod visual_info;

View File

@@ -0,0 +1,94 @@
use crate::x11::xcb_connection::XcbConnection;
use std::error::Error;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{
Colormap, ColormapAlloc, ConnectionExt, Screen, VisualClass, Visualid,
};
use x11rb::COPY_FROM_PARENT;
pub(super) struct WindowVisualConfig {
#[cfg(feature = "opengl")]
pub fb_config: Option<crate::gl::x11::FbConfig>,
pub visual_depth: u8,
pub visual_id: Visualid,
pub color_map: Option<Colormap>,
}
// TODO: make visual negotiation actually check all of a visual's parameters
impl WindowVisualConfig {
#[cfg(feature = "opengl")]
pub fn find_best_visual_config_for_gl(
connection: &XcbConnection, gl_config: Option<crate::gl::GlConfig>,
) -> Result<Self, Box<dyn Error>> {
let Some(gl_config) = gl_config else { return Self::find_best_visual_config(connection) };
// SAFETY: TODO
let (fb_config, window_config) = unsafe {
crate::gl::platform::GlContext::get_fb_config_and_visual(connection.dpy, gl_config)
}
.expect("Could not fetch framebuffer config");
Ok(Self {
fb_config: Some(fb_config),
visual_depth: window_config.depth,
visual_id: window_config.visual,
color_map: Some(create_color_map(connection, window_config.visual)?),
})
}
pub fn find_best_visual_config(connection: &XcbConnection) -> Result<Self, Box<dyn Error>> {
match find_visual_for_depth(connection.screen(), 32) {
None => Ok(Self::copy_from_parent()),
Some(visual_id) => Ok(Self {
#[cfg(feature = "opengl")]
fb_config: None,
visual_id,
visual_depth: 32,
color_map: Some(create_color_map(connection, visual_id)?),
}),
}
}
const fn copy_from_parent() -> Self {
Self {
#[cfg(feature = "opengl")]
fb_config: None,
visual_depth: COPY_FROM_PARENT as u8,
visual_id: COPY_FROM_PARENT,
color_map: None,
}
}
}
// For this 32-bit depth to work, you also need to define a color map and set a border
// pixel: https://cgit.freedesktop.org/xorg/xserver/tree/dix/window.c#n818
fn create_color_map(
connection: &XcbConnection, visual_id: Visualid,
) -> Result<Colormap, Box<dyn Error>> {
let colormap = connection.conn.generate_id()?;
connection.conn.create_colormap(
ColormapAlloc::NONE,
colormap,
connection.screen().root,
visual_id,
)?;
Ok(colormap)
}
fn find_visual_for_depth(screen: &Screen, depth: u8) -> Option<Visualid> {
for candidate_depth in &screen.allowed_depths {
if candidate_depth.depth != depth {
continue;
}
for candidate_visual in &candidate_depth.visuals {
if candidate_visual.class == VisualClass::TRUE_COLOR {
return Some(candidate_visual.visual_id);
}
}
}
None
}

View File

@@ -0,0 +1,380 @@
use std::cell::Cell;
use std::error::Error;
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, XlibDisplayHandle,
XlibWindowHandle,
};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{
AtomEnum, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt as _, CreateGCAux,
CreateWindowAux, EventMask, PropMode, Visualid, Window as XWindow, WindowClass,
};
use x11rb::wrapper::ConnectionExt as _;
use super::XcbConnection;
use crate::{
Event, MouseCursor, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions,
WindowScalePolicy,
};
#[cfg(feature = "opengl")]
use crate::gl::{platform, GlContext};
use crate::x11::event_loop::EventLoop;
use crate::x11::visual_info::WindowVisualConfig;
pub struct WindowHandle {
raw_window_handle: Option<RawWindowHandle>,
event_loop_handle: Option<JoinHandle<()>>,
close_requested: Arc<AtomicBool>,
is_open: Arc<AtomicBool>,
}
impl WindowHandle {
pub fn close(&mut self) {
self.close_requested.store(true, Ordering::Relaxed);
if let Some(event_loop) = self.event_loop_handle.take() {
let _ = event_loop.join();
}
}
pub fn is_open(&self) -> bool {
self.is_open.load(Ordering::Relaxed)
}
}
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
if let Some(raw_window_handle) = self.raw_window_handle {
if self.is_open.load(Ordering::Relaxed) {
return raw_window_handle;
}
}
RawWindowHandle::Xlib(XlibWindowHandle::empty())
}
}
pub(crate) struct ParentHandle {
close_requested: Arc<AtomicBool>,
is_open: Arc<AtomicBool>,
}
impl ParentHandle {
pub fn new() -> (Self, WindowHandle) {
let close_requested = Arc::new(AtomicBool::new(false));
let is_open = Arc::new(AtomicBool::new(true));
let handle = WindowHandle {
raw_window_handle: None,
event_loop_handle: None,
close_requested: Arc::clone(&close_requested),
is_open: Arc::clone(&is_open),
};
(Self { close_requested, is_open }, handle)
}
pub fn parent_did_drop(&self) -> bool {
self.close_requested.load(Ordering::Relaxed)
}
}
impl Drop for ParentHandle {
fn drop(&mut self) {
self.is_open.store(false, Ordering::Relaxed);
}
}
pub(crate) struct WindowInner {
// GlContext should be dropped **before** XcbConnection is dropped
#[cfg(feature = "opengl")]
gl_context: Option<GlContext>,
pub(crate) xcb_connection: XcbConnection,
window_id: XWindow,
pub(crate) window_info: WindowInfo,
visual_id: Visualid,
mouse_cursor: Cell<MouseCursor>,
pub(crate) close_requested: Cell<bool>,
}
pub struct Window<'a> {
pub(crate) inner: &'a WindowInner,
}
// Hack to allow sending a RawWindowHandle between threads. Do not make public
struct SendableRwh(RawWindowHandle);
unsafe impl Send for SendableRwh {}
type WindowOpenResult = Result<SendableRwh, ()>;
impl<'a> Window<'a> {
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
where
P: HasRawWindowHandle,
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
// Convert parent into something that X understands
let parent_id = match parent.raw_window_handle() {
RawWindowHandle::Xlib(h) => h.window as u32,
RawWindowHandle::Xcb(h) => h.window,
h => panic!("unsupported parent handle type {:?}", h),
};
let (tx, rx) = mpsc::sync_channel::<WindowOpenResult>(1);
let (parent_handle, mut window_handle) = ParentHandle::new();
let join_handle = thread::spawn(move || {
Self::window_thread(Some(parent_id), options, build, tx.clone(), Some(parent_handle))
.unwrap();
});
let raw_window_handle = rx.recv().unwrap().unwrap();
window_handle.raw_window_handle = Some(raw_window_handle.0);
window_handle.event_loop_handle = Some(join_handle);
window_handle
}
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
let (tx, rx) = mpsc::sync_channel::<WindowOpenResult>(1);
let thread = thread::spawn(move || {
Self::window_thread(None, options, build, tx, None).unwrap();
});
let _ = rx.recv().unwrap().unwrap();
thread.join().unwrap_or_else(|err| {
eprintln!("Window thread panicked: {:#?}", err);
});
}
fn window_thread<H, B>(
parent: Option<u32>, options: WindowOpenOptions, build: B,
tx: mpsc::SyncSender<WindowOpenResult>, parent_handle: Option<ParentHandle>,
) -> Result<(), Box<dyn Error>>
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
B: Send + 'static,
{
// Connect to the X server
// FIXME: baseview error type instead of unwrap()
let xcb_connection = XcbConnection::new()?;
// Get screen information
let screen = xcb_connection.screen();
let parent_id = parent.unwrap_or(screen.root);
let gc_id = xcb_connection.conn.generate_id()?;
xcb_connection.conn.create_gc(
gc_id,
parent_id,
&CreateGCAux::new().foreground(screen.black_pixel).graphics_exposures(0),
)?;
let scaling = match options.scale {
WindowScalePolicy::SystemScaleFactor => xcb_connection.get_scaling().unwrap_or(1.0),
WindowScalePolicy::ScaleFactor(scale) => scale,
};
let window_info = WindowInfo::from_logical_size(options.size, scaling);
#[cfg(feature = "opengl")]
let visual_info =
WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?;
#[cfg(not(feature = "opengl"))]
let visual_info = WindowVisualConfig::find_best_visual_config(&xcb_connection)?;
let window_id = xcb_connection.conn.generate_id()?;
xcb_connection.conn.create_window(
visual_info.visual_depth,
window_id,
parent_id,
0, // x coordinate of the new window
0, // y coordinate of the new window
window_info.physical_size().width as u16, // window width
window_info.physical_size().height as u16, // window height
0, // window border
WindowClass::INPUT_OUTPUT,
visual_info.visual_id,
&CreateWindowAux::new()
.event_mask(
EventMask::EXPOSURE
| EventMask::POINTER_MOTION
| EventMask::BUTTON_PRESS
| EventMask::BUTTON_RELEASE
| EventMask::KEY_PRESS
| EventMask::KEY_RELEASE
| EventMask::STRUCTURE_NOTIFY
| EventMask::ENTER_WINDOW
| EventMask::LEAVE_WINDOW,
)
// As mentioned above, these two values are needed to be able to create a window
// with a depth of 32-bits when the parent window has a different depth
.colormap(visual_info.color_map)
.border_pixel(0),
)?;
xcb_connection.conn.map_window(window_id)?;
// Change window title
let title = options.title;
xcb_connection.conn.change_property8(
PropMode::REPLACE,
window_id,
AtomEnum::WM_NAME,
AtomEnum::STRING,
title.as_bytes(),
)?;
xcb_connection.conn.change_property32(
PropMode::REPLACE,
window_id,
xcb_connection.atoms.WM_PROTOCOLS,
AtomEnum::ATOM,
&[xcb_connection.atoms.WM_DELETE_WINDOW],
)?;
xcb_connection.conn.flush()?;
// TODO: These APIs could use a couple tweaks now that everything is internal and there is
// no error handling anymore at this point. Everything is more or less unchanged
// compared to when raw-gl-context was a separate crate.
#[cfg(feature = "opengl")]
let gl_context = visual_info.fb_config.map(|fb_config| {
use std::ffi::c_ulong;
let window = window_id as c_ulong;
let display = xcb_connection.dpy;
// Because of the visual negotation we had to take some extra steps to create this context
let context = unsafe { platform::GlContext::create(window, display, fb_config) }
.expect("Could not create OpenGL context");
GlContext::new(context)
});
let mut inner = WindowInner {
xcb_connection,
window_id,
window_info,
visual_id: visual_info.visual_id,
mouse_cursor: Cell::new(MouseCursor::default()),
close_requested: Cell::new(false),
#[cfg(feature = "opengl")]
gl_context,
};
let mut window = crate::Window::new(Window { inner: &mut inner });
let mut handler = build(&mut window);
// Send an initial window resized event so the user is alerted of
// the correct dpi scaling.
handler.on_event(&mut window, Event::Window(WindowEvent::Resized(window_info)));
let _ = tx.send(Ok(SendableRwh(window.raw_window_handle())));
EventLoop::new(inner, handler, parent_handle).run()?;
Ok(())
}
pub fn physical_size(&self) -> crate::PhySize {
self.inner.window_info.physical_size()
}
pub fn set_mouse_cursor(&self, mouse_cursor: MouseCursor) {
if self.inner.mouse_cursor.get() == mouse_cursor {
return;
}
let xid = self.inner.xcb_connection.get_cursor(mouse_cursor).unwrap();
if xid != 0 {
let _ = self.inner.xcb_connection.conn.change_window_attributes(
self.inner.window_id,
&ChangeWindowAttributesAux::new().cursor(xid),
);
let _ = self.inner.xcb_connection.conn.flush();
}
self.inner.mouse_cursor.set(mouse_cursor);
}
pub fn close(&mut self) {
self.inner.close_requested.set(true);
}
pub fn has_focus(&mut self) -> bool {
unimplemented!()
}
pub fn focus(&mut self) {
unimplemented!()
}
pub fn resize(&mut self, size: Size) {
let scaling = self.inner.window_info.scale();
let new_window_info = WindowInfo::from_logical_size(size, scaling);
let _ = self.inner.xcb_connection.conn.configure_window(
self.inner.window_id,
&ConfigureWindowAux::new()
.width(new_window_info.physical_size().width)
.height(new_window_info.physical_size().height),
);
let _ = self.inner.xcb_connection.conn.flush();
// This will trigger a `ConfigureNotify` event which will in turn change `self.window_info`
// and notify the window handler about it
}
#[cfg(feature = "opengl")]
pub fn gl_context(&self) -> Option<&crate::gl::GlContext> {
self.inner.gl_context.as_ref()
}
}
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
fn raw_window_handle(&self) -> RawWindowHandle {
let mut handle = XlibWindowHandle::empty();
handle.window = self.inner.window_id.into();
handle.visual_id = self.inner.visual_id.into();
RawWindowHandle::Xlib(handle)
}
}
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
fn raw_display_handle(&self) -> RawDisplayHandle {
let display = self.inner.xcb_connection.dpy;
let mut handle = XlibDisplayHandle::empty();
handle.display = display as *mut c_void;
handle.screen = unsafe { x11::xlib::XDefaultScreen(display) };
RawDisplayHandle::Xlib(handle)
}
}
pub fn copy_to_clipboard(_data: &str) {
todo!()
}

View File

@@ -0,0 +1,132 @@
use std::cell::RefCell;
use std::collections::hash_map::{Entry, HashMap};
use std::error::Error;
use x11::{xlib, xlib::Display, xlib_xcb};
use x11rb::connection::Connection;
use x11rb::cursor::Handle as CursorHandle;
use x11rb::protocol::xproto::{Cursor, Screen};
use x11rb::resource_manager;
use x11rb::xcb_ffi::XCBConnection;
use crate::MouseCursor;
use super::cursor;
x11rb::atom_manager! {
pub Atoms: AtomsCookie {
WM_PROTOCOLS,
WM_DELETE_WINDOW,
}
}
/// A very light abstraction around the XCB connection.
///
/// Keeps track of the xcb connection itself and the xlib display ID that was used to connect.
pub struct XcbConnection {
pub(crate) dpy: *mut Display,
pub(crate) conn: XCBConnection,
pub(crate) screen: usize,
pub(crate) atoms: Atoms,
pub(crate) resources: resource_manager::Database,
pub(crate) cursor_handle: CursorHandle,
pub(super) cursor_cache: RefCell<HashMap<MouseCursor, u32>>,
}
impl XcbConnection {
pub fn new() -> Result<Self, Box<dyn Error>> {
let dpy = unsafe { xlib::XOpenDisplay(std::ptr::null()) };
assert!(!dpy.is_null());
let xcb_connection = unsafe { xlib_xcb::XGetXCBConnection(dpy) };
assert!(!xcb_connection.is_null());
let screen = unsafe { xlib::XDefaultScreen(dpy) } as usize;
let conn = unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection, false)? };
unsafe {
xlib_xcb::XSetEventQueueOwner(dpy, xlib_xcb::XEventQueueOwner::XCBOwnsEventQueue)
};
let atoms = Atoms::new(&conn)?.reply()?;
let resources = resource_manager::new_from_default(&conn)?;
let cursor_handle = CursorHandle::new(&conn, screen, &resources)?.reply()?;
Ok(Self {
dpy,
conn,
screen,
atoms,
resources,
cursor_handle,
cursor_cache: RefCell::new(HashMap::new()),
})
}
// Try to get the scaling with this function first.
// If this gives you `None`, fall back to `get_scaling_screen_dimensions`.
// If neither work, I guess just assume 96.0 and don't do any scaling.
fn get_scaling_xft(&self) -> Result<Option<f64>, Box<dyn Error>> {
if let Some(dpi) = self.resources.get_value::<u32>("Xft.dpi", "")? {
Ok(Some(dpi as f64 / 96.0))
} else {
Ok(None)
}
}
// Try to get the scaling with `get_scaling_xft` first.
// Only use this function as a fallback.
// If neither work, I guess just assume 96.0 and don't do any scaling.
fn get_scaling_screen_dimensions(&self) -> f64 {
// Figure out screen information
let screen = self.screen();
// Get the DPI from the screen struct
//
// there are 2.54 centimeters to an inch; so there are 25.4 millimeters.
// dpi = N pixels / (M millimeters / (25.4 millimeters / 1 inch))
// = N pixels / (M inch / 25.4)
// = N * 25.4 pixels / M inch
let width_px = screen.width_in_pixels as f64;
let width_mm = screen.width_in_millimeters as f64;
let height_px = screen.height_in_pixels as f64;
let height_mm = screen.height_in_millimeters as f64;
let _xres = width_px * 25.4 / width_mm;
let yres = height_px * 25.4 / height_mm;
// TODO: choose between `xres` and `yres`? (probably both are the same?)
yres / 96.0
}
#[inline]
pub fn get_scaling(&self) -> Result<f64, Box<dyn Error>> {
Ok(self.get_scaling_xft()?.unwrap_or(self.get_scaling_screen_dimensions()))
}
#[inline]
pub fn get_cursor(&self, cursor: MouseCursor) -> Result<Cursor, Box<dyn Error>> {
// PANIC: this function is the only point where we access the cache, and we never call
// external functions that may make a reentrant call to this function
let mut cursor_cache = self.cursor_cache.borrow_mut();
match cursor_cache.entry(cursor) {
Entry::Occupied(entry) => Ok(*entry.get()),
Entry::Vacant(entry) => {
let cursor =
cursor::get_xcursor(&self.conn, self.screen, &self.cursor_handle, cursor)?;
entry.insert(cursor);
Ok(cursor)
}
}
}
pub fn screen(&self) -> &Screen {
&self.conn.setup().roots[self.screen]
}
}
impl Drop for XcbConnection {
fn drop(&mut self) {
unsafe {
xlib::XCloseDisplay(self.dpy);
}
}
}