lib/windowing/src/input.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const event = @import("event.zig");
3 const Key = @import("key.zig").Key;
4 const MouseButton = @import("mouse.zig").MouseButton;
5 const Modifier = @import("modifier.zig").Modifier;
6
7 pub const Point = struct {
8 x: i32 = 0,
9 y: i32 = 0,
10 };
11
12 pub const Wheel = struct {
13 x: f32 = 0,
14 y: f32 = 0,
15 };
16
17 pub const Limits = struct {
18 retained_pressed_key_count: usize = 256,
19 retained_text_codepoint_count: usize = 256,
20 };
21
22 pub const CapacityError = error{
23 PressedKeyHistoryEmpty,
24 TextCodepointHistoryEmpty,
25 CapacityOverflow,
26 };
27
28 pub const Capacity = struct {
29 retained_pressed_key_count: usize,
30 retained_text_codepoint_count: usize,
31 pressed_key_bytes: usize,
32 text_codepoint_bytes: usize,
33 retained_history_bytes: usize,
34
35 pub fn derive(limits: Limits) CapacityError!Capacity {
36 if (limits.retained_pressed_key_count == 0) return error.PressedKeyHistoryEmpty;
37 if (limits.retained_text_codepoint_count == 0) return error.TextCodepointHistoryEmpty;
38 const pressed_key_bytes = std.math.mul(
39 usize,
40 limits.retained_pressed_key_count,
41 @sizeOf(Key),
42 ) catch return error.CapacityOverflow;
43 const text_codepoint_bytes = std.math.mul(
44 usize,
45 limits.retained_text_codepoint_count,
46 @sizeOf(u21),
47 ) catch return error.CapacityOverflow;
48 const retained_history_bytes = std.math.add(
49 usize,
50 pressed_key_bytes,
51 text_codepoint_bytes,
52 ) catch return error.CapacityOverflow;
53 std.debug.assert(pressed_key_bytes / @sizeOf(Key) == limits.retained_pressed_key_count);
54 std.debug.assert(
55 text_codepoint_bytes / @sizeOf(u21) == limits.retained_text_codepoint_count,
56 );
57 return .{
58 .retained_pressed_key_count = limits.retained_pressed_key_count,
59 .retained_text_codepoint_count = limits.retained_text_codepoint_count,
60 .pressed_key_bytes = pressed_key_bytes,
61 .text_codepoint_bytes = text_codepoint_bytes,
62 .retained_history_bytes = retained_history_bytes,
63 };
64 }
65 };
66
67 pub const Status = struct {
68 dropped_pressed_key_count: u64,
69 dropped_text_codepoint_count: u64,
70 };
71
72 pub const State = struct {
73 allocator: std.mem.Allocator,
74 keys: [key_state_capacity]bool = @as([key_state_capacity]bool, @splat(false)),
75 key_pressed: [key_state_capacity]bool = @as([key_state_capacity]bool, @splat(false)),
76 key_released: [key_state_capacity]bool = @as([key_state_capacity]bool, @splat(false)),
77 pressed_keys: History(Key),
78 mouse_buttons: [mouse_button_count]bool = @as([mouse_button_count]bool, @splat(false)),
79 mouse_pressed: [mouse_button_count]bool = @as([mouse_button_count]bool, @splat(false)),
80 mouse_released: [mouse_button_count]bool = @as([mouse_button_count]bool, @splat(false)),
81 mouse_position: Point = .{},
82 mouse_delta: Point = .{},
83 mouse_wheel: Wheel = .{},
84 text_codepoints: History(u21),
85
86 pub fn init(allocator: std.mem.Allocator, capacity: Capacity) std.mem.Allocator.Error!State {
87 std.debug.assert(capacity.retained_pressed_key_count > 0);
88 std.debug.assert(capacity.retained_text_codepoint_count > 0);
89 std.debug.assert(
90 capacity.pressed_key_bytes / @sizeOf(Key) == capacity.retained_pressed_key_count,
91 );
92 std.debug.assert(
93 capacity.text_codepoint_bytes / @sizeOf(u21) == capacity.retained_text_codepoint_count,
94 );
95 const pressed_keys = try allocator.alloc(Key, capacity.retained_pressed_key_count);
96 errdefer allocator.free(pressed_keys);
97 const text_codepoints = try allocator.alloc(u21, capacity.retained_text_codepoint_count);
98 return .{
99 .allocator = allocator,
100 .pressed_keys = History(Key).init(pressed_keys),
101 .text_codepoints = History(u21).init(text_codepoints),
102 };
103 }
104
105 pub fn deinit(self: *State) void {
106 std.debug.assert(self.pressed_keys.items.len > 0);
107 std.debug.assert(self.text_codepoints.items.len > 0);
108 self.allocator.free(self.text_codepoints.items);
109 self.allocator.free(self.pressed_keys.items);
110 self.* = undefined;
111 }
112
113 pub fn beginPoll(self: *State) void {
114 @memset(&self.key_pressed, false);
115 @memset(&self.key_released, false);
116 self.pressed_keys.reset();
117 @memset(&self.mouse_pressed, false);
118 @memset(&self.mouse_released, false);
119 self.mouse_delta = .{};
120 self.mouse_wheel = .{};
121 self.text_codepoints.reset();
122 }
123
124 pub fn pressKey(self: *State, key: Key) void {
125 const index = keyIndex(key) orelse return;
126 self.keys[index] = true;
127 self.key_pressed[index] = true;
128 self.pushPressedKey(key);
129 }
130
131 pub fn releaseKey(self: *State, key: Key) void {
132 const index = keyIndex(key) orelse return;
133 self.keys[index] = false;
134 self.key_released[index] = true;
135 }
136
137 pub fn isKeyDown(self: *const State, key: Key) bool {
138 const index = keyIndex(key) orelse return false;
139 return self.keys[index];
140 }
141
142 pub fn isKeyPressed(self: *const State, key: Key) bool {
143 const index = keyIndex(key) orelse return false;
144 return self.key_pressed[index];
145 }
146
147 pub fn isKeyReleased(self: *const State, key: Key) bool {
148 const index = keyIndex(key) orelse return false;
149 return self.key_released[index];
150 }
151
152 pub fn nextPressedKey(self: *State) ?Key {
153 return self.pressed_keys.next();
154 }
155
156 pub fn modifiers(self: *const State) Modifier {
157 return .{
158 .shift = self.isKeyDown(.left_shift) or self.isKeyDown(.right_shift),
159 .ctrl = self.isKeyDown(.left_ctrl) or self.isKeyDown(.right_ctrl),
160 .alt = self.isKeyDown(.left_alt) or self.isKeyDown(.right_alt),
161 .super = self.isKeyDown(.left_super) or self.isKeyDown(.right_super),
162 };
163 }
164
165 pub fn pressMouse(self: *State, button: MouseButton) void {
166 const index = mouseButtonIndex(button);
167 self.mouse_buttons[index] = true;
168 self.mouse_pressed[index] = true;
169 }
170
171 pub fn releaseMouse(self: *State, button: MouseButton) void {
172 const index = mouseButtonIndex(button);
173 self.mouse_buttons[index] = false;
174 self.mouse_released[index] = true;
175 }
176
177 pub fn isMouseButtonDown(self: *const State, button: MouseButton) bool {
178 return self.mouse_buttons[mouseButtonIndex(button)];
179 }
180
181 pub fn isMouseButtonPressed(self: *const State, button: MouseButton) bool {
182 return self.mouse_pressed[mouseButtonIndex(button)];
183 }
184
185 pub fn isMouseButtonReleased(self: *const State, button: MouseButton) bool {
186 return self.mouse_released[mouseButtonIndex(button)];
187 }
188
189 pub fn setMousePosition(self: *State, x: i32, y: i32) void {
190 self.mouse_position = .{ .x = x, .y = y };
191 }
192
193 pub fn moveMouse(self: *State, x: i32, y: i32) void {
194 const previous = self.mouse_position;
195 self.mouse_position = .{ .x = x, .y = y };
196 self.mouse_delta.x = saturatingI32Add(self.mouse_delta.x, saturatingI32Diff(x, previous.x));
197 self.mouse_delta.y = saturatingI32Add(self.mouse_delta.y, saturatingI32Diff(y, previous.y));
198 }
199
200 pub fn addMouseWheel(self: *State, x: f32, y: f32) void {
201 self.mouse_wheel.x += x;
202 self.mouse_wheel.y += y;
203 }
204
205 pub fn pushTextInput(self: *State, bytes: []const u8) bool {
206 if (bytes.len == 0 or bytes.len > event.max_text_input_bytes) return false;
207 if (!std.unicode.utf8ValidateSlice(bytes)) return false;
208
209 var iterator = std.unicode.Utf8Iterator{ .bytes = bytes, .i = 0 };
210 while (iterator.nextCodepoint()) |codepoint| {
211 if (codepoint < 0x20 or codepoint == 0x7f) return false;
212 }
213
214 iterator = std.unicode.Utf8Iterator{ .bytes = bytes, .i = 0 };
215 while (iterator.nextCodepoint()) |codepoint| {
216 self.pushTextCodepoint(codepoint);
217 }
218 return true;
219 }
220
221 pub fn nextTextCodepoint(self: *State) ?u21 {
222 return self.text_codepoints.next();
223 }
224
225 pub fn status(self: *const State) Status {
226 return .{
227 .dropped_pressed_key_count = self.pressed_keys.dropped_count,
228 .dropped_text_codepoint_count = self.text_codepoints.dropped_count,
229 };
230 }
231
232 fn pushPressedKey(self: *State, key: Key) void {
233 _ = self.pressed_keys.push(key);
234 }
235
236 fn pushTextCodepoint(self: *State, codepoint: u21) void {
237 _ = self.text_codepoints.push(codepoint);
238 }
239 };
240
241 const key_state_capacity: usize = blk: {
242 var max_value: comptime_int = 0;
243 for (@typeInfo(Key).@"enum".field_values) |field_value| {
244 if (field_value > max_value) max_value = field_value;
245 }
246 break :blk max_value + 1;
247 };
248
249 const mouse_button_count = @typeInfo(MouseButton).@"enum".field_names.len;
250
251 fn History(comptime T: type) type {
252 return struct {
253 items: []T,
254 read_index: usize = 0,
255 retained_count: usize = 0,
256 dropped_count: u64 = 0,
257
258 fn init(items: []T) @This() {
259 std.debug.assert(items.len > 0);
260 return .{ .items = items };
261 }
262
263 fn reset(self: *@This()) void {
264 std.debug.assert(self.read_index <= self.retained_count);
265 std.debug.assert(self.retained_count <= self.items.len);
266 self.read_index = 0;
267 self.retained_count = 0;
268 }
269
270 fn push(self: *@This(), item: T) bool {
271 std.debug.assert(self.retained_count <= self.items.len);
272 if (self.retained_count == self.items.len) {
273 self.dropped_count +|= 1;
274 return false;
275 }
276 self.items[self.retained_count] = item;
277 self.retained_count += 1;
278 return true;
279 }
280
281 fn next(self: *@This()) ?T {
282 std.debug.assert(self.read_index <= self.retained_count);
283 std.debug.assert(self.retained_count <= self.items.len);
284 if (self.read_index == self.retained_count) return null;
285 const item = self.items[self.read_index];
286 self.read_index += 1;
287 return item;
288 }
289 };
290 }
291
292 fn keyIndex(key: Key) ?usize {
293 const raw = @backingInt(key);
294 inline for (
295 @typeInfo(Key).@"enum".field_names,
296 @typeInfo(Key).@"enum".field_values,
297 ) |field_name, field_name_value| {
298 const field = .{ .name = field_name, .value = field_name_value };
299 if (raw == field.value) {
300 if (raw == @backingInt(Key.unknown)) return null;
301 return @intCast(raw);
302 }
303 }
304 return null;
305 }
306
307 fn mouseButtonIndex(button: MouseButton) usize {
308 return @intCast(@backingInt(button));
309 }
310
311 fn saturatingI32FromI64(value: i64) i32 {
312 if (value < std.math.minInt(i32)) return std.math.minInt(i32);
313 if (value > std.math.maxInt(i32)) return std.math.maxInt(i32);
314 return @intCast(value);
315 }
316
317 fn saturatingI32Add(left: i32, right: i32) i32 {
318 return std.math.add(i32, left, right) catch if (right < 0)
319 std.math.minInt(i32)
320 else
321 std.math.maxInt(i32);
322 }
323
324 fn saturatingI32Diff(next: i32, previous: i32) i32 {
325 return saturatingI32FromI64(@as(i64, next) - @as(i64, previous));
326 }
327
328 fn initTestState() !State {
329 return State.init(std.testing.allocator, try Capacity.derive(.{}));
330 }
331
332 test "input history capacity derives exact storage and rejects invalid limits" {
333 const capacity = try Capacity.derive(.{});
334 try std.testing.expectEqual(@as(usize, 256), capacity.retained_pressed_key_count);
335 try std.testing.expectEqual(@as(usize, 256), capacity.retained_text_codepoint_count);
336 try std.testing.expectEqual(256 * @sizeOf(Key), capacity.pressed_key_bytes);
337 try std.testing.expectEqual(256 * @sizeOf(u21), capacity.text_codepoint_bytes);
338 try std.testing.expectEqual(
339 capacity.pressed_key_bytes + capacity.text_codepoint_bytes,
340 capacity.retained_history_bytes,
341 );
342 try std.testing.expectError(error.PressedKeyHistoryEmpty, Capacity.derive(.{
343 .retained_pressed_key_count = 0,
344 }));
345 try std.testing.expectError(error.TextCodepointHistoryEmpty, Capacity.derive(.{
346 .retained_text_codepoint_count = 0,
347 }));
348 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
349 .retained_pressed_key_count = std.math.maxInt(usize) / @sizeOf(Key) + 1,
350 }));
351 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
352 .retained_pressed_key_count = std.math.maxInt(usize) / @sizeOf(Key),
353 .retained_text_codepoint_count = 1,
354 }));
355 }
356
357 test "input histories retain their limits and allocate only during initialization" {
358 const capacity = try Capacity.derive(.{
359 .retained_pressed_key_count = 2,
360 .retained_text_codepoint_count = 2,
361 });
362 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 2 });
363 var state = try State.init(failing.allocator(), capacity);
364 defer state.deinit();
365 try std.testing.expectEqual(@as(usize, 2), failing.allocations);
366
367 state.pressKey(.a);
368 state.pressKey(.b);
369 state.pressKey(.enter);
370 try std.testing.expect(state.isKeyDown(.enter));
371 try std.testing.expect(state.pushTextInput("abc"));
372 try std.testing.expectEqual(Status{
373 .dropped_pressed_key_count = 1,
374 .dropped_text_codepoint_count = 1,
375 }, state.status());
376 try std.testing.expectEqual(Key.a, state.nextPressedKey().?);
377 try std.testing.expectEqual(Key.b, state.nextPressedKey().?);
378 try std.testing.expect(state.nextPressedKey() == null);
379 try std.testing.expectEqual(@as(u21, 'a'), state.nextTextCodepoint().?);
380 try std.testing.expectEqual(@as(u21, 'b'), state.nextTextCodepoint().?);
381 try std.testing.expect(state.nextTextCodepoint() == null);
382
383 state.beginPoll();
384 state.pressKey(.tab);
385 try std.testing.expect(state.pushTextInput("λ"));
386 try std.testing.expectEqual(Key.tab, state.nextPressedKey().?);
387 try std.testing.expectEqual(@as(u21, 'λ'), state.nextTextCodepoint().?);
388 try std.testing.expectEqual(Status{
389 .dropped_pressed_key_count = 1,
390 .dropped_text_codepoint_count = 1,
391 }, state.status());
392
393 state.pressed_keys.dropped_count = std.math.maxInt(u64);
394 state.text_codepoints.dropped_count = std.math.maxInt(u64);
395 state.beginPoll();
396 state.pressKey(.a);
397 state.pressKey(.b);
398 state.pressKey(.enter);
399 try std.testing.expect(state.pushTextInput("abc"));
400 try std.testing.expectEqual(Status{
401 .dropped_pressed_key_count = std.math.maxInt(u64),
402 .dropped_text_codepoint_count = std.math.maxInt(u64),
403 }, state.status());
404 try std.testing.expectEqual(@as(usize, 2), failing.allocations);
405 }
406
407 test "input state tracks keys and ignores unknown keys" {
408 var state = try initTestState();
409 defer state.deinit();
410
411 try std.testing.expect(!state.isKeyDown(.a));
412 try std.testing.expect(!state.isKeyPressed(.a));
413 state.pressKey(.a);
414 try std.testing.expect(state.isKeyDown(.a));
415 try std.testing.expect(state.isKeyPressed(.a));
416 try std.testing.expect(!state.isKeyReleased(.a));
417 try std.testing.expectEqual(Key.a, state.nextPressedKey().?);
418 try std.testing.expect(state.nextPressedKey() == null);
419 state.releaseKey(.a);
420 try std.testing.expect(!state.isKeyDown(.a));
421 try std.testing.expect(state.isKeyReleased(.a));
422
423 state.pressKey(.unknown);
424 try std.testing.expect(!state.isKeyDown(.unknown));
425 try std.testing.expect(!state.isKeyPressed(.unknown));
426 try std.testing.expect(state.nextPressedKey() == null);
427 state.pressKey(@fromBackingInt(@intCast(100)));
428 try std.testing.expect(!state.isKeyDown(@fromBackingInt(@intCast(100))));
429 try std.testing.expect(!state.isKeyPressed(@fromBackingInt(@intCast(100))));
430 try std.testing.expect(state.nextPressedKey() == null);
431 try std.testing.expect(!state.isKeyDown(@fromBackingInt(@intCast(4096))));
432 }
433
434 test "input state queues pressed keys in poll order" {
435 var state = try initTestState();
436 defer state.deinit();
437
438 state.pressKey(.b);
439 state.pressKey(.a);
440
441 try std.testing.expectEqual(Key.b, state.nextPressedKey().?);
442 try std.testing.expectEqual(Key.a, state.nextPressedKey().?);
443 try std.testing.expect(state.nextPressedKey() == null);
444 }
445
446 test "input state derives active modifiers from key state" {
447 var state = try initTestState();
448 defer state.deinit();
449
450 state.pressKey(.left_ctrl);
451 state.pressKey(.right_shift);
452
453 var modifiers = state.modifiers();
454 try std.testing.expect(modifiers.ctrl);
455 try std.testing.expect(modifiers.shift);
456 try std.testing.expect(!modifiers.alt);
457 try std.testing.expect(!modifiers.super);
458
459 state.releaseKey(.left_ctrl);
460 modifiers = state.modifiers();
461 try std.testing.expect(!modifiers.ctrl);
462 try std.testing.expect(modifiers.shift);
463 }
464
465 test "input state tracks mouse buttons" {
466 var state = try initTestState();
467 defer state.deinit();
468
469 try std.testing.expect(!state.isMouseButtonDown(.left));
470 try std.testing.expect(!state.isMouseButtonPressed(.left));
471 state.pressMouse(.left);
472 try std.testing.expect(state.isMouseButtonDown(.left));
473 try std.testing.expect(state.isMouseButtonPressed(.left));
474 try std.testing.expect(!state.isMouseButtonReleased(.left));
475 state.releaseMouse(.left);
476 try std.testing.expect(!state.isMouseButtonDown(.left));
477 try std.testing.expect(state.isMouseButtonReleased(.left));
478 }
479
480 test "input state resets per-poll transient accumulators" {
481 var state = try initTestState();
482 defer state.deinit();
483
484 state.pressKey(.enter);
485 state.releaseKey(.tab);
486 state.pressMouse(.right);
487 state.releaseMouse(.middle);
488 try std.testing.expect(state.pushTextInput("xλ"));
489 state.setMousePosition(10, 20);
490 state.moveMouse(15, 12);
491 state.addMouseWheel(1.5, -2.0);
492
493 try std.testing.expect(state.isKeyPressed(.enter));
494 try std.testing.expect(state.isKeyReleased(.tab));
495 try std.testing.expectEqual(Key.enter, state.nextPressedKey().?);
496 try std.testing.expectEqual(@as(u21, 'x'), state.nextTextCodepoint().?);
497 try std.testing.expectEqual(@as(u21, 'λ'), state.nextTextCodepoint().?);
498 try std.testing.expect(state.isMouseButtonPressed(.right));
499 try std.testing.expect(state.isMouseButtonReleased(.middle));
500 try std.testing.expectEqual(Point{ .x = 15, .y = 12 }, state.mouse_position);
501 try std.testing.expectEqual(Point{ .x = 5, .y = -8 }, state.mouse_delta);
502 try std.testing.expectEqual(Wheel{ .x = 1.5, .y = -2.0 }, state.mouse_wheel);
503
504 state.beginPoll();
505 try std.testing.expect(!state.isKeyPressed(.enter));
506 try std.testing.expect(!state.isKeyReleased(.tab));
507 try std.testing.expect(state.nextPressedKey() == null);
508 try std.testing.expect(state.nextTextCodepoint() == null);
509 try std.testing.expect(!state.isMouseButtonPressed(.right));
510 try std.testing.expect(!state.isMouseButtonReleased(.middle));
511 try std.testing.expectEqual(Point{}, state.mouse_delta);
512 try std.testing.expectEqual(Wheel{}, state.mouse_wheel);
513 try std.testing.expectEqual(Point{ .x = 15, .y = 12 }, state.mouse_position);
514 }
515
516 test "input state clamps extreme pointer deltas" {
517 var state = try initTestState();
518 defer state.deinit();
519
520 state.setMousePosition(std.math.maxInt(i32), 0);
521 state.moveMouse(std.math.minInt(i32), 0);
522
523 try std.testing.expectEqual(Point{ .x = std.math.minInt(i32), .y = 0 }, state.mouse_delta);
524 }
525
526 test "input state queues valid text codepoints" {
527 var state = try initTestState();
528 defer state.deinit();
529
530 try std.testing.expect(state.pushTextInput("aλ"));
531 try std.testing.expectEqual(@as(u21, 'a'), state.nextTextCodepoint().?);
532 try std.testing.expectEqual(@as(u21, 'λ'), state.nextTextCodepoint().?);
533 try std.testing.expect(state.nextTextCodepoint() == null);
534
535 try std.testing.expect(!state.pushTextInput("\n"));
536 try std.testing.expect(!state.pushTextInput(&(@as([(event.max_text_input_bytes + 1)]u8, @splat('x')))));
537 try std.testing.expect(state.nextTextCodepoint() == null);
538 }