Files
zterm/examples/continous.zig
Yves Biener 76f708d9d7 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.
2025-05-30 23:02:56 +02:00

250 lines
7.8 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, 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,
});