Desktop: Mac fix keyboard input (#3371)

* add browser debug port env

* mac use option as alt

* fix cef texture double sRGB conversion by using cef-rs fork with fix

* fix keyboard input on mac

* add missing frontend messages

* fixup

* fix keyboard input mac

* dbg

* re implement keyboard mapping

Co-authored-by: csmoe <csmoe@msn.com>

* Fix double arrow keys

* try fix for non mac

* test

* Revert "test"

This reverts commit c7cde9c597.

* fix mac

* some cleanup

* fix zoom in shortcut on mac introduced in #3377

* disable menu shortcut display

* fixup
This commit is contained in:
Timon
2025-11-17 11:51:59 +00:00
committed by GitHub
parent 181c30bc0a
commit e751979e5c
14 changed files with 365 additions and 383 deletions

50
Cargo.lock generated
View File

@@ -725,14 +725,13 @@ dependencies = [
[[package]]
name = "cef"
version = "141.4.0+141.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05b0ae455c0cf0693da242b3336586cd0057702c0516731d8ba72962559a625"
source = "git+https://github.com/timon-schelling/cef-rs.git?rev=98493a182928f1ff8d5bf8b9eea61483235df75d#98493a182928f1ff8d5bf8b9eea61483235df75d"
dependencies = [
"ash",
"cef-dll-sys",
"libc",
"libloading",
"metal 0.32.0",
"metal",
"objc",
"objc2-io-surface",
"thiserror 2.0.16",
@@ -744,9 +743,8 @@ dependencies = [
[[package]]
name = "cef-dll-sys"
version = "141.6.0+141.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95e419bbb8b2190df8b6eb3934ecc4137533803fcc5ae2b076e591ee789071"
version = "141.4.0+141.0.9"
source = "git+https://github.com/timon-schelling/cef-rs.git?rev=98493a182928f1ff8d5bf8b9eea61483235df75d#98493a182928f1ff8d5bf8b9eea61483235df75d"
dependencies = [
"anyhow",
"cmake",
@@ -1016,17 +1014,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
@@ -1337,8 +1324,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "download-cef"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98178d9254efef0f69c1f584713d69c790ec00668cd98f783a5085fbefdbddc"
source = "git+https://github.com/timon-schelling/cef-rs.git?rev=98493a182928f1ff8d5bf8b9eea61483235df75d#98493a182928f1ff8d5bf8b9eea61483235df75d"
dependencies = [
"bzip2",
"clap",
@@ -2302,22 +2288,17 @@ dependencies = [
name = "graphite-desktop"
version = "0.1.0"
dependencies = [
"ash",
"bytemuck",
"cef",
"cef-dll-sys",
"clap",
"core-foundation 0.10.1",
"derivative",
"dirs",
"futures",
"glam",
"graphite-desktop-embedded-resources",
"graphite-desktop-wrapper",
"libc",
"metal 0.31.0",
"muda",
"objc",
"open",
"rand 0.9.2",
"rfd",
@@ -3422,21 +3403,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "metal"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
dependencies = [
"bitflags 2.9.3",
"block",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
"log",
"objc",
"paste",
]
[[package]]
name = "metal"
version = "0.32.0"
@@ -3445,7 +3411,7 @@ checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
dependencies = [
"bitflags 2.9.3",
"block",
"core-graphics-types 0.2.0",
"core-graphics-types",
"foreign-types 0.5.0",
"log",
"objc",
@@ -6893,7 +6859,7 @@ dependencies = [
"bytemuck",
"cfg-if",
"cfg_aliases",
"core-graphics-types 0.2.0",
"core-graphics-types",
"glow",
"glutin_wgl_sys",
"gpu-alloc",
@@ -6905,7 +6871,7 @@ dependencies = [
"libc",
"libloading",
"log",
"metal 0.32.0",
"metal",
"naga 27.0.3",
"ndk-sys",
"objc",

View File

@@ -188,8 +188,9 @@ iai-callgrind = { version = "0.16" }
ndarray = "0.16"
strum = { version = "0.27", features = ["derive"] }
dirs = "6.0"
cef = { version = "141", features = ["accelerated_osr"] }
cef-dll-sys = "141"
# TODO: remove fork usage when https://github.com/tauri-apps/cef-rs/pull/272 is merged and published
cef = { git = "https://github.com/timon-schelling/cef-rs.git", rev = "98493a182928f1ff8d5bf8b9eea61483235df75d" }
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", rev = "98493a182928f1ff8d5bf8b9eea61483235df75d" }
include_dir = "0.7"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1"

View File

@@ -17,12 +17,7 @@ default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
gpu = ["graphite-desktop-wrapper/gpu"]
# Hardware acceleration features
accelerated_paint = ["accelerated_paint_dmabuf", "accelerated_paint_d3d11", "accelerated_paint_iosurface"]
accelerated_paint_dmabuf = ["libc", "ash"]
accelerated_paint_d3d11 = ["windows", "ash"]
accelerated_paint_iosurface = ["metal", "objc", "core-foundation"]
accelerated_paint = ["cef/accelerated_osr"]
[dependencies]
# Local dependencies
@@ -49,32 +44,19 @@ rand = { workspace = true, features = ["thread_rng"] }
serde = { workspace = true }
clap = { workspace = true, features = ["derive"] }
# Hardware acceleration dependencies
ash = { version = "0.38", optional = true }
# Windows-specific dependencies
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D12",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_Controls",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_HiDpi",
], optional = true }
] }
# macOS-specific dependencies
[target.'cfg(target_os = "macos")'.dependencies]
muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false }
metal = { version = "0.31.0", optional = true }
objc = { version = "0.2", optional = true }
core-foundation = { version = "0.10", optional = true }
# Linux-specific dependencies
[target.'cfg(target_os = "linux")'.dependencies]
libc = { version = "0.2", optional = true }

View File

@@ -1,29 +1,30 @@
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
use std::time::Instant;
use winit::dpi::PhysicalPosition;
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
mod keymap;
use keymap::{ToNativeKeycode, ToVKBits};
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
use super::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
mod state;
pub(crate) use state::InputState;
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
match event {
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => {
input_state.cursor_move(&position);
input_state.cursor_move(position);
let Some(host) = browser.host() else { return };
host.send_mouse_move_event(Some(&input_state.into()), 0);
}
WindowEvent::PointerLeft { position, .. } => {
if let Some(position) = position {
input_state.cursor_move(&position);
input_state.cursor_move(position);
}
let Some(host) = browser.host() else { return };
host.send_mouse_move_event(Some(&input_state.into()), 1);
host.send_mouse_move_event(Some(&(input_state.into())), 1);
}
WindowEvent::PointerButton { state, button, .. } => {
let mouse_button = match button {
@@ -64,70 +65,45 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
input_state.modifiers_changed(&modifiers.state());
}
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
let (named_key, character) = match &event.logical_key {
winit::keyboard::Key::Named(named_key) => (
Some(named_key),
match named_key {
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
_ => None,
},
),
winit::keyboard::Key::Character(str) => {
let char = str.chars().next().unwrap_or('\0');
(None, Some(char))
}
_ => return,
};
let native_key_code = event.physical_key.to_native_keycode();
let modifiers = input_state.cef_modifiers(&event.location, event.repeat).raw();
let Some(host) = browser.host() else { return };
let mut key_event = KeyEvent {
size: size_of::<KeyEvent>(),
modifiers,
type_: match (event.state, &event.logical_key) {
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR,
(ElementState::Pressed, _) => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
(ElementState::Released, _) => cef_key_event_type_t::KEYEVENT_KEYUP,
}
.into(),
..Default::default()
};
if let Some(named_key) = named_key {
key_event.windows_key_code = named_key.to_vk_bits();
} else if let Some(char) = character {
key_event.windows_key_code = char.to_vk_bits();
key_event.modifiers = input_state.cef_modifiers(&event.location, event.repeat).raw();
key_event.windows_key_code = match &event.logical_key {
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
_ => 0,
};
key_event.native_key_code = event.physical_key.to_native_keycode();
key_event.character = event.logical_key.to_char_representation() as u16;
key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16;
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() {
key_event.unmodified_character = 1;
}
key_event.native_key_code = native_key_code;
if key_event.type_ == cef_key_event_type_t::KEYEVENT_CHAR.into() {
let mut key_down_event = key_event.clone();
key_down_event.type_ = cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into();
host.send_key_event(Some(&key_down_event));
let Some(host) = browser.host() else { return };
match event.state {
ElementState::Pressed => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
host.send_key_event(Some(&key_event));
if let Some(char) = character {
let mut char_key_event = KeyEvent {
size: size_of::<KeyEvent>(),
modifiers,
is_system_key: 0,
..Default::default()
};
let mut buf = [0; 2];
char.encode_utf16(&mut buf);
char_key_event.windows_key_code = buf[0] as i32;
char_key_event.character = buf[0];
char_key_event.native_key_code = native_key_code;
let mut buf = [0; 2];
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
char_key_event.unmodified_character = buf[0];
char_key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_CHAR);
host.send_key_event(Some(&char_key_event));
}
}
ElementState::Released => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_KEYUP);
host.send_key_event(Some(&key_event));
}
key_event.windows_key_code = event.logical_key.to_char_representation() as i32;
}
host.send_key_event(Some(&key_event));
}
WindowEvent::PinchGesture { delta, .. } => {
if !delta.is_normal() {
@@ -146,237 +122,3 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
_ => {}
}
}
#[derive(Default)]
pub(crate) struct InputState {
modifiers: winit::keyboard::ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
mouse_click_tracker: ClickTracker,
}
impl InputState {
fn modifiers_changed(&mut self, modifiers: &winit::keyboard::ModifiersState) {
self.modifiers = *modifiers;
}
fn cursor_move(&mut self, position: &PhysicalPosition<f64>) {
self.mouse_position = position.into();
}
fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
self.mouse_state.update(button, state);
self.mouse_click_tracker.input(button, state, self.mouse_position)
}
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(self, location, is_repeat)
}
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<InputState> for CefModifiers {
fn from(val: InputState) -> Self {
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<&InputState> for MouseEvent {
fn from(val: &InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_modifiers_mouse_event().raw(),
}
}
}
impl From<&mut InputState> for MouseEvent {
fn from(val: &mut InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_modifiers_mouse_event().raw(),
}
}
}
#[derive(Default, Clone, Copy)]
pub(crate) struct MousePosition {
x: usize,
y: usize,
}
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as usize,
y: position.y as usize,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
impl MouseState {
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
match state {
ElementState::Pressed => match button {
MouseButton::Left => self.left = true,
MouseButton::Right => self.right = true,
MouseButton::Middle => self.middle = true,
_ => {}
},
ElementState::Released => match button {
MouseButton::Left => self.left = false,
MouseButton::Right => self.right = false,
MouseButton::Middle => self.middle = false,
_ => {}
},
}
}
}
#[derive(Default)]
struct ClickTracker {
left: Option<ClickRecord>,
middle: Option<ClickRecord>,
right: Option<ClickRecord>,
}
impl ClickTracker {
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
let record = match button {
MouseButton::Left => &mut self.left,
MouseButton::Right => &mut self.right,
MouseButton::Middle => &mut self.middle,
_ => return ClickCount::Single,
};
let Some(record) = record else {
*record = Some(ClickRecord { position, ..Default::default() });
return ClickCount::Single;
};
let prev_time = record.time;
let prev_position = record.position;
let now = Instant::now();
record.time = now;
record.position = position;
match state {
ElementState::Pressed if record.down_count == ClickCount::Double => {
*record = ClickRecord {
down_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
ElementState::Released if record.up_count == ClickCount::Double => {
*record = ClickRecord {
up_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
_ => {}
}
let dx = position.x.abs_diff(prev_position.x);
let dy = position.y.abs_diff(prev_position.y);
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
let within_time = now.saturating_duration_since(prev_time) <= MULTICLICK_TIMEOUT;
let count = if within_time && within_dist { ClickCount::Double } else { ClickCount::Single };
*record = match state {
ElementState::Pressed => ClickRecord { down_count: count, ..*record },
ElementState::Released => ClickRecord { up_count: count, ..*record },
};
count
}
}
#[derive(Clone, Copy, PartialEq, Default)]
enum ClickCount {
#[default]
Single,
Double,
}
impl From<ClickCount> for i32 {
fn from(count: ClickCount) -> i32 {
match count {
ClickCount::Single => 1,
ClickCount::Double => 2,
}
}
}
#[derive(Clone, Copy)]
struct ClickRecord {
time: Instant,
position: MousePosition,
down_count: ClickCount,
up_count: ClickCount,
}
impl Default for ClickRecord {
fn default() -> Self {
Self {
time: Instant::now(),
position: Default::default(),
down_count: Default::default(),
up_count: Default::default(),
}
}
}
struct CefModifiers(u32);
impl CefModifiers {
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
let mut inner = 0;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input_state.modifiers.meta_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
}
inner |= match location {
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
winit::keyboard::KeyLocation::Standard => 0,
};
Self(inner)
}
fn raw(&self) -> u32 {
self.0
}
}

View File

@@ -1,23 +1,44 @@
pub trait ToNativeKeycode {
use winit::keyboard::{Key, NamedKey, PhysicalKey};
pub(crate) trait ToCharRepresentation {
fn to_char_representation(&self) -> char;
}
impl ToCharRepresentation for Key {
fn to_char_representation(&self) -> char {
match self {
Key::Named(named) => match named {
NamedKey::Tab => '\t',
NamedKey::Enter => '\r',
NamedKey::Backspace => '\x08',
NamedKey::Escape => '\x1b',
_ => '\0',
},
Key::Character(char) => char.chars().next().unwrap_or_default(),
_ => '\0',
}
}
}
pub(crate) trait ToNativeKeycode {
fn to_native_keycode(&self) -> i32;
}
impl ToNativeKeycode for winit::keyboard::PhysicalKey {
impl ToNativeKeycode for PhysicalKey {
fn to_native_keycode(&self) -> i32 {
use winit::platform::scancode::PhysicalKeyExtScancode;
#[cfg(target_os = "linux")]
{
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or(0)
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
self.to_scancode().map(|c| c as i32).unwrap_or(0)
self.to_scancode().map(|c| c as i32).unwrap_or_default()
}
}
}
// Windows Virtual keyboard binary representation
pub(crate) trait ToVKBits {
fn to_vk_bits(&self) -> i32;
}
@@ -32,10 +53,8 @@ macro_rules! map_enum {
}
};
}
impl ToVKBits for winit::keyboard::NamedKey {
fn to_vk_bits(&self) -> i32 {
use winit::keyboard::NamedKey;
map_enum!(
self,
NamedKey,
@@ -49,10 +68,10 @@ impl ToVKBits for winit::keyboard::NamedKey {
(0x5B, Meta),
(0x0D, Enter),
(0x09, Tab),
(0x28, ArrowDown),
(0x25, ArrowLeft),
(0x27, ArrowRight),
(0x26, ArrowUp),
(0x27, ArrowRight),
(0x28, ArrowDown),
(0x23, End),
(0x24, Home),
(0x22, PageDown),
@@ -152,7 +171,6 @@ macro_rules! map {
}
};
}
impl ToVKBits for char {
fn to_vk_bits(&self) -> i32 {
map!(

View File

@@ -0,0 +1,242 @@
use cef::MouseEvent;
use std::time::Instant;
use winit::dpi::PhysicalPosition;
use winit::event::{ElementState, MouseButton};
use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
#[derive(Default)]
pub(crate) struct InputState {
modifiers: winit::keyboard::ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
mouse_click_tracker: ClickTracker,
}
impl InputState {
pub(crate) fn modifiers_changed(&mut self, modifiers: &winit::keyboard::ModifiersState) {
self.modifiers = *modifiers;
}
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) {
self.mouse_position = position.into();
}
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
self.mouse_state.update(button, state);
self.mouse_click_tracker.input(button, state, self.mouse_position)
}
pub(crate) fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(self, location, is_repeat)
}
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<InputState> for CefModifiers {
fn from(val: InputState) -> Self {
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<&InputState> for MouseEvent {
fn from(val: &InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_mouse_modifiers().raw(),
}
}
}
impl From<&mut InputState> for MouseEvent {
fn from(val: &mut InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_mouse_modifiers().raw(),
}
}
}
#[derive(Default, Clone, Copy)]
pub(crate) struct MousePosition {
x: usize,
y: usize,
}
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as usize,
y: position.y as usize,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
impl MouseState {
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
match state {
ElementState::Pressed => match button {
MouseButton::Left => self.left = true,
MouseButton::Right => self.right = true,
MouseButton::Middle => self.middle = true,
_ => {}
},
ElementState::Released => match button {
MouseButton::Left => self.left = false,
MouseButton::Right => self.right = false,
MouseButton::Middle => self.middle = false,
_ => {}
},
}
}
}
#[derive(Default)]
struct ClickTracker {
left: Option<ClickRecord>,
middle: Option<ClickRecord>,
right: Option<ClickRecord>,
}
impl ClickTracker {
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
let record = match button {
MouseButton::Left => &mut self.left,
MouseButton::Right => &mut self.right,
MouseButton::Middle => &mut self.middle,
_ => return ClickCount::Single,
};
let Some(record) = record else {
*record = Some(ClickRecord { position, ..Default::default() });
return ClickCount::Single;
};
let prev_time = record.time;
let prev_position = record.position;
let now = Instant::now();
record.time = now;
record.position = position;
match state {
ElementState::Pressed if record.down_count == ClickCount::Double => {
*record = ClickRecord {
down_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
ElementState::Released if record.up_count == ClickCount::Double => {
*record = ClickRecord {
up_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
_ => {}
}
let dx = position.x.abs_diff(prev_position.x);
let dy = position.y.abs_diff(prev_position.y);
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
let within_time = now.saturating_duration_since(prev_time) <= MULTICLICK_TIMEOUT;
let count = if within_time && within_dist { ClickCount::Double } else { ClickCount::Single };
*record = match state {
ElementState::Pressed => ClickRecord { down_count: count, ..*record },
ElementState::Released => ClickRecord { up_count: count, ..*record },
};
count
}
}
#[derive(Clone, Copy, PartialEq, Default)]
pub(crate) enum ClickCount {
#[default]
Single,
Double,
}
impl From<ClickCount> for i32 {
fn from(count: ClickCount) -> i32 {
match count {
ClickCount::Single => 1,
ClickCount::Double => 2,
}
}
}
#[derive(Clone, Copy)]
struct ClickRecord {
time: Instant,
position: MousePosition,
down_count: ClickCount,
up_count: ClickCount,
}
impl Default for ClickRecord {
fn default() -> Self {
Self {
time: Instant::now(),
position: Default::default(),
down_count: Default::default(),
up_count: Default::default(),
}
}
}
pub(crate) struct CefModifiers(u32);
impl CefModifiers {
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
use cef::sys::cef_event_flags_t;
let mut inner = 0;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input_state.modifiers.meta_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
}
inner |= match location {
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
winit::keyboard::KeyLocation::Standard => 0,
};
Self(inner)
}
pub(crate) fn raw(&self) -> u32 {
self.0
}
}

View File

@@ -72,6 +72,14 @@ impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
// Hide user prompt asking for keychain access
cmd.append_switch(Some(&CefString::from("use-mock-keychain")));
}
// Enable browser debugging via environment variable
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
&& let Some(port) = env.parse::<u16>().ok()
{
cmd.append_switch_with_value(Some(&CefString::from("remote-debugging-port")), Some(&CefString::from(port.to_string().as_str())));
cmd.append_switch_with_value(Some(&CefString::from("remote-allow-origins")), Some(&CefString::from("*")));
}
}
}

View File

@@ -1,4 +1,5 @@
use winit::event_loop::ActiveEventLoop;
use winit::platform::macos::WindowAttributesMacOS;
use winit::window::{Window, WindowAttributes};
use crate::consts::APP_NAME;
@@ -11,7 +12,7 @@ pub(super) struct NativeWindowImpl {
impl super::NativeWindow for NativeWindowImpl {
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
let mac_window = winit::platform::macos::WindowAttributesMacOS::default()
let mac_window = WindowAttributesMacOS::default()
.with_titlebar_transparent(true)
.with_fullsize_content_view(true)
.with_title_hidden(true);

View File

@@ -1,4 +1,6 @@
use winit::event_loop::ActiveEventLoop;
use winit::icon::Icon;
use winit::platform::windows::WinIcon;
use winit::window::{Window, WindowAttributes};
use crate::event::AppEventScheduler;
@@ -9,8 +11,8 @@ pub(super) struct NativeWindowImpl {
impl super::NativeWindow for NativeWindowImpl {
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
if let Ok(win_icon) = winit::platform::windows::WinIcon::from_resource(1, None) {
let icon = winit::icon::Icon(std::sync::Arc::new(win_icon));
if let Ok(win_icon) = WinIcon::from_resource(1, None) {
let icon = Icon(std::sync::Arc::new(win_icon));
attributes.with_window_icon(Some(icon))
} else {
attributes

View File

@@ -2,6 +2,6 @@ use super::DesktopWrapperMessageDispatcher;
use super::messages::EditorMessage;
pub(super) fn intercept_editor_message(_dispatcher: &mut DesktopWrapperMessageDispatcher, message: EditorMessage) -> Option<EditorMessage> {
// TODO: remove it turns out to be unnecessary
// TODO: remove if it turns out to be unnecessary
Some(message)
}

View File

@@ -164,7 +164,8 @@ fn convert_menu_bar_entry_to_menu_item(
}
let shortcut = match shortcut {
Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => convert_layout_keys_to_shortcut(keys),
//TODO: Reenable shortcuts once a workaround for missing keyboard events is found
Some(ActionKeys::Keys(LayoutKeysGroup(keys))) if false => convert_layout_keys_to_shortcut(keys),
_ => None,
};
@@ -321,6 +322,7 @@ fn convert_layout_keys_to_shortcut(layout_keys: &Vec<LayoutKey>) -> Option<Short
Key::ScrollLock => key = Some(KeyCode::ScrollLock),
Key::Pause => key = Some(KeyCode::Pause),
Key::Unidentified => key = Some(KeyCode::Unidentified),
Key::FakeKeyPlus => key = Some(KeyCode::Equal),
_ => key = None,
}
}

View File

@@ -37,13 +37,16 @@ impl MessageHandler<ViewportMessage, ()> for ViewportMessageHandler {
ViewportMessage::RepropagateUpdate => {}
}
let physical_bounds = self.bounds().to_physical();
responses.add(FrontendMessage::UpdateViewportPhysicalBounds {
x: physical_bounds.x(),
y: physical_bounds.y(),
width: physical_bounds.width(),
height: physical_bounds.height(),
});
#[cfg(not(target_family = "wasm"))]
{
let physical_bounds = self.bounds().to_physical();
responses.add(FrontendMessage::UpdateViewportPhysicalBounds {
x: physical_bounds.x(),
y: physical_bounds.y(),
width: physical_bounds.width(),
height: physical_bounds.height(),
});
}
responses.add(NavigationMessage::CanvasPan { delta: DVec2::ZERO });
responses.add(NodeGraphMessage::SetGridAlignedEdges);

View File

@@ -10,6 +10,7 @@ import {
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerSaveActiveDocument,
TriggerOpenLaunchDocuments,
} from "@graphite/messages";
import { type PortfolioState } from "@graphite/state-providers/portfolio";
@@ -171,6 +172,9 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
editor.subscriptions.subscribeJsMessage(TriggerLoadRestAutoSaveDocuments, async () => {
await loadRestDocuments();
});
editor.subscriptions.subscribeJsMessage(TriggerOpenLaunchDocuments, async () => {
// TODO: Could be used to load documents from URL params or similar on launch
});
editor.subscriptions.subscribeJsMessage(TriggerSaveActiveDocument, async (triggerSaveActiveDocument) => {
const documentId = String(triggerSaveActiveDocument.documentId);
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);

View File

@@ -305,6 +305,13 @@ export class UpdateViewportHolePunch extends JsMessage {
readonly active!: boolean;
}
export class UpdateViewportPhysicalBounds extends JsMessage {
readonly x!: number;
readonly y!: number;
readonly width!: number;
readonly height!: number;
}
export class UpdateInputHints extends JsMessage {
@Type(() => HintInfo)
readonly hintData!: HintData;
@@ -724,6 +731,8 @@ export class UpdateMouseCursor extends JsMessage {
export class TriggerLoadFirstAutoSaveDocument extends JsMessage {}
export class TriggerLoadRestAutoSaveDocuments extends JsMessage {}
export class TriggerOpenLaunchDocuments extends JsMessage {}
export class TriggerLoadPreferences extends JsMessage {}
export class TriggerFetchAndOpenDocument extends JsMessage {
@@ -1653,6 +1662,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerLoadFirstAutoSaveDocument,
TriggerLoadPreferences,
TriggerLoadRestAutoSaveDocuments,
TriggerOpenLaunchDocuments,
TriggerOpenDocument,
TriggerPaste,
TriggerSaveActiveDocument,
@@ -1705,6 +1715,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateViewportHolePunch,
UpdateViewportPhysicalBounds,
UpdateVisibleNodes,
UpdateWirePathInProgress,
UpdateWorkingColorsLayout,