Trying to clena the mess opened by plugins
This commit is contained in:
10
plugins/baseview/src/clipboard.rs
Normal file
10
plugins/baseview/src/clipboard.rs
Normal 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)
|
||||
}
|
||||
170
plugins/baseview/src/event.rs
Normal file
170
plugins/baseview/src/event.rs
Normal 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),
|
||||
}
|
||||
155
plugins/baseview/src/gl/macos.rs
Normal file
155
plugins/baseview/src/gl/macos.rs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
115
plugins/baseview/src/gl/mod.rs
Normal file
115
plugins/baseview/src/gl/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
308
plugins/baseview/src/gl/win.rs
Normal file
308
plugins/baseview/src/gl/win.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
249
plugins/baseview/src/gl/x11.rs
Normal file
249
plugins/baseview/src/gl/x11.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
166
plugins/baseview/src/gl/x11/errors.rs
Normal file
166
plugins/baseview/src/gl/x11/errors.rs
Normal 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 {}
|
||||
55
plugins/baseview/src/keyboard.rs
Normal file
55
plugins/baseview/src/keyboard.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
24
plugins/baseview/src/lib.rs
Normal file
24
plugins/baseview/src/lib.rs
Normal 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::*;
|
||||
357
plugins/baseview/src/macos/keyboard.rs
Normal file
357
plugins/baseview/src/macos/keyboard.rs
Normal 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
|
||||
}
|
||||
21
plugins/baseview/src/macos/mod.rs
Normal file
21
plugins/baseview/src/macos/mod.rs
Normal 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::*;
|
||||
617
plugins/baseview/src/macos/view.rs
Normal file
617
plugins/baseview/src/macos/view.rs
Normal 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
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
484
plugins/baseview/src/macos/window.rs
Normal file
484
plugins/baseview/src/macos/window.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
44
plugins/baseview/src/mouse_cursor.rs
Normal file
44
plugins/baseview/src/mouse_cursor.rs
Normal 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,
|
||||
}
|
||||
54
plugins/baseview/src/win/cursor.rs
Normal file
54
plugins/baseview/src/win/cursor.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
288
plugins/baseview/src/win/drop_target.rs
Normal file
288
plugins/baseview/src/win/drop_target.rs
Normal 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
|
||||
}
|
||||
}
|
||||
142
plugins/baseview/src/win/hook.rs
Normal file
142
plugins/baseview/src/win/hook.rs
Normal 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
|
||||
}
|
||||
704
plugins/baseview/src/win/keyboard.rs
Normal file
704
plugins/baseview/src/win/keyboard.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
plugins/baseview/src/win/mod.rs
Normal file
7
plugins/baseview/src/win/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod cursor;
|
||||
mod drop_target;
|
||||
mod hook;
|
||||
mod keyboard;
|
||||
mod window;
|
||||
|
||||
pub use window::*;
|
||||
867
plugins/baseview/src/win/window.rs
Normal file
867
plugins/baseview/src/win/window.rs
Normal 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!()
|
||||
}
|
||||
136
plugins/baseview/src/window.rs
Normal file
136
plugins/baseview/src/window.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
144
plugins/baseview/src/window_info.rs
Normal file
144
plugins/baseview/src/window_info.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
29
plugins/baseview/src/window_open_options.rs
Normal file
29
plugins/baseview/src/window_open_options.rs
Normal 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>,
|
||||
}
|
||||
100
plugins/baseview/src/x11/cursor.rs
Normal file
100
plugins/baseview/src/x11/cursor.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
305
plugins/baseview/src/x11/event_loop.rs
Normal file
305
plugins/baseview/src/x11/event_loop.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
406
plugins/baseview/src/x11/keyboard.rs
Normal file
406
plugins/baseview/src/x11/keyboard.rs
Normal 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 }
|
||||
}
|
||||
10
plugins/baseview/src/x11/mod.rs
Normal file
10
plugins/baseview/src/x11/mod.rs
Normal 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;
|
||||
94
plugins/baseview/src/x11/visual_info.rs
Normal file
94
plugins/baseview/src/x11/visual_info.rs
Normal 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
|
||||
}
|
||||
380
plugins/baseview/src/x11/window.rs
Normal file
380
plugins/baseview/src/x11/window.rs
Normal 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!()
|
||||
}
|
||||
132
plugins/baseview/src/x11/xcb_connection.rs
Normal file
132
plugins/baseview/src/x11/xcb_connection.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user