diff --git a/include/ghostty.h b/include/ghostty.h index 664ae8011..d2d74ea6c 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -283,6 +283,7 @@ ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t void ghostty_app_free(ghostty_app_t); bool ghostty_app_tick(ghostty_app_t); void *ghostty_app_userdata(ghostty_app_t); +void ghostty_app_keyboard_changed(ghostty_app_t); ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); @@ -292,7 +293,7 @@ void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); -void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_key_e, ghostty_input_mods_e); +void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, uint32_t, ghostty_input_mods_e); void ghostty_surface_char(ghostty_surface_t, uint32_t); void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e); void ghostty_surface_mouse_pos(ghostty_surface_t, double, double); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b4df4d048..e29c09cc2 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */; }; + A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; @@ -45,6 +46,7 @@ A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitView.swift; sourceTree = ""; }; + A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; @@ -64,6 +66,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */, A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -177,6 +180,7 @@ A5D495A3299BECBA00DD1313 /* Frameworks */ = { isa = PBXGroup; children = ( + A56B880A2A840447007A0E29 /* Carbon.framework */, A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */, ); name = Frameworks; diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 3bc180288..68076c224 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -74,6 +74,13 @@ extension Ghostty { return } self.app = app + + // Subscribe to notifications for keyboard layout change so that we can update Ghostty. + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardSelectionDidChange(notification:)), + name: NSTextInputContext.keyboardSelectionDidChangeNotification, + object: nil) self.readiness = .ready } @@ -82,6 +89,12 @@ extension Ghostty { // This will force the didSet callbacks to run which free. self.app = nil self.config = nil + + // Remove our observer + NotificationCenter.default.removeObserver( + self, + name: NSTextInputContext.keyboardSelectionDidChangeNotification, + object: nil) } /// Initializes a new configuration and loads all the values. @@ -132,6 +145,13 @@ extension Ghostty { ghostty_surface_split_focus(surface, direction.toNative()) } + // Called when the selected keyboard changes. We have to notify Ghostty so that + // it can reload the keyboard mapping for input. + @objc private func keyboardSelectionDidChange(notification: NSNotification) { + guard let app = self.app else { return } + ghostty_app_keyboard_changed(app) + } + // MARK: Ghostty Callbacks static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 2bb9c5e8d..178885976 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -349,7 +349,15 @@ extension Ghostty { let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS keyAction(action, event: event) - self.interpretKeyEvents([event]) + // We specifically DO NOT call interpretKeyEvents because ghostty_surface_key + // automatically handles all key translation, and we don't handle any commands + // currently. + // + // It is possible that in the future we'll have to modify ghostty_surface_key + // and the embedding API so that we can call this because macOS needs to do + // some things with certain keys. I'm not sure. For now this works. + // + // self.interpretKeyEvents([event]) } override func keyUp(with event: NSEvent) { @@ -359,26 +367,7 @@ extension Ghostty { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) - let unmapped_key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID - - // We translate the key to the localized keyboard layout. However, we only support - // ASCII characters to make our translation easier across platforms. This is something - // we want to make a lot more robust in the future, so this will hopefully change. - // For now, this makes most keyboard layouts work, and for those that don't, they can - // use physical keycode mappings. - let key = { - if let str = event.characters(byApplyingModifiers: .init(rawValue: 0)) { - if str.utf8.count == 1, let firstByte = str.utf8.first { - if let translatedKey = Self.ascii[firstByte] { - return translatedKey - } - } - } - - return unmapped_key - }() - - ghostty_surface_key(surface, action, key, unmapped_key, mods) + ghostty_surface_key(surface, action, UInt32(event.keyCode), mods) } // MARK: Menu Handlers diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index c9bd6b3ba..bfaeaf398 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -35,6 +35,7 @@ pub fn link( .file = .{ .path = comptime thisDir() ++ "/text/ext.c" }, .flags = flags.items, }); + step.linkFramework("Carbon"); step.linkFramework("CoreFoundation"); step.linkFramework("CoreText"); return lib; diff --git a/pkg/macos/foundation/data.zig b/pkg/macos/foundation/data.zig index 7f3a6b455..c4047e87d 100644 --- a/pkg/macos/foundation/data.zig +++ b/pkg/macos/foundation/data.zig @@ -19,6 +19,10 @@ pub const Data = opaque { pub fn release(self: *Data) void { foundation.CFRelease(self); } + + pub fn getPointer(self: *Data) *const anyopaque { + return @ptrCast(c.CFDataGetBytePtr(@ptrCast(self))); + } }; test { diff --git a/src/Surface.zig b/src/Surface.zig index 29455592e..56bab2ec5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -90,12 +90,6 @@ padding: renderer.Padding, /// the lifetime of. This makes updating config at runtime easier. config: DerivedConfig, -/// Set to true for a single GLFW key/char callback cycle to cause the -/// char callback to ignore. GLFW seems to always do key followed by char -/// callbacks so we abuse that here. This is to solve an issue where commands -/// like such as "control-v" will write a "v" even if they're intercepted. -ignore_char: bool = false, - /// This is set to true if our IO thread notifies us our child exited. /// This is used to determine if we need to confirm, hold open, etc. child_exited: bool = false, @@ -972,6 +966,22 @@ pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void { try self.io_thread.wakeup.notify(); } +/// Called to set the preedit state for character input. Preedit is used +/// with dead key states, for example, when typing an accent character. +/// This should be called with null to reset the preedit state. +/// +/// The core surface will NOT reset the preedit state on charCallback or +/// keyCallback and we rely completely on the apprt implementation to track +/// the preedit state correctly. +pub fn preeditCallback(self: *Surface, preedit: ?u21) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.renderer_state.preedit = if (preedit) |v| .{ + .codepoint = v, + } else null; + try self.queueRender(); +} + pub fn charCallback(self: *Surface, codepoint: u21) !void { const tracy = trace(@src()); defer tracy.end(); @@ -986,12 +996,6 @@ pub fn charCallback(self: *Surface, codepoint: u21) !void { } else |_| {} } - // Ignore if requested. See field docs for more information. - if (self.ignore_char) { - self.ignore_char = false; - return; - } - // Critical area { self.renderer_state.mutex.lock(); @@ -1022,13 +1026,19 @@ pub fn charCallback(self: *Surface, codepoint: u21) !void { try self.io_thread.wakeup.notify(); } +/// Called for a single key event. +/// +/// This will return true if the key was handled/consumed. In that case, +/// the caller doesn't need to call a subsequent `charCallback` for the +/// same event. However, the caller can call `charCallback` if they want, +/// the surface will retain state to ensure the event is ignored. pub fn keyCallback( self: *Surface, action: input.Action, key: input.Key, - unmapped_key: input.Key, + physical_key: input.Key, mods: input.Mods, -) !void { +) !bool { const tracy = trace(@src()); defer tracy.end(); @@ -1042,10 +1052,6 @@ pub fn keyCallback( } else |_| {} } - // Reset the ignore char setting. If we didn't handle the char - // by here, we aren't going to get it so we just reset this. - self.ignore_char = false; - if (action == .press or action == .repeat) { // Mods for bindings never include caps/num lock. const binding_mods = mods: { @@ -1064,8 +1070,8 @@ pub fn keyCallback( const set = self.config.keybind.set; if (set.get(trigger)) |v| break :action v; - trigger.key = unmapped_key; - trigger.unmapped = true; + trigger.key = physical_key; + trigger.physical = true; if (set.get(trigger)) |v| break :action v; break :action null; @@ -1074,12 +1080,7 @@ pub fn keyCallback( if (binding_action_) |binding_action| { //log.warn("BINDING ACTION={}", .{binding_action}); try self.performBindingAction(binding_action); - - // Bindings always result in us ignoring the char if printable - self.ignore_char = true; - - // No matter what, if there is a binding then we are done. - return; + return true; } // Handle non-printables @@ -1137,18 +1138,6 @@ pub fn keyCallback( }; }; if (char > 0) { - // We are handling this char so don't allow charCallback to do - // anything. Normally it shouldn't because charCallback should not - // be called for control characters. But, we found a scenario where - // it does: https://github.com/mitchellh/ghostty/issues/267 - // - // In case that URL goes away: on macOS, after typing a dead - // key sequence, macOS would call `insertText` with control - // characters. Prior to calling a dead key sequence, it would - // not. I don't know. It doesn't matter, this is more correct - // anyways. - self.ignore_char = true; - // Ask our IO thread to write the data var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(char); @@ -1170,8 +1159,12 @@ pub fn keyCallback( log.warn("error scrolling to bottom err={}", .{err}); }; } + + return true; } } + + return false; } pub fn focusCallback(self: *Surface, focused: bool) !void { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index ca6b0eaa9..892733d08 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -75,17 +75,32 @@ pub const App = struct { core_app: *CoreApp, config: *const Config, opts: Options, + keymap: input.Keymap, pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App { return .{ .core_app = core_app, .config = config, .opts = opts, + .keymap = try input.Keymap.init(), }; } pub fn terminate(self: App) void { - _ = self; + self.keymap.deinit(); + } + + /// This should be called whenever the keyboard layout was changed. + pub fn reloadKeymap(self: *App) !void { + // Reload the keymap + try self.keymap.reload(); + + // Clear the dead key state since we changed the keymap, any + // dead key state is just forgotten. i.e. if you type ' on us-intl + // and then switch to us and type a, you'll get a rather than á. + for (self.core_app.surfaces.items) |surface| { + surface.keymap_state = .{}; + } } pub fn reloadConfig(self: *App) !?*const Config { @@ -140,6 +155,7 @@ pub const Surface = struct { size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, opts: Options, + keymap_state: input.Keymap.State, pub const Options = extern struct { /// Userdata passed to some of the callbacks. @@ -164,6 +180,7 @@ pub const Surface = struct { .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .opts = opts, + .keymap_state = .{}, }; // Add ourselves to the list of surfaces on the app. @@ -367,15 +384,99 @@ pub const Surface = struct { pub fn keyCallback( self: *Surface, action: input.Action, - key: input.Key, - unmapped_key: input.Key, + keycode: u32, mods: input.Mods, - ) void { - // log.warn("key action={} key={} mods={}", .{ action, key, mods }); - self.core_surface.keyCallback(action, key, unmapped_key, mods) catch |err| { - log.err("error in key callback err={}", .{err}); + ) !void { + // We don't handle release events because we don't use them yet. + if (action != .press and action != .repeat) return; + + // Translate our key using the keymap for our localized keyboard layout. + var buf: [128]u8 = undefined; + const result = try self.app.keymap.translate( + &buf, + &self.keymap_state, + @intCast(keycode), + mods, + ); + + // If we aren't composing, then we set our preedit to empty no matter what. + if (!result.composing) { + self.core_surface.preeditCallback(null) catch {}; + } + + // log.warn("TRANSLATE: action={} keycode={x} dead={} key={any} key_str={s} mods={}", .{ + // action, + // keycode, + // result.composing, + // result.text, + // result.text, + // mods, + // }); + + // We want to get the physical unmapped key to process keybinds. + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == keycode) break :keycode entry.key; + } else .invalid; + + // If the resulting text has length 1 then we can take its key + // and attempt to translate it to a key enum and call the key callback. + // If the length is greater than 1 then we're going to call the + // charCallback. + // + // We also only do key translation if this is not a dead key. + const key = if (!result.composing and result.text.len == 1) key: { + // A completed key. If the length of the key is one then we can + // attempt to translate it to a key enum and call the key callback. + break :key input.Key.fromASCII(result.text[0]) orelse physical_key; + } else .invalid; + + // If both keys are invalid then we won't call the key callback. But + // if either one is valid, we want to give it a chance. + if (key != .invalid or physical_key != .invalid) { + const consumed = self.core_surface.keyCallback( + action, + key, + physical_key, + mods, + ) catch |err| { + log.err("error in key callback err={}", .{err}); + return; + }; + + // If we consume the key then we want to reset the dead key state. + if (consumed) { + self.keymap_state = .{}; + self.core_surface.preeditCallback(null) catch {}; + return; + } + } + + // No matter what happens next we'll want a utf8 view. + const view = std.unicode.Utf8View.init(result.text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); return; }; + var it = view.iterator(); + + // If this is a dead key, then we're composing a character and + // we end processing here. We don't process keybinds for dead keys. + if (result.composing) { + const cp: u21 = it.nextCodepoint() orelse 0; + self.core_surface.preeditCallback(cp) catch |err| { + log.err("error in preedit callback err={}", .{err}); + return; + }; + + return; + } + + // Next, we want to call the char callback with each codepoint. + while (it.nextCodepoint()) |cp| { + self.core_surface.charCallback(cp) catch |err| { + log.err("error in char callback err={}", .{err}); + return; + }; + } } pub fn charCallback(self: *Surface, cp_: u32) void { @@ -471,6 +572,15 @@ pub const CAPI = struct { core_app.destroy(); } + /// Notify the app that the keyboard was changed. This causes the + /// keyboard layout to be reloaded from the OS. + export fn ghostty_app_keyboard_changed(v: *App) void { + v.reloadKeymap() catch |err| { + log.err("error reloading keyboard map err={}", .{err}); + return; + }; + } + /// Create a new surface as part of an app. export fn ghostty_surface_new( app: *App, @@ -524,23 +634,32 @@ pub const CAPI = struct { surface.focusCallback(focused); } - /// Tell the surface that it needs to schedule a render + /// Send this for raw keypresses (i.e. the keyDown event on macOS). + /// This will handle the keymap translation and send the appropriate + /// key and char events. + /// + /// You do NOT need to also send "ghostty_surface_char" unless + /// you want to send a unicode character that is not associated + /// with a keypress, i.e. IME keyboard. export fn ghostty_surface_key( surface: *Surface, action: input.Action, - key: input.Key, - unmapped_key: input.Key, - mods: c_int, + keycode: u32, + c_mods: c_int, ) void { surface.keyCallback( action, - key, - unmapped_key, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(mods))))), - ); + keycode, + @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(c_mods))))), + ) catch |err| { + log.err("error processing key event err={}", .{err}); + return; + }; } - /// Tell the surface that it needs to schedule a render + /// Send for a unicode character. This is used for IME input. This + /// should only be sent for characters that are not the result of + /// key events. export fn ghostty_surface_char(surface: *Surface, codepoint: u32) void { surface.charCallback(codepoint); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 83de490d2..a5cfcc12a 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -282,6 +282,10 @@ pub const Surface = struct { /// A core surface core_surface: CoreSurface, + /// This is set to true when keyCallback consumes the input, suppressing + /// the charCallback from being fired. + key_consumed: bool = false, + pub const Options = struct {}; /// Initialize the surface into the given self pointer. This gives a @@ -586,6 +590,13 @@ pub const Surface = struct { defer tracy.end(); const core_win = window.getUserPointer(CoreSurface) orelse return; + + // If our keyCallback consumed the key input, don't emit a char. + if (core_win.rt_surface.key_consumed) { + core_win.rt_surface.key_consumed = false; + return; + } + core_win.charCallback(codepoint) catch |err| { log.err("error in char callback err={}", .{err}); return; @@ -601,6 +612,11 @@ pub const Surface = struct { ) void { _ = scancode; + const core_win = window.getUserPointer(CoreSurface) orelse return; + + // Reset our consumption state + core_win.rt_surface.key_consumed = false; + const tracy = trace(@src()); defer tracy.end(); @@ -739,8 +755,12 @@ pub const Surface = struct { // TODO: we need to do mapped keybindings - const core_win = window.getUserPointer(CoreSurface) orelse return; - core_win.keyCallback(action, key, key, mods) catch |err| { + core_win.rt_surface.key_consumed = core_win.keyCallback( + action, + key, + key, + mods, + ) catch |err| { log.err("error in key callback err={}", .{err}); return; }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index e1f7be92f..90782765a 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -675,6 +675,13 @@ pub const Surface = struct { cursor_pos: apprt.CursorPos, clipboard: c.GValue, + /// Key input states. See gtkKeyPressed for detailed descriptions. + in_keypress: bool = false, + im_context: *c.GtkIMContext, + im_composing: bool = false, + im_buf: [128]u8 = undefined, + im_len: u7 = 0, + pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget = @as(*c.GtkWidget, @ptrCast(opts.gl_area)); c.gtk_gl_area_set_required_version(opts.gl_area, 3, 3); @@ -694,15 +701,6 @@ pub const Surface = struct { c.gtk_widget_add_controller(widget, ec_focus); errdefer c.gtk_widget_remove_controller(widget, ec_focus); - // Tell the key controller that we're interested in getting a full - // input method so raw characters/strings are given too. - const im_context = c.gtk_im_multicontext_new(); - errdefer c.g_object_unref(im_context); - c.gtk_event_controller_key_set_im_context( - @ptrCast(ec_key), - im_context, - ); - // Create a second key controller so we can receive the raw // key-press events BEFORE the input method gets them. const ec_key_press = c.gtk_event_controller_key_new(); @@ -729,6 +727,12 @@ pub const Surface = struct { errdefer c.g_object_unref(ec_scroll); c.gtk_widget_add_controller(widget, ec_scroll); + // The input method context that we use to translate key events into + // characters. This doesn't have an event key controller attached because + // we call it manually from our own key controller. + const im_context = c.gtk_im_multicontext_new(); + errdefer c.g_object_unref(im_context); + // The GL area has to be focusable so that it can receive events c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); @@ -748,6 +752,7 @@ pub const Surface = struct { .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .clipboard = std.mem.zeroes(c.GValue), + .im_context = im_context, }; errdefer self.* = undefined; @@ -761,11 +766,14 @@ pub const Surface = struct { _ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(>kInputPreeditStart), self, null, G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -792,8 +800,6 @@ pub const Surface = struct { } pub fn deinit(self: *Surface) void { - c.g_value_unset(&self.clipboard); - // We don't allocate anything if we aren't realized. if (!self.realized) return; @@ -803,6 +809,10 @@ pub const Surface = struct { // Clean up our core surface so that all the rendering and IO stop. self.core_surface.deinit(); self.core_surface = undefined; + + // Free all our GTK stuff + c.g_object_unref(self.im_context); + c.g_value_unset(&self.clipboard); } fn render(self: *Surface) !void { @@ -1123,69 +1133,135 @@ pub const Surface = struct { }; } + /// Key press event. This is where we do ALL of our key handling, + /// translation to keyboard layouts, dead key handling, etc. Key handling + /// is complicated so this comment will explain what's going on. + /// + /// At a high level, we want to do the following: + /// + /// 1. Emit a keyCallback for the key press with the right keys. + /// 2. Emit a charCallback if a unicode char was generated from the + /// keypresses, but only if keyCallback didn't consume the input. + /// + /// This callback will first set the "in_keypress" flag to true. This + /// lets our IM callbacks know that we're in a keypress event so they don't + /// emit a charCallback since this function will do it after the keyCallback + /// (remember, the order matters!). + /// + /// Next, we run the keypress through the input method context in order + /// to determine if we're in a dead key state, completed unicode char, etc. + /// This all happens through various callbacks: preedit, commit, etc. + /// These inspect "in_keypress" if they have to and set some instance + /// state. + /// + /// Finally, we map our keys to input.Keys, emit the keyCallback, then + /// emit the charCallback if we have to. + /// + /// Note we ALSO have an IMContext attached directly to the widget + /// which can emit preedit and commit callbacks. But, if we're not + /// in a keypress, we let those automatically work. fn gtkKeyPressed( - _: *c.GtkEventControllerKey, - keyval_event: c.guint, + ec_key: *c.GtkEventControllerKey, + _: c.guint, keycode: c.guint, - state: c.GdkModifierType, + gtk_mods: c.GdkModifierType, ud: ?*anyopaque, ) callconv(.C) c.gboolean { const self = userdataSelf(ud.?); - const display = c.gtk_widget_get_display(@ptrCast(self.gl_area)).?; + const mods = translateMods(gtk_mods); - // We want to use only the key that corresponds to the hardware key. - // I suspect this logic is actually wrong for customized keyboards, - // maybe international keyboards, but I don't have an easy way to - // test that that I know of... sorry! - var keys: [*c]c.GdkKeymapKey = undefined; - var keyvals: [*c]c.guint = undefined; - var keys_len: c_int = undefined; - const found = c.gdk_display_map_keycode(display, keycode, &keys, &keyvals, &keys_len); - defer if (found > 0) { - c.g_free(keys); - c.g_free(keyvals); - }; + // We mark that we're in a keypress event. We use this in our + // IM commit callback to determine if we need to send a char callback + // to the core surface or not. + self.in_keypress = true; + defer self.in_keypress = false; - // We look for the keyval corresponding to this key pressed with - // zero modifiers. We're assuming this always exist but unsure if - // that assumption is true. - const keyval = keyval: { - if (found > 0) { - for (keys[0..@intCast(keys_len)], 0..) |key, i| { - if (key.group == 0 and key.level == 0) - break :keyval keyvals[i]; - } + // We always reset our committed text when ending a keypress so that + // future keypresses don't think we have a commit event. + defer self.im_len = 0; + + // We want to get the physical unmapped key to process physical keybinds. + // (These are keybinds explicitly marked as requesting physical mapping). + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == keycode) break :keycode entry.key; + } else .invalid; + + // Pass the event through the IM controller to handle dead key states. + // Filter is true if the event was handled by the IM controller. + const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); + _ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0; + + // If we aren't composing, then we set our preedit to empty no matter what. + if (!self.im_composing) { + self.core_surface.preeditCallback(null) catch {}; + } + + // If we're not in a dead key state, we want to translate our text + // to some input.Key. + const key = if (!self.im_composing) key: { + if (self.im_len != 1) break :key physical_key; + break :key input.Key.fromASCII(self.im_buf[0]) orelse physical_key; + } else .invalid; + + // If both keys are invalid then we won't call the key callback. But + // if either one is valid, we want to give it a chance. + if (key != .invalid or physical_key != .invalid) { + const consumed = self.core_surface.keyCallback( + .press, + key, + physical_key, + mods, + ) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; + + // If we consume the key then we want to reset the dead key state. + if (consumed) { + c.gtk_im_context_reset(self.im_context); + self.core_surface.preeditCallback(null) catch {}; + return 1; + } + } + + // If this is a dead key, then we're composing a character and + // we end processing here. We don't process keybinds for dead keys. + if (self.im_composing) { + const text = self.im_buf[0..self.im_len]; + const view = std.unicode.Utf8View.init(text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + return 0; + }; + var it = view.iterator(); + + const cp: u21 = it.nextCodepoint() orelse 0; + self.core_surface.preeditCallback(cp) catch |err| { + log.err("error in preedit callback err={}", .{err}); + return 0; + }; + + return 0; + } + + // Next, we want to call the char callback with each codepoint. + if (self.im_len > 0) { + const text = self.im_buf[0..self.im_len]; + const view = std.unicode.Utf8View.init(text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + return 0; + }; + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + self.core_surface.charCallback(cp) catch |err| { + log.err("error in char callback err={}", .{err}); + return 0; + }; } - log.warn("key-press with unknown key keyval={} keycode={}", .{ - keyval_event, - keycode, - }); - return 0; - }; + return 1; + } - const key = translateKey(keyval); - const mods = translateMods(state); - log.debug("key-press code={} key={} mods={}", .{ keycode, key, mods }); - self.core_surface.keyCallback(.press, key, key, mods) catch |err| { - log.err("error in key callback err={}", .{err}); - return 0; - }; - - // We generally just say we didn't handle it. We control our - // GTK environment so for any keys that matter we'll grab them. - // One of the reasons we say we didn't handle it is so that the - // IME can still work. - return switch (keyval) { - // If the key is tab, we say we handled it because we don't want - // tab to move focus from our surface. - c.GDK_KEY_Tab => 1, - // We do the same for up, because that steals focus from the surface, - // in case we have multiple tabs open. - c.GDK_KEY_Up => 1, - - else => 0, - }; + return 0; } fn gtkKeyReleased( @@ -1200,12 +1276,55 @@ pub const Surface = struct { const key = translateKey(keyval); const mods = translateMods(state); const self = userdataSelf(ud.?); - self.core_surface.keyCallback(.release, key, key, mods) catch |err| { + const consumed = self.core_surface.keyCallback(.release, key, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return 0; }; - return 0; + return if (consumed) 1 else 0; + } + + fn gtkInputPreeditStart( + _: *c.GtkIMContext, + ud: ?*anyopaque, + ) callconv(.C) void { + //log.debug("preedit start", .{}); + const self = userdataSelf(ud.?); + if (!self.in_keypress) return; + + // Mark that we are now composing a string with a dead key state. + // We'll record the string in the preedit-changed callback. + self.im_composing = true; + } + + fn gtkInputPreeditChanged( + ctx: *c.GtkIMContext, + ud: ?*anyopaque, + ) callconv(.C) void { + const self = userdataSelf(ud.?); + if (!self.in_keypress) return; + + // Get our pre-edit string that we'll use to show the user. + var buf: [*c]u8 = undefined; + _ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null); + defer c.g_free(buf); + const str = std.mem.sliceTo(buf, 0); + + // Copy the preedit string into the im_buf. This is safe because + // commit will always overwrite this. + self.im_len = @intCast(@min(self.im_buf.len, str.len)); + @memcpy(self.im_buf[0..self.im_len], str); + } + + fn gtkInputPreeditEnd( + _: *c.GtkIMContext, + ud: ?*anyopaque, + ) callconv(.C) void { + //log.debug("preedit end", .{}); + const self = userdataSelf(ud.?); + if (!self.in_keypress) return; + self.im_composing = false; + self.im_len = 0; } fn gtkInputCommit( @@ -1213,13 +1332,30 @@ pub const Surface = struct { bytes: [*:0]u8, ud: ?*anyopaque, ) callconv(.C) void { + const self = userdataSelf(ud.?); const str = std.mem.sliceTo(bytes, 0); + + // If we're in a key event, then we want to buffer the commit so + // that we can send the proper keycallback followed by the char + // callback. + if (self.in_keypress) { + if (str.len <= self.im_buf.len) { + @memcpy(self.im_buf[0..str.len], str); + self.im_len = @intCast(str.len); + } else { + log.warn("not enough buffer space for input method commit", .{}); + } + + return; + } + + // We're not in a keypress, so this was sent from an on-screen emoji + // keyboard or someting like that. Send the characters directly to + // the surface. const view = std.unicode.Utf8View.init(str) catch |err| { log.warn("cannot build utf8 view over input: {}", .{err}); return; }; - - const self = userdataSelf(ud.?); var it = view.iterator(); while (it.nextCodepoint()) |cp| { self.core_surface.charCallback(cp) catch |err| { diff --git a/src/input.zig b/src/input.zig index 53306eb26..a356cefc5 100644 --- a/src/input.zig +++ b/src/input.zig @@ -1,11 +1,20 @@ const std = @import("std"); +const builtin = @import("builtin"); pub usingnamespace @import("input/mouse.zig"); pub usingnamespace @import("input/key.zig"); +pub const keycodes = @import("input/keycodes.zig"); pub const Binding = @import("input/Binding.zig"); pub const SplitDirection = Binding.Action.SplitDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; +// Keymap is only available on macOS right now. We could implement it +// in theory for XKB too on Linux but we don't need it right now. +pub const Keymap = switch (builtin.os.tag) { + .macos => @import("input/KeymapDarwin.zig"), + else => struct {}, +}; + test { std.testing.refAllDecls(@This()); } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 46292ee90..fd6fa940c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -53,11 +53,11 @@ pub fn parse(input: []const u8) !Binding { } } - // If the key starts with "unmapped" then this is an unmapped key. - const unmapped_prefix = "unmapped:"; - const key_part = if (std.mem.startsWith(u8, part, unmapped_prefix)) key_part: { - result.unmapped = true; - break :key_part part[unmapped_prefix.len..]; + // If the key starts with "physical" then this is an physical key. + const physical = "physical:"; + const key_part = if (std.mem.startsWith(u8, part, physical)) key_part: { + result.physical = true; + break :key_part part[physical.len..]; } else part; // Check if its a key @@ -286,18 +286,18 @@ pub const Trigger = struct { /// The key modifiers that must be active for this to match. mods: key.Mods = .{}, - /// key is the "unmapped" version. This is the same as mapped for + /// key is the "physical" version. This is the same as mapped for /// standard US keyboard layouts. For non-US keyboard layouts, this /// is used to bind to a physical key location rather than a translated /// key. - unmapped: bool = false, + physical: bool = false, /// Returns a hash code that can be used to uniquely identify this trigger. pub fn hash(self: Binding) u64 { var hasher = std.hash.Wyhash.init(0); std.hash.autoHash(&hasher, self.key); std.hash.autoHash(&hasher, self.mods); - std.hash.autoHash(&hasher, self.unmapped); + std.hash.autoHash(&hasher, self.physical); return hasher.final(); } }; @@ -382,15 +382,15 @@ test "parse: triggers" { .action = .{ .ignore = {} }, }, try parse("a+shift=ignore")); - // unmapped keys + // physical keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .a, - .unmapped = true, + .physical = true, }, .action = .{ .ignore = {} }, - }, try parse("shift+unmapped:a=ignore")); + }, try parse("shift+physical:a=ignore")); // invalid key try testing.expectError(Error.InvalidFormat, parse("foo=ignore")); diff --git a/src/input/KeymapDarwin.zig b/src/input/KeymapDarwin.zig new file mode 100644 index 000000000..8cd453e2e --- /dev/null +++ b/src/input/KeymapDarwin.zig @@ -0,0 +1,237 @@ +// Keymap is responsible for translating keyboard inputs into localized chars. +/// +/// For example, the physical key "S" on a US-layout keyboard might mean "O" +/// in Dvorak. On international keyboard layouts, it may require multiple +/// keystrokes to produce a single character that is otherwise a single +/// keystroke on a US-layout keyboard. +/// +/// This information is critical to know for many reasons. For keybindings, +/// if a user configures "ctrl+o" to do something, it should work with the +/// physical "ctrl+S" key on a Dvorak keyboard and so on. +/// +/// This is currently only implemented for macOS. +const Keymap = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const macos = @import("macos"); +const codes = @import("keycodes.zig").entries; +const Key = @import("key.zig").Key; +const Mods = @import("key.zig").Mods; + +/// The current input source that is selected for the keyboard. This can +/// and does change whenever the user selects a new keyboard layout. This +/// change doesn't happen automatically; the user of this struct has to +/// detect it and then call `reload` to update the keymap. +source: *TISInputSource, + +/// The keyboard layout for the current input source. +/// +/// This doesn't need to be freed because its owned by the InputSource. +unicode_layout: *const UCKeyboardLayout, + +pub const Error = error{ + GetInputSourceFailed, + TranslateFailed, +}; + +/// The state that has to be passed in with each call to translate. +/// The contents of this are meant to mostly be opaque and can change +/// for platform-specific reasons. +pub const State = struct { + dead_key: u32 = 0, +}; + +/// The result of a translation. The result of a translation can be multiple +/// states. For example, if the user types a dead key, the result will be +/// "composing" since they're still in the process of composing a full +/// character. +pub const Translation = struct { + /// The translation result. If this is a dead key state, then this will + /// be pre-edit text that can be displayed but will ultimately be replaced. + text: []const u8, + + /// Whether the text is still composing, i.e. this is a dead key state. + composing: bool, +}; + +pub fn init() !Keymap { + var keymap: Keymap = .{ .source = undefined, .unicode_layout = undefined }; + try keymap.reinit(); + return keymap; +} + +pub fn deinit(self: *const Keymap) void { + macos.foundation.CFRelease(self.source); +} + +/// Reload the keymap. This must be called if the user changes their +/// keyboard layout. +pub fn reload(self: *Keymap) !void { + macos.foundation.CFRelease(self.source); + try self.reinit(); +} + +/// Reinit reinitializes the keymap. It assumes that all the memory associated +/// with the keymap is already freed. +fn reinit(self: *Keymap) !void { + self.source = TISCopyCurrentKeyboardLayoutInputSource() orelse + return Error.GetInputSourceFailed; + + self.unicode_layout = layout: { + // This returns a CFDataRef + const data_raw = TISGetInputSourceProperty( + self.source, + kTISPropertyUnicodeKeyLayoutData, + ) orelse return Error.GetInputSourceFailed; + const data: *CFData = @ptrCast(data_raw); + + // The CFDataRef contains a UCKeyboardLayout pointer + break :layout @ptrCast(data.getPointer()); + }; +} + +/// Translate a single key input into a utf8 sequence. +pub fn translate( + self: *const Keymap, + out: []u8, + state: *State, + code: u16, + mods: Mods, +) !Translation { + // Get the keycode for the space key, using comptime. + const code_space: u16 = comptime space: for (codes) |entry| { + if (std.mem.eql(u8, entry.code, "Space")) + break :space entry.native; + } else @compileError("space code not found"); + + // Convert our mods from our format to the Carbon API format + const modifier_state: u32 = (MacMods{ + .alt = if (mods.alt) true else false, + .ctrl = if (mods.ctrl) true else false, + .meta = if (mods.super) true else false, + .shift = if (mods.shift) true else false, + }).ucKeyTranslate(); + + // We use 4 here because the Chromium source code uses 4 and Chrome + // works pretty well. They have a todo to look into longer sequences + // but given how mature that software is I think this is fine. + // + // From Chromium: + // Per Apple docs, the buffer length can be up to 255 but is rarely more than 4. + // https://developer.apple.com/documentation/coreservices/1390584-uckeytranslate + var char: [4]u16 = undefined; + var char_count: c_ulong = 0; + if (UCKeyTranslate( + self.unicode_layout, + code, + kUCKeyActionDown, + modifier_state, + LMGetKbdType(), + kUCKeyTranslateNoDeadKeysBit, + &state.dead_key, + char.len, + &char_count, + &char, + ) != 0) return Error.TranslateFailed; + + // If we got a dead key, then we translate again with "space" + // in order to get the pre-edit text. + const composing = if (state.dead_key != 0 and char_count == 0) composing: { + // We need to copy our dead key state so that it isn't modified. + var dead_key_ignore: u32 = state.dead_key; + if (UCKeyTranslate( + self.unicode_layout, + code_space, + kUCKeyActionDown, + modifier_state, + LMGetKbdType(), + kUCKeyTranslateNoDeadKeysMask, + &dead_key_ignore, + char.len, + &char_count, + &char, + ) != 0) return Error.TranslateFailed; + break :composing true; + } else false; + + // Convert the utf16 to utf8 + const len = try std.unicode.utf16leToUtf8(out, char[0..char_count]); + return .{ .text = out[0..len], .composing = composing }; +} + +/// Map to the modifiers format used by the UCKeyTranslate function. +/// We use a u32 here because our bit arithmetic is all u32 anyways. +const MacMods = packed struct(u32) { + _padding_start: u16 = 0, + caps_lock: bool = false, + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + meta: bool = false, + num_lock: bool = false, + help: bool = false, + function: bool = false, + _padding_end: u8 = 0, + + /// Translate NSEventModifierFlags into the format used by UCKeyTranslate. + fn ucKeyTranslate(self: MacMods) u32 { + const int: u32 = @bitCast(self); + return (int >> 16) & 0xFF; + } + + comptime { + // Just to be super sure + const v: u32 = @bitCast(MacMods{ .shift = true }); + std.debug.assert(v == 1 << 17); + } +}; + +// The documentation for all of these types and functions is in the macOS SDK: +// Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/TextInputSources.h +extern "c" fn TISCopyCurrentKeyboardLayoutInputSource() ?*TISInputSource; +extern "c" fn TISGetInputSourceProperty(*TISInputSource, *CFString) ?*anyopaque; +extern "c" fn LMGetKbdLast() u8; +extern "c" fn LMGetKbdType() u8; +extern "c" fn UCKeyTranslate(*const UCKeyboardLayout, u16, u16, u32, u32, u32, *u32, c_ulong, *c_ulong, [*]u16) i32; +extern const kTISPropertyLocalizedName: *CFString; +extern const kTISPropertyUnicodeKeyLayoutData: *CFString; +const TISInputSource = opaque {}; +const UCKeyboardLayout = opaque {}; +const kUCKeyActionDown: u16 = 0; +const kUCKeyActionUp: u16 = 1; +const kUCKeyActionAutoKey: u16 = 2; +const kUCKeyActionDisplay: u16 = 3; +const kUCKeyTranslateNoDeadKeysBit: u32 = 0; +const kUCKeyTranslateNoDeadKeysMask: u32 = 1 << kUCKeyTranslateNoDeadKeysBit; + +const CFData = macos.foundation.Data; +const CFString = macos.foundation.String; + +test { + var keymap = try init(); + defer keymap.deinit(); + + // These tests are all commented because they depend on the user-selected + // keyboard layout... + // + // // Single quote ' which is fine on US, but dead on US-International + // var buf: [4]u8 = undefined; + // var state: State = .{}; + // { + // const result = try keymap.translate(&buf, &state, 0x27, .{}); + // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing }); + // } + // + // // Then type "a" which should combine with the dead key to make á + // { + // const result = try keymap.translate(&buf, &state, 0x00, .{}); + // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing }); + // } + // + // // Shift+1 = ! on US + // { + // const result = try keymap.translate(&buf, &state, 0x12, .{ .shift = true }); + // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing }); + // } +} diff --git a/src/input/key.zig b/src/input/key.zig index 21261de65..6ef6ae38d 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -180,4 +180,140 @@ pub const Key = enum(c_int) { // To support more keys (there are obviously more!) add them here // and ensure the mapping is up to date in the Window key handler. + + /// Converts an ASCII character to a key, if possible. This returns + /// null if the character is unknown. + /// + /// Note that this can't distinguish between physical keys, i.e. '0' + /// may be from the number row or the keypad, but it always maps + /// to '.zero'. + /// + /// This is what we want, we awnt people to create keybindings that + /// are independent of the physical key. + pub fn fromASCII(ch: u8) ?Key { + return switch (ch) { + 'a' => .a, + 'b' => .b, + 'c' => .c, + 'd' => .d, + 'e' => .e, + 'f' => .f, + 'g' => .g, + 'h' => .h, + 'i' => .i, + 'j' => .j, + 'k' => .k, + 'l' => .l, + 'm' => .m, + 'n' => .n, + 'o' => .o, + 'p' => .p, + 'q' => .q, + 'r' => .r, + 's' => .s, + 't' => .t, + 'u' => .u, + 'v' => .v, + 'w' => .w, + 'x' => .x, + 'y' => .y, + 'z' => .z, + '0' => .zero, + '1' => .one, + '2' => .two, + '3' => .three, + '4' => .four, + '5' => .five, + '6' => .six, + '7' => .seven, + '8' => .eight, + '9' => .nine, + ';' => .semicolon, + ' ' => .space, + '\'' => .apostrophe, + ',' => .comma, + '`' => .grave_accent, + '.' => .period, + '/' => .slash, + '-' => .minus, + '=' => .equal, + '[' => .left_bracket, + ']' => .right_bracket, + '\\' => .backslash, + else => null, + }; + } + + /// True if this key represents a printable character. + pub fn printable(self: Key) bool { + return switch (self) { + .a, + .b, + .c, + .d, + .e, + .f, + .g, + .h, + .i, + .j, + .k, + .l, + .m, + .n, + .o, + .p, + .q, + .r, + .s, + .t, + .u, + .v, + .w, + .x, + .y, + .z, + .zero, + .one, + .two, + .three, + .four, + .five, + .six, + .seven, + .eight, + .nine, + .semicolon, + .space, + .apostrophe, + .comma, + .grave_accent, + .period, + .slash, + .minus, + .equal, + .left_bracket, + .right_bracket, + .backslash, + .kp_0, + .kp_1, + .kp_2, + .kp_3, + .kp_4, + .kp_5, + .kp_6, + .kp_7, + .kp_8, + .kp_9, + .kp_decimal, + .kp_divide, + .kp_multiply, + .kp_subtract, + .kp_add, + .kp_equal, + => true, + + else => false, + }; + } }; diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig new file mode 100644 index 000000000..d29158bc9 --- /dev/null +++ b/src/input/keycodes.zig @@ -0,0 +1,668 @@ +// Based on the Chromium source. The Chromium source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/keycodes/dom/dom_code_data.inc + +const std = @import("std"); +const builtin = @import("builtin"); +const Key = @import("key.zig").Key; + +/// The full list of entries for the current platform. +pub const entries: []const Entry = entries: { + const native_idx = switch (builtin.os.tag) { + .macos => 4, // mac + .windows => 3, // win + .linux => 2, // xkb + else => @compileError("unsupported platform"), + }; + + var result: [raw_entries.len]Entry = undefined; + for (raw_entries, 0..) |raw, i| { + @setEvalBranchQuota(10000); + result[i] = .{ + .key = code_to_key.get(raw[5]) orelse .invalid, + .usb = raw[0], + .code = raw[5], + .native = raw[native_idx], + }; + } + break :entries &result; +}; + +/// Entry contains the USB code, native keycode, and W3C dom code for +/// the current platform. +pub const Entry = struct { + key: Key, // input key enum + usb: u32, // USB HID usage code + native: u32, // Native keycode + code: []const u8, // W3C DOM code, static memory +}; + +/// A map from code to key. This isn't meant to be used at runtime +/// (though it could), so it isn't exported. It it used to build the +/// key value for Entry. +const code_to_key = code_to_key: { + @setEvalBranchQuota(5000); + break :code_to_key std.ComptimeStringMap(Key, .{ + .{ "KeyA", .a }, + .{ "KeyB", .b }, + .{ "KeyC", .c }, + .{ "KeyD", .d }, + .{ "KeyE", .e }, + .{ "KeyF", .f }, + .{ "KeyG", .g }, + .{ "KeyH", .h }, + .{ "KeyI", .i }, + .{ "KeyJ", .j }, + .{ "KeyK", .k }, + .{ "KeyL", .l }, + .{ "KeyM", .m }, + .{ "KeyN", .n }, + .{ "KeyO", .o }, + .{ "KeyP", .p }, + .{ "KeyQ", .q }, + .{ "KeyR", .r }, + .{ "KeyS", .s }, + .{ "KeyT", .t }, + .{ "KeyU", .u }, + .{ "KeyV", .v }, + .{ "KeyW", .w }, + .{ "KeyX", .x }, + .{ "KeyY", .y }, + .{ "KeyZ", .z }, + .{ "Digit1", .one }, + .{ "Digit2", .two }, + .{ "Digit3", .three }, + .{ "Digit4", .four }, + .{ "Digit5", .five }, + .{ "Digit6", .six }, + .{ "Digit7", .seven }, + .{ "Digit8", .eight }, + .{ "Digit9", .nine }, + .{ "Digit0", .zero }, + .{ "Enter", .enter }, + .{ "Escape", .escape }, + .{ "Backspace", .backspace }, + .{ "Tab", .tab }, + .{ "Space", .space }, + .{ "Minus", .minus }, + .{ "Equal", .equal }, + .{ "BracketLeft", .left_bracket }, + .{ "BracketRight", .right_bracket }, + .{ "Backslash", .backslash }, + .{ "Semicolon", .semicolon }, + .{ "Quote", .apostrophe }, + .{ "Backquote", .grave_accent }, + .{ "Comma", .comma }, + .{ "Period", .period }, + .{ "Slash", .slash }, + .{ "CapsLock", .caps_lock }, + .{ "F1", .f1 }, + .{ "F2", .f2 }, + .{ "F3", .f3 }, + .{ "F4", .f4 }, + .{ "F5", .f5 }, + .{ "F6", .f6 }, + .{ "F7", .f7 }, + .{ "F8", .f8 }, + .{ "F9", .f9 }, + .{ "F10", .f10 }, + .{ "F11", .f11 }, + .{ "F12", .f12 }, + .{ "F13", .f13 }, + .{ "F14", .f14 }, + .{ "F15", .f15 }, + .{ "F16", .f16 }, + .{ "F17", .f17 }, + .{ "F18", .f18 }, + .{ "F19", .f19 }, + .{ "F20", .f20 }, + .{ "F21", .f21 }, + .{ "F22", .f22 }, + .{ "F23", .f23 }, + .{ "F24", .f24 }, + .{ "PrintScreen", .print_screen }, + .{ "ScrollLock", .scroll_lock }, + .{ "Pause", .pause }, + .{ "Insert", .insert }, + .{ "Home", .home }, + .{ "PageUp", .page_up }, + .{ "Delete", .delete }, + .{ "End", .end }, + .{ "PageDown", .page_down }, + .{ "ArrowRight", .right }, + .{ "ArrowLeft", .left }, + .{ "ArrowDown", .down }, + .{ "ArrowUp", .up }, + .{ "NumLock", .num_lock }, + .{ "NumpadDivide", .kp_divide }, + .{ "NumpadMultiply", .kp_multiply }, + .{ "NumpadSubtract", .kp_subtract }, + .{ "NumpadAdd", .kp_add }, + .{ "NumpadEnter", .kp_enter }, + .{ "Numpad1", .kp_1 }, + .{ "Numpad2", .kp_2 }, + .{ "Numpad3", .kp_3 }, + .{ "Numpad4", .kp_4 }, + .{ "Numpad5", .kp_5 }, + .{ "Numpad6", .kp_6 }, + .{ "Numpad7", .kp_7 }, + .{ "Numpad8", .kp_8 }, + .{ "Numpad9", .kp_9 }, + .{ "Numpad0", .kp_0 }, + .{ "NumpadDecimal", .kp_decimal }, + .{ "NumpadEqual", .kp_equal }, + .{ "ControlLeft", .left_control }, + .{ "ShiftLeft", .left_shift }, + .{ "AltLeft", .left_alt }, + .{ "MetaLeft", .left_super }, + .{ "ControlRight", .right_control }, + .{ "ShiftRight", .right_shift }, + .{ "AltRight", .right_alt }, + .{ "MetaRight", .right_super }, + }); +}; + +/// The codes for the table from the Chromium data set. These are ALL the +/// codes, not just the ones that are supported by the current platform. +/// These are `pub` but you shouldn't use this because it uses way more +/// memory than is necessary. +/// +/// The format is: usb, evdev, xkb, win, mac, code +pub const RawEntry = struct { u32, u32, u32, u32, u32, []const u8 }; + +/// All of the full entries. This is marked pub but it should NOT be referenced +/// directly because it contains too much data for normal usage. Use `entries` +/// instead which contains just the relevant data for the target platform. +pub const raw_entries: []const RawEntry = &.{ + // USB evdev XKB Win Mac Code + .{ 0x000000, 0x0000, 0x0000, 0x0000, 0xffff, "" }, + + // ========================================= + // Non-USB codes + // ========================================= + + // USB evdev XKB Win Mac Code + .{ 0x000010, 0x0000, 0x0000, 0x0000, 0xffff, "Hyper" }, + .{ 0x000011, 0x0000, 0x0000, 0x0000, 0xffff, "Super" }, + .{ 0x000012, 0x0000, 0x0000, 0x0000, 0xffff, "Fn" }, + .{ 0x000013, 0x0000, 0x0000, 0x0000, 0xffff, "FnLock" }, + .{ 0x000014, 0x0000, 0x0000, 0x0000, 0xffff, "Suspend" }, + .{ 0x000015, 0x0000, 0x0000, 0x0000, 0xffff, "Resume" }, + .{ 0x000016, 0x0000, 0x0000, 0x0000, 0xffff, "Turbo" }, + + // ========================================= + // USB Usage Page 0x01: Generic Desktop Page + // ========================================= + + // Sleep could be encoded as USB#0c0032, but there's no corresponding WakeUp + // in the 0x0c USB page. + // USB evdev XKB Win Mac + .{ 0x010082, 0x008e, 0x0096, 0xe05f, 0xffff, "Sleep" }, + .{ 0x010083, 0x008f, 0x0097, 0xe063, 0xffff, "WakeUp" }, + .{ 0x0100a9, 0x00f8, 0x0100, 0x0000, 0xffff, "" }, + .{ 0x0100b5, 0x00e3, 0x00eb, 0x0000, 0xffff, "DisplayToggleIntExt" }, + + // ========================================= + // USB Usage Page 0x07: Keyboard/Keypad Page + // ========================================= + + // TODO(garykac): + // XKB#005c ISO Level3 Shift (AltGr) + // XKB#005e <>|| + // XKB#006d Linefeed + // XKB#008a SunProps cf. USB#0700a3 CrSel/Props + // XKB#008e SunOpen + // Mac#003f kVK_Function + // Mac#000a kVK_ISO_Section (ISO keyboards only) + // Mac#0066 kVK_JIS_Eisu (USB#07008a Henkan?) + + // USB evdev XKB Win Mac + .{ 0x070000, 0x0000, 0x0000, 0x0000, 0xffff, "" }, + .{ 0x070001, 0x0000, 0x0000, 0x00ff, 0xffff, "" }, + .{ 0x070002, 0x0000, 0x0000, 0x00fc, 0xffff, "" }, + .{ 0x070003, 0x0000, 0x0000, 0x0000, 0xffff, "" }, + .{ 0x070004, 0x001e, 0x0026, 0x001e, 0x0000, "KeyA" }, + .{ 0x070005, 0x0030, 0x0038, 0x0030, 0x000b, "KeyB" }, + .{ 0x070006, 0x002e, 0x0036, 0x002e, 0x0008, "KeyC" }, + .{ 0x070007, 0x0020, 0x0028, 0x0020, 0x0002, "KeyD" }, + + .{ 0x070008, 0x0012, 0x001a, 0x0012, 0x000e, "KeyE" }, + .{ 0x070009, 0x0021, 0x0029, 0x0021, 0x0003, "KeyF" }, + .{ 0x07000a, 0x0022, 0x002a, 0x0022, 0x0005, "KeyG" }, + .{ 0x07000b, 0x0023, 0x002b, 0x0023, 0x0004, "KeyH" }, + .{ 0x07000c, 0x0017, 0x001f, 0x0017, 0x0022, "KeyI" }, + .{ 0x07000d, 0x0024, 0x002c, 0x0024, 0x0026, "KeyJ" }, + .{ 0x07000e, 0x0025, 0x002d, 0x0025, 0x0028, "KeyK" }, + .{ 0x07000f, 0x0026, 0x002e, 0x0026, 0x0025, "KeyL" }, + + .{ 0x070010, 0x0032, 0x003a, 0x0032, 0x002e, "KeyM" }, + .{ 0x070011, 0x0031, 0x0039, 0x0031, 0x002d, "KeyN" }, + .{ 0x070012, 0x0018, 0x0020, 0x0018, 0x001f, "KeyO" }, + .{ 0x070013, 0x0019, 0x0021, 0x0019, 0x0023, "KeyP" }, + .{ 0x070014, 0x0010, 0x0018, 0x0010, 0x000c, "KeyQ" }, + .{ 0x070015, 0x0013, 0x001b, 0x0013, 0x000f, "KeyR" }, + .{ 0x070016, 0x001f, 0x0027, 0x001f, 0x0001, "KeyS" }, + .{ 0x070017, 0x0014, 0x001c, 0x0014, 0x0011, "KeyT" }, + + .{ 0x070018, 0x0016, 0x001e, 0x0016, 0x0020, "KeyU" }, + .{ 0x070019, 0x002f, 0x0037, 0x002f, 0x0009, "KeyV" }, + .{ 0x07001a, 0x0011, 0x0019, 0x0011, 0x000d, "KeyW" }, + .{ 0x07001b, 0x002d, 0x0035, 0x002d, 0x0007, "KeyX" }, + .{ 0x07001c, 0x0015, 0x001d, 0x0015, 0x0010, "KeyY" }, + .{ 0x07001d, 0x002c, 0x0034, 0x002c, 0x0006, "KeyZ" }, + .{ 0x07001e, 0x0002, 0x000a, 0x0002, 0x0012, "Digit1" }, + .{ 0x07001f, 0x0003, 0x000b, 0x0003, 0x0013, "Digit2" }, + + .{ 0x070020, 0x0004, 0x000c, 0x0004, 0x0014, "Digit3" }, + .{ 0x070021, 0x0005, 0x000d, 0x0005, 0x0015, "Digit4" }, + .{ 0x070022, 0x0006, 0x000e, 0x0006, 0x0017, "Digit5" }, + .{ 0x070023, 0x0007, 0x000f, 0x0007, 0x0016, "Digit6" }, + .{ 0x070024, 0x0008, 0x0010, 0x0008, 0x001a, "Digit7" }, + .{ 0x070025, 0x0009, 0x0011, 0x0009, 0x001c, "Digit8" }, + .{ 0x070026, 0x000a, 0x0012, 0x000a, 0x0019, "Digit9" }, + .{ 0x070027, 0x000b, 0x0013, 0x000b, 0x001d, "Digit0" }, + + .{ 0x070028, 0x001c, 0x0024, 0x001c, 0x0024, "Enter" }, + .{ 0x070029, 0x0001, 0x0009, 0x0001, 0x0035, "Escape" }, + .{ 0x07002a, 0x000e, 0x0016, 0x000e, 0x0033, "Backspace" }, + .{ 0x07002b, 0x000f, 0x0017, 0x000f, 0x0030, "Tab" }, + .{ 0x07002c, 0x0039, 0x0041, 0x0039, 0x0031, "Space" }, + .{ 0x07002d, 0x000c, 0x0014, 0x000c, 0x001b, "Minus" }, + .{ 0x07002e, 0x000d, 0x0015, 0x000d, 0x0018, "Equal" }, + .{ 0x07002f, 0x001a, 0x0022, 0x001a, 0x0021, "BracketLeft" }, + + .{ 0x070030, 0x001b, 0x0023, 0x001b, 0x001e, "BracketRight" }, + .{ 0x070031, 0x002b, 0x0033, 0x002b, 0x002a, "Backslash" }, + // USB#070032 never appears on keyboards that have USB#070031. + // Platforms use the same scancode as for the two keys. + // Hence this code can only be generated synthetically + // (e.g. in a DOM Level 3 KeyboardEvent). + // The keycap varies on international keyboards: + // Dan: '* Dutch: <> Ger: #' UK: #~ + // TODO(garykac): Verify Mac intl keyboard. + //.{ 0x070032, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x070033, 0x0027, 0x002f, 0x0027, 0x0029, "Semicolon" }, + .{ 0x070034, 0x0028, 0x0030, 0x0028, 0x0027, "Quote" }, + .{ 0x070035, 0x0029, 0x0031, 0x0029, 0x0032, "Backquote" }, + .{ 0x070036, 0x0033, 0x003b, 0x0033, 0x002b, "Comma" }, + .{ 0x070037, 0x0034, 0x003c, 0x0034, 0x002f, "Period" }, + + .{ 0x070038, 0x0035, 0x003d, 0x0035, 0x002c, "Slash" }, + // TODO(garykac): CapsLock requires special handling for each platform. + .{ 0x070039, 0x003a, 0x0042, 0x003a, 0x0039, "CapsLock" }, + .{ 0x07003a, 0x003b, 0x0043, 0x003b, 0x007a, "F1" }, + .{ 0x07003b, 0x003c, 0x0044, 0x003c, 0x0078, "F2" }, + .{ 0x07003c, 0x003d, 0x0045, 0x003d, 0x0063, "F3" }, + .{ 0x07003d, 0x003e, 0x0046, 0x003e, 0x0076, "F4" }, + .{ 0x07003e, 0x003f, 0x0047, 0x003f, 0x0060, "F5" }, + .{ 0x07003f, 0x0040, 0x0048, 0x0040, 0x0061, "F6" }, + + .{ 0x070040, 0x0041, 0x0049, 0x0041, 0x0062, "F7" }, + .{ 0x070041, 0x0042, 0x004a, 0x0042, 0x0064, "F8" }, + .{ 0x070042, 0x0043, 0x004b, 0x0043, 0x0065, "F9" }, + .{ 0x070043, 0x0044, 0x004c, 0x0044, 0x006d, "F10" }, + .{ 0x070044, 0x0057, 0x005f, 0x0057, 0x0067, "F11" }, + .{ 0x070045, 0x0058, 0x0060, 0x0058, 0x006f, "F12" }, + // PrintScreen is effectively F13 on Mac OS X. + .{ 0x070046, 0x0063, 0x006b, 0xe037, 0xffff, "PrintScreen" }, + .{ 0x070047, 0x0046, 0x004e, 0x0046, 0xffff, "ScrollLock" }, + + .{ 0x070048, 0x0077, 0x007f, 0x0045, 0xffff, "Pause" }, + // USB#0x070049 Insert, labeled "Help/Insert" on Mac -- see note M1 at top. + .{ 0x070049, 0x006e, 0x0076, 0xe052, 0x0072, "Insert" }, + .{ 0x07004a, 0x0066, 0x006e, 0xe047, 0x0073, "Home" }, + .{ 0x07004b, 0x0068, 0x0070, 0xe049, 0x0074, "PageUp" }, + // Delete (Forward Delete) named DEL because DELETE conflicts with + .{ 0x07004c, 0x006f, 0x0077, 0xe053, 0x0075, "Delete" }, + .{ 0x07004d, 0x006b, 0x0073, 0xe04f, 0x0077, "End" }, + .{ 0x07004e, 0x006d, 0x0075, 0xe051, 0x0079, "PageDown" }, + .{ 0x07004f, 0x006a, 0x0072, 0xe04d, 0x007c, "ArrowRight" }, + + .{ 0x070050, 0x0069, 0x0071, 0xe04b, 0x007b, "ArrowLeft" }, + .{ 0x070051, 0x006c, 0x0074, 0xe050, 0x007d, "ArrowDown" }, + .{ 0x070052, 0x0067, 0x006f, 0xe048, 0x007e, "ArrowUp" }, + .{ 0x070053, 0x0045, 0x004d, 0xe045, 0x0047, "NumLock" }, + .{ 0x070054, 0x0062, 0x006a, 0xe035, 0x004b, "NumpadDivide" }, + .{ 0x070055, 0x0037, 0x003f, 0x0037, 0x0043, "NumpadMultiply" }, + .{ 0x070056, 0x004a, 0x0052, 0x004a, 0x004e, "NumpadSubtract" }, + .{ 0x070057, 0x004e, 0x0056, 0x004e, 0x0045, "NumpadAdd" }, + + .{ 0x070058, 0x0060, 0x0068, 0xe01c, 0x004c, "NumpadEnter" }, + .{ 0x070059, 0x004f, 0x0057, 0x004f, 0x0053, "Numpad1" }, + .{ 0x07005a, 0x0050, 0x0058, 0x0050, 0x0054, "Numpad2" }, + .{ 0x07005b, 0x0051, 0x0059, 0x0051, 0x0055, "Numpad3" }, + .{ 0x07005c, 0x004b, 0x0053, 0x004b, 0x0056, "Numpad4" }, + .{ 0x07005d, 0x004c, 0x0054, 0x004c, 0x0057, "Numpad5" }, + .{ 0x07005e, 0x004d, 0x0055, 0x004d, 0x0058, "Numpad6" }, + .{ 0x07005f, 0x0047, 0x004f, 0x0047, 0x0059, "Numpad7" }, + + .{ 0x070060, 0x0048, 0x0050, 0x0048, 0x005b, "Numpad8" }, + .{ 0x070061, 0x0049, 0x0051, 0x0049, 0x005c, "Numpad9" }, + .{ 0x070062, 0x0052, 0x005a, 0x0052, 0x0052, "Numpad0" }, + .{ 0x070063, 0x0053, 0x005b, 0x0053, 0x0041, "NumpadDecimal" }, + // USB#070064 is not present on US keyboard. + // This key is typically located near LeftShift key. + // The keycap varies on international keyboards: + // Dan: <> Dutch: ][ Ger: <> UK: \| + .{ 0x070064, 0x0056, 0x005e, 0x0056, 0x000a, "IntlBackslash" }, + // USB#0x070065 Application Menu (next to RWin key) -- see note L2 at top. + .{ 0x070065, 0x007f, 0x0087, 0xe05d, 0x006e, "ContextMenu" }, + .{ 0x070066, 0x0074, 0x007c, 0xe05e, 0xffff, "Power" }, + .{ 0x070067, 0x0075, 0x007d, 0x0059, 0x0051, "NumpadEqual" }, + + .{ 0x070068, 0x00b7, 0x00bf, 0x0064, 0x0069, "F13" }, + .{ 0x070069, 0x00b8, 0x00c0, 0x0065, 0x006b, "F14" }, + .{ 0x07006a, 0x00b9, 0x00c1, 0x0066, 0x0071, "F15" }, + .{ 0x07006b, 0x00ba, 0x00c2, 0x0067, 0x006a, "F16" }, + .{ 0x07006c, 0x00bb, 0x00c3, 0x0068, 0x0040, "F17" }, + .{ 0x07006d, 0x00bc, 0x00c4, 0x0069, 0x004f, "F18" }, + .{ 0x07006e, 0x00bd, 0x00c5, 0x006a, 0x0050, "F19" }, + .{ 0x07006f, 0x00be, 0x00c6, 0x006b, 0x005a, "F20" }, + + .{ 0x070070, 0x00bf, 0x00c7, 0x006c, 0xffff, "F21" }, + .{ 0x070071, 0x00c0, 0x00c8, 0x006d, 0xffff, "F22" }, + .{ 0x070072, 0x00c1, 0x00c9, 0x006e, 0xffff, "F23" }, + // USB#0x070073 -- see note W1 at top. + .{ 0x070073, 0x00c2, 0x00ca, 0x0076, 0xffff, "F24" }, + .{ 0x070074, 0x0086, 0x008e, 0x0000, 0xffff, "Open" }, + // USB#0x070075 Help -- see note M1 at top. + .{ 0x070075, 0x008a, 0x0092, 0xe03b, 0xffff, "Help" }, + // USB#0x070076 Keyboard Menu -- see note L2 at top. + //.{ 0x070076, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x070077, 0x0084, 0x008c, 0x0000, 0xffff, "Select" }, + + //.{ 0x070078, 0x0080, 0x0088, 0x0000, 0xffff, ""}, + .{ 0x070079, 0x0081, 0x0089, 0x0000, 0xffff, "Again" }, + .{ 0x07007a, 0x0083, 0x008b, 0xe008, 0xffff, "Undo" }, + .{ 0x07007b, 0x0089, 0x0091, 0xe017, 0xffff, "Cut" }, + .{ 0x07007c, 0x0085, 0x008d, 0xe018, 0xffff, "Copy" }, + .{ 0x07007d, 0x0087, 0x008f, 0xe00a, 0xffff, "Paste" }, + .{ 0x07007e, 0x0088, 0x0090, 0x0000, 0xffff, "Find" }, + .{ 0x07007f, 0x0071, 0x0079, 0xe020, 0x004a, "AudioVolumeMute" }, + + .{ 0x070080, 0x0073, 0x007b, 0xe030, 0x0048, "AudioVolumeUp" }, + .{ 0x070081, 0x0072, 0x007a, 0xe02e, 0x0049, "AudioVolumeDown" }, + //.{ 0x070082, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x070083, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x070084, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x070085, 0x0079, 0x0081, 0x007e, 0x005f, "NumpadComma" }, + + // International1 + // USB#070086 is used on AS/400 keyboards. Standard Keypad_= is USB#070067. + //.{ 0x070086, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // USB#070087 is used for Brazilian /? and Japanese _ 'ro'. + .{ 0x070087, 0x0059, 0x0061, 0x0073, 0x005e, "IntlRo" }, + // International2 + // USB#070088 is used as Japanese Hiragana/Katakana key. + .{ 0x070088, 0x005d, 0x0065, 0x0070, 0xffff, "KanaMode" }, + // International3 + // USB#070089 is used as Japanese Yen key. + .{ 0x070089, 0x007c, 0x0084, 0x007d, 0x005d, "IntlYen" }, + // International4 + // USB#07008a is used as Japanese Henkan (Convert) key. + .{ 0x07008a, 0x005c, 0x0064, 0x0079, 0xffff, "Convert" }, + // International5 + // USB#07008b is used as Japanese Muhenkan (No-convert) key. + .{ 0x07008b, 0x005e, 0x0066, 0x007b, 0xffff, "NonConvert" }, + //.{ 0x07008c, 0x005f, 0x0067, 0x005c, 0xffff, ""}, + //.{ 0x07008d, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07008e, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07008f, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + // LANG1 + // USB#070090 is used as Korean Hangul/English toggle key, and as the Kana key + // on the Apple Japanese keyboard. + .{ 0x070090, 0x007a, 0x0082, 0x0072, 0x0068, "Lang1" }, + // LANG2 + // USB#070091 is used as Korean Hanja conversion key, and as the Eisu key on + // the Apple Japanese keyboard. + .{ 0x070091, 0x007b, 0x0083, 0x0071, 0x0066, "Lang2" }, + // LANG3 + // USB#070092 is used as Japanese Katakana key. + .{ 0x070092, 0x005a, 0x0062, 0x0078, 0xffff, "Lang3" }, + // LANG4 + // USB#070093 is used as Japanese Hiragana key. + .{ 0x070093, 0x005b, 0x0063, 0x0077, 0xffff, "Lang4" }, + // LANG5 + // USB#070094 is used as Japanese Zenkaku/Hankaku (Fullwidth/halfwidth) key. + // Not mapped on Windows -- see note W1 at top. + .{ 0x070094, 0x0055, 0x005d, 0x0000, 0xffff, "Lang5" }, + //.{ 0x070095, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x070096, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x070097, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x070098, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + //.{ 0x070099, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07009a, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x07009b, 0x0000, 0x0000, 0x0000, 0xffff, "Abort" }, + // USB#0x07009c Keyboard Clear -- see note L1 at top. + //.{ 0x07009c, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07009d, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07009e, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x07009f, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + //.{ 0x0700a0, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700a1, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700a2, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // USB#0x0700a3 Props -- see note L2 at top. + .{ 0x0700a3, 0x0000, 0x0000, 0x0000, 0xffff, "Props" }, + //.{ 0x0700a4, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + //.{ 0x0700b0, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b1, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b2, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b3, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b4, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b5, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x0700b6, 0x00b3, 0x00bb, 0x0000, 0xffff, "NumpadParenLeft" }, + .{ 0x0700b7, 0x00b4, 0x00bc, 0x0000, 0xffff, "NumpadParenRight" }, + + //.{ 0x0700b8, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700b9, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700ba, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x0700bb, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadBackspace" }, + //.{ 0x0700bc, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700bd, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700be, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700bf, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + //.{ 0x0700c0, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c1, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c2, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c3, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c4, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c5, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c6, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c7, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + //.{ 0x0700c8, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700c9, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700ca, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // NUMPAD_DOUBLE_VERTICAL_BAR), // Keypad_|| + //.{ 0x0700cb, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700cc, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700cd, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700ce, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700cf, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + .{ 0x0700d0, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryStore" }, + .{ 0x0700d1, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryRecall" }, + .{ 0x0700d2, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryClear" }, + .{ 0x0700d3, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryAdd" }, + .{ 0x0700d4, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemorySubtract" }, + //.{ 0x0700d5, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700d6, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x0700d7, 0x0076, 0x007e, 0x0000, 0xffff, "" }, + // USB#0x0700d8 Keypad Clear -- see note L1 at top. + .{ 0x0700d8, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClear" }, + .{ 0x0700d9, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClearEntry" }, + //.{ 0x0700da, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700db, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700dc, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0700dd, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + + // USB#0700de - #0700df are reserved. + .{ 0x0700e0, 0x001d, 0x0025, 0x001d, 0x003b, "ControlLeft" }, + .{ 0x0700e1, 0x002a, 0x0032, 0x002a, 0x0038, "ShiftLeft" }, + // USB#0700e2: left Alt key (Mac left Option key). + .{ 0x0700e2, 0x0038, 0x0040, 0x0038, 0x003a, "AltLeft" }, + // USB#0700e3: left GUI key, e.g. Windows, Mac Command, ChromeOS Search. + .{ 0x0700e3, 0x007d, 0x0085, 0xe05b, 0x0037, "MetaLeft" }, + .{ 0x0700e4, 0x0061, 0x0069, 0xe01d, 0x003e, "ControlRight" }, + .{ 0x0700e5, 0x0036, 0x003e, 0x0036, 0x003c, "ShiftRight" }, + // USB#0700e6: right Alt key (Mac right Option key). + .{ 0x0700e6, 0x0064, 0x006c, 0xe038, 0x003d, "AltRight" }, + // USB#0700e7: right GUI key, e.g. Windows, Mac Command, ChromeOS Search. + .{ 0x0700e7, 0x007e, 0x0086, 0xe05c, 0x0036, "MetaRight" }, + + // USB#0700e8 - #07ffff are reserved + + // ================================== + // USB Usage Page 0x0c: Consumer Page + // ================================== + // AL = Application Launch + // AC = Application Control + + // TODO(garykac): Many XF86 keys have multiple scancodes mapping to them. + // We need to map all of these into a canonical USB scancode without + // confusing the reverse-lookup - most likely by simply returning the first + // found match. + + // TODO(garykac): Find appropriate mappings for: + // Win#e03c Music - USB#0c0193 is AL_AVCapturePlayback + // Win#e064 Pictures + // XKB#0080 XF86LaunchA + // XKB#0099 XF86Send + // XKB#009b XF86Xfer + // XKB#009c XF86Launch1 + // XKB#009d XF86Launch2 + // XKB... remaining XF86 keys + + // KEY_BRIGHTNESS* added in Linux 3.16 + // http://www.usb.org/developers/hidpage/HUTRR41.pdf + // + // Keyboard backlight/illumination spec update. + // https://www.usb.org/sites/default/files/hutrr73_-_fn_key_and_keyboard_backlight_brightness_0.pdf + // USB evdev XKB Win Mac Code + .{ 0x0c0060, 0x0166, 0x016e, 0x0000, 0xffff, "" }, + .{ 0x0c0061, 0x0172, 0x017a, 0x0000, 0xffff, "" }, + .{ 0x0c006f, 0x00e1, 0x00e9, 0x0000, 0xffff, "BrightnessUp" }, + .{ 0x0c0070, 0x00e0, 0x00e8, 0x0000, 0xffff, "BrightnessDown" }, + .{ 0x0c0072, 0x01af, 0x01b7, 0x0000, 0xffff, "" }, + .{ 0x0c0073, 0x0250, 0x0258, 0x0000, 0xffff, "" }, + .{ 0x0c0074, 0x0251, 0x0259, 0x0000, 0xffff, "" }, + .{ 0x0c0075, 0x00f4, 0x00fc, 0x0000, 0xffff, "" }, + .{ 0x0c0079, 0x00e6, 0x00ee, 0x0000, 0xffff, "" }, + .{ 0x0c007a, 0x00e5, 0x00ed, 0x0000, 0xffff, "" }, + .{ 0x0c007c, 0x00e4, 0x00ec, 0x0000, 0xffff, "" }, + .{ 0x0c0083, 0x0195, 0x019d, 0x0000, 0xffff, "" }, + .{ 0x0c008c, 0x00a9, 0x00b1, 0x0000, 0xffff, "" }, + .{ 0x0c008d, 0x016a, 0x0172, 0x0000, 0xffff, "" }, + .{ 0x0c0094, 0x00ae, 0x00b6, 0x0000, 0xffff, "" }, + .{ 0x0c009c, 0x019a, 0x01a2, 0x0000, 0xffff, "" }, + .{ 0x0c009d, 0x019b, 0x01a3, 0x0000, 0xffff, "" }, + + // USB evdev XKB Win Mac + .{ 0x0c00b0, 0x00cf, 0x00d7, 0x0000, 0xffff, "MediaPlay" }, + .{ 0x0c00b1, 0x00c9, 0x00d1, 0x0000, 0xffff, "MediaPause" }, + .{ 0x0c00b2, 0x00a7, 0x00af, 0x0000, 0xffff, "MediaRecord" }, + .{ 0x0c00b3, 0x00d0, 0x00d8, 0x0000, 0xffff, "MediaFastForward" }, + .{ 0x0c00b4, 0x00a8, 0x00b0, 0x0000, 0xffff, "MediaRewind" }, + .{ 0x0c00b5, 0x00a3, 0x00ab, 0xe019, 0xffff, "MediaTrackNext" }, + .{ 0x0c00b6, 0x00a5, 0x00ad, 0xe010, 0xffff, "MediaTrackPrevious" }, + .{ 0x0c00b7, 0x00a6, 0x00ae, 0xe024, 0xffff, "MediaStop" }, + .{ 0x0c00b8, 0x00a1, 0x00a9, 0xe02c, 0xffff, "Eject" }, + .{ 0x0c00cd, 0x00a4, 0x00ac, 0xe022, 0xffff, "MediaPlayPause" }, + .{ 0x0c00cf, 0x0246, 0x024e, 0x0000, 0xffff, "" }, + .{ 0x0c00d8, 0x024a, 0x0252, 0x0000, 0xffff, "" }, + .{ 0x0c00d9, 0x0249, 0x0251, 0x0000, 0xffff, "" }, + .{ 0x0c00e5, 0x00d1, 0x00d9, 0x0000, 0xffff, "" }, + //.{ 0x0c00e6, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0150, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0151, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0152, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0153, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0154, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + //.{ 0x0c0155, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // USB#0c0183: AL Consumer Control Configuration + .{ 0x0c0183, 0x00ab, 0x00b3, 0xe06d, 0xffff, "MediaSelect" }, + .{ 0x0c0184, 0x01a5, 0x01ad, 0x0000, 0xffff, "" }, + .{ 0x0c0186, 0x01a7, 0x01af, 0x0000, 0xffff, "" }, + // USB#0x0c018a AL_EmailReader + .{ 0x0c018a, 0x009b, 0x00a3, 0xe06c, 0xffff, "LaunchMail" }, + // USB#0x0c018d: AL Contacts/Address Book + .{ 0x0c018d, 0x01ad, 0x01b5, 0x0000, 0xffff, "" }, + // USB#0x0c018e: AL Calendar/Schedule + .{ 0x0c018e, 0x018d, 0x0195, 0x0000, 0xffff, "" }, + // USB#0x0c018f AL Task/Project Manager + //.{ 0x0c018f, 0x0241, 0x0249, 0x0000, 0xffff, ""}, + // USB#0x0c0190: AL Log/Journal/Timecard + //.{ 0x0c0190, 0x0242, 0x024a, 0x0000, 0xffff, ""}, + // USB#0x0c0192: AL_Calculator + .{ 0x0c0192, 0x008c, 0x0094, 0xe021, 0xffff, "LaunchApp2" }, + // USB#0c0194: My Computer (AL_LocalMachineBrowser) + .{ 0x0c0194, 0x0090, 0x0098, 0xe06b, 0xffff, "LaunchApp1" }, + .{ 0x0c0196, 0x0096, 0x009e, 0x0000, 0xffff, "" }, + .{ 0x0c019C, 0x01b1, 0x01b9, 0x0000, 0xffff, "" }, + // USB#0x0c019e: AL Terminal Lock/Screensaver + .{ 0x0c019e, 0x0098, 0x00a0, 0x0000, 0xffff, "" }, + // USB#0x0c019f AL Control Panel + .{ 0x0c019f, 0x0243, 0x024b, 0x0000, 0xffff, "LaunchControlPanel" }, + // USB#0x0c01a2: AL Select Task/Application + .{ 0x0c01a2, 0x0244, 0x024c, 0x0000, 0xffff, "SelectTask" }, + // USB#0x0c01a7: AL_Documents + .{ 0x0c01a7, 0x00eb, 0x00f3, 0x0000, 0xffff, "" }, + .{ 0x0c01ab, 0x01b0, 0x01b8, 0x0000, 0xffff, "" }, + // USB#0x0c01ae: AL Keyboard Layout + .{ 0x0c01ae, 0x0176, 0x017e, 0x0000, 0xffff, "" }, + .{ 0x0c01b1, 0x0245, 0x024d, 0x0000, 0xffff, "LaunchScreenSaver" }, + .{ 0x0c01cb, 0x0247, 0x024f, 0x0000, 0xffff, "LaunchAssistant" }, + // USB#0c01b4: Home Directory (AL_FileBrowser) (Explorer) + //.{ 0x0c01b4, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // USB#0x0c01b7: AL Audio Browser + .{ 0x0c01b7, 0x0188, 0x0190, 0x0000, 0xffff, "" }, + // USB#0x0c0201: AC New + .{ 0x0c0201, 0x00b5, 0x00bd, 0x0000, 0xffff, "" }, + // USB#0x0c0203: AC Close + .{ 0x0c0203, 0x00ce, 0x00d6, 0x0000, 0xffff, "" }, + // USB#0x0c0207: AC Close + .{ 0x0c0207, 0x00ea, 0x00f2, 0x0000, 0xffff, "" }, + // USB#0x0c0208: AC Print + .{ 0x0c0208, 0x00d2, 0x00da, 0x0000, 0xffff, "" }, + // USB#0x0c0221: AC_Search + .{ 0x0c0221, 0x00d9, 0x00e1, 0xe065, 0xffff, "BrowserSearch" }, + // USB#0x0c0223: AC_Home + .{ 0x0c0223, 0x00ac, 0x00b4, 0xe032, 0xffff, "BrowserHome" }, + // USB#0x0c0224: AC_Back + .{ 0x0c0224, 0x009e, 0x00a6, 0xe06a, 0xffff, "BrowserBack" }, + // USB#0x0c0225: AC_Forward + .{ 0x0c0225, 0x009f, 0x00a7, 0xe069, 0xffff, "BrowserForward" }, + // USB#0x0c0226: AC_Stop + .{ 0x0c0226, 0x0080, 0x0088, 0xe068, 0xffff, "BrowserStop" }, + // USB#0x0c0227: AC_Refresh (Reload) + .{ 0x0c0227, 0x00ad, 0x00b5, 0xe067, 0xffff, "BrowserRefresh" }, + // USB#0x0c022a: AC_Bookmarks (Favorites) + .{ 0x0c022a, 0x009c, 0x00a4, 0xe066, 0xffff, "BrowserFavorites" }, + .{ 0x0c022d, 0x01a2, 0x01aa, 0x0000, 0xffff, "" }, + .{ 0x0c022e, 0x01a3, 0x01ab, 0x0000, 0xffff, "" }, + // USB#0x0c0230: AC Full Screen View + //.{ 0x0c0230, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + // USB#0x0c0231: AC Normal View + //.{ 0x0c0231, 0x0000, 0x0000, 0x0000, 0xffff, ""}, + .{ 0x0c0232, 0x0174, 0x017c, 0x0000, 0xffff, "ZoomToggle" }, + // USB#0x0c0279: AC Redo/Repeat + .{ 0x0c0279, 0x00b6, 0x00be, 0x0000, 0xffff, "" }, + // USB#0x0c0289: AC_Reply + .{ 0x0c0289, 0x00e8, 0x00f0, 0x0000, 0xffff, "MailReply" }, + // USB#0x0c028b: AC_ForwardMsg (MailForward) + .{ 0x0c028b, 0x00e9, 0x00f1, 0x0000, 0xffff, "MailForward" }, + // USB#0x0c028c: AC_Send + .{ 0x0c028c, 0x00e7, 0x00ef, 0x0000, 0xffff, "MailSend" }, + // USB#0x0c029d: AC Next Keyboard Layout Select + .{ 0x0c029d, 0x0248, 0x0250, 0x0000, 0xffff, "KeyboardLayoutSelect" }, + .{ 0x0c029f, 0x0078, 0x0080, 0x0000, 0xffff, "ShowAllWindows" }, + .{ 0x0c02a2, 0x00cc, 0x00d4, 0x0000, 0xffff, "" }, + .{ 0x0c02d0, 0x0279, 0x0281, 0x0000, 0xffff, "" }, +}; + +test {} diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 9e80aaed9..f116e94fd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -521,6 +521,7 @@ pub fn render( selection: ?terminal.Selection, screen: terminal.Screen, draw_cursor: bool, + preedit: ?renderer.State.Preedit, }; // Update all our data as tightly as possible within the mutex. @@ -533,6 +534,9 @@ pub fn render( // then it is not visible. if (!state.cursor.visible) break :visible false; + // If we are in preedit, then we always show the cursor + if (state.preedit != null) break :visible true; + // If the cursor isn't a blinking style, then never blink. if (!state.cursor.style.blinking()) break :visible true; @@ -540,10 +544,17 @@ pub fn render( break :visible self.cursor_visible; }; - if (self.focused) { - self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; - } else { - self.cursor_style = .box_hollow; + // The cursor style only needs to be set if its visible. + if (self.cursor_visible) { + self.cursor_style = cursor_style: { + // If we have a dead key preedit then we always use a box style + if (state.preedit != null) break :cursor_style .box; + + // If we aren't focused, we use a hollow box + if (!self.focused) break :cursor_style .box_hollow; + + break :cursor_style renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; + }; } // Swap bg/fg if the terminal is reversed @@ -580,12 +591,16 @@ pub fn render( else null; + // Whether to draw our cursor or not. + const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(); + break :critical .{ .bg = self.config.background, .devmode = if (state.devmode) |dm| dm.visible else false, .selection = selection, .screen = screen_copy, - .draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(), + .draw_cursor = draw_cursor, + .preedit = if (draw_cursor) state.preedit else null, }; }; defer critical.screen.deinit(); @@ -599,6 +614,7 @@ pub fn render( critical.selection, &critical.screen, critical.draw_cursor, + critical.preedit, ); // Get our drawable (CAMetalDrawable) @@ -848,6 +864,7 @@ fn rebuildCells( term_selection: ?terminal.Selection, screen: *terminal.Screen, draw_cursor: bool, + preedit: ?renderer.State.Preedit, ) !void { // Bg cells at most will need space for the visible screen size self.cells_bg.clearRetainingCapacity(); @@ -962,8 +979,30 @@ fn rebuildCells( // a cursor cell then we invert the colors on that and add it in so // that we can always see it. if (draw_cursor) { - self.addCursor(screen); + const real_cursor_cell = self.addCursor(screen); + + // If we have a preedit, we try to render the preedit text on top + // of the cursor. + if (preedit) |preedit_v| preedit: { + if (preedit_v.codepoint > 0) { + // We try to base on the cursor cell but if its not there + // we use the actual cursor and if thats not there we give + // up on preedit rendering. + var cell: GPUCell = cursor_cell orelse + (real_cursor_cell orelse break :preedit).*; + cell.color = .{ 0, 0, 0, 255 }; + + // If preedit rendering succeeded then we don't want to + // re-render the underlying cell fg + if (self.updateCellChar(&cell, preedit_v.codepoint)) { + cursor_cell = null; + self.cells.appendAssumeCapacity(cell); + } + } + } + if (cursor_cell) |*cell| { + // We always invert the cell color under the cursor. cell.color = .{ 0, 0, 0, 255 }; self.cells.appendAssumeCapacity(cell.*); } @@ -1155,7 +1194,7 @@ pub fn updateCell( return true; } -fn addCursor(self: *Metal, screen: *terminal.Screen) void { +fn addCursor(self: *Metal, screen: *terminal.Screen) ?*const GPUCell { // Add the cursor const cell = screen.getCell( .active, @@ -1182,7 +1221,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void { .{}, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); - return; + return null; }; self.cells.appendAssumeCapacity(.{ @@ -1197,6 +1236,46 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void { .glyph_size = .{ glyph.width, glyph.height }, .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, }); + + return &self.cells.items[self.cells.items.len - 1]; +} + +/// Updates cell with the the given character. This returns true if the +/// cell was successfully updated. +fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool { + // Get the font index for this codepoint + const font_index = if (self.font_group.indexForCodepoint( + self.alloc, + @intCast(cp), + .regular, + .text, + )) |index| index orelse return false else |_| return false; + + // Get the font face so we can get the glyph + const face = self.font_group.group.faceFromIndex(font_index) catch |err| { + log.warn("error getting face for font_index={} err={}", .{ font_index, err }); + return false; + }; + + // Use the face to now get the glyph index + const glyph_index = face.glyphIndex(@intCast(cp)) orelse return false; + + // Render the glyph for our preedit text + const glyph = self.font_group.renderGlyph( + self.alloc, + font_index, + glyph_index, + .{}, + ) catch |err| { + log.warn("error rendering preedit glyph err={}", .{err}); + return false; + }; + + // Update the cell glyph + cell.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }; + cell.glyph_size = .{ glyph.width, glyph.height }; + cell.glyph_offset = .{ glyph.offset_x, glyph.offset_y }; + return true; } /// Sync the vertex buffer inputs to the GPU. This will attempt to reuse diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 186451e3f..a0c4f4ec4 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -721,6 +721,7 @@ pub fn render( selection: ?terminal.Selection, screen: terminal.Screen, draw_cursor: bool, + preedit: ?renderer.State.Preedit, }; // Update all our data as tightly as possible within the mutex. @@ -733,6 +734,9 @@ pub fn render( // then it is not visible. if (!state.cursor.visible) break :visible false; + // If we are in preedit, then we always show the cursor + if (state.preedit != null) break :visible true; + // If the cursor isn't a blinking style, then never blink. if (!state.cursor.style.blinking()) break :visible true; @@ -740,10 +744,17 @@ pub fn render( break :visible self.cursor_visible; }; - if (self.focused) { - self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; - } else { - self.cursor_style = .box_hollow; + // The cursor style only needs to be set if its visible. + if (self.cursor_visible) { + self.cursor_style = cursor_style: { + // If we have a dead key preedit then we always use a box style + if (state.preedit != null) break :cursor_style .box; + + // If we aren't focused, we use a hollow box + if (!self.focused) break :cursor_style .box_hollow; + + break :cursor_style renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; + }; } // Swap bg/fg if the terminal is reversed @@ -796,13 +807,17 @@ pub fn render( else null; + // Whether to draw our cursor or not. + const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(); + break :critical .{ .gl_bg = self.config.background, .devmode_data = devmode_data, .active_screen = state.terminal.active_screen, .selection = selection, .screen = screen_copy, - .draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(), + .draw_cursor = draw_cursor, + .preedit = if (draw_cursor) state.preedit else null, }; }; defer critical.screen.deinit(); @@ -821,6 +836,7 @@ pub fn render( critical.selection, &critical.screen, critical.draw_cursor, + critical.preedit, ); } @@ -858,6 +874,7 @@ pub fn rebuildCells( term_selection: ?terminal.Selection, screen: *terminal.Screen, draw_cursor: bool, + preedit: ?renderer.State.Preedit, ) !void { const t = trace(@src()); defer t.end(); @@ -1006,7 +1023,31 @@ pub fn rebuildCells( // a cursor cell then we invert the colors on that and add it in so // that we can always see it. if (draw_cursor) { - self.addCursor(screen); + const real_cursor_cell = self.addCursor(screen); + + // If we have a preedit, we try to render the preedit text on top + // of the cursor. + if (preedit) |preedit_v| preedit: { + if (preedit_v.codepoint > 0) { + // We try to base on the cursor cell but if its not there + // we use the actual cursor and if thats not there we give + // up on preedit rendering. + var cell: GPUCell = cursor_cell orelse + (real_cursor_cell orelse break :preedit).*; + cell.fg_r = 0; + cell.fg_g = 0; + cell.fg_b = 0; + cell.fg_a = 255; + + // If preedit rendering succeeded then we don't want to + // re-render the underlying cell fg + if (self.updateCellChar(&cell, preedit_v.codepoint)) { + cursor_cell = null; + self.cells.appendAssumeCapacity(cell); + } + } + } + if (cursor_cell) |*cell| { cell.fg_r = 0; cell.fg_g = 0; @@ -1023,7 +1064,7 @@ pub fn rebuildCells( } } -fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { +fn addCursor(self: *OpenGL, screen: *terminal.Screen) ?*const GPUCell { // Add the cursor const cell = screen.getCell( .active, @@ -1050,7 +1091,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { .{}, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); - return; + return null; }; self.cells.appendAssumeCapacity(.{ @@ -1073,6 +1114,49 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { .glyph_offset_x = glyph.offset_x, .glyph_offset_y = glyph.offset_y, }); + + return &self.cells.items[self.cells.items.len - 1]; +} + +/// Updates cell with the the given character. This returns true if the +/// cell was successfully updated. +fn updateCellChar(self: *OpenGL, cell: *GPUCell, cp: u21) bool { + // Get the font index for this codepoint + const font_index = if (self.font_group.indexForCodepoint( + self.alloc, + @intCast(cp), + .regular, + .text, + )) |index| index orelse return false else |_| return false; + + // Get the font face so we can get the glyph + const face = self.font_group.group.faceFromIndex(font_index) catch |err| { + log.warn("error getting face for font_index={} err={}", .{ font_index, err }); + return false; + }; + + // Use the face to now get the glyph index + const glyph_index = face.glyphIndex(@intCast(cp)) orelse return false; + + // Render the glyph for our preedit text + const glyph = self.font_group.renderGlyph( + self.alloc, + font_index, + glyph_index, + .{}, + ) catch |err| { + log.warn("error rendering preedit glyph err={}", .{err}); + return false; + }; + + // Update the cell glyph + cell.glyph_x = glyph.atlas_x; + cell.glyph_y = glyph.atlas_y; + cell.glyph_width = glyph.width; + cell.glyph_height = glyph.height; + cell.glyph_offset_x = glyph.offset_x; + cell.glyph_offset_y = glyph.offset_y; + return true; } /// Update a single cell. The bool returns whether the cell was updated diff --git a/src/renderer/State.zig b/src/renderer/State.zig index 5b1db42a0..e73a88348 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -18,6 +18,12 @@ cursor: Cursor, /// The terminal data. terminal: *terminal.Terminal, +/// Dead key state. This will render the current dead key preedit text +/// over the cursor. This currently only ever renders a single codepoint. +/// Preedit can in theory be multiple codepoints long but that is left as +/// a future exercise. +preedit: ?Preedit = null, + /// The devmode data. devmode: ?*const DevMode = null, @@ -31,3 +37,14 @@ pub const Cursor = struct { /// cursor ON or OFF. visible: bool = true, }; + +/// The pre-edit state. See Surface.preeditCallback for more information. +pub const Preedit = struct { + /// The codepoint to render as preedit text. We only support single + /// codepoint for now. In theory this can be multiple codepoints but + /// that is left as a future exercise. + /// + /// This can also be "0" in which case we can know we're in a preedit + /// mode but we don't have any preedit text to render. + codepoint: u21 = 0, +};