const std = @import("std"); const root = @import("root"); const ArrayListUnmanaged = std.ArrayListUnmanaged; pub const Action = @import("app.zig").Action; pub const DeviceInput = union(enum) { pub const PointerId = u8; pub const GamepadId = u8; pub const DeviceId = u8; pub const PointerAxis = enum {x, y, scroll_x, scroll_y, pressure}; key: struct { code: root.Key }, pointer_axis: struct { pointer_id: PointerId = 0, axis: PointerAxis, }, pointer_button: struct { pointer_id: PointerId = 0, button: root.Button, }, gamepad_axis: struct { gamepad_id: GamepadId = 0, axis: u8, }, gamepad_button: struct { gamepad_id: GamepadId = 0, button: root.Button, }, gyro_axis: struct { device_id: DeviceId = 0, axis: enum {x, y, z}, }, accel_axis: struct { device_id: DeviceId = 0, axis: enum {x, y, z}, }, pub fn eql(self: DeviceInput, other: DeviceInput) bool { return std.meta.eql(self, other); } }; pub const InputBinding = struct { pub const InputState = struct { input: DeviceInput, state: bool = false, }; inputs: []InputState, //input: DeviceInput, curve: Curve, action: Action, }; pub const State = struct { // TODO it should be possible to initialize this to a non-zero value, maybe to curve.eval(0) current: f32 = 0, previous: f32 = 0, pub fn started(self: State) bool { return self.previous < 1 and self.current >= 1; } pub fn active(self: State) bool { return self.current >= 1; } pub fn ended(self: State) bool { return self.current < 1 and self.previous >= 1; } pub fn value(self: State) f32 { return self.current; } pub fn delta(self: State) f32 { return self.previous - self.previous; } }; pub fn started(action: Action) bool { return getState(action).started(); } pub fn active(action: Action) bool { return getState(action).active(); } pub fn ended(action: Action) bool { return getState(action).ended(); } pub fn value(action: Action) f32 { return getState(action).value(); } pub fn delta(action: Action) f32 { return getState(action).delta(); } pub var allocator: std.mem.Allocator = undefined; pub var bindings: ArrayListUnmanaged(InputBinding) = undefined; pub var action_states = [_]State{.{}}**std.meta.fields(Action).len; const InitArgs = struct { allocator: ?std.mem.Allocator = null, initial_binding_capacity: usize = 16, }; pub fn _init(args: InitArgs) !void { allocator = args.allocator orelse root.allocator; bindings = try @TypeOf(bindings).initCapacity(allocator, args.initial_binding_capacity); errdefer bindings.deinit(allocator); } pub fn _deinit() void { for (bindings.items) |*binding| { allocator.free(binding.inputs); if (binding.curve.keys) |keys| allocator.free(keys); } bindings.deinit(allocator); } pub fn _before_poll() void { for (&action_states) |*state| { state.previous = state.current; } } pub fn getState(action: Action) *State { return &action_states[@intFromEnum(action)]; } pub fn simulate(input: DeviceInput, raw_value: f32) void { // FIXME if we have Ctrl+A = .foo and A = .bar, then start with A, and later hold Ctrl, both .foo and .bar are active var winner: ?*InputBinding = null; for (bindings.items) |*binding| { var hit = false; var state = true; for (binding.inputs) |*binding_input| { if (binding_input.input.eql(input)) { hit = true; binding_input.state = raw_value != 0; break; } state &= binding_input.state; } if (!hit) continue; if (winner != null and binding.inputs.len <= winner.?.inputs.len) continue; if (!state) continue; winner = binding; } if (winner == null) return; for (winner.?.inputs) |is| { if (raw_value != 0 and !is.state) return; } const action_state = getState(winner.?.action); action_state.*.current = winner.?.curve.eval(raw_value); } pub fn bind(action: Action, input: []const DeviceInput, curve: Curve) !void { const input_states = try allocator.alloc(InputBinding.InputState, input.len); errdefer allocator.free(input_states); for (input_states, input) |*input_state, device_input| { input_state.* = .{ .input = device_input, .state = switch (device_input) {.key, .pointer_button => false, else => true}, }; } try bindings.append(allocator, .{ .inputs = input_states, .curve = curve, .action = action, }); } pub fn bindKey(action: Action, key: CommonKey) !void { try bind(action, &.{.{.key = .{.code = root.getPlatformKey(key)}}}, Curve.linear()); } pub fn bindPointerAxis(action: Action, axis: DeviceInput.PointerAxis) !void { try bind(action, &.{.{.pointer_axis = .{.axis = axis}}}, Curve.linear()); } pub fn bindPointerButton(action: Action, button: CommonButton) !void { try bind(action, &.{.{.pointer_button = .{.button = root.getPlatformButton(button)}}}, Curve.linear()); } pub const Curve = struct { pub const Key = struct { x: f32, y: f32, in_tangent: f32, out_tangent: f32, }; keys: ?[]Key, pub fn init(keys: []Key) !Curve { const var_keys = try allocator.alloc(Key, keys.len); errdefer allocator.free(var_keys); @memcpy(var_keys, keys); return .{ .keys = var_keys, }; } pub fn eval(c: Curve, x: f32) f32 { if (c.keys == null) return x; const n = c.keys.?.len; if (n == 0) return 0; var i: usize = 0; if (x <= c.keys.?[0].x) { i = 0; } else if (x >= c.keys.?[n-1].x) { i = n - 2; } else { while (i+1 < n and x > c.keys.?[i+1].x) : (i += 1) {} } // https://en.wikipedia.org/wiki/Cubic_Hermite_spline const k0 = c.keys.?[i]; const k1 = c.keys.?[i+1]; const t = (x - k0.x) / (k1.x - k0.x); const t2 = t*t; const t3 = t2*t; const h00 = 2*t3 - 3*t2 + 1; const h10 = t3 - 2*t2 + t; const h01 = -2*t3 + 3*t2; const h11 = t3 - t2; const dx = k1.x - k0.x; return h00 * k0.y + h10 * k0.out_tangent * dx + h01 * k1.y + h11 * k1.in_tangent * dx; } pub fn linear() Curve { return .{.keys = null}; } // TODO //pub fn invert(c: *const Curve) *const Curve { // for (c.keys) |*k| { // k.y = 1 - k.y; // } // return c; //} //pub fn deadzone(c: *const Curve, min_activation: f32) *const Curve { // for (c.keys) |*k| { // if (k.* >= min_activation) return; // k.* = 0; // } // return c; //} }; pub const CommonKey = enum { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, left_control, right_control, left_shift, right_shift, arrow_left, arrow_right, arrow_up, arrow_down, }; pub const CommonButton = enum { left, middle, right, };