feat(scrollable): make Container scrollable through Element Scrollable
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m35s

This commit is contained in:
2025-02-19 20:32:26 +01:00
parent f55d71a7cb
commit 86b3e7d4ed
8 changed files with 191 additions and 69 deletions

View File

@@ -4,6 +4,7 @@ const code_point = @import("code_point");
const event = @import("event.zig");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
@@ -11,7 +12,6 @@ const isTaggedUnion = event.isTaggedUnion;
const Mouse = input.Mouse;
const Key = input.Key;
const Size = @import("size.zig").Size;
const Queue = @import("queue.zig").Queue;
const log = std.log.scoped(.app);
@@ -44,8 +44,9 @@ pub fn App(comptime E: type) type {
const element = @import("element.zig");
pub const Element = element.Element(Event);
pub const Scrollable = element.Scrollable(Event);
pub const Queue = queue.Queue(Event, 256);
queue: Queue(Event, 256),
queue: Queue,
thread: ?std.Thread,
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
@@ -175,6 +176,7 @@ pub fn App(comptime E: type) type {
while (true) {
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
// FIX: in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
const read_bytes = try terminal.read(buf[0..]);
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
// escape key presses

View File

@@ -225,9 +225,8 @@ pub const Layout = packed struct {
};
pub fn Container(comptime Event: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Container(comptime Event: type)`");
}
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
const Element = @import("element.zig").Element(Event);
return struct {
allocator: std.mem.Allocator,
@@ -243,6 +242,7 @@ pub fn Container(comptime Event: type) type {
border: Border = .{},
rectangle: Rectangle = .{},
layout: Layout = .{},
min_size: Size = .{},
};
pub fn init(
@@ -270,6 +270,24 @@ pub fn Container(comptime Event: type) type {
try this.elements.append(element);
}
pub fn minSize(this: @This()) Size {
var size: Size = .{};
const len: u16 = @truncate(this.elements.items.len);
if (len > 0) {
for (this.elements.items) |element| {
size = size.merge(element.minSize());
}
switch (this.properties.layout.direction) {
.horizontal => size.cols += this.properties.layout.gap * (len - 1),
.vertical => size.rows += this.properties.layout.gap * (len - 1),
}
}
return .{
.cols = @max(size.cols, this.properties.min_size.cols),
.rows = @max(size.rows, this.properties.min_size.rows),
};
}
pub fn handle(this: *@This(), event: Event) !void {
switch (event) {
.resize => |size| resize: {

View File

@@ -1,9 +1,11 @@
//! Interface for Element's which describe the contents of a `Container`.
const std = @import("std");
const s = @import("size.zig");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const Mouse = input.Mouse;
const Position = s.Position;
const Size = s.Size;
@@ -82,9 +84,12 @@ pub fn Scrollable(Event: type) type {
/// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen.
size: Size = .{},
/// Anchor
/// `Size` of the `Container` content that is scrollable and mapped to
/// the *size* of the `Scrollable` `Element`.
container_size: Size = .{},
/// Anchor of the viewport of the scrollable `Container`.
anchor: Position = .{},
/// The actual container, that is *scrollable*
/// 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,
@@ -104,24 +109,42 @@ pub fn Scrollable(Event: type) type {
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 });
const min_size = this.container.minSize();
this.container_size = .{
.anchor = size.anchor,
.cols = @max(min_size.cols, size.cols),
.rows = @max(min_size.rows, size.rows),
};
try this.container.handle(.{ .resize = this.container_size });
},
.mouse => |mouse| {
std.log.debug("mouse event detected in scrollable element {any}", .{mouse.in(this.size)});
try this.container.handle(.{
.mouse => |mouse| switch (mouse.button) {
Mouse.Button.wheel_up => if (this.vertical) {
this.anchor.row -|= 1;
},
Mouse.Button.wheel_down => if (this.vertical) {
const max_anchor_row = this.container_size.rows -| this.size.rows;
this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
},
Mouse.Button.wheel_left => if (this.horizontal) {
this.anchor.col -|= 1;
},
Mouse.Button.wheel_right => if (this.horizontal) {
const max_anchor_col = this.container_size.cols -| this.size.cols;
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
},
else => 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),
}
@@ -131,39 +154,41 @@ pub fn Scrollable(Event: type) type {
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_size = this.container.size;
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.cols) * @as(usize, container_size.rows));
{
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);
}
// TODO: render correct view port into the container_cells
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);
const anchor = (@as(usize, e_size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) + @as(usize, e_size.anchor.col -| container_size.anchor.col);
var idx: usize = 0;
blk: for (0..e_size.rows) |row| {
for (0..e_size.cols) |col| {
blk: for (0..container_size.rows) |row| {
for (0..container_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;
container_cells[anchor + (row * container_size.cols) + col].style = cell.style;
container_cells[anchor + (row * container_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;
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
for (0..size.rows) |row| {
for (0..size.cols) |col| {
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
}
}
this.container.allocator.free(container_cells);
}

View File

@@ -60,17 +60,34 @@ pub const Key = packed struct {
/// ```zig
/// switch (event) {
/// .quit => break,
/// .key => |key| {
/// // ctrl+c to quit
/// if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } }))
/// app.quit.set();
/// },
/// .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit.set(),
/// else => {},
/// }
/// ```
pub fn eql(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
}
/// Determine if the `Key` is an ascii character that can be printed to
/// the screen. This means that the code point of the `Key` is an ascii
/// character between 32 - 255 (with the exception of 127 = Delete) and no
/// modifiers (alt and/or ctrl) are used.
///
/// # Example
///
/// Get user input's from the .key event from the application event loop:
///
/// ```zig
/// switch (event) {
/// .key => |key| if (key.isAscii()) try this.input.append(key.cp),
/// else => {},
/// }
/// ```
pub fn isAscii(this: @This()) bool {
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
this.cp >= 128 and this.cp <= 255); // extended ascii codes
}
};
// codepoints for keys

View File

@@ -42,15 +42,13 @@ pub const HelloWorldText = packed struct {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.init => log.debug(".init event", .{}),
.key => |key| {
if (key.eql(.{ .cp = input.Space })) {
var next_color_idx = @intFromEnum(this.text_color);
next_color_idx += 1;
next_color_idx %= 17; // iterate over the first 16 colors (but exclude `.default` == 0)
if (next_color_idx == 0) next_color_idx += 1;
this.text_color = @enumFromInt(next_color_idx);
log.debug("Next color: {s}", .{@tagName(this.text_color)});
}
.key => |key| if (key.eql(.{ .cp = input.Space })) {
var next_color_idx = @intFromEnum(this.text_color);
next_color_idx += 1;
next_color_idx %= 17;
if (next_color_idx == @intFromEnum(zterm.Color.default)) next_color_idx += 1;
this.text_color = @enumFromInt(next_color_idx);
log.debug("Next color: {s}", .{@tagName(this.text_color)});
},
else => {},
}
@@ -84,6 +82,7 @@ pub fn main() !void {
.direction = .vertical,
.padding = .vertical(1),
},
.min_size = .{ .rows = 100 },
}, .{});
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },

View File

@@ -2,6 +2,13 @@ pub const Size = packed struct {
anchor: Position = .{},
cols: u16 = 0,
rows: u16 = 0,
pub fn merge(this: @This(), other: @This()) Size {
return .{
.cols = this.cols + other.cols,
.rows = this.rows + other.rows,
};
}
};
pub const Position = packed struct {