From 686ceb88da67480313831848088303e3fb6dd7a9 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 30 Jan 2021 18:41:47 +0100 Subject: [PATCH] First draft of Windows ui-layer --- Cargo.lock | 25 +++ Cargo.toml | 1 + espanso-detect/Cargo.toml | 1 + espanso-detect/src/lib.rs | 2 +- espanso-detect/src/win32/mod.rs | 109 ++++++++--- espanso-detect/src/win32/native.cpp | 96 ++++++---- espanso-detect/src/win32/native.h | 14 +- espanso-ui/Cargo.toml | 16 ++ espanso-ui/build.rs | 57 ++++++ espanso-ui/src/event.rs | 23 +++ espanso-ui/src/icons.rs | 8 + espanso-ui/src/lib.rs | 5 + espanso-ui/src/win32/mod.rs | 269 +++++++++++++++++++++++++++ espanso-ui/src/win32/native.cpp | 276 ++++++++++++++++++++++++++++ espanso-ui/src/win32/native.h | 59 ++++++ espanso/Cargo.toml | 4 +- espanso/src/main.rs | 40 +++- 17 files changed, 935 insertions(+), 70 deletions(-) create mode 100644 espanso-ui/Cargo.toml create mode 100644 espanso-ui/build.rs create mode 100644 espanso-ui/src/event.rs create mode 100644 espanso-ui/src/icons.rs create mode 100644 espanso-ui/src/lib.rs create mode 100644 espanso-ui/src/win32/mod.rs create mode 100644 espanso-ui/src/win32/native.cpp create mode 100644 espanso-ui/src/win32/native.h diff --git a/Cargo.lock b/Cargo.lock index 0725ab5..3260b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,8 @@ name = "espanso" version = "1.0.0" dependencies = [ "espanso-detect 0.1.0", + "espanso-ui 0.1.0", + "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -34,6 +36,17 @@ version = "0.1.0" dependencies = [ "cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", + "widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "espanso-ui" +version = "0.1.0" +dependencies = [ + "cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", "widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -46,6 +59,11 @@ dependencies = [ "unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "log" version = "0.4.14" @@ -54,6 +72,11 @@ dependencies = [ "cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro2" version = "1.0.24" @@ -100,7 +123,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" "checksum enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" "checksum heck 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +"checksum lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" "checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" "checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" "checksum quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" "checksum syn 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)" = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" diff --git a/Cargo.toml b/Cargo.toml index 4fd902e..8c71a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,5 @@ members = [ "espanso", "espanso-detect", + "espanso-ui", ] \ No newline at end of file diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index 621e651..52cee9b 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -10,6 +10,7 @@ log = "0.4.14" [target.'cfg(windows)'.dependencies] widestring = "0.4.3" +lazycell = "1.3.0" [build-dependencies] cc = "1.0.66" diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index 4f50a52..5b23648 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -20,4 +20,4 @@ pub mod event; #[cfg(target_os = "windows")] -pub mod win32; \ No newline at end of file +pub mod win32; diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs index 09d5401..040179c 100644 --- a/espanso-detect/src/win32/mod.rs +++ b/espanso-detect/src/win32/mod.rs @@ -17,7 +17,10 @@ * along with espanso. If not, see . */ -use log::{trace, warn}; +use std::ffi::c_void; + +use lazycell::LazyCell; +use log::{error, trace, warn}; use widestring::U16CStr; use crate::event::Status::*; @@ -57,35 +60,81 @@ pub struct RawInputEvent { } #[allow(improper_ctypes)] -#[link(name = "native", kind = "static")] +#[link(name = "espansodetect", kind = "static")] extern "C" { - pub fn raw_eventloop( - _self: *const Win32Source, + pub fn detect_initialize(_self: *const Win32Source) -> *mut c_void; + + pub fn detect_eventloop( + window: *const c_void, event_callback: extern "C" fn(_self: *mut Win32Source, event: RawInputEvent), - ); + ) -> i32; + + pub fn detect_destroy(window: *const c_void) -> i32; } pub type Win32SourceCallback = Box; pub struct Win32Source { - callback: Win32SourceCallback, + handle: *mut c_void, + callback: LazyCell, } impl Win32Source { - pub fn new(callback: Win32SourceCallback) -> Win32Source { - Self { callback } + pub fn new() -> Win32Source { + Self { + handle: std::ptr::null_mut(), + callback: LazyCell::new(), + } } - pub fn eventloop(&self) { - unsafe { - extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) { - let event: Option = event.into(); + + pub fn initialize(&mut self) { + let handle = unsafe { detect_initialize(self as *const Win32Source) }; + + if handle.is_null() { + panic!("Unable to initialize Win32EventLoop"); + } + + self.handle = handle; + } + + pub fn eventloop(&self, event_callback: Win32SourceCallback) { + if self.handle.is_null() { + panic!("Attempt to start Win32Source eventloop without initialization"); + } + + if let Err(_) = self.callback.fill(event_callback) { + panic!("Unable to set Win32Source event callback"); + } + + extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) { + let event: Option = event.into(); + if let Some(callback) = unsafe { (*_self).callback.borrow() } { if let Some(event) = event { - unsafe { (*(*_self).callback)(event) } + callback(event) } else { trace!("Unable to convert raw event to input event"); } } + } - raw_eventloop(self as *const Win32Source, callback); + let error_code = unsafe { detect_eventloop(self.handle, callback) }; + + if error_code <= 0 { + panic!("Win32Source eventloop returned a negative error code"); + } + } +} + +impl Drop for Win32Source { + fn drop(&mut self) { + if self.handle.is_null() { + error!("Win32Source destruction cannot be performed, handle is null"); + return; + } + + let result = unsafe { detect_destroy(self.handle) }; + + if result != 0 { + error!("Win32EventLoop destruction returned non-zero code"); } } } @@ -260,17 +309,19 @@ mod tests { raw.key_code = 0x4B; let result: Option = raw.into(); - assert_eq!(result.unwrap(), InputEvent::Keyboard(KeyboardEvent { - key: Other(0x4B), - status: Released, - value: Some("k".to_string()), - variant: None, - })); + assert_eq!( + result.unwrap(), + InputEvent::Keyboard(KeyboardEvent { + key: Other(0x4B), + status: Released, + value: Some("k".to_string()), + variant: None, + }) + ); } #[test] fn raw_to_input_event_mouse_works_correctly() { - let mut raw = default_raw_input_event(); raw.event_type = INPUT_EVENT_TYPE_MOUSE; raw.status = INPUT_STATUS_RELEASED; @@ -278,10 +329,13 @@ mod tests { raw.key_code = INPUT_MOUSE_RIGHT_BUTTON; let result: Option = raw.into(); - assert_eq!(result.unwrap(), InputEvent::Mouse(MouseEvent { - status: Released, - button: MouseButton::Right, - })); + assert_eq!( + result.unwrap(), + InputEvent::Mouse(MouseEvent { + status: Released, + button: MouseButton::Right, + }) + ); } #[test] @@ -299,13 +353,14 @@ mod tests { #[test] fn raw_to_input_event_returns_none_when_missing_type() { let result: Option = RawInputEvent { - event_type: 0, // Missing type + event_type: 0, // Missing type buffer: [0; 24], buffer_len: 0, key_code: 123, variant: INPUT_LEFT_VARIANT, status: INPUT_STATUS_PRESSED, - }.into(); + } + .into(); assert!(result.is_none()); } } diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp index 6e586aa..9e7db4f 100644 --- a/espanso-detect/src/win32/native.cpp +++ b/espanso-detect/src/win32/native.cpp @@ -18,6 +18,7 @@ */ #include "native.h" +#include #include #include #include @@ -39,29 +40,41 @@ #include // How many milliseconds must pass between events before refreshing the keyboard layout -const long refreshKeyboardLayoutInterval = 2000; -const USHORT mouseDownFlags = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN | +const long DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL = 2000; +const wchar_t *const DETECT_WINCLASS = L"EspansoDetect"; +const USHORT MOUSE_DOWN_FLAGS = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN | RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN; -const USHORT mouseUpFlags = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP | +const USHORT MOUSE_UP_FLAGS = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP | RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP | RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP; -DWORD lastKeyboardPressTick = 0; -HKL currentKeyboardLayout; -HWND window; -const wchar_t *const winclass = L"Espanso"; +typedef struct { + HKL current_keyboard_layout; + DWORD last_key_press_tick; -void *self = NULL; -EventCallback event_callback = NULL; + // Rust interop + void * rust_instance; + EventCallback event_callback; +} DetectVariables; /* - * Message handler procedure for the windows + * Message handler procedure for the window */ -LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) +LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) { + DetectVariables * variables = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); + switch (msg) { + case WM_DESTROY: + PostQuitMessage(0); + + // Free the window variables + delete variables; + SetWindowLongPtrW(window, GWLP_USERDATA, NULL); + + return 0L; case WM_INPUT: // Message relative to the RAW INPUT events { InputEvent event = {}; @@ -104,7 +117,7 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR DWORD currentTick = GetTickCount(); // If enough time has passed between the last keypress and now, refresh the keyboard layout - if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) + if ((currentTick - variables->last_key_press_tick) > DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL) { // Because keyboard layouts on windows are Window-specific, to get the current @@ -119,11 +132,11 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR // It's not always valid, so update the current value only if available. if (newKeyboardLayout != 0) { - currentKeyboardLayout = newKeyboardLayout; + variables->current_keyboard_layout = newKeyboardLayout; } } - lastKeyboardPressTick = currentTick; + variables->last_key_press_tick = currentTick; } // Get keyboard state ( necessary to decode the associated Unicode char ) @@ -134,7 +147,7 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 UINT flags = 1 << 2; - int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, currentKeyboardLayout); + int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout); // Handle the corresponding string if present if (result >= 1) @@ -187,16 +200,16 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse events { // Make sure the mouse event belongs to the supported ones - if ((raw->data.mouse.usButtonFlags & (mouseDownFlags | mouseUpFlags)) == 0) { + if ((raw->data.mouse.usButtonFlags & (MOUSE_DOWN_FLAGS | MOUSE_UP_FLAGS)) == 0) { return 0; } event.event_type = INPUT_EVENT_TYPE_MOUSE; - if ((raw->data.mouse.usButtonFlags & mouseDownFlags) != 0) + if ((raw->data.mouse.usButtonFlags & MOUSE_DOWN_FLAGS) != 0) { event.status = INPUT_STATUS_PRESSED; - } else if ((raw->data.mouse.usButtonFlags & mouseUpFlags) != 0) { + } else if ((raw->data.mouse.usButtonFlags & MOUSE_UP_FLAGS) != 0) { event.status = INPUT_STATUS_RELEASED; } @@ -221,9 +234,9 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR } // If valid, send the event to the Rust layer - if (event.event_type != 0 && self != NULL && event_callback != NULL) + if (event.event_type != 0 && variables->rust_instance != NULL && variables->event_callback != NULL) { - event_callback(self, event); + variables->event_callback(variables->rust_instance, event); } return 0; @@ -233,17 +246,16 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR } } -int32_t raw_eventloop(void *_self, EventCallback _callback) +void * detect_initialize(void *_self) { - // Initialize the default keyboard layout - currentKeyboardLayout = GetKeyboardLayout(0); + HWND window = NULL; // Initialize the Worker window // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa WNDCLASSEX wndclass = { sizeof(WNDCLASSEX), // cbSize: Size of this structure 0, // style: Class styles - window_procedure, // lpfnWndProc: Pointer to the window procedure + detect_window_procedure, // lpfnWndProc: Pointer to the window procedure 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. @@ -251,16 +263,22 @@ int32_t raw_eventloop(void *_self, EventCallback _callback) LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor. NULL, // hbrBackground: A handle to the class background brush. NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu - winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. + DETECT_WINCLASS, // lpszClassName: A pointer to a null-terminated string or is an atom. NULL // hIconSm: A handle to a small icon that is associated with the window class. }; if (RegisterClassEx(&wndclass)) { + DetectVariables * variables = new DetectVariables(); + variables->rust_instance = _self; + + // Initialize the default keyboard layout + variables->current_keyboard_layout = GetKeyboardLayout(0); + // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw window = CreateWindowEx( 0, // dwExStyle: The extended window style of the window being created. - winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass + DETECT_WINCLASS, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass L"Espanso Worker Window", // lpWindowName: The window name. WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. CW_USEDEFAULT, // X: The initial horizontal position of the window. @@ -273,6 +291,8 @@ int32_t raw_eventloop(void *_self, EventCallback _callback) NULL // lpParam: Pointer to a value to be passed to the window ); + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables)); + // Register raw inputs RAWINPUTDEVICE Rid[2]; @@ -288,22 +308,27 @@ int32_t raw_eventloop(void *_self, EventCallback _callback) if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. - return -1; + return nullptr; } } else { // Something went wrong, error. - return -2; + return nullptr; } - event_callback = _callback; - self = _self; + return window; +} +int32_t detect_eventloop(void * window, EventCallback _callback) +{ if (window) { + DetectVariables * variables = reinterpret_cast(GetWindowLongPtrW((HWND) window, GWLP_USERDATA)); + variables->event_callback = _callback; + // Hide the window - ShowWindow(window, SW_HIDE); + ShowWindow((HWND) window, SW_HIDE); // Enter the Event loop MSG msg; @@ -311,8 +336,11 @@ int32_t raw_eventloop(void *_self, EventCallback _callback) DispatchMessage(&msg); } - event_callback = NULL; - self = NULL; - return 1; +} + +int32_t detect_destroy(void * window) { + if (window) { + return DestroyWindow((HWND) window); + } } \ No newline at end of file diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h index afd5552..eeb6d7a 100644 --- a/espanso-detect/src/win32/native.h +++ b/espanso-detect/src/win32/native.h @@ -60,10 +60,16 @@ typedef struct { int32_t status; } InputEvent; -typedef void (*EventCallback)(void * self, InputEvent data); -extern EventCallback event_callback; +typedef void (*EventCallback)(void * rust_istance, InputEvent data); -// Initialize the Raw Input API and run the event loop. Blocking call. -extern "C" int32_t raw_eventloop(void * self, EventCallback callback); + +// Initialize the Raw Input API and the Window. +extern "C" void * detect_initialize(void * rust_istance); + +// Run the event loop. Blocking call. +extern "C" int32_t detect_eventloop(void * window, EventCallback callback); + +// Destroy the given window. +extern "C" int32_t detect_destroy(void * window); #endif //ESPANSO_DETECT_H \ No newline at end of file diff --git a/espanso-ui/Cargo.toml b/espanso-ui/Cargo.toml new file mode 100644 index 0000000..e1e28ad --- /dev/null +++ b/espanso-ui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "espanso-ui" +version = "0.1.0" +authors = ["Federico Terzi "] +edition = "2018" +build="build.rs" + +[dependencies] +log = "0.4.14" + +[target.'cfg(windows)'.dependencies] +widestring = "0.4.3" +lazycell = "1.3.0" + +[build-dependencies] +cc = "1.0.66" \ No newline at end of file diff --git a/espanso-ui/build.rs b/espanso-ui/build.rs new file mode 100644 index 0000000..8d917fd --- /dev/null +++ b/espanso-ui/build.rs @@ -0,0 +1,57 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[cfg(target_os = "windows")] +fn cc_config() { + println!("cargo:rerun-if-changed=src/win32/native.cpp"); + println!("cargo:rerun-if-changed=src/win32/native.h"); + cc::Build::new() + .cpp(true) + .include("src/win32/native.h") + .file("src/win32/native.cpp") + .compile("espansoui"); + + println!("cargo:rustc-link-lib=static=espansoui"); + println!("cargo:rustc-link-lib=dylib=user32"); + #[cfg(target_env = "gnu")] + println!("cargo:rustc-link-lib=dylib=stdc++"); + #[cfg(target_env = "gnu")] + println!("cargo:rustc-link-lib=dylib=gdiplus"); +} + +#[cfg(target_os = "linux")] +fn cc_config() { + println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); + println!("cargo:rustc-link-lib=static=linuxbridge"); + println!("cargo:rustc-link-lib=dylib=X11"); + println!("cargo:rustc-link-lib=dylib=Xtst"); + println!("cargo:rustc-link-lib=dylib=xdo"); +} + +#[cfg(target_os = "macos")] +fn cc_config() { + println!("cargo:rustc-link-lib=dylib=c++"); + println!("cargo:rustc-link-lib=static=macbridge"); + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=IOKit"); +} + +fn main() { + cc_config(); +} diff --git a/espanso-ui/src/event.rs b/espanso-ui/src/event.rs new file mode 100644 index 0000000..98a298a --- /dev/null +++ b/espanso-ui/src/event.rs @@ -0,0 +1,23 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[derive(Debug, PartialEq, Clone)] +pub enum UIEvent { + TrayIconClick, +} diff --git a/espanso-ui/src/icons.rs b/espanso-ui/src/icons.rs new file mode 100644 index 0000000..5fea61e --- /dev/null +++ b/espanso-ui/src/icons.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TrayIcon { + Normal, + Disabled, + + // For example, when macOS activates SecureInput + SystemDisabled, +} diff --git a/espanso-ui/src/lib.rs b/espanso-ui/src/lib.rs new file mode 100644 index 0000000..58e04ee --- /dev/null +++ b/espanso-ui/src/lib.rs @@ -0,0 +1,5 @@ +pub mod event; +pub mod icons; + +#[cfg(target_os = "windows")] +pub mod win32; diff --git a/espanso-ui/src/win32/mod.rs b/espanso-ui/src/win32/mod.rs new file mode 100644 index 0000000..ab9d9e3 --- /dev/null +++ b/espanso-ui/src/win32/mod.rs @@ -0,0 +1,269 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use std::{ + cmp::min, + collections::HashMap, + ffi::c_void, + sync::{ + atomic::{AtomicPtr, Ordering}, + Arc, + }, + thread::ThreadId, +}; + +use lazycell::LazyCell; +use log::{error, trace}; +use widestring::WideString; + +use crate::{event::UIEvent, icons::TrayIcon}; + +// IMPORTANT: if you change these, also edit the native.h file. +const MAX_FILE_PATH: usize = 260; +const MAX_ICON_COUNT: usize = 3; + +const UI_EVENT_TYPE_ICON_CLICK: i32 = 1; +//const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2; + +// Take a look at the native.h header file for an explanation of the fields +#[repr(C)] +pub struct RawUIOptions { + pub show_icon: i32, + + pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT], + pub icon_paths_count: i32, +} +// Take a look at the native.h header file for an explanation of the fields +#[repr(C)] +pub struct RawUIEvent { + pub event_type: i32, +} + +#[allow(improper_ctypes)] +#[link(name = "espansoui", kind = "static")] +extern "C" { + pub fn ui_initialize(_self: *const Win32EventLoop, options: RawUIOptions) -> *mut c_void; + pub fn ui_eventloop( + window_handle: *const c_void, + event_callback: extern "C" fn(_self: *mut Win32EventLoop, event: RawUIEvent), + ) -> i32; + pub fn ui_destroy(window_handle: *const c_void) -> i32; + pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32); +} + +pub struct Win32UIOptions<'a> { + pub show_icon: bool, + pub icon_paths: &'a Vec<(TrayIcon, String)>, +} + +pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) { + let handle: Arc> = Arc::new(AtomicPtr::new(std::ptr::null_mut())); + + // Validate icons + if options.icon_paths.len() > MAX_ICON_COUNT { + panic!("Win32 UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more"); + } + + // Convert the icon paths to the internal representation + let mut icon_indexes: HashMap = HashMap::new(); + let mut icons = Vec::new(); + for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() { + icon_indexes.insert(tray_icon.clone(), index); + icons.push(path.clone()); + } + + let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon); + let remote = Win32Remote::new(handle, icon_indexes); + + (remote, eventloop) +} + +pub type Win32UIEventCallback = Box; + +pub struct Win32EventLoop { + handle: Arc>, + + show_icon: bool, + icons: Vec, + + // Internal + _event_callback: LazyCell, + _init_thread_id: LazyCell, +} + +impl Win32EventLoop { + pub(crate) fn new(handle: Arc>, icons: Vec, show_icon: bool) -> Self { + Self { + handle, + icons, + show_icon, + _event_callback: LazyCell::new(), + _init_thread_id: LazyCell::new(), + } + } + + pub fn initialize(&mut self) { + let window_handle = self.handle.load(Ordering::Acquire); + if !window_handle.is_null() { + panic!("Attempt to initialize Win32EventLoop on non-null window handle"); + } + + // Convert the icon paths to the raw representation + let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] = + [[0; MAX_FILE_PATH]; MAX_ICON_COUNT]; + for i in 0..self.icons.len() { + let wide_path = WideString::from_str(&self.icons[i]); + let len = min(wide_path.len(), MAX_FILE_PATH - 1); + icon_paths[i][0..len].clone_from_slice(&wide_path.as_slice()[..len]); + // TODO: test overflow, correct case + } + + let options = RawUIOptions { + show_icon: if self.show_icon { 1 } else { 0 }, + icon_paths, + icon_paths_count: self.icons.len() as i32, + }; + + let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options) }; + + if handle.is_null() { + panic!("Unable to initialize Win32EventLoop"); + } + + self.handle.store(handle, Ordering::Release); + + // TODO: explain + self + ._init_thread_id + .fill(std::thread::current().id()) + .expect("Unable to set initialization thread id"); + } + + pub fn run(&self, event_callback: Win32UIEventCallback) { + // Make sure the run() method is called in the same thread as initialize() + if let Some(init_id) = self._init_thread_id.borrow() { + if init_id != &std::thread::current().id() { + panic!("Win32EventLoop run() and initialize() methods should be called in the same thread"); + // TODO: test + } + } + + let window_handle = self.handle.load(Ordering::Acquire); + if window_handle.is_null() { + panic!("Attempt to run Win32EventLoop on a null window handle"); + // TODO: test + } + + if let Err(_) = self._event_callback.fill(event_callback) { + panic!("Unable to set Win32EventLoop callback"); + } + + extern "C" fn callback(_self: *mut Win32EventLoop, event: RawUIEvent) { + if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } { + let event: Option = event.into(); + if let Some(event) = event { + callback(event) + } else { + trace!("Unable to convert raw event to input event"); + } + } + } + + let error_code = unsafe { ui_eventloop(window_handle, callback) }; + + if error_code <= 0 { + panic!("Win32EventLoop exited with <= 0 code") + } + } +} + +impl Drop for Win32EventLoop { + fn drop(&mut self) { + let handle = self.handle.swap(std::ptr::null_mut(), Ordering::Acquire); + if handle.is_null() { + error!("Win32EventLoop destruction cannot be performed, handle is null"); + return; + } + + let result = unsafe { ui_destroy(handle) }; + + if result != 0 { + error!("Win32EventLoop destruction returned non-zero code"); + } + } +} + +pub struct Win32Remote { + handle: Arc>, + + // Maps icon name to their index + icon_indexes: HashMap, +} + +impl Win32Remote { + pub(crate) fn new( + handle: Arc>, + icon_indexes: HashMap, + ) -> Self { + Self { + handle, + icon_indexes, + } + } + + pub fn update_tray_icon(&self, icon: TrayIcon) { + let handle = self.handle.load(Ordering::Acquire); + if handle.is_null() { + error!("Unable to update tray icon, pointer is null"); + return; + } + + if let Some(index) = self.icon_indexes.get(&icon) { + unsafe { ui_update_tray_icon(handle, (*index) as i32) } + } else { + error!("Unable to update tray icon, invalid icon id"); + return; + } + } +} + +impl From for Option { + fn from(raw: RawUIEvent) -> Option { + match raw.event_type { + // Keyboard events + UI_EVENT_TYPE_ICON_CLICK => { + return Some(UIEvent::TrayIconClick); + } + _ => {} + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constants_are_not_changed_by_mistake() { + assert_eq!(MAX_FILE_PATH, 260); + assert_eq!(MAX_ICON_COUNT, 3); + } +} diff --git a/espanso-ui/src/win32/native.cpp b/espanso-ui/src/win32/native.cpp new file mode 100644 index 0000000..3216200 --- /dev/null +++ b/espanso-ui/src/win32/native.cpp @@ -0,0 +1,276 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#include "native.h" +#include +#include +#include +#include +#include +#include + +#define UNICODE + +#ifdef __MINGW32__ +#ifndef WINVER +#define WINVER 0x0606 +#endif +#define STRSAFE_NO_DEPRECATE +#endif + +#include +#include +#include +#pragma comment(lib, "Shell32.lib") +#include + +#pragma comment(lib, "Gdi32.lib") +#include + +#define APPWM_ICON_CLICK (WM_APP + 1) +#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2) +#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3) + +const wchar_t *const ui_winclass = L"EspansoUI"; + +typedef struct { + UIOptions options; + NOTIFYICONDATA nid; + HICON g_icons[MAX_ICON_COUNT]; + + // Rust interop + void *rust_instance; + EventCallback event_callback; +} UIVariables; + + +// Needed to detect when Explorer crashes +UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); + +/* + * Message handler procedure for the window + */ +LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) +{ + UIEvent event = {}; + UIVariables * variables = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA)); + + switch (msg) + { + case WM_DESTROY: + PostQuitMessage(0); + + // Free the tray icons + for (int i = 0; i < variables->options.icon_paths_count; i++) + { + DeleteObject(variables->g_icons[i]); + } + + // Free the window variables + delete variables; + SetWindowLongPtrW(window, GWLP_USERDATA, NULL); + + return 0L; + case WM_COMMAND: // Click on the tray icon context menu + { + UINT idItem = (UINT)LOWORD(wp); + UINT flags = (UINT)HIWORD(wp); + + if (flags == 0) + { + std::cout << "click menu" << std::flush; + //context_menu_click_callback(manager_instance, (int32_t)idItem); + } + + break; + } + case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu + { + HMENU hPopupMenu = CreatePopupMenu(); + + // Create the menu + + /* + int32_t count = static_cast(lp); + std::unique_ptr items(reinterpret_cast(wp)); + + for (int i = 0; i= variables->options.icon_paths_count) { + break; + } + + variables->nid.hIcon = variables->g_icons[index]; + if (variables->options.show_icon) { + Shell_NotifyIcon(NIM_MODIFY, &variables->nid); + } + + break; + } + case APPWM_ICON_CLICK: // Click on the tray icon + { + switch (lp) + { + case WM_LBUTTONUP: + case WM_RBUTTONUP: + event.event_type = UI_EVENT_TYPE_ICON_CLICK; + if (variables->event_callback && variables->rust_instance) + { + variables->event_callback(variables->rust_instance, event); + } + break; + } + } + default: + if (msg == WM_TASKBARCREATED) + { // Explorer crashed, recreate the icon + if (variables->options.show_icon) + { + Shell_NotifyIcon(NIM_ADD, &variables->nid); + } + } + return DefWindowProc(window, msg, wp, lp); + } +} + +void * ui_initialize(void *_self, UIOptions _options) { + HWND window = NULL; + + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + + // Service Window + + // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa + WNDCLASSEX uiwndclass = { + sizeof(WNDCLASSEX), // cbSize: Size of this structure + 0, // style: Class styles + ui_window_procedure, // lpfnWndProc: Pointer to the window procedure + 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure + 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. + GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. + NULL, // hIcon: A handle to the class icon. + LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor. + NULL, // hbrBackground: A handle to the class background brush. + NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu + ui_winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. + NULL // hIconSm: A handle to a small icon that is associated with the window class. + }; + + if (RegisterClassEx(&uiwndclass)) + { + // Initialize the service window + window = CreateWindowEx( + 0, // dwExStyle: The extended window style of the window being created. + ui_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass + L"Espanso UI Window", // lpWindowName: The window name. + WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. + CW_USEDEFAULT, // X: The initial horizontal position of the window. + CW_USEDEFAULT, // Y: The initial vertical position of the window. + 100, // nWidth: The width, in device units, of the window. + 100, // nHeight: The height, in device units, of the window. + NULL, // hWndParent: handle to the parent or owner window of the window being created. + NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. + GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. + NULL // lpParam: Pointer to a value to be passed to the window + ); + + if (window) + { + UIVariables * variables = new UIVariables(); + variables->options = _options; + variables->rust_instance = _self; + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables)); + + // Load the tray icons + for (int i = 0; i < variables->options.icon_paths_count; i++) + { + variables->g_icons[i] = (HICON)LoadImage(NULL, variables->options.icon_paths[i], IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); + } + + // Hide the window + ShowWindow(window, SW_HIDE); + + // Setup the icon in the notification space + SendMessage(window, WM_SETICON, ICON_BIG, (LPARAM)variables->g_icons[0]); + SendMessage(window, WM_SETICON, ICON_SMALL, (LPARAM)variables->g_icons[0]); + + // Notification + variables->nid.cbSize = sizeof(variables->nid); + variables->nid.hWnd = window; + variables->nid.uID = 1; + variables->nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; + variables->nid.uCallbackMessage = APPWM_ICON_CLICK; + variables->nid.hIcon = variables->g_icons[0]; + StringCchCopyW(variables->nid.szTip, ARRAYSIZE(variables->nid.szTip), L"espanso"); + + // Show the tray icon + if (variables->options.show_icon) + { + Shell_NotifyIcon(NIM_ADD, &variables->nid); + } + } + } + + return window; +} + +int32_t ui_eventloop(void * window, EventCallback _callback) +{ + if (window) + { + UIVariables * variables = reinterpret_cast(GetWindowLongPtrW((HWND) window, GWLP_USERDATA)); + variables->event_callback = _callback; + + // Enter the Event loop + MSG msg; + while (GetMessage(&msg, 0, 0, 0)) + DispatchMessage(&msg); + } + + return 1; +} + +int32_t ui_destroy(void * window) { + if (window) { + return DestroyWindow((HWND) window); + } +} + +void ui_update_tray_icon(void * window, int32_t index) +{ + if (window) { + PostMessage((HWND) window, APPWM_UPDATE_TRAY_ICON, 0, index); + } +} \ No newline at end of file diff --git a/espanso-ui/src/win32/native.h b/espanso-ui/src/win32/native.h new file mode 100644 index 0000000..4c91acd --- /dev/null +++ b/espanso-ui/src/win32/native.h @@ -0,0 +1,59 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#ifndef ESPANSO_UI_H +#define ESPANSO_UI_H + +#include + +// Explicitly define this constant as we need to use it from the Rust side +// https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +#define MAX_FILE_PATH 260 +#define MAX_ICON_COUNT 3 + +#define UI_EVENT_TYPE_ICON_CLICK 1 +#define UI_EVENT_TYPE_CONTEXT_MENU_CLICK 2 + +typedef struct { + int32_t show_icon; + + wchar_t icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH]; + int32_t icon_paths_count; +} UIOptions; + +typedef struct { + int32_t event_type; +} UIEvent; + +typedef void (*EventCallback)(void * self, UIEvent data); + +// Initialize the hidden UI window, the tray icon and returns the window handle. +extern "C" void * ui_initialize(void * self, UIOptions options); + +// Run the event loop. Blocking call. +extern "C" int32_t ui_eventloop(void * window, EventCallback callback); + +// Destroy the given window. +extern "C" int32_t ui_destroy(void * window); + +// Updates the tray icon to the given one. The method accepts an index that refers to +// the icon within the UIOptions.icon_paths array. +extern "C" void ui_update_tray_icon(void * window, int32_t index); + +#endif //ESPANSO_UI_H \ No newline at end of file diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 396144a..f85b5c1 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -9,4 +9,6 @@ homepage = "https://github.com/federico-terzi/espanso" edition = "2018" [dependencies] -espanso-detect = { path = "../espanso-detect" } \ No newline at end of file +espanso-detect = { path = "../espanso-detect" } +espanso-ui = { path = "../espanso-ui" } +maplit = "1.0.2" \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs index f4802c0..a5663dd 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -1,8 +1,42 @@ +use espanso_detect::event::InputEvent; + fn main() { println!("Hello, world!z"); - let source = espanso_detect::win32::Win32Source::new(Box::new(|event| { + let icon_paths = vec![ + ( + espanso_ui::icons::TrayIcon::Normal, + r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(), + ), + ( + espanso_ui::icons::TrayIcon::Disabled, + r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(), + ), + ]; + + let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions { + show_icon: true, + icon_paths: &icon_paths, + }); + + std::thread::spawn(move || { + let mut source = espanso_detect::win32::Win32Source::new(); + source.initialize(); + source.eventloop(Box::new(move |event: InputEvent| { println!("ev {:?}", event); - })); - source.eventloop(); + match event { + InputEvent::Mouse(_) => {} + InputEvent::Keyboard(evt) => { + if evt.key == espanso_detect::event::Key::Shift { + remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled); + } + } + } + })); + }); + + eventloop.initialize(); + eventloop.run(Box::new(|event| { + println!("ui {:?}", event); + })) }