From 9b75f4a7999e4a0c85b4ef9f2c67c8f0157b8188 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jan 2026 09:48:26 -0800 Subject: [PATCH] inspector: page improvements --- src/inspector/main.zig | 1 - src/inspector/page.zig | 163 ------------ src/inspector/widgets.zig | 1 + src/inspector/widgets/page.zig | 393 +++++++++++++++++++++++++++++ src/inspector/widgets/pagelist.zig | 320 ++--------------------- 5 files changed, 414 insertions(+), 464 deletions(-) delete mode 100644 src/inspector/page.zig create mode 100644 src/inspector/widgets/page.zig diff --git a/src/inspector/main.zig b/src/inspector/main.zig index f0376f8a0..ae2c3b16f 100644 --- a/src/inspector/main.zig +++ b/src/inspector/main.zig @@ -1,7 +1,6 @@ const std = @import("std"); pub const cell = @import("cell.zig"); pub const key = @import("key.zig"); -pub const page = @import("page.zig"); pub const termio = @import("termio.zig"); diff --git a/src/inspector/page.zig b/src/inspector/page.zig deleted file mode 100644 index fd9d3bfb4..000000000 --- a/src/inspector/page.zig +++ /dev/null @@ -1,163 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const cimgui = @import("dcimgui"); -const terminal = @import("../terminal/main.zig"); -const units = @import("units.zig"); - -pub fn render(page: *const terminal.Page) void { - cimgui.c.ImGui_PushIDPtr(page); - defer cimgui.c.ImGui_PopID(); - - _ = cimgui.c.ImGui_BeginTable( - "##page_state", - 2, - cimgui.c.ImGuiTableFlags_None, - ); - defer cimgui.c.ImGui_EndTable(); - - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Memory Size"); - } - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len)); - cimgui.c.ImGui_Text("%d VM pages", page.memory.len / std.heap.page_size_min); - } - } - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Unique Styles"); - } - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", page.styles.count()); - } - } - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Grapheme Entries"); - } - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", page.graphemeCount()); - } - } - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Capacity"); - } - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - _ = cimgui.c.ImGui_BeginTable( - "##capacity", - 2, - cimgui.c.ImGuiTableFlags_None, - ); - defer cimgui.c.ImGui_EndTable(); - - const cap = page.capacity; - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Columns"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.cols))); - } - } - - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Rows"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.rows))); - } - } - - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Unique Styles"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.styles))); - } - } - - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Grapheme Bytes"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", cap.grapheme_bytes); - } - } - } - } - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Size"); - } - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - _ = cimgui.c.ImGui_BeginTable( - "##size", - 2, - cimgui.c.ImGuiTableFlags_None, - ); - defer cimgui.c.ImGui_EndTable(); - - const size = page.size; - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Columns"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.cols))); - } - } - { - cimgui.c.ImGui_TableNextRow(); - { - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Rows"); - } - - { - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.rows))); - } - } - } - } // size table -} diff --git a/src/inspector/widgets.zig b/src/inspector/widgets.zig index bea49dda1..3064a3ec4 100644 --- a/src/inspector/widgets.zig +++ b/src/inspector/widgets.zig @@ -1,5 +1,6 @@ const cimgui = @import("dcimgui"); +pub const page = @import("widgets/page.zig"); pub const pagelist = @import("widgets/pagelist.zig"); pub const screen = @import("widgets/screen.zig"); pub const style = @import("widgets/style.zig"); diff --git a/src/inspector/widgets/page.zig b/src/inspector/widgets/page.zig new file mode 100644 index 000000000..b55bf1726 --- /dev/null +++ b/src/inspector/widgets/page.zig @@ -0,0 +1,393 @@ +const std = @import("std"); +const cimgui = @import("dcimgui"); +const terminal = @import("../../terminal/main.zig"); +const units = @import("../units.zig"); +const widgets = @import("../widgets.zig"); + +const PageList = terminal.PageList; +const Page = terminal.Page; + +pub fn inspector(page: *const terminal.Page) void { + cimgui.c.ImGui_SeparatorText("Managed Memory"); + managedMemory(page); +} + +/// Draw a tree node header with metadata about this page. Returns if +/// the tree node is open or not. If it is open you must close it with +/// TreePop. +pub fn treeNode(state: struct { + /// The page + page: *const terminal.Page, + /// The index of the page in a page list, used for headers. + index: usize, + /// The range of rows this page covers, inclusive. + row_range: [2]usize, + /// Whether this page is the active or viewport node. + active: bool, + viewport: bool, +}) bool { + // Setup our node. + const open = open: { + var label_buf: [160]u8 = undefined; + const label = std.fmt.bufPrintZ( + &label_buf, + "Page {d}", + .{state.index}, + ) catch "Page"; + + const flags = cimgui.c.ImGuiTreeNodeFlags_AllowOverlap | + cimgui.c.ImGuiTreeNodeFlags_SpanFullWidth | + cimgui.c.ImGuiTreeNodeFlags_FramePadding; + break :open cimgui.c.ImGui_TreeNodeEx(label.ptr, flags); + }; + + // Move our cursor into the tree header so we can add extra info. + const header_min = cimgui.c.ImGui_GetItemRectMin(); + const header_max = cimgui.c.ImGui_GetItemRectMax(); + const header_height = header_max.y - header_min.y; + const text_line = cimgui.c.ImGui_GetTextLineHeight(); + const y_center = header_min.y + (header_height - text_line) * 0.5; + cimgui.c.ImGui_SetCursorScreenPos(.{ .x = header_min.x + 170, .y = y_center }); + + // Metadata + cimgui.c.ImGui_TextDisabled( + "%dc x %dr", + state.page.size.cols, + state.page.size.rows, + ); + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("rows %d..%d", state.row_range[0], state.row_range[1]); + + // Labels + if (state.active) { + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active"); + } + if (state.viewport) { + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport"); + } + if (state.page.isDirty()) { + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty"); + } + + return open; +} + +pub fn managedMemory(page: *const Page) void { + if (cimgui.c.ImGui_BeginTable( + "##overview", + 3, + cimgui.c.ImGuiTableFlags_BordersInnerV | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_SizingFixedFit, + )) { + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + widgets.helpMarker( + "Memory allocated for this page. Note the backing memory " ++ + "may be a larger allocation from which this page " ++ + "uses a portion.", + ); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text( + "%d KiB", + units.toKibiBytes(page.memory.len), + ); + } + + if (cimgui.c.ImGui_BeginTable( + "##managed", + 3, + cimgui.c.ImGuiTableFlags_BordersInnerV | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_SizingFixedFit, + )) { + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableSetupColumn("Resource", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Used", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Capacity", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableHeadersRow(); + + const size = page.size; + const cap = page.capacity; + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", size.cols); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", cap.cols); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", size.rows); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", cap.rows); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Styles"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.styles.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", page.styles.layout.cap); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Graphemes"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.graphemeCount()); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", page.graphemeCapacity()); + + { + const StringAlloc = @TypeOf(page.string_alloc); + const string_chunk = StringAlloc.bytesRequired(u8, 1); + const string_total_chunks = page.string_alloc.bitmap_count * StringAlloc.bitmap_bit_size; + var string_free_chunks: usize = 0; + const string_bitmaps = page.string_alloc.bitmap.ptr(page.memory); + for (string_bitmaps[0..page.string_alloc.bitmap_count]) |bitmap| { + string_free_chunks += @popCount(bitmap); + } + const string_used_chunks = string_total_chunks - string_free_chunks; + const string_used_bytes = string_used_chunks * string_chunk; + const string_capacity_bytes = string_total_chunks * string_chunk; + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Strings (bytes)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", string_used_bytes); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", string_capacity_bytes); + } + + { + const hyperlink_map = page.hyperlink_map.map(page.memory); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hyperlink Map"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", hyperlink_map.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", hyperlink_map.capacity()); + } + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hyperlink IDs"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.hyperlink_set.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", page.hyperlink_set.layout.cap); + } +} + +pub const Context = struct { + page_node: *const PageList.List.Node, + index: usize, + row_start: usize, + row_end: usize, + active_node: *const PageList.List.Node, + viewport_node: *const PageList.List.Node, +}; + +pub const InspectorState = struct { + open: bool, + rows_with_text: usize, +}; + +const PageStats = struct { + rows_with_text: usize, +}; + +fn pageStats(page: *const terminal.Page) PageStats { + var stats: PageStats = .{ .rows_with_text = 0 }; + const rows = page.rows.ptr(page.memory)[0..page.size.rows]; + for (rows) |*row| { + const cells = page.getCells(row); + for (cells) |cell| { + if (cell.hasText()) { + stats.rows_with_text += 1; + break; + } + } + } + return stats; +} + +pub fn draw(page: *const terminal.Page) void { + cimgui.c.ImGui_PushIDPtr(page); + defer cimgui.c.ImGui_PopID(); + + cimgui.c.ImGui_SeparatorText("Memory"); + memoryTable(page); + + cimgui.c.ImGui_SeparatorText("Grid"); + gridTable(page); + + cimgui.c.ImGui_SeparatorText("Rows"); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("Per-row metadata. Cells are coming next."); + rowsTable(page); +} + +fn memoryTable(page: *const terminal.Page) void { + if (!cimgui.c.ImGui_BeginTable( + "##page_memory", + 2, + cimgui.c.ImGuiTableFlags_BordersInnerV | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_SizingFixedFit, + )) return; + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( + "%d bytes (%d KiB)", + page.memory.len, + units.toKibiBytes(page.memory.len), + ); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("VM Pages"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.memory.len / std.heap.page_size_min); +} + +fn gridTable(page: *const terminal.Page) void { + if (!cimgui.c.ImGui_BeginTable( + "##page_grid", + 3, + cimgui.c.ImGuiTableFlags_BordersInnerV | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_SizingFixedFit, + )) return; + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableSetupColumn("Dimension", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Used", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Capacity", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableHeadersRow(); + + const size = page.size; + const cap = page.capacity; + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", size.cols); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", cap.cols); + + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", size.rows); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + cimgui.c.ImGui_Text("%d", cap.rows); +} + +fn rowsTable(page: *const terminal.Page) void { + const visible_rows: usize = @min(page.size.rows, 12); + const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing(); + const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0); + + _ = cimgui.c.ImGui_BeginChild( + "##page_rows", + .{ .x = 0.0, .y = child_height }, + cimgui.c.ImGuiChildFlags_Borders, + cimgui.c.ImGuiWindowFlags_None, + ); + defer cimgui.c.ImGui_EndChild(); + + if (!cimgui.c.ImGui_BeginTable( + "##page_rows_table", + 10, + cimgui.c.ImGuiTableFlags_BordersInnerV | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_SizingFixedFit, + )) return; + defer cimgui.c.ImGui_EndTable(); + + cimgui.c.ImGui_TableSetupColumn("Row", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Text", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Wrap", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Cont", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Styled", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Grapheme", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Link", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Prompt", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableSetupColumn("Kitty", cimgui.c.ImGuiTableColumnFlags_WidthFixed); + cimgui.c.ImGui_TableHeadersRow(); + + const rows = page.rows.ptr(page.memory)[0..page.size.rows]; + for (rows, 0..) |*row, row_index| { + var text_cells: usize = 0; + const cells = page.getCells(row); + for (cells) |cell| { + if (cell.hasText()) { + text_cells += 1; + } + } + + cimgui.c.ImGui_TableNextRow(); + + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("%d", row_index); + + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (text_cells == 0) { + cimgui.c.ImGui_TextDisabled("0"); + } else { + cimgui.c.ImGui_Text("%d", text_cells); + } + + _ = cimgui.c.ImGui_TableSetColumnIndex(2); + flagCell(row.dirty); + + _ = cimgui.c.ImGui_TableSetColumnIndex(3); + flagCell(row.wrap); + + _ = cimgui.c.ImGui_TableSetColumnIndex(4); + flagCell(row.wrap_continuation); + + _ = cimgui.c.ImGui_TableSetColumnIndex(5); + flagCell(row.styled); + + _ = cimgui.c.ImGui_TableSetColumnIndex(6); + flagCell(row.grapheme); + + _ = cimgui.c.ImGui_TableSetColumnIndex(7); + flagCell(row.hyperlink); + + _ = cimgui.c.ImGui_TableSetColumnIndex(8); + cimgui.c.ImGui_Text("%s", @tagName(row.semantic_prompt).ptr); + + _ = cimgui.c.ImGui_TableSetColumnIndex(9); + flagCell(row.kitty_virtual_placeholder); + } +} + +fn flagCell(value: bool) void { + if (value) { + cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "yes"); + } else { + cimgui.c.ImGui_TextDisabled("-"); + } +} diff --git a/src/inspector/widgets/pagelist.zig b/src/inspector/widgets/pagelist.zig index 604446fa0..6a40c0e4e 100644 --- a/src/inspector/widgets/pagelist.zig +++ b/src/inspector/widgets/pagelist.zig @@ -3,7 +3,6 @@ const cimgui = @import("dcimgui"); const terminal = @import("../../terminal/main.zig"); const widgets = @import("../widgets.zig"); const units = @import("../units.zig"); -const page_inspector = @import("../page.zig"); const PageList = terminal.PageList; @@ -47,100 +46,41 @@ pub const Inspector = struct { cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { widgets.helpMarker( - "Pages are linked in scrollback order. Each page holds a grid of rows/cells " ++ + "Pages are shown most-recent first. Each page holds a grid of rows/cells " ++ "plus metadata tables for styles, graphemes, strings, and hyperlinks.", ); const active_pin = pages.getTopLeft(.active); const viewport_pin = pages.getTopLeft(.viewport); - var row_offset: usize = 0; - var index: usize = 0; - var node = pages.pages.first; - while (node) |page_node| : (node = page_node.next) { + var row_offset = pages.total_rows; + var index: usize = pages.totalPages(); + var node = pages.pages.last; + while (node) |page_node| : (node = page_node.prev) { const page = &page_node.data; - const row_start = row_offset; - const row_end = row_offset + page.size.rows - 1; - const stats = pageStats(page); + row_offset -= page.size.rows; + index -= 1; - row_offset += page.size.rows; - - cimgui.c.ImGui_PushIDInt(@intCast(index)); + // Use our node pointer, which is guaranteed to be unique + // in this hierarchy, as the ID. + cimgui.c.ImGui_PushIDPtr(node); defer cimgui.c.ImGui_PopID(); - const header_state = pageHeaderRow( - index, - page, - page_node, - row_start, - row_end, - active_pin, - viewport_pin, - stats, - ); - - if (header_state.open) { - pageMetaTable(page_node, row_start, row_end, active_pin, viewport_pin, stats); - cimgui.c.ImGui_Separator(); - page_inspector.render(page); - cimgui.c.ImGui_Separator(); - contentStatsTable(stats); - cimgui.c.ImGui_TreePop(); - } - - index += 1; + // Open up the tree node. + if (!widgets.page.treeNode(.{ + .page = page, + .index = index, + .row_range = .{ row_offset, row_offset + page.size.rows - 1 }, + .active = node == active_pin.node, + .viewport = node == viewport_pin.node, + })) continue; + defer cimgui.c.ImGui_TreePop(); + widgets.page.inspector(page); } } } }; -const PageStats = struct { - rows_with_text: usize = 0, - cells_with_text: usize = 0, - dirty_rows: usize = 0, - wrap_rows: usize = 0, - wrap_cont_rows: usize = 0, - styled_rows: usize = 0, - grapheme_rows: usize = 0, - hyperlink_rows: usize = 0, - first_text_row: ?usize = null, - last_text_row: ?usize = null, - hyperlink_cells: usize = 0, - styled_cells: usize = 0, - grapheme_cells: usize = 0, -}; - -fn pageStats(page: *const terminal.Page) PageStats { - var stats: PageStats = .{}; - const rows = page.rows.ptr(page.memory)[0..page.size.rows]; - for (rows, 0..) |*row, row_index| { - if (row.dirty) stats.dirty_rows += 1; - if (row.wrap) stats.wrap_rows += 1; - if (row.wrap_continuation) stats.wrap_cont_rows += 1; - if (row.styled) stats.styled_rows += 1; - if (row.grapheme) stats.grapheme_rows += 1; - if (row.hyperlink) stats.hyperlink_rows += 1; - - const cells = page.getCells(row); - var row_cells_with_text: usize = 0; - for (cells) |cell| { - if (cell.hasText()) row_cells_with_text += 1; - if (cell.hasStyling()) stats.styled_cells += 1; - if (cell.hasGrapheme()) stats.grapheme_cells += 1; - if (cell.hyperlink) stats.hyperlink_cells += 1; - } - - if (row_cells_with_text > 0) { - stats.rows_with_text += 1; - stats.cells_with_text += row_cells_with_text; - if (stats.first_text_row == null) stats.first_text_row = row_index; - stats.last_text_row = row_index; - } - } - - return stats; -} - fn summaryTable(pages: *const PageList) void { if (!cimgui.c.ImGui_BeginTable( "pagelist_summary", @@ -219,158 +159,6 @@ fn summaryTable(pages: *const PageList) void { cimgui.c.ImGui_Text("%d", pages.countTrackedPins()); } -fn pageMetaTable( - node: *const PageList.List.Node, - row_start: usize, - row_end: usize, - active_pin: terminal.Pin, - viewport_pin: terminal.Pin, - stats: PageStats, -) void { - if (!cimgui.c.ImGui_BeginTable( - "page_meta", - 2, - cimgui.c.ImGuiTableFlags_BordersInnerV | - cimgui.c.ImGuiTableFlags_RowBg | - cimgui.c.ImGuiTableFlags_SizingFixedFit, - )) return; - defer cimgui.c.ImGui_EndTable(); - - const page = &node.data; - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Row Range"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d..%d", row_start, row_end); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Serial"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", node.serial); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Dirty"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%s", if (page.isDirty()) "true".ptr else "false".ptr); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Active Top"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%s", if (node == active_pin.node) "true".ptr else "false".ptr); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Viewport Top"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%s", if (node == viewport_pin.node) "true".ptr else "false".ptr); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Links"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text( - "%d map / %d set", - page.hyperlink_map.map(page.memory).count(), - page.hyperlink_set.count(), - ); - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Text Coverage"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d/%d rows", stats.rows_with_text, page.size.rows); -} - -fn contentStatsTable(stats: PageStats) void { - if (!cimgui.c.ImGui_BeginTable( - "page_content_stats", - 2, - cimgui.c.ImGuiTableFlags_BordersInnerV | - cimgui.c.ImGuiTableFlags_RowBg | - cimgui.c.ImGuiTableFlags_SizingFixedFit, - )) return; - defer cimgui.c.ImGui_EndTable(); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Rows w/ Text"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.rows_with_text); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Cells w/ Text"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.cells_with_text); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Text Row Range"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - if (stats.first_text_row) |first| { - cimgui.c.ImGui_Text("%d..%d", first, stats.last_text_row.?); - } else { - cimgui.c.ImGui_TextDisabled("(none)"); - } - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Dirty Rows"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.dirty_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Wrap Rows"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.wrap_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Wrap Continuations"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.wrap_cont_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Styled Rows"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.styled_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Styled Cells"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.styled_cells); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Grapheme Rows"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.grapheme_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Grapheme Cells"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.grapheme_cells); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Hyperlink Rows"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.hyperlink_rows); - - cimgui.c.ImGui_TableNextRow(); - _ = cimgui.c.ImGui_TableSetColumnIndex(0); - cimgui.c.ImGui_Text("Hyperlink Cells"); - _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d", stats.hyperlink_cells); -} - fn scrollbarInfo(pages: *PageList) void { const scrollbar = pages.scrollbar(); @@ -554,74 +342,6 @@ fn trackedPinsTable(pages: *const PageList) void { } } -const PageHeaderState = struct { - open: bool, -}; - -fn pageHeaderRow( - index: usize, - page: *const terminal.Page, - page_node: *const PageList.List.Node, - row_start: usize, - row_end: usize, - active_pin: terminal.Pin, - viewport_pin: terminal.Pin, - stats: PageStats, -) PageHeaderState { - var label_buf: [160]u8 = undefined; - const label = std.fmt.bufPrintZ( - &label_buf, - "Page {d}", - .{index}, - ) catch "Page"; - - const flags = cimgui.c.ImGuiTreeNodeFlags_AllowOverlap | - cimgui.c.ImGuiTreeNodeFlags_SpanFullWidth | - cimgui.c.ImGuiTreeNodeFlags_FramePadding; - const open = cimgui.c.ImGui_TreeNodeEx(label.ptr, flags); - - const header_min = cimgui.c.ImGui_GetItemRectMin(); - const header_max = cimgui.c.ImGui_GetItemRectMax(); - const header_height = header_max.y - header_min.y; - const text_line = cimgui.c.ImGui_GetTextLineHeight(); - const y_center = header_min.y + (header_height - text_line) * 0.5; - - cimgui.c.ImGui_SetCursorScreenPos(.{ .x = header_min.x + 170, .y = y_center }); - cimgui.c.ImGui_TextDisabled("%dc x %dr", page.size.cols, page.size.rows); - - cimgui.c.ImGui_SameLine(); - cimgui.c.ImGui_Text("rows %d..%d", row_start, row_end); - - if (page_node == active_pin.node) { - cimgui.c.ImGui_SameLine(); - cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active"); - } - if (page_node == viewport_pin.node) { - cimgui.c.ImGui_SameLine(); - cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport"); - } - if (page.isDirty()) { - cimgui.c.ImGui_SameLine(); - cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty"); - } - - const coverage = if (page.size.rows > 0) - @as(f32, @floatFromInt(stats.rows_with_text)) / - @as(f32, @floatFromInt(page.size.rows)) - else - 0.0; - - const bar_width: f32 = 140; - const bar_height: f32 = 0; - cimgui.c.ImGui_SetCursorScreenPos(.{ .x = header_max.x - bar_width - 10, .y = y_center }); - cimgui.c.ImGui_ProgressBar(coverage, .{ .x = bar_width, .y = bar_height }, null); - if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) { - cimgui.c.ImGui_SetTooltip("Text coverage: %d/%d rows", stats.rows_with_text, page.size.rows); - } - - return .{ .open = open }; -} - fn scrollbarWidget( scrollbar: *const PageList.Scrollbar, delta_row: *isize,