zcatgui/src/core/input.zig
reugenio 14d717d7f4 🔧 Font completo + key repeat para navegación
- Font bitmap 8x8 completo (ASCII 32-126)
- navKeyPressed() ahora detecta key repeat de SDL2
- Exportar default_font desde render module

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 00:50:44 +01:00

471 lines
12 KiB
Zig

//! Input - Keyboard and mouse input state
//!
//! Tracks the current state of input devices.
//! Updated by the backend each frame.
const std = @import("std");
/// Key codes (subset, extend as needed)
pub const Key = enum(u16) {
// Letters
a,
b,
c,
d,
e,
f,
g,
h,
i,
j,
k,
l,
m,
n,
o,
p,
q,
r,
s,
t,
u,
v,
w,
x,
y,
z,
// Numbers
@"0",
@"1",
@"2",
@"3",
@"4",
@"5",
@"6",
@"7",
@"8",
@"9",
// Function keys
f1,
f2,
f3,
f4,
f5,
f6,
f7,
f8,
f9,
f10,
f11,
f12,
// Navigation
up,
down,
left,
right,
home,
end,
page_up,
page_down,
// Editing
backspace,
delete,
insert,
tab,
enter,
escape,
space,
// Modifiers (as keys)
left_shift,
right_shift,
left_ctrl,
right_ctrl,
left_alt,
right_alt,
// Punctuation
minus,
equals,
left_bracket,
right_bracket,
backslash,
semicolon,
apostrophe,
grave,
comma,
period,
slash,
// Unknown
unknown,
_,
};
/// Key modifiers
pub const KeyModifiers = packed struct {
shift: bool = false,
ctrl: bool = false,
alt: bool = false,
super: bool = false,
pub const none = KeyModifiers{};
};
/// A keyboard event
pub const KeyEvent = struct {
key: Key,
modifiers: KeyModifiers,
/// The character produced (if any)
char: ?u21 = null,
/// True if key was pressed, false if released
pressed: bool,
/// Check if this is a printable character
pub fn isPrintable(self: KeyEvent) bool {
return self.char != null and self.char.? >= 32;
}
/// Get the character as a slice (for convenience)
pub fn charAsSlice(self: KeyEvent, buf: *[4]u8) ?[]const u8 {
if (self.char) |c| {
const len = std.unicode.utf8Encode(c, buf) catch return null;
return buf[0..len];
}
return null;
}
};
/// Mouse buttons
pub const MouseButton = enum {
left,
right,
middle,
x1,
x2,
};
/// A mouse event
pub const MouseEvent = struct {
x: i32,
y: i32,
button: ?MouseButton = null,
pressed: bool = false,
scroll_x: i32 = 0,
scroll_y: i32 = 0,
};
/// Maximum number of keys we track
const MAX_KEYS: usize = 128;
/// Maximum key events per frame
const MAX_KEY_EVENTS: usize = 16;
/// Current input state
pub const InputState = struct {
// Mouse position
mouse_x: i32 = 0,
mouse_y: i32 = 0,
// Mouse buttons (current frame)
mouse_down: [5]bool = .{ false, false, false, false, false },
// Mouse buttons (previous frame, for detecting clicks)
mouse_down_prev: [5]bool = .{ false, false, false, false, false },
// Scroll delta
scroll_x: i32 = 0,
scroll_y: i32 = 0,
// Key modifiers
modifiers: KeyModifiers = .{},
// Text input this frame
text_input: [64]u8 = undefined,
text_input_len: usize = 0,
// Keyboard state (current frame)
keys_down: [MAX_KEYS]bool = [_]bool{false} ** MAX_KEYS,
// Keyboard state (previous frame)
keys_down_prev: [MAX_KEYS]bool = [_]bool{false} ** MAX_KEYS,
// Key events this frame (for widgets that need event-based input)
key_events: [MAX_KEY_EVENTS]KeyEvent = undefined,
key_event_count: usize = 0,
const Self = @This();
/// Initialize input state
pub fn init() Self {
return .{};
}
/// Call at end of frame to prepare for next
pub fn endFrame(self: *Self) void {
self.mouse_down_prev = self.mouse_down;
self.keys_down_prev = self.keys_down;
self.scroll_x = 0;
self.scroll_y = 0;
self.text_input_len = 0;
self.key_event_count = 0;
}
/// Update mouse position
pub fn setMousePos(self: *Self, x: i32, y: i32) void {
self.mouse_x = x;
self.mouse_y = y;
}
/// Update mouse button state
pub fn setMouseButton(self: *Self, button: MouseButton, pressed: bool) void {
self.mouse_down[@intFromEnum(button)] = pressed;
}
/// Add scroll delta
pub fn addScroll(self: *Self, x: i32, y: i32) void {
self.scroll_x += x;
self.scroll_y += y;
}
/// Update key modifiers
pub fn setModifiers(self: *Self, mods: KeyModifiers) void {
self.modifiers = mods;
}
/// Handle a key event from the backend
pub fn handleKeyEvent(self: *Self, event: KeyEvent) void {
// Update key state
const key_idx = @intFromEnum(event.key);
if (key_idx < MAX_KEYS) {
self.keys_down[key_idx] = event.pressed;
}
// Update modifiers
self.modifiers = event.modifiers;
// Store event for widgets that need event-based input
if (self.key_event_count < MAX_KEY_EVENTS) {
self.key_events[self.key_event_count] = event;
self.key_event_count += 1;
}
// If it's a printable character being pressed, add to text input
if (event.pressed) {
if (event.char) |c| {
if (c >= 32 and c < 127) {
// ASCII printable
if (self.text_input_len < self.text_input.len) {
self.text_input[self.text_input_len] = @intCast(c);
self.text_input_len += 1;
}
} else if (c >= 127) {
// Unicode - encode as UTF-8
var buf: [4]u8 = undefined;
const len = std.unicode.utf8Encode(c, &buf) catch return;
const remaining = self.text_input.len - self.text_input_len;
const to_copy = @min(len, remaining);
@memcpy(self.text_input[self.text_input_len..][0..to_copy], buf[0..to_copy]);
self.text_input_len += to_copy;
}
}
}
}
/// Set key state directly (useful for testing)
pub fn setKeyState(self: *Self, key: Key, pressed: bool) void {
const key_idx = @intFromEnum(key);
if (key_idx < MAX_KEYS) {
self.keys_down[key_idx] = pressed;
}
}
/// Add text input
pub fn addTextInput(self: *Self, text: []const u8) void {
const remaining = self.text_input.len - self.text_input_len;
const to_copy = @min(text.len, remaining);
@memcpy(self.text_input[self.text_input_len..][0..to_copy], text[0..to_copy]);
self.text_input_len += to_copy;
}
// =========================================================================
// Query functions
// =========================================================================
/// Get current mouse position
pub fn mousePos(self: Self) struct { x: i32, y: i32 } {
return .{ .x = self.mouse_x, .y = self.mouse_y };
}
/// Check if mouse button is currently down
pub fn mouseDown(self: Self, button: MouseButton) bool {
return self.mouse_down[@intFromEnum(button)];
}
/// Check if mouse button was just pressed this frame
pub fn mousePressed(self: Self, button: MouseButton) bool {
const idx = @intFromEnum(button);
return self.mouse_down[idx] and !self.mouse_down_prev[idx];
}
/// Check if mouse button was just released this frame
pub fn mouseReleased(self: Self, button: MouseButton) bool {
const idx = @intFromEnum(button);
return !self.mouse_down[idx] and self.mouse_down_prev[idx];
}
/// Get text input this frame
pub fn getTextInput(self: Self) []const u8 {
return self.text_input[0..self.text_input_len];
}
// =========================================================================
// Keyboard query functions
// =========================================================================
/// Check if a key is currently held down
pub fn keyDown(self: Self, key: Key) bool {
const key_idx = @intFromEnum(key);
if (key_idx >= MAX_KEYS) return false;
return self.keys_down[key_idx];
}
/// Check if a key was just pressed this frame
pub fn keyPressed(self: Self, key: Key) bool {
const key_idx = @intFromEnum(key);
if (key_idx >= MAX_KEYS) return false;
return self.keys_down[key_idx] and !self.keys_down_prev[key_idx];
}
/// Check if a key was just released this frame
pub fn keyReleased(self: Self, key: Key) bool {
const key_idx = @intFromEnum(key);
if (key_idx >= MAX_KEYS) return false;
return !self.keys_down[key_idx] and self.keys_down_prev[key_idx];
}
/// Get all key events this frame
pub fn getKeyEvents(self: Self) []const KeyEvent {
return self.key_events[0..self.key_event_count];
}
/// Check if any navigation key was pressed (includes key repeat)
pub fn navKeyPressed(self: Self) ?Key {
// Check key events (includes repeats from SDL2)
for (self.key_events[0..self.key_event_count]) |event| {
if (event.pressed) {
switch (event.key) {
.up, .down, .left, .right, .home, .end, .page_up, .page_down, .tab, .enter, .escape => return event.key,
else => {},
}
}
}
return null;
}
};
// =============================================================================
// Tests
// =============================================================================
test "InputState mouse" {
var input = InputState.init();
input.setMousePos(100, 200);
try std.testing.expectEqual(@as(i32, 100), input.mouse_x);
try std.testing.expectEqual(@as(i32, 200), input.mouse_y);
input.setMouseButton(.left, true);
try std.testing.expect(input.mouseDown(.left));
try std.testing.expect(input.mousePressed(.left));
input.endFrame();
try std.testing.expect(input.mouseDown(.left));
try std.testing.expect(!input.mousePressed(.left));
input.setMouseButton(.left, false);
try std.testing.expect(input.mouseReleased(.left));
}
test "KeyEvent char" {
var buf: [4]u8 = undefined;
const event = KeyEvent{
.key = .a,
.modifiers = .{},
.char = 'A',
.pressed = true,
};
const slice = event.charAsSlice(&buf);
try std.testing.expect(slice != null);
try std.testing.expectEqualStrings("A", slice.?);
}
test "InputState keyboard" {
var input = InputState.init();
// Test keyPressed
input.setKeyState(.up, true);
try std.testing.expect(input.keyDown(.up));
try std.testing.expect(input.keyPressed(.up));
input.endFrame();
try std.testing.expect(input.keyDown(.up));
try std.testing.expect(!input.keyPressed(.up)); // Not pressed anymore, just held
// Test keyReleased
input.setKeyState(.up, false);
try std.testing.expect(!input.keyDown(.up));
try std.testing.expect(input.keyReleased(.up));
input.endFrame();
try std.testing.expect(!input.keyReleased(.up));
}
test "InputState handleKeyEvent" {
var input = InputState.init();
const event = KeyEvent{
.key = .a,
.modifiers = .{ .shift = true },
.char = 'A',
.pressed = true,
};
input.handleKeyEvent(event);
// Key state updated
try std.testing.expect(input.keyDown(.a));
try std.testing.expect(input.keyPressed(.a));
// Modifiers updated
try std.testing.expect(input.modifiers.shift);
// Event stored
try std.testing.expectEqual(@as(usize, 1), input.key_event_count);
try std.testing.expectEqual(Key.a, input.key_events[0].key);
// Text input updated
try std.testing.expectEqualStrings("A", input.getTextInput());
}
test "InputState navKeyPressed" {
var input = InputState.init();
try std.testing.expect(input.navKeyPressed() == null);
input.setKeyState(.down, true);
try std.testing.expect(input.navKeyPressed() == .down);
input.endFrame();
try std.testing.expect(input.navKeyPressed() == null); // Not pressed, just held
input.setKeyState(.enter, true);
try std.testing.expect(input.navKeyPressed() == .enter);
}