diff --git a/examples/hello.rs b/examples/hello.rs index 7cbe051..90b4a36 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -41,7 +41,7 @@ fn main() { // Log spam warning: it's commented for a reason // MouseEvent::CursorMove(x, y) => println!("Cursor moved to ({x}, {y})"), _ => {} - } + }, EventKind::Keyboard(event) => match event { KeyboardEvent::KeyPress(keycode) => println!("[{win:?}] Key {keycode:?} pressed"), KeyboardEvent::KeyRelease(keycode) => { @@ -49,7 +49,7 @@ fn main() { } KeyboardEvent::KeyRepeat(keycode) => println!("[{win:?}] Key {keycode:?} repeated"), KeyboardEvent::ImeCommit(commit) => println!("[{win:?}] IME commit -> {commit:?}"), - } + }, EventKind::Resized(width, height) => { println!("[{win:?}] Window resized to ({width}, {height})") } @@ -60,6 +60,11 @@ fn main() { lok::close_window(win); println!("[{win:?}] Closed upon request"); } + EventKind::Destroyed => { + println!("[{win:?}] Destroyed") + } + EventKind::FocusIn => println!("[{win:?}] Window focused"), + EventKind::FocusOut => println!("[{win:?}] Window lost focus"), _ => {} } } diff --git a/src/native/macos/ffi_rust.rs b/src/native/macos/ffi_rust.rs index 8ac091b..77de731 100644 --- a/src/native/macos/ffi_rust.rs +++ b/src/native/macos/ffi_rust.rs @@ -1,45 +1 @@ //! FFI functions for Swift to use - -use { - super::{ - ffi_swift::{SwiftMouseButton, SwiftMouseEvent}, - EVENT_QUEUE, - }, - crate::{ - event::{Event, EventKind}, - window::WindowHandle, - }, - std::time::Duration, -}; - -/// Lokinit's mouse event callback -#[no_mangle] -pub extern "C" fn rust_mouse_callback( - window: i32, - mouse_btn: SwiftMouseButton, - mouse_event: SwiftMouseEvent, - x: f64, - y: f64, -) { - EVENT_QUEUE.with(move |queue| { - let mouse_event = mouse_event.into_mouse_event(x, y, mouse_btn); - - queue.borrow_mut().push_back(Event { - time: Duration::ZERO, - window: WindowHandle(window as usize), - kind: EventKind::Mouse(mouse_event), - }); - }); -} - -/// Lokinit's window resize callback -#[no_mangle] -pub extern "C" fn rust_window_resize_callback(window: usize, width: u32, height: u32) { - EVENT_QUEUE.with(move |queue| { - queue.borrow_mut().push_back(Event { - time: Duration::ZERO, - window: WindowHandle(window), - kind: EventKind::Resized(width, height), - }); - }); -} diff --git a/src/native/macos/ffi_swift.rs b/src/native/macos/ffi_swift.rs index 8862298..9b86bc2 100644 --- a/src/native/macos/ffi_swift.rs +++ b/src/native/macos/ffi_swift.rs @@ -1,47 +1,140 @@ //! Bindings to the external Swift code use { - crate::event::{MouseButton, MouseEvent}, - std::ffi::c_char, + super::keysym, + crate::{ + event::{Event, EventKind, KeyboardEvent, MouseButton, MouseEvent}, + window::WindowHandle, + }, + std::{ffi::c_char, time::Duration}, }; -/// The MouseButton enum, exported for Swift #[repr(i32)] -pub enum SwiftMouseButton { - Left = 0, - Middle = 1, - Right = 2, -} -impl From for MouseButton { - fn from(value: SwiftMouseButton) -> Self { - match value { - SwiftMouseButton::Left => MouseButton::Left, - SwiftMouseButton::Right => MouseButton::Right, - SwiftMouseButton::Middle => MouseButton::Middle, - } - } -} +#[allow(dead_code)] +pub enum SwiftEventType { + MouseDownLeft, + MouseDownMiddle, + MouseDownRight, + MouseDownOther, -/// The MouseEvent enum, exported for Swift -#[repr(i32)] -pub enum SwiftMouseEvent { - Pressed = 0, - Released = 1, - Moved = 2, + MouseUpLeft, + MouseUpMiddle, + MouseUpRight, + MouseUpOther, + + MouseMoved, + MouseEntered, + MouseExited, + MouseScrolled, + + WindowResized, + WindowMoved, + WindowCloseRequested, + WindowDestroyed, + WindowGainedFocus, + WindowLostFocus, + + KeyPressed, + KeyReleased, + KeyRepeated, + + AppQuit, +} +#[repr(C)] +pub struct SwiftEvent { + pub kind: SwiftEventType, + pub data1: i32, + pub data2: i32, + pub data3: i32, + pub window: usize, } +impl TryInto for SwiftEvent { + type Error = (); -impl SwiftMouseEvent { - /// Translates the SwiftMouseEvent enum into Lokinit's MouseEvent enum - pub fn into_mouse_event(self, x: f64, y: f64, button: SwiftMouseButton) -> MouseEvent { - let x = x as i32; - let y = y as i32; - let button = button.into(); + fn try_into(self) -> Result { + let kind = match self.kind { + SwiftEventType::MouseDownLeft => EventKind::Mouse(MouseEvent::ButtonPress( + MouseButton::Left, + self.data1, + self.data2, + )), + SwiftEventType::MouseUpLeft => EventKind::Mouse(MouseEvent::ButtonRelease( + MouseButton::Left, + self.data1, + self.data2, + )), + SwiftEventType::MouseDownRight => EventKind::Mouse(MouseEvent::ButtonPress( + MouseButton::Right, + self.data1, + self.data2, + )), + SwiftEventType::MouseUpMiddle => EventKind::Mouse(MouseEvent::ButtonRelease( + MouseButton::Middle, + self.data1, + self.data2, + )), + SwiftEventType::MouseDownMiddle => EventKind::Mouse(MouseEvent::ButtonPress( + MouseButton::Middle, + self.data1, + self.data2, + )), + SwiftEventType::MouseUpRight => EventKind::Mouse(MouseEvent::ButtonRelease( + MouseButton::Right, + self.data1, + self.data2, + )), + SwiftEventType::MouseDownOther => EventKind::Mouse(MouseEvent::ButtonPress( + MouseButton::Other(self.data3.try_into().unwrap()), + self.data1, + self.data2, + )), + SwiftEventType::MouseUpOther => EventKind::Mouse(MouseEvent::ButtonRelease( + MouseButton::Other(self.data3.try_into().unwrap()), + self.data1, + self.data2, + )), + SwiftEventType::MouseMoved => { + EventKind::Mouse(MouseEvent::CursorMove(self.data1, self.data2)) + } + SwiftEventType::WindowResized => { + EventKind::Resized(self.data1 as u32, self.data2 as u32) + } + SwiftEventType::WindowMoved => EventKind::Moved(self.data1, self.data2), + SwiftEventType::WindowDestroyed => EventKind::Destroyed, + SwiftEventType::MouseEntered => { + EventKind::Mouse(MouseEvent::CursorIn(self.data1, self.data2)) + } + SwiftEventType::MouseExited => { + EventKind::Mouse(MouseEvent::CursorOut(self.data1, self.data2)) + } + SwiftEventType::WindowGainedFocus => EventKind::FocusIn, + SwiftEventType::WindowLostFocus => EventKind::FocusOut, + SwiftEventType::KeyPressed => { + match keysym::to_keycode(self.data1.try_into().unwrap()) { + None => return Err(()), + Some(key) => EventKind::Keyboard(KeyboardEvent::KeyPress(key)), + } + } + SwiftEventType::KeyRepeated => { + match keysym::to_keycode(self.data1.try_into().unwrap()) { + None => return Err(()), + Some(key) => EventKind::Keyboard(KeyboardEvent::KeyRepeat(key)), + } + } + SwiftEventType::KeyReleased => { + match keysym::to_keycode(self.data1.try_into().unwrap()) { + None => return Err(()), + Some(key) => EventKind::Keyboard(KeyboardEvent::KeyRelease(key)), + } + } + _ => return Err(()), + }; - match self { - Self::Pressed => MouseEvent::ButtonPress(button, x, y), - Self::Released => MouseEvent::ButtonRelease(button, x, y), - Self::Moved => MouseEvent::CursorMove(x, y), - } + Ok(Event { + time: Duration::ZERO, + window: WindowHandle(self.window), + kind, + }) } } @@ -66,5 +159,5 @@ extern "C" { /// the event loop. Instead, Lokinit calls this each time `poll_event()` /// is called, which updates the app state without getting stuck in Apple's /// run loop. - pub fn update() -> bool; + pub fn update() -> SwiftEvent; } diff --git a/src/native/macos/keysym.rs b/src/native/macos/keysym.rs new file mode 100644 index 0000000..146f2e2 --- /dev/null +++ b/src/native/macos/keysym.rs @@ -0,0 +1,137 @@ +use crate::keycode::KeyCode; + +pub fn to_keycode(keysym: u32) -> Option { + Some(match keysym { + // Big thanks to Mozilla: + // https://developer.mozilla.org/en-US/docs/web/api/ui_events/keyboard_event_code_values#code_values_on_mac + // Mozilla's always there for me when the bad fruit company isn't <3 + 0x00 => KeyCode::A, + 0x01 => KeyCode::S, + 0x02 => KeyCode::D, + 0x03 => KeyCode::F, + 0x04 => KeyCode::H, + 0x05 => KeyCode::G, + 0x06 => KeyCode::Z, + 0x07 => KeyCode::X, + 0x08 => KeyCode::C, + 0x09 => KeyCode::V, + // 0x0A => ISO section? + 0x0B => KeyCode::B, + 0x0C => KeyCode::Q, + 0x0D => KeyCode::W, + 0x0E => KeyCode::E, + 0x0F => KeyCode::R, + 0x10 => KeyCode::Y, + 0x11 => KeyCode::T, + 0x12 => KeyCode::Key1, + 0x13 => KeyCode::Key2, + 0x14 => KeyCode::Key3, + 0x15 => KeyCode::Key4, + 0x16 => KeyCode::Key6, + 0x17 => KeyCode::Key5, + 0x18 => KeyCode::Equals, + 0x19 => KeyCode::Key9, + 0x1A => KeyCode::Key7, + 0x1B => KeyCode::Minus, + 0x1C => KeyCode::Key8, + 0x1D => KeyCode::Key0, + 0x1E => KeyCode::RBracket, + 0x1F => KeyCode::O, + 0x20 => KeyCode::U, + 0x21 => KeyCode::LBracket, + 0x22 => KeyCode::I, + 0x23 => KeyCode::P, + 0x24 => KeyCode::Enter, + 0x25 => KeyCode::L, + 0x26 => KeyCode::J, + 0x27 => KeyCode::SingleQuote, + 0x28 => KeyCode::K, + 0x29 => KeyCode::Semicolon, + 0x2A => KeyCode::Backslash, + 0x2B => KeyCode::Comma, + 0x2C => KeyCode::Slash, + 0x2D => KeyCode::N, + 0x2E => KeyCode::M, + 0x2F => KeyCode::Point, + 0x30 => KeyCode::Tab, + 0x31 => KeyCode::Space, + 0x32 => KeyCode::Backtick, + 0x33 => KeyCode::Backspace, + // 0x34 => numpad enter on powerbook + 0x35 => KeyCode::Escape, + 0x36 => KeyCode::RCommand, + 0x37 => KeyCode::LCommand, + 0x38 => KeyCode::LShift, + 0x39 => KeyCode::CapsLock, + 0x3A => KeyCode::LAlt, + 0x3B => KeyCode::LCtrl, + 0x3C => KeyCode::RShift, + 0x3D => KeyCode::RAlt, + 0x3E => KeyCode::RCtrl, + // 0x3F => nil + // 0x40 => F17 + 0x41 => KeyCode::NumpadDecimal, + // 0x42 => nil + 0x43 => KeyCode::NumpadMultiply, + // 0x44 => nil + 0x45 => KeyCode::NumpadAdd, + // 0x46 => nil + 0x47 => KeyCode::NumLock, + // 0x48 => volume up + // 0x49 => volume down + // 0x4A => mute volume + 0x4B => KeyCode::NumpadDivide, + 0x4C => KeyCode::NumpadEnter, + // 0x4D => nil + 0x4E => KeyCode::NumpadSubtract, + // 0x4F => F18 + // 0x50 => F19 + 0x51 => KeyCode::NumpadEquals, + 0x52 => KeyCode::Numpad0, + 0x53 => KeyCode::Numpad1, + 0x54 => KeyCode::Numpad2, + 0x55 => KeyCode::Numpad3, + 0x56 => KeyCode::Numpad4, + 0x57 => KeyCode::Numpad5, + 0x58 => KeyCode::Numpad6, + 0x59 => KeyCode::Numpad7, + // 0x5A => F20 + 0x5B => KeyCode::Numpad8, + 0x5C => KeyCode::Numpad9, + // 0x5D => JIS yen + // 0x5E => JIS underscore + 0x5F => KeyCode::NumpadComma, + 0x60 => KeyCode::F5, + 0x61 => KeyCode::F6, + 0x62 => KeyCode::F7, + 0x63 => KeyCode::F3, + 0x64 => KeyCode::F8, + 0x65 => KeyCode::F9, + // 0x66 => JIS eisu + 0x67 => KeyCode::F11, + // 0x68 => JIS kana + // 0x69 => F13 + // 0x6A => F16 + // 0x6B => F14 + // 0x6C => nil + 0x6D => KeyCode::F10, + // 0x6E => context menu? + 0x6F => KeyCode::F12, + // 0x70 => nil + // 0x71 => F15 + // 0x72 => help or insert? + 0x73 => KeyCode::Home, + 0x74 => KeyCode::PageUp, + 0x75 => KeyCode::Delete, + 0x76 => KeyCode::F4, + 0x77 => KeyCode::End, + 0x78 => KeyCode::F2, + 0x79 => KeyCode::PageDown, + 0x7A => KeyCode::F1, + 0x7B => KeyCode::ArrowLeft, + 0x7C => KeyCode::ArrowRight, + 0x7D => KeyCode::ArrowDown, + 0x7E => KeyCode::ArrowUp, + _ => return None, + }) +} diff --git a/src/native/macos/mod.rs b/src/native/macos/mod.rs index 7dae194..ac5fbe5 100644 --- a/src/native/macos/mod.rs +++ b/src/native/macos/mod.rs @@ -2,6 +2,7 @@ use crate::lok::{CreateWindowError, LokinitBackend}; mod ffi_rust; mod ffi_swift; +mod keysym; use { crate::{ @@ -11,10 +12,6 @@ use { std::{cell::RefCell, collections::VecDeque, ffi::CString}, }; -thread_local! { - static EVENT_QUEUE: RefCell> = RefCell::new(VecDeque::new()); -} - pub struct MacosBackend; impl LokinitBackend for MacosBackend { @@ -46,14 +43,6 @@ impl LokinitBackend for MacosBackend { } fn poll_event(&mut self) -> Option { - let mut event = None; - while event.is_none() { - // update() will return `True` if the app should terminate - if unsafe { ffi_swift::update() } { - return None; - } - event = EVENT_QUEUE.with(|queue| queue.borrow_mut().pop_front()); - } - event + unsafe { ffi_swift::update() }.try_into().ok() } } diff --git a/swift/Sources/AppleBindings/MacOS/bscursor.swift b/swift/Sources/AppleBindings/MacOS/bscursor.swift new file mode 100644 index 0000000..eebca53 --- /dev/null +++ b/swift/Sources/AppleBindings/MacOS/bscursor.swift @@ -0,0 +1,27 @@ +#if os(macOS) + +import Foundation +import AppKit + +// Loads external cursors that aren't in NSCursor +public struct BSCursor { + static let baseCursorPath = URL(string: "file:///System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors/")! + + public static let windowResizeNorthSouth: NSCursor = fetchHICursor("resizenorthsouth") + public static let windowResizeEastWest: NSCursor = fetchHICursor("resizeeastwest") + public static let windowResizeNorthEastSouthWest: NSCursor = fetchHICursor("resizenortheastsouthwest") + public static let windowResizeNorthWestSouthEast: NSCursor = fetchHICursor("resizenorthwestsoutheast") + public static let empty: NSCursor = NSCursor(image: NSImage(size: NSSize.zero), hotSpot: NSPoint.zero) + + // Loads cursors from HIServices.framework + // https://stackoverflow.com/a/21786835/19707043 + static func fetchHICursor(_ name: String) -> NSCursor { + let path = baseCursorPath.append(name + "/") + let image = NSImage(byReferencing: path.append("cursor.pdf")) + let info = try! NSDictionary(contentsOf: path.append("info.plist"), error: ()) + let hotspot = NSPoint(x: info.value(forKey: "hotx")! as! Double, y: info.value(forKey: "hoty")! as! Double) + return NSCursor(image: image, hotSpot: hotspot) + } +} + +#endif diff --git a/swift/Sources/AppleBindings/MacOS/extensions.swift b/swift/Sources/AppleBindings/MacOS/extensions.swift new file mode 100644 index 0000000..4ccc159 --- /dev/null +++ b/swift/Sources/AppleBindings/MacOS/extensions.swift @@ -0,0 +1,63 @@ +#if os(macOS) + +import Foundation +import AppKit + +// Used to get the currently frontmost window +extension NSApplication { + public var frontWindow: NSWindow { + self.orderedWindows[0] + } +} + +// Adds an append method, since `.appending` isn't available on older macOS versions +extension URL { + public func append(_ path: String) -> URL { + return URL(string: path, relativeTo: self)! + } +} + +// Adds convenience initializers so the events aren't so annoying to make +extension LokEvent { + init(_ event: LokEventType, _ window: UInt) { + self.init( + type: event, + data1: 0, + data2: 0, + data3: 0, + window: window + ) + } + + init(_ event: LokEventType, _ data1: Int32, _ window: UInt) { + self.init( + type: event, + data1: data1, + data2: 0, + data3: 0, + window: window + ) + } + + init(_ event: LokEventType, _ data1: Int32, _ data2: Int32, _ window: UInt) { + self.init( + type: event, + data1: data1, + data2: data2, + data3: 0, + window: window + ) + } + + init(_ event: LokEventType, _ data1: Int32, _ data2: Int32, _ data3: Int32, _ window: UInt) { + self.init( + type: event, + data1: data1, + data2: data2, + data3: data3, + window: window + ) + } +} + +#endif diff --git a/swift/Sources/AppleBindings/MacOS/window.swift b/swift/Sources/AppleBindings/MacOS/window.swift new file mode 100644 index 0000000..859958d --- /dev/null +++ b/swift/Sources/AppleBindings/MacOS/window.swift @@ -0,0 +1,399 @@ +#if os(macOS) + +import Foundation +import AppKit + +// All of the sides of a window, and its corners +public enum WindowBorderLocation { + case Left + case Right + case Top + case Bottom + case TopLeft + case TopRight + case BottomLeft + case BottomRight +} + +// A customized NSWindow, with sensible defaults and helper functions/variables +public class BSWindow: NSWindow { + // Window style masks to make it resizable and have a title bar + static let masks = NSWindow.StyleMask.init(rawValue: + NSWindow.StyleMask.titled.rawValue | + NSWindow.StyleMask.closable.rawValue | + NSWindow.StyleMask.miniaturizable.rawValue | + NSWindow.StyleMask.resizable.rawValue + ) + // The size of the box that holds the "traffic light" close/minimise/maximise buttons + static let titlebarButtonBox = NSRect( + x: 7, + y: 6, + width: 54, + height: 16 + ) + // The minimum size a window can be + static let minimumWindowSize = CGSize( + width: 50, + height: 50 + ) + // Sizes used for detecting if the cursor is over a window border + static let windowResizeCornerHitboxSize = 15.0 + static let windowResizeCornerHitboxOffset = (windowResizeCornerHitboxSize + 1.0) / 2.0 + static let windowResizeSideHitboxSize = 7.0 + static let windowResizeSideHitboxOffset = (windowResizeSideHitboxSize + 1.0) / 2.0 + + // Which part of the window is being resized right now, if it's being resized + var resizeBorder: WindowBorderLocation? = nil + // If the cursor is a non-default cursor + var nonDefaultCursor = false + // If the mouse was in the window (tracks mouse entered/left events) + var mouseWasInWindow = false + + init(_ size: NSRect, _ centered: Bool, _ title: String) { + print("Making new window") + + super.init( + contentRect: size, + styleMask: Self.masks, + backing: NSWindow.BackingStoreType.buffered, + defer: false + ) + + self.title = title + + // Show the window, and make it the primary window + self.makeKeyAndOrderFront(nil) + self.makeMain() + + if centered { + self.center() + } + + self.disableCursorRects() + } + + // Mouse press down handler for windows + // Returns true if the event was handled, false if it wasn't + func leftButtonDownHandler(_ event: NSEvent) -> Bool { + if !self.isMainWindow { + // print("Making self main window due to click") + self.focus() + } + + if let border = self.checkMouseInBorder() { + self.resizeBorder = border + return true + } + + return false + } + // Mouse dragged handler for windows + // Returns an event that should be forwarded to Lokinit + func leftButtonDraggedHandler(_ event: NSEvent) -> LokEvent? { + let mousePos = self.mouseLocationOutsideOfEventStream + + // Resize the window if we're currently resizing + if let border = self.resizeBorder { + let rect: CGRect + + switch border { + case .Top: + rect = CGRect( + x: self.frame.origin.x, + y: self.frame.origin.y, + width: self.frame.width, + height: mousePos.y + ) + case .Bottom: + rect = CGRect( + x: self.frame.origin.x, + y: self.frame.origin.y + mousePos.y, + width: self.frame.width, + height: self.frame.height - mousePos.y + ) + case .Left: + rect = CGRect( + x: self.frame.origin.x + mousePos.x, + y: self.frame.origin.y, + width: self.frame.width - mousePos.x, + height: self.frame.height + ) + case .Right: + rect = CGRect( + x: self.frame.origin.x, + y: self.frame.origin.y, + width: mousePos.x, + height: self.frame.height + ) + case .TopLeft: + rect = CGRect( + x: self.frame.origin.x + mousePos.x, + y: self.frame.origin.y, + width: self.frame.width - mousePos.x, + height: mousePos.y + ) + case .TopRight: + rect = CGRect( + x: self.frame.origin.x, + y: self.frame.origin.y, + width: mousePos.x, + height: mousePos.y + ) + case .BottomLeft: + rect = CGRect( + x: self.frame.origin.x + mousePos.x, + y: self.frame.origin.y + mousePos.y, + width: self.frame.width - mousePos.x, + height: self.frame.height - mousePos.y + ) + case .BottomRight: + rect = CGRect( + x: self.frame.origin.x, + y: self.frame.origin.y + mousePos.y, + width: mousePos.x, + height: self.frame.height - mousePos.y + ) + } + + // Make sure the resized window has a valid size; it might break the window otherwise + if rect.size.width > Self.minimumWindowSize.width && rect.size.height > Self.minimumWindowSize.height { + self.setFrame(rect, display: true, animate: false) + + return LokEvent(.WindowResized, Int32(rect.width), Int32(rect.height), UInt(event.window!.windowNumber)) + } + } + + return LokEvent(.MouseMoved, Int32(mousePos.x), Int32(mousePos.y), UInt(event.window!.windowNumber)) + } + // Mouse release handler for windows + // Returns true if the event was handled, false if it wasn't + func leftButtonUpHandler(_ event: NSEvent) -> Bool { + if self.resizeBorder != nil { + self.resizeBorder = nil + + return true + } + + let mousePos = self.getMouseLocation() + for btn in self.windowButtons() { + if btn.frame.contains(mousePos) { + btn.isHighlighted = true + btn.performClick(nil) + + return true + } + } + + return false + } + // Mouse movement handler for windows + // Returns true if the event was handled, false if it wasn't + func mouseMovedHandler(_ event: NSEvent) -> Bool { + let mousePos = self.getMouseLocation() + + // Check if the mouse entered or left the window + if self.checkMouseInWindow() && !self.mouseWasInWindow { + self.mouseWasInWindow = true + EventBuffer.append(LokEvent( + .MouseEntered, + Int32(mousePos.x), + Int32(mousePos.y), + UInt(self.windowNumber) + )) + } else if self.checkMouseOutsideWindow() && self.mouseWasInWindow { + self.mouseWasInWindow = false + EventBuffer.append(LokEvent( + .MouseExited, + Int32(mousePos.x), + Int32(mousePos.y), + UInt(self.windowNumber) + )) + } + + // Check if cursor is over one of the window borders + let mouseInBorder = self.checkMouseInBorder() + if let border = mouseInBorder { + switch border { + case .Top, .Bottom: + self.setCursor(BSCursor.windowResizeNorthSouth) + case .Left, .Right: + self.setCursor(BSCursor.windowResizeEastWest) + case .TopLeft, .BottomRight: + self.setCursor(BSCursor.windowResizeNorthWestSouthEast) + case .TopRight, .BottomLeft: + self.setCursor(BSCursor.windowResizeNorthEastSouthWest) + } + return true + } else if self.nonDefaultCursor { + self.setCursor(NSCursor.arrow, false) + } + + // Check if cursor is over one of the titlebar buttons + if BSWindow.titlebarButtonBox.contains(mousePos) { + for btn in self.windowButtons() { + btn.isHighlighted = true + } + + return true + } else { + for btn in self.windowButtons() { + btn.isHighlighted = false + } + } + + return false + } + + // Returns an array of all the stoplight buttons + func windowButtons() -> [NSButton] { + return [ + self.standardWindowButton(ButtonType.closeButton)!, + self.standardWindowButton(ButtonType.miniaturizeButton)!, + self.standardWindowButton(ButtonType.zoomButton)!, + ] + } + + // The mouse points apple hands us have an inverted Y-coordinate. + // This corrects the points so they're actually useable + func correctWindowPoint(_ point: NSPoint) -> NSPoint { + return NSPoint( + x: point.x, + y: self.frame.height - point.y - 1.0 + ) + } + + // Gets the mouse location in the window and corrects its location + func getMouseLocation() -> NSPoint { + return self.correctWindowPoint(self.mouseLocationOutsideOfEventStream) + } + + // Checks if the mouse is out of the window + func checkMouseOutsideWindow() -> Bool { + let window = CGRect( + x: -Self.windowResizeSideHitboxOffset, + y: -Self.windowResizeSideHitboxOffset, + width: self.frame.width + (Self.windowResizeSideHitboxOffset * 2), + height: self.frame.height + (Self.windowResizeSideHitboxOffset * 2) + ) + + return !window.contains(self.getMouseLocation()) + } + // Checks if the mouse is in the window + func checkMouseInWindow() -> Bool { + let innerWindow = CGRect( + x: Self.windowResizeSideHitboxOffset, + y: Self.windowResizeSideHitboxOffset, + width: self.frame.width - (Self.windowResizeSideHitboxOffset * 2), + height: self.frame.height - (Self.windowResizeSideHitboxOffset * 2) + ) + + return innerWindow.contains(self.getMouseLocation()) + } + // Checks if the mouse is in any of the window borders + func checkMouseInBorder() -> WindowBorderLocation? { + let mouseLocation = NSEvent.mouseLocation + + if self.checkMouseInWindow() { + return nil + } + + // print("Mouse moved to \(mouseLocation) || Window frame: \(frame)") + + // Calculate boxes for the top, bottom, left, and right edges of the window + let top = CGRect( + x: self.frame.minX, + y: self.frame.maxY - Self.windowResizeSideHitboxOffset, + width: self.frame.width, + height: Self.windowResizeSideHitboxSize + ) + let bottom = CGRect( + x: self.frame.minX, + y: self.frame.minY - Self.windowResizeSideHitboxOffset, + width: self.frame.width, + height: Self.windowResizeSideHitboxSize + ) + let left = CGRect( + x: self.frame.minX - Self.windowResizeSideHitboxOffset, + y: self.frame.minY, + width: Self.windowResizeSideHitboxSize, + height: self.frame.height + ) + let right = CGRect( + x: self.frame.maxX - Self.windowResizeSideHitboxOffset, + y: self.frame.minY, + width: Self.windowResizeSideHitboxSize, + height: self.frame.height + ) + + // Calculate boxes for the corners of the window + let topLeft = CGRect( + x: self.frame.minX - Self.windowResizeCornerHitboxOffset, + y: self.frame.maxY - Self.windowResizeCornerHitboxOffset, + width: Self.windowResizeCornerHitboxSize, + height: Self.windowResizeCornerHitboxSize + ) + let topRight = CGRect( + x: self.frame.maxX - Self.windowResizeCornerHitboxOffset, + y: self.frame.maxY - Self.windowResizeCornerHitboxOffset, + width: Self.windowResizeCornerHitboxSize, + height: Self.windowResizeCornerHitboxSize + ) + let bottomLeft = CGRect( + x: self.frame.minX - Self.windowResizeCornerHitboxOffset, + y: self.frame.minY - Self.windowResizeCornerHitboxOffset, + width: Self.windowResizeCornerHitboxSize, + height: Self.windowResizeCornerHitboxSize + ) + let bottomRight = CGRect( + x: self.frame.maxX - Self.windowResizeCornerHitboxOffset, + y: self.frame.minY - Self.windowResizeCornerHitboxOffset, + width: Self.windowResizeCornerHitboxSize, + height: Self.windowResizeCornerHitboxSize + ) + + // Return the border the mouse is on + if topLeft.contains(mouseLocation) { + return .TopLeft + } else if topRight.contains(mouseLocation) { + return .TopRight + } else if bottomLeft.contains(mouseLocation) { + return .BottomLeft + } else if bottomRight.contains(mouseLocation) { + return .BottomRight + } else if top.contains(mouseLocation) { + return .Top + } else if bottom.contains(mouseLocation) { + return .Bottom + } else if left.contains(mouseLocation) { + return .Left + } else if right.contains(mouseLocation) { + return .Right + } else { + return nil + } + } + + // Changes the cursor icon + func setCursor(_ cursor: NSCursor, _ nonDefault: Bool = true) { + cursor.set() + self.nonDefaultCursor = nonDefault + } + + // Pass the window destroyed event to Lokinit + public override func close() { + EventBuffer.append(LokEvent(.WindowDestroyed, UInt(self.windowNumber))) + super.close() + } + + // Make the window the main window, and send focus events to Lokinit + public func focus() { + EventBuffer.append(LokEvent(.WindowLostFocus, UInt(NSApp.frontWindow.windowNumber))) + EventBuffer.append(LokEvent(.WindowGainedFocus, UInt(self.windowNumber))) + // Yes... we need all 3 of these just to make the window the main window :dawae: + self.makeKeyAndOrderFront(nil) + self.makeMain() + self.becomeMain() + } +} + +#endif diff --git a/swift/Sources/AppleBindings/bridging-header.h b/swift/Sources/AppleBindings/bridging-header.h index 7d6dac7..8a1d31e 100644 --- a/swift/Sources/AppleBindings/bridging-header.h +++ b/swift/Sources/AppleBindings/bridging-header.h @@ -1,5 +1,43 @@ #include +// Swift repr of the events +typedef CF_ENUM(int, LokEventType) { + MouseDownLeft, + MouseDownMiddle, + MouseDownRight, + MouseDownOther, + + MouseUpLeft, + MouseUpMiddle, + MouseUpRight, + MouseUpOther, + + MouseMoved, + MouseEntered, + MouseExited, + MouseScrolled, + + WindowResized, + WindowMoved, + WindowCloseRequested, + WindowDestroyed, + WindowGainedFocus, + WindowLostFocus, + + KeyPressed, + KeyReleased, + KeyRepeated, + + AppQuit +}; +struct LokEvent { + LokEventType type; + int data1; + int data2; + int data3; + unsigned long window; +}; +typedef struct LokEvent LokEvent; // Swift representation of the MouseButton and MouseEvent enums typedef CF_ENUM(int, MouseButton) { Left = 0, @@ -13,5 +51,4 @@ typedef CF_ENUM(int, MouseEvent) { }; // Rust FFI callbacks -void rust_mouse_callback(int window, MouseButton btn, MouseEvent event, double x, double y); -void rust_window_resize_callback(unsigned long window, unsigned int width, unsigned int height); \ No newline at end of file +void rust_queue_event(LokEvent event); diff --git a/swift/Sources/AppleBindings/macos-ffi.swift b/swift/Sources/AppleBindings/macos-ffi.swift index 2a003cf..8e2d3ce 100644 --- a/swift/Sources/AppleBindings/macos-ffi.swift +++ b/swift/Sources/AppleBindings/macos-ffi.swift @@ -1,42 +1,162 @@ #if os(macOS) +import Foundation import AppKit +public var EventBuffer: Array = Array() +var LastKeySym: Int32? = nil + @_cdecl("setup") func ffiSetup() { - let nsApp = NSApplication.shared - nsApp.setActivationPolicy(NSApplication.ActivationPolicy.regular) - nsApp.activate(ignoringOtherApps: true) - nsApp.finishLaunching() + // Init NSApplication + let NSApp = NSApplication.shared + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + NSApp.finishLaunching() } @_cdecl("create_window") func ffiCreateWindow(x: Int, y: Int, width: Int, height: Int, centered: Bool, title: UnsafePointer) -> UInt64 { - let title = String.init(cString: title) - let size = NSRect.init(x: x, y: y, width: width, height: height) - let window = MacOSWindow.init(size, centered, title) + let title = String(cString: title) + let size = NSRect(x: x, y: y, width: width, height: height) + let window = BSWindow(size, centered, title) return UInt64(window.windowNumber) } @_cdecl("update") -func ffiUpdate() -> Bool { - if NSApp.windows.count == 0 { - return true - } +func ffiUpdate() -> LokEvent { while true { + if NSApp.windows.count == 0 { + return LokEvent(.AppQuit, 0) + } + + if EventBuffer.first != nil { + return EventBuffer.removeFirst() + } + let event = NSApp.nextEvent( matching: NSEvent.EventTypeMask.any, - until: nil, + until: Date.distantFuture, inMode: RunLoop.Mode.default, dequeue: true - ) - if event == nil { - break - } + )! - NSApp.sendEvent(event!) + switch event.type { + case .appKitDefined: + switch event.subtype { + // So far as I can tell, these events use private APIs, so we have to use sendEvent + case .applicationActivated: + NSApp.sendEvent(event) + case .applicationDeactivated: + NSApp.sendEvent(event) + case .screenChanged: + fatalError("screenChanged event not yet handled") + case .windowExposed: + fatalError("windowExposed event not yet implemented") + case .windowMoved: + let window = event.window! + window.sendEvent(event) + return LokEvent( + .WindowMoved, + Int32(window.frame.origin.x), + Int32(window.frame.origin.y), + UInt(window.windowNumber) + ) + default: + continue + } + case .systemDefined: + switch event.subtype { + case .powerOff: + fatalError("powerOff event not yet handled") + default: + continue + } + case .leftMouseDown: + let window = event.window! as! BSWindow + let handled = window.leftButtonDownHandler(event) + if !handled { + let mousePos = window.getMouseLocation() + return LokEvent(.MouseDownLeft, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + } + case .leftMouseDragged: + let window = event.window! as! BSWindow + let forwardEvent = window.leftButtonDraggedHandler(event) + if let event = forwardEvent { + return event + } + case .leftMouseUp: + let window = event.window! as! BSWindow + let handled = window.leftButtonUpHandler(event) + if !handled { + let mousePos = window.getMouseLocation() + return LokEvent(.MouseUpLeft, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + } + case .rightMouseDown: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + return LokEvent(.MouseDownRight, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + case .rightMouseDragged: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + return LokEvent(.MouseMoved, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + case .rightMouseUp: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + return LokEvent(.MouseUpRight, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + case .otherMouseDown: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + let mouseBtn = event.buttonNumber + + if mouseBtn == 2 { + return LokEvent(.MouseDownMiddle, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + } else { + return LokEvent(.MouseDownOther, Int32(mousePos.x), Int32(mousePos.y), Int32(mouseBtn), UInt(window.windowNumber)) + } + case .otherMouseDragged: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + + return LokEvent(.MouseMoved, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + case .otherMouseUp: + let window = event.window! as! BSWindow + let mousePos = window.getMouseLocation() + let mouseBtn = event.buttonNumber + + if mouseBtn == 2 { + return LokEvent(.MouseUpMiddle, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + } else { + return LokEvent(.MouseUpOther, Int32(mousePos.x), Int32(mousePos.y), Int32(event.buttonNumber), UInt(window.windowNumber)) + } + case .mouseMoved: + let window = NSApp.frontWindow as! BSWindow + let handled = window.mouseMovedHandler(event) + if !handled { + let mousePos = window.getMouseLocation() + return LokEvent(.MouseMoved, Int32(mousePos.x), Int32(mousePos.y), UInt(window.windowNumber)) + } + case .keyDown: + let keySym = Int32(event.keyCode) + + if keySym == LastKeySym { + return LokEvent(.KeyRepeated, keySym, UInt(event.windowNumber)) + } else { + LastKeySym = keySym + return LokEvent(.KeyPressed, keySym, UInt(event.windowNumber)) + } + case .keyUp: + let keySym = Int32(event.keyCode) + + if keySym == LastKeySym { + LastKeySym = nil + } + + return LokEvent(.KeyReleased, keySym, UInt(event.windowNumber)) + default: + continue + } } - return false } -#endif \ No newline at end of file +#endif diff --git a/swift/Sources/AppleBindings/macos.swift b/swift/Sources/AppleBindings/macos.swift deleted file mode 100644 index ad2cba3..0000000 --- a/swift/Sources/AppleBindings/macos.swift +++ /dev/null @@ -1,109 +0,0 @@ -#if os(macOS) - -import AppKit; - -public class MacOSWindow: NSWindow, NSWindowDelegate { - static let masks = - NSWindow.StyleMask.titled.rawValue | - NSWindow.StyleMask.closable.rawValue | - NSWindow.StyleMask.miniaturizable.rawValue | - NSWindow.StyleMask.resizable.rawValue - - init(_ size: NSRect, _ centered: Bool, _ title: String) { - super.init( - contentRect: size, - styleMask: NSWindow.StyleMask.init(rawValue: Self.masks), - backing: NSWindow.BackingStoreType.buffered, - defer: false - ) - - self.acceptsMouseMovedEvents = true - self.title = title - self.delegate = self - - if centered { - self.center() - } - - let view = MacOSView.init(size, UInt64(self.windowNumber)) - self.contentView = view - self.makeFirstResponder(view) - - self.makeKeyAndOrderFront(nil) - } - - // Window resize event - public func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize { - rust_window_resize_callback( - UInt(sender.windowNumber), - UInt32(frameSize.width), - UInt32(frameSize.height) - ) - - return frameSize - } -} - -public class MacOSView: NSView { - let id: UInt64 - - init(_ frame: NSRect, _ id: UInt64) { - self.id = id - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // Allows the user to interact with elements on the window, - // even if it isn't focused, and focus at the same time - override public func acceptsFirstMouse(for event: NSEvent?) -> Bool { - return true - } - - // The points macOS gives us for click events aren't in the View's - // local coordinate system. They aren't scaled for DPI, and their Y - // coordinate is inverted. This method adjusts points to correct that. - func translateMousePoint(_ point: NSPoint) -> (Float64, Float64) { - let scaled_point = self.convertToBacking(point) - let y = Float64(self.bounds.height) - Float64(scaled_point.y) - 1.0 - return (Float64(scaled_point.x), y) - } - - // Mouse down events - override public func mouseDown(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Left, MouseEvent.Pressed, point.0, point.1) - } - override public func rightMouseDown(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Right, MouseEvent.Pressed, point.0, point.1) - } - override public func otherMouseDown(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Middle, MouseEvent.Pressed, point.0, point.1) - } - - // Mouse up events - override public func mouseUp(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Left, MouseEvent.Released, point.0, point.1) - } - override public func rightMouseUp(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Right, MouseEvent.Released, point.0, point.1) - } - override public func otherMouseUp(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Middle, MouseEvent.Released, point.0, point.1) - } - - // Mouse movement events - override public func mouseMoved(with event: NSEvent) { - let point = self.translateMousePoint(event.locationInWindow) - rust_mouse_callback(Int32(event.windowNumber), MouseButton.Left, MouseEvent.Moved, point.0, point.1) - } -} - -#endif \ No newline at end of file