diff --git a/src/core/linux/X11.zig b/src/core/linux/X11.zig index 2af3f11afe..e301552d1d 100644 --- a/src/core/linux/X11.zig +++ b/src/core/linux/X11.zig @@ -10,6 +10,7 @@ const c = @cImport({ @cInclude("X11/cursorfont.h"); @cInclude("X11/Xcursor/Xcursor.h"); @cInclude("X11/extensions/Xrandr.h"); + @cInclude("xkbcommon/xkbcommon.h"); }); const mach = @import("../../main.zig"); const gpu = mach.gpu; @@ -17,7 +18,6 @@ const Event = Core.Event; const KeyEvent = Core.KeyEvent; const MouseButtonEvent = Core.MouseButtonEvent; const MouseButton = Core.MouseButton; -const Size = Core.Size; const DisplayMode = Core.DisplayMode; const CursorShape = Core.CursorShape; const VSyncMode = Core.VSyncMode; @@ -34,11 +34,13 @@ pub const X11 = @This(); allocator: std.mem.Allocator, core: *Core, +state: *Core, libx11: LibX11, libxrr: ?LibXRR, libgl: ?LibGL, libxcursor: ?LibXCursor, +libxkbcommon: LibXkbCommon, gl_ctx: ?*LibGL.Context, display: *c.Display, width: c_int, @@ -72,7 +74,7 @@ display_mode: DisplayMode = .windowed, vsync_mode: VSyncMode = .triple, border: bool, headless: bool, -size: Size, +size: Core.Size, cursor_mode: CursorMode = .normal, cursor_shape: CursorShape = .arrow, surface_descriptor: *gpu.Surface.DescriptorFromXlibWindow, @@ -82,8 +84,6 @@ pub fn init( core: *Core.Mod, options: InitOptions, ) !X11 { - _ = core; - // TODO(core): return errors.NotSupported if not supported const libx11 = try LibX11.load(); const libgl: ?LibGL = LibGL.load() catch |err| switch (err) { @@ -130,7 +130,7 @@ pub fn init( ); var window_attrs: c.XWindowAttributes = undefined; _ = libx11.XGetWindowAttributes(display, window, &window_attrs); - const window_size = Size{ + const window_size = Core.Size{ .width = @intCast(window_attrs.width), .height = @intCast(window_attrs.height), }; @@ -150,6 +150,7 @@ pub fn init( }; var x11 = X11{ .core = @fieldParentPtr("platform", linux), + .state = core.state(), .allocator = options.allocator, .display = display, .libx11 = libx11, @@ -182,6 +183,7 @@ pub fn init( .size = window_size, .cursors = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]?c.Cursor), .surface_descriptor = surface_descriptor, + .libxkbcommon = try LibXkbCommon.load(), }; _ = libx11.XSetErrorHandler(errorHandler); _ = libx11.XInitThreads(); @@ -269,6 +271,21 @@ pub fn deinit( std.posix.close(x11.empty_event_pipe[1]); } +// Called on the main thread +pub fn update(x11: *X11) !void { + while (c.QLength(x11.display) != 0) { + var event: c.XEvent = undefined; + _ = x11.libx11.XNextEvent(x11.display, &event); + x11.processEvent(&event); + } + _ = x11.libx11.XFlush(x11.display); + + // const frequency_delay = @as(f32, @floatFromInt(x11.input.delay_ns)) / @as(f32, @floatFromInt(std.time.ns_per_s)); + // TODO: glfw.waitEventsTimeout(frequency_delay); + + x11.core.input.tick(); +} + const LibX11 = struct { handle: std.DynLib, XInitThreads: *const @TypeOf(c.XInitThreads), @@ -392,6 +409,41 @@ const LibGL = struct { } }; +const LibXkbCommon = struct { + handle: std.DynLib, + + // xkb_context_new: *const @TypeOf(c.xkb_context_new), + // xkb_keymap_new_from_string: *const @TypeOf(c.xkb_keymap_new_from_string), + // xkb_state_new: *const @TypeOf(c.xkb_state_new), + // xkb_keymap_unref: *const @TypeOf(c.xkb_keymap_unref), + // xkb_state_unref: *const @TypeOf(c.xkb_state_unref), + // xkb_compose_table_new_from_locale: *const @TypeOf(c.xkb_compose_table_new_from_locale), + // xkb_compose_state_new: *const @TypeOf(c.xkb_compose_state_new), + // xkb_compose_table_unref: *const @TypeOf(c.xkb_compose_table_unref), + // xkb_keymap_mod_get_index: *const @TypeOf(c.xkb_keymap_mod_get_index), + // xkb_state_update_mask: *const @TypeOf(c.xkb_state_update_mask), + // xkb_state_mod_index_is_active: *const @TypeOf(c.xkb_state_mod_index_is_active), + // xkb_state_key_get_syms: *const @TypeOf(c.xkb_state_key_get_syms), + // xkb_compose_state_feed: *const @TypeOf(c.xkb_compose_state_feed), + // xkb_compose_state_get_status: *const @TypeOf(c.xkb_compose_state_get_status), + // xkb_compose_state_get_one_sym: *const @TypeOf(c.xkb_compose_state_get_one_sym), + xkb_keysym_to_utf32: *const @TypeOf(c.xkb_keysym_to_utf32), + + pub fn load() !LibXkbCommon { + var lib: LibXkbCommon = undefined; + lib.handle = std.DynLib.open("libxkbcommon.so.0") catch return error.LibraryNotFound; + inline for (@typeInfo(LibXkbCommon).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse { + log.err("Symbol lookup failed for {s}", .{name}); + return error.SymbolLookup; + }; + } + return lib; + } +}; + fn errorHandler(display: ?*c.Display, event: [*c]c.XErrorEvent) callconv(.C) c_int { _ = display; log.err("X11: error code {d}\n", .{event.*.error_code}); @@ -438,3 +490,335 @@ fn createStandardCursor(x11: *X11, shape: CursorShape) !c.Cursor { if (cursor == 0) return error.FailedToCreateCursor; return cursor; } + +fn getCursorPos(x11: *X11) Position { + var root_window: c.Window = undefined; + var child_window: c.Window = undefined; + var root_cursor_x: c_int = 0; + var root_cursor_y: c_int = 0; + var cursor_x: c_int = 0; + var cursor_y: c_int = 0; + var mask: c_uint = 0; + _ = x11.libx11.XQueryPointer( + x11.display, + x11.window, + &root_window, + &child_window, + &root_cursor_x, + &root_cursor_y, + &cursor_x, + &cursor_y, + &mask, + ); + + return .{ .x = @floatFromInt(cursor_x), .y = @floatFromInt(cursor_y) }; +} + +fn processEvent(x11: *X11, event: *c.XEvent) void { + switch (event.type) { + c.KeyPress, c.KeyRelease => { + // TODO: key repeat event + + var keysym: c.KeySym = undefined; + _ = x11.libx11.XLookupString(&event.xkey, null, 0, &keysym, null); + + const key_event = KeyEvent{ .key = toMachKey(keysym), .mods = toMachMods(event.xkey.state) }; + + switch (event.type) { + c.KeyPress => { + x11.input_mu.lock(); + x11.state.input_state.keys.set(@intFromEnum(key_event.key)); + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .key_press = key_event }); + + const codepoint = x11.libxkbcommon.xkb_keysym_to_utf32(@truncate(keysym)); + if (codepoint != 0) { + x11.state.pushEvent(.{ .char_input = .{ .codepoint = @truncate(codepoint) } }); + } + }, + c.KeyRelease => { + x11.input_mu.lock(); + x11.state.input_state.keys.unset(@intFromEnum(key_event.key)); + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .key_release = key_event }); + }, + else => unreachable, + } + }, + c.ButtonPress => { + const button = toMachButton(event.xbutton.button) orelse { + // Modern X provides scroll events as mouse button presses + const scroll: struct { f32, f32 } = switch (event.xbutton.button) { + c.Button4 => .{ 0.0, 1.0 }, + c.Button5 => .{ 0.0, -1.0 }, + 6 => .{ 1.0, 0.0 }, + 7 => .{ -1.0, 0.0 }, + else => unreachable, + }; + x11.state.pushEvent(.{ .mouse_scroll = .{ .xoffset = scroll[0], .yoffset = scroll[1] } }); + return; + }; + const cursor_pos = x11.getCursorPos(); + const mouse_button = MouseButtonEvent{ + .button = button, + .pos = cursor_pos, + .mods = toMachMods(event.xbutton.state), + }; + + x11.input_mu.lock(); + x11.state.input_state.mouse_buttons.set(@intFromEnum(mouse_button.button)); + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .mouse_press = mouse_button }); + }, + c.ButtonRelease => { + const button = toMachButton(event.xbutton.button) orelse return; + const cursor_pos = x11.getCursorPos(); + const mouse_button = MouseButtonEvent{ + .button = button, + .pos = cursor_pos, + .mods = toMachMods(event.xbutton.state), + }; + + x11.input_mu.lock(); + x11.state.input_state.mouse_buttons.unset(@intFromEnum(mouse_button.button)); + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .mouse_release = mouse_button }); + }, + c.ClientMessage => { + if (event.xclient.message_type == c.None) return; + + if (event.xclient.message_type == x11.wm_protocols) { + const protocol = event.xclient.data.l[0]; + if (protocol == c.None) return; + + if (protocol == x11.wm_delete_window) { + x11.state.pushEvent(.close); + } else if (protocol == x11.net_wm_ping) { + // The window manager is pinging the application to ensure + // it's still responding to events + var reply = event.*; + reply.xclient.window = x11.root_window; + _ = x11.libx11.XSendEvent( + x11.display, + x11.root_window, + c.False, + c.SubstructureNotifyMask | c.SubstructureRedirectMask, + &reply, + ); + } + } + }, + c.EnterNotify => { + const x: f32 = @floatFromInt(event.xcrossing.x); + const y: f32 = @floatFromInt(event.xcrossing.y); + x11.input_mu.lock(); + x11.state.input_state.mouse_position = .{ .x = x, .y = y }; + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y } } }); + }, + c.MotionNotify => { + const x: f32 = @floatFromInt(event.xmotion.x); + const y: f32 = @floatFromInt(event.xmotion.y); + x11.input_mu.lock(); + x11.state.input_state.mouse_position = .{ .x = x, .y = y }; + x11.input_mu.unlock(); + x11.state.pushEvent(.{ .mouse_motion = .{ .pos = .{ .x = x, .y = y } } }); + }, + c.ConfigureNotify => { + if (event.xconfigure.width != x11.size.width or + event.xconfigure.height != x11.size.height) + { + x11.size.width = @intCast(event.xconfigure.width); + x11.size.height = @intCast(event.xconfigure.height); + x11.core.swap_chain_update.set(); + x11.state.pushEvent(.{ + .framebuffer_resize = .{ + .width = x11.size.width, + .height = x11.size.height, + }, + }); + } + }, + c.FocusIn => { + if (event.xfocus.mode == c.NotifyGrab or + event.xfocus.mode == c.NotifyUngrab) + { + // Ignore focus events from popup indicator windows, window menu + // key chords and window dragging + return; + } + + x11.state.pushEvent(.focus_gained); + }, + c.FocusOut => { + if (event.xfocus.mode == c.NotifyGrab or + event.xfocus.mode == c.NotifyUngrab) + { + // Ignore focus events from popup indicator windows, window menu + // key chords and window dragging + return; + } + + x11.state.pushEvent(.focus_lost); + }, + else => {}, + } +} + +fn toMachMods(mods: c_uint) KeyMods { + return .{ + .shift = mods & c.ShiftMask != 0, + .control = mods & c.ControlMask != 0, + .alt = mods & c.Mod1Mask != 0, + .super = mods & c.Mod4Mask != 0, + .caps_lock = mods & c.LockMask != 0, + .num_lock = mods & c.Mod2Mask != 0, + }; +} + +fn toMachButton(button: c_uint) ?MouseButton { + return switch (button) { + c.Button1 => .left, + c.Button2 => .middle, + c.Button3 => .right, + // Scroll events are handled by caller + c.Button4, c.Button5, 6, 7 => null, + // Additional buttons after 7 are treated as regular buttons + 8 => .four, + 9 => .five, + 10 => .six, + 11 => .seven, + 12 => .eight, + // Unknown button + else => null, + }; +} + +fn toMachKey(key: c.KeySym) Key { + return switch (key) { + c.XK_a, c.XK_A => .a, + c.XK_b, c.XK_B => .b, + c.XK_c, c.XK_C => .c, + c.XK_d, c.XK_D => .d, + c.XK_e, c.XK_E => .e, + c.XK_f, c.XK_F => .f, + c.XK_g, c.XK_G => .g, + c.XK_h, c.XK_H => .h, + c.XK_i, c.XK_I => .i, + c.XK_j, c.XK_J => .j, + c.XK_k, c.XK_K => .k, + c.XK_l, c.XK_L => .l, + c.XK_m, c.XK_M => .m, + c.XK_n, c.XK_N => .n, + c.XK_o, c.XK_O => .o, + c.XK_p, c.XK_P => .p, + c.XK_q, c.XK_Q => .q, + c.XK_r, c.XK_R => .r, + c.XK_s, c.XK_S => .s, + c.XK_t, c.XK_T => .t, + c.XK_u, c.XK_U => .u, + c.XK_v, c.XK_V => .v, + c.XK_w, c.XK_W => .w, + c.XK_x, c.XK_X => .x, + c.XK_y, c.XK_Y => .y, + c.XK_z, c.XK_Z => .z, + + c.XK_0 => .zero, + c.XK_1 => .one, + c.XK_2 => .two, + c.XK_3 => .three, + c.XK_4 => .four, + c.XK_5 => .five, + c.XK_6 => .six, + c.XK_7 => .seven, + c.XK_8 => .eight, + c.XK_9 => .nine, + + c.XK_F1 => .f1, + c.XK_F2 => .f2, + c.XK_F3 => .f3, + c.XK_F4 => .f4, + c.XK_F5 => .f5, + c.XK_F6 => .f6, + c.XK_F7 => .f7, + c.XK_F8 => .f8, + c.XK_F9 => .f9, + c.XK_F10 => .f10, + c.XK_F11 => .f11, + c.XK_F12 => .f12, + c.XK_F13 => .f13, + c.XK_F14 => .f14, + c.XK_F15 => .f15, + c.XK_F16 => .f16, + c.XK_F17 => .f17, + c.XK_F18 => .f18, + c.XK_F19 => .f19, + c.XK_F20 => .f20, + c.XK_F21 => .f21, + c.XK_F22 => .f22, + c.XK_F23 => .f23, + c.XK_F24 => .f24, + c.XK_F25 => .f25, + + c.XK_KP_Divide => .kp_divide, + c.XK_KP_Multiply => .kp_multiply, + c.XK_KP_Subtract => .kp_subtract, + c.XK_KP_Add => .kp_add, + c.XK_KP_0 => .kp_0, + c.XK_KP_1 => .kp_1, + c.XK_KP_2 => .kp_2, + c.XK_KP_3 => .kp_3, + c.XK_KP_4 => .kp_4, + c.XK_KP_5 => .kp_5, + c.XK_KP_6 => .kp_6, + c.XK_KP_7 => .kp_7, + c.XK_KP_8 => .kp_8, + c.XK_KP_9 => .kp_9, + c.XK_KP_Decimal => .kp_decimal, + c.XK_KP_Equal => .kp_equal, + c.XK_KP_Enter => .kp_enter, + + c.XK_Return => .enter, + c.XK_Escape => .escape, + c.XK_Tab => .tab, + c.XK_Shift_L => .left_shift, + c.XK_Shift_R => .right_shift, + c.XK_Control_L => .left_control, + c.XK_Control_R => .right_control, + c.XK_Alt_L => .left_alt, + c.XK_Alt_R => .right_alt, + c.XK_Super_L => .left_super, + c.XK_Super_R => .right_super, + c.XK_Menu => .menu, + c.XK_Num_Lock => .num_lock, + c.XK_Caps_Lock => .caps_lock, + c.XK_Print => .print, + c.XK_Scroll_Lock => .scroll_lock, + c.XK_Pause => .pause, + c.XK_Delete => .delete, + c.XK_Home => .home, + c.XK_End => .end, + c.XK_Page_Up => .page_up, + c.XK_Page_Down => .page_down, + c.XK_Insert => .insert, + c.XK_Left => .left, + c.XK_Right => .right, + c.XK_Up => .up, + c.XK_Down => .down, + c.XK_BackSpace => .backspace, + c.XK_space => .space, + c.XK_minus => .minus, + c.XK_equal => .equal, + c.XK_braceleft => .left_bracket, + c.XK_braceright => .right_bracket, + c.XK_backslash => .backslash, + c.XK_semicolon => .semicolon, + c.XK_apostrophe => .apostrophe, + c.XK_comma => .comma, + c.XK_period => .period, + c.XK_slash => .slash, + c.XK_grave => .grave, + + else => .unknown, + }; +}