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