mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
inspector: key events
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
434
src/inspector/widgets/key.zig
Normal file
434
src/inspector/widgets/key.zig
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user