diff --git a/src/app.zig b/src/app.zig index a8e1dd3..dc37d5c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -38,7 +38,9 @@ pub fn App(comptime E: type) type { return struct { pub const Event = mergeTaggedUnions(event.SystemEvent, E); pub const Container = @import("container.zig").Container(Event); - pub const Element = @import("element.zig").Element(Event); + const element = @import("element.zig"); + pub const Element = element.Element(Event); + pub const Scrollable = element.Scrollable(Event); queue: Queue(Event, 256), thread: ?std.Thread, diff --git a/src/container.zig b/src/container.zig index 20d988c..6ccda5c 100644 --- a/src/container.zig +++ b/src/container.zig @@ -11,11 +11,14 @@ const log = std.log.scoped(.container); /// Border configuration struct pub const Border = packed struct { + // corners: pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; + // separator.line: pub const line: [2]u21 = .{ '│', '─' }; pub const dotted: [2]u21 = .{ '┆', '┄' }; pub const double: [2]u21 = .{ '║', '═' }; + /// Color to use for the border color: Color = .default, /// Configure the corner type to be used for the border @@ -277,6 +280,7 @@ pub fn Container(comptime Event: type) type { size.rows, }); this.size = size; + try this.element.handle(event); if (this.elements.items.len == 0) break :resize; @@ -397,6 +401,7 @@ pub fn Container(comptime Event: type) type { this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); this.properties.rectangle.contents(cells, this.size); + // NOTE: Layout has no contents to provide, hence no content method is called (or even exists in the `Layout` struct) try this.element.content(cells, this.size); diff --git a/src/element.zig b/src/element.zig index 9a2ce93..8443ab2 100644 --- a/src/element.zig +++ b/src/element.zig @@ -1,8 +1,11 @@ //! 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 Size = @import("size.zig").Size; +const Position = s.Position; +const Size = s.Size; pub fn Element(Event: type) type { return struct { @@ -45,6 +48,7 @@ pub fn Element(Event: type) type { /// /// 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) { @@ -58,13 +62,99 @@ pub fn Template(Event: type) type { } fn handle(ctx: *anyopaque, event: Event) !void { - _ = ctx; - _ = event; + const this: *@This() = @ptrCast(@alignCast(ctx)); + _ = this; + switch (event) { + else => {}, + } } fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void { - _ = ctx; + 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) { + // TODO: emit `.resize` event for the container to set the size for the scrollable `Container` + // - how would I determine the required or necessary `Size`? + .init => try this.container.handle(event), + .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 }); + }, + 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); } }; } diff --git a/src/main.zig b/src/main.zig index 7e3fbc7..70a1ab7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -77,19 +77,6 @@ pub fn main() !void { var element_wrapper: HelloWorldText = .{}; const element = element_wrapper.element(); - var container = try App.Container.init(allocator, .{ - .border = .{ - .separator = .{ - .enabled = true, - .line = .double, - }, - }, - .layout = .{ - .gap = 2, - .padding = .all(5), - .direction = .vertical, - }, - }, .{}); var box = try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .layout = .{ @@ -107,7 +94,28 @@ pub fn main() !void { try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, }, .{})); - try container.append(box); + defer box.deinit(); + + var scrollable: App.Scrollable = .{ + .container = box, + .vertical = true, + }; + + var container = try App.Container.init(allocator, .{ + .border = .{ + .separator = .{ + .enabled = true, + .line = .double, + }, + }, + .layout = .{ + .gap = 2, + .padding = .all(5), + .direction = .vertical, + }, + }, .{}); + try container.append(try App.Container.init(allocator, .{}, scrollable.element())); + try container.append(try App.Container.init(allocator, .{ .border = .{ .color = .light_blue, diff --git a/src/zterm.zig b/src/zterm.zig index 725b18c..03084b9 100644 --- a/src/zterm.zig +++ b/src/zterm.zig @@ -19,6 +19,7 @@ pub const Layout = container.Layout; pub const Cell = @import("cell.zig"); pub const Color = color.Color; pub const Key = @import("key.zig"); +pub const Position = size.Position; pub const Size = size.Size; pub const Style = @import("style.zig");