Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m21s
With the `isUnicode` method the read unicode characters from the user can be checked against displayable text and rendered on the screen accordingly.
251 lines
8.0 KiB
Zig
251 lines
8.0 KiB
Zig
const QuitText = struct {
|
|
const text = "Press ctrl+c to quit.";
|
|
|
|
pub fn element(this: *@This()) App.Element {
|
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
|
_ = ctx;
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
const row = 2;
|
|
const col = size.x / 2 -| (text.len / 2);
|
|
const anchor = (row * size.x) + col;
|
|
|
|
for (text, 0..) |cp, idx| {
|
|
cells[anchor + idx].style.fg = .white;
|
|
cells[anchor + idx].style.bg = .black;
|
|
cells[anchor + idx].cp = cp;
|
|
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (anchor + idx == cells.len - 1) break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Spinner element implementation that runs a simple animation that requires
|
|
/// the continuous draw loop.
|
|
const Spinner = struct {
|
|
counter: u8 = 0,
|
|
index: u8 = 0,
|
|
|
|
const map: [6]u21 = .{ '', '', '', '', '', '' };
|
|
|
|
pub fn element(this: *@This()) App.Element {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
cells[size.x + 1].cp = map[this.index];
|
|
|
|
this.counter += 1;
|
|
if (this.counter >= 20) {
|
|
this.index += 1;
|
|
this.index %= 6;
|
|
this.counter = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
const InputField = struct {
|
|
allocator: std.mem.Allocator,
|
|
input: std.ArrayList(u21),
|
|
queue: *App.Queue,
|
|
|
|
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
|
return .{
|
|
.allocator = allocator,
|
|
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
|
|
.queue = queue,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(this: *@This()) void {
|
|
this.input.deinit(this.allocator);
|
|
}
|
|
|
|
pub fn element(this: *@This()) App.Element {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
.key => |key| {
|
|
if (key.isUnicode()) try this.input.append(this.allocator, key.cp);
|
|
|
|
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
|
this.queue.push(.{ .accept = try this.input.toOwnedSlice(this.allocator) });
|
|
|
|
if (key.eql(.{ .cp = zterm.input.Backspace }))
|
|
_ = this.input.pop();
|
|
|
|
if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
|
_ = this.input.pop();
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
if (this.input.items.len == 0) return;
|
|
|
|
const row = 1;
|
|
const col = 1;
|
|
const anchor = (row * size.x) + col;
|
|
|
|
for (this.input.items, 0..) |cp, idx| {
|
|
cells[anchor + idx].style.fg = .black;
|
|
cells[anchor + idx].style.cursor = false;
|
|
cells[anchor + idx].cp = cp;
|
|
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (anchor + idx == cells.len - 1) break;
|
|
|
|
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn main() !void {
|
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
|
|
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
|
|
|
const allocator = gpa.allocator();
|
|
|
|
var threaded_io: std.Io.Threaded = .init(allocator, .{});
|
|
defer threaded_io.deinit();
|
|
|
|
var app: App = .init(threaded_io.ioBasic(), .{});
|
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
|
defer renderer.deinit();
|
|
|
|
var input_field: InputField = .init(allocator, &app.queue);
|
|
defer input_field.deinit();
|
|
|
|
var quit_text: QuitText = .{};
|
|
var spinner: Spinner = .{};
|
|
|
|
var container = try App.Container.init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
.layout = .{
|
|
.direction = .vertical,
|
|
.padding = .all(5),
|
|
},
|
|
}, quit_text.element());
|
|
defer container.deinit();
|
|
|
|
try container.append(try App.Container.init(allocator, .{
|
|
.rectangle = .{ .fill = .lightgrey },
|
|
.size = .{
|
|
.grow = .horizontal,
|
|
.dim = .{ .y = 10 },
|
|
},
|
|
}, input_field.element()));
|
|
|
|
const nested_container: App.Container = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .lightgrey },
|
|
}, spinner.element());
|
|
try container.append(nested_container);
|
|
|
|
try app.start();
|
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
|
|
|
var framerate: u64 = 60;
|
|
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
|
|
var next_frame_ms: u64 = 0;
|
|
|
|
// draw loop
|
|
draw: while (true) {
|
|
const now_ms: u64 = @intCast(time.milliTimestamp());
|
|
if (now_ms >= next_frame_ms) {
|
|
next_frame_ms = now_ms + tick_ms;
|
|
} else {
|
|
std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
|
|
next_frame_ms += tick_ms;
|
|
}
|
|
|
|
const len = blk: {
|
|
app.queue.lock();
|
|
defer app.queue.unlock();
|
|
break :blk app.queue.len();
|
|
};
|
|
|
|
// handle events
|
|
for (0..len) |_| {
|
|
const event = app.queue.drain() orelse break;
|
|
log.debug("handling event: {s}", .{@tagName(event)});
|
|
// pre event handling
|
|
switch (event) {
|
|
.key => |key| {
|
|
log.debug("key {any}", .{key});
|
|
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
|
},
|
|
.accept => |input| {
|
|
defer allocator.free(input);
|
|
var string = try allocator.alloc(u8, input.len);
|
|
defer allocator.free(string);
|
|
for (0.., input) |i, char| string[i] = @intCast(char);
|
|
log.debug("Accepted input '{s}'", .{string});
|
|
},
|
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
|
.focus => |b| {
|
|
// NOTE reduce framerate in case the window is not focused and restore again when focused
|
|
framerate = if (b) 60 else 15;
|
|
tick_ms = @divFloor(time.ms_per_s, framerate);
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
|
.err = .{
|
|
.err = err,
|
|
.msg = "Container Event handling failed",
|
|
},
|
|
});
|
|
|
|
// post event handling
|
|
switch (event) {
|
|
.quit => break :draw,
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
container.resize(&app.model, try renderer.resize());
|
|
container.reposition(&app.model, .{});
|
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
|
try renderer.flush();
|
|
}
|
|
}
|
|
|
|
pub const panic = App.panic_handler;
|
|
const log = std.log.scoped(.default);
|
|
|
|
const std = @import("std");
|
|
const time = std.time;
|
|
const assert = std.debug.assert;
|
|
const zterm = @import("zterm");
|
|
const App = zterm.App(struct {}, union(enum) {
|
|
accept: []u21,
|
|
});
|