Trying to clena the mess opened by plugins

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

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);
}
}
}