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 }