Files
ghostty/src/apprt/gtk/class/surface_scrolled_window.zig
Daniel Kinzler 6cd684d5d3 gtk: fix stale pointers to property bindings
Previously, the property binding created in `Surface.bindIsSplit` would
get freed automatically when the source object (the SplitTree widget)
got finalized. A subsequent call to `bindIsSplit` could then cause a
crash by using the stale pointer to the binding. This bug could e.g. be
triggered by dragging the surface from a single-surface tab to another
tab.

We now create an extra reference to the binding object so that Surface
essentially owns the binding and is responsible for freeing it.

Updated the binding in `SurfaceScrolledWindow` to use the same pattern.
That one was probably fine, because the binding is only created once,
but let's be safe.
2026-08-29 12:15:24 +02:00

234 lines
7.4 KiB
Zig

const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gtk_version = @import("../gtk_version.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Surface = @import("surface.zig").Surface;
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_surface_scrolled_window);
/// A wrapper widget that embeds a Surface inside a GtkScrolledWindow.
/// This provides scrollbar functionality for the terminal surface.
/// The surface property can be set during initialization or changed
/// dynamically via the surface property.
pub const SurfaceScrolledWindow = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhostttySurfaceScrolledWindow",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const surface = struct {
pub const name = "surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = .{
.getter = getSurfaceValue,
.setter = setSurfaceValue,
},
},
);
};
};
const Private = struct {
config: ?*Config = null,
config_binding: ?*gobject.Binding = null,
surface: ?*Surface = null,
scrolled_window: *gtk.ScrolledWindow,
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
if (gtk_version.runtimeUntil(4, 20, 1)) self.disableKineticScroll();
}
fn disableKineticScroll(self: *Self) void {
// Until gtk 4.20.1 trackpads have kinetic scrolling behavior regardless
// of `Gtk.ScrolledWindow.kinetic_scrolling`. As a workaround, disable
// EventControllerScroll.kinetic
const controllers = self.private().scrolled_window.as(gtk.Widget).observeControllers();
defer controllers.unref();
var i: c_uint = 0;
while (controllers.getObject(i)) |obj| : (i += 1) {
defer obj.unref();
const controller = gobject.ext.cast(gtk.EventControllerScroll, obj) orelse continue;
var flags = controller.getFlags();
flags.kinetic = false;
controller.setFlags(flags);
}
}
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config_binding) |binding| {
binding.unbind();
binding.unref();
priv.config_binding = null;
}
if (priv.config) |v| {
v.unref();
priv.config = null;
}
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
gobject.ext.Value.set(
value,
self.private().surface,
);
}
fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
self.setSurface(gobject.ext.Value.get(
value,
?*Surface,
));
}
pub fn getSurface(self: *Self) ?*Surface {
return self.private().surface;
}
pub fn setSurface(self: *Self, surface_: ?*Surface) void {
const priv = self.private();
if (surface_ == priv.surface) return;
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
priv.surface = surface_;
}
fn closureScrollbarPolicy(
_: *Self,
config_: ?*Config,
) callconv(.c) gtk.PolicyType {
const config = if (config_) |c| c.get() else return .automatic;
return switch (config.scrollbar) {
.never => .never,
.system => .automatic,
};
}
fn propSurface(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
const scrolled_window = self.private().scrolled_window.as(gtk.ScrolledWindow);
scrolled_window.setChild(if (priv.surface) |s| s.as(gtk.Widget) else null);
// Unbind old config binding if it exists
if (priv.config_binding) |binding| {
binding.unbind();
binding.unref();
priv.config_binding = null;
}
// Bind config from surface to our config property
if (priv.surface) |surface| {
const binding = surface.as(gobject.Object).bindProperty(
properties.config.name,
self.as(gobject.Object),
properties.config.name,
.{ .sync_create = true },
);
// Keep another ref, otherwise the binding would be freed and
// our pointer become stale if the surface gets finalized.
binding.ref();
priv.config_binding = binding;
}
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "surface-scrolled-window",
}),
);
// Bindings
class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy);
class.bindTemplateCallback("notify_surface", &propSurface);
class.bindTemplateChildPrivate("scrolled_window", .{});
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
properties.surface.impl,
});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};