Skip to content

Commit

Permalink
add a simple sticky key buffer
Browse files Browse the repository at this point in the history
  • Loading branch information
floooh committed Jul 25, 2024
1 parent 531aedf commit 5e2b449
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/common/common.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub const glue = @import("glue.zig");
pub const clock = @import("clock.zig");
pub const filter = @import("filter.zig");
pub const utils = @import("utils.zig");
pub const keybuf = @import("keybuf.zig");
95 changes: 95 additions & 0 deletions src/common/keybuf.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
///! a 'sticky' key buffer to store key presses for a guaranteed amount of time
const assert = @import("std").debug.assert;

// comptime config options
const Config = struct {
num_slots: comptime_int = 8, // number of max simultanously pressed keys
};

pub fn Type(comptime cfg: Config) type {
assert(cfg.num_slots > 0);
return struct {
const Self = @This();

// runtime options
pub const Options = struct {
sticky_time: u64 = 32, // number of time units a pressed key should 'stick'
};

// state of a single currently pressed key
pub const Slot = struct {
// key code of the pressed key (zero if slot is not populated)
key: u32 = 0,
// optional keyboard matrix bit mask
mask: u32 = 0,
// timestamp of when the key was pressed down
pressed_time: u64 = 0,
// set to true when the key has been released
released: bool = false,
};

slots: [cfg.num_slots]Slot = [_]Slot{.{}} ** cfg.num_slots,
cur_time: u64 = 0,
sticky_time: u64 = 0,

pub fn init(options: Options) Self {
return .{
.sticky_time = options.sticky_time,
};
}

// call once per frame with frame duration in your chosen time unit
pub fn update(self: *Self, frame_time: u64) void {
self.cur_time +%= frame_time;
for (&self.slots) |*slot| {
if (slot.released) {
// properly handle time wraparound
if ((self.cur_time < slot.pressed_time) or
(self.cur_time >= (slot.pressed_time +% self.sticky_time)))
{
slot.* = .{};
}
}
}
}

// call when a key is pressed down
pub fn keyDown(self: *Self, key: u32, mask: u32) void {
// first check if the key is already buffered, if yes only update timestamp
for (&self.slots) |*slot| {
if (key == slot.key) {
assert(slot.mask == mask);
slot.pressed_time = self.cur_time;
return;
}
}
// otherwise find a populate a free slot
for (&self.slots) |*slot| {
if (0 == slot.key) {
slot.key = key;
slot.mask = mask;
slot.pressed_time = self.cur_time;
slot.released = false;
return;
}
}
}

// call when a pressed key is released
pub fn keyUp(self: *Self, key: u32) void {
for (&self.slots) |*slot| {
if (key == slot.key) {
slot.released = true;
return;
}
}
}

// 'unpress' all keys, call this when the emulator window looses focus
pub fn flush(self: *Self) void {
for (&self.slots) |*slot| {
slot.* = .{};
}
}
};
}
1 change: 1 addition & 0 deletions tests/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub fn build(b: *Build, opts: Options) void {
"ay3891",
"z80ctc",
"z80pio",
"keybuf",
};
const test_step = b.step("test", "Run unit tests");
inline for (unit_tests) |name| {
Expand Down
69 changes: 69 additions & 0 deletions tests/keybuf.test.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const expect = @import("std").testing.expect;
const keybuf = @import("chipz").common.keybuf;

const KeyBuf = keybuf.Type(.{ .num_slots = 4 });

test "single key" {
var kb = KeyBuf.init(.{ .sticky_time = 1000 });
try expect(kb.sticky_time == 1000);
kb.update(250);
try expect(kb.cur_time == 250);
kb.keyDown('A', 0x1234);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].mask == 0x1234);
try expect(kb.slots[0].pressed_time == 250);
try expect(kb.slots[0].released == false);
try expect(kb.slots[1].key == 0);
kb.keyUp('A');
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].released);
kb.update(250);
try expect(kb.cur_time == 500);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].released);
kb.update(250);
try expect(kb.cur_time == 750);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].released);
kb.update(250);
try expect(kb.cur_time == 1000);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].released);
kb.update(250);
try expect(kb.cur_time == 1250);
try expect(kb.slots[0].key == 0);
try expect(kb.slots[0].mask == 0);
try expect(kb.slots[0].pressed_time == 0);
try expect(kb.slots[0].released == false);
}

test "update existing key" {
var kb = KeyBuf.init(.{ .sticky_time = 1000 });
try expect(kb.sticky_time == 1000);
kb.update(250);
try expect(kb.cur_time == 250);
kb.keyDown('A', 0x1234);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].mask == 0x1234);
try expect(kb.slots[0].pressed_time == 250);
try expect(kb.slots[0].released == false);
kb.update(250);
try expect(kb.cur_time == 500);
kb.keyDown('A', 0x1234);
try expect(kb.slots[0].key == 'A');
try expect(kb.slots[0].mask == 0x1234);
try expect(kb.slots[0].pressed_time == 500);
try expect(kb.slots[0].released == false);
try expect(kb.slots[1].key == 0);
kb.update(500);
try expect(kb.slots[0].key == 'A');
kb.keyUp('A');
kb.update(500);
try expect(kb.cur_time == 1500);
try expect(kb.slots[0].key == 0);
try expect(kb.slots[0].mask == 0);
try expect(kb.slots[0].pressed_time == 0);
try expect(kb.slots[0].released == false);
}

// FIXME: multiple pressed keys

0 comments on commit 5e2b449

Please sign in to comment.