aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:51 +0300
commit5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch)
tree672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
parent32abb3776668ee768200019c170956f67fe33a86 (diff)
chore: update libghostty vendor
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig')
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig165
1 files changed, 120 insertions, 45 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig b/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
index 311fbd8a..5aef53d2 100644
--- a/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
@@ -158,6 +158,13 @@ pub const SplitTree = extern struct {
/// used to debounce updates.
rebuild_source: ?c_uint = null,
+ /// The source that we use to restore focus. With enough nested
+ /// splits, some surfaces might initially be allocated a width or
+ /// height of 0 which causes them to get unmapped and lose focus.
+ /// We can reliably restore focus to the last focused surface only
+ /// once it is mapped again.
+ restore_focus_source: ?c_uint = null,
+
/// Used to store state about a pending surface close for the
/// close dialog.
pending_close: ?Surface.Tree.Node.Handle,
@@ -415,6 +422,13 @@ pub const SplitTree = extern struct {
self,
.{ .detail = "focused" },
);
+ _ = gobject.Object.signals.notify.connect(
+ surface,
+ *Self,
+ propSurfaceMapped,
+ self,
+ .{ .detail = "mapped" },
+ );
}
}
@@ -571,6 +585,12 @@ pub const SplitTree = extern struct {
}
priv.rebuild_source = null;
}
+ if (priv.restore_focus_source) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove restore_focus source", .{});
+ }
+ priv.restore_focus_source = null;
+ }
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
@@ -766,6 +786,24 @@ pub const SplitTree = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
+ fn propSurfaceMapped(
+ surface: *Surface,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ if (!surface.getMapped()) return;
+
+ // We could add the idle callback only if this is actually the last
+ // focused surface. But we can avoid that check because usually all
+ // the surfaces get mapped at once, so the idle callback will run
+ // only once anyway.
+ const priv = self.private();
+ if (priv.restore_focus_source == null) priv.restore_focus_source = glib.idleAdd(
+ onRestoreFocus,
+ self,
+ );
+ }
+
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
@@ -779,14 +817,20 @@ pub const SplitTree = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
- // If we were planning a rebuild, always remove that so we can
- // start from a clean slate.
+ // If we were planning a rebuild or focus restore, always remove
+ // that so we can start from a clean slate.
if (priv.rebuild_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove rebuild source", .{});
}
priv.rebuild_source = null;
}
+ if (priv.restore_focus_source) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove restore_focus source", .{});
+ }
+ priv.restore_focus_source = null;
+ }
// If we transitioned to an empty tree, clear immediately instead of
// waiting for an idle callback. Delaying teardown can keep the last
@@ -842,6 +886,26 @@ pub const SplitTree = extern struct {
return 0;
}
+ fn onRestoreFocus(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+
+ // Always mark our source as null since we're done.
+ const priv = self.private();
+ priv.restore_focus_source = null;
+
+ // If we have a last-focused surface and it is mapped, restore focus
+ // to it. Depending on the available size, the surface might already
+ // have focus because it never got unmapped. In that case grabbing
+ // focus will have no effect.
+ if (priv.last_focused.get()) |v| {
+ defer v.unref();
+ if (v.getMapped()) {
+ v.grabFocus();
+ }
+ }
+ return 0;
+ }
+
/// Builds the widget tree associated with a surface split tree.
///
/// Returned widgets are expected to be attached to a parent by the caller.
@@ -1044,6 +1108,12 @@ const SplitTreeSplit = extern struct {
/// Source to handle repositioning the split when properties change.
idle: ?c_uint = null,
+ /// Whether the max-position/position property of the gtk.Paned widget
+ /// changed. We use these to distinguish between a resize and the user
+ /// manually moving the split divider. See the "on-idle" function.
+ max_changed: bool = false,
+ pos_changed: bool = false,
+
// Template bindings
paned: *gtk.Paned,
@@ -1083,21 +1153,37 @@ const SplitTreeSplit = extern struct {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
- fn refresh(self: *Self) void {
- const priv = self.private();
- if (priv.idle == null) priv.idle = glib.idleAdd(
- onIdle,
- self,
- );
- }
-
+ // We need to keep the split ratios from the tree datastructure and
+ // widget tree in sync. Using the max-position and position properties
+ // of the gtk.Paned widget, we can distinguish a resize from a manual
+ // update (e.g. the user dragging the divider).If max-position changes,
+ // we always have a widget resize. Usually position will change as well
+ // but it might not if the size change is small enough. If only position
+ // changes, we have a manual human update.
+ //
+ // This is a hack, it relies on the timing of property notifcations.
+ // From looking at the GTK source code, it should not be possible that
+ // we interpret a position change from a resize as a manual update.
+ // When a gtk.Paned is resized, internally the gtk_paned_calc_position
+ // function will change both max-position and position and synchronously
+ // call our propMaxPosition and propPosition functions. I.e. when the
+ // widget is resized, it should not be possible for onIdle to run before
+ // we have been notified of both property changes.
fn onIdle(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
const priv = self.private();
const paned = priv.paned;
- // Our idle source is always over
- priv.idle = null;
+ // Clear source and fields at the end. Otherwise if setPosition is
+ // called below, propPosition is triggered and would add another
+ // idle callback before this one is finished.
+ defer priv.idle = null;
+ defer priv.max_changed = false;
+ defer priv.pos_changed = false;
+
+ if (!priv.max_changed and !priv.pos_changed) {
+ return 0;
+ }
// Get our split. This is the most dangerous part of this entire
// widget. We assume that this widget is always a child of a
@@ -1132,16 +1218,6 @@ const SplitTreeSplit = extern struct {
);
break :max gobject.ext.Value.get(&val, c_int);
};
- const pos_set: bool = max: {
- var val = gobject.ext.Value.new(c_int);
- defer val.unset();
- gobject.Object.getProperty(
- paned.as(gobject.Object),
- "position-set",
- &val,
- );
- break :max gobject.ext.Value.get(&val, c_int) != 0;
- };
// We don't actually use min, but we don't expect this to ever
// be non-zero, so let's add an assert to ensure that.
@@ -1172,51 +1248,51 @@ const SplitTreeSplit = extern struct {
return 0;
}
- // If we're out of bounds, then we need to either set the position
- // to what we expect OR update our expected ratio.
-
- // If we've never set the position, then we set it to the desired.
- if (!pos_set) {
+ if (priv.max_changed) {
+ // Widget got resized, update position to match desired ratio.
+ // Note that if max-position is small, it might not be possible
+ // to accurately set the desired ratio. E.g. with max-position=2
+ // you can only have ratios 0, 0.5 and 1.
const desired_pos: c_int = desired_pos: {
const max_f64: f64 = @floatFromInt(max);
break :desired_pos @intFromFloat(@round(max_f64 * desired_ratio));
};
paned.setPosition(desired_pos);
- return 0;
+ } else {
+ // If only position changed, this is a manual human update and
+ // we need to write our update back to the tree.
+ tree.resizeInPlace(priv.handle, @floatCast(current_ratio));
}
-
- // If we've set the position, then this is a manual human update
- // and we need to write our update back to the tree.
- tree.resizeInPlace(priv.handle, @floatCast(current_ratio));
-
return 0;
}
//---------------------------------------------------------------
// Signal handlers
- fn propPosition(
- _: *gtk.Paned,
- _: *gobject.ParamSpec,
- self: *Self,
- ) callconv(.c) void {
- self.refresh();
- }
-
fn propMaxPosition(
_: *gtk.Paned,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
- self.refresh();
+ const priv = self.private();
+ priv.max_changed = true;
+ if (priv.idle == null) priv.idle = glib.idleAdd(
+ onIdle,
+ self,
+ );
}
- fn propMinPosition(
+ fn propPosition(
_: *gtk.Paned,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
- self.refresh();
+ const priv = self.private();
+ priv.pos_changed = true;
+ if (priv.idle == null) priv.idle = glib.idleAdd(
+ onIdle,
+ self,
+ );
}
//---------------------------------------------------------------
@@ -1275,7 +1351,6 @@ const SplitTreeSplit = extern struct {
// Template Callbacks
class.bindTemplateCallback("notify_max_position", &propMaxPosition);
- class.bindTemplateCallback("notify_min_position", &propMinPosition);
class.bindTemplateCallback("notify_position", &propPosition);
// Virtual methods