refactor: compare widgets via the .ptr memeber instead of via their addresses

Widget is a handle type. Sort of a smart pointer. Comparing their addresses
is brittle because it requires keeping Widget pointers stable. This is
nonsense because Widget identity is actually determined by the actual
widget object it points to.

This big refactor elimits the requirement that Widget addresses remain
stable to work properly with Widget.walk and Widget.get.
This commit is contained in:
CJ van den Berg 2026-02-23 21:44:24 +01:00
parent 2266c92eab
commit cdd6fee9d6
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
9 changed files with 59 additions and 52 deletions

View file

@ -186,7 +186,14 @@ pub fn State(ctx_type: type) type {
} }
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return self.menu.walk(walk_ctx, f, if (self.frame_widget) |*frame| frame else &self.container_widget); for (self.menu.widgets.items) |*w|
if (w.widget.walk(walk_ctx, f))
return true;
return if (self.frame_widget) |frame|
frame.walk(walk_ctx, f)
else
self.container_widget.walk(walk_ctx, f);
} }
pub fn count(self: *Self) usize { pub fn count(self: *Self) usize {

View file

@ -21,7 +21,7 @@ vtable: *const VTable,
const Self = @This(); const Self = @This();
pub const WalkFn = *const fn (ctx: *anyopaque, w: *Self) bool; pub const WalkFn = *const fn (ctx: *anyopaque, w: Self) bool;
pub const Direction = enum { horizontal, vertical }; pub const Direction = enum { horizontal, vertical };
pub const Layout = union(enum) { pub const Layout = union(enum) {
@ -51,8 +51,8 @@ pub const VTable = struct {
layout: *const fn (ctx: *anyopaque) Layout, layout: *const fn (ctx: *anyopaque) Layout,
subscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void, subscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
unsubscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void, unsubscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
get: *const fn (ctx: *const anyopaque, name_: []const u8) ?*const Self, get: *const fn (ctx: *const anyopaque, name_: []const u8) ?Self,
walk: *const fn (ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn, self_widget: *Self) bool, walk: *const fn (ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool,
focus: *const fn (ctx: *anyopaque) void, focus: *const fn (ctx: *anyopaque) void,
unfocus: *const fn (ctx: *anyopaque) void, unfocus: *const fn (ctx: *anyopaque) void,
hover: *const fn (ctx: *const anyopaque) bool, hover: *const fn (ctx: *const anyopaque) bool,
@ -134,13 +134,13 @@ pub fn to(pimpl: anytype) Self {
} }
}.unsubscribe, }.unsubscribe,
.get = struct { .get = struct {
pub fn get(ctx: *const anyopaque, name_: []const u8) ?*const Self { pub fn get(ctx: *const anyopaque, name_: []const u8) ?Self {
return if (comptime @hasDecl(child, "get")) child.get(@as(*const child, @ptrCast(@alignCast(ctx))), name_) else null; return if (comptime @hasDecl(child, "get")) child.get(@as(*const child, @ptrCast(@alignCast(ctx))), name_) else null;
} }
}.get, }.get,
.walk = struct { .walk = struct {
pub fn walk(ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn, self: *Self) bool { pub fn walk(ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (comptime @hasDecl(child, "walk")) child.walk(@as(*child, @ptrCast(@alignCast(ctx))), walk_ctx, f, self) else false; return if (comptime @hasDecl(child, "walk")) child.walk(@as(*child, @ptrCast(@alignCast(ctx))), walk_ctx, f) else false;
} }
}.walk, }.walk,
.focus = struct { .focus = struct {
@ -225,16 +225,16 @@ pub fn unsubscribe(self: Self, h: EventHandler) !void {
return self.vtable.unsubscribe(self.ptr, h); return self.vtable.unsubscribe(self.ptr, h);
} }
pub fn get(self: *const Self, name_: []const u8) ?*const Self { pub fn get(self: *const Self, name_: []const u8) ?Self {
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
return if (std.mem.eql(u8, self.plane.name(&buf), name_)) return if (std.mem.eql(u8, self.plane.name(&buf), name_))
self self.*
else else
self.vtable.get(self.ptr, name_); self.vtable.get(self.ptr, name_);
} }
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: WalkFn) bool { pub fn walk(self: *const Self, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (self.vtable.walk(self.ptr, walk_ctx, f, self)) true else f(walk_ctx, self); return if (self.vtable.walk(self.ptr, walk_ctx, f)) true else f(walk_ctx, self.*);
} }
pub fn focus(self: *Self) void { pub fn focus(self: *Self) void {
@ -300,12 +300,12 @@ pub fn empty(allocator: Allocator, parent: Plane, layout_: Layout) !Self {
} }
}.unsubscribe, }.unsubscribe,
.get = struct { .get = struct {
pub fn get(_: *const anyopaque, _: []const u8) ?*const Self { pub fn get(_: *const anyopaque, _: []const u8) ?Self {
return null; return null;
} }
}.get, }.get,
.walk = struct { .walk = struct {
pub fn walk(_: *anyopaque, _: *anyopaque, _: WalkFn, _: *Self) bool { pub fn walk(_: *anyopaque, _: *anyopaque, _: WalkFn) bool {
return false; return false;
} }
}.walk, }.walk,

View file

@ -115,7 +115,7 @@ pub fn addP(self: *Self, w_: Widget) !*Widget {
return &w.widget; return &w.widget;
} }
pub fn get(self: *const Self, name_: []const u8) ?*const Widget { pub fn get(self: *const Self, name_: []const u8) ?Widget {
for (self.widgets.items) |*w| for (self.widgets.items) |*w|
if (w.widget.get(name_)) |p| if (w.widget.get(name_)) |p|
return p; return p;
@ -347,10 +347,10 @@ fn do_resize(self: *Self, padding: Widget.Style.Margin) void {
} }
} }
pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, self_w: *Widget) bool { pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.widgets.items) |*w| for (self.widgets.items) |*w|
if (w.widget.walk(ctx, f)) return true; if (w.widget.walk(ctx, f)) return true;
return f(ctx, self_w); return f(ctx, Widget.to(self));
} }
pub fn focus(self: *Self) void { pub fn focus(self: *Self) void {

View file

@ -38,7 +38,7 @@ highlight: bool,
symbols: bool, symbols: bool,
width: usize, width: usize,
editor: *ed.Editor, editor: *ed.Editor,
editor_widget: ?*const Widget = null, editor_widget: ?Widget = null,
differ: diffz.AsyncDiffer, differ: diffz.AsyncDiffer,
const Self = @This(); const Self = @This();

View file

@ -107,8 +107,8 @@ pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.update_scrollbar(); self.update_scrollbar();
} }
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn, w: *Widget) bool { pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return self.menu.container_widget.walk(walk_ctx, f) or f(walk_ctx, w); return self.menu.container_widget.walk(walk_ctx, f) or f(walk_ctx, Widget.to(self));
} }
pub fn add_item(self: *Self, entry_: Entry) !void { pub fn add_item(self: *Self, entry_: Entry) !void {

View file

@ -201,8 +201,8 @@ pub fn update(self: *Self) void {
self.menu.update(); self.menu.update();
} }
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn, w: *Widget) bool { pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return self.menu.walk(walk_ctx, f) or f(walk_ctx, w); return self.menu.walk(walk_ctx, f) or f(walk_ctx, Widget.to(self));
} }
pub fn receive(_: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { pub fn receive(_: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {

View file

@ -1732,8 +1732,8 @@ pub fn get_active_buffer(self: *Self) ?*Buffer {
return if (self.get_active_editor()) |editor| editor.buffer orelse null else null; return if (self.get_active_editor()) |editor| editor.buffer orelse null else null;
} }
pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, w: *Widget) bool { pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn) bool {
return self.floating_views.walk(ctx, f) or self.widgets.walk(ctx, f, &self.widgets_widget) or f(ctx, w); return self.floating_views.walk(ctx, f) or self.widgets.walk(ctx, f) or f(ctx, Widget.to(self));
} }
fn close_all_editors(self: *Self) !void { fn close_all_editors(self: *Self) !void {
@ -1754,12 +1754,12 @@ fn add_and_activate_view(self: *Self, widget: Widget) !void {
_ = try self.widgets_widget.msg(.{"splits_updated"}); _ = try self.widgets_widget.msg(.{"splits_updated"});
} }
pub fn find_view_for_widget(self: *Self, w_: *const Widget) ?usize { pub fn find_view_for_widget(self: *Self, w_: Widget) ?usize {
const Ctx = struct { const Ctx = struct {
w: *const Widget, w: Widget,
fn find(ctx_: *anyopaque, w: *Widget) bool { fn find(ctx_: *anyopaque, w: Widget) bool {
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
return ctx.w == w; return ctx.w.ptr == w.ptr;
} }
}; };
var ctx: Ctx = .{ .w = w_ }; var ctx: Ctx = .{ .w = w_ };
@ -1768,7 +1768,7 @@ pub fn find_view_for_widget(self: *Self, w_: *const Widget) ?usize {
return null; return null;
} }
pub fn focus_view_by_widget(self: *Self, w: *const Widget) tui.FocusAction { pub fn focus_view_by_widget(self: *Self, w: Widget) tui.FocusAction {
const n = self.find_view_for_widget(w) orelse return .notfound; const n = self.find_view_for_widget(w) orelse return .notfound;
if (n >= self.views.widgets.items.len) return .notfound; if (n >= self.views.widgets.items.len) return .notfound;
if (n == self.active_view) return .same; if (n == self.active_view) return .same;

View file

@ -286,11 +286,11 @@ pub const TabBar = struct {
self.plane = self.splits_list.plane; self.plane = self.splits_list.plane;
} }
pub fn get(self: *const Self, name: []const u8) ?*const Widget { pub fn get(self: *const Self, name: []const u8) ?Widget {
return self.splits_list_widget.get(name); return self.splits_list_widget.get(name);
} }
pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, self_w: *Widget) bool { pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.tabs) |*tab| { for (self.tabs) |*tab| {
const clipped, _ = self.is_tab_clipped(tab); const clipped, _ = self.is_tab_clipped(tab);
if (!clipped) if (!clipped)
@ -300,14 +300,14 @@ pub const TabBar = struct {
for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(drop_target.ButtonType)) |_| { for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(drop_target.ButtonType)) |_| {
if (widget_state.widget.walk(ctx, f)) return true; if (widget_state.widget.walk(ctx, f)) return true;
}; };
return f(ctx, self_w); return f(ctx, Widget.to(self));
} }
pub fn hover(self: *Self) bool { pub fn hover(self: *Self) bool {
return self.splits_list_widget.hover(); return self.splits_list_widget.hover();
} }
fn update_tabs(self: *Self, drag_source: ?*Widget) !void { fn update_tabs(self: *Self, drag_source: ?Widget) !bool {
const buffers_changed = try self.update_tab_buffers(); const buffers_changed = try self.update_tab_buffers();
const dragging = for (self.tabs) |*tab| { const dragging = for (self.tabs) |*tab| {
if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn|

View file

@ -54,12 +54,12 @@ input_mode_outer_: ?Mode = null,
input_listeners_: EventHandler.List, input_listeners_: EventHandler.List,
keyboard_focus: ?Widget = null, keyboard_focus: ?Widget = null,
mini_mode_: ?MiniMode = null, mini_mode_: ?MiniMode = null,
hover_focus: ?*Widget = null, hover_focus: ?Widget = null,
last_hover_x: c_int = -1, last_hover_x: c_int = -1,
last_hover_y: c_int = -1, last_hover_y: c_int = -1,
commands: Commands = undefined, commands: Commands = undefined,
logger: log.Logger, logger: log.Logger,
drag_source: ?*Widget = null, drag_source: ?Widget = null,
drag_button: input.MouseType = 0, drag_button: input.MouseType = 0,
dark_theme: Widget.Theme, dark_theme: Widget.Theme,
dark_parsed_theme: ?std.json.Parsed(Widget.Theme), dark_parsed_theme: ?std.json.Parsed(Widget.Theme),
@ -787,12 +787,12 @@ fn handle_system_clipboard(self: *Self, text: []const u8) !void {
return command.executeName("paste", command.fmt(.{text})); return command.executeName("paste", command.fmt(.{text}));
} }
fn find_coord_widget(self: *Self, y: usize, x: usize) ?*Widget { fn find_coord_widget(self: *Self, y: usize, x: usize) ?Widget {
const Ctx = struct { const Ctx = struct {
widget: ?*Widget = null, widget: ?Widget = null,
y: usize, y: usize,
x: usize, x: usize,
fn find(ctx_: *anyopaque, w: *Widget) bool { fn find(ctx_: *anyopaque, w: Widget) bool {
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
if (w.box().is_abs_coord_inside(ctx.y, ctx.x)) { if (w.box().is_abs_coord_inside(ctx.y, ctx.x)) {
ctx.widget = w; ctx.widget = w;
@ -810,12 +810,12 @@ pub fn is_abs_coord_in_widget(w: *const Widget, y: usize, x: usize) bool {
return w.box().is_abs_coord_inside(y, x); return w.box().is_abs_coord_inside(y, x);
} }
fn is_live_widget_ptr(self: *Self, w_: *Widget) bool { fn is_live_widget_ptr(self: *Self, w_: Widget) bool {
const Ctx = struct { const Ctx = struct {
w: *Widget, w: Widget,
fn find(ctx_: *anyopaque, w: *Widget) bool { fn find(ctx_: *anyopaque, w: Widget) bool {
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
return ctx.w == w; return ctx.w.ptr == w.ptr;
} }
}; };
var ctx: Ctx = .{ .w = w_ }; var ctx: Ctx = .{ .w = w_ };
@ -824,7 +824,7 @@ fn is_live_widget_ptr(self: *Self, w_: *Widget) bool {
pub const FocusAction = enum { same, changed, notfound }; pub const FocusAction = enum { same, changed, notfound };
pub fn set_focus_by_widget(w: *const Widget) FocusAction { pub fn set_focus_by_widget(w: Widget) FocusAction {
const mv = mainview() orelse return .notfound; const mv = mainview() orelse return .notfound;
return mv.focus_view_by_widget(w); return mv.focus_view_by_widget(w);
} }
@ -869,12 +869,12 @@ fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.mess
if (self.drag_source) |w| _ = try w.send(from, m); if (self.drag_source) |w| _ = try w.send(from, m);
} }
fn update_hover(self: *Self, y: c_int, x: c_int) !?*Widget { fn update_hover(self: *Self, y: c_int, x: c_int) !?Widget {
self.last_hover_y = y; self.last_hover_y = y;
self.last_hover_x = x; self.last_hover_x = x;
if (y >= 0 and x >= 0) if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| { 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) { if (if (self.hover_focus) |h| h.ptr != w.ptr else true) {
tp.trace(tp.channel.debug, .{ "update_hover", if (self.hover_focus) |h| @intFromPtr(h) else 0, @intFromPtr(w) }); tp.trace(tp.channel.debug, .{ "update_hover", if (self.hover_focus) |h| @as(u64, @intFromPtr(h.ptr)) else 0, @as(u64, @intFromPtr(w.ptr)) });
if (self.hover_focus) |h| if (self.is_live_widget_ptr(h)) if (self.hover_focus) |h| if (self.is_live_widget_ptr(h))
try send_hover_msg(h, false); try send_hover_msg(h, false);
self.hover_focus = w; self.hover_focus = w;
@ -889,19 +889,19 @@ fn update_hover(self: *Self, y: c_int, x: c_int) !?*Widget {
fn clear_hover_focus(self: *Self, src: std.builtin.SourceLocation) tp.result { fn clear_hover_focus(self: *Self, src: std.builtin.SourceLocation) tp.result {
if (self.hover_focus) |h| if (self.is_live_widget_ptr(h)) if (self.hover_focus) |h| if (self.is_live_widget_ptr(h))
try send_hover_msg(h, false); 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 }); tp.trace(tp.channel.debug, .{ "tui", "clear_hover_focus", if (self.hover_focus) |h| @intFromPtr(h.ptr) else 0, src.fn_name, src.file, src.line });
self.hover_focus = null; self.hover_focus = null;
} }
fn send_hover_msg(widget: *const Widget, hover: bool) tp.result { fn send_hover_msg(widget: Widget, hover: bool) tp.result {
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
tp.trace(tp.channel.debug, .{ "hover_msg", @intFromPtr(widget), hover }); tp.trace(tp.channel.debug, .{ "hover_msg", @intFromPtr(widget.ptr), hover });
_ = try widget.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", hover }) catch |e| return tp.exit_error(e, @errorReturnTrace())); _ = 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 { pub fn refresh_hover(src: std.builtin.SourceLocation) void {
const self = current(); 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 }); tp.trace(tp.channel.debug, .{ "tui", "refresh_hover", if (self.hover_focus) |h| @intFromPtr(h.ptr) else 0, src.fn_name, src.file, src.line });
_ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {}; _ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {};
} }
@ -1906,18 +1906,18 @@ pub fn get_keybind_mode() ?Mode {
return self.input_mode_ orelse self.delayed_init_input_mode; return self.input_mode_ orelse self.delayed_init_input_mode;
} }
pub fn update_drag_source(drag_source: *Widget, btn: input.MouseType) void { pub fn update_drag_source(drag_source: Widget, btn: input.MouseType) void {
const self = current(); const self = current();
self.drag_source = drag_source; self.drag_source = drag_source;
self.drag_button = btn; self.drag_button = btn;
} }
fn set_drag_source(self: *Self, drag_source: ?*Widget, btn: input.MouseType) void { fn set_drag_source(self: *Self, drag_source: ?Widget, btn: input.MouseType) void {
self.drag_source = drag_source; self.drag_source = drag_source;
self.drag_button = btn; self.drag_button = btn;
} }
pub fn get_drag_source() struct { ?*Widget, input.MouseType } { pub fn get_drag_source() struct { ?Widget, input.MouseType } {
const self = current(); const self = current();
return .{ self.drag_source, self.drag_button }; return .{ self.drag_source, self.drag_button };
} }