From 7cfac87fc43d3f00b99bd7ed943ec3449c789eec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 28 Jan 2026 08:52:08 -0800 Subject: [PATCH] inspector: trying new stuff --- src/inspector/Inspector.zig | 8 +- src/inspector/widgets.zig | 114 +++++++++++++++++++++++++++ src/inspector/widgets/surface.zig | 43 +++++------ src/inspector/widgets/terminal.zig | 120 +++++++++++++++++++++++++---- 4 files changed, 244 insertions(+), 41 deletions(-) diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 7f1d8cddf..dfe03bf50 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -14,6 +14,7 @@ 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"; @@ -57,6 +58,9 @@ windows: struct { terminal: inspector.terminal.Window = .{}, } = .{}, +// ImGui state +gui: widgets.surface.Inspector = .empty, + /// Enum representing keyboard navigation actions const KeyAction = enum { down, @@ -216,9 +220,7 @@ pub fn recordPtyRead(self: *Inspector, data: []const u8) !void { /// Render the frame. pub fn render(self: *Inspector) void { - const widgets = @import("widgets.zig"); - var s: widgets.surface.Inspector = .{ .surface = self.surface }; - s.draw(); + self.gui.draw(self.surface); if (true) return; const dock_id = cimgui.c.ImGui_DockSpaceOverViewport(); diff --git a/src/inspector/widgets.zig b/src/inspector/widgets.zig index d23346a8c..af3bd414f 100644 --- a/src/inspector/widgets.zig +++ b/src/inspector/widgets.zig @@ -16,6 +16,120 @@ pub fn helpMarker(text: [:0]const u8) void { cimgui.c.ImGui_TextUnformatted(text.ptr); } +/// DetachableHeader allows rendering a collapsing header that can be +/// detached into its own window. +pub const DetachableHeader = struct { + /// Set whether the window is detached. + detached: bool = false, + + /// If true, detaching will move the item into a docking position + /// to the right. + dock: bool = true, + + // Internal state do not touch. + window_first: bool = true, + + pub fn windowEnd(self: *DetachableHeader) void { + _ = self; + + // If we started the window, we need to end it. + cimgui.c.ImGui_End(); + } + + /// Returns null if there is no window created (not detached). + /// Otherwise returns whether the window is open. + pub fn window( + self: *DetachableHeader, + label: [:0]const u8, + ) ?bool { + // If we're not detached, we don't create a window. + if (!self.detached) { + self.window_first = true; + return null; + } + + // If this is our first time showing the window then we need to + // setup docking. We only do this on the first time because we + // don't want to reset a user's docking behavior later. + if (self.window_first) dock: { + self.window_first = false; + if (!self.dock) break :dock; + const dock_id = cimgui.c.ImGui_GetWindowDockID(); + if (dock_id == 0) break :dock; + var dock_id_right: cimgui.c.ImGuiID = 0; + var dock_id_left: cimgui.c.ImGuiID = 0; + _ = cimgui.ImGui_DockBuilderSplitNode( + dock_id, + cimgui.c.ImGuiDir_Right, + 0.4, + &dock_id_right, + &dock_id_left, + ); + cimgui.ImGui_DockBuilderDockWindow(label, dock_id_right); + cimgui.ImGui_DockBuilderFinish(dock_id); + } + + return cimgui.c.ImGui_Begin( + label, + &self.detached, + cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, + ); + } + + pub fn header( + self: *DetachableHeader, + label: [:0]const u8, + ) bool { + // If we're detached, create a separate window. + if (self.detached) return false; + + // Make sure all headers have a unique ID in the stack. We only + // need to do this for the header side because creating a window + // automatically creates an ID. + cimgui.c.ImGui_PushID(label); + defer cimgui.c.ImGui_PopID(); + + // Create the collapsing header with the pop out button overlaid. + cimgui.c.ImGui_SetNextItemAllowOverlap(); + const is_open = cimgui.c.ImGui_CollapsingHeader( + label, + cimgui.c.ImGuiTreeNodeFlags_None, + ); + + // Place pop-out button inside the header bar + const header_max = cimgui.c.ImGui_GetItemRectMax(); + const header_min = cimgui.c.ImGui_GetItemRectMin(); + const frame_height = cimgui.c.ImGui_GetFrameHeight(); + const button_size = frame_height - 4; + const padding = 4; + + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_SetCursorScreenPos(.{ + .x = header_max.x - button_size - padding, + .y = header_min.y + 2, + }); + { + cimgui.c.ImGui_PushStyleVarImVec2( + cimgui.c.ImGuiStyleVar_FramePadding, + .{ .x = 0, .y = 0 }, + ); + defer cimgui.c.ImGui_PopStyleVar(); + if (cimgui.c.ImGui_ButtonEx( + ">>##detach", + .{ .x = button_size, .y = button_size }, + )) { + self.detached = true; + } + } + + if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) { + cimgui.c.ImGui_SetTooltip("Detach into separate window"); + } + + return is_open; + } +}; + pub const DetachableHeaderState = struct { show_window: bool = false, diff --git a/src/inspector/widgets/surface.zig b/src/inspector/widgets/surface.zig index 00dc3992c..e22ddc6d4 100644 --- a/src/inspector/widgets/surface.zig +++ b/src/inspector/widgets/surface.zig @@ -12,10 +12,14 @@ const window_imgui_demo = "Dear ImGui Demo"; const window_terminal = "Terminal"; pub const Inspector = struct { - /// The surface being inspected. - surface: *const Surface, + /// Internal GUI state + terminal_info: widgets.terminal.Info, - pub fn draw(self: *Inspector) void { + pub const empty: Inspector = .{ + .terminal_info = .empty, + }; + + pub fn draw(self: *Inspector, surface: *const Surface) 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"); @@ -30,10 +34,20 @@ pub const Inspector = struct { // Draw everything that requires the terminal state mutex. { - self.surface.renderer_state.mutex.lock(); - defer self.surface.renderer_state.mutex.unlock(); - const t = self.surface.renderer_state.terminal; - drawTerminalWindow(.{ .terminal = t }); + surface.renderer_state.mutex.lock(); + defer surface.renderer_state.mutex.unlock(); + const t = surface.renderer_state.terminal; + + // Terminal info window + { + const open = cimgui.c.ImGui_Begin( + window_terminal, + null, + cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, + ); + defer cimgui.c.ImGui_End(); + self.terminal_info.draw(open, t); + } } if (first_render) { @@ -86,18 +100,3 @@ pub const Inspector = struct { return setup; } }; - -fn drawTerminalWindow(state: struct { - terminal: *terminal.Terminal, -}) void { - defer cimgui.c.ImGui_End(); - if (!cimgui.c.ImGui_Begin( - window_terminal, - null, - cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, - )) return; - - widgets.terminal.drawInfo(.{ - .terminal = state.terminal, - }); -} diff --git a/src/inspector/widgets/terminal.zig b/src/inspector/widgets/terminal.zig index af3da07b5..9025e1e63 100644 --- a/src/inspector/widgets/terminal.zig +++ b/src/inspector/widgets/terminal.zig @@ -3,27 +3,115 @@ const builtin = @import("builtin"); const assert = @import("../../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const cimgui = @import("dcimgui"); +const widgets = @import("../widgets.zig"); const terminal = @import("../../terminal/main.zig"); const Terminal = terminal.Terminal; pub const Info = struct { - terminal: *Terminal, -}; + misc_header: widgets.DetachableHeader, -pub fn drawInfo(data: Info) void { - if (cimgui.c.ImGui_CollapsingHeader( - "Help", - cimgui.c.ImGuiTreeNodeFlags_None, - )) { - cimgui.c.ImGui_TextWrapped( - "This window displays the internal state of the terminal. " ++ - "The terminal state is global to this terminal. Some state " ++ - "is specific to the active screen or other subsystems. Values " ++ - "here reflect the running state and will update as the terminal " ++ - "application modifies them via escape sequences or shell integration. " ++ - "Some can be modified directly for debugging purposes.", - ); + pub const empty: Info = .{ + .misc_header = .{}, + }; + + const misc_header_label = "Misc"; + + /// Draw the terminal info window. + pub fn draw( + self: *Info, + open: bool, + t: *Terminal, + ) void { + // Draw our open state if we're open. + if (open) self.drawOpen(t); + + // Draw our detached state that draws regardless of if + // we're open or not. + if (self.misc_header.window(misc_header_label)) |visible| { + defer self.misc_header.windowEnd(); + if (visible) miscTable(t); + } } - _ = data; + fn drawOpen(self: *Info, t: *Terminal) void { + if (cimgui.c.ImGui_CollapsingHeader( + "Help", + cimgui.c.ImGuiTreeNodeFlags_None, + )) { + cimgui.c.ImGui_TextWrapped( + "This window displays the internal state of the terminal. " ++ + "The terminal state is global to this terminal. Some state " ++ + "is specific to the active screen or other subsystems. Values " ++ + "here reflect the running state and will update as the terminal " ++ + "application modifies them via escape sequences or shell integration. " ++ + "Some can be modified directly for debugging purposes.", + ); + } + + if (self.misc_header.header(misc_header_label)) miscTable(t); + } +}; + +pub fn miscTable(t: *Terminal) void { + _ = cimgui.c.ImGui_BeginTable( + "table_misc", + 2, + cimgui.c.ImGuiTableFlags_None, + ); + defer cimgui.c.ImGui_EndTable(); + + { + cimgui.c.ImGui_TableNextRow(); + { + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Working Directory"); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("The current working directory reported by the shell."); + } + { + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (t.pwd.items.len > 0) { + cimgui.c.ImGui_Text( + "%.*s", + t.pwd.items.len, + t.pwd.items.ptr, + ); + } else { + cimgui.c.ImGui_TextDisabled("(none)"); + } + } + } + + { + cimgui.c.ImGui_TableNextRow(); + { + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Focused"); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("Whether the terminal itself is currently focused."); + } + { + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + var value: bool = t.flags.focused; + _ = cimgui.c.ImGui_Checkbox("##focused", &value); + } + } + + { + cimgui.c.ImGui_TableNextRow(); + { + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Previous Char"); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("The previously printed character, used only for the REP sequence."); + } + { + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (t.previous_char) |c| { + cimgui.c.ImGui_Text("U+%04X", @as(u32, c)); + } else { + cimgui.c.ImGui_TextDisabled("(none)"); + } + } + } }