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 }