split_tree: track zoomed state

pull/8217/head
Mitchell Hashimoto 2025-08-12 12:36:21 -07:00
parent dfabb8aa4f
commit 145d1c1739
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 276 additions and 22 deletions

View File

@ -615,12 +615,11 @@ pub const Application = extern struct {
.toggle_tab_overview => return Action.toggleTabOverview(target),
.toggle_window_decorations => return Action.toggleWindowDecorations(target),
.toggle_command_palette => return Action.toggleCommandPalette(target),
.toggle_split_zoom => return Action.toggleSplitZoom(target),
// Unimplemented but todo on gtk-ng branch
.prompt_title,
.inspector,
// TODO: splits
.toggle_split_zoom,
=> {
log.warn("unimplemented action={}", .{action});
return false;
@ -2121,6 +2120,21 @@ const Action = struct {
return true;
}
pub fn toggleSplitZoom(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("toggle_split_zoom to app is unexpected", .{});
return false;
},
.surface => |core| {
// TODO: pass surface ID when we have that
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0;
},
}
}
fn getQuickTerminalWindow() ?*Window {
// Find a quick terminal window.
const list = gtk.Window.listToplevels();

View File

@ -165,6 +165,7 @@ pub const SplitTree = extern struct {
.{ "new-down", actionNewDown, null },
.{ "equalize", actionEqualize, null },
.{ "zoom", actionZoom, null },
};
// We need to collect our actions into a group since we're just
@ -600,6 +601,23 @@ pub const SplitTree = extern struct {
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,
scope: *const Surface.CloseScope,
@ -797,7 +815,10 @@ pub const SplitTree = extern struct {
// Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(tree, 0));
priv.tree_bin.setChild(self.buildTree(
tree,
tree.zoomed orelse 0,
));
}
// If we have a last focused surface, we need to refocus it, because

View File

@ -56,6 +56,11 @@ pub fn SplitTree(comptime V: type) type {
/// All the nodes in the tree. Node at index 0 is always the root.
nodes: []const Node,
/// The handle of the zoomed node. A "zoomed" node is one that is
/// expected to be made the full size of the split tree. Various
/// operations may unzoom (e.g. resize).
zoomed: ?Node.Handle,
/// An empty tree.
pub const empty: Self = .{
// Arena can be undefined because we have zero allocated nodes.
@ -63,6 +68,7 @@ pub fn SplitTree(comptime V: type) type {
// arena.
.arena = undefined,
.nodes = &.{},
.zoomed = null,
};
pub const Node = union(enum) {
@ -98,6 +104,7 @@ pub fn SplitTree(comptime V: type) type {
return .{
.arena = arena,
.nodes = nodes,
.zoomed = null,
};
}
@ -136,6 +143,7 @@ pub fn SplitTree(comptime V: type) type {
return .{
.arena = arena,
.nodes = nodes,
.zoomed = self.zoomed,
};
}
@ -177,6 +185,13 @@ pub fn SplitTree(comptime V: type) type {
}
};
/// Change the zoomed state to the given node. Assumes the handle
/// is valid.
pub fn zoom(self: *Self, handle: ?Node.Handle) void {
if (handle) |v| assert(v >= 0 and v < self.nodes.len);
self.zoomed = handle;
}
pub const Goto = union(enum) {
/// Previous view, null if we're the first view.
previous,
@ -472,7 +487,12 @@ pub fn SplitTree(comptime V: type) type {
// We need to increase the reference count of all the nodes.
try refNodes(gpa, nodes);
return .{ .arena = arena, .nodes = nodes };
return .{
.arena = arena,
.nodes = nodes,
// Splitting always resets zoom state.
.zoomed = null,
};
}
/// Remove a node from the tree.
@ -499,9 +519,15 @@ pub fn SplitTree(comptime V: type) type {
0,
));
var result: Self = .{
.arena = arena,
.nodes = nodes,
.zoomed = null,
};
// Traverse the tree and copy all our nodes into place.
assert(self.removeNode(
nodes,
&result,
0,
0,
at,
@ -510,27 +536,39 @@ pub fn SplitTree(comptime V: type) type {
// Increase the reference count of all the nodes.
try refNodes(gpa, nodes);
return .{
.arena = arena,
.nodes = nodes,
};
return result;
}
fn removeNode(
self: *Self,
nodes: []Node,
old: *Self,
new: *Self,
new_offset: Node.Handle,
current: Node.Handle,
target: Node.Handle,
) Node.Handle {
assert(current != target);
switch (self.nodes[current]) {
// If we have a zoomed node and this is it then we migrate it.
if (old.zoomed) |v| {
if (v == current) {
assert(new.zoomed == null);
new.zoomed = new_offset;
}
}
// Let's talk about this constCast. Our member are const but
// we actually always own their memory. We don't want consumers
// who directly access the nodes to be able to modify them
// (without nasty stuff like this), but given this is internal
// usage its perfectly fine to modify the node in-place.
const new_nodes: []Node = @constCast(new.nodes);
switch (old.nodes[current]) {
// Leaf is simple, just copy it over. We don't ref anything
// yet because it'd make undo (errdefer) harder. We do that
// all at once later.
.leaf => |view| {
nodes[new_offset] = .{ .leaf = view };
new_nodes[new_offset] = .{ .leaf = view };
return 1;
},
@ -538,35 +576,35 @@ pub fn SplitTree(comptime V: type) type {
// If we're removing one of the split node sides then
// we remove the split node itself as well and only add
// the other (non-removed) side.
if (s.left == target) return self.removeNode(
nodes,
if (s.left == target) return old.removeNode(
new,
new_offset,
s.right,
target,
);
if (s.right == target) return self.removeNode(
nodes,
if (s.right == target) return old.removeNode(
new,
new_offset,
s.left,
target,
);
// Neither side is being directly removed, so we traverse.
const left = self.removeNode(
nodes,
const left = old.removeNode(
new,
new_offset + 1,
s.left,
target,
);
assert(left > 0);
const right = self.removeNode(
nodes,
const right = old.removeNode(
new,
new_offset + 1 + left,
s.right,
target,
);
assert(right > 0);
nodes[new_offset] = .{ .split = .{
new_nodes[new_offset] = .{ .split = .{
.layout = s.layout,
.ratio = s.ratio,
.left = new_offset + 1,
@ -679,6 +717,7 @@ pub fn SplitTree(comptime V: type) type {
return .{
.arena = arena,
.nodes = nodes,
.zoomed = self.zoomed,
};
}
@ -1005,6 +1044,10 @@ pub fn SplitTree(comptime V: type) type {
) !void {
for (0..depth) |_| try writer.writeAll(" ");
if (self.zoomed) |zoomed| if (zoomed == current) {
try writer.writeAll("(zoomed) ");
};
switch (self.nodes[current]) {
.leaf => |v| if (@hasDecl(View, "splitTreeLabel"))
try writer.print("leaf: {s}\n", .{v.splitTreeLabel()})
@ -1970,3 +2013,179 @@ test "SplitTree: clone empty tree" {
);
}
}
test "SplitTree: zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// A | B horizontal
var split = try t1.split(
alloc,
0, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
split.zoom(at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "B")) {
break entry.handle;
}
} else return error.NotFound;
});
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{split});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ (zoomed) leaf: B
\\
);
}
// Clone preserves zoom
var clone = try split.clone(alloc);
defer clone.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{clone});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ (zoomed) leaf: B
\\
);
}
}
test "SplitTree: split resets zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// Zoom A
t1.zoom(at: {
var it = t1.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
});
// A | B horizontal
var split = try t1.split(
alloc,
0, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{split});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ leaf: B
\\
);
}
}
test "SplitTree: remove and zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// A | B horizontal
var split = try t1.split(
alloc,
0, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
split.zoom(at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
});
// Remove A, should unzoom
{
var removed = try split.remove(
alloc,
at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
},
);
defer removed.deinit();
try testing.expect(removed.zoomed == null);
const str = try std.fmt.allocPrint(alloc, "{text}", .{removed});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\leaf: B
\\
);
}
// Remove B, should keep zoom
{
var removed = try split.remove(
alloc,
at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "B")) {
break entry.handle;
}
} else return error.NotFound;
},
);
defer removed.deinit();
const str = try std.fmt.allocPrint(alloc, "{text}", .{removed});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\(zoomed) leaf: A
\\
);
}
}