diff --git a/build.zig.zon b/build.zig.zon index 3495f0fb..566d6ec6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -43,8 +43,8 @@ .lazy = true, }, .wio = .{ - .url = "git+https://github.com/neurocyte/wio?ref=master#6186501162ace8645036e71fdb7ebad76a0dca71", - .hash = "wio-0.0.0-8xHrr8XzBQAYPvWczIC0RU40hP-cVH2HR1zcTVfP7r8l", + .url = "git+https://github.com/neurocyte/wio?ref=master#a5f4ccb81fb6bafa348196747a0051cc65e10db9", + .hash = "wio-0.0.0-8xHrr0MKBgAYo_rlL6A2llfOLjikoWrPdV6Aml3OOBwJ", .lazy = true, }, .sokol = .{ diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index 5bccc85a..0e115e0b 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -23,6 +23,10 @@ const gui_config = @import("gui_config"); const log = std.log.scoped(.wio_app); +const press: u8 = 1; +const repeat: u8 = 2; +const release: u8 = 3; + // Re-export cursor types so renderer.zig (which imports 'app' but not 'gpu') // can use them without a direct dependency on the gpu module. pub const CursorInfo = gpu.CursorInfo; @@ -47,6 +51,7 @@ var screen_mutex: std.Thread.Mutex = .{}; var screen_pending: std.atomic.Value(bool) = .init(false); var screen_snap: ?ScreenSnapshot = null; var tui_pid: thespian.pid = undefined; +var last_mods: input_translate.Mods = .{}; var font_size_px: u16 = 16; var font_name_buf: [256]u8 = undefined; var font_name_len: usize = 0; @@ -481,7 +486,7 @@ fn wioLoop() void { }, .button_press => |btn| { held_buttons.press(btn); - const mods = input_translate.Mods.fromButtons(held_buttons); + const mods = syncModifiers(); if (input_translate.mouseButtonId(btn)) |mb_id| { const cp = pixelToCellPos(mouse_pos); tui_pid.send(.{ @@ -496,15 +501,15 @@ fn wioLoop() void { } else { if (input_translate.codepointFromButton(btn, .{})) |base_cp| { const shifted_cp = if (mods.shift) input_translate.codepointFromButton(btn, .{ .shift = true }) else base_cp; - sendKey(1, base_cp, shifted_cp orelse base_cp, mods); + sendKey(press, base_cp, shifted_cp orelse base_cp, mods); } else { if (input_translate.modifierCodepoint(btn)) |mod_cp| - sendKey(1, mod_cp, mod_cp, mods); + sendKey(press, mod_cp, mod_cp, mods); } } }, .button_repeat => |btn| { - const mods = input_translate.Mods.fromButtons(held_buttons); + const mods = syncModifiers(); if (input_translate.mouseButtonId(btn) == null) { if (input_translate.codepointFromButton(btn, .{})) |base_cp| { const shifted_cp = if (mods.shift) input_translate.codepointFromButton(btn, .{ .shift = true }) else base_cp; @@ -514,7 +519,7 @@ fn wioLoop() void { }, .button_release => |btn| { held_buttons.release(btn); - const mods = input_translate.Mods.fromButtons(held_buttons); + const mods = syncModifiers(); if (input_translate.mouseButtonId(btn)) |mb_id| { const cp = pixelToCellPos(mouse_pos); tui_pid.send(.{ @@ -540,8 +545,8 @@ fn wioLoop() void { // ASCII keys are fully handled by .button_press with correct // base/shifted codepoints, avoiding double-firing on X11. if (cp > 0x7f) { - const mods = input_translate.Mods.fromButtons(held_buttons); - sendKey(1, cp, cp, mods); + const mods = syncModifiers(); + sendKey(press, cp, cp, mods); } }, .mouse => |pos| { @@ -564,23 +569,12 @@ fn wioLoop() void { tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, cp.col, cp.row, cp.xoff, cp.yoff }) catch {}; }, .focused => { + _ = syncModifiers(); window.enableTextInput(.{}); tui_pid.send(.{"focus_in"}) catch {}; }, .unfocused => { window.disableTextInput(); - // Synthesize release events for any modifier keys still held so the TUI - // doesn't see them as stuck. Release in order so mods reflects reality after - // each release. - for (input_translate.modifier_buttons) |mod_btn| { - if (held_buttons.has(mod_btn)) { - held_buttons.release(mod_btn); - const mods = input_translate.Mods.fromButtons(held_buttons); - if (input_translate.modifierCodepoint(mod_btn)) |mod_cp| - sendKey(3, mod_cp, mod_cp, mods); - } - } - held_buttons = .{}; tui_pid.send(.{"focus_out"}) catch {}; }, else => { @@ -722,3 +716,27 @@ fn sendKey(kind: u8, codepoint: u21, shifted_codepoint: u21, mods: input_transla @as(u8, @bitCast(mods)), }) catch {}; } + +fn syncModifiers() input_translate.Mods { + const mods = input_translate.fromWioModifiers(wio.getModifiers()); + // Synthesize release events for any modifier keys no + // longer held so they don't appear stuck. + if (mods.shift != last_mods.shift) { + last_mods.shift = mods.shift; + sendKey(if (last_mods.shift) press else release, vaxis.Key.left_shift, vaxis.Key.left_shift, last_mods); + } + if (mods.alt != last_mods.alt) { + last_mods.alt = mods.alt; + sendKey(if (last_mods.alt) press else release, vaxis.Key.left_alt, vaxis.Key.left_alt, last_mods); + } + if (mods.ctrl != last_mods.ctrl) { + last_mods.ctrl = mods.ctrl; + sendKey(if (last_mods.ctrl) press else release, vaxis.Key.left_control, vaxis.Key.left_control, last_mods); + } + if (mods.super != last_mods.super) { + last_mods.super = mods.super; + sendKey(if (last_mods.super) press else release, vaxis.Key.left_super, vaxis.Key.left_super, last_mods); + } + last_mods = mods; + return mods; +} diff --git a/src/gui/wio/input.zig b/src/gui/wio/input.zig index 4c22e413..601598ac 100644 --- a/src/gui/wio/input.zig +++ b/src/gui/wio/input.zig @@ -7,29 +7,20 @@ const vaxis = @import("vaxis"); // Modifiers bitmask (matches vaxis.Key.Modifiers packed struct layout used // by the rest of Flow's input handling). -pub const Mods = packed struct(u8) { - shift: bool = false, - alt: bool = false, - ctrl: bool = false, - super: bool = false, - hyper: bool = false, - meta: bool = false, - _pad: u2 = 0, +pub const Mods = vaxis.Key.Modifiers; - pub fn fromButtons(pressed: ButtonSet) Mods { - return .{ - .shift = pressed.has(.left_shift) or pressed.has(.right_shift), - .alt = pressed.has(.left_alt) or pressed.has(.right_alt), - .ctrl = pressed.has(.left_control) or pressed.has(.right_control), - .super = pressed.has(.left_gui) or pressed.has(.right_gui), - }; - } +pub fn fromWioModifiers(modifiers: wio.Modifiers) Mods { + return .{ + .shift = modifiers.shift, + .alt = modifiers.alt, + .ctrl = modifiers.control, + .super = modifiers.super, + }; +} - /// True only when shift is the sole active modifier. - pub fn shiftOnly(self: Mods) bool { - return self.shift and !self.alt and !self.ctrl and !self.super and !self.hyper and !self.meta; - } -}; +pub fn isShifted(mods: Mods) bool { + return mods.shift and !mods.alt and !mods.ctrl and !mods.super and !mods.hyper and !mods.meta; +} // Simple set of currently held buttons (for modifier tracking) pub const ButtonSet = struct { @@ -60,58 +51,58 @@ pub const KeyEvent = struct { // Map a wio.Button to the primary codepoint for that key pub fn codepointFromButton(b: wio.Button, mods: Mods) ?u21 { return switch (b) { - .a => if (mods.shiftOnly()) 'A' else 'a', - .b => if (mods.shiftOnly()) 'B' else 'b', - .c => if (mods.shiftOnly()) 'C' else 'c', - .d => if (mods.shiftOnly()) 'D' else 'd', - .e => if (mods.shiftOnly()) 'E' else 'e', - .f => if (mods.shiftOnly()) 'F' else 'f', - .g => if (mods.shiftOnly()) 'G' else 'g', - .h => if (mods.shiftOnly()) 'H' else 'h', - .i => if (mods.shiftOnly()) 'I' else 'i', - .j => if (mods.shiftOnly()) 'J' else 'j', - .k => if (mods.shiftOnly()) 'K' else 'k', - .l => if (mods.shiftOnly()) 'L' else 'l', - .m => if (mods.shiftOnly()) 'M' else 'm', - .n => if (mods.shiftOnly()) 'N' else 'n', - .o => if (mods.shiftOnly()) 'O' else 'o', - .p => if (mods.shiftOnly()) 'P' else 'p', - .q => if (mods.shiftOnly()) 'Q' else 'q', - .r => if (mods.shiftOnly()) 'R' else 'r', - .s => if (mods.shiftOnly()) 'S' else 's', - .t => if (mods.shiftOnly()) 'T' else 't', - .u => if (mods.shiftOnly()) 'U' else 'u', - .v => if (mods.shiftOnly()) 'V' else 'v', - .w => if (mods.shiftOnly()) 'W' else 'w', - .x => if (mods.shiftOnly()) 'X' else 'x', - .y => if (mods.shiftOnly()) 'Y' else 'y', - .z => if (mods.shiftOnly()) 'Z' else 'z', - .@"0" => if (mods.shiftOnly()) ')' else '0', - .@"1" => if (mods.shiftOnly()) '!' else '1', - .@"2" => if (mods.shiftOnly()) '@' else '2', - .@"3" => if (mods.shiftOnly()) '#' else '3', - .@"4" => if (mods.shiftOnly()) '$' else '4', - .@"5" => if (mods.shiftOnly()) '%' else '5', - .@"6" => if (mods.shiftOnly()) '^' else '6', - .@"7" => if (mods.shiftOnly()) '&' else '7', - .@"8" => if (mods.shiftOnly()) '*' else '8', - .@"9" => if (mods.shiftOnly()) '(' else '9', + .a => if (isShifted(mods)) 'A' else 'a', + .b => if (isShifted(mods)) 'B' else 'b', + .c => if (isShifted(mods)) 'C' else 'c', + .d => if (isShifted(mods)) 'D' else 'd', + .e => if (isShifted(mods)) 'E' else 'e', + .f => if (isShifted(mods)) 'F' else 'f', + .g => if (isShifted(mods)) 'G' else 'g', + .h => if (isShifted(mods)) 'H' else 'h', + .i => if (isShifted(mods)) 'I' else 'i', + .j => if (isShifted(mods)) 'J' else 'j', + .k => if (isShifted(mods)) 'K' else 'k', + .l => if (isShifted(mods)) 'L' else 'l', + .m => if (isShifted(mods)) 'M' else 'm', + .n => if (isShifted(mods)) 'N' else 'n', + .o => if (isShifted(mods)) 'O' else 'o', + .p => if (isShifted(mods)) 'P' else 'p', + .q => if (isShifted(mods)) 'Q' else 'q', + .r => if (isShifted(mods)) 'R' else 'r', + .s => if (isShifted(mods)) 'S' else 's', + .t => if (isShifted(mods)) 'T' else 't', + .u => if (isShifted(mods)) 'U' else 'u', + .v => if (isShifted(mods)) 'V' else 'v', + .w => if (isShifted(mods)) 'W' else 'w', + .x => if (isShifted(mods)) 'X' else 'x', + .y => if (isShifted(mods)) 'Y' else 'y', + .z => if (isShifted(mods)) 'Z' else 'z', + .@"0" => if (isShifted(mods)) ')' else '0', + .@"1" => if (isShifted(mods)) '!' else '1', + .@"2" => if (isShifted(mods)) '@' else '2', + .@"3" => if (isShifted(mods)) '#' else '3', + .@"4" => if (isShifted(mods)) '$' else '4', + .@"5" => if (isShifted(mods)) '%' else '5', + .@"6" => if (isShifted(mods)) '^' else '6', + .@"7" => if (isShifted(mods)) '&' else '7', + .@"8" => if (isShifted(mods)) '*' else '8', + .@"9" => if (isShifted(mods)) '(' else '9', .space => vaxis.Key.space, .enter => vaxis.Key.enter, .tab => vaxis.Key.tab, .backspace => vaxis.Key.backspace, .escape => vaxis.Key.escape, - .minus => if (mods.shiftOnly()) '_' else '-', - .equals => if (mods.shiftOnly()) '+' else '=', - .left_bracket => if (mods.shiftOnly()) '{' else '[', - .right_bracket => if (mods.shiftOnly()) '}' else ']', - .backslash => if (mods.shiftOnly()) '|' else '\\', - .semicolon => if (mods.shiftOnly()) ':' else ';', - .apostrophe => if (mods.shiftOnly()) '"' else '\'', - .grave => if (mods.shiftOnly()) '~' else '`', - .comma => if (mods.shiftOnly()) '<' else ',', - .dot => if (mods.shiftOnly()) '>' else '.', - .slash => if (mods.shiftOnly()) '?' else '/', + .minus => if (isShifted(mods)) '_' else '-', + .equals => if (isShifted(mods)) '+' else '=', + .left_bracket => if (isShifted(mods)) '{' else '[', + .right_bracket => if (isShifted(mods)) '}' else ']', + .backslash => if (isShifted(mods)) '|' else '\\', + .semicolon => if (isShifted(mods)) ':' else ';', + .apostrophe => if (isShifted(mods)) '"' else '\'', + .grave => if (isShifted(mods)) '~' else '`', + .comma => if (isShifted(mods)) '<' else ',', + .dot => if (isShifted(mods)) '>' else '.', + .slash => if (isShifted(mods)) '?' else '/', // Navigation and function keys: kitty protocol codepoints (vaxis.Key). .up => vaxis.Key.up, .down => vaxis.Key.down,