diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 129147ece..3483fd279 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -1675,7 +1675,10 @@ const Action = struct { return tree.goto(switch (to) { .previous => .previous_wrapped, .next => .next_wrapped, - else => @panic("TODO"), + .up => .{ .spatial = .up }, + .down => .{ .spatial = .down }, + .left => .{ .spatial = .left }, + .right => .{ .spatial = .right }, }); }, } diff --git a/src/apprt/gtk-ng/class/split_tree.zig b/src/apprt/gtk-ng/class/split_tree.zig index f951d3d0a..7364c0ade 100644 --- a/src/apprt/gtk-ng/class/split_tree.zig +++ b/src/apprt/gtk-ng/class/split_tree.zig @@ -263,7 +263,18 @@ pub const SplitTree = extern struct { 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 = tree.goto(active, to) 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; @@ -594,8 +605,10 @@ pub const SplitTree = extern struct { // its the next. const old_tree = self.getTree() orelse return; const next_focus: ?*Surface = next_focus: { - const next_handle = old_tree.goto(handle, .previous) orelse - old_tree.goto(handle, .next) orelse + 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; diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig index 26da9d89c..6af7e45d1 100644 --- a/src/datastruct/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -177,7 +177,7 @@ pub fn SplitTree(comptime V: type) type { } }; - pub const Goto = enum { + pub const Goto = union(enum) { /// Previous view, null if we're the first view. previous, @@ -191,20 +191,34 @@ pub fn SplitTree(comptime V: type) type { /// Next view, but wrapped around to the first view. May return /// the same view if this is the last view. next_wrapped, + + /// A spatial direction. "Spatial" means that the direction is + /// based on the nearest surface in the given direction visually + /// as the surfaces are laid out on a 2D grid. + spatial: Spatial.Direction, }; /// Goto a view from a certain point in the split tree. Returns null /// if the direction results in no visitable view. + /// + /// Allocator is only used for temporary state for spatial navigation. pub fn goto( self: *const Self, + alloc: Allocator, from: Node.Handle, to: Goto, - ) ?Node.Handle { + ) Allocator.Error!?Node.Handle { return switch (to) { .previous => self.previous(from), .next => self.next(from), .previous_wrapped => self.previous(from) orelse self.deepest(.right, 0), .next_wrapped => self.next(from) orelse self.deepest(.left, 0), + .spatial => |d| spatial: { + // Get our spatial representation. + var sp = try self.spatial(alloc); + defer sp.deinit(alloc); + break :spatial sp.nearestLeaf(from, d); + }, }; } @@ -586,17 +600,69 @@ pub fn SplitTree(comptime V: type) type { pub const empty: Spatial = .{ .slots = &.{} }; + pub const Direction = enum { left, right, down, up }; + const Slot = struct { x: f16, y: f16, width: f16, height: f16, + + fn maxX(self: *const Slot) f16 { + return self.x + self.width; + } + + fn maxY(self: *const Slot) f16 { + return self.y + self.height; + } }; - pub fn deinit(self: *const Spatial, alloc: Allocator) void { + pub fn deinit(self: *Spatial, alloc: Allocator) void { alloc.free(self.slots); self.* = undefined; } + + /// Returns the nearest leaf node (view) in the given direction. + pub fn nearestLeaf( + self: *const Spatial, + from: Node.Handle, + direction: Direction, + ) ?Node.Handle { + const target = self.slots[from]; + + var nearest: ?struct { + handle: Node.Handle, + distance: f16, + } = null; + for (self.slots, 0..) |slot, handle| { + // Never match ourself + if (handle == from) continue; + + // Ensure it is in the proper direction + if (!switch (direction) { + .left => slot.maxX() <= target.maxX(), + .right => slot.x >= target.maxX(), + .up => slot.maxY() <= target.y, + .down => slot.y >= target.maxY(), + }) continue; + + // Track our distance + const dx = slot.x - target.x; + const dy = slot.y - target.y; + const distance = @sqrt(dx * dx + dy * dy); + + // If we have a nearest it must be closer. + if (nearest) |n| { + if (distance >= n.distance) continue; + } + nearest = .{ + .handle = @intCast(handle), + .distance = distance, + }; + } + + return if (nearest) |n| n.handle else null; + } }; /// Spatial representation of the split tree. This can be used to