All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
Pass through mouse events where they actually match the corresponding `Container`. Adopt mouse event accordingly in `Scrollable` `Element` trait when passing through the mouse event to the scrollable `Container`.
172 lines
7.3 KiB
Zig
172 lines
7.3 KiB
Zig
//! 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);
|
|
}
|
|
};
|
|
}
|