From 5512be7d9ed89ca1ab0768c25fa6ca46df4d4d6b Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 6 Jul 2026 20:03:00 +0300 Subject: [PATCH] add input handling --- src/app.zig | 37 +++- src/imgui.zig | 40 +++- src/platform.zig | 554 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 584 insertions(+), 47 deletions(-) diff --git a/src/app.zig b/src/app.zig index 1beddcc..7e08e7c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -3,17 +3,20 @@ const CLI = @import("./cli.zig"); const Platform = @import("./platform.zig"); const log = std.log.scoped(.app); +const Math = @import("math"); +const Vec2 = Math.Vec2; + const App = @This(); bg: Platform.Color, show_first_window: bool, check: bool, -player_x: f32, +player_pos: Vec2, pub fn init(self: *App, plt: Platform.Init) !?u8 { self.* = App{ .bg = .black, - .player_x = 0, + .player_pos = .init(100, 100), .show_first_window = true, .check = false, }; @@ -24,7 +27,25 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { pub fn frame(self: *App, plt: *Platform.Frame) !void { const gfx = plt.gfx; - gfx.drawRectangle(.init(self.player_x + 100, 100), .init(100, 100), .rgb(200, 0, 0)); + const dt = plt.deltaTime(); + + var dir: Vec2 = .init(0, 0); + if (plt.input.isKeyDown(.S)) { + dir.y += 1; + } + if (plt.input.isKeyDown(.W)) { + dir.y -= 1; + } + if (plt.input.isKeyDown(.D)) { + dir.x += 1; + } + if (plt.input.isKeyDown(.A)) { + dir.x -= 1; + } + dir = dir.normalized(); + self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); + + gfx.drawRectangle(self.player_pos, .init(100, 100), .rgb(200, 0, 0)); if (plt.imgui) |imgui| { if (imgui.beginWindow(.{ @@ -34,18 +55,10 @@ pub fn frame(self: *App, plt: *Platform.Frame) !void { })) { defer imgui.endWindow(); - imgui.text("Hello", .{}); + imgui.text("Player pos: {}", .{self.player_pos}); if (imgui.button("foo")) { std.debug.print("pressed\n", .{}); } - if (imgui.slider(.{ - .label = "player x", - .value = &self.player_x, - .min = 0, - .max = 100 - })) { - std.debug.print("player x changed\n", .{}); - } if (imgui.checkbox("Hello", &self.check)) { std.debug.print("checkbox changed\n", .{}); } diff --git a/src/imgui.zig b/src/imgui.zig index 0001183..117eed8 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -14,6 +14,8 @@ pub const State = struct { const simgui = sokol.imgui; const ig = @import("cimgui"); + enabled: bool, + commands_buffer: [128]Command, strings_buffer: [Math.bytes_per_kib * 16]u8, id_stack_buffer: [64]WidgetId, @@ -31,6 +33,7 @@ pub const State = struct { } self.* = @This(){ + .enabled = true, .widget_results = .{}, .commands_buffer = undefined, .strings_buffer = undefined, @@ -44,11 +47,18 @@ pub const State = struct { } pub fn event(self: State, e: sapp.Event) bool { - _ = self; // autofix - return simgui.handleEvent(e); + if (self.enabled) { + return simgui.handleEvent(e); + } + + return false; } pub fn render(self: *State, gpa: Allocator, imgui: *Interface) !void { + if (!self.enabled) { + return; + } + simgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), @@ -57,8 +67,8 @@ pub const State = struct { }); const viewport = ig.igGetMainViewport(); - ig.igSetNextWindowPos(viewport[0].Pos, ig.ImGuiCond_Always); - ig.igSetNextWindowSize(viewport[0].Size, ig.ImGuiCond_Always); + ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always); + ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always); ig.igSetNextWindowViewport(viewport[0].ID); const window_flags = @@ -92,6 +102,8 @@ pub const State = struct { ig.igSetNextWindowSize(.{ .x = size.x, .y = size.y }, ig.ImGuiCond_Once); } + ig.igSetNextWindowBgAlpha(1); + const name = imgui.getString(opts.name); var open: bool = true; @@ -181,12 +193,24 @@ pub const State = struct { } } + // TODO: + // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { + // // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()}); + // } + // ig.igEnd(); + ig.igEnd(); - // TODO: - // if (ig.igBeginMainMenuBar()) { - // ig.igEndMainMenuBar(); - // } + if (ig.igBeginMainMenuBar()) { + defer ig.igEndMainMenuBar(); + + if (ig.igBeginMenu("foo")) { + defer ig.igEndMenu(); + + if (ig.igMenuItem("bar")) { + } + } + } simgui.render(); } diff --git a/src/platform.zig b/src/platform.zig index cf0ef48..fc3fff8 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -24,16 +24,209 @@ const shd = @import("shader"); const ImGUI = @import("./imgui.zig"); +pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { + SPACE = 32, + APOSTROPHE = 39, + COMMA = 44, + MINUS = 45, + PERIOD = 46, + SLASH = 47, + _0 = 48, + _1 = 49, + _2 = 50, + _3 = 51, + _4 = 52, + _5 = 53, + _6 = 54, + _7 = 55, + _8 = 56, + _9 = 57, + SEMICOLON = 59, + EQUAL = 61, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LEFT_BRACKET = 91, + BACKSLASH = 92, + RIGHT_BRACKET = 93, + GRAVE_ACCENT = 96, + WORLD_1 = 161, + WORLD_2 = 162, + ESCAPE = 256, + ENTER = 257, + TAB = 258, + BACKSPACE = 259, + INSERT = 260, + DELETE = 261, + RIGHT = 262, + LEFT = 263, + DOWN = 264, + UP = 265, + PAGE_UP = 266, + PAGE_DOWN = 267, + HOME = 268, + END = 269, + CAPS_LOCK = 280, + SCROLL_LOCK = 281, + NUM_LOCK = 282, + PRINT_SCREEN = 283, + PAUSE = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + F13 = 302, + F14 = 303, + F15 = 304, + F16 = 305, + F17 = 306, + F18 = 307, + F19 = 308, + F20 = 309, + F21 = 310, + F22 = 311, + F23 = 312, + F24 = 313, + F25 = 314, + KP_0 = 320, + KP_1 = 321, + KP_2 = 322, + KP_3 = 323, + KP_4 = 324, + KP_5 = 325, + KP_6 = 326, + KP_7 = 327, + KP_8 = 328, + KP_9 = 329, + KP_DECIMAL = 330, + KP_DIVIDE = 331, + KP_MULTIPLY = 332, + KP_SUBTRACT = 333, + KP_ADD = 334, + KP_ENTER = 335, + KP_EQUAL = 336, + LEFT_SHIFT = 340, + LEFT_CONTROL = 341, + LEFT_ALT = 342, + LEFT_SUPER = 343, + RIGHT_SHIFT = 344, + RIGHT_CONTROL = 345, + RIGHT_ALT = 346, + RIGHT_SUPER = 347, + MENU = 348, +}; + +pub const MouseButton = enum { + left, + right, + middle, + + pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton { + return switch(mouse_button) { + .LEFT => .left, + .RIGHT => .right, + .MIDDLE => .middle, + else => null + }; + } +}; + +pub const Event = union(enum) { + mouse_pressed: MouseButton, + mouse_released: MouseButton, + mouse_move: Vec2, + mouse_enter: Vec2, + mouse_leave, + mouse_scroll: Vec2, + key_pressed: KeyCode, + key_released: KeyCode, + char: u21, + window_resize: Vec2, + focused, + unfocused, +}; + +fn KeyStateType(T: type) type { + return struct { + pressed: std.EnumSet(T), + released: std.EnumSet(T), + down: std.EnumSet(T), + + pub const empty = @This(){ + .pressed = .empty, + .released = .empty, + .down = .empty, + }; + + pub fn press(self: *@This(), key: T) void { + self.pressed.insert(key); + self.down.insert(key); + } + + pub fn release(self: *@This(), key: T) void { + self.released.insert(key); + self.down.remove(key); + } + + pub fn releaseAll(self: *@This()) void { + var iter = self.down.iterator(); + while (iter.next()) |key| { + self.release(key); + } + } + }; +} + fn PlatformType(App: type) type { const GraphicsState = struct { const max_quads = 2048; - pipeline: sg.Pipeline = .{}, - bindings: sg.Bindings = .{}, + screen_size: Vec2, + + pipeline: sg.Pipeline, + bindings: sg.Bindings, quads_buffer: [max_quads]Quad, fn init(self: *@This()) void { + self.* = @This(){ + .quads_buffer = undefined, + .pipeline = .{}, + .bindings = .{}, + .screen_size = .init(sapp.widthf(), sapp.heightf()) + }; + sg.setup(.{ .environment = sglue.environment(), .logger = .{ .func = sokolLogCallback }, @@ -101,7 +294,7 @@ fn PlatformType(App: type) type { fn getInterface(self: *@This()) Graphics { return .{ .clear_color = .black, - .canvas_size = .init(320, 180), + .screen_size = self.screen_size, .quads = .initBuffer(&self.quads_buffer) }; } @@ -111,6 +304,7 @@ fn PlatformType(App: type) type { const Self = @This(); gpa: Allocator, + io: Io, cli_args: []const [:0]const u8, app: *App, @@ -118,9 +312,21 @@ fn PlatformType(App: type) type { gfx: GraphicsState, imgui: if (build_options.has_imgui) ImGUI.State else void, + started_at: Io.Timestamp, + last_frame_at: Io.Timestamp, + events_buffer: [256]Event, + events_overflow: bool, + last_key_press_is_repeat: bool, + + input: Input, + next_input: Input, + fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); + self.started_at = Io.Timestamp.now(self.io, .awake); + self.last_frame_at = self.started_at; + self.gfx.init(); if (build_options.has_imgui) { self.imgui.init(.{ @@ -136,31 +342,20 @@ fn PlatformType(App: type) type { } } - fn createProjectionMatrix(screen_size: Vec2, canvas_size: Vec2) Mat4 { - const scale_x = screen_size.x/canvas_size.x; - const scale_y = screen_size.y/canvas_size.y; - const min_scale = @min(scale_x, scale_y); - - const offscreen_canvas_size = canvas_size.multiply(.{ - .x = scale_x / min_scale, - .y = scale_y / min_scale - }); - + fn createProjectionMatrix(screen_size: Vec2) Mat4 { return Mat4.initIdentity() + // Convert normalized [-1; 1] coordinates to [0; 1] .multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 })) .multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 })) + // Make the top-left corner (0,0) .multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 })) .multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 })) + // Scretch [0; 1] so that they map 1-to-1 on to screen coordinates. .multiply(Mat4.initScale(.{ - .x = 1/offscreen_canvas_size.x, - .y = 1/offscreen_canvas_size.y, - .z = 0 - })) - .multiply(Mat4.initTranslate(.{ - .x = (offscreen_canvas_size.x - canvas_size.x)/2, - .y = (offscreen_canvas_size.y - canvas_size.y)/2, + .x = 1/screen_size.x, + .y = 1/screen_size.y, .z = 0 })); } @@ -168,11 +363,28 @@ fn PlatformType(App: type) type { fn frame(self: *Self) !void { tracy.frameMark(); + if (!self.events_overflow) { + self.input = self.next_input; + } + + if (build_options.has_imgui) { + if (self.input.keyboard.pressed.contains(.F4)) { + self.imgui.enabled = !self.imgui.enabled; + } + } + + const now = Io.Timestamp.now(self.io, .awake); + var gfx = self.gfx.getInterface(); var plt = Frame{ + .t = self.started_at.durationTo(now), + .dt = self.last_frame_at.durationTo(now), .gfx = &gfx, - .imgui = undefined + .input = &self.input, + .imgui = null }; + self.last_frame_at = now; + var imgui: ImGUI.Interface = undefined; if (build_options.has_imgui) { imgui = self.imgui.getInterface(); @@ -192,8 +404,10 @@ fn PlatformType(App: type) type { const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); var vs_params: shd.VsParams = .{ + // TODO: Maybe 'view' matrix is not needed. + // We probably should only have a combined 'mvp' matrix .view = .initIdentity(), - .projection = createProjectionMatrix(screen_size, gfx.canvas_size) + .projection = createProjectionMatrix(screen_size) }; var pass_action: sg.PassAction = .{}; @@ -206,12 +420,21 @@ fn PlatformType(App: type) type { sg.applyPipeline(self.gfx.pipeline); sg.applyBindings(self.gfx.bindings); sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); - sg.draw(0, 6, 1); + sg.draw(0, @intCast(gfx.quads.items.len * 6), 1); if (build_options.has_imgui) { try self.imgui.render(self.gpa, &imgui); } sg.endPass(); sg.commit(); + + self.events_overflow = false; + inline for (&.{ &self.input, &self.next_input }) |input| { + input.events.clearRetainingCapacity(); + input.keyboard.pressed = .empty; + input.keyboard.released = .empty; + input.mouse_buttons.pressed = .empty; + input.mouse_buttons.released = .empty; + } } fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { @@ -225,10 +448,215 @@ fn PlatformType(App: type) type { }; } + fn processEvent(input: *Input, e: Event) void { + switch (e) { + .key_pressed => |key| { + assert(!input.isKeyDown(key)); + input.keyboard.press(key); + }, + .key_released => |key| { + assert(input.isKeyDown(key)); + input.keyboard.release(key); + }, + .mouse_pressed => |button| { + assert(input.mouse_position != null); + assert(!input.isMouseDown(button)); + input.mouse_buttons.press(button); + }, + .mouse_released => |button| { + assert(input.mouse_position != null); + assert(input.isMouseDown(button)); + input.mouse_buttons.release(button); + }, + .mouse_enter => |pos| { + assert(input.mouse_position == null); + input.mouse_position = pos; + }, + .mouse_move => |pos| { + assert(input.mouse_position != null); + input.mouse_position = pos; + }, + .mouse_leave => { + assert(input.mouse_position != null); + input.mouse_position = null; + }, + .char => |char| { + const last_event = input.events.getLast(); + assert(last_event == .key_pressed); + input.key_code_mapping.put(last_event.key_pressed, char); + }, + .mouse_scroll => |offset| { + assert(input.mouse_position != null); + input.mouse_scroll = input.mouse_scroll.add(offset); + }, + .window_resize => {}, + .focused => {}, + .unfocused => { + assert(input.mouse_position == null); + assert(input.keyboard.down.eql(.empty)); + assert(input.mouse_buttons.down.eql(.empty)); + } + } + } + + fn appendEvent(self: *Self, e: Event) void { + const input = &self.next_input; + + if (input.events.capacity == input.events.items.len) { + if (!self.events_overflow) { + log.warn("too many events, limit: {}", .{input.events.capacity}); + } + self.events_overflow = true; + return; + } + + Self.processEvent(input, e); + input.events.appendAssumeCapacity(e); + } + + fn pushEvent(self: *Self, e: Event) void { + const input = &self.next_input; + + if (input.focused) { + if (e == .focused) { + return; + } + + if (e == .key_pressed and input.isKeyDown(e.key_pressed)) { + return; + } + if (e == .key_released and !input.isKeyDown(e.key_released)) { + return; + } + if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) { + return; + } + + if (e == .unfocused) { + var key_iter = input.keyboard.down.iterator(); + while (key_iter.next()) |key| { + self.appendEvent(.{ .key_released = key }); + } + var mouse_buttons_iter = input.mouse_buttons.down.iterator(); + while (mouse_buttons_iter.next()) |button| { + self.appendEvent(.{ .mouse_released = button }); + } + if (input.mouse_position != null) { + self.appendEvent(.{ .mouse_leave = {} }); + } + + input.focused = false; + } + + } else { + if (e == .unfocused) { + return; + } + + if (e != .focused) { + self.appendEvent(.focused); + + if (e == .mouse_move) { + self.appendEvent(.{ .mouse_enter = e.mouse_move }); + } + } + input.focused = true; + } + + self.appendEvent(e); + } + + fn event(self: *Self, e: sapp.Event) !bool { + if (build_options.has_imgui) { + if (self.imgui.event(e)) { + self.pushEvent(.{ + .unfocused = {} + }); + return true; + } + } + + switch (e.type) { + .MOUSE_DOWN => { + self.pushEvent(.{ + .mouse_pressed = MouseButton.fromSokol(e.mouse_button) orelse return false, + }); + }, + .MOUSE_UP => { + self.pushEvent(.{ + .mouse_released = MouseButton.fromSokol(e.mouse_button) orelse return false, + }); + }, + .MOUSE_MOVE => { + self.pushEvent(.{ + .mouse_move = .init(e.mouse_x, e.mouse_y) + }); + }, + .MOUSE_ENTER => { + self.pushEvent(.{ + .mouse_enter = .init(e.mouse_x, e.mouse_y) + }); + }, + .MOUSE_LEAVE => { + self.pushEvent(.{ + .mouse_leave = {} + }); + }, + .MOUSE_SCROLL => { + self.pushEvent(.{ + .mouse_scroll = .init(e.scroll_x, e.scroll_y) + }); + }, + .KEY_DOWN => { + self.last_key_press_is_repeat = e.key_repeat; + if (!e.key_repeat) { + self.pushEvent(.{ + .key_pressed = @enumFromInt(@intFromEnum(e.key_code)) + }); + } + }, + .KEY_UP => { + self.pushEvent(.{ + .key_released = @enumFromInt(@intFromEnum(e.key_code)), + }); + }, + .CHAR => { + if (!self.last_key_press_is_repeat) { + self.pushEvent(.{ + .char = @intCast(e.char_code) + }); + } + }, + .FOCUSED => { + self.pushEvent(.{ + .focused = {} + }); + }, + .UNFOCUSED => { + self.pushEvent(.{ + .unfocused = {} + }); + }, + else => {} + } + + return true; + } + fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); - if (build_options.has_imgui) { - _ = self.imgui.event(ev.*); + + const consumed_event = self.event(ev.*) catch |err| blk: { + log.err("event() error: {}", .{err}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpErrorReturnTrace(trace); + } + sapp.quit(); + break :blk false; + }; + + if (consumed_event) { + sapp.consumeEvent(); } } @@ -265,7 +693,7 @@ pub const Quad = [4]Vertex; pub const Graphics = struct { clear_color: Color, - canvas_size: Vec2, + screen_size: Vec2, quads: std.ArrayList(Quad), pub fn drawQuad(self: *Graphics, quad: Quad) void { @@ -333,9 +761,73 @@ pub const Graphics = struct { pub const Init = struct { }; +pub const Input = struct { + key_code_mapping: std.EnumMap(KeyCode, u21), + + focused: bool, + keyboard: KeyStateType(KeyCode), + mouse_buttons: KeyStateType(MouseButton), + mouse_position: ?Vec2, + mouse_scroll: Vec2, + + events: std.ArrayList(Event), + + fn init(events_buffer: []Event) Input { + return .{ + .events = .initBuffer(events_buffer), + .key_code_mapping = .{}, + .keyboard = .empty, + .mouse_buttons = .empty, + .mouse_position = null, + .mouse_scroll = .init(0, 0), + .focused = false + }; + } + + pub fn isKeyPressed(self: Input, key: KeyCode) bool { + return self.keyboard.pressed.contains(key); + } + + pub fn isKeyReleased(self: Input, key: KeyCode) bool { + return self.keyboard.released.contains(key); + } + + pub fn isKeyDown(self: Input, key: KeyCode) bool { + return self.keyboard.down.contains(key); + } + + pub fn isMousePressed(self: Input, button: MouseButton) bool { + return self.mouse_buttons.pressed.contains(button); + } + + pub fn iMouseReleased(self: Input, button: MouseButton) bool { + return self.mouse_buttons.released.contains(button); + } + + pub fn isMouseDown(self: Input, button: MouseButton) bool { + return self.mouse_buttons.down.contains(button); + } +}; + pub const Frame = struct { + t: Io.Duration, + dt: Io.Duration, + gfx: *Graphics, + input: *Input, imgui: ?*ImGUI.Interface, + + fn nanosecondsToSeconds(nanoseconds: i96) f32 { + return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s); + } + + pub fn deltaTime(self: Frame) f32 { + return nanosecondsToSeconds(self.dt.nanoseconds); + } + + pub fn time(self: Frame) f32 { + return nanosecondsToSeconds(self.t.nanoseconds); + } }; pub fn run(App: type, opts: RunOptions) void { @@ -346,10 +838,18 @@ pub fn run(App: type, opts: RunOptions) void { const plt = arena.create(PlatformApp) catch @panic("OOM"); plt.* = PlatformApp{ .gpa = gpa, + .io = opts.init.io, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), .app = gpa.create(App) catch @panic("OOM"), .gfx = undefined, - .imgui = undefined + .imgui = undefined, + .events_buffer = undefined, + .events_overflow = false, + .input = .init(&[0]Event{}), + .next_input = .init(&plt.events_buffer), + .started_at = undefined, + .last_frame_at = undefined, + .last_key_press_is_repeat = false, }; var icon: sapp.IconDesc = .{};