apprt/gtk-ng: spatial navigation

pull/8210/head
Mitchell Hashimoto 2025-08-11 10:17:08 -07:00
parent 70d48d03a5
commit 5a01877c77
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 89 additions and 7 deletions

View File

@ -1675,7 +1675,10 @@ const Action = struct {
return tree.goto(switch (to) { return tree.goto(switch (to) {
.previous => .previous_wrapped, .previous => .previous_wrapped,
.next => .next_wrapped, .next => .next_wrapped,
else => @panic("TODO"), .up => .{ .spatial = .up },
.down => .{ .spatial = .down },
.left => .{ .spatial = .left },
.right => .{ .spatial = .right },
}); });
}, },
} }

View File

@ -263,7 +263,18 @@ pub const SplitTree = extern struct {
pub fn goto(self: *Self, to: Surface.Tree.Goto) bool { pub fn goto(self: *Self, to: Surface.Tree.Goto) bool {
const tree = self.getTree() orelse return false; const tree = self.getTree() orelse return false;
const active = self.getActiveSurfaceHandle() 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 we aren't changing targets then we did nothing.
if (active == target) return false; if (active == target) return false;
@ -594,8 +605,10 @@ pub const SplitTree = extern struct {
// its the next. // its the next.
const old_tree = self.getTree() orelse return; const old_tree = self.getTree() orelse return;
const next_focus: ?*Surface = next_focus: { const next_focus: ?*Surface = next_focus: {
const next_handle = old_tree.goto(handle, .previous) orelse const alloc = Application.default().allocator();
old_tree.goto(handle, .next) orelse 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; break :next_focus null;
if (next_handle == handle) break :next_focus null; if (next_handle == handle) break :next_focus null;

View File

@ -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 view, null if we're the first view.
previous, previous,
@ -191,20 +191,34 @@ pub fn SplitTree(comptime V: type) type {
/// Next view, but wrapped around to the first view. May return /// Next view, but wrapped around to the first view. May return
/// the same view if this is the last view. /// the same view if this is the last view.
next_wrapped, 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 /// Goto a view from a certain point in the split tree. Returns null
/// if the direction results in no visitable view. /// if the direction results in no visitable view.
///
/// Allocator is only used for temporary state for spatial navigation.
pub fn goto( pub fn goto(
self: *const Self, self: *const Self,
alloc: Allocator,
from: Node.Handle, from: Node.Handle,
to: Goto, to: Goto,
) ?Node.Handle { ) Allocator.Error!?Node.Handle {
return switch (to) { return switch (to) {
.previous => self.previous(from), .previous => self.previous(from),
.next => self.next(from), .next => self.next(from),
.previous_wrapped => self.previous(from) orelse self.deepest(.right, 0), .previous_wrapped => self.previous(from) orelse self.deepest(.right, 0),
.next_wrapped => self.next(from) orelse self.deepest(.left, 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 empty: Spatial = .{ .slots = &.{} };
pub const Direction = enum { left, right, down, up };
const Slot = struct { const Slot = struct {
x: f16, x: f16,
y: f16, y: f16,
width: f16, width: f16,
height: 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); alloc.free(self.slots);
self.* = undefined; 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 /// Spatial representation of the split tree. This can be used to