mirror-ghostty/src/apprt/gtk/class/split_tree.zig

1174 lines
39 KiB
Zig

const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const gtk_version = @import("../gtk_version.zig");
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;
const Surface = @import("surface.zig").Surface;
const log = std.log.scoped(.gtk_ghostty_split_tree);
pub const SplitTree = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = gtk.Box;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttySplitTree",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
/// The active surface is the surface that should be receiving all
/// surface-targeted actions. This is usually the focused surface,
/// but may also not be focused if the user has selected a non-surface
/// widget.
pub const @"active-surface" = struct {
pub const name = "active-surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface,
.{
.getter = getActiveSurface,
},
),
},
);
};
pub const @"has-surfaces" = struct {
pub const name = "has-surfaces";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{
.getter = getHasSurfaces,
},
),
},
);
};
pub const @"is-zoomed" = struct {
pub const name = "is-zoomed";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{
.getter = getIsZoomed,
},
),
},
);
};
pub const tree = struct {
pub const name = "tree";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface.Tree,
.{
.accessor = .{
.getter = getTreeValue,
.setter = setTreeValue,
},
},
);
};
};
pub const signals = struct {
/// Emitted whenever the tree property has changed, with access
/// to the previous and new values.
pub const changed = struct {
pub const name = "changed";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{ ?*const Surface.Tree, ?*const Surface.Tree },
void,
);
};
};
const Private = struct {
/// The tree datastructure containing all of our surface views.
tree: ?*Surface.Tree,
// 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) = .empty,
/// The source that we use to rebuild the tree. This is also
/// used to debounce updates.
rebuild_source: ?c_uint = null,
/// Tracks whether we want a rebuild to happen at the next tick
/// that our surface tree has no surfaces with parents. See the
/// propTree function for a lot more details.
rebuild_pending: bool,
/// Used to store state about a pending surface close for the
/// close dialog.
pending_close: ?Surface.Tree.Node.Handle,
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Initialize our actions
self.initActionMap();
// Initialize some basic state
const priv = self.private();
priv.pending_close = null;
}
fn initActionMap(self: *Self) void {
const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_variant_type.free();
const actions = [_]ext.actions.Action(Self){
// All of these will eventually take a target surface parameter.
// For now all our targets originate from the focused surface.
.init("new-split", actionNewSplit, s_variant_type),
.init("equalize", actionEqualize, null),
.init("zoom", actionZoom, null),
};
ext.actions.addAsGroup(Self, self, "split-tree", &actions);
}
/// Create a new split in the given direction from the currently
/// active surface.
///
/// If the tree is empty this will create a new tree with a new surface
/// and ignore the direction.
///
/// The parent will be used as the parent of the surface regardless of
/// if that parent is in this split tree or not. This allows inheriting
/// surface properties from anywhere.
pub fn newSplit(
self: *Self,
direction: Surface.Tree.Split.Direction,
parent_: ?*Surface,
) Allocator.Error!void {
const alloc = Application.default().allocator();
// Create our new surface.
const surface: *Surface = .new();
defer surface.unref();
_ = surface.refSink();
// Inherit properly if we were asked to.
if (parent_) |p| {
if (p.core()) |core| {
surface.setParent(core);
}
}
// Create our tree
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);
return;
};
// The handle we create the split relative to. Today this is the active
// surface but this might be the handle of the given parent if we want.
const handle = self.getActiveSurfaceHandle() orelse .root;
// Create our split!
var new_tree = try old_tree.split(
alloc,
handle,
direction,
0.5, // Always split equally for new splits
&single_tree,
);
defer new_tree.deinit();
log.debug(
"new split at={} direction={} old_tree={} new_tree={}",
.{ handle, direction, old_tree, &new_tree },
);
// Replace our tree
self.setTree(&new_tree);
}
pub fn resize(
self: *Self,
direction: Surface.Tree.Split.Direction,
amount: u16,
) Allocator.Error!bool {
// Avoid useless work
if (amount == 0) return false;
const old_tree = self.getTree() orelse return false;
const active = self.getActiveSurfaceHandle() orelse return false;
// Get all our dimensions we're going to need to turn our
// amount into a percentage.
const priv = self.private();
const width = priv.tree_bin.as(gtk.Widget).getWidth();
const height = priv.tree_bin.as(gtk.Widget).getHeight();
if (width == 0 or height == 0) return false;
const width_f64: f64 = @floatFromInt(width);
const height_f64: f64 = @floatFromInt(height);
const amount_f64: f64 = @floatFromInt(amount);
// Get our ratio and use positive/neg for directions.
const ratio: f64 = switch (direction) {
.right => amount_f64 / width_f64,
.left => -(amount_f64 / width_f64),
.down => amount_f64 / height_f64,
.up => -(amount_f64 / height_f64),
};
const layout: Surface.Tree.Split.Layout = switch (direction) {
.left, .right => .horizontal,
.up, .down => .vertical,
};
var new_tree = try old_tree.resize(
Application.default().allocator(),
active,
layout,
@floatCast(ratio),
);
defer new_tree.deinit();
self.setTree(&new_tree);
return true;
}
/// Move focus from the currently focused surface to the given
/// direction. Returns true if focus switched to a new surface.
pub fn goto(self: *Self, to: Surface.Tree.Goto) bool {
const tree = self.getTree() orelse return false;
const active = self.getActiveSurfaceHandle() orelse return false;
const target = if (tree.goto(
Application.default().allocator(),
active,
to,
)) |handle_|
handle_ orelse return false
else |err| switch (err) {
// Nothing we can do in this scenario. This is highly unlikely
// since split trees don't use that much memory. The application
// is probably about to crash in other ways.
error.OutOfMemory => return false,
};
// If we aren't changing targets then we did nothing.
if (active == target) return false;
// Get the surface at the target location and grab focus.
const surface = tree.nodes[target.idx()].leaf;
surface.grabFocus();
return true;
}
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;
_ = Surface.signals.@"close-request".connect(
surface,
*Self,
surfaceCloseRequest,
self,
.{},
);
_ = gobject.Object.signals.notify.connect(
surface,
*Self,
propSurfaceFocused,
self,
.{ .detail = "focused" },
);
_ = gobject.Object.signals.notify.connect(
surface.as(gtk.Widget),
*Self,
propSurfaceParent,
self,
.{ .detail = "parent" },
);
}
}
//---------------------------------------------------------------
// Properties
/// Returns true if this split tree needs confirmation before quitting based
/// on the various Ghostty configurations.
pub fn getNeedsConfirmQuit(self: *Self) bool {
const tree = self.getTree() orelse return false;
var it = tree.iterator();
while (it.next()) |entry| {
if (entry.view.core()) |core| {
if (core.needsConfirmQuit()) {
return true;
}
}
}
return false;
}
/// Get the currently active surface. See the "active-surface" property.
/// This does not ref the value.
pub fn getActiveSurface(self: *Self) ?*Surface {
const tree = self.getTree() orelse return null;
const handle = self.getActiveSurfaceHandle() orelse return null;
return tree.nodes[handle.idx()].leaf;
}
fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle {
const tree = self.getTree() orelse return null;
var it = tree.iterator();
while (it.next()) |entry| {
if (entry.view.getFocused()) return entry.handle;
}
// If none are currently focused, the most previously focused
// surface (if it exists) is our active surface. This lets things
// like apprt actions and bell ringing continue to work in the
// background.
if (self.private().last_focused.get()) |v| {
defer v.unref();
// We need to find the handle of the last focused surface.
it = tree.iterator();
while (it.next()) |entry| {
if (entry.view == v) return entry.handle;
}
}
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;
}
/// Returns whether any of the surfaces in the tree have a parent.
/// This is important because we can only rebuild the widget tree
/// when every surface has no parent.
fn getTreeHasParents(self: *Self) bool {
const tree: *const Surface.Tree = self.getTree() orelse &.empty;
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
if (surface.as(gtk.Widget).getParent() != null) return true;
}
return false;
}
pub fn getHasSurfaces(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return !tree.isEmpty();
}
pub fn getIsZoomed(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return tree.zoomed != null;
}
/// Get the tree data model that we're showing in this widget. This
/// does not clone the tree.
pub fn getTree(self: *Self) ?*Surface.Tree {
return self.private().tree;
}
/// Set the tree data model that we're showing in this widget. This
/// will clone the given tree.
pub fn setTree(self: *Self, tree_: ?*const Surface.Tree) void {
const priv = self.private();
// We always normalize our tree parameter so that empty trees
// become null so that we don't have to deal with callers being
// confused about that.
const tree: ?*const Surface.Tree = tree: {
const tree = tree_ orelse break :tree null;
if (tree.isEmpty()) break :tree null;
break :tree tree;
};
// Emit the signal so that handlers can witness both the before and
// after values of the tree.
signals.changed.impl.emit(
self,
null,
.{ priv.tree, tree },
null,
);
if (priv.tree) |old_tree| {
self.disconnectSurfaceHandlers();
ext.boxedFree(Surface.Tree, old_tree);
priv.tree = null;
}
if (tree) |new_tree| {
assert(priv.tree == null);
assert(!new_tree.isEmpty());
priv.tree = ext.boxedCopy(Surface.Tree, new_tree);
self.connectSurfaceHandlers();
}
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
}
fn getTreeValue(self: *Self, value: *gobject.Value) void {
gobject.ext.Value.set(
value,
self.private().tree,
);
}
fn setTreeValue(self: *Self, value: *const gobject.Value) void {
self.setTree(gobject.ext.Value.get(
value,
?*Surface.Tree,
));
}
//---------------------------------------------------------------
// Virtual methods
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", .{});
}
priv.rebuild_source = 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 {
const priv = self.private();
if (priv.tree) |tree| {
ext.boxedFree(Surface.Tree, tree);
priv.tree = null;
}
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Signal handlers
pub fn actionNewSplit(
_: *gio.SimpleAction,
args_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const args = args_ orelse {
log.warn("split-tree.new-split called without a parameter", .{});
return;
};
var dir: ?[*:0]const u8 = null;
args.get("&s", &dir);
const direction = std.meta.stringToEnum(
Surface.Tree.Split.Direction,
std.mem.span(dir) orelse return,
) orelse {
// Need to be defensive here since actions can be triggered externally.
log.warn("invalid split direction for split-tree.new-split: {s}", .{dir.?});
return;
};
self.newSplit(
direction,
self.getActiveSurface(),
) catch |err| {
log.warn("new split failed error={}", .{err});
};
}
pub fn actionEqualize(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = parameter_;
const old_tree = self.getTree() orelse return;
var new_tree = old_tree.equalize(Application.default().allocator()) catch |err| {
log.warn("unable to equalize tree: {}", .{err});
return;
};
defer new_tree.deinit();
self.setTree(&new_tree);
}
pub fn actionZoom(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const tree = self.getTree() orelse return;
if (tree.zoomed != null) {
tree.zoomed = null;
} else {
const active = self.getActiveSurfaceHandle() orelse return;
if (tree.zoomed == active) return;
tree.zoom(active);
}
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
}
fn surfaceCloseRequest(
surface: *Surface,
self: *Self,
) callconv(.c) void {
const core = surface.core() orelse return;
// Reset our pending close state
const priv = self.private();
priv.pending_close = null;
// Find the surface in the tree to verify this is valid and
// set our pending close handle.
priv.pending_close = handle: {
const tree = self.getTree() orelse return;
var it = tree.iterator();
while (it.next()) |entry| {
if (entry.view == surface) {
break :handle entry.handle;
}
}
return;
};
// If we don't need to confirm then just close immediately.
if (!core.needsConfirmQuit()) {
closeConfirmationClose(
null,
self,
);
return;
}
// Show a confirmation dialog
const dialog: *CloseConfirmationDialog = .new(.surface);
_ = CloseConfirmationDialog.signals.@"close-request".connect(
dialog,
*Self,
closeConfirmationClose,
self,
.{},
);
dialog.present(self.as(gtk.Widget));
}
fn closeConfirmationClose(
_: ?*CloseConfirmationDialog,
self: *Self,
) callconv(.c) void {
// Get the handle we're closing
const priv = self.private();
const handle = priv.pending_close orelse return;
priv.pending_close = null;
// Figure out our next focus target. The next focus target is
// always the "previous" surface unless we're the leftmost then
// its the next.
const old_tree = self.getTree() orelse return;
const next_focus: ?*Surface = next_focus: {
const alloc = Application.default().allocator();
const next_handle: Surface.Tree.Node.Handle =
(old_tree.goto(alloc, handle, .previous) catch null) orelse
(old_tree.goto(alloc, handle, .next) catch null) orelse
break :next_focus null;
if (next_handle == handle) break :next_focus null;
// Note: we don't need to ref this or anything because its
// guaranteed to remain in the new tree since its not part
// of the handle we're removing.
break :next_focus old_tree.nodes[next_handle.idx()].leaf;
};
// Remove it from the tree.
var new_tree = old_tree.remove(
Application.default().allocator(),
handle,
) catch |err| {
log.warn("unable to remove surface from tree: {}", .{err});
return;
};
defer new_tree.deinit();
self.setTree(&new_tree);
// Grab focus. We have to set this on the "last focused" because our
// focus will be set when the tree is redrawn.
if (next_focus) |v| priv.last_focused.set(v);
}
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);
// Our active surface probably changed
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
fn propSurfaceParent(
_: *gtk.Widget,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// If we're not waiting to rebuild then ignore this.
if (!priv.rebuild_pending) return;
// If any parents still exist in our tree then don't do anything.
if (self.getTreeHasParents()) return;
// Schedule the rebuild. Note, I tried to do this immediately (not
// on an idle tick) and it didn't work and had obvious rendering
// glitches. Something to look into in the future.
assert(priv.rebuild_source == null);
priv.rebuild_pending = false;
priv.rebuild_source = glib.idleAdd(onRebuild, self);
}
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
// If we were planning a rebuild, 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;
}
// We need to wait for all our previous surfaces to lose their
// parent before adding them to a new one. I'm not sure if its a GTK
// bug, but manually forcing an unparent of all prior surfaces AND
// adding them to a new parent in the same tick causes the GLArea
// to break (it seems). I didn't investigate too deeply.
//
// Note, we also can't just defer to an idle tick (via idleAdd) because
// sometimes it takes more than one tick for all our surfaces to
// lose their parent.
//
// To work around this issue, if we have any surfaces that have
// a parent, we set the build pending flag and wait for the tree
// to be fully parent-free before building.
priv.rebuild_pending = self.getTreeHasParents();
// Reset our prior bin. This will force all prior surfaces to
// unparent... eventually.
priv.tree_bin.setChild(null);
// If none of the surfaces we plan on drawing require an unparent
// then we can setup our tree immediately. Otherwise, it'll happen
// via the `propSurfaceParent` callback.
if (!priv.rebuild_pending and priv.rebuild_source == null) {
priv.rebuild_source = glib.idleAdd(
onRebuild,
self,
);
}
// Dependent properties
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
}
fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
// Always mark our rebuild source as null since we're done.
const priv = self.private();
priv.rebuild_source = null;
// Prior to rebuilding the tree, our surface tree must be
// comprised of fully orphaned surfaces.
assert(!self.getTreeHasParents());
// Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(
tree,
tree.zoomed orelse .root,
));
}
// 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();
}
// Our active surface may have changed
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
return 0;
}
/// Builds the widget tree associated with a surface split tree.
///
/// The final returned widget is expected to be a floating reference,
/// ready to be attached to a parent widget.
fn buildTree(
self: *Self,
tree: *const Surface.Tree,
current: Surface.Tree.Node.Handle,
) *gtk.Widget {
return switch (tree.nodes[current.idx()]) {
.leaf => |v| v.as(gtk.Widget),
.split => |s| SplitTreeSplit.new(
current,
&s,
self.buildTree(tree, s.left),
self.buildTree(tree, s.right),
).as(gtk.Widget),
};
}
//---------------------------------------------------------------
// Class
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 {
gobject.ext.ensureType(Surface);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "split-tree",
}),
);
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.@"has-surfaces".impl,
properties.@"is-zoomed".impl,
properties.tree.impl,
});
// Bindings
class.bindTemplateChildPrivate("tree_bin", .{});
// Template Callbacks
class.bindTemplateCallback("notify_tree", &propTree);
// Signals
signals.changed.impl.register(.{});
// 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;
};
};
/// This is an internal-only widget that represents a split in the
/// split tree. This is a wrapper around gtk.Paned that allows us to handle
/// ratio (0 to 1) based positioning of the split, and also allows us to
/// write back the updated ratio to the split tree when the user manually
/// adjusts the split position.
///
/// Since this is internal, it expects to be nested within a SplitTree and
/// will use `getAncestor` to find the SplitTree it belongs to.
///
/// This is an _immutable_ widget. It isn't meant to be updated after
/// creation. As such, there are no properties or APIs to change the split,
/// access the paned, etc.
const SplitTreeSplit = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttySplitTreeSplit",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
const Private = struct {
/// The handle of the node in the tree that this split represents.
/// Assumed to be correct.
handle: Surface.Tree.Node.Handle,
/// Source to handle repositioning the split when properties change.
idle: ?c_uint = null,
// Template bindings
paned: *gtk.Paned,
pub var offset: c_int = 0;
};
/// Create a new split.
///
/// The reason we don't use GObject properties here is because this is
/// an immutable widget and we don't want to deal with the overhead of
/// all the boilerplate for properties, signals, bindings, etc.
pub fn new(
handle: Surface.Tree.Node.Handle,
split: *const Surface.Tree.Split,
start_child: *gtk.Widget,
end_child: *gtk.Widget,
) *Self {
const self = gobject.ext.newInstance(Self, .{});
const priv = self.private();
priv.handle = handle;
// Setup our paned fields
const paned = priv.paned;
paned.setStartChild(start_child);
paned.setEndChild(end_child);
paned.as(gtk.Orientable).setOrientation(switch (split.layout) {
.horizontal => .horizontal,
.vertical => .vertical,
});
// Signals and so on are setup in the template.
return self;
}
fn init(self: *Self, _: *Class) callconv(.c) void {
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,
);
}
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;
// Get our split. This is the most dangerous part of this entire
// widget. We assume that this widget is always a child of a
// SplitTree, we assume that our handle is valid, and we assume
// the handle is always a split node.
const split_tree = ext.getAncestor(
SplitTree,
self.as(gtk.Widget),
) orelse return 0;
const tree = split_tree.getTree() orelse return 0;
const split: *const Surface.Tree.Split = &tree.nodes[priv.handle.idx()].split;
// Current, min, and max positions as pixels.
const pos = paned.getPosition();
const min = min: {
var val = gobject.ext.Value.new(c_int);
defer val.unset();
gobject.Object.getProperty(
paned.as(gobject.Object),
"min-position",
&val,
);
break :min gobject.ext.Value.get(&val, c_int);
};
const max = max: {
var val = gobject.ext.Value.new(c_int);
defer val.unset();
gobject.Object.getProperty(
paned.as(gobject.Object),
"max-position",
&val,
);
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.
assert(min == 0);
// If our max is zero then we can't do any math. I don't know
// if this is possible but I suspect it can be if you make a nested
// split completely minimized.
if (max == 0) return 0;
// Determine our current ratio.
const current_ratio: f64 = ratio: {
const pos_f64: f64 = @floatFromInt(pos);
const max_f64: f64 = @floatFromInt(max);
break :ratio pos_f64 / max_f64;
};
const desired_ratio: f64 = @floatCast(split.ratio);
// If our ratio is close enough to our desired ratio, then
// we ignore the update. This is to avoid constant split updates
// for lossy floating point math.
if (std.math.approxEqAbs(
f64,
current_ratio,
desired_ratio,
0.001,
)) {
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) {
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;
}
// 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();
}
fn propMinPosition(
_: *gtk.Paned,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.refresh();
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.idle) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove idle source", .{});
}
priv.idle = 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),
);
}
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 = "split-tree-split",
}),
);
// Bindings
class.bindTemplateChildPrivate("paned", .{});
// Template Callbacks
class.bindTemplateCallback("notify_max_position", &propMaxPosition);
class.bindTemplateCallback("notify_min_position", &propMinPosition);
class.bindTemplateCallback("notify_position", &propPosition);
// 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;
};
};