Start rebuilding with a cleanly-separated UI framework
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
KeyDown {
|
||||
keystroke: Keystroke,
|
||||
chars: String,
|
||||
},
|
||||
ScrollWheel {
|
||||
position: Vector2F,
|
||||
delta: Vector2F,
|
||||
precise: bool,
|
||||
},
|
||||
LeftMouseDown {
|
||||
position: Vector2F,
|
||||
cmd: bool,
|
||||
},
|
||||
LeftMouseUp {
|
||||
position: Vector2F,
|
||||
},
|
||||
LeftMouseDragged {
|
||||
position: Vector2F,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
use super::Event;
|
||||
pub use cocoa::foundation::NSSize;
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSString},
|
||||
};
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{Class, Object, Sel},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
os::raw::{c_char, c_void},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
finish_launching_callback: Option<Box<dyn FnOnce()>>,
|
||||
become_active_callback: Option<Box<dyn FnMut()>>,
|
||||
resign_active_callback: Option<Box<dyn FnMut()>>,
|
||||
event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
|
||||
open_files_callback: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
|
||||
}
|
||||
|
||||
const RUST_WRAPPER_IVAR_NAME: &'static str = "rustWrapper";
|
||||
|
||||
impl super::App for App {
|
||||
fn on_finish_launching<F: 'static + FnOnce()>(mut self, callback: F) -> Self {
|
||||
self.finish_launching_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_become_active<F: 'static + FnMut()>(mut self, callback: F) -> Self {
|
||||
self.become_active_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_resign_active<F: 'static + FnMut()>(mut self, callback: F) -> Self {
|
||||
self.resign_active_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_event<F: 'static + FnMut(Event) -> bool>(mut self, callback: F) -> Self {
|
||||
self.event_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_open_files<F: 'static + FnMut(Vec<PathBuf>)>(mut self, callback: F) -> Self {
|
||||
self.open_files_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
fn run(self) {
|
||||
unsafe {
|
||||
let self_ptr = Box::into_raw(Box::new(self));
|
||||
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
let app: id = msg_send![build_app_class(), sharedApplication];
|
||||
(*app).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
let app_delegate: id = msg_send![build_app_delegate_class(), new];
|
||||
(*app_delegate).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
let _: () = msg_send![app, setDelegate: app_delegate];
|
||||
let _: () = msg_send![app, run];
|
||||
let _: () = msg_send![pool, drain];
|
||||
|
||||
// App is done running when we get here, so we can reinstantiate the Box and drop it.
|
||||
Box::from_raw(self_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_app_class() -> *const Class {
|
||||
unsafe {
|
||||
let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(RUST_WRAPPER_IVAR_NAME);
|
||||
decl.add_method(
|
||||
sel!(sendEvent:),
|
||||
send_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_app_delegate_class() -> *const Class {
|
||||
unsafe {
|
||||
let superclass = class!(NSResponder);
|
||||
let mut decl = ClassDecl::new("GPUIApplicationDelegate", superclass).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(RUST_WRAPPER_IVAR_NAME);
|
||||
decl.add_method(
|
||||
sel!(applicationDidFinishLaunching:),
|
||||
did_finish_launching as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationDidBecomeActive:),
|
||||
did_become_active as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationDidResignActive:),
|
||||
did_resign_active as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(application:openFiles:),
|
||||
open_files as extern "C" fn(&Object, Sel, id, id),
|
||||
);
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_app(object: &Object) -> &mut App {
|
||||
let wrapper_ptr: *mut c_void = *object.get_ivar(RUST_WRAPPER_IVAR_NAME);
|
||||
&mut *(wrapper_ptr as *mut App)
|
||||
}
|
||||
|
||||
extern "C" fn send_event(this: &Object, _sel: Sel, native_event: id) {
|
||||
let event = unsafe { Event::from_native(native_event, None) };
|
||||
|
||||
if let Some(event) = event {
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Some(callback) = app.event_callback.as_mut() {
|
||||
if callback(event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![super(this, class!(NSApplication)), sendEvent: native_event];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Some(callback) = app.finish_launching_callback.take() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_become_active(this: &Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Some(callback) = app.become_active_callback.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_resign_active(this: &Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Some(callback) = app.resign_active_callback.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn open_files(this: &Object, _: Sel, _: id, paths: id) {
|
||||
let paths = unsafe {
|
||||
(0..paths.count())
|
||||
.into_iter()
|
||||
.filter_map(|i| {
|
||||
let path = paths.objectAtIndex(i);
|
||||
match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
|
||||
Ok(string) => Some(PathBuf::from(string)),
|
||||
Err(err) => {
|
||||
log::error!("error converting path to string: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Some(callback) = app.open_files_callback.as_mut() {
|
||||
callback(paths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::Event;
|
||||
use crate::{geometry::vector::vec2f, keymap::Keystroke};
|
||||
use cocoa::appkit::{
|
||||
NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
|
||||
NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
|
||||
NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
|
||||
NSUpArrowFunctionKey as ARROW_UP_KEY,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent as _, NSEventModifierFlags, NSEventType},
|
||||
base::{id, YES},
|
||||
foundation::NSString as _,
|
||||
};
|
||||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const ESCAPE_KEY: u16 = 0x1b;
|
||||
|
||||
impl Event {
|
||||
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
|
||||
let event_type = native_event.eventType();
|
||||
|
||||
// Filter out event types that aren't in the NSEventType enum.
|
||||
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
|
||||
match event_type as u64 {
|
||||
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match event_type {
|
||||
NSEventType::NSKeyDown => {
|
||||
let modifiers = native_event.modifierFlags();
|
||||
let unmodified_chars = native_event.charactersIgnoringModifiers();
|
||||
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
|
||||
match first_char as u16 {
|
||||
ARROW_UP_KEY => "up",
|
||||
ARROW_DOWN_KEY => "down",
|
||||
ARROW_LEFT_KEY => "left",
|
||||
ARROW_RIGHT_KEY => "right",
|
||||
PAGE_UP_KEY => "pageup",
|
||||
PAGE_DOWN_KEY => "pagedown",
|
||||
BACKSPACE_KEY => "backspace",
|
||||
ENTER_KEY => "enter",
|
||||
DELETE_KEY => "delete",
|
||||
ESCAPE_KEY => "escape",
|
||||
_ => unmodified_chars,
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let chars = native_event.characters();
|
||||
let chars = CStr::from_ptr(chars.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
Some(Self::KeyDown {
|
||||
keystroke: Keystroke {
|
||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
key: unmodified_chars.into(),
|
||||
},
|
||||
chars,
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseDown => {
|
||||
window_height.map(|window_height| Self::LeftMouseDown {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
}),
|
||||
NSEventType::NSLeftMouseDragged => {
|
||||
window_height.map(|window_height| Self::LeftMouseDragged {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
})
|
||||
}
|
||||
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
mod app;
|
||||
mod event;
|
||||
|
||||
pub fn app() -> impl App {
|
||||
app::App::default()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
mod event;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
pub mod current {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use super::mac::*;
|
||||
}
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use event::Event;
|
||||
|
||||
pub trait App {
|
||||
fn on_finish_launching<F: 'static + FnOnce()>(self, callback: F) -> Self where;
|
||||
fn on_become_active<F: 'static + FnMut()>(self, callback: F) -> Self;
|
||||
fn on_resign_active<F: 'static + FnMut()>(self, callback: F) -> Self;
|
||||
fn on_event<F: 'static + FnMut(Event) -> bool>(self, callback: F) -> Self;
|
||||
fn on_open_files<F: 'static + FnMut(Vec<PathBuf>)>(self, callback: F) -> Self;
|
||||
fn run(self);
|
||||
}
|
||||
Reference in New Issue
Block a user