//! Focus Groups //! //! Manages focus navigation within groups of widgets. //! Supports tab order, wrapping, and nested groups. const std = @import("std"); const Input = @import("input.zig"); /// Maximum widgets per group const MAX_GROUP_SIZE = 64; /// Maximum number of groups const MAX_GROUPS = 32; /// Focus direction pub const Direction = enum { next, previous, up, down, left, right, }; /// A focus group containing related widgets pub const FocusGroup = struct { /// Group identifier id: u64, /// Widget IDs in this group (in tab order) widgets: [MAX_GROUP_SIZE]u64 = undefined, /// Number of widgets count: usize = 0, /// Currently focused widget index focused_index: ?usize = null, /// Wrap focus at boundaries wrap: bool = true, /// Is this group active active: bool = true, /// Parent group (for nested focus) parent_group: ?u64 = null, const Self = @This(); /// Create a new focus group pub fn init(id: u64) Self { return .{ .id = id }; } /// Add a widget to the group pub fn add(self: *Self, widget_id: u64) void { if (self.count >= MAX_GROUP_SIZE) return; self.widgets[self.count] = widget_id; self.count += 1; } /// Remove a widget from the group pub fn remove(self: *Self, widget_id: u64) void { var i: usize = 0; while (i < self.count) { if (self.widgets[i] == widget_id) { // Shift remaining widgets var j = i; while (j < self.count - 1) : (j += 1) { self.widgets[j] = self.widgets[j + 1]; } self.count -= 1; // Adjust focused index if (self.focused_index) |idx| { if (idx == i) { self.focused_index = if (self.count > 0) @min(idx, self.count - 1) else null; } else if (idx > i) { self.focused_index = idx - 1; } } return; } i += 1; } } /// Get the currently focused widget ID pub fn getFocused(self: Self) ?u64 { if (self.focused_index) |idx| { if (idx < self.count) { return self.widgets[idx]; } } return null; } /// Set focus to a specific widget pub fn setFocus(self: *Self, widget_id: u64) bool { for (self.widgets[0..self.count], 0..) |id, i| { if (id == widget_id) { self.focused_index = i; return true; } } return false; } /// Focus the next widget pub fn focusNext(self: *Self) ?u64 { if (self.count == 0) return null; if (self.focused_index) |idx| { if (idx + 1 < self.count) { self.focused_index = idx + 1; } else if (self.wrap) { self.focused_index = 0; } } else { self.focused_index = 0; } return self.getFocused(); } /// Focus the previous widget pub fn focusPrevious(self: *Self) ?u64 { if (self.count == 0) return null; if (self.focused_index) |idx| { if (idx > 0) { self.focused_index = idx - 1; } else if (self.wrap) { self.focused_index = self.count - 1; } } else { self.focused_index = self.count - 1; } return self.getFocused(); } /// Focus first widget pub fn focusFirst(self: *Self) ?u64 { if (self.count == 0) return null; self.focused_index = 0; return self.getFocused(); } /// Focus last widget pub fn focusLast(self: *Self) ?u64 { if (self.count == 0) return null; self.focused_index = self.count - 1; return self.getFocused(); } /// Clear focus pub fn clearFocus(self: *Self) void { self.focused_index = null; } /// Check if a widget has focus pub fn hasFocus(self: Self, widget_id: u64) bool { if (self.getFocused()) |focused| { return focused == widget_id; } return false; } /// Get index of a widget pub fn indexOf(self: Self, widget_id: u64) ?usize { for (self.widgets[0..self.count], 0..) |id, i| { if (id == widget_id) { return i; } } return null; } }; /// Focus Group Manager - manages multiple focus groups pub const FocusGroupManager = struct { groups: [MAX_GROUPS]FocusGroup = undefined, group_count: usize = 0, active_group: ?u64 = null, const Self = @This(); /// Initialize the manager pub fn init() Self { return .{}; } /// Create a new group pub fn createGroup(self: *Self, id: u64) *FocusGroup { if (self.group_count >= MAX_GROUPS) { // Return first group as fallback return &self.groups[0]; } self.groups[self.group_count] = FocusGroup.init(id); const group = &self.groups[self.group_count]; self.group_count += 1; // Set as active if first group if (self.active_group == null) { self.active_group = id; } return group; } /// Get a group by ID pub fn getGroup(self: *Self, id: u64) ?*FocusGroup { for (self.groups[0..self.group_count]) |*group| { if (group.id == id) { return group; } } return null; } /// Remove a group pub fn removeGroup(self: *Self, id: u64) void { var i: usize = 0; while (i < self.group_count) { if (self.groups[i].id == id) { var j = i; while (j < self.group_count - 1) : (j += 1) { self.groups[j] = self.groups[j + 1]; } self.group_count -= 1; if (self.active_group == id) { self.active_group = if (self.group_count > 0) self.groups[0].id else null; } return; } i += 1; } } /// Set the active group pub fn setActiveGroup(self: *Self, id: u64) void { if (self.getGroup(id) != null) { self.active_group = id; } } /// Get the active group pub fn getActiveGroup(self: *Self) ?*FocusGroup { if (self.active_group) |id| { return self.getGroup(id); } return null; } /// Handle focus navigation input pub fn handleInput(self: *Self, input: *const Input.InputState) ?u64 { const group = self.getActiveGroup() orelse return null; if (input.keyPressed(.tab)) { if (input.keyDown(.left_shift) or input.keyDown(.right_shift)) { return group.focusPrevious(); } else { return group.focusNext(); } } return null; } /// Get currently focused widget across all groups pub fn getGlobalFocus(self: Self) ?u64 { if (self.active_group) |active_id| { for (self.groups[0..self.group_count]) |group| { if (group.id == active_id) { return group.getFocused(); } } } return null; } /// Focus next group pub fn focusNextGroup(self: *Self) void { if (self.group_count <= 1) return; if (self.active_group) |active_id| { for (self.groups[0..self.group_count], 0..) |group, i| { if (group.id == active_id) { const next_idx = (i + 1) % self.group_count; self.active_group = self.groups[next_idx].id; return; } } } } /// Focus previous group pub fn focusPreviousGroup(self: *Self) void { if (self.group_count <= 1) return; if (self.active_group) |active_id| { for (self.groups[0..self.group_count], 0..) |group, i| { if (group.id == active_id) { const prev_idx = if (i == 0) self.group_count - 1 else i - 1; self.active_group = self.groups[prev_idx].id; return; } } } } }; // ============================================================================= // Tests // ============================================================================= test "FocusGroup init" { const group = FocusGroup.init(1); try std.testing.expectEqual(@as(u64, 1), group.id); try std.testing.expectEqual(@as(usize, 0), group.count); } test "FocusGroup add and remove" { var group = FocusGroup.init(1); group.add(100); group.add(200); group.add(300); try std.testing.expectEqual(@as(usize, 3), group.count); group.remove(200); try std.testing.expectEqual(@as(usize, 2), group.count); try std.testing.expectEqual(@as(u64, 100), group.widgets[0]); try std.testing.expectEqual(@as(u64, 300), group.widgets[1]); } test "FocusGroup navigation" { var group = FocusGroup.init(1); group.add(100); group.add(200); group.add(300); // Focus first try std.testing.expectEqual(@as(?u64, 100), group.focusFirst()); try std.testing.expectEqual(@as(?usize, 0), group.focused_index); // Focus next try std.testing.expectEqual(@as(?u64, 200), group.focusNext()); try std.testing.expectEqual(@as(?u64, 300), group.focusNext()); // Wrap around try std.testing.expectEqual(@as(?u64, 100), group.focusNext()); // Focus previous try std.testing.expectEqual(@as(?u64, 300), group.focusPrevious()); } test "FocusGroup setFocus" { var group = FocusGroup.init(1); group.add(100); group.add(200); group.add(300); try std.testing.expect(group.setFocus(200)); try std.testing.expectEqual(@as(?u64, 200), group.getFocused()); try std.testing.expect(!group.setFocus(999)); // Non-existent } test "FocusGroup hasFocus" { var group = FocusGroup.init(1); group.add(100); group.add(200); _ = group.focusFirst(); try std.testing.expect(group.hasFocus(100)); try std.testing.expect(!group.hasFocus(200)); } test "FocusGroupManager create groups" { var manager = FocusGroupManager.init(); const group1 = manager.createGroup(1); const group2 = manager.createGroup(2); try std.testing.expectEqual(@as(usize, 2), manager.group_count); try std.testing.expectEqual(@as(u64, 1), group1.id); try std.testing.expectEqual(@as(u64, 2), group2.id); // First group should be active try std.testing.expectEqual(@as(?u64, 1), manager.active_group); } test "FocusGroupManager get and remove" { var manager = FocusGroupManager.init(); _ = manager.createGroup(1); _ = manager.createGroup(2); const group = manager.getGroup(2); try std.testing.expect(group != null); try std.testing.expectEqual(@as(u64, 2), group.?.id); manager.removeGroup(1); try std.testing.expectEqual(@as(usize, 1), manager.group_count); try std.testing.expect(manager.getGroup(1) == null); } test "FocusGroupManager active group" { var manager = FocusGroupManager.init(); _ = manager.createGroup(1); _ = manager.createGroup(2); manager.setActiveGroup(2); try std.testing.expectEqual(@as(?u64, 2), manager.active_group); manager.focusNextGroup(); try std.testing.expectEqual(@as(?u64, 1), manager.active_group); }