Skip to content

Commit cc3f769

Browse files
MagicalTuxclaude
andcommitted
Fix macOS backend title bar offset using dynamic API
Use window.contentLayoutRect to dynamically calculate the title bar height instead of hardcoding 30 pixels. This ensures correct positioning on all Mac systems regardless of title bar size. Also includes major refactoring of the macOS backend event handling: - Add X11EventQueue for thread-safe event passing from NSView to Rust - Implement proper NSView event handlers (keyDown, mouseDown, etc.) - Use NSApplication.run() for proper event loop processing - Fix coordinate conversion for mouse events - Add proper focus event handling (FocusIn/FocusOut) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 29c8d57 commit cc3f769

File tree

5 files changed

+1186
-278
lines changed

5 files changed

+1186
-278
lines changed

src/backend/macos.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,54 +1286,43 @@ impl MacOSBackend {
12861286
.find(|(_, data)| data.swift_id == window_id)
12871287
.map(|(id, _)| BackendWindow(*id))?;
12881288

1289+
// Event type mapping from Swift backend:
1290+
// 2 = KeyPress, 3 = KeyRelease, 4 = ButtonPress, 5 = ButtonRelease, 6 = MotionNotify
1291+
// 8 = FocusIn, 9 = FocusOut, 10 = EnterNotify, 11 = LeaveNotify
12891292
match event_type {
1290-
1 => Some(BackendEvent::Expose {
1291-
window,
1292-
x: x as u16,
1293-
y: y as u16,
1294-
width: width as u16,
1295-
height: height as u16,
1296-
}),
1297-
2 => Some(BackendEvent::Configure {
1298-
window,
1299-
x: x as i16,
1300-
y: y as i16,
1301-
width: width as u16,
1302-
height: height as u16,
1303-
}),
1304-
3 => Some(BackendEvent::KeyPress {
1293+
2 => Some(BackendEvent::KeyPress {
13051294
window,
13061295
keycode: keycode as u8,
13071296
state: state as u16,
13081297
time: time as u32,
13091298
x: x as i16,
13101299
y: y as i16,
13111300
}),
1312-
4 => Some(BackendEvent::KeyRelease {
1301+
3 => Some(BackendEvent::KeyRelease {
13131302
window,
13141303
keycode: keycode as u8,
13151304
state: state as u16,
13161305
time: time as u32,
13171306
x: x as i16,
13181307
y: y as i16,
13191308
}),
1320-
5 => Some(BackendEvent::ButtonPress {
1309+
4 => Some(BackendEvent::ButtonPress {
13211310
window,
13221311
button: button as u8,
13231312
state: state as u16,
13241313
time: time as u32,
13251314
x: x as i16,
13261315
y: y as i16,
13271316
}),
1328-
6 => Some(BackendEvent::ButtonRelease {
1317+
5 => Some(BackendEvent::ButtonRelease {
13291318
window,
13301319
button: button as u8,
13311320
state: state as u16,
13321321
time: time as u32,
13331322
x: x as i16,
13341323
y: y as i16,
13351324
}),
1336-
7 => Some(BackendEvent::MotionNotify {
1325+
6 => Some(BackendEvent::MotionNotify {
13371326
window,
13381327
state: state as u16,
13391328
time: time as u32,

src/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,16 +344,16 @@ fn main() {
344344
}
345345

346346
// Keep the main thread alive
347-
// On macOS, we need to run the CFRunLoop to service DispatchQueue.main
347+
// On macOS, we need to run NSApplication.run() to process events properly
348348
// On other platforms, this just prevents the main thread from exiting
349349
#[cfg(target_os = "macos")]
350350
{
351351
extern "C" {
352-
fn CFRunLoopRun();
352+
fn macos_backend_run_app();
353353
}
354-
log::info!("Running macOS CFRunLoop on main thread");
354+
log::info!("Running macOS NSApplication event loop on main thread");
355355
unsafe {
356-
CFRunLoopRun();
356+
macos_backend_run_app();
357357
}
358358
}
359359

src/server/listener.rs

Lines changed: 232 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
//! Server listener and connection handling
22
use std::error::Error;
3-
use std::io::{Read, Write};
4-
use std::net::TcpListener;
3+
use std::io::{ErrorKind, Read, Write};
4+
use std::net::{TcpListener, TcpStream};
55
#[cfg(unix)]
6-
use std::os::unix::net::UnixListener;
6+
use std::os::unix::net::{UnixListener, UnixStream};
77
use std::sync::{Arc, Mutex};
88
use std::thread;
9+
use std::time::Duration;
910

1011
use super::Server;
1112
use crate::protocol::setup::{SetupRequest, SetupResponse};
1213

14+
/// Trait for streams that can set read timeouts
15+
trait TimeoutStream: Read + Write {
16+
fn set_read_timeout(&self, dur: Option<Duration>) -> std::io::Result<()>;
17+
}
18+
19+
impl TimeoutStream for TcpStream {
20+
fn set_read_timeout(&self, dur: Option<Duration>) -> std::io::Result<()> {
21+
TcpStream::set_read_timeout(self, dur)
22+
}
23+
}
24+
25+
#[cfg(unix)]
26+
impl TimeoutStream for UnixStream {
27+
fn set_read_timeout(&self, dur: Option<Duration>) -> std::io::Result<()> {
28+
UnixStream::set_read_timeout(self, dur)
29+
}
30+
}
31+
1332
/// Start TCP listener for X11 connections
1433
pub fn start_tcp_listener(
1534
display: u16,
@@ -76,7 +95,7 @@ pub fn start_unix_listener(
7695
Ok(())
7796
}
7897

79-
fn handle_client<S: Read + Write>(
98+
fn handle_client<S: TimeoutStream>(
8099
mut stream: S,
81100
server: Arc<Mutex<Server>>,
82101
) -> Result<(), Box<dyn Error + Send + Sync>> {
@@ -105,12 +124,56 @@ fn handle_client<S: Read + Write>(
105124
// Note: The first request after connection has sequence 1
106125
let mut sequence_number: u16 = 0;
107126

127+
// Set read timeout for event polling
128+
// 10ms allows responsive event delivery while still processing requests quickly
129+
stream.set_read_timeout(Some(Duration::from_millis(10)))?;
130+
131+
// Helper function to send pending events to the client
132+
// The sequence_number is the client's current request sequence - events use this
133+
fn send_pending_events<W: Write>(
134+
stream: &mut W,
135+
server: &Arc<Mutex<Server>>,
136+
client_sequence: u16,
137+
) -> std::io::Result<()> {
138+
// Poll for new events and send any pending events
139+
let events_by_window = {
140+
let mut server = server.lock().unwrap();
141+
server.poll_and_queue_events();
142+
server.take_all_pending_events()
143+
};
144+
145+
// Flatten events from all windows and send them
146+
// Patch the sequence number in each event (bytes 2-3) to match the client's sequence
147+
for (_window, events) in events_by_window {
148+
for mut event_data in events {
149+
// X11 events are 32 bytes, sequence number is at bytes 2-3 (little-endian)
150+
if event_data.len() >= 4 {
151+
let seq_bytes = client_sequence.to_le_bytes();
152+
event_data[2] = seq_bytes[0];
153+
event_data[3] = seq_bytes[1];
154+
}
155+
stream.write_all(&event_data)?;
156+
}
157+
}
158+
Ok(())
159+
}
160+
108161
// Handle requests in a loop
109162
loop {
163+
// First, poll and send any pending events to this client
164+
if let Err(e) = send_pending_events(&mut stream, &server, sequence_number) {
165+
log::warn!("Client {} event send error: {}", client_id, e);
166+
break;
167+
}
168+
110169
// Read request header (4 bytes minimum)
111170
let mut header = [0u8; 4];
112171
match stream.read_exact(&mut header) {
113172
Ok(_) => {}
173+
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
174+
// Timeout - no client request, loop back to check for events
175+
continue;
176+
}
114177
Err(_) => {
115178
log::info!("Client {} disconnected", client_id);
116179
break;
@@ -4064,6 +4127,134 @@ fn handle_change_keyboard_mapping<S: Write>(
40644127
Ok(())
40654128
}
40664129

4130+
/// Map macOS keycode (after +8 offset) to X11 keysym
4131+
/// macOS keycodes are not sequential - this provides the correct mapping
4132+
fn macos_keycode_to_keysym(keycode: u8) -> u32 {
4133+
// macOS keycode + 8 = our keycode, so subtract 8 to get macOS keycode
4134+
let mac_keycode = keycode.saturating_sub(8);
4135+
4136+
// macOS virtual key codes to X11 keysyms
4137+
// Reference: Carbon/HIToolbox/Events.h (kVK_* constants)
4138+
match mac_keycode {
4139+
// Letters (QWERTY layout)
4140+
0 => 0x61, // kVK_ANSI_A -> 'a'
4141+
1 => 0x73, // kVK_ANSI_S -> 's'
4142+
2 => 0x64, // kVK_ANSI_D -> 'd'
4143+
3 => 0x66, // kVK_ANSI_F -> 'f'
4144+
4 => 0x68, // kVK_ANSI_H -> 'h'
4145+
5 => 0x67, // kVK_ANSI_G -> 'g'
4146+
6 => 0x7a, // kVK_ANSI_Z -> 'z'
4147+
7 => 0x78, // kVK_ANSI_X -> 'x'
4148+
8 => 0x63, // kVK_ANSI_C -> 'c'
4149+
9 => 0x76, // kVK_ANSI_V -> 'v'
4150+
11 => 0x62, // kVK_ANSI_B -> 'b'
4151+
12 => 0x71, // kVK_ANSI_Q -> 'q'
4152+
13 => 0x77, // kVK_ANSI_W -> 'w'
4153+
14 => 0x65, // kVK_ANSI_E -> 'e'
4154+
15 => 0x72, // kVK_ANSI_R -> 'r'
4155+
16 => 0x79, // kVK_ANSI_Y -> 'y'
4156+
17 => 0x74, // kVK_ANSI_T -> 't'
4157+
18 => 0x31, // kVK_ANSI_1 -> '1'
4158+
19 => 0x32, // kVK_ANSI_2 -> '2'
4159+
20 => 0x33, // kVK_ANSI_3 -> '3'
4160+
21 => 0x34, // kVK_ANSI_4 -> '4'
4161+
22 => 0x36, // kVK_ANSI_6 -> '6'
4162+
23 => 0x35, // kVK_ANSI_5 -> '5'
4163+
24 => 0x3d, // kVK_ANSI_Equal -> '='
4164+
25 => 0x39, // kVK_ANSI_9 -> '9'
4165+
26 => 0x37, // kVK_ANSI_7 -> '7'
4166+
27 => 0x2d, // kVK_ANSI_Minus -> '-'
4167+
28 => 0x38, // kVK_ANSI_8 -> '8'
4168+
29 => 0x30, // kVK_ANSI_0 -> '0'
4169+
30 => 0x5d, // kVK_ANSI_RightBracket -> ']'
4170+
31 => 0x6f, // kVK_ANSI_O -> 'o'
4171+
32 => 0x75, // kVK_ANSI_U -> 'u'
4172+
33 => 0x5b, // kVK_ANSI_LeftBracket -> '['
4173+
34 => 0x69, // kVK_ANSI_I -> 'i'
4174+
35 => 0x70, // kVK_ANSI_P -> 'p'
4175+
37 => 0x6c, // kVK_ANSI_L -> 'l'
4176+
38 => 0x6a, // kVK_ANSI_J -> 'j'
4177+
39 => 0x27, // kVK_ANSI_Quote -> '''
4178+
40 => 0x6b, // kVK_ANSI_K -> 'k'
4179+
41 => 0x3b, // kVK_ANSI_Semicolon -> ';'
4180+
42 => 0x5c, // kVK_ANSI_Backslash -> '\'
4181+
43 => 0x2c, // kVK_ANSI_Comma -> ','
4182+
44 => 0x2f, // kVK_ANSI_Slash -> '/'
4183+
45 => 0x6e, // kVK_ANSI_N -> 'n'
4184+
46 => 0x6d, // kVK_ANSI_M -> 'm'
4185+
47 => 0x2e, // kVK_ANSI_Period -> '.'
4186+
50 => 0x60, // kVK_ANSI_Grave -> '`'
4187+
4188+
// Special keys
4189+
36 => 0xff0d, // kVK_Return -> XK_Return
4190+
48 => 0xff09, // kVK_Tab -> XK_Tab
4191+
49 => 0x20, // kVK_Space -> ' '
4192+
51 => 0xff08, // kVK_Delete (backspace) -> XK_BackSpace
4193+
53 => 0xff1b, // kVK_Escape -> XK_Escape
4194+
4195+
// Arrow keys
4196+
123 => 0xff51, // kVK_LeftArrow -> XK_Left
4197+
124 => 0xff53, // kVK_RightArrow -> XK_Right
4198+
125 => 0xff54, // kVK_DownArrow -> XK_Down
4199+
126 => 0xff52, // kVK_UpArrow -> XK_Up
4200+
4201+
// Function keys
4202+
122 => 0xffbe, // kVK_F1 -> XK_F1
4203+
120 => 0xffbf, // kVK_F2 -> XK_F2
4204+
99 => 0xffc0, // kVK_F3 -> XK_F3
4205+
118 => 0xffc1, // kVK_F4 -> XK_F4
4206+
96 => 0xffc2, // kVK_F5 -> XK_F5
4207+
97 => 0xffc3, // kVK_F6 -> XK_F6
4208+
98 => 0xffc4, // kVK_F7 -> XK_F7
4209+
100 => 0xffc5, // kVK_F8 -> XK_F8
4210+
101 => 0xffc6, // kVK_F9 -> XK_F9
4211+
109 => 0xffc7, // kVK_F10 -> XK_F10
4212+
103 => 0xffc8, // kVK_F11 -> XK_F11
4213+
111 => 0xffc9, // kVK_F12 -> XK_F12
4214+
4215+
// Modifier keys
4216+
56 => 0xffe1, // kVK_Shift -> XK_Shift_L
4217+
60 => 0xffe2, // kVK_RightShift -> XK_Shift_R
4218+
58 => 0xffe9, // kVK_Option -> XK_Alt_L
4219+
61 => 0xffea, // kVK_RightOption -> XK_Alt_R
4220+
59 => 0xffe3, // kVK_Control -> XK_Control_L
4221+
62 => 0xffe4, // kVK_RightControl -> XK_Control_R
4222+
55 => 0xffeb, // kVK_Command -> XK_Super_L
4223+
54 => 0xffec, // kVK_RightCommand -> XK_Super_R
4224+
57 => 0xffe5, // kVK_CapsLock -> XK_Caps_Lock
4225+
4226+
// Keypad
4227+
65 => 0xffae, // kVK_ANSI_KeypadDecimal -> XK_KP_Decimal
4228+
67 => 0xffaa, // kVK_ANSI_KeypadMultiply -> XK_KP_Multiply
4229+
69 => 0xffab, // kVK_ANSI_KeypadPlus -> XK_KP_Add
4230+
71 => 0xff7f, // kVK_ANSI_KeypadClear -> XK_Num_Lock
4231+
75 => 0xffaf, // kVK_ANSI_KeypadDivide -> XK_KP_Divide
4232+
76 => 0xff8d, // kVK_ANSI_KeypadEnter -> XK_KP_Enter
4233+
78 => 0xffad, // kVK_ANSI_KeypadMinus -> XK_KP_Subtract
4234+
81 => 0xffbd, // kVK_ANSI_KeypadEquals -> XK_KP_Equal
4235+
82 => 0xffb0, // kVK_ANSI_Keypad0 -> XK_KP_0
4236+
83 => 0xffb1, // kVK_ANSI_Keypad1 -> XK_KP_1
4237+
84 => 0xffb2, // kVK_ANSI_Keypad2 -> XK_KP_2
4238+
85 => 0xffb3, // kVK_ANSI_Keypad3 -> XK_KP_3
4239+
86 => 0xffb4, // kVK_ANSI_Keypad4 -> XK_KP_4
4240+
87 => 0xffb5, // kVK_ANSI_Keypad5 -> XK_KP_5
4241+
88 => 0xffb6, // kVK_ANSI_Keypad6 -> XK_KP_6
4242+
89 => 0xffb7, // kVK_ANSI_Keypad7 -> XK_KP_7
4243+
91 => 0xffb8, // kVK_ANSI_Keypad8 -> XK_KP_8
4244+
92 => 0xffb9, // kVK_ANSI_Keypad9 -> XK_KP_9
4245+
4246+
// Navigation keys
4247+
115 => 0xff50, // kVK_Home -> XK_Home
4248+
116 => 0xff55, // kVK_PageUp -> XK_Page_Up
4249+
117 => 0xffff, // kVK_ForwardDelete -> XK_Delete
4250+
119 => 0xff57, // kVK_End -> XK_End
4251+
121 => 0xff56, // kVK_PageDown -> XK_Page_Down
4252+
4253+
// Unknown key
4254+
_ => 0, // NoSymbol
4255+
}
4256+
}
4257+
40674258
fn handle_get_keyboard_mapping<S: Write>(
40684259
stream: &mut S,
40694260
header: &[u8],
@@ -4088,9 +4279,8 @@ fn handle_get_keyboard_mapping<S: Write>(
40884279
// Get the sequence number from header
40894280
let sequence = u16::from_le_bytes([header[2], header[3]]);
40904281

4091-
// Return a minimal keyboard mapping (1 keysym per keycode)
4092-
// For simplicity, just return the keycode as the keysym (ASCII-like mapping)
4093-
let keysyms_per_keycode = 1u8;
4282+
// Return keyboard mapping with 2 keysyms per keycode (normal + shifted)
4283+
let keysyms_per_keycode = 2u8;
40944284
let n_keysyms = (count as usize) * (keysyms_per_keycode as usize);
40954285
let reply_length = n_keysyms; // in 4-byte units (each keysym is 4 bytes)
40964286

@@ -4101,17 +4291,44 @@ fn handle_get_keyboard_mapping<S: Write>(
41014291
reply[2..4].copy_from_slice(&sequence.to_le_bytes());
41024292
reply[4..8].copy_from_slice(&(reply_length as u32).to_le_bytes());
41034293

4104-
// Fill in keysyms - map keycodes to basic keysyms
4294+
// Fill in keysyms using macOS keycode mapping
41054295
for i in 0..count as usize {
4106-
let keycode = first_keycode as usize + i;
4107-
// Simple mapping: keycode -> keysym (for printable ASCII)
4108-
let keysym = if (0x20..0x7f).contains(&keycode) {
4109-
keycode as u32
4110-
} else {
4111-
0 // NoSymbol
4296+
let keycode = (first_keycode as usize + i) as u8;
4297+
let keysym = macos_keycode_to_keysym(keycode);
4298+
4299+
// Calculate shifted keysym for letters and some symbols
4300+
let shifted_keysym = match keysym {
4301+
// Lowercase letters -> uppercase
4302+
0x61..=0x7a => keysym - 0x20, // 'a'-'z' -> 'A'-'Z'
4303+
// Number row shifted symbols
4304+
0x31 => 0x21, // '1' -> '!'
4305+
0x32 => 0x40, // '2' -> '@'
4306+
0x33 => 0x23, // '3' -> '#'
4307+
0x34 => 0x24, // '4' -> '$'
4308+
0x35 => 0x25, // '5' -> '%'
4309+
0x36 => 0x5e, // '6' -> '^'
4310+
0x37 => 0x26, // '7' -> '&'
4311+
0x38 => 0x2a, // '8' -> '*'
4312+
0x39 => 0x28, // '9' -> '('
4313+
0x30 => 0x29, // '0' -> ')'
4314+
0x2d => 0x5f, // '-' -> '_'
4315+
0x3d => 0x2b, // '=' -> '+'
4316+
0x5b => 0x7b, // '[' -> '{'
4317+
0x5d => 0x7d, // ']' -> '}'
4318+
0x5c => 0x7c, // '\' -> '|'
4319+
0x3b => 0x3a, // ';' -> ':'
4320+
0x27 => 0x22, // ''' -> '"'
4321+
0x60 => 0x7e, // '`' -> '~'
4322+
0x2c => 0x3c, // ',' -> '<'
4323+
0x2e => 0x3e, // '.' -> '>'
4324+
0x2f => 0x3f, // '/' -> '?'
4325+
// For other keys, shifted is the same
4326+
_ => keysym,
41124327
};
4113-
let offset = 32 + i * 4;
4328+
4329+
let offset = 32 + i * 8; // 2 keysyms * 4 bytes each
41144330
reply[offset..offset + 4].copy_from_slice(&keysym.to_le_bytes());
4331+
reply[offset + 4..offset + 8].copy_from_slice(&shifted_keysym.to_le_bytes());
41154332
}
41164333

41174334
stream.write_all(&reply)?;

0 commit comments

Comments
 (0)