diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 324f913f1..b8fd75460 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -10,15 +10,12 @@ const builtin = @import("builtin"); const cimgui = @import("dcimgui"); const Surface = @import("../Surface.zig"); const font = @import("../font/main.zig"); -const input = @import("../input.zig"); -const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const inspector = @import("main.zig"); const widgets = @import("widgets.zig"); /// The window names. These are used with docking so we need to have access. const window_cell = "Cell"; -const window_keyboard = "Keyboard"; const window_termio = "Terminal IO"; const window_imgui_demo = "Dear ImGui Demo"; @@ -36,9 +33,6 @@ mouse: widgets.surface.Mouse = .{}, /// A selected cell. cell: CellInspect = .{ .idle = {} }, -/// The list of keyboard events -key_events: inspector.key.EventRing, - /// The VT stream vt_events: inspector.termio.VTEventRing, vt_stream: inspector.termio.Stream, @@ -53,7 +47,7 @@ need_scroll_to_selected: bool = false, is_keyboard_selection: bool = false, // ImGui state -gui: widgets.surface.Inspector = .empty, +gui: widgets.surface.Inspector, /// Enum representing keyboard navigation actions const KeyAction = enum { @@ -152,32 +146,27 @@ pub fn setup() void { } pub fn init(surface: *Surface) !Inspector { - var key_buf = try inspector.key.EventRing.init(surface.alloc, 2); - errdefer key_buf.deinit(surface.alloc); - var vt_events = try inspector.termio.VTEventRing.init(surface.alloc, 2); errdefer vt_events.deinit(surface.alloc); var vt_handler = inspector.termio.VTHandler.init(surface); errdefer vt_handler.deinit(); + var gui: widgets.surface.Inspector = try .init(surface.alloc); + errdefer gui.deinit(surface.alloc); + return .{ .surface = surface, - .key_events = key_buf, + .gui = gui, .vt_events = vt_events, .vt_stream = .initAlloc(surface.alloc, vt_handler), }; } pub fn deinit(self: *Inspector) void { + self.gui.deinit(self.surface.alloc); self.cell.deinit(); - { - var it = self.key_events.iterator(.forward); - while (it.next()) |v| v.deinit(self.surface.alloc); - self.key_events.deinit(self.surface.alloc); - } - { var it = self.vt_events.iterator(.forward); while (it.next()) |v| v.deinit(self.surface.alloc); @@ -190,17 +179,19 @@ pub fn deinit(self: *Inspector) void { /// Record a keyboard event. pub fn recordKeyEvent(self: *Inspector, ev: inspector.key.Event) !void { const max_capacity = 50; - self.key_events.append(ev) catch |err| switch (err) { - error.OutOfMemory => if (self.key_events.capacity() < max_capacity) { + + const events: *widgets.key.EventRing = &self.gui.key_stream.events; + events.append(ev) catch |err| switch (err) { + error.OutOfMemory => if (events.capacity() < max_capacity) { // We're out of memory, but we can allocate to our capacity. - const new_capacity = @min(self.key_events.capacity() * 2, max_capacity); - try self.key_events.resize(self.surface.alloc, new_capacity); - try self.key_events.append(ev); + const new_capacity = @min(events.capacity() * 2, max_capacity); + try events.resize(self.surface.alloc, new_capacity); + try events.append(ev); } else { - var it = self.key_events.iterator(.forward); + var it = events.iterator(.forward); if (it.next()) |old_ev| old_ev.deinit(self.surface.alloc); - self.key_events.deleteOldest(1); - try self.key_events.append(ev); + events.deleteOldest(1); + try events.append(ev); }, else => return err, @@ -214,7 +205,10 @@ pub fn recordPtyRead(self: *Inspector, data: []const u8) !void { /// Render the frame. pub fn render(self: *Inspector) void { - self.gui.draw(self.surface, self.mouse); + self.gui.draw( + self.surface, + self.mouse, + ); if (true) return; const dock_id = cimgui.c.ImGui_DockSpaceOverViewport(); @@ -231,7 +225,6 @@ pub fn render(self: *Inspector) void { .surface = self.surface, .mouse = self.mouse, }); - self.renderKeyboardWindow(); self.renderTermioWindow(); self.renderCellWindow(); } @@ -262,7 +255,6 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { // Surface is docked first so it appears as the first tab. cimgui.ImGui_DockBuilderDockWindow(inspector.surface.Window.name, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(inspector.terminal.Window.name, dock_id_main); - cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(window_cell, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main); @@ -331,63 +323,6 @@ fn renderCellWindow(self: *Inspector) void { ); } -fn renderKeyboardWindow(self: *Inspector) void { - // Start our window. If we're collapsed we do nothing. - defer cimgui.c.ImGui_End(); - if (!cimgui.c.ImGui_Begin( - window_keyboard, - null, - cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, - )) return; - - list: { - if (self.key_events.empty()) { - cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++ - "terminal focused to record it."); - break :list; - } - - if (cimgui.c.ImGui_Button("Clear")) { - var it = self.key_events.iterator(.forward); - while (it.next()) |v| v.deinit(self.surface.alloc); - self.key_events.clear(); - self.vt_stream.handler.current_seq = 1; - } - - cimgui.c.ImGui_Separator(); - - _ = cimgui.c.ImGui_BeginTable( - "table_key_events", - 1, - //cimgui.c.ImGuiTableFlags_ScrollY | - cimgui.c.ImGuiTableFlags_RowBg | - cimgui.c.ImGuiTableFlags_Borders, - ); - defer cimgui.c.ImGui_EndTable(); - - var it = self.key_events.iterator(.reverse); - while (it.next()) |ev| { - // Need to push an ID so that our selectable is unique. - cimgui.c.ImGui_PushIDPtr(ev); - defer cimgui.c.ImGui_PopID(); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - - var buf: [1024]u8 = undefined; - const label = ev.label(&buf) catch "Key Event"; - _ = cimgui.c.ImGui_SelectableBoolPtr( - label.ptr, - &ev.imgui_state.selected, - cimgui.c.ImGuiSelectableFlags_None, - ); - - if (!ev.imgui_state.selected) continue; - ev.render(); - } - } // table -} - /// Helper function to check keyboard state and determine navigation action. fn getKeyAction(self: *Inspector) KeyAction { _ = self; diff --git a/src/inspector/key.zig b/src/inspector/key.zig deleted file mode 100644 index 12d91a107..000000000 --- a/src/inspector/key.zig +++ /dev/null @@ -1,240 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const input = @import("../input.zig"); -const CircBuf = @import("../datastruct/main.zig").CircBuf; -const cimgui = @import("dcimgui"); - -/// Circular buffer of key events. -pub const EventRing = CircBuf(Event, undefined); - -/// Represents a recorded keyboard event. -pub const Event = struct { - /// The input event. - event: input.KeyEvent, - - /// The binding that was triggered as a result of this event. - /// Multiple bindings are possible if they are chained. - binding: []const input.Binding.Action = &.{}, - - /// The data sent to the pty as a result of this keyboard event. - /// This is allocated using the inspector allocator. - pty: []const u8 = "", - - /// State for the inspector GUI. Do not set this unless you're the inspector. - imgui_state: struct { - selected: bool = false, - } = .{}, - - pub fn init(alloc: Allocator, event: input.KeyEvent) !Event { - var copy = event; - copy.utf8 = ""; - if (event.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, event.utf8); - return .{ .event = copy }; - } - - pub fn deinit(self: *const Event, alloc: Allocator) void { - alloc.free(self.binding); - if (self.event.utf8.len > 0) alloc.free(self.event.utf8); - if (self.pty.len > 0) alloc.free(self.pty); - } - - /// Returns a label that can be used for this event. This is null-terminated - /// so it can be easily used with C APIs. - pub fn label(self: *const Event, buf: []u8) ![:0]const u8 { - var buf_stream = std.io.fixedBufferStream(buf); - const writer = buf_stream.writer(); - - switch (self.event.action) { - .press => try writer.writeAll("Press: "), - .release => try writer.writeAll("Release: "), - .repeat => try writer.writeAll("Repeat: "), - } - - if (self.event.mods.shift) try writer.writeAll("Shift+"); - if (self.event.mods.ctrl) try writer.writeAll("Ctrl+"); - if (self.event.mods.alt) try writer.writeAll("Alt+"); - if (self.event.mods.super) try writer.writeAll("Super+"); - - // Write our key. If we have an invalid key we attempt to write - // the utf8 associated with it if we have it to handle non-ascii. - try writer.writeAll(switch (self.event.key) { - .unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key), - else => @tagName(self.event.key), - }); - - // Deadkey - if (self.event.composing) try writer.writeAll(" (composing)"); - - // Null-terminator - try writer.writeByte(0); - return buf[0..(buf_stream.getWritten().len - 1) :0]; - } - - /// Render this event in the inspector GUI. - pub fn render(self: *const Event) void { - _ = cimgui.c.ImGui_BeginTable( - "##event", - 2, - cimgui.c.ImGuiTableFlags_None, - ); - defer cimgui.c.ImGui_EndTable(); - - if (self.binding.len > 0) { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Triggered Binding"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - - const height: f32 = height: { - const item_count: f32 = @floatFromInt(@min(self.binding.len, 5)); - const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2; - break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding; - }; - if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) { - defer cimgui.c.ImGui_EndListBox(); - for (self.binding) |action| { - _ = cimgui.c.ImGui_SelectableEx( - @tagName(action).ptr, - false, - cimgui.c.ImGuiSelectableFlags_None, - .{ .x = 0, .y = 0 }, - ); - } - } - } - - pty: { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Encoding to Pty"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - if (self.pty.len == 0) { - cimgui.c.ImGui_TextDisabled("(no data)"); - break :pty; - } - - self.renderPty() catch { - cimgui.c.ImGui_TextDisabled("(error rendering pty data)"); - break :pty; - }; - } - - { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Action"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr); - } - { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Key"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr); - } - if (!self.event.mods.empty()) { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Mods"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - if (self.event.mods.shift) cimgui.c.ImGui_Text("shift "); - if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl "); - if (self.event.mods.alt) cimgui.c.ImGui_Text("alt "); - if (self.event.mods.super) cimgui.c.ImGui_Text("super "); - } - if (self.event.composing) { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Composing"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("true"); - } - utf8: { - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("UTF-8"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - if (self.event.utf8.len == 0) { - cimgui.c.ImGui_TextDisabled("(empty)"); - break :utf8; - } - - self.renderUtf8(self.event.utf8) catch { - cimgui.c.ImGui_TextDisabled("(error rendering utf-8)"); - break :utf8; - }; - } - } - - fn renderUtf8(self: *const Event, utf8: []const u8) !void { - _ = self; - - // Format the codepoint sequence - var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); - if (std.unicode.Utf8View.init(utf8)) |view| { - var it = view.iterator(); - while (it.nextCodepoint()) |cp| { - try writer.print("U+{X} ", .{cp}); - } - } else |_| { - try writer.writeAll("(invalid utf-8)"); - } - try writer.writeByte(0); - - // Render as a textbox - _ = cimgui.c.ImGui_InputText( - "##utf8", - &buf, - buf_stream.getWritten().len - 1, - cimgui.c.ImGuiInputTextFlags_ReadOnly, - ); - } - - fn renderPty(self: *const Event) !void { - // Format the codepoint sequence - var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); - - for (self.pty) |byte| { - // Print ESC special because its so common - if (byte == 0x1B) { - try writer.writeAll("ESC "); - continue; - } - - // Print ASCII as-is - if (byte > 0x20 and byte < 0x7F) { - try writer.writeByte(byte); - continue; - } - - // Everything else as a hex byte - try writer.print("0x{X} ", .{byte}); - } - - try writer.writeByte(0); - - // Render as a textbox - _ = cimgui.c.ImGui_InputText( - "##pty", - &buf, - buf_stream.getWritten().len - 1, - cimgui.c.ImGuiInputTextFlags_ReadOnly, - ); - } -}; - -test "event string" { - const testing = std.testing; - const alloc = testing.allocator; - - var event = try Event.init(alloc, .{ .key = .key_a }); - defer event.deinit(alloc); - - var buf: [1024]u8 = undefined; - try testing.expectEqualStrings("Press: key_a", try event.label(&buf)); -} diff --git a/src/inspector/main.zig b/src/inspector/main.zig index ae2c3b16f..27b20a41f 100644 --- a/src/inspector/main.zig +++ b/src/inspector/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); pub const cell = @import("cell.zig"); -pub const key = @import("key.zig"); +pub const key = @import("widgets/key.zig"); pub const termio = @import("termio.zig"); diff --git a/src/inspector/widgets.zig b/src/inspector/widgets.zig index 3064a3ec4..8577e731f 100644 --- a/src/inspector/widgets.zig +++ b/src/inspector/widgets.zig @@ -2,6 +2,7 @@ const cimgui = @import("dcimgui"); pub const page = @import("widgets/page.zig"); pub const pagelist = @import("widgets/pagelist.zig"); +pub const key = @import("widgets/key.zig"); pub const screen = @import("widgets/screen.zig"); pub const style = @import("widgets/style.zig"); pub const surface = @import("widgets/surface.zig"); diff --git a/src/inspector/widgets/key.zig b/src/inspector/widgets/key.zig new file mode 100644 index 000000000..91d1a5d37 --- /dev/null +++ b/src/inspector/widgets/key.zig @@ -0,0 +1,434 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const input = @import("../../input.zig"); +const CircBuf = @import("../../datastruct/main.zig").CircBuf; +const cimgui = @import("dcimgui"); + +/// Circular buffer of key events. +pub const EventRing = CircBuf(Event, undefined); + +/// Represents a recorded keyboard event. +pub const Event = struct { + /// The input event. + event: input.KeyEvent, + + /// The binding that was triggered as a result of this event. + /// Multiple bindings are possible if they are chained. + binding: []const input.Binding.Action = &.{}, + + /// The data sent to the pty as a result of this keyboard event. + /// This is allocated using the inspector allocator. + pty: []const u8 = "", + + /// State for the inspector GUI. Do not set this unless you're the inspector. + imgui_state: struct { + selected: bool = false, + } = .{}, + + pub fn init(alloc: Allocator, ev: input.KeyEvent) !Event { + var copy = ev; + copy.utf8 = ""; + if (ev.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, ev.utf8); + return .{ .event = copy }; + } + + pub fn deinit(self: *const Event, alloc: Allocator) void { + alloc.free(self.binding); + if (self.event.utf8.len > 0) alloc.free(self.event.utf8); + if (self.pty.len > 0) alloc.free(self.pty); + } + + /// Returns a label that can be used for this event. This is null-terminated + /// so it can be easily used with C APIs. + pub fn label(self: *const Event, buf: []u8) ![:0]const u8 { + var buf_stream = std.io.fixedBufferStream(buf); + const writer = buf_stream.writer(); + + switch (self.event.action) { + .press => try writer.writeAll("Press: "), + .release => try writer.writeAll("Release: "), + .repeat => try writer.writeAll("Repeat: "), + } + + if (self.event.mods.shift) try writer.writeAll("Shift+"); + if (self.event.mods.ctrl) try writer.writeAll("Ctrl+"); + if (self.event.mods.alt) try writer.writeAll("Alt+"); + if (self.event.mods.super) try writer.writeAll("Super+"); + + // Write our key. If we have an invalid key we attempt to write + // the utf8 associated with it if we have it to handle non-ascii. + try writer.writeAll(switch (self.event.key) { + .unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key), + else => @tagName(self.event.key), + }); + + // Deadkey + if (self.event.composing) try writer.writeAll(" (composing)"); + + // Null-terminator + try writer.writeByte(0); + return buf[0..(buf_stream.getWritten().len - 1) :0]; + } + + /// Render this event in the inspector GUI. + pub fn render(self: *const Event) void { + _ = cimgui.c.ImGui_BeginTable( + "##event", + 2, + cimgui.c.ImGuiTableFlags_None, + ); + defer cimgui.c.ImGui_EndTable(); + + if (self.binding.len > 0) { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Triggered Binding"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + + const height: f32 = height: { + const item_count: f32 = @floatFromInt(@min(self.binding.len, 5)); + const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2; + break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding; + }; + if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) { + defer cimgui.c.ImGui_EndListBox(); + for (self.binding) |action| { + _ = cimgui.c.ImGui_SelectableEx( + @tagName(action).ptr, + false, + cimgui.c.ImGuiSelectableFlags_None, + .{ .x = 0, .y = 0 }, + ); + } + } + } + + pty: { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Encoding to Pty"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (self.pty.len == 0) { + cimgui.c.ImGui_TextDisabled("(no data)"); + break :pty; + } + + self.renderPty() catch { + cimgui.c.ImGui_TextDisabled("(error rendering pty data)"); + break :pty; + }; + } + + { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Action"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr); + } + { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Key"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr); + } + if (!self.event.mods.empty()) { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Mods"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (self.event.mods.shift) cimgui.c.ImGui_Text("shift "); + if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl "); + if (self.event.mods.alt) cimgui.c.ImGui_Text("alt "); + if (self.event.mods.super) cimgui.c.ImGui_Text("super "); + } + if (self.event.composing) { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Composing"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); + } + utf8: { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("UTF-8"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (self.event.utf8.len == 0) { + cimgui.c.ImGui_TextDisabled("(empty)"); + break :utf8; + } + + self.renderUtf8(self.event.utf8) catch { + cimgui.c.ImGui_TextDisabled("(error rendering utf-8)"); + break :utf8; + }; + } + } + + fn renderUtf8(self: *const Event, utf8: []const u8) !void { + _ = self; + + // Format the codepoint sequence + var buf: [1024]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + const writer = buf_stream.writer(); + if (std.unicode.Utf8View.init(utf8)) |view| { + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + try writer.print("U+{X} ", .{cp}); + } + } else |_| { + try writer.writeAll("(invalid utf-8)"); + } + try writer.writeByte(0); + + // Render as a textbox + _ = cimgui.c.ImGui_InputText( + "##utf8", + &buf, + buf_stream.getWritten().len - 1, + cimgui.c.ImGuiInputTextFlags_ReadOnly, + ); + } + + fn renderPty(self: *const Event) !void { + // Format the codepoint sequence + var buf: [1024]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + const writer = buf_stream.writer(); + + for (self.pty) |byte| { + // Print ESC special because its so common + if (byte == 0x1B) { + try writer.writeAll("ESC "); + continue; + } + + // Print ASCII as-is + if (byte > 0x20 and byte < 0x7F) { + try writer.writeByte(byte); + continue; + } + + // Everything else as a hex byte + try writer.print("0x{X} ", .{byte}); + } + + try writer.writeByte(0); + + // Render as a textbox + _ = cimgui.c.ImGui_InputText( + "##pty", + &buf, + buf_stream.getWritten().len - 1, + cimgui.c.ImGuiInputTextFlags_ReadOnly, + ); + } +}; + +fn modsTooltip( + mods: *const input.Mods, + buf: []u8, +) ![:0]const u8 { + var stream = std.io.fixedBufferStream(buf); + const writer = stream.writer(); + var first = true; + if (mods.shift) { + try writer.writeAll("Shift"); + first = false; + } + if (mods.ctrl) { + if (!first) try writer.writeAll("+"); + try writer.writeAll("Ctrl"); + first = false; + } + if (mods.alt) { + if (!first) try writer.writeAll("+"); + try writer.writeAll("Alt"); + first = false; + } + if (mods.super) { + if (!first) try writer.writeAll("+"); + try writer.writeAll("Super"); + } + try writer.writeByte(0); + const written = stream.getWritten(); + return written[0 .. written.len - 1 :0]; +} + +/// Keyboard event stream inspector widget. +pub const Stream = struct { + events: EventRing, + + pub fn init(alloc: Allocator) !Stream { + var events: EventRing = try .init(alloc, 2); + errdefer events.deinit(alloc); + return .{ .events = events }; + } + + pub fn deinit(self: *Stream, alloc: Allocator) void { + var it = self.events.iterator(.forward); + while (it.next()) |v| v.deinit(alloc); + self.events.deinit(alloc); + } + + pub fn draw( + self: *Stream, + open: bool, + alloc: Allocator, + ) void { + if (!open) return; + + if (self.events.empty()) { + cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++ + "terminal focused to record it."); + return; + } + + if (cimgui.c.ImGui_Button("Clear")) { + var it = self.events.iterator(.forward); + while (it.next()) |v| v.deinit(alloc); + self.events.clear(); + } + + cimgui.c.ImGui_Separator(); + + const table_flags = cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_Borders | + cimgui.c.ImGuiTableFlags_Resizable | + cimgui.c.ImGuiTableFlags_ScrollY | + cimgui.c.ImGuiTableFlags_SizingFixedFit; + + if (!cimgui.c.ImGui_BeginTable("table_key_events", 6, table_flags)) return; + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableSetupScrollFreeze(0, 1); + cimgui.c.ImGui_TableSetupColumnEx("Action", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 60, 0); + cimgui.c.ImGui_TableSetupColumnEx("Key", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 100, 0); + cimgui.c.ImGui_TableSetupColumnEx("Mods", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 150, 0); + cimgui.c.ImGui_TableSetupColumnEx("UTF-8", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 80, 0); + cimgui.c.ImGui_TableSetupColumnEx("PTY Encoding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0); + cimgui.c.ImGui_TableSetupColumnEx("Binding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0); + cimgui.c.ImGui_TableHeadersRow(); + + var it = self.events.iterator(.reverse); + while (it.next()) |ev| { + cimgui.c.ImGui_PushIDPtr(ev); + defer cimgui.c.ImGui_PopID(); + + cimgui.c.ImGui_TableNextRow(); + + // Action + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("%s", @tagName(ev.event.action).ptr); + + // Key + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + const key_name = switch (ev.event.key) { + .unidentified => if (ev.event.utf8.len > 0) ev.event.utf8 else @tagName(ev.event.key), + else => @tagName(ev.event.key), + }; + cimgui.c.ImGui_Text("%s", key_name.ptr); + + // Mods + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + mods: { + if (ev.event.mods.empty()) { + cimgui.c.ImGui_TextDisabled("-"); + break :mods; + } + + var any_hovered = false; + if (ev.event.mods.shift) { + _ = cimgui.c.ImGui_SmallButton("S"); + any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None); + cimgui.c.ImGui_SameLine(); + } + if (ev.event.mods.ctrl) { + _ = cimgui.c.ImGui_SmallButton("C"); + any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None); + cimgui.c.ImGui_SameLine(); + } + if (ev.event.mods.alt) { + _ = cimgui.c.ImGui_SmallButton("A"); + any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None); + cimgui.c.ImGui_SameLine(); + } + if (ev.event.mods.super) { + _ = cimgui.c.ImGui_SmallButton("M"); + any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None); + cimgui.c.ImGui_SameLine(); + } + cimgui.c.ImGui_NewLine(); + + if (any_hovered) tooltip: { + var tooltip_buf: [64]u8 = undefined; + const tooltip = modsTooltip( + &ev.event.mods, + &tooltip_buf, + ) catch break :tooltip; + cimgui.c.ImGui_SetTooltip("%s", tooltip.ptr); + } + } + + // UTF-8 + _ = cimgui.c.ImGui_TableSetColumnIndex(3); + if (ev.event.utf8.len == 0) { + cimgui.c.ImGui_TextDisabled("-"); + } else { + var utf8_buf: [128]u8 = undefined; + var utf8_stream = std.io.fixedBufferStream(&utf8_buf); + const utf8_writer = utf8_stream.writer(); + if (std.unicode.Utf8View.init(ev.event.utf8)) |view| { + var utf8_it = view.iterator(); + while (utf8_it.nextCodepoint()) |cp| { + utf8_writer.print("U+{X} ", .{cp}) catch break; + } + } else |_| { + utf8_writer.writeAll("?") catch {}; + } + utf8_writer.writeByte(0) catch {}; + cimgui.c.ImGui_Text("%s", &utf8_buf); + } + + // PTY + _ = cimgui.c.ImGui_TableSetColumnIndex(4); + if (ev.pty.len == 0) { + cimgui.c.ImGui_TextDisabled("-"); + } else { + var pty_buf: [256]u8 = undefined; + var pty_stream = std.io.fixedBufferStream(&pty_buf); + const pty_writer = pty_stream.writer(); + for (ev.pty) |byte| { + if (byte == 0x1B) { + pty_writer.writeAll("ESC ") catch break; + } else if (byte > 0x20 and byte < 0x7F) { + pty_writer.writeByte(byte) catch break; + } else { + pty_writer.print("0x{X} ", .{byte}) catch break; + } + } + pty_writer.writeByte(0) catch {}; + cimgui.c.ImGui_Text("%s", &pty_buf); + } + + // Binding + _ = cimgui.c.ImGui_TableSetColumnIndex(5); + if (ev.binding.len == 0) { + cimgui.c.ImGui_TextDisabled("-"); + } else { + var binding_buf: [256]u8 = undefined; + var binding_stream = std.io.fixedBufferStream(&binding_buf); + const binding_writer = binding_stream.writer(); + for (ev.binding, 0..) |action, i| { + if (i > 0) binding_writer.writeAll(", ") catch break; + binding_writer.writeAll(@tagName(action)) catch break; + } + binding_writer.writeByte(0) catch {}; + cimgui.c.ImGui_Text("%s", &binding_buf); + } + } + } +}; diff --git a/src/inspector/widgets/surface.zig b/src/inspector/widgets/surface.zig index a71144081..b7953d77b 100644 --- a/src/inspector/widgets/surface.zig +++ b/src/inspector/widgets/surface.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const assert = @import("../../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const cimgui = @import("dcimgui"); +const inspector = @import("../main.zig"); const widgets = @import("../widgets.zig"); const input = @import("../../input.zig"); const renderer = @import("../../renderer.zig"); @@ -11,20 +12,33 @@ const Surface = @import("../../Surface.zig"); /// This is discovered via the hardcoded string in the ImGui demo window. const window_imgui_demo = "Dear ImGui Demo"; +const window_keyboard = "Keyboard"; const window_terminal = "Terminal"; const window_surface = "Surface"; pub const Inspector = struct { /// Internal GUI state surface_info: Info, + key_stream: widgets.key.Stream, terminal_info: widgets.terminal.Info, - pub const empty: Inspector = .{ - .surface_info = .empty, - .terminal_info = .empty, - }; + pub fn init(alloc: Allocator) !Inspector { + return .{ + .surface_info = .empty, + .key_stream = try .init(alloc), + .terminal_info = .empty, + }; + } - pub fn draw(self: *Inspector, surface: *const Surface, mouse: Mouse) void { + pub fn deinit(self: *Inspector, alloc: Allocator) void { + self.key_stream.deinit(alloc); + } + + pub fn draw( + self: *Inspector, + surface: *const Surface, + mouse: Mouse, + ) void { // Create our dockspace first. If we had to setup our dockspace, // then it is a first render. const dockspace_id = cimgui.c.ImGui_GetID("Main Dockspace"); @@ -68,6 +82,20 @@ pub const Inspector = struct { mouse, ); } + + // Keyboard info window + { + const open = cimgui.c.ImGui_Begin( + window_keyboard, + null, + cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, + ); + defer cimgui.c.ImGui_End(); + self.key_stream.draw( + open, + surface.alloc, + ); + } } if (first_render) { @@ -107,6 +135,7 @@ pub const Inspector = struct { const dock_id_main: cimgui.c.ImGuiID = dockspace_id; cimgui.ImGui_DockBuilderDockWindow(window_terminal, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(window_surface, dock_id_main); + cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id_main); cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main); cimgui.ImGui_DockBuilderFinish(dockspace_id); }