add(continous): example which provides a fixed render schedule
The other examples did rendering based on events, which this renderer does not. This makes these applications potentially not that efficient, but allows for consistent frame times that make animations, etc. possible. This example serves to show that you can use `zterm` for both types of render scheduling and even change between them without much efford.
This commit is contained in:
@@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void {
|
||||
const Examples = enum {
|
||||
all,
|
||||
demo,
|
||||
continous,
|
||||
// elements:
|
||||
alignment,
|
||||
button,
|
||||
@@ -54,6 +55,7 @@ pub fn build(b: *std.Build) void {
|
||||
.name = e.name,
|
||||
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
|
||||
.demo => "examples/demo.zig",
|
||||
.continous => "examples/continous.zig",
|
||||
// elements:
|
||||
.alignment => "examples/elements/alignment.zig",
|
||||
.button => "examples/elements/button.zig",
|
||||
|
||||
249
examples/continous.zig
Normal file
249
examples/continous.zig
Normal file
@@ -0,0 +1,249 @@
|
||||
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, 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 continous 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, 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 {
|
||||
input: std.ArrayList(u21),
|
||||
queue: *App.Queue,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
||||
return .{
|
||||
.input = .init(allocator),
|
||||
.queue = queue,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: @This()) void {
|
||||
this.input.deinit();
|
||||
}
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
if (key.isAscii()) try this.input.append(key.cp);
|
||||
|
||||
// TODO support arrow keys for navigation?
|
||||
// TODO support readline keybindings (i.e. ctrl-k, ctrl-u, ctrl-b, ctrl-f, etc. and the equivalent alt bindings)
|
||||
// create an own `Element` implementation from this
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
||||
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
|
||||
|
||||
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, 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.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
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 = .light_grey },
|
||||
.size = .{
|
||||
.grow = .horizontal,
|
||||
.dim = .{ .y = 10 },
|
||||
},
|
||||
}, input_field.element()));
|
||||
|
||||
const nested_container: App.Container = try .init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
}, 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 {
|
||||
time.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(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break :draw,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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(union(enum) {
|
||||
accept: []u21,
|
||||
});
|
||||
@@ -90,6 +90,23 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
assert(!this.isEmptyLH());
|
||||
}
|
||||
|
||||
pub fn lock(this: *QueueType) void {
|
||||
this.mutex.lock();
|
||||
}
|
||||
|
||||
pub fn unlock(this: *QueueType) void {
|
||||
this.mutex.unlock();
|
||||
}
|
||||
|
||||
/// Used to efficiently drain the queue
|
||||
pub fn drain(this: *QueueType) ?T {
|
||||
if (this.isEmptyLH()) return null;
|
||||
|
||||
const result = this.buf[this.mask(this.read_index)];
|
||||
this.read_index = this.mask2(this.read_index + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn isEmptyLH(this: QueueType) bool {
|
||||
return this.write_index == this.read_index;
|
||||
}
|
||||
@@ -114,7 +131,7 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
}
|
||||
|
||||
/// Returns the length
|
||||
fn len(this: QueueType) usize {
|
||||
pub fn len(this: QueueType) usize {
|
||||
const wrap_offset = 2 * this.buf.len *
|
||||
@intFromBool(this.write_index < this.read_index);
|
||||
const adjusted_write_index = this.write_index + wrap_offset;
|
||||
|
||||
Reference in New Issue
Block a user