zcatgui/src/widgets/focus.zig
reugenio 1ee10b8e17 feat: Añadir archivos nuevos de widgets y focus
Archivos que estaban localmente pero no en el repo:
- src/core/focus_group.zig (416 líneas)
- src/widgets/focus.zig (272 líneas)
- src/widgets/progress.zig (806 líneas)
- src/widgets/table.zig (1592 líneas)
- src/widgets/textarea.zig (871 líneas)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 22:55:41 +01:00

272 lines
7.4 KiB
Zig

//! 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());
}