From 998ee051bae2882715eecfbd1235c8450cbb0a65 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 15:45:32 +0100 Subject: [PATCH 01/14] refactor: simplify tui.drag_button type --- src/tui/tui.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e89336c..3bf874a 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -58,7 +58,7 @@ last_hover_y: c_int = -1, commands: Commands = undefined, logger: log.Logger, drag_source: ?*Widget = null, -drag_button: ?input.MouseType = null, +drag_button: input.MouseType = 0, dark_theme: Widget.Theme, dark_parsed_theme: ?std.json.Parsed(Widget.Theme), light_theme: Widget.Theme, From 760d498f53a23526abea398905aaaa3f001160ed Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 15:45:55 +0100 Subject: [PATCH 02/14] refactor: allow updating full drag context --- src/tui/tui.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 3bf874a..6ff83d1 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1775,9 +1775,10 @@ pub fn get_keybind_mode() ?Mode { return self.input_mode_ orelse self.delayed_init_input_mode; } -pub fn update_drag_source(drag_source: *Widget) void { +pub fn update_drag_source(drag_source: *Widget, btn: input.MouseType) void { const self = current(); self.drag_source = drag_source; + self.drag_button = btn; } fn set_drag_source(self: *Self, drag_source: ?*Widget, btn: input.MouseType) void { From 58bd1fe12a4c9174cae80e919ec8c66ee3290add Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 15:46:12 +0100 Subject: [PATCH 03/14] refactor: add tui.get_drag_source --- src/tui/tui.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 6ff83d1..84b7d97 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1786,6 +1786,11 @@ fn set_drag_source(self: *Self, drag_source: ?*Widget, btn: input.MouseType) voi self.drag_button = btn; } +pub fn get_drag_source() struct { ?*Widget, input.MouseType } { + const self = current(); + return .{ self.drag_source, self.drag_button }; +} + pub fn reset_drag_context() void { const self = current(); self.drag_source = null; From c3cf5ea02f325ef450a575f96a92371198796d71 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 17:16:07 +0100 Subject: [PATCH 04/14] refactor: add source location tracing for need_render calls --- src/tui/Button.zig | 8 +++---- src/tui/InputBox.zig | 8 +++---- src/tui/Widget.zig | 2 +- src/tui/editor.zig | 2 +- src/tui/home.zig | 4 ++-- src/tui/mainview.zig | 32 ++++++++++++++-------------- src/tui/mode/mini/file_browser.zig | 2 +- src/tui/mode/overlay/dropdown.zig | 2 +- src/tui/mode/overlay/open_recent.zig | 6 +++--- src/tui/mode/overlay/palette.zig | 2 +- src/tui/mode/overlay/vcs_status.zig | 6 +++--- src/tui/status/clock.zig | 2 +- src/tui/tui.zig | 13 +++++------ 13 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/tui/Button.zig b/src/tui/Button.zig index bca03a0..b8e29ae 100644 --- a/src/tui/Button.zig +++ b/src/tui/Button.zig @@ -123,7 +123,7 @@ fn State(ctx_type: type) type { input.mouse.BUTTON1 => { self.active = true; self.drag_anchor = self.to_rel_cursor(x, y); - tui.need_render(); + tui.need_render(@src()); }, input.mouse.BUTTON4, input.mouse.BUTTON5 => { self.call_click_handler(btn_enum, self.to_rel_cursor(x, y)); @@ -136,7 +136,7 @@ fn State(ctx_type: type) type { self.drag_anchor = null; self.drag_pos = null; self.call_click_handler(@enumFromInt(btn), self.to_rel_cursor(x, y)); - tui.need_render(); + tui.need_render(@src()); return true; } else if (try m.match(.{ "D", input.event.press, tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.any, tp.any })) { self.drag_pos = .{ .x = x, .y = y }; @@ -153,11 +153,11 @@ fn State(ctx_type: type) type { self.drag_anchor = null; self.drag_pos = null; self.call_click_handler(@enumFromInt(btn), self.to_rel_cursor(x, y)); - tui.need_render(); + tui.need_render(@src()); return true; } else if (try m.match(.{ "H", tp.extract(&self.hover) })) { tui.rdr().request_mouse_cursor_pointer(self.hover); - tui.need_render(); + tui.need_render(@src()); return true; } self.drag_anchor = null; diff --git a/src/tui/InputBox.zig b/src/tui/InputBox.zig index cffeaa4..8a06b09 100644 --- a/src/tui/InputBox.zig +++ b/src/tui/InputBox.zig @@ -132,21 +132,21 @@ pub fn State(ctx_type: type) type { pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.any, tp.any, tp.any })) { self.active = true; - tui.need_render(); + tui.need_render(@src()); return true; } else if (try m.match(.{ "B", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.any, tp.any, tp.any })) { self.opts.on_click(self.opts.ctx, self); self.active = false; - tui.need_render(); + tui.need_render(@src()); return true; } else if (try m.match(.{ "D", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.any, tp.any, tp.any })) { self.opts.on_click(self.opts.ctx, self); self.active = false; - tui.need_render(); + tui.need_render(@src()); return true; } else if (try m.match(.{ "H", tp.extract(&self.hover) })) { tui.rdr().request_mouse_cursor_pointer(self.hover); - tui.need_render(); + tui.need_render(@src()); return true; } return false; diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 89a0aef..d1c345a 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -170,7 +170,7 @@ pub fn dynamic_cast(self: Self, comptime T: type) ?*T { } pub fn need_render() void { - tui.need_render(); + tui.need_render(@src()); } pub fn need_reflow() void { diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 3406268..1bc51be 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -6934,7 +6934,7 @@ pub const EditorWidget = struct { return; } else { self.editor.cancel_all_matches(); - tui.need_render(); + tui.need_render(@src()); } }, .none => {}, diff --git a/src/tui/home.zig b/src/tui/home.zig index abc6381..5fae6c0 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -209,7 +209,7 @@ pub fn receive(_: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { var hover: bool = false; if (try m.match(.{ "H", tp.extract(&hover) })) { tui.rdr().request_mouse_cursor_default(hover); - tui.need_render(); + tui.need_render(@src()); return true; } if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.more }) or @@ -453,7 +453,7 @@ const cmds = struct { const padding = tui.get_widget_style(widget_type).padding; self.menu_len = self.menu_count + padding.top + padding.bottom; self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; - tui.need_render(); + tui.need_render(@src()); try tui.save_config(); } pub const home_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 877981c..67f812a 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -605,7 +605,7 @@ const cmds = struct { if (view == null) try command.executeName("scroll_view_center", .{}); } - tui.need_render(); + tui.need_render(@src()); self.location_update_from_editor(); } @@ -613,7 +613,7 @@ const cmds = struct { tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", command.fmt(.{ "help", @embedFile("help.md"), "markdown" })); - tui.need_render(); + tui.need_render(@src()); self.location_update_from_editor(); } pub const open_help_meta: Meta = .{ .description = "Open help" }; @@ -622,7 +622,7 @@ const cmds = struct { tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", command.fmt(.{ "font test", @import("fonts.zig").font_test_text, "text" })); - tui.need_render(); + tui.need_render(@src()); self.location_update_from_editor(); } pub const open_font_test_text_meta: Meta = .{ .description = "Open font glyph test text" }; @@ -631,7 +631,7 @@ const cmds = struct { tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", command.fmt(.{ "version", root.version_info, "gitcommit" })); - tui.need_render(); + tui.need_render(@src()); self.location_update_from_editor(); } pub const open_version_info_meta: Meta = .{ .description = "Version" }; @@ -726,7 +726,7 @@ const cmds = struct { tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", .{ .args = args }); - tui.need_render(); + tui.need_render(@src()); self.location_update_from_editor(); } pub const create_scratch_buffer_meta: Meta = .{ .arguments = &.{ .string, .string, .string } }; @@ -807,7 +807,7 @@ const cmds = struct { new_buffer.mark_dirty(); new_editor.clamp(); new_editor.update_buf(new_buffer.root) catch {}; - tui.need_render(); + tui.need_render(@src()); } try command.executeName("save_file", .{}); try command.executeName("place_next_tab", command.fmt(.{ @@ -836,7 +836,7 @@ const cmds = struct { const logger = log.logger("buffer"); defer logger.deinit(); logger.print("deleted buffer {s}", .{file_path}); - tui.need_render(); + tui.need_render(@src()); } pub const delete_buffer_meta: Meta = .{ .arguments = &.{.string} }; @@ -852,13 +852,13 @@ const cmds = struct { return; } _ = self.buffer_manager.close_buffer(buffer); - tui.need_render(); + tui.need_render(@src()); } pub const close_buffer_meta: Meta = .{ .arguments = &.{.string} }; pub fn restore_session(self: *Self, _: Ctx) Result { try self.read_restore_info(); - tui.need_render(); + tui.need_render(@src()); } pub const restore_session_meta: Meta = .{}; @@ -1117,7 +1117,7 @@ const cmds = struct { if (self.get_active_editor()) |editor| if (std.mem.eql(u8, file_path, editor.file_path orelse "")) { self.symbols_complete = true; try tui.open_overlay(@import("mode/overlay/symbol_palette.zig").Type); - tui.need_render(); + tui.need_render(@src()); }; } pub const add_document_symbol_done_meta: Meta = .{ @@ -1181,7 +1181,7 @@ const cmds = struct { .palette => try tui.open_overlay(@import("mode/overlay/completion_palette.zig").Type), .dropdown => try tui.open_overlay(@import("mode/overlay/completion_dropdown.zig").Type), } - tui.need_render(); + tui.need_render(@src()); } }; } @@ -1383,7 +1383,7 @@ const cmds = struct { const buffer = self.buffer_manager.buffer_from_ref(buffer_ref) orelse return; if (self.get_editor_for_buffer(buffer)) |editor| if (editor.buffer) |eb| if (eb == buffer) { editor.smart_buffer_append(command.fmt(.{output})) catch {}; - tui.need_render(); + tui.need_render(@src()); return; }; var cursor: Buffer.Cursor = .{}; @@ -1393,7 +1393,7 @@ const cmds = struct { _, _, root_ = try root_.insert_chars(cursor.row, cursor.col, output, self.allocator, metrics); buffer.store_undo(&[_]u8{}) catch {}; buffer.update(root_); - tui.need_render(); + tui.need_render(@src()); } pub const shell_execute_stream_output_meta: Meta = .{ .arguments = &.{ .integer, .string } }; @@ -1407,7 +1407,7 @@ const cmds = struct { return; } buffer.mark_clean(); - tui.need_render(); + tui.need_render(@src()); } pub const shell_execute_stream_output_complete_meta: Meta = .{ .arguments = &.{ .integer, .string } }; @@ -2074,12 +2074,12 @@ pub fn set_info_content(self: *Self, content: []const u8, mode: enum { replace, .replace => info.set_content(content) catch |e| return tp.exit_error(e, @errorReturnTrace()), .append => info.append_content(content) catch |e| return tp.exit_error(e, @errorReturnTrace()), } - tui.need_render(); + tui.need_render(@src()); } pub fn cancel_info_content(self: *Self) tp.result { _ = self.toggle_panel_view(info_view, .disable) catch |e| return tp.exit_error(e, @errorReturnTrace()); - tui.need_render(); + tui.need_render(@src()); } pub fn vcs_id_update(self: *Self, m: tp.message) void { diff --git a/src/tui/mode/mini/file_browser.zig b/src/tui/mode/mini/file_browser.zig index 88f54ea..82e29d9 100644 --- a/src/tui/mode/mini/file_browser.zig +++ b/src/tui/mode/mini/file_browser.zig @@ -179,7 +179,7 @@ pub fn Create(options: type) type { } else { log.logger("file_browser").err("receive", tp.unexpected(m)); } - tui.need_render(); + tui.need_render(@src()); } fn add_entry(self: *Self, file_name: []const u8, entry_type: EntryType, file_type: []const u8, icon: []const u8, color: u24) !void { diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index 41448a9..f5a10ee 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -580,7 +580,7 @@ pub fn Create(options: type) type { tui.set_next_style(widget_type); const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); - tui.need_render(); + tui.need_render(@src()); try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index ad8ae61..91a79ec 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -273,7 +273,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void self.menu.select_down(); self.need_select_first = false; } - tui.need_render(); + tui.need_render(@src()); } else if (try cbor.match(m.buf, .{ "PRJ", "recent", @@ -291,7 +291,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void self.menu.select_down(); self.need_select_first = false; } - tui.need_render(); + tui.need_render(@src()); } else if (try cbor.match(m.buf, .{ "PRJ", "recent_done", tp.extract(&self.longest), tp.extract(&query), tp.extract(&self.total_files_in_project) })) { self.update_count_hint(); self.query_pending = false; @@ -492,7 +492,7 @@ const cmds = struct { pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { tui.set_next_style(widget_type); self.do_resize(); - tui.need_render(); + tui.need_render(@src()); try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 1b4193b..8824b14 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -635,7 +635,7 @@ pub fn Create(options: type) type { tui.set_next_style(widget_type); const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); - tui.need_render(); + tui.need_render(@src()); try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mode/overlay/vcs_status.zig b/src/tui/mode/overlay/vcs_status.zig index b25b698..a6687c7 100644 --- a/src/tui/mode/overlay/vcs_status.zig +++ b/src/tui/mode/overlay/vcs_status.zig @@ -188,7 +188,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void self.menu.select_down(); self.need_select_first = false; } - tui.need_render(); + tui.need_render(@src()); } else if (try cbor.match(m.buf, .{ "PRJ", "new_or_modified_files", @@ -207,7 +207,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void self.menu.select_down(); self.need_select_first = false; } - tui.need_render(); + tui.need_render(@src()); } else if (try cbor.match(m.buf, .{ "PRJ", "new_or_modified_files_done", tp.extract(&self.longest), tp.extract(&query) })) { self.query_pending = false; self.need_reset = true; @@ -369,7 +369,7 @@ const cmds = struct { pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { tui.set_next_style(widget_type); self.do_resize(); - tui.need_render(); + tui.need_render(@src()); try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/status/clock.zig b/src/tui/status/clock.zig index fe5b6cd..a2f99aa 100644 --- a/src/tui/status/clock.zig +++ b/src/tui/status/clock.zig @@ -95,7 +95,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn receive_tick(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { if (try cbor.match(m.buf, .{"CLOCK"})) { - tui.need_render(); + tui.need_render(@src()); self.update_tick_timer(.ticked); return true; } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 84b7d97..6c92868 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -407,7 +407,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { }; try self.dispatch_flush_input_event(); if (self.unrendered_input_events_count > 0 and !self.frame_clock_running) - need_render(); + need_render(@src()); return; } @@ -1613,21 +1613,21 @@ const cmds = struct { pub fn panel_next_widget_style(_: *Self, _: Ctx) Result { set_next_style(.panel); - need_render(); + need_render(@src()); try save_config(); } pub const panel_next_widget_style_meta: Meta = .{}; pub fn hint_window_next_widget_style(_: *Self, _: Ctx) Result { set_next_style(.hint_window); - need_render(); + need_render(@src()); try save_config(); } pub const hint_window_next_widget_style_meta: Meta = .{}; pub fn dropdown_next_widget_style(_: *Self, _: Ctx) Result { set_next_style(.dropdown); - need_render(); + need_render(@src()); try save_config(); } pub const dropdown_next_widget_style_meta: Meta = .{}; @@ -1803,9 +1803,10 @@ fn maybe_reset_drag_source(self: *Self, btn: input.MouseType) void { self.drag_button = 0; } -pub fn need_render() void { +pub fn need_render(src: std.builtin.SourceLocation) void { const self = current(); if (!(self.render_pending or self.frame_clock_running)) { + tp.trace(tp.channel.debug, .{ "tui", "need_render", src.fn_name, src.file, src.line }); self.render_pending = true; tp.self_pid().send(.{"render"}) catch {}; } @@ -1819,7 +1820,7 @@ pub fn frames_rendered() usize { pub fn resize() void { mainview_widget().resize(screen()); refresh_hover(); - need_render(); + need_render(@src()); } pub fn plane() renderer.Plane { From ef60a95d5590ad972d90a6171de4ab64acb60e32 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 17:16:36 +0100 Subject: [PATCH 05/14] refactor: add widget call stack tracing for render continuations --- src/tui/Widget.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index d1c345a..36376f7 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -203,7 +203,10 @@ pub fn update(self: Self) void { } pub fn render(self: Self, theme: *const Theme) bool { - return self.vtable.render(self.ptr, theme); + const more = self.vtable.render(self.ptr, theme); + if (more) + tp.trace(tp.channel.widget, .{ "continue_by", self.vtable.type_name }); + return more; } pub fn resize(self: Self, pos: Box) void { From 9d016527f25c2100520e29b34d36d666b351f798 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 17:19:10 +0100 Subject: [PATCH 06/14] refactor: add tracing of frame_clock_running --- src/tui/tui.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 6c92868..1405002 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -668,11 +668,13 @@ fn render(self: *Self) void { if (!self.frame_clock_running) { self.frame_clock.start() catch {}; self.frame_clock_running = true; + tp.trace(tp.channel.widget, .{ "frame_clock_running", "started", more }); } } else { if (self.frame_clock_running) { self.frame_clock.stop() catch {}; self.frame_clock_running = false; + tp.trace(tp.channel.widget, .{ "frame_clock_running", "stopped", more }); } } } From 8cfab31104045e48d65fda9d74914edd9bf8f4ca Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 17:53:12 +0100 Subject: [PATCH 07/14] refactor: add tracing for update_hover, clear_hover_focus and refresh_hover --- src/tui/mode/overlay/dropdown.zig | 2 +- src/tui/mode/overlay/palette.zig | 2 +- src/tui/status/tabs.zig | 6 ++--- src/tui/tui.zig | 43 +++++++++++++++++-------------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index f5a10ee..e90e885 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -298,7 +298,7 @@ pub fn Create(options: type) type { self.menu.select_down(); const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); - tui.refresh_hover(); + tui.refresh_hover(@src()); self.selection_updated(); } } diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 8824b14..f0c5ae9 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -349,7 +349,7 @@ pub fn Create(options: type) type { self.menu.select_down(); const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); - tui.refresh_hover(); + tui.refresh_hover(@src()); self.selection_updated(); } } diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 4265eba..5baf53a 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -206,8 +206,8 @@ pub const TabBar = struct { if (self.tabs[dragging].widget.dynamic_cast(Tab.ButtonType)) |btn| btn.hover = false; self.update(); for (self.widget_list.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| - tui.update_drag_source(&widgetstate.widget); - tui.refresh_hover(); + tui.update_drag_source(&widgetstate.widget, 0); + tui.refresh_hover(@src()); } } } @@ -249,7 +249,7 @@ pub const TabBar = struct { } } if (prev_widget_count != self.widget_list.widgets.items.len) - tui.refresh_hover(); + tui.refresh_hover(@src()); } fn update_tab_buffers(self: *Self) !void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 1405002..184041c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -556,7 +556,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { if (try m.match(.{"MOUSE_IDLE"})) { if (self.mouse_idle_timer) |*t| t.deinit(); self.mouse_idle_timer = null; - try self.clear_hover_focus(); + try self.clear_hover_focus(@src()); return; } @@ -851,32 +851,35 @@ fn update_hover(self: *Self, y: c_int, x: c_int) !?*Widget { self.last_hover_x = x; if (y >= 0 and x >= 0) if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| { if (if (self.hover_focus) |h| h != w else true) { - var buf: [256]u8 = undefined; - if (self.hover_focus) |h| { - if (self.is_live_widget_ptr(h)) - _ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace())); - } + tp.trace(tp.channel.debug, .{ "update_hover", if (self.hover_focus) |h| @intFromPtr(h) else 0, @intFromPtr(w) }); + if (self.hover_focus) |h| if (self.is_live_widget_ptr(h)) + try send_hover_msg(h, false); self.hover_focus = w; - _ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e, @errorReturnTrace())); + try send_hover_msg(w, true); } return w; }; - try self.clear_hover_focus(); + try self.clear_hover_focus(@src()); return null; } -fn clear_hover_focus(self: *Self) tp.result { - if (self.hover_focus) |h| { - var buf: [256]u8 = undefined; - if (self.is_live_widget_ptr(h)) - _ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace())); - } +fn clear_hover_focus(self: *Self, src: std.builtin.SourceLocation) tp.result { + if (self.hover_focus) |h| if (self.is_live_widget_ptr(h)) + try send_hover_msg(h, false); + tp.trace(tp.channel.debug, .{ "tui", "clear_hover_focus", if (self.hover_focus) |h| @intFromPtr(h) else 0, src.fn_name, src.file, src.line }); self.hover_focus = null; } -pub fn refresh_hover() void { +fn send_hover_msg(widget: *const Widget, hover: bool) tp.result { + var buf: [256]u8 = undefined; + tp.trace(tp.channel.debug, .{ "hover_msg", @intFromPtr(widget), hover }); + _ = try widget.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", hover }) catch |e| return tp.exit_error(e, @errorReturnTrace())); +} + +pub fn refresh_hover(src: std.builtin.SourceLocation) void { const self = current(); - self.clear_hover_focus() catch return; + tp.trace(tp.channel.debug, .{ "tui", "refresh_hover", if (self.hover_focus) |h| @intFromPtr(h) else 0, src.fn_name, src.file, src.line }); + self.clear_hover_focus(@src()) catch return; _ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {}; } @@ -900,7 +903,7 @@ fn enter_overlay_mode(self: *Self, mode: type) command.Result { self.input_mode_outer_ = self.input_mode_; self.input_mode_ = new_mode; if (self.input_mode_) |*m| m.run_init(); - refresh_hover(); + refresh_hover(@src()); } fn enter_overlay_mode_with_args(self: *Self, mode: type, ctx: command.Context) command.Result { @@ -912,7 +915,7 @@ fn enter_overlay_mode_with_args(self: *Self, mode: type, ctx: command.Context) c self.input_mode_outer_ = self.input_mode_; self.input_mode_ = try mode.create_with_args(self.allocator, ctx); if (self.input_mode_) |*m| m.run_init(); - refresh_hover(); + refresh_hover(@src()); } fn get_input_mode(self: *Self, mode_name: []const u8) !Mode { @@ -1388,7 +1391,7 @@ const cmds = struct { if (self.input_mode_) |*mode| mode.deinit(); self.input_mode_ = self.input_mode_outer_; self.input_mode_outer_ = null; - refresh_hover(); + refresh_hover(@src()); } pub const exit_overlay_mode_meta: Meta = .{}; @@ -1821,7 +1824,7 @@ pub fn frames_rendered() usize { pub fn resize() void { mainview_widget().resize(screen()); - refresh_hover(); + refresh_hover(@src()); need_render(@src()); } From 120a9f0bf50ac288f679137e2e78240622e96869 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 17:23:44 +0100 Subject: [PATCH 08/14] fix: don't clear_hover_focus in refresh_hover This causes an endless render loop because the hover status ping pongs. This was probably originally added to avoid dangling widget pointers. We handle that correctly now elsewhere with is_live_widget_ptr. --- src/tui/tui.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 184041c..b579062 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -879,7 +879,6 @@ fn send_hover_msg(widget: *const Widget, hover: bool) tp.result { pub fn refresh_hover(src: std.builtin.SourceLocation) void { const self = current(); tp.trace(tp.channel.debug, .{ "tui", "refresh_hover", if (self.hover_focus) |h| @intFromPtr(h) else 0, src.fn_name, src.file, src.line }); - self.clear_hover_focus(@src()) catch return; _ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {}; } From ff38c37df7a6064083f09909301e2b8aa5a7f998 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 15:46:30 +0100 Subject: [PATCH 09/14] refactor: make tab bar split aware --- src/tui/status/tabs.zig | 113 ++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 5baf53a..d7c81a4 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -104,6 +104,7 @@ pub const TabBar = struct { const TabBarTab = struct { buffer_ref: usize, widget: Widget, + view: ?usize, }; fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, min_tabs: ?usize) !Self { @@ -138,9 +139,14 @@ pub const TabBar = struct { } pub fn update(self: *Self) void { - self.update_tabs() catch {}; + const drag_source, const drag_btn = tui.get_drag_source(); + self.update_tabs(drag_source) catch {}; self.widget_list_widget.resize(Widget.Box.from(self.plane)); self.widget_list_widget.update(); + for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| + for (split.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| + tui.update_drag_source(&widgetstate.widget, drag_btn); + tui.refresh_hover(@src()); } pub fn render(self: *Self, theme: *const Widget.Theme) bool { @@ -200,14 +206,9 @@ pub const TabBar = struct { if (btn.hover) break idx; } else return; if (dragging != hover_) { - const tmp = self.tabs[dragging]; - self.tabs[dragging] = self.tabs[hover_]; - self.tabs[hover_] = tmp; + self.swap_tabs_by_index(dragging, hover_); if (self.tabs[dragging].widget.dynamic_cast(Tab.ButtonType)) |btn| btn.hover = false; self.update(); - for (self.widget_list.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| - tui.update_drag_source(&widgetstate.widget, 0); - tui.refresh_hover(@src()); } } } @@ -229,23 +230,49 @@ pub const TabBar = struct { return self.widget_list_widget.hover(); } - fn update_tabs(self: *Self) !void { + fn update_tabs(self: *Self, drag_source: ?*Widget) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); try self.update_tab_buffers(); - const prev_widget_count = self.widget_list.widgets.items.len; - while (self.widget_list.pop()) |widget| if (widget.dynamic_cast(Tab.ButtonType) == null) - widget.deinit(self.widget_list.allocator); - var first = true; - for (self.tabs) |tab| { - if (first) { - first = false; - } else { - try self.widget_list.add(try self.make_spacer()); - } - try self.widget_list.add(tab.widget); - if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| { - if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| - try btn.update_label(Tab.name_from_buffer(buffer)); + var prev_widget_count: usize = 0; + for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { + prev_widget_count += 1; + for (split.widgets.items) |_| prev_widget_count += 1; + }; + + for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| { + for (split.widgets.items) |*widget| + if (&widget.widget == drag_source) tui.reset_drag_context(); + }; + while (self.widget_list.pop()) |split_widget| if (split_widget.dynamic_cast(WidgetList)) |split| { + while (split.pop()) |widget| if (widget.dynamic_cast(Tab.ButtonType) == null) + widget.deinit(self.widget_list.allocator); + split.deinit(self.widget_list.allocator); + }; + + var max_view: usize = 0; + for (self.tabs) |tab| max_view = @max(max_view, tab.view orelse 0); + + var widget_count: usize = 0; + for (0..max_view + 1) |view| { + var first = true; + var view_widget_list = try WidgetList.createH(self.allocator, self.widget_list.plane, "split", .dynamic); + try self.widget_list.add(view_widget_list.widget()); + widget_count += 1; + for (self.tabs) |tab| { + const tab_view = tab.view orelse 0; + if (tab_view != view) continue; + if (first) { + first = false; + } else { + try view_widget_list.add(try self.make_spacer(view_widget_list.plane)); + widget_count += 1; + } + try view_widget_list.add(tab.widget); + widget_count += 1; + if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| { + if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| + try btn.update_label(Tab.name_from_buffer(buffer)); + } } } if (prev_widget_count != self.widget_list.widgets.items.len) @@ -295,14 +322,14 @@ pub const TabBar = struct { else try result.addOne(self.allocator), }; - pos.* = .{ .buffer_ref = buffer_ref, .widget = tab }; + pos.* = .{ .buffer_ref = buffer_ref, .widget = tab, .view = buffer.get_last_view() }; self.place_next = .atend; } - fn make_spacer(self: @This()) !Widget { + fn make_spacer(self: @This(), parent: Plane) !Widget { return spacer.create( self.allocator, - self.widget_list.plane, + parent, self.tab_style.spacer, self.tab_style.spacer_fg, self.tab_style.spacer_bg, @@ -371,9 +398,23 @@ pub const TabBar = struct { tp.trace(tp.channel.debug, .{ "swap_tabs", "not_found", "buffer_ref_b" }); return; }; + self.swap_tabs_by_index(tab_a_idx, tab_b_idx); + } + + fn swap_tabs_by_index(self: *Self, tab_a_idx: usize, tab_b_idx: usize) void { const tmp = self.tabs[tab_a_idx]; self.tabs[tab_a_idx] = self.tabs[tab_b_idx]; self.tabs[tab_b_idx] = tmp; + const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); + if (buffer_manager.buffer_from_ref(self.tabs[tab_a_idx].buffer_ref)) |buffer_a| + if (buffer_manager.buffer_from_ref(self.tabs[tab_b_idx].buffer_ref)) |buffer_b| { + const view_a = buffer_a.get_last_view(); + const view_b = buffer_b.get_last_view(); + if (view_a != view_b) { + buffer_a.set_last_view(view_b); + buffer_b.set_last_view(view_a); + } + }; tp.trace(tp.channel.debug, .{ "swap_tabs", "swapped", "indexes", tab_a_idx, tab_b_idx }); } @@ -417,21 +458,27 @@ pub const TabBar = struct { while (count > 0) : (count -= 1) { var buffer_name: ?[]const u8 = undefined; if (!(cbor.matchValue(&iter2, cbor.extract(&buffer_name)) catch false)) return error.MatchTabBufferNameFailed; - if (buffer_name) |name| if (name_to_ref(name)) |buffer_ref| { - (try result.addOne(self.allocator)).* = .{ - .buffer_ref = buffer_ref, - .widget = try Tab.create(self, buffer_ref, &self.tab_style), - }; - }; + if (buffer_name) |name| { + const buffer_ref_, const buffer_view = name_to_ref_and_view(name); + if (buffer_ref_) |buffer_ref| + (try result.addOne(self.allocator)).* = .{ + .buffer_ref = buffer_ref, + .widget = try Tab.create(self, buffer_ref, &self.tab_style), + .view = buffer_view, + }; + } } self.tabs = try result.toOwnedSlice(self.allocator); iter.* = iter2; } - fn name_to_ref(buffer_name: []const u8) ?usize { + fn name_to_ref_and_view(buffer_name: []const u8) struct { ?usize, ?usize } { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); - return if (buffer_manager.get_buffer_for_file(buffer_name)) |buffer| buffer_manager.buffer_to_ref(buffer) else null; + return if (buffer_manager.get_buffer_for_file(buffer_name)) |buffer| + .{ buffer_manager.buffer_to_ref(buffer), buffer.get_last_view() } + else + .{ null, null }; } }; From 8c6091c41984407d847f437a2c050224baeca030 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 19:53:40 +0100 Subject: [PATCH 10/14] fix: only update tabbar widgets if buffers have changed --- src/tui/status/tabs.zig | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index d7c81a4..011e663 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -232,7 +232,7 @@ pub const TabBar = struct { fn update_tabs(self: *Self, drag_source: ?*Widget) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); - try self.update_tab_buffers(); + if (!try self.update_tab_buffers()) return; var prev_widget_count: usize = 0; for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { prev_widget_count += 1; @@ -275,11 +275,11 @@ pub const TabBar = struct { } } } - if (prev_widget_count != self.widget_list.widgets.items.len) + if (prev_widget_count != widget_count) tui.refresh_hover(@src()); } - fn update_tab_buffers(self: *Self) !void { + fn update_tab_buffers(self: *Self) !bool { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffers = try buffer_manager.list_unordered(self.allocator); defer self.allocator.free(buffers); @@ -305,6 +305,16 @@ pub const TabBar = struct { } self.tabs = try result.toOwnedSlice(self.allocator); + + if (existing_tabs.len != self.tabs.len) + return true; + for (existing_tabs, self.tabs) |tab_a, tab_b| { + if (tab_a.buffer_ref == tab_b.buffer_ref and + tab_a.view == tab_b.view) + continue; + return true; + } + return false; } fn place_new_tab(self: *Self, result: *std.ArrayListUnmanaged(TabBarTab), buffer: *Buffer) !void { From a632305a6fbf5452a1ea875be974f88c022d1255 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 20:12:17 +0100 Subject: [PATCH 11/14] refactor: tab rendering --- src/tui/status/tabs.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 011e663..5ba1ae5 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -159,7 +159,8 @@ pub const TabBar = struct { }); self.plane.fill(" "); self.plane.home(); - return self.widget_list_widget.render(theme); + for (self.tabs) |*tab| _ = tab.widget.render(theme); + return false; } pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { From a4b80377c11fb05c503912cceed8f1570affefce Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 20:30:13 +0100 Subject: [PATCH 12/14] fix: tabbar initial render --- src/tui/status/tabs.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 5ba1ae5..f43ff5b 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -233,7 +233,8 @@ pub const TabBar = struct { fn update_tabs(self: *Self, drag_source: ?*Widget) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); - if (!try self.update_tab_buffers()) return; + const buffers_changed = try self.update_tab_buffers(); + if (!buffers_changed and self.widget_list.widgets.items.len > 0) return; var prev_widget_count: usize = 0; for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { prev_widget_count += 1; From e9a67d4c712b4e44b6d2ef7fded7dd2a3c143871 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 20:45:37 +0100 Subject: [PATCH 13/14] refactor: make Widget.hover const --- src/tui/Widget.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 36376f7..e298fb5 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -55,7 +55,7 @@ pub const VTable = struct { walk: *const fn (ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn, self_widget: *Self) bool, focus: *const fn (ctx: *anyopaque) void, unfocus: *const fn (ctx: *anyopaque) void, - hover: *const fn (ctx: *anyopaque) bool, + hover: *const fn (ctx: *const anyopaque) bool, type_name: []const u8, }; @@ -154,8 +154,8 @@ pub fn to(pimpl: anytype) Self { } }.unfocus, .hover = struct { - pub fn hover(ctx: *anyopaque) bool { - return if (comptime @hasField(child, "hover")) @as(*child, @ptrCast(@alignCast(ctx))).hover else false; + pub fn hover(ctx: *const anyopaque) bool { + return if (comptime @hasField(child, "hover")) @as(*const child, @ptrCast(@alignCast(ctx))).hover else false; } }.hover, }, @@ -245,7 +245,7 @@ pub fn unfocus(self: *Self) void { self.vtable.unfocus(self.ptr); } -pub fn hover(self: *Self) bool { +pub fn hover(self: *const Self) bool { return self.vtable.hover(self.ptr); } @@ -316,7 +316,7 @@ pub fn empty(allocator: Allocator, parent: Plane, layout_: Layout) !Self { pub fn unfocus(_: *anyopaque) void {} }.unfocus, .hover = struct { - pub fn hover(_: *anyopaque) bool { + pub fn hover(_: *const anyopaque) bool { return false; } }.hover, From 48c42737a6a159a93c3b2db497010212067f7ac5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 15 Jan 2026 22:07:34 +0100 Subject: [PATCH 14/14] fix: don't skip tab updates while dragging --- src/tui/status/tabs.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index f43ff5b..4e5128d 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -234,7 +234,11 @@ pub const TabBar = struct { fn update_tabs(self: *Self, drag_source: ?*Widget) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffers_changed = try self.update_tab_buffers(); - if (!buffers_changed and self.widget_list.widgets.items.len > 0) return; + const dragging = for (self.tabs) |*tab| { + if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| + if (btn.drag_pos) |_| break true; + } else false; + if (!dragging and !buffers_changed and self.widget_list.widgets.items.len > 0) return; var prev_widget_count: usize = 0; for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { prev_widget_count += 1;