lib/windowing/src/event.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const Key = @import("key.zig").Key;
3 const MouseButton = @import("mouse.zig").MouseButton;
4 const Modifier = @import("modifier.zig").Modifier;
5
6 pub const max_text_input_bytes = 16;
7
8 pub const Limits = struct {
9 retained_event_count: usize = 256,
10 };
11
12 pub const CapacityError = error{
13 RetainedEventsEmpty,
14 CapacityOverflow,
15 };
16
17 pub const Capacity = struct {
18 retained_event_count: usize,
19 retained_event_bytes: usize,
20
21 pub fn derive(limits: Limits) CapacityError!Capacity {
22 if (limits.retained_event_count == 0) return error.RetainedEventsEmpty;
23 const retained_event_bytes = std.math.mul(
24 usize,
25 limits.retained_event_count,
26 @sizeOf(Event),
27 ) catch return error.CapacityOverflow;
28 std.debug.assert(retained_event_bytes / @sizeOf(Event) == limits.retained_event_count);
29 return .{
30 .retained_event_count = limits.retained_event_count,
31 .retained_event_bytes = retained_event_bytes,
32 };
33 }
34 };
35
36 pub const KeyEvent = struct {
37 key: Key,
38 modifiers: Modifier = .{},
39 repeated: bool = false,
40 };
41
42 pub const TextInputEvent = struct {
43 bytes: [max_text_input_bytes]u8 = undefined,
44 len: u8 = 0,
45 modifiers: Modifier = .{},
46
47 pub fn text(self: *const TextInputEvent) []const u8 {
48 return self.bytes[0..self.len];
49 }
50 };
51
52 pub const MouseButtonEvent = struct {
53 button: MouseButton,
54 x: i32,
55 y: i32,
56 modifiers: Modifier = .{},
57 };
58
59 pub const MouseMoveEvent = struct {
60 x: i32,
61 y: i32,
62 };
63
64 pub const MouseWheelEvent = struct {
65 x: i32,
66 y: i32,
67 delta_x: f32 = 0,
68 delta_y: f32 = 0,
69 modifiers: Modifier = .{},
70 };
71
72 pub const SizeEvent = struct {
73 width: u32,
74 height: u32,
75 };
76
77 pub const FocusEvent = struct {
78 focused: bool,
79 };
80
81 pub const Event = union(enum) {
82 key_press: KeyEvent,
83 key_release: KeyEvent,
84 text_input: TextInputEvent,
85 mouse_press: MouseButtonEvent,
86 mouse_release: MouseButtonEvent,
87 mouse_move: MouseMoveEvent,
88 mouse_wheel: MouseWheelEvent,
89 window_resize: SizeEvent,
90 window_close,
91 window_focus: FocusEvent,
92
93 pub fn textInput(bytes: []const u8, modifiers: Modifier) ?Event {
94 const len = textInputLen(bytes) orelse return null;
95 var event = TextInputEvent{
96 .len = @intCast(len),
97 .modifiers = modifiers,
98 };
99 @memcpy(event.bytes[0..len], bytes[0..len]);
100 return .{ .text_input = event };
101 }
102 };
103
104 fn textInputLen(bytes: []const u8) ?usize {
105 if (bytes.len == 0 or bytes.len > max_text_input_bytes) return null;
106 if (!std.unicode.utf8ValidateSlice(bytes)) return null;
107
108 var iterator = std.unicode.Utf8Iterator{ .bytes = bytes, .i = 0 };
109 while (iterator.nextCodepoint()) |codepoint| {
110 if (codepoint < 0x20 or codepoint == 0x7f) return null;
111 }
112 return bytes.len;
113 }
114
115 pub const EventIterator = struct {
116 events: []const Event,
117 index: usize = 0,
118
119 pub fn next(self: *EventIterator) ?Event {
120 if (self.index >= self.events.len) return null;
121 const ev = self.events[self.index];
122 self.index += 1;
123 return ev;
124 }
125
126 pub fn reset(self: *EventIterator) void {
127 self.index = 0;
128 }
129 };
130
131 pub const Queue = struct {
132 allocator: std.mem.Allocator,
133 items: []Event,
134 retained_count: usize = 0,
135 dropped_count: u64 = 0,
136
137 pub fn init(allocator: std.mem.Allocator, capacity: Capacity) std.mem.Allocator.Error!Queue {
138 std.debug.assert(capacity.retained_event_count > 0);
139 std.debug.assert(
140 capacity.retained_event_bytes / @sizeOf(Event) == capacity.retained_event_count,
141 );
142 return .{
143 .allocator = allocator,
144 .items = try allocator.alloc(Event, capacity.retained_event_count),
145 };
146 }
147
148 pub fn deinit(self: *Queue) void {
149 std.debug.assert(self.items.len > 0);
150 std.debug.assert(self.retained_count <= self.items.len);
151 self.allocator.free(self.items);
152 self.* = undefined;
153 }
154
155 pub fn beginPoll(self: *Queue) void {
156 std.debug.assert(self.retained_count <= self.items.len);
157 self.retained_count = 0;
158 }
159
160 pub fn push(self: *Queue, event: Event) bool {
161 std.debug.assert(self.retained_count <= self.items.len);
162 if (self.retained_count == self.items.len) {
163 self.dropped_count +|= 1;
164 return false;
165 }
166 self.items[self.retained_count] = event;
167 self.retained_count += 1;
168 return true;
169 }
170
171 pub fn iterator(self: *const Queue) EventIterator {
172 std.debug.assert(self.retained_count <= self.items.len);
173 return .{ .events = self.items[0..self.retained_count] };
174 }
175
176 pub fn droppedEventCount(self: *const Queue) u64 {
177 return self.dropped_count;
178 }
179 };
180
181 test "event queue capacity derives exact storage and rejects invalid limits" {
182 const capacity = try Capacity.derive(.{});
183 try std.testing.expectEqual(@as(usize, 256), capacity.retained_event_count);
184 try std.testing.expectEqual(256 * @sizeOf(Event), capacity.retained_event_bytes);
185 try std.testing.expectError(error.RetainedEventsEmpty, Capacity.derive(.{
186 .retained_event_count = 0,
187 }));
188 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
189 .retained_event_count = std.math.maxInt(usize) / @sizeOf(Event) + 1,
190 }));
191 }
192
193 test "event queue retains its limit and allocates only during initialization" {
194 const capacity = try Capacity.derive(.{ .retained_event_count = 2 });
195 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
196 var queue = try Queue.init(failing.allocator(), capacity);
197 defer queue.deinit();
198 try std.testing.expectEqual(@as(usize, 1), failing.allocations);
199
200 try std.testing.expect(queue.push(.window_close));
201 try std.testing.expect(queue.push(.{ .window_focus = .{ .focused = true } }));
202 try std.testing.expect(!queue.push(.{ .window_focus = .{ .focused = false } }));
203 try std.testing.expectEqual(@as(u64, 1), queue.droppedEventCount());
204
205 var retained = queue.iterator();
206 try std.testing.expect(retained.next().? == .window_close);
207 try std.testing.expect(retained.next().?.window_focus.focused);
208 try std.testing.expect(retained.next() == null);
209
210 queue.beginPoll();
211 try std.testing.expect(queue.push(.{ .window_focus = .{ .focused = false } }));
212 retained = queue.iterator();
213 try std.testing.expect(!retained.next().?.window_focus.focused);
214 try std.testing.expect(retained.next() == null);
215
216 queue.dropped_count = std.math.maxInt(u64);
217 try std.testing.expect(queue.push(.window_close));
218 try std.testing.expect(!queue.push(.window_close));
219 try std.testing.expectEqual(std.math.maxInt(u64), queue.droppedEventCount());
220 try std.testing.expectEqual(@as(usize, 1), failing.allocations);
221 }
222
223 test "text input event stores valid UTF-8 text" {
224 const event = Event.textInput("λ", .{ .ctrl = true }).?;
225 try std.testing.expectEqualStrings("λ", event.text_input.text());
226 try std.testing.expect(event.text_input.modifiers.ctrl);
227 }
228
229 test "text input event rejects controls and oversized chunks" {
230 try std.testing.expect(Event.textInput("\n", .{}) == null);
231 try std.testing.expect(
232 Event.textInput(&(@as([(max_text_input_bytes + 1)]u8, @splat('x'))), .{}) == null,
233 );
234 }