- 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>
471 lines
12 KiB
Zig
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);
|
|
}
|