//! Interface for Element's which describe the contents of a `Container`. const std = @import("std"); const s = @import("size.zig"); const Container = @import("container.zig").Container; const Cell = @import("cell.zig"); const Position = s.Position; const Size = s.Size; pub fn Element(Event: type) type { return struct { ptr: *anyopaque = undefined, vtable: *const VTable = &.{}, pub const VTable = struct { handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null, content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Size) anyerror!void = null, }; /// Handle the received event. The event is one of the user provided /// events or a system event, with the exception of the `.resize` /// `Event` as every `Container` already handles that event. /// /// In case of user errors this function should return an error. This /// error may then be used by the application to display information /// about the user error. pub inline fn handle(this: @This(), event: Event) !void { if (this.vtable.handle) |handle_fn| try handle_fn(this.ptr, event); } /// Write content into the `cells` of the `Container`. The associated /// `cells` slice has the size of (`size.cols * size.rows`). The /// renderer will know where to place the contents on the screen. /// /// This function should only fail with an error if the error is /// non-recoverable (i.e. an allocation error, system error, etc.). /// Otherwise user specific errors should be caught using the `handle` /// function before the rendering of the `Container` happens. pub inline fn content(this: @This(), cells: []Cell, size: Size) !void { if (this.vtable.content) |content_fn| try content_fn(this.ptr, cells, size); } }; } /// This is an empty template implementation for an Element type which `zterm` may provide. /// /// TODO: Should elements need to be composible with each other, such that they may build complexer outputs? /// - the goal would rather be to have re-usable parts of handlers and/or content functions which serve similar functionalities. /// - composible through empty `Container`'s? -> make sense for the `Scrollable` `Element` pub fn Template(Event: type) type { return packed struct { pub fn element(this: *@This()) Element(Event) { return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content, }, }; } fn handle(ctx: *anyopaque, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); _ = this; switch (event) { else => {}, } } fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); _ = this; } }; } pub fn Scrollable(Event: type) type { return struct { /// `Size` of the actual contents where the anchor and the size is /// representing the size and location on screen. size: Size = .{}, /// Anchor anchor: Position = .{}, /// The actual container, that is *scrollable* container: Container(Event), /// Enable horizontal scrolling. This also renders a scrollbar (along the bottom of the viewport). horizontal: bool = false, /// Enable vertical scrolling. This also renders a scrollbar (along the right of the viewport). vertical: bool = false, pub fn element(this: *@This()) Element(Event) { return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content, }, }; } fn handle(ctx: *anyopaque, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .init => try this.container.handle(event), // TODO: emit `.resize` event for the container to set the size for the scrollable `Container` // - how would I determine the required or necessary `Size`? .resize => |size| { this.size = size; // TODO: not just pass through the given size, but rather the size that is necessary for scrollable content try this.container.handle(.{ .resize = size }); }, .mouse => |mouse| { std.log.debug("mouse event detected in scrollable element {any}", .{mouse.in(this.size)}); try this.container.handle(.{ .mouse = .{ .col = mouse.col + this.anchor.col, .row = mouse.row + this.anchor.row, .button = mouse.button, .kind = mouse.kind, }, }); }, else => try this.container.handle(event), } } fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows)); const container_cells = try this.container.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)); @memset(cells, .{}); { const container_cells_const = try this.container.contents(); defer this.container.allocator.free(container_cells_const); const container_size = this.container.size; std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows)); @memcpy(container_cells, container_cells_const); } for (this.container.elements.items) |*e| { const e_size = e.size; const element_cells = try e.contents(); defer e.allocator.free(element_cells); const anchor = (@as(usize, e_size.anchor.row -| size.anchor.row) * @as(usize, size.cols)) + @as(usize, e_size.anchor.col -| size.anchor.col); var idx: usize = 0; blk: for (0..e_size.rows) |row| { for (0..e_size.cols) |col| { const cell = element_cells[idx]; idx += 1; container_cells[anchor + (row * size.cols) + col].style = cell.style; container_cells[anchor + (row * size.cols) + col].cp = cell.cp; if (element_cells.len == idx) break :blk; } } } const anchor = (@as(usize, this.anchor.row) * @as(usize, size.cols)) + @as(usize, this.anchor.col); for (container_cells, 0..) |cell, idx| { cells[anchor + idx] = cell; } this.container.allocator.free(container_cells); } }; }