diff --git a/build.zig b/build.zig index 24d00e5..d26a6a2 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,7 @@ pub fn build(b: *std.Build) void { alignment, button, input, + popup, progress, scrollable, // layouts: @@ -61,6 +62,7 @@ pub fn build(b: *std.Build) void { .alignment => "examples/elements/alignment.zig", .button => "examples/elements/button.zig", .input => "examples/elements/input.zig", + .popup => "examples/elements/popup.zig", .progress => "examples/elements/progress.zig", .scrollable => "examples/elements/scrollable.zig", // layouts: diff --git a/examples/elements/popup.zig b/examples/elements/popup.zig new file mode 100644 index 0000000..2d23fbf --- /dev/null +++ b/examples/elements/popup.zig @@ -0,0 +1,247 @@ +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; + } + } +}; + +const MouseDraw = struct { + position: ?zterm.Point = null, + + 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) { + .mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y }, + else => this.position = null, + } + } + + fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + const this: *@This() = @ptrCast(@alignCast(ctx)); + + if (this.position) |pos| { + const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x); + cells[idx].cp = 'x'; + cells[idx].style.fg = .red; + } + } +}; + +const Popup = struct { + container: ?*App.Container = null, + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .resize = resize, + .reposition = reposition, + .handle = handle, + .content = content, + }, + }; + } + + fn resize(ctx: *anyopaque, size: zterm.Point) void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + if (this.container) |container| container.resize(size); + } + + fn reposition(ctx: *anyopaque, _: zterm.Point) void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + if (this.container) |container| container.reposition(.{}); + } + + fn handle(ctx: *anyopaque, event: App.Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + switch (event) { + // TODO should the `Element` handle the pop_down element triggering (i.e. by defining a usual key for this?) + .pop_up => |optional| if (optional) |container| { + this.container = @ptrCast(@alignCast(container)); + } else { + this.container = null; + }, + else => if (this.container) |container| try container.handle(event), + } + } + + fn render_container(container: App.Container, cells: []zterm.Cell, container_size: zterm.Point) !void { + const size = container.size; + const origin = container.origin; + const contents = try container.content(); + + const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x); + + var idx: usize = 0; + blk: for (0..size.y) |row| { + for (0..size.x) |col| { + cells[anchor + (row * container_size.x) + col] = contents[idx]; + idx += 1; + + if (contents.len == idx) break :blk; + } + } + // free immediately + container.allocator.free(contents); + + for (container.elements.items) |child| try render_container(child, cells, size); + } + + 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.container) |container| { + assert(cells.len == @as(usize, container.size.x) * @as(usize, container.size.y)); + const popup_cells = try container.content(); + + for (container.elements.items) |child| try render_container(child, popup_cells, size); + + assert(cells.len == popup_cells.len); + @memcpy(cells, popup_cells); + + container.allocator.free(popup_cells); + } + } +}; + +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 app: App = .init; + var renderer = zterm.Renderer.Buffered.init(allocator); + defer renderer.deinit(); + + var popup: Popup = .{}; + var quit_text: QuitText = .{}; + + // TODO + // - rendering of first Container contents will be overwritten by contents of the appended Container's (see the missing blue rectangle) + // - when rendering "nothing" it causes the below Container to be overwritten to "nothing" too! + // - not sure how this should be done? + // - provide the pop-up with a area where to draw? (i.e. somewhere to draw the provided `Container`) + // - This however will not suffice as the contents will be overwritten! + + var container = try App.Container.init(allocator, .{ + .rectangle = .{ + .fill = .blue, + }, + }, .{}); + defer container.deinit(); + + var popup_root_container = try App.Container.init(allocator, .{ + .layout = .{ + .padding = .{ + .top = -17, + .left = -40, + .right = 5, + .bottom = 5, + }, + }, + }, .{}); + try popup_root_container.append(try App.Container.init(allocator, .{}, popup.element())); + + try container.append(try .init(allocator, .{}, quit_text.element())); + try container.append(popup_root_container); // FIXME it should not be appended (as it would become part of the layout) + + var mouse: MouseDraw = .{}; + var popup_container: App.Container = try .init(allocator, .{ + .rectangle = .{ .fill = .green }, + .layout = .{ + .padding = .{ + .top = -4, + .bottom = 1, + .left = 3, + .right = 3, + }, + }, + }, .{}); + // showcase that inner `Container`s handle `Element`s accordingly + try popup_container.append(try .init(allocator, .{ + .rectangle = .{ .fill = .grey }, + }, mouse.element())); + defer popup_container.deinit(); + + try app.start(); + defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); + + // event loop + while (true) { + const event = app.nextEvent(); + log.debug("received event: {s}", .{@tagName(event)}); + + // pre event handling + switch (event) { + .key => |key| { + if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); + if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .pop_up = &popup_container }); + if (key.eql(.{ .cp = zterm.input.Escape })) app.postEvent(.{ .pop_up = null }); + }, + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), + else => {}, + } + + container.handle(event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + + // post event handling + switch (event) { + .quit => break, + 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 assert = std.debug.assert; +const zterm = @import("zterm"); +const App = zterm.App(union(enum) { + pop_up: ?*anyopaque, +});