//! Focus Management - Track and navigate widget focus //! //! Manages which widget has keyboard focus and provides //! Tab/Shift+Tab navigation between focusable widgets. const std = @import("std"); const Input = @import("../core/input.zig"); /// Maximum number of focusable widgets per frame pub const MAX_FOCUSABLES = 64; /// Focus manager state pub const FocusManager = struct { /// Currently focused widget ID focused_id: ?u32 = null, /// List of focusable widget IDs this frame (in order) focusables: [MAX_FOCUSABLES]u32 = undefined, focusable_count: usize = 0, /// Widget ID to focus next frame (from keyboard nav) pending_focus: ?u32 = null, /// Whether Tab was pressed this frame tab_pressed: bool = false, shift_tab_pressed: bool = false, const Self = @This(); /// Reset for new frame pub fn beginFrame(self: *Self) void { self.focusable_count = 0; self.tab_pressed = false; self.shift_tab_pressed = false; // Apply pending focus if (self.pending_focus) |id| { self.focused_id = id; self.pending_focus = null; } } /// Process keyboard input for focus navigation pub fn processInput(self: *Self, input: *const Input.InputState, key_events: []const Input.KeyEvent) void { _ = input; for (key_events) |event| { if (event.key == .tab and event.pressed) { if (event.modifiers.shift) { self.shift_tab_pressed = true; } else { self.tab_pressed = true; } } } } /// Register a widget as focusable pub fn registerFocusable(self: *Self, id: u32) void { if (self.focusable_count >= MAX_FOCUSABLES) return; self.focusables[self.focusable_count] = id; self.focusable_count += 1; } /// Check if a widget has focus pub fn hasFocus(self: Self, id: u32) bool { return self.focused_id == id; } /// Request focus for a widget pub fn requestFocus(self: *Self, id: u32) void { self.focused_id = id; } /// Clear focus pub fn clearFocus(self: *Self) void { self.focused_id = null; } /// End of frame: process Tab navigation pub fn endFrame(self: *Self) void { if (self.focusable_count == 0) return; if (self.tab_pressed) { self.focusNext(); } else if (self.shift_tab_pressed) { self.focusPrev(); } } /// Focus next widget in order fn focusNext(self: *Self) void { if (self.focusable_count == 0) return; if (self.focused_id) |current| { // Find current index for (self.focusables[0..self.focusable_count], 0..) |id, i| { if (id == current) { // Focus next (wrap around) const next_idx = (i + 1) % self.focusable_count; self.pending_focus = self.focusables[next_idx]; return; } } } // No current focus, focus first self.pending_focus = self.focusables[0]; } /// Focus previous widget in order fn focusPrev(self: *Self) void { if (self.focusable_count == 0) return; if (self.focused_id) |current| { // Find current index for (self.focusables[0..self.focusable_count], 0..) |id, i| { if (id == current) { // Focus previous (wrap around) const prev_idx = if (i == 0) self.focusable_count - 1 else i - 1; self.pending_focus = self.focusables[prev_idx]; return; } } } // No current focus, focus last self.pending_focus = self.focusables[self.focusable_count - 1]; } /// Focus specific index pub fn focusIndex(self: *Self, idx: usize) void { if (idx < self.focusable_count) { self.pending_focus = self.focusables[idx]; } } /// Get the index of the focused widget pub fn focusedIndex(self: Self) ?usize { if (self.focused_id) |current| { for (self.focusables[0..self.focusable_count], 0..) |id, i| { if (id == current) { return i; } } } return null; } }; /// Focus ring - circular focus navigation helper pub const FocusRing = struct { ids: [MAX_FOCUSABLES]u32 = undefined, count: usize = 0, current: usize = 0, const Self = @This(); /// Add a widget ID to the ring pub fn add(self: *Self, id: u32) void { if (self.count >= MAX_FOCUSABLES) return; self.ids[self.count] = id; self.count += 1; } /// Get current focused ID pub fn currentId(self: Self) ?u32 { if (self.count == 0) return null; return self.ids[self.current]; } /// Move to next pub fn next(self: *Self) void { if (self.count == 0) return; self.current = (self.current + 1) % self.count; } /// Move to previous pub fn prev(self: *Self) void { if (self.count == 0) return; self.current = if (self.current == 0) self.count - 1 else self.current - 1; } /// Check if widget has focus pub fn isFocused(self: Self, id: u32) bool { if (self.count == 0) return false; return self.ids[self.current] == id; } /// Focus specific widget by ID pub fn focusId(self: *Self, id: u32) bool { for (self.ids[0..self.count], 0..) |widget_id, i| { if (widget_id == id) { self.current = i; return true; } } return false; } /// Reset the ring pub fn reset(self: *Self) void { self.count = 0; self.current = 0; } }; // ============================================================================= // Tests // ============================================================================= test "FocusManager navigation" { var fm = FocusManager{}; fm.beginFrame(); fm.registerFocusable(100); fm.registerFocusable(200); fm.registerFocusable(300); // No focus initially try std.testing.expectEqual(@as(?u32, null), fm.focused_id); // Tab to first fm.tab_pressed = true; fm.endFrame(); fm.beginFrame(); try std.testing.expectEqual(@as(?u32, 100), fm.focused_id); // Register again for new frame fm.registerFocusable(100); fm.registerFocusable(200); fm.registerFocusable(300); // Tab to second fm.tab_pressed = true; fm.endFrame(); fm.beginFrame(); try std.testing.expectEqual(@as(?u32, 200), fm.focused_id); } test "FocusRing" { var ring = FocusRing{}; ring.add(10); ring.add(20); ring.add(30); try std.testing.expectEqual(@as(?u32, 10), ring.currentId()); try std.testing.expect(ring.isFocused(10)); ring.next(); try std.testing.expectEqual(@as(?u32, 20), ring.currentId()); ring.prev(); try std.testing.expectEqual(@as(?u32, 10), ring.currentId()); ring.prev(); // Wrap to end try std.testing.expectEqual(@as(?u32, 30), ring.currentId()); } test "FocusRing focusId" { var ring = FocusRing{}; ring.add(100); ring.add(200); ring.add(300); const found = ring.focusId(200); try std.testing.expect(found); try std.testing.expectEqual(@as(?u32, 200), ring.currentId()); }