From dec728cfa28547f17c650dd945d5d2b73086f879 Mon Sep 17 00:00:00 2001 From: Francesca Frangipane Date: Thu, 17 May 2018 21:28:30 -0400 Subject: [PATCH] macOS: Implement NSTextInputClient (#518) Fixes #263 --- CHANGELOG.md | 3 + src/lib.rs | 1 - src/os/unix.rs | 8 - src/platform/android/mod.rs | 5 + src/platform/emscripten/mod.rs | 5 + src/platform/ios/mod.rs | 5 + src/platform/linux/mod.rs | 8 + src/platform/macos/events_loop.rs | 160 ++++-------- src/platform/macos/ffi.rs | 74 ++++++ src/platform/macos/mod.rs | 3 + src/platform/macos/util.rs | 38 +++ src/platform/macos/view.rs | 411 ++++++++++++++++++++++++++++++ src/platform/macos/window.rs | 63 +++-- src/platform/windows/window.rs | 5 + src/window.rs | 6 + 15 files changed, 648 insertions(+), 147 deletions(-) create mode 100644 src/platform/macos/ffi.rs create mode 100644 src/platform/macos/util.rs create mode 100644 src/platform/macos/view.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ffee774a..3e39147b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ - On macOS, `Window::get_current_monitor` now returns accurate values. - Added `WindowBuilderExt::with_resize_increments` to macOS. - **Breaking:** On X11, `WindowBuilderExt::with_resize_increments` and `WindowBuilderExt::with_base_size` now take `u32` values rather than `i32`. +- macOS keyboard handling has been overhauled, allowing for the use of dead keys, IME, etc. Right modifier keys are also no longer reported as being left. +- Added the `Window::set_ime_spot(x: i32, y: i32)` method, which is implemented on X11 and macOS. +- **Breaking**: `os::unix::WindowExt::send_xim_spot(x: i16, y: i16)` no longer exists. Switch to the new `Window::set_ime_spot(x: i32, y: i32)`, which has equivalent functionality. # Version 0.14.0 (2018-05-09) diff --git a/src/lib.rs b/src/lib.rs index 70a38eb2..16b3fc7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,7 +80,6 @@ //! to create an OpenGL/Vulkan/DirectX/Metal/etc. context that will draw on the window. //! -#[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "windows"))] #[macro_use] extern crate lazy_static; extern crate libc; diff --git a/src/os/unix.rs b/src/os/unix.rs index 04bfa278..774c5ca8 100644 --- a/src/os/unix.rs +++ b/src/os/unix.rs @@ -94,8 +94,6 @@ pub trait WindowExt { fn get_xlib_xconnection(&self) -> Option>; - fn send_xim_spot(&self, x: i16, y: i16); - /// This function returns the underlying `xcb_connection_t` of an xlib `Display`. /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). @@ -165,12 +163,6 @@ impl WindowExt for Window { } } - fn send_xim_spot(&self, x: i16, y: i16) { - if let LinuxWindow::X(ref w) = self.window { - w.send_xim_spot(x, y); - } - } - #[inline] fn get_wayland_surface(&self) -> Option<*mut raw::c_void> { match self.window { diff --git a/src/platform/android/mod.rs b/src/platform/android/mod.rs index 8055b950..2ff43420 100644 --- a/src/platform/android/mod.rs +++ b/src/platform/android/mod.rs @@ -328,6 +328,11 @@ impl Window { // N/A } + #[inline] + pub fn set_ime_spot(&self, _x: i32, _y: i32) { + // N/A + } + #[inline] pub fn get_current_monitor(&self) -> RootMonitorId { RootMonitorId{inner: MonitorId} diff --git a/src/platform/emscripten/mod.rs b/src/platform/emscripten/mod.rs index a907dbe7..61db9f26 100644 --- a/src/platform/emscripten/mod.rs +++ b/src/platform/emscripten/mod.rs @@ -548,6 +548,11 @@ impl Window { // N/A } + #[inline] + pub fn set_ime_spot(&self, _x: i32, _y: i32) { + // N/A + } + #[inline] pub fn get_current_monitor(&self) -> ::MonitorId { ::MonitorId{inner: MonitorId} diff --git a/src/platform/ios/mod.rs b/src/platform/ios/mod.rs index 58206831..5c82bf38 100644 --- a/src/platform/ios/mod.rs +++ b/src/platform/ios/mod.rs @@ -375,6 +375,11 @@ impl Window { // N/A } + #[inline] + pub fn set_ime_spot(&self, _x: i32, _y: i32) { + // N/A + } + #[inline] pub fn get_current_monitor(&self) -> RootMonitorId { RootMonitorId{inner: MonitorId} diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index fb72894f..46ff806e 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -310,6 +310,14 @@ impl Window { } } + #[inline] + pub fn set_ime_spot(&self, x: i32, y: i32) { + match self { + &Window::X(ref w) => w.send_xim_spot(x as i16, y as i16), + &Window::Wayland(_) => (), + } + } + #[inline] pub fn get_current_monitor(&self) -> RootMonitorId { match self { diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index 01ffb564..22c349dd 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -6,6 +6,7 @@ use std::collections::VecDeque; use std::sync::{Arc, Mutex, Weak}; use super::window::Window2; use std; +use std::os::raw::*; use super::DeviceId; @@ -315,127 +316,50 @@ impl EventsLoop { }); match event_type { - - appkit::NSKeyDown => { - let mut events = std::collections::VecDeque::new(); - let received_c_str = foundation::NSString::UTF8String(ns_event.characters()); - let received_str = std::ffi::CStr::from_ptr(received_c_str); - - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - let state = ElementState::Pressed; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, - }; - for received_char in std::str::from_utf8(received_str.to_bytes()).unwrap().chars() { - let window_event = WindowEvent::ReceivedCharacter(received_char); - events.push_back(into_event(window_event)); - } - self.shared.pending_events.lock().unwrap().extend(events.into_iter()); - Some(into_event(window_event)) - }, - - appkit::NSKeyUp => { - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - - let state = ElementState::Released; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, - }; - Some(into_event(window_event)) - }, - appkit::NSFlagsChanged => { - unsafe fn modifier_event(event: cocoa::base::id, - keymask: NSEventModifierFlags, - key: events::VirtualKeyCode, - key_pressed: bool) -> Option - { - if !key_pressed && NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Pressed; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - - } else if key_pressed && !NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Released; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - - } else { - None - } - } - let mut events = std::collections::VecDeque::new(); - if let Some(window_event) = modifier_event(ns_event, - NSEventModifierFlags::NSShiftKeyMask, - events::VirtualKeyCode::LShift, - self.modifiers.shift_pressed) - { + + if let Some(window_event) = modifier_event( + ns_event, + NSEventModifierFlags::NSShiftKeyMask, + self.modifiers.shift_pressed, + ) { self.modifiers.shift_pressed = !self.modifiers.shift_pressed; events.push_back(into_event(window_event)); } - if let Some(window_event) = modifier_event(ns_event, - NSEventModifierFlags::NSControlKeyMask, - events::VirtualKeyCode::LControl, - self.modifiers.ctrl_pressed) - { + if let Some(window_event) = modifier_event( + ns_event, + NSEventModifierFlags::NSControlKeyMask, + self.modifiers.ctrl_pressed, + ) { self.modifiers.ctrl_pressed = !self.modifiers.ctrl_pressed; events.push_back(into_event(window_event)); } - if let Some(window_event) = modifier_event(ns_event, - NSEventModifierFlags::NSCommandKeyMask, - events::VirtualKeyCode::LWin, - self.modifiers.win_pressed) - { + if let Some(window_event) = modifier_event( + ns_event, + NSEventModifierFlags::NSCommandKeyMask, + self.modifiers.win_pressed, + ) { self.modifiers.win_pressed = !self.modifiers.win_pressed; events.push_back(into_event(window_event)); } - if let Some(window_event) = modifier_event(ns_event, - NSEventModifierFlags::NSAlternateKeyMask, - events::VirtualKeyCode::LAlt, - self.modifiers.alt_pressed) - { + if let Some(window_event) = modifier_event( + ns_event, + NSEventModifierFlags::NSAlternateKeyMask, + self.modifiers.alt_pressed, + ) { self.modifiers.alt_pressed = !self.modifiers.alt_pressed; events.push_back(into_event(window_event)); } let event = events.pop_front(); - self.shared.pending_events.lock().unwrap().extend(events.into_iter()); + self.shared.pending_events + .lock() + .unwrap() + .extend(events.into_iter()); event }, @@ -618,8 +542,7 @@ impl Proxy { } } - -fn to_virtual_key_code(code: u16) -> Option { +pub fn to_virtual_key_code(code: c_ushort) -> Option { Some(match code { 0x00 => events::VirtualKeyCode::A, 0x01 => events::VirtualKeyCode::S, @@ -755,7 +678,7 @@ fn to_virtual_key_code(code: u16) -> Option { }) } -fn event_mods(event: cocoa::base::id) -> ModifiersState { +pub fn event_mods(event: cocoa::base::id) -> ModifiersState { let flags = unsafe { NSEvent::modifierFlags(event) }; @@ -767,5 +690,30 @@ fn event_mods(event: cocoa::base::id) -> ModifiersState { } } +unsafe fn modifier_event( + ns_event: cocoa::base::id, + keymask: NSEventModifierFlags, + key_pressed: bool, +) -> Option { + if !key_pressed && NSEvent::modifierFlags(ns_event).contains(keymask) + || key_pressed && !NSEvent::modifierFlags(ns_event).contains(keymask) { + let state = ElementState::Released; + let keycode = NSEvent::keyCode(ns_event); + let scancode = keycode as u32; + let virtual_keycode = to_virtual_key_code(keycode); + Some(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state, + scancode, + virtual_keycode, + modifiers: event_mods(ns_event), + }, + }) + } else { + None + } +} + // Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); +pub const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); diff --git a/src/platform/macos/ffi.rs b/src/platform/macos/ffi.rs new file mode 100644 index 00000000..988d1fd3 --- /dev/null +++ b/src/platform/macos/ffi.rs @@ -0,0 +1,74 @@ +// TODO: Upstream these + +#![allow(non_snake_case, non_upper_case_globals)] + +use cocoa::base::{class, id}; +use cocoa::foundation::{NSInteger, NSUInteger}; +use objc; + +pub const NSNotFound: NSInteger = NSInteger::max_value(); + +#[repr(C)] +pub struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + #[inline] + pub fn new(location: NSUInteger, length: NSUInteger) -> NSRange { + NSRange { location, length } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + // TODO: Verify that this is correct + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str(), + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + +pub trait NSMutableAttributedString: Sized { + unsafe fn alloc(_: Self) -> id { + msg_send![class("NSMutableAttributedString"), alloc] + } + + unsafe fn init(self) -> id; // *mut NSMutableAttributedString + unsafe fn initWithString(self, string: id) -> id; + unsafe fn initWithAttributedString(self, string: id) -> id; + + unsafe fn string(self) -> id; // *mut NSString + unsafe fn mutableString(self) -> id; // *mut NSMutableString + unsafe fn length(self) -> NSUInteger; +} + +impl NSMutableAttributedString for id { + unsafe fn init(self) -> id { + msg_send![self, init] + } + + unsafe fn initWithString(self, string: id) -> id { + msg_send![self, initWithString:string] + } + + unsafe fn initWithAttributedString(self, string: id) -> id { + msg_send![self, initWithAttributedString:string] + } + + unsafe fn string(self) -> id { + msg_send![self, string] + } + + unsafe fn mutableString(self) -> id { + msg_send![self, mutableString] + } + + unsafe fn length(self) -> NSUInteger { + msg_send![self, length] + } +} diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index f957b3ac..9639482a 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -38,5 +38,8 @@ impl Window { } mod events_loop; +mod ffi; mod monitor; +mod util; +mod view; mod window; diff --git a/src/platform/macos/util.rs b/src/platform/macos/util.rs new file mode 100644 index 00000000..29836271 --- /dev/null +++ b/src/platform/macos/util.rs @@ -0,0 +1,38 @@ +use cocoa::appkit::NSWindowStyleMask; +use cocoa::base::{class, id, nil}; +use cocoa::foundation::{NSRect, NSUInteger}; +use core_graphics::display::CGDisplay; + +use platform::platform::ffi; +use platform::platform::window::IdRef; + +pub const EMPTY_RANGE: ffi::NSRange = ffi::NSRange { + location: ffi::NSNotFound as NSUInteger, + length: 0, +}; + +// For consistency with other platforms, this will... +// 1. translate the bottom-left window corner into the top-left window corner +// 2. translate the coordinate from a bottom-left origin coordinate system to a top-left one +pub fn bottom_left_to_top_left(rect: NSRect) -> i32 { + (CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height)) as _ +} + +pub unsafe fn set_style_mask(window: id, view: id, mask: NSWindowStyleMask) { + use cocoa::appkit::NSWindow; + window.setStyleMask_(mask); + // If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly! + window.makeFirstResponder_(view); +} + +pub unsafe fn create_input_context(view: id) -> IdRef { + let input_context: id = msg_send![class("NSTextInputContext"), alloc]; + let input_context: id = msg_send![input_context, initWithClient:view]; + IdRef::new(input_context) +} + +#[allow(dead_code)] +pub unsafe fn open_emoji_picker() { + let app: id = msg_send![class("NSApplication"), sharedApplication]; + let _: () = msg_send![app, orderFrontCharacterPalette:nil]; +} diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs new file mode 100644 index 00000000..d6677a1d --- /dev/null +++ b/src/platform/macos/view.rs @@ -0,0 +1,411 @@ +// This is a pretty close port of the implementation in GLFW: +// https://github.com/glfw/glfw/blob/7ef34eb06de54dd9186d3d21a401b2ef819b59e7/src/cocoa_window.m + +use std::{slice, str}; +use std::boxed::Box; +use std::collections::VecDeque; +use std::os::raw::*; +use std::sync::Weak; + +use cocoa::base::{class, id, nil}; +use cocoa::appkit::NSWindow; +use cocoa::foundation::{NSPoint, NSRect, NSSize, NSString, NSUInteger}; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Protocol, Sel, BOOL}; + +use {ElementState, Event, KeyboardInput, WindowEvent, WindowId}; +use platform::platform::events_loop::{DEVICE_ID, event_mods, Shared, to_virtual_key_code}; +use platform::platform::util; +use platform::platform::ffi::*; +use platform::platform::window::{get_window_id, IdRef}; + +struct ViewState { + window: id, + shared: Weak, + ime_spot: Option<(i32, i32)>, + raw_characters: Option, +} + +pub fn new_view(window: id, shared: Weak) -> IdRef { + let state = ViewState { window, shared, ime_spot: None, raw_characters: None }; + unsafe { + // This is free'd in `dealloc` + let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; + let view: id = msg_send![VIEW_CLASS.0, alloc]; + IdRef::new(msg_send![view, initWithWinit:state_ptr]) + } +} + +pub fn set_ime_spot(view: id, input_context: id, x: i32, y: i32) { + unsafe { + let state_ptr: *mut c_void = *(*view).get_mut_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let content_rect = NSWindow::contentRectForFrameRect_( + state.window, + NSWindow::frame(state.window), + ); + let base_x = content_rect.origin.x as i32; + let base_y = (content_rect.origin.y + content_rect.size.height) as i32; + state.ime_spot = Some((base_x + x, base_y - y)); + let _: () = msg_send![input_context, invalidateCharacterCoordinates]; + } +} + +struct ViewClass(*const Class); +unsafe impl Send for ViewClass {} +unsafe impl Sync for ViewClass {} + +lazy_static! { + static ref VIEW_CLASS: ViewClass = unsafe { + let superclass = Class::get("NSView").unwrap(); + let mut decl = ClassDecl::new("WinitView", superclass).unwrap(); + decl.add_method(sel!(dealloc), dealloc as extern fn(&Object, Sel)); + decl.add_method( + sel!(initWithWinit:), + init_with_winit as extern fn(&Object, Sel, *mut c_void) -> id, + ); + decl.add_method(sel!(hasMarkedText), has_marked_text as extern fn(&Object, Sel) -> BOOL); + decl.add_method( + sel!(markedRange), + marked_range as extern fn(&Object, Sel) -> NSRange, + ); + decl.add_method(sel!(selectedRange), selected_range as extern fn(&Object, Sel) -> NSRange); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern fn(&mut Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern fn(&Object, Sel)); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(characterIndexForPoint:), + character_index_for_point as extern fn(&Object, Sel, NSPoint) -> NSUInteger, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range + as extern fn(&Object, Sel, NSRange, *mut c_void) -> NSRect, + ); + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern fn(&Object, Sel, Sel), + ); + decl.add_method(sel!(keyDown:), key_down as extern fn(&Object, Sel, id)); + decl.add_method(sel!(keyUp:), key_up as extern fn(&Object, Sel, id)); + decl.add_method(sel!(insertTab:), insert_tab as extern fn(&Object, Sel, id)); + decl.add_method(sel!(insertBackTab:), insert_back_tab as extern fn(&Object, Sel, id)); + decl.add_ivar::<*mut c_void>("winitState"); + decl.add_ivar::("markedText"); + let protocol = Protocol::get("NSTextInputClient").unwrap(); + decl.add_protocol(&protocol); + ViewClass(decl.register()) + }; +} + +extern fn dealloc(this: &Object, _sel: Sel) { + unsafe { + let state: *mut c_void = *this.get_ivar("winitState"); + let marked_text: id = *this.get_ivar("markedText"); + let _: () = msg_send![marked_text, release]; + Box::from_raw(state as *mut ViewState); + } +} + +extern fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id { + unsafe { + let this: id = msg_send![this, init]; + if this != nil { + (*this).set_ivar("winitState", state); + let marked_text = ::init( + NSMutableAttributedString::alloc(nil), + ); + (*this).set_ivar("markedText", marked_text); + } + this + } +} + +extern fn has_marked_text(this: &Object, _sel: Sel) -> BOOL { + unsafe { + let marked_text: id = *this.get_ivar("markedText"); + (marked_text.length() > 0) as i8 + } +} + +extern fn marked_range(this: &Object, _sel: Sel) -> NSRange { + unsafe { + let marked_text: id = *this.get_ivar("markedText"); + let length = marked_text.length(); + if length > 0 { + NSRange::new(0, length - 1) + } else { + util::EMPTY_RANGE + } + } +} + +extern fn selected_range(_this: &Object, _sel: Sel) -> NSRange { + util::EMPTY_RANGE +} + +extern fn set_marked_text( + this: &mut Object, + _sel: Sel, + string: id, + _selected_range: NSRange, + _replacement_range: NSRange, +) { + unsafe { + let marked_text_ref: &mut id = this.get_mut_ivar("markedText"); + let _: () = msg_send![(*marked_text_ref), release]; + let marked_text = NSMutableAttributedString::alloc(nil); + let has_attr = msg_send![string, isKindOfClass:class("NSAttributedString")]; + if has_attr { + marked_text.initWithAttributedString(string); + } else { + marked_text.initWithString(string); + }; + *marked_text_ref = marked_text; + } +} + +extern fn unmark_text(this: &Object, _sel: Sel) { + unsafe { + let marked_text: id = *this.get_ivar("markedText"); + let mutable_string = marked_text.mutableString(); + let _: () = msg_send![mutable_string, setString:""]; + let input_context: id = msg_send![this, inputContext]; + let _: () = msg_send![input_context, discardMarkedText]; + } +} + +extern fn valid_attributes_for_marked_text(_this: &Object, _sel: Sel) -> id { + unsafe { msg_send![class("NSArray"), array] } +} + +extern fn attributed_substring_for_proposed_range( + _this: &Object, + _sel: Sel, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> id { + nil +} + +extern fn character_index_for_point(_this: &Object, _sel: Sel, _point: NSPoint) -> NSUInteger { + 0 +} + +extern fn first_rect_for_character_range( + this: &Object, + _sel: Sel, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> NSRect { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let (x, y) = state.ime_spot.unwrap_or_else(|| { + let content_rect = NSWindow::contentRectForFrameRect_( + state.window, + NSWindow::frame(state.window), + ); + let x = content_rect.origin.x; + let y = util::bottom_left_to_top_left(content_rect); + (x as i32, y as i32) + }); + + NSRect::new( + NSPoint::new(x as _, y as _), + NSSize::new(0.0, 0.0), + ) + } +} + +extern fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_range: NSRange) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let has_attr = msg_send![string, isKindOfClass:class("NSAttributedString")]; + let characters = if has_attr { + // This is a *mut NSAttributedString + msg_send![string, string] + } else { + // This is already a *mut NSString + string + }; + + let slice = slice::from_raw_parts( + characters.UTF8String() as *const c_uchar, + characters.len(), + ); + let string = str::from_utf8_unchecked(slice); + + // We don't need this now, but it's here if that changes. + //let event: id = msg_send![class("NSApp"), currentEvent]; + + let mut events = VecDeque::with_capacity(characters.len()); + for character in string.chars() { + events.push_back(Event::WindowEvent { + window_id: WindowId(get_window_id(state.window)), + event: WindowEvent::ReceivedCharacter(character), + }); + } + + if let Some(shared) = state.shared.upgrade() { + shared.pending_events + .lock() + .unwrap() + .append(&mut events); + } + } +} + +extern fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character + // happens, i.e. newlines, tabs, and Ctrl+C. + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let shared = if let Some(shared) = state.shared.upgrade() { + shared + } else { + return; + }; + + let mut events = VecDeque::with_capacity(1); + if command == sel!(insertNewline:) { + // The `else` condition would emit the same character, but I'm keeping this here both... + // 1) as a reminder for how `doCommandBySelector` works + // 2) to make the newline character explicit (...not that it matters) + events.push_back(Event::WindowEvent { + window_id: WindowId(get_window_id(state.window)), + event: WindowEvent::ReceivedCharacter('\n'), + }); + } else { + let raw_characters = state.raw_characters.take(); + if let Some(raw_characters) = raw_characters { + for character in raw_characters.chars() { + events.push_back(Event::WindowEvent { + window_id: WindowId(get_window_id(state.window)), + event: WindowEvent::ReceivedCharacter(character), + }); + } + } + }; + + shared.pending_events + .lock() + .unwrap() + .append(&mut events); + } +} + +extern fn key_down(this: &Object, _sel: Sel, event: id) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let keycode: c_ushort = msg_send![event, keyCode]; + let virtual_keycode = to_virtual_key_code(keycode); + let scancode = keycode as u32; + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Pressed, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + }, + }; + + state.raw_characters = { + let characters: id = msg_send![event, characters]; + let slice = slice::from_raw_parts( + characters.UTF8String() as *const c_uchar, + characters.len(), + ); + let string = str::from_utf8_unchecked(slice); + Some(string.to_owned()) + }; + + if let Some(shared) = state.shared.upgrade() { + shared.pending_events + .lock() + .unwrap() + .push_back(window_event); + } + + let array: id = msg_send![class("NSArray"), arrayWithObject:event]; + let (): _ = msg_send![this, interpretKeyEvents:array]; + } +} + +extern fn key_up(this: &Object, _sel: Sel, event: id) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let keycode: c_ushort = msg_send![event, keyCode]; + let virtual_keycode = to_virtual_key_code(keycode); + let scancode = keycode as u32; + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Released, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + }, + }; + + if let Some(shared) = state.shared.upgrade() { + shared.pending_events + .lock() + .unwrap() + .push_back(window_event); + } + } +} + +extern fn insert_tab(this: &Object, _sel: Sel, _sender: id) { + unsafe { + let window: id = msg_send![this, window]; + let first_responder: id = msg_send![window, firstResponder]; + let this_ptr = this as *const _ as *mut _; + if first_responder == this_ptr { + let (): _ = msg_send![window, selectNextKeyView:this]; + } + } +} + +extern fn insert_back_tab(this: &Object, _sel: Sel, _sender: id) { + unsafe { + let window: id = msg_send![this, window]; + let first_responder: id = msg_send![window, firstResponder]; + let this_ptr = this as *const _ as *mut _; + if first_responder == this_ptr { + let (): _ = msg_send![window, selectPreviousKeyView:this]; + } + } +} diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index f9c80266..e0c134d1 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -25,6 +25,8 @@ use std::sync::Weak; use std::cell::{Cell, RefCell}; use super::events_loop::{EventsLoop, Shared}; +use platform::platform::util; +use platform::platform::view::{new_view, set_ime_spot}; use window::MonitorId as RootMonitorId; @@ -56,15 +58,14 @@ impl DelegateState { let curr_mask = self.window.styleMask(); if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { - self.window - .setStyleMask_(NSWindowStyleMask::NSResizableWindowMask); + util::set_style_mask(*self.window, *self.view, NSWindowStyleMask::NSResizableWindowMask); } let is_zoomed: BOOL = msg_send![*self.window, isZoomed]; // Roll back temp styles if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { - self.window.setStyleMask_(curr_mask); + util::set_style_mask(*self.window, *self.view, curr_mask); } is_zoomed != 0 @@ -79,7 +80,7 @@ impl DelegateState { let save_style_opt = self.save_style_mask.take(); if let Some(save_style) = save_style_opt { - self.window.setStyleMask_(save_style); + util::set_style_mask(*self.window, *self.view, save_style); } win_attribs.maximized @@ -167,7 +168,7 @@ impl WindowDelegate { unsafe fn emit_move_event(state: &mut DelegateState) { let frame_rect = NSWindow::frame(*state.window); let x = frame_rect.origin.x as _; - let y = Window2::bottom_left_to_top_left(frame_rect); + let y = util::bottom_left_to_top_left(frame_rect); let moved = state.previous_position != Some((x, y)); if moved { state.previous_position = Some((x, y)); @@ -495,6 +496,7 @@ pub struct Window2 { pub view: IdRef, pub window: IdRef, pub delegate: WindowDelegate, + pub input_context: IdRef, } unsafe impl Send for Window2 {} @@ -582,7 +584,7 @@ impl Window2 { return Err(OsError(format!("Couldn't create NSWindow"))); }, }; - let view = match Window2::create_view(*window) { + let view = match Window2::create_view(*window, Weak::clone(&shared)) { Some(view) => view, None => { let _: () = unsafe { msg_send![autoreleasepool, drain] }; @@ -590,6 +592,8 @@ impl Window2 { }, }; + let input_context = unsafe { util::create_input_context(*view) }; + unsafe { if win_attribs.transparent { (*window as id).setOpaque_(NO); @@ -628,6 +632,7 @@ impl Window2 { view: view, window: window, delegate: WindowDelegate::new(ds), + input_context, }; // Set fullscreen mode after we setup everything @@ -681,13 +686,10 @@ impl Window2 { static INIT: std::sync::Once = std::sync::ONCE_INIT; INIT.call_once(|| unsafe { - extern fn on_key_down(_this: &Object, _sel: Sel, _id: id) {} - let window_superclass = Class::get("NSWindow").unwrap(); let mut decl = ClassDecl::new("WinitWindow", window_superclass).unwrap(); decl.add_method(sel!(canBecomeMainWindow), yes as extern fn(&Object, Sel) -> BOOL); decl.add_method(sel!(canBecomeKeyWindow), yes as extern fn(&Object, Sel) -> BOOL); - decl.add_method(sel!(keyDown:), on_key_down as extern fn(&Object, Sel, id)); WINDOW2_CLASS = decl.register(); }); @@ -788,12 +790,13 @@ impl Window2 { } } - fn create_view(window: id) -> Option { + fn create_view(window: id, shared: Weak) -> Option { unsafe { - let view = IdRef::new(NSView::init(NSView::alloc(nil))); + let view = new_view(window, shared); view.non_nil().map(|view| { view.setWantsBestResolutionOpenGLSurface_(YES); window.setContentView_(*view); + window.makeFirstResponder_(*view); view }) } @@ -816,18 +819,11 @@ impl Window2 { unsafe { NSWindow::orderOut_(*self.window, nil); } } - // For consistency with other platforms, this will... - // 1. translate the bottom-left window corner into the top-left window corner - // 2. translate the coordinate from a bottom-left origin coordinate system to a top-left one - fn bottom_left_to_top_left(rect: NSRect) -> i32 { - (CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height)) as _ - } - pub fn get_position(&self) -> Option<(i32, i32)> { let frame_rect = unsafe { NSWindow::frame(*self.window) }; Some(( frame_rect.origin.x as i32, - Self::bottom_left_to_top_left(frame_rect), + util::bottom_left_to_top_left(frame_rect), )) } @@ -840,7 +836,7 @@ impl Window2 { }; Some(( content_rect.origin.x as i32, - Self::bottom_left_to_top_left(content_rect), + util::bottom_left_to_top_left(content_rect), )) } @@ -1029,10 +1025,9 @@ impl Window2 { let curr_mask = state.window.styleMask(); if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { - state.window.setStyleMask_( - NSWindowStyleMask::NSTitledWindowMask - | NSWindowStyleMask::NSResizableWindowMask, - ); + let mask = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSResizableWindowMask; + util::set_style_mask(*self.window, *self.view, mask); state.save_style_mask.set(Some(curr_mask)); } } @@ -1067,21 +1062,26 @@ impl Window2 { } else { NSWindowStyleMask::NSBorderlessWindowMask }; - - state.window.setStyleMask_(new_mask); + util::set_style_mask(*state.window, *state.view, new_mask); } } #[inline] pub fn set_window_icon(&self, _icon: Option<::Icon>) { // macOS doesn't have window icons. Though, there is `setRepresentedFilename`, but that's - // semantically distinct and should only be used when the window is in some representing a - // specific file/directory. For instance, Terminal.app uses this for the CWD. Anyway, that - // should eventually be implemented as `WindowBuilderExt::with_represented_file` or - // something, and doesn't have anything to do with this. + // semantically distinct and should only be used when the window is in some way + // representing a specific file/directory. For instance, Terminal.app uses this for the + // CWD. Anyway, that should eventually be implemented as + // `WindowBuilderExt::with_represented_file` or something, and doesn't have anything to do + // with `set_window_icon`. // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html } + #[inline] + pub fn set_ime_spot(&self, x: i32, y: i32) { + set_ime_spot(*self.view, *self.input_context, x, y); + } + #[inline] pub fn get_current_monitor(&self) -> RootMonitorId { unsafe { @@ -1169,8 +1169,7 @@ impl Drop for IdRef { fn drop(&mut self) { if self.0 != nil { unsafe { - let autoreleasepool = - NSAutoreleasePool::new(nil); + let autoreleasepool = NSAutoreleasePool::new(nil); let _ : () = msg_send![self.0, release]; let _ : () = msg_send![autoreleasepool, release]; }; diff --git a/src/platform/windows/window.rs b/src/platform/windows/window.rs index fbf6ab25..29b912a0 100644 --- a/src/platform/windows/window.rs +++ b/src/platform/windows/window.rs @@ -643,6 +643,11 @@ impl Window { } self.taskbar_icon.replace(taskbar_icon); } + + #[inline] + pub fn set_ime_spot(&self, _x: i32, _y: i32) { + unimplemented!(); + } } impl Drop for Window { diff --git a/src/window.rs b/src/window.rs index 9a3b24b1..7491b478 100644 --- a/src/window.rs +++ b/src/window.rs @@ -376,6 +376,12 @@ impl Window { self.window.set_window_icon(window_icon) } + //// Sets location of IME candidate box in client area coordinates relative to the top left. + #[inline] + pub fn set_ime_spot(&self, x: i32, y: i32) { + self.window.set_ime_spot(x, y) + } + /// Returns the monitor on which the window currently resides pub fn get_current_monitor(&self) -> MonitorId { self.window.get_current_monitor()