ref(event): split Size into two Points (one for the size and one for the anchor / origin)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 39s
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 39s
This commit is contained in:
139
src/element.zig
139
src/element.zig
@@ -1,13 +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;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub fn Element(Event: type) type {
|
||||
return struct {
|
||||
@@ -16,11 +14,11 @@ pub fn Element(Event: type) type {
|
||||
|
||||
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,
|
||||
content: ?*const fn (ctx: *anyopaque, cells: []Cell, origin: Point, size: Point) 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`
|
||||
/// events or a system event, with the exception of the `.size`
|
||||
/// `Event` as every `Container` already handles that event.
|
||||
///
|
||||
/// In case of user errors this function should return an error. This
|
||||
@@ -32,23 +30,23 @@ pub fn Element(Event: type) type {
|
||||
}
|
||||
|
||||
/// Write content into the `cells` of the `Container`. The associated
|
||||
/// `cells` slice has the size of (`size.cols * size.rows`). The
|
||||
/// `cells` slice has the size of (`size.x * size.y`). The
|
||||
/// renderer will know where to place the contents on the screen.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
|
||||
/// ```zig
|
||||
/// std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
/// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
/// ```
|
||||
///
|
||||
/// - 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 {
|
||||
pub inline fn content(this: @This(), cells: []Cell, origin: Point, size: Point) !void {
|
||||
if (this.vtable.content) |content_fn|
|
||||
try content_fn(this.ptr, cells, size);
|
||||
try content_fn(this.ptr, cells, origin, size);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -57,16 +55,17 @@ 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 = .{},
|
||||
size: Point = .{},
|
||||
/// Minimal `Size` of the scrollable `Container` to be used. If the
|
||||
/// actual scrollable container's size is larger it will be used instead
|
||||
/// (no scrolling will be necessary for that screen size).
|
||||
min_size: Size = .{},
|
||||
min_size: Point = .{},
|
||||
/// `Size` of the `Container` content that is scrollable and mapped to
|
||||
/// the *size* of the `Scrollable` `Element`.
|
||||
container_size: Size = .{},
|
||||
container_size: Point = .{},
|
||||
container_origin: Point = .{},
|
||||
/// Anchor of the viewport of the scrollable `Container`.
|
||||
anchor: Position = .{},
|
||||
anchor: Point = .{},
|
||||
/// The actual `Container`, that is scrollable.
|
||||
container: Container(Event),
|
||||
|
||||
@@ -80,7 +79,7 @@ pub fn Scrollable(Event: type) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(container: Container(Event), min_size: Size) @This() {
|
||||
pub fn init(container: Container(Event), min_size: Point) @This() {
|
||||
return .{
|
||||
.container = container,
|
||||
.min_size = min_size,
|
||||
@@ -90,33 +89,33 @@ pub fn Scrollable(Event: type) type {
|
||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
.size => |size| {
|
||||
this.size = size;
|
||||
// TODO scrollbar space - depending on configuration and only if necessary?
|
||||
this.container_size = size.max(this.min_size);
|
||||
this.container_size.anchor = size.anchor;
|
||||
try this.container.handle(.{ .resize = this.container_size });
|
||||
this.container_origin = size; // TODO the size should be a provided origin
|
||||
try this.container.handle(.{ .size = this.container_size });
|
||||
},
|
||||
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
|
||||
.mouse => |mouse| switch (mouse.button) {
|
||||
Mouse.Button.wheel_up => if (this.container_size.rows > this.size.rows) {
|
||||
this.anchor.row -|= 1;
|
||||
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
|
||||
this.anchor.y -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_down => if (this.container_size.rows > this.size.rows) {
|
||||
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_down => if (this.container_size.y > this.size.y) {
|
||||
const max_origin_y = this.container_size.y -| this.size.y;
|
||||
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
|
||||
},
|
||||
Mouse.Button.wheel_left => if (this.container_size.cols > this.size.cols) {
|
||||
this.anchor.col -|= 1;
|
||||
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
|
||||
this.anchor.x -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_right => if (this.container_size.cols > this.size.cols) {
|
||||
const max_anchor_col = this.container_size.cols -| this.size.cols;
|
||||
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
|
||||
Mouse.Button.wheel_right => if (this.container_size.x > this.size.x) {
|
||||
const max_anchor_x = this.container_size.x -| this.size.x;
|
||||
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
|
||||
},
|
||||
else => try this.container.handle(.{
|
||||
.mouse = .{
|
||||
.col = mouse.col + this.anchor.col,
|
||||
.row = mouse.row + this.anchor.row,
|
||||
.x = mouse.x + this.anchor.x,
|
||||
.y = mouse.y + this.anchor.y,
|
||||
.button = mouse.button,
|
||||
.kind = mouse.kind,
|
||||
},
|
||||
@@ -126,48 +125,50 @@ pub fn Scrollable(Event: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_container(container: Container(Event), cells: []Cell, container_size: Size) !void {
|
||||
fn render_container(container: Container(Event), cells: []Cell, container_origin: Point, container_size: Point) !void {
|
||||
const size = container.size;
|
||||
const origin = container.origin;
|
||||
const contents = try container.contents();
|
||||
defer container.allocator.free(contents);
|
||||
|
||||
const anchor = (@as(usize, size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) +
|
||||
@as(usize, size.anchor.col -| container_size.anchor.col);
|
||||
const anchor = (@as(usize, origin.y -| container_origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x -| container_origin.x);
|
||||
|
||||
var idx: usize = 0;
|
||||
blk: for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
cells[anchor + (row * container_size.cols) + col] = contents[idx];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
for (container.elements.items) |child| try render_container(child, cells, size);
|
||||
for (container.elements.items) |child| try render_container(child, cells, origin, size);
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []Cell, origin: Point, size: Point) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
||||
_ = origin; // this should be used
|
||||
std.debug.assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
|
||||
|
||||
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_origin = this.container.origin;
|
||||
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.x) * @as(usize, container_size.y));
|
||||
{
|
||||
const container_cells_const = try this.container.contents();
|
||||
defer this.container.allocator.free(container_cells_const);
|
||||
std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
||||
std.debug.assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
|
||||
@memcpy(container_cells, container_cells_const);
|
||||
}
|
||||
|
||||
// FIX this is not resolving the rendering recursively! This means that the content is only shown for the first children
|
||||
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
|
||||
for (this.container.elements.items) |child| try render_container(child, container_cells, container_origin, container_size);
|
||||
|
||||
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
|
||||
const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
|
||||
// TODO render scrollbar according to configuration!
|
||||
for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
|
||||
for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
|
||||
}
|
||||
}
|
||||
this.container.allocator.free(container_cells);
|
||||
@@ -183,9 +184,9 @@ test "scrollable vertical" {
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
const allocator = std.testing.allocator;
|
||||
const size: Size = .{
|
||||
.rows = 20,
|
||||
.cols = 30,
|
||||
const size: Point = .{
|
||||
.x = 30,
|
||||
.y = 20,
|
||||
};
|
||||
|
||||
var box: Container(event.SystemEvent) = try .init(allocator, .{
|
||||
@@ -210,7 +211,7 @@ test "scrollable vertical" {
|
||||
}, .{}));
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .rows = size.rows + 15 });
|
||||
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .y = size.y + 15 });
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(allocator, .{
|
||||
.border = .{
|
||||
@@ -223,33 +224,33 @@ test "scrollable vertical" {
|
||||
var renderer: testing.Renderer = .init(allocator, size);
|
||||
defer renderer.deinit();
|
||||
|
||||
try container.handle(.{ .resize = size });
|
||||
try container.handle(.{ .size = size });
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
|
||||
|
||||
// scroll down 15 times (exactly to the end)
|
||||
for (0..15) |_| try container.handle(.{
|
||||
.mouse = .{
|
||||
.button = .wheel_down,
|
||||
.kind = .press,
|
||||
.col = 5,
|
||||
.row = 5,
|
||||
.x = 5,
|
||||
.y = 5,
|
||||
},
|
||||
});
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
||||
|
||||
// further scrolling down will not change anything
|
||||
try container.handle(.{
|
||||
.mouse = .{
|
||||
.button = .wheel_down,
|
||||
.kind = .press,
|
||||
.col = 5,
|
||||
.row = 5,
|
||||
.x = 5,
|
||||
.y = 5,
|
||||
},
|
||||
});
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
||||
}
|
||||
|
||||
test "scrollable horizontal" {
|
||||
@@ -257,9 +258,9 @@ test "scrollable horizontal" {
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
const allocator = std.testing.allocator;
|
||||
const size: Size = .{
|
||||
.rows = 20,
|
||||
.cols = 30,
|
||||
const size: Point = .{
|
||||
.x = 30,
|
||||
.y = 20,
|
||||
};
|
||||
|
||||
var box: Container(event.SystemEvent) = try .init(allocator, .{
|
||||
@@ -284,7 +285,7 @@ test "scrollable horizontal" {
|
||||
}, .{}));
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .cols = size.cols + 15 });
|
||||
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .x = size.x + 15 });
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(allocator, .{
|
||||
.border = .{
|
||||
@@ -297,31 +298,31 @@ test "scrollable horizontal" {
|
||||
var renderer: testing.Renderer = .init(allocator, size);
|
||||
defer renderer.deinit();
|
||||
|
||||
try container.handle(.{ .resize = size });
|
||||
try container.handle(.{ .size = size });
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
|
||||
|
||||
// scroll right 15 times (exactly to the end)
|
||||
for (0..15) |_| try container.handle(.{
|
||||
.mouse = .{
|
||||
.button = .wheel_right,
|
||||
.kind = .press,
|
||||
.col = 5,
|
||||
.row = 5,
|
||||
.x = 5,
|
||||
.y = 5,
|
||||
},
|
||||
});
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
||||
|
||||
// further scrolling right will not change anything
|
||||
try container.handle(.{
|
||||
.mouse = .{
|
||||
.button = .wheel_right,
|
||||
.kind = .press,
|
||||
.col = 5,
|
||||
.row = 5,
|
||||
.x = 5,
|
||||
.y = 5,
|
||||
},
|
||||
});
|
||||
try renderer.render(Container(event.SystemEvent), &container);
|
||||
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
||||
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user