diff --git a/src/Key.zig b/src/Key.zig index debc737..dc3e41a 100644 --- a/src/Key.zig +++ b/src/Key.zig @@ -1,3 +1,7 @@ +const std = @import("std"); +const testing = std.testing; +const ziglyph = @import("ziglyph"); + const Key = @This(); pub const Modifiers = packed struct(u8) { @@ -30,6 +34,81 @@ base_layout_codepoint: ?u21 = null, mods: Modifiers = .{}, +// matches follows a loose matching algorithm for key matches. +// 1. If the codepoint and modifiers are exact matches +// 2. If the utf8 encoding of the codepoint matches the text +// 3. If there is a shifted codepoint and it matches after removing the shift +// modifier from self +pub fn matches(self: Key, cp: u21, mods: Modifiers) bool { + // rule 1 + if (self.matchExact(cp, mods)) return true; + + // rule 2 + if (self.matchText(cp, mods)) return true; + + // rule 3 + if (self.matchShiftedCodepoint(cp, mods)) return true; + + // rule 4 + if (self.matchShiftedCodepoint(cp, mods)) return true; + + return false; +} + +// matches base layout codes, useful for shortcut matching when an alternate key +// layout is used +pub fn matchShortcut(self: Key, cp: u21, mods: Modifiers) bool { + if (self.base_layout_codepoint == null) return false; + return cp == self.base_layout_codepoint.? and std.meta.eql(self.mods, mods); +} + +// matches keys that aren't upper case versions when shifted. For example, shift +// + semicolon produces a colon. The key can be matched against shift + +// semicolon or just colon...or shift + ctrl + ; or just ctrl + : +pub fn matchShiftedCodepoint(self: Key, cp: u21, mods: Modifiers) bool { + if (self.shifted_codepoint == null) return false; + if (!self.mods.shift) return false; + var self_mods = self.mods; + self_mods.shift = false; + return cp == self.shifted_codepoint.? and std.meta.eql(self_mods, mods); +} + +// matches when the utf8 encoding of the codepoint and relevant mods matches the +// text of the key. This function will consume Shift and Caps Lock when matching +pub fn matchText(self: Key, cp: u21, mods: Modifiers) bool { + // return early if we have no text + if (self.text == null) return false; + + var self_mods = self.mods; + var arg_mods = mods; + var code = cp; + // if the passed codepoint is upper, we consume all shift and caps mods for + // checking + if (ziglyph.isUpper(cp)) { + // consume mods + self_mods.shift = false; + self_mods.caps_lock = false; + arg_mods.shift = false; + arg_mods.caps_lock = false; + } else if (mods.shift or mods.caps_lock) { + // uppercase the cp and consume all mods + code = ziglyph.toUpper(cp); + self_mods.shift = false; + self_mods.caps_lock = false; + arg_mods.shift = false; + arg_mods.caps_lock = false; + } + + var buf: [4]u8 = undefined; + const n = std.unicode.utf8Encode(cp, buf[0..]) catch return false; + return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods); +} + +// The key must exactly match the codepoint and modifiers +pub fn matchExact(self: Key, cp: u21, mods: Modifiers) bool { + return self.codepoint == cp and std.meta.eql(self.mods, mods); +} + // a few special keys that we encode as their actual ascii value pub const enter: u21 = 0x0D; pub const tab: u21 = 0x09; @@ -106,3 +185,47 @@ pub const kp_8: u21 = 57407; pub const kp_9: u21 = 57408; pub const kp_begin: u21 = 57427; // TODO: Finish the kitty keys + +test "matches 'a'" { + const key: Key = .{ + .codepoint = 'a', + }; + try testing.expect(key.matches('a', .{})); +} + +test "matches 'shift+a'" { + const key: Key = .{ + .codepoint = 'a', + .mods = .{ .shift = true }, + .text = "A", + }; + try testing.expect(key.matches('a', .{ .shift = true })); + try testing.expect(key.matches('A', .{})); + try testing.expect(!key.matches('A', .{ .ctrl = true })); +} + +test "matches 'shift+tab'" { + const key: Key = .{ + .codepoint = Key.tab, + .mods = .{ .shift = true }, + }; + try testing.expect(key.matches(Key.tab, .{ .shift = true })); + try testing.expect(!key.matches(Key.tab, .{})); +} + +test "matches 'shift+;'" { + const key: Key = .{ + .codepoint = ';', + .shifted_codepoint = ':', + .mods = .{ .shift = true }, + .text = ":", + }; + try testing.expect(key.matches(';', .{ .shift = true })); + try testing.expect(key.matches(':', .{})); + + const colon: Key = .{ + .codepoint = ':', + .mods = .{}, + }; + try testing.expect(colon.matches(':', .{})); +} diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig index 6a9cf65..45b80ce 100644 --- a/src/widgets/TextInput.zig +++ b/src/widgets/TextInput.zig @@ -41,24 +41,21 @@ pub fn update(self: *TextInput, event: Event) !void { self.cursor_idx += 1; self.grapheme_count += 1; } - switch (key.codepoint) { - Key.backspace => { - if (self.cursor_idx == 0) return; - // Get the grapheme behind our cursor - self.deleteBeforeCursor(); - }, - Key.delete => { - if (self.cursor_idx == self.grapheme_count) return; - self.deleteAtCursor(); - }, - Key.left => { - if (self.cursor_idx > 0) self.cursor_idx -= 1; - }, - Key.right => { - if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1; - }, - else => {}, + if (key.matches(Key.backspace, .{})) { + if (self.cursor_idx == 0) return; + self.deleteBeforeCursor(); } + if (key.matches(Key.delete, .{})) { + if (self.cursor_idx == self.grapheme_count) return; + self.deleteAtCursor(); + } + if (key.matches(Key.left, .{})) { + if (self.cursor_idx > 0) self.cursor_idx -= 1; + } + if (key.matches(Key.right, .{})) { + if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1; + } + // TODO: readline bindings }, } }