apprt/gtk-ng: keep track of last focused surface

pull/8207/head
Mitchell Hashimoto 2025-08-08 15:03:28 -07:00
parent 517f17995c
commit a3c041bcb4
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 110 additions and 3 deletions

View File

@ -1257,6 +1257,7 @@ pub const Application = extern struct {
diag.close();
diag.unref(); // strong ref from get()
}
priv.config_errors_dialog.set(null);
if (priv.signal_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove signal source", .{});

View File

@ -17,6 +17,7 @@ const adw_version = @import("../adw_version.zig");
const ext = @import("../ext.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
@ -118,6 +119,10 @@ pub const SplitTree = extern struct {
// Template bindings
tree_bin: *adw.Bin,
/// Last focused surface in the tree. We need this to handle various
/// tree change states.
last_focused: WeakRef(Surface) = .{},
/// The source that we use to rebuild the tree. This is also
/// used to debounce updates.
rebuild_source: ?c_uint = null,
@ -208,6 +213,13 @@ pub const SplitTree = extern struct {
var single_tree = try Surface.Tree.init(alloc, surface);
defer single_tree.deinit();
// We want to move our focus to the new surface no matter what.
// But we need to be careful to restore state if we fail.
const old_last_focused = self.private().last_focused.get();
defer if (old_last_focused) |v| v.unref(); // unref strong ref from get
self.private().last_focused.set(surface);
errdefer self.private().last_focused.set(old_last_focused);
// If we have no tree yet, then this becomes our tree and we're done.
const old_tree = self.getTree() orelse {
self.setTree(&single_tree);
@ -238,6 +250,38 @@ pub const SplitTree = extern struct {
surface.grabFocus();
}
fn disconnectSurfaceHandlers(self: *Self) void {
const tree = self.getTree() orelse return;
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = gobject.signalHandlersDisconnectMatched(
surface.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
}
}
fn connectSurfaceHandlers(self: *Self) void {
const tree = self.getTree() orelse return;
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = gobject.Object.signals.notify.connect(
surface,
*Self,
propSurfaceFocused,
self,
.{ .detail = "focused" },
);
}
}
//---------------------------------------------------------------
// Properties
@ -259,6 +303,15 @@ pub const SplitTree = extern struct {
return null;
}
/// Returns the last focused surface in the tree.
pub fn getLastFocusedSurface(self: *Self) ?*Surface {
const surface = self.private().last_focused.get() orelse return null;
// We unref because get() refs the surface. We don't use the weakref
// in a multi-threaded context so this is safe.
surface.unref();
return surface;
}
pub fn getHasSurfaces(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return !tree.isEmpty();
@ -285,12 +338,14 @@ pub const SplitTree = extern struct {
);
if (priv.tree) |old_tree| {
self.disconnectSurfaceHandlers();
ext.boxedFree(Surface.Tree, old_tree);
priv.tree = null;
}
if (tree) |new_tree| {
priv.tree = ext.boxedCopy(Surface.Tree, new_tree);
self.connectSurfaceHandlers();
}
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
@ -315,6 +370,7 @@ pub const SplitTree = extern struct {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
priv.last_focused.set(null);
if (priv.rebuild_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove rebuild source", .{});
@ -363,6 +419,18 @@ pub const SplitTree = extern struct {
};
}
fn propSurfaceFocused(
surface: *Surface,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// We never CLEAR our last_focused because the property is specifically
// the last focused surface. We let the weakref clear itself when
// the surface is destroyed.
if (!surface.getFocused()) return;
self.private().last_focused.set(surface);
}
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
@ -402,6 +470,14 @@ pub const SplitTree = extern struct {
priv.tree_bin.setChild(buildTree(tree, 0));
}
// If we have a last focused surface, we need to refocus it, because
// during the frame between setting the bin to null and rebuilding,
// GTK will reset our focus state (as it should!)
if (priv.last_focused.get()) |v| {
defer v.unref();
v.grabFocus();
}
return 0;
}

View File

@ -45,6 +45,38 @@
...
}
# Reproduction:
#
# 1. Launch Ghostty
# 2. Split Right
# 3. Hit "X" to close
{
GTK CSS Node State
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
fun:g_malloc
fun:g_memdup2
fun:gtk_css_node_declaration_set_state
fun:gtk_css_node_set_state
fun:gtk_widget_propagate_state
fun:gtk_widget_update_state_flags
fun:gtk_main_do_event
fun:surface_event
fun:_gdk_marshal_BOOLEAN__POINTERv
fun:gdk_surface_event_marshallerv
fun:_g_closure_invoke_va
fun:signal_emit_valist_unlocked
fun:g_signal_emit_valist
fun:g_signal_emit
fun:gdk_surface_handle_event
fun:gdk_wayland_event_source_dispatch
fun:g_main_context_dispatch_unlocked
fun:g_main_context_iterate_unlocked.isra.0
fun:g_main_context_iteration
...
}
{
GTK CSS Provider Leak
Memcheck:Leak
@ -516,9 +548,7 @@
pango font map
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
fun:g_malloc0
fun:g_rc_box_alloc_full
...
fun:pango_fc_font_map_load_fontset
...
}