Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m26s
1777 lines
75 KiB
Zig
1777 lines
75 KiB
Zig
//! Interface for Element's which describe the contents of a `Container`.
|
|
|
|
// TODO following features need to be implemented here
|
|
// - support for dynamic layouts? it can already be done, but the way you do this is pretty annoying
|
|
// -> currently you need to swap out the entire `Container` accordingly before rendering
|
|
// -> this might be necessary for size depending layout options, etc.
|
|
// -> maybe this can be implemented / supported similarly as to how the scrollable `Element`'s are implemented
|
|
|
|
// FIX known issues:
|
|
// - hold fewer instances of the `Allocator`
|
|
|
|
pub fn Element(Model: type, Event: type) type {
|
|
return struct {
|
|
ptr: *anyopaque = undefined,
|
|
vtable: *const VTable = &.{},
|
|
|
|
pub const VTable = struct {
|
|
minSize: ?*const fn (ctx: *anyopaque, size: Point) Point = null,
|
|
resize: ?*const fn (ctx: *anyopaque, size: Point) void = null,
|
|
reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null,
|
|
handle: ?*const fn (ctx: *anyopaque, model: *Model, event: Event) anyerror!void = null,
|
|
content: ?*const fn (ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) anyerror!void = null,
|
|
};
|
|
|
|
/// Request the minimal size required for this `Element` the provided
|
|
/// available *size* can be used as a fallback. This function ensures
|
|
/// that the minimal size returned has at least the dimensions of the
|
|
/// available *size*.
|
|
///
|
|
/// If the associated `Container`'s size is known at compile time (and
|
|
/// does not change) the size can be directly provided through the
|
|
/// `.size` Property for the `Container` instead. In this case you
|
|
/// should not implement / use this function.
|
|
///
|
|
/// Currently only used for `Scrollable` elements, such that the
|
|
/// directly nested element in the `Scrollable`'s `Container` can
|
|
/// request a minimal size for its contents.
|
|
pub inline fn minSize(this: @This(), size: Point) Point {
|
|
if (this.vtable.minSize) |minSize_fn| {
|
|
const min_size = minSize_fn(this.ptr, size);
|
|
return .{
|
|
.x = @max(size.x, min_size.x),
|
|
.y = @max(size.y, min_size.y),
|
|
};
|
|
} else return size;
|
|
}
|
|
|
|
/// Resize the corresponding `Element` with the given *size*.
|
|
pub inline fn resize(this: @This(), size: Point) void {
|
|
if (this.vtable.resize) |resize_fn|
|
|
resize_fn(this.ptr, size);
|
|
}
|
|
|
|
/// Reposition the corresponding `Element` with the given *origin*.
|
|
pub inline fn reposition(this: @This(), origin: Point) void {
|
|
if (this.vtable.reposition) |reposition_fn|
|
|
reposition_fn(this.ptr, origin);
|
|
}
|
|
|
|
/// Handle the received event. The event is one of the user provided
|
|
/// events or a system event. The model can be updated through the
|
|
/// provided pointer.
|
|
///
|
|
/// 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 error to the user and is left intentionally up to the
|
|
/// implementation to decide.
|
|
pub inline fn handle(this: @This(), model: *Model, event: Event) !void {
|
|
if (this.vtable.handle) |handle_fn|
|
|
try handle_fn(this.ptr, model, event);
|
|
}
|
|
|
|
/// Write content into the `cells` of the `Container`. The associated
|
|
/// `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 the size usually by assertion:
|
|
/// ```zig
|
|
/// 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.
|
|
///
|
|
/// - The provided model may be used as a read-only reference to create
|
|
/// the contents for the `cells`. Changes should only be done through
|
|
/// the `handle` callback.
|
|
///
|
|
/// - When facing layout problems it might help to enable the
|
|
/// build option for debug rendering, which renders corresponding
|
|
/// placeholders.
|
|
/// - **e** for `Element`
|
|
/// - **c** for `Container`
|
|
/// - **s** for `Separator`
|
|
/// - **r** for `Rectangle`
|
|
pub inline fn content(this: @This(), model: *const Model, cells: []Cell, size: Point) !void {
|
|
if (this.vtable.content) |content_fn| {
|
|
try content_fn(this.ptr, model, cells, size);
|
|
|
|
// DEBUG render corresponding top left corner of this `Element` *red*
|
|
// - only rendered if the corresponding associated element renders contents into the `Container`
|
|
if (comptime build_options.debug) {
|
|
cells[0].style.fg = .red;
|
|
cells[0].style.bg = .black;
|
|
cells[0].cp = 'e'; // 'e' for *element*
|
|
}
|
|
}
|
|
}
|
|
};
|
|
} // Element(Model: type, Event: type)
|
|
|
|
pub fn Alignment(Model: type, Event: type) type {
|
|
return struct {
|
|
/// `Size` of the actual contents that should be aligned.
|
|
size: Point = .{},
|
|
/// Alignment Configuration to use for aligning the associated contents of this `Element`.
|
|
configuration: Configuration,
|
|
/// `Container` to render in alignment. It needs to use the sizing options accordingly to be effective.
|
|
container: Container(Model, Event),
|
|
|
|
/// Configuration for Alignment
|
|
pub const Configuration = packed struct {
|
|
h: Align = .start,
|
|
v: Align = .start,
|
|
|
|
/// Alignment Options for configuration for vertical and horizontal orientations
|
|
pub const Align = enum(u2) { start, center, end };
|
|
|
|
/// Configuration for vertical alignment
|
|
pub fn vertical(a: Align) @This() {
|
|
return .{ .v = a };
|
|
}
|
|
/// Configuration for horizontal alignment
|
|
pub fn horizontal(a: Align) @This() {
|
|
return .{ .h = a };
|
|
}
|
|
|
|
pub const center: @This() = .{ .v = .center, .h = .center };
|
|
};
|
|
|
|
pub fn init(container: Container(Model, Event), configuration: Configuration) @This() {
|
|
return .{
|
|
.container = container,
|
|
.configuration = configuration,
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.resize = resize,
|
|
.reposition = reposition,
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn resize(ctx: *anyopaque, size: Point) void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
this.size = size;
|
|
this.container.resize(size);
|
|
}
|
|
|
|
fn reposition(ctx: *anyopaque, anchor: Point) void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
var origin = anchor;
|
|
origin.x = switch (this.configuration.h) {
|
|
.start => origin.x,
|
|
.center => origin.x + (this.size.x / 2) -| (this.container.size.x / 2),
|
|
.end => this.size.x -| this.container.size.x,
|
|
};
|
|
origin.y = switch (this.configuration.v) {
|
|
.start => origin.y,
|
|
.center => origin.y + (this.size.y / 2) -| (this.container.size.y / 2),
|
|
.end => this.size.y -| this.container.size.y,
|
|
};
|
|
this.container.reposition(origin);
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
try this.container.handle(model, event);
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
const origin = this.container.origin;
|
|
const csize = this.container.size;
|
|
|
|
const container_cells = try this.container.content(model);
|
|
defer this.container.allocator.free(container_cells);
|
|
|
|
outer: for (0..csize.y) |row| {
|
|
inner: for (0..csize.x) |col| {
|
|
// do not read/write out of bounce
|
|
if (this.size.x < row) break :outer;
|
|
if (this.size.y < col) break :inner;
|
|
|
|
cells[((row + origin.y) * size.x) + col + origin.x] = container_cells[(row * csize.x) + col];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
} // Alignment(Model: type, Event: type)
|
|
|
|
pub fn Scrollable(Model: type, 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: Point = .{},
|
|
/// `Size` of the `Container` content that is scrollable and mapped to
|
|
/// the *size* of the `Scrollable` `Element`.
|
|
container_size: Point = .{},
|
|
/// Anchor of the viewport of the scrollable `Container`.
|
|
anchor: Point = .{},
|
|
/// The actual `Container`, that is scrollable.
|
|
container: Container(Model, Event),
|
|
/// Whether the scrollable contents should show a scroll bar or not.
|
|
/// The scroll bar will only be shown if required and enabled. With the
|
|
/// corresponding provided color if to be shown.
|
|
configuration: Configuration,
|
|
|
|
pub const Configuration = packed struct {
|
|
scrollbar: bool,
|
|
/// Primary color to be used for the scrollbar (and background if enabled)
|
|
color: Color = .default,
|
|
/// With a backgroud the `color` rendered with emphasis `.dim`
|
|
/// will be used to highlight the scrollbar from the background;
|
|
/// otherwise nothing is shown for the background
|
|
show_background: bool = false,
|
|
/// should the scrollbar be shown for the x-axis (horizontal)
|
|
x_axis: bool = false,
|
|
/// should the scrollbar be shown for the y-axis (vertical)
|
|
y_axis: bool = false,
|
|
|
|
pub const disabled: @This() = .{ .scrollbar = false };
|
|
|
|
pub fn enabled(color: Color, show_background: bool) @This() {
|
|
return .{
|
|
.scrollbar = true,
|
|
.color = color,
|
|
.show_background = show_background,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn init(container: Container(Model, Event), configuration: Configuration) @This() {
|
|
return .{
|
|
.container = container,
|
|
.configuration = configuration,
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.resize = resize,
|
|
.reposition = reposition,
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn resize(ctx: *anyopaque, size: Point) void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
const last_max_anchor_x = this.container_size.x -| this.size.x;
|
|
const last_max_anchor_y = this.container_size.y -| this.size.y;
|
|
|
|
// NOTE `container_size` denotes the `size` required for the container contents
|
|
var container_size = this.container.element.minSize(size);
|
|
if (this.configuration.scrollbar) {
|
|
this.configuration.y_axis = this.container.properties.size.dim.x > size.x or container_size.x > size.x;
|
|
this.configuration.x_axis = this.container.properties.size.dim.y > size.y or container_size.y > size.y;
|
|
// correct size dimensions due to space needed for the axis (if any)
|
|
container_size.x -= if (this.configuration.x_axis) 1 else 0;
|
|
container_size.y -= if (this.configuration.y_axis) 1 else 0;
|
|
}
|
|
|
|
const new_max_anchor_x = container_size.x -| size.x;
|
|
const new_max_anchor_y = container_size.y -| size.y;
|
|
|
|
// correct anchor if necessary
|
|
if (new_max_anchor_x < last_max_anchor_x and this.anchor.x > new_max_anchor_x) this.anchor.x = new_max_anchor_x;
|
|
if (new_max_anchor_y < last_max_anchor_y and this.anchor.y > new_max_anchor_y) this.anchor.y = new_max_anchor_y;
|
|
|
|
this.size = size;
|
|
this.container.resize(container_size); // notify the container about the minimal size it should have
|
|
this.container_size = container_size;
|
|
}
|
|
|
|
fn reposition(ctx: *anyopaque, _: Point) void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
this.container.reposition(.{});
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
// TODO what about multiple scrollable `Element` usages? mouse
|
|
// can differ between them (due to the event being only send to
|
|
// the `Container` / `Element` one under the cursor!)
|
|
.key => |key| {
|
|
if (key.eql(.{ .cp = 'j' }) or key.eql(.{ .cp = input.Down })) {
|
|
const max_anchor_y = this.container_size.y -| this.size.y;
|
|
this.anchor.y = @min(this.anchor.y + 1, max_anchor_y);
|
|
}
|
|
if (key.eql(.{ .cp = 'k' }) or key.eql(.{ .cp = input.Up })) {
|
|
this.anchor.y -|= 1;
|
|
}
|
|
if (key.eql(.{ .cp = 'd', .mod = .{ .ctrl = true } })) {
|
|
// half page down
|
|
const half_page = this.size.y / 2;
|
|
const max_anchor_y = this.container_size.y -| this.size.y;
|
|
this.anchor.y = @min(this.anchor.y + half_page, max_anchor_y);
|
|
}
|
|
if (key.eql(.{ .cp = 'u', .mod = .{ .ctrl = true } })) {
|
|
// half page up
|
|
const half_page = this.size.y / 2;
|
|
this.anchor.y -|= half_page;
|
|
}
|
|
if (key.eql(.{ .cp = 'f', .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.PageDown })) {
|
|
const max_anchor_y = this.container_size.y -| this.size.y;
|
|
this.anchor.y = @min(this.anchor.y + this.size.y, max_anchor_y);
|
|
}
|
|
if (key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.PageDown })) {
|
|
this.anchor.y -|= this.size.y;
|
|
}
|
|
if (key.eql(.{ .cp = 'g' }) or key.eql(.{ .cp = input.Home })) {
|
|
this.anchor.y = 0;
|
|
}
|
|
if (key.eql(.{ .cp = 'G' }) or key.eql(.{ .cp = input.End })) {
|
|
this.anchor.y = this.container_size.y -| this.size.y;
|
|
}
|
|
},
|
|
.mouse => |mouse| switch (mouse.button) {
|
|
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
|
|
this.anchor.y -|= 1;
|
|
},
|
|
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
|
|
const max_anchor_y = this.container_size.y -| this.size.y;
|
|
this.anchor.y = @min(this.anchor.y + 1, max_anchor_y);
|
|
},
|
|
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
|
|
this.anchor.x -|= 1;
|
|
},
|
|
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(model, .{
|
|
.mouse = .{
|
|
.x = mouse.x + this.anchor.x,
|
|
.y = mouse.y + this.anchor.y,
|
|
.button = mouse.button,
|
|
.kind = mouse.kind,
|
|
},
|
|
}),
|
|
},
|
|
else => try this.container.handle(model, event),
|
|
}
|
|
}
|
|
|
|
fn render_container(container: Container(Model, Event), model: *const Model, cells: []Cell, container_size: Point) !void {
|
|
const size = container.size;
|
|
const origin = container.origin;
|
|
const contents = try container.content(model);
|
|
|
|
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
|
|
|
|
var idx: usize = 0;
|
|
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;
|
|
}
|
|
}
|
|
// free immediately
|
|
container.allocator.free(contents);
|
|
|
|
for (container.elements.items) |child| try render_container(child, model, cells, size);
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
|
|
const offset_x: usize = if (this.configuration.x_axis) 1 else 0;
|
|
const offset_y: usize = if (this.configuration.y_axis) 1 else 0;
|
|
|
|
const container_size = this.container.size;
|
|
const container_cells = try this.container.content(model);
|
|
|
|
for (this.container.elements.items) |child| try render_container(child, model, container_cells, container_size);
|
|
|
|
const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
|
|
for (0..size.y - offset_y) |row| {
|
|
for (0..size.x - offset_x) |col| {
|
|
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
|
|
}
|
|
}
|
|
if (this.configuration.scrollbar) {
|
|
if (this.configuration.x_axis) {
|
|
const ratio: f32 = @as(f32, @floatFromInt(this.size.y)) / @as(f32, @floatFromInt(this.container.size.y));
|
|
const scrollbar_size: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.y)) * ratio);
|
|
const pos_ratio: f32 = @as(f32, @floatFromInt(this.anchor.y)) / @as(f32, @floatFromInt(this.container.size.y));
|
|
const scrollbar_starting_pos: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.y)) * pos_ratio);
|
|
for (0..size.y) |row| {
|
|
cells[(row * size.x) + size.x - 1] = .{
|
|
.style = .{
|
|
.fg = this.configuration.color,
|
|
.emphasis = if (row >= scrollbar_starting_pos and row <= scrollbar_starting_pos + scrollbar_size)
|
|
&.{} // scrollbar itself
|
|
else if (this.configuration.show_background)
|
|
&.{.dim} // background (around scrollbar)
|
|
else
|
|
&.{.hidden}, // hide background
|
|
},
|
|
.cp = '🮋', // 7/8 block right
|
|
};
|
|
}
|
|
}
|
|
if (this.configuration.y_axis) {
|
|
const ratio: f32 = @as(f32, @floatFromInt(this.size.x)) / @as(f32, @floatFromInt(this.container.size.x));
|
|
const scrollbar_size: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.x)) * ratio);
|
|
const pos_ratio: f32 = @as(f32, @floatFromInt(this.anchor.x)) / @as(f32, @floatFromInt(this.container.size.x));
|
|
const scrollbar_starting_pos: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.x)) * pos_ratio);
|
|
for (0..size.x) |col| {
|
|
cells[((size.y - 1) * size.x) + col] = .{
|
|
.style = .{
|
|
.fg = this.configuration.color,
|
|
.emphasis = if (col >= scrollbar_starting_pos and col <= scrollbar_starting_pos + scrollbar_size)
|
|
&.{} // scrollbar itself
|
|
else if (this.configuration.show_background)
|
|
&.{.dim} // background (around scrollbar)
|
|
else
|
|
&.{.hidden}, // hide background
|
|
},
|
|
.cp = '🮄', // 5/8 block top (to make it look more equivalent to the vertical scrollbar)
|
|
};
|
|
}
|
|
}
|
|
}
|
|
this.container.allocator.free(container_cells);
|
|
}
|
|
};
|
|
} // Scrollable(Model: type, Event: type)
|
|
|
|
// TODO features
|
|
// - clear input (with and without retaining of capacity) through an public api
|
|
// - make handle / content functions public
|
|
pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
|
|
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
|
|
const input_struct = struct {
|
|
pub fn input_fn(accept_event: meta.FieldEnum(Event)) type {
|
|
const event_type: enum { ascii, utf8 } = blk: { // check for type correctness and the associated type to use for the passed `accept_event`
|
|
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `[]u8` or `[]u21` are allowed.";
|
|
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
|
|
.pointer => |pointer| {
|
|
if (pointer.size != .slice) @compileError(err_msg);
|
|
switch (@typeInfo(pointer.child)) {
|
|
.int => |num| {
|
|
if (num.signedness != .unsigned) @compileError(err_msg);
|
|
switch (num.bits) {
|
|
8 => break :blk .ascii,
|
|
21 => break :blk .utf8,
|
|
else => @compileError(err_msg),
|
|
}
|
|
},
|
|
else => @compileError(err_msg),
|
|
}
|
|
},
|
|
else => @compileError(err_msg),
|
|
}
|
|
};
|
|
return struct {
|
|
allocator: std.mem.Allocator,
|
|
/// Offset from the end describing the current position of the cursor.
|
|
cursor_offset: usize = 0,
|
|
/// Configuration for the InputField.
|
|
configuration: Configuration,
|
|
/// Array holding the value of the input.
|
|
input: std.ArrayList(u21),
|
|
/// Reference to the app's queue to issue the associated event to trigger when completing the input.
|
|
queue: *Queue,
|
|
|
|
/// Configuration for InputField's.
|
|
pub const Configuration = packed struct {
|
|
color: Color,
|
|
|
|
pub fn init(color: Color) @This() {
|
|
return .{ .color = color };
|
|
}
|
|
};
|
|
|
|
pub fn init(allocator: std.mem.Allocator, queue: *Queue, configuration: Configuration) @This() {
|
|
return .{
|
|
.allocator = allocator,
|
|
.configuration = configuration,
|
|
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
|
|
.queue = queue,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(this: *@This()) void {
|
|
this.input.deinit(this.allocator);
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
.key => |key| {
|
|
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
|
|
|
|
// readline commands
|
|
if (key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = input.KpLeft }) or key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
|
|
if (this.cursor_offset < this.input.items.len) this.cursor_offset += 1;
|
|
}
|
|
|
|
if (key.eql(.{ .cp = input.Right }) or key.eql(.{ .cp = input.KpRight }) or key.eql(.{ .cp = 'f', .mod = .{ .ctrl = true } }))
|
|
this.cursor_offset -|= 1;
|
|
|
|
if (key.eql(.{ .cp = 'e', .mod = .{ .ctrl = true } })) this.cursor_offset = 0;
|
|
|
|
if (key.eql(.{ .cp = 'a', .mod = .{ .ctrl = true } })) this.cursor_offset = this.input.items.len;
|
|
|
|
if (key.eql(.{ .cp = 'k', .mod = .{ .ctrl = true } })) {
|
|
while (this.cursor_offset > 0) {
|
|
_ = this.input.pop();
|
|
this.cursor_offset -= 1;
|
|
}
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'u', .mod = .{ .ctrl = true } })) {
|
|
const len = this.input.items.len - this.cursor_offset;
|
|
for (0..len) |_| _ = this.input.orderedRemove(0);
|
|
this.cursor_offset = this.input.items.len;
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'w', .mod = .{ .ctrl = true } })) {
|
|
var non_whitespace = false;
|
|
while (this.cursor_offset < this.input.items.len) {
|
|
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break;
|
|
|
|
// see backspace
|
|
const removed = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
|
|
if (removed != ' ') non_whitespace = true;
|
|
}
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.KpLeft, .mod = .{ .ctrl = true } })) {
|
|
var non_whitespace = false;
|
|
while (this.cursor_offset < this.input.items.len) {
|
|
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break;
|
|
|
|
// see backspace
|
|
this.cursor_offset += 1;
|
|
if (this.cursor_offset == this.input.items.len) break;
|
|
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
|
|
if (next != ' ') non_whitespace = true;
|
|
}
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'f', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.KpRight, .mod = .{ .ctrl = true } })) {
|
|
var non_whitespace = false;
|
|
while (this.cursor_offset > 0) {
|
|
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') {
|
|
this.cursor_offset += 1; // correct cursor position back again to make sure the cursor is not on the whitespace, but at the end of the jumped word
|
|
break;
|
|
}
|
|
|
|
// see backspace
|
|
this.cursor_offset -= 1;
|
|
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
|
|
if (next != ' ') non_whitespace = true;
|
|
}
|
|
}
|
|
|
|
// usual input keys
|
|
if (key.isAscii()) try this.input.insert(this.allocator, this.input.items.len - this.cursor_offset, key.cp);
|
|
|
|
if (key.eql(.{ .cp = input.Backspace })) {
|
|
if (this.cursor_offset < this.input.items.len) {
|
|
_ = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
|
|
}
|
|
}
|
|
|
|
if (key.eql(.{ .cp = input.Delete }) or key.eql(.{ .cp = input.KpDelete })) {
|
|
if (this.cursor_offset > 0) {
|
|
_ = this.input.orderedRemove(this.input.items.len - this.cursor_offset);
|
|
this.cursor_offset -= 1;
|
|
}
|
|
}
|
|
|
|
// TODO enter to accept?
|
|
// - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box?
|
|
if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) {
|
|
switch (event_type) {
|
|
.ascii => {
|
|
// NOTE convert unicode characters to ascii characters; if non ascii characters are found this is will fail!
|
|
var slice = try this.allocator.alloc(u8, this.input.items.len);
|
|
for (0.., this.input.items) |i, c| slice[i] = @intCast(c);
|
|
this.input.clearAndFree(this.allocator);
|
|
this.queue.push(@unionInit(
|
|
Event,
|
|
@tagName(accept_event),
|
|
slice,
|
|
));
|
|
},
|
|
.utf8 => this.queue.push(@unionInit(
|
|
Event,
|
|
@tagName(accept_event),
|
|
try this.input.toOwnedSlice(this.allocator),
|
|
)),
|
|
}
|
|
this.cursor_offset = 0;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
const offset = if (this.input.items.len - this.cursor_offset + 1 >= cells.len) this.input.items.len + 1 - cells.len else 0;
|
|
for (this.input.items[offset..], 0..) |cp, idx| {
|
|
cells[idx].style.fg = this.configuration.color;
|
|
cells[idx].cp = cp;
|
|
// display ellipse at the beginning
|
|
if (offset > 0 and idx == 0) cells[idx].cp = '…';
|
|
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (idx == cells.len - 1) {
|
|
// display ellipse at the end
|
|
if (this.input.items.len >= cells.len and this.cursor_offset > 0) cells[idx].cp = '…';
|
|
break;
|
|
}
|
|
}
|
|
if (this.input.items.len < cells.len)
|
|
cells[this.input.items.len - this.cursor_offset].style.cursor = true
|
|
else
|
|
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return input_struct.input_fn;
|
|
} // Input(Model: type, Event: type, Queue: type)
|
|
|
|
pub fn Button(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
|
|
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
|
|
const button_struct = struct {
|
|
pub fn button_fn(accept_event: meta.FieldEnum(Event)) type {
|
|
{ // check for type correctness and the associated type to use for the passed `accept_event`
|
|
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `void` is allowed.";
|
|
// TODO supported nested tagged unions to be also be used for triggering if the enum is then still of `void`type!
|
|
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
|
|
.void => |_| {},
|
|
else => @compileError(err_msg),
|
|
}
|
|
}
|
|
return struct {
|
|
queue: *Queue,
|
|
configuration: Configuration,
|
|
|
|
/// Configuration for InputField's.
|
|
pub const Configuration = struct {
|
|
color: Color,
|
|
text: []const u8,
|
|
|
|
pub fn init(color: Color, text: []const u8) @This() {
|
|
return .{
|
|
.color = color,
|
|
.text = text,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn init(queue: *Queue, configuration: Configuration) @This() {
|
|
return .{
|
|
.queue = queue,
|
|
.configuration = configuration,
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
// TODO should this also support key presses to accept?
|
|
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(accept_event),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
// NOTE center text in the middle of the available cell slice
|
|
const row = size.y / 2 -| (this.configuration.text.len / 2);
|
|
const col = size.x / 2 -| (this.configuration.text.len / 2);
|
|
const anchor = (row * size.x) + col;
|
|
|
|
for (0.., this.configuration.text) |idx, cp| {
|
|
cells[anchor + idx].style.fg = this.configuration.color;
|
|
cells[anchor + idx].style.emphasis = &.{.bold};
|
|
cells[anchor + idx].cp = cp;
|
|
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (anchor + idx == cells.len - 1) break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return button_struct.button_fn;
|
|
} // Button(Model: type, Event: type, Queue: type)
|
|
|
|
pub fn RadioButton(Model: type, Event: type) type {
|
|
return struct {
|
|
configuration: Configuration,
|
|
value: bool,
|
|
|
|
pub const Configuration = struct {
|
|
// TODO support more user control for colors (i.e. background, foreground, checked, unchecked, etc.)
|
|
color: Color = .default,
|
|
style: enum(u1) {
|
|
squared,
|
|
rounded,
|
|
} = .rounded,
|
|
label: []const u8,
|
|
};
|
|
|
|
pub fn init(initial_value: bool, configuration: Configuration) @This() {
|
|
return .{
|
|
.value = initial_value,
|
|
.configuration = configuration,
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
|
var this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
// TODO should this also support key presses to accept?
|
|
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
|
|
this.value = !this.value;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
cells[0].cp = switch (this.configuration.style) {
|
|
.rounded => if (this.value) '●' else '○',
|
|
.squared => if (this.value) '■' else '□',
|
|
};
|
|
cells[0].style.fg = this.configuration.color;
|
|
|
|
for (2.., this.configuration.label) |idx, cp| {
|
|
cells[idx].style.fg = this.configuration.color;
|
|
cells[idx].cp = cp;
|
|
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (idx == cells.len - 1) break;
|
|
}
|
|
}
|
|
};
|
|
} // RadioButton(Model: type, Event: type)
|
|
|
|
pub fn Selection(Model: type, Event: type) fn (type) type {
|
|
const selection_struct = struct {
|
|
pub fn selection_fn(Enum: type) type {
|
|
switch (@typeInfo(Enum)) {
|
|
.@"enum" => |e| if (!e.is_exhaustive) @compileError("Selection's enum value needs to be exhaustive."),
|
|
else => @compileError("Selection's `Enum` type is not an `enum` type."),
|
|
}
|
|
return struct {
|
|
value: Enum,
|
|
configuration: Configuration,
|
|
width: u16,
|
|
|
|
pub const Configuration = struct {
|
|
label: []const u8,
|
|
};
|
|
|
|
pub fn init(configuration: Configuration) @This() {
|
|
var max_len: usize = 0;
|
|
inline for (std.meta.fields(Enum)) |field| max_len = @max(max_len, field.name.len);
|
|
return .{
|
|
.value = @enumFromInt(0),
|
|
.configuration = configuration,
|
|
.width = @intCast(max_len),
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
switch (event) {
|
|
.mouse => |mouse| if (mouse.y == 0) {
|
|
if (mouse.button == .left and mouse.kind == .release) {
|
|
// left button
|
|
if (mouse.x == this.configuration.label.len + 1) {
|
|
const next = if (@intFromEnum(this.value) > 0)
|
|
@intFromEnum(this.value) - 1
|
|
else
|
|
std.meta.fields(Enum).len - 1;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
|
|
// right button
|
|
if (mouse.x == this.configuration.label.len + 4 + this.width) {
|
|
const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1)
|
|
@intFromEnum(this.value) + 1
|
|
else
|
|
0;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
}
|
|
|
|
if (mouse.x > this.configuration.label.len and mouse.x < this.configuration.label.len + 4 + this.width) {
|
|
if (mouse.button == .wheel_down) {
|
|
const next = if (@intFromEnum(this.value) > 0)
|
|
@intFromEnum(this.value) - 1
|
|
else
|
|
std.meta.fields(Enum).len - 1;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
|
|
if (mouse.button == .wheel_up) {
|
|
const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1)
|
|
@intFromEnum(this.value) + 1
|
|
else
|
|
0;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
}
|
|
},
|
|
.key => |key| {
|
|
if (key.eql(.{ .cp = 'j' }) or key.eql(.{ .cp = input.Down }) or key.eql(.{ .cp = input.KpDown })) {
|
|
const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1)
|
|
@intFromEnum(this.value) + 1
|
|
else
|
|
0;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'k' }) or key.eql(.{ .cp = input.Up }) or key.eql(.{ .cp = input.KpUp })) {
|
|
const next = if (@intFromEnum(this.value) > 0)
|
|
@intFromEnum(this.value) - 1
|
|
else
|
|
std.meta.fields(Enum).len - 1;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'h' }) or key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = input.KpLeft })) {
|
|
const next = if (@intFromEnum(this.value) > 0)
|
|
@intFromEnum(this.value) - 1
|
|
else
|
|
std.meta.fields(Enum).len - 1;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
|
|
if (key.eql(.{ .cp = 'l' }) or key.eql(.{ .cp = input.Right }) or key.eql(.{ .cp = input.KpRight })) {
|
|
const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1)
|
|
@intFromEnum(this.value) + 1
|
|
else
|
|
0;
|
|
this.value = @enumFromInt(next);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
if (this.configuration.label.len + 4 + this.width >= cells.len) return Error.TooSmall;
|
|
|
|
for (0.., this.configuration.label) |i, c| {
|
|
if (i == cells.len - 1) break;
|
|
|
|
cells[i].cp = c;
|
|
}
|
|
cells[this.configuration.label.len + 1].cp = '◀';
|
|
|
|
const value = @tagName(this.value);
|
|
const offset = (this.width - value.len) / 2; // to center the value's text
|
|
for (this.configuration.label.len + 3 + offset.., value) |i, c| {
|
|
if (i == cells.len - 1) break;
|
|
|
|
cells[i].cp = c;
|
|
}
|
|
cells[this.configuration.label.len + 4 + this.width].cp = '▶';
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return selection_struct.selection_fn;
|
|
} // Selection(Model: type, Event: type)
|
|
|
|
pub fn Progress(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
|
|
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
|
|
const progress_struct = struct {
|
|
pub fn progress_fn(progress_event: meta.FieldEnum(Event)) type {
|
|
{ // check for type correctness and the associated type to use for the passed `progress_event`
|
|
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `u8` is allowed.";
|
|
switch (@typeInfo(@FieldType(Event, @tagName(progress_event)))) {
|
|
.int => |num| {
|
|
if (num.signedness != .unsigned) @compileError(err_msg);
|
|
switch (num.bits) {
|
|
8 => {},
|
|
else => @compileError(err_msg),
|
|
}
|
|
},
|
|
else => @compileError(err_msg),
|
|
}
|
|
}
|
|
return struct {
|
|
configuration: Configuration,
|
|
progress: u8 = 0,
|
|
queue: *Queue,
|
|
|
|
pub const Configuration = packed struct {
|
|
/// Control whether the percentage of the progress be rendered.
|
|
percent: packed struct {
|
|
enabled: bool = false,
|
|
alignment: enum(u2) { left, middle, right } = .middle,
|
|
} = .{},
|
|
/// Foreground color to use for the progress bar.
|
|
fg: Color,
|
|
/// Background color to use for the progress bar.
|
|
bg: Color,
|
|
/// Code point to use for rendering the progress bar with.
|
|
cp: u21 = '━',
|
|
};
|
|
|
|
pub fn init(queue: *Queue, configuration: Configuration) @This() {
|
|
return .{
|
|
.configuration = configuration,
|
|
.queue = queue,
|
|
};
|
|
}
|
|
|
|
pub fn element(this: *@This()) Element(Model, Event) {
|
|
return .{
|
|
.ptr = this,
|
|
.vtable = &.{
|
|
.handle = handle,
|
|
.content = content,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
// TODO should this `Element` trigger a completion event? (I don't think that this is useful?)
|
|
switch (event) {
|
|
progress_event => |value| {
|
|
assert(value >= 0 and value <= 100);
|
|
this.progress = value;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
|
|
|
const ratio: f32 = @as(f32, @floatFromInt(size.x)) / 100.0;
|
|
const scrollbar_end_pos: usize = @intFromFloat(ratio * @as(f32, @floatFromInt(this.progress)));
|
|
|
|
// NOTE remain `undefined` in case percentage rendering is not enabled
|
|
var percent_buf: [6]u8 = undefined;
|
|
var percent_buf_len: usize = undefined;
|
|
if (this.configuration.percent.enabled) percent_buf_len = (try std.fmt.bufPrint(&percent_buf, "[{d: >3}%]", .{this.progress})).len;
|
|
|
|
for (0..size.x) |idx| {
|
|
// NOTE the progress bar is rendered at the bottom row of the `Container` taking its entire columns
|
|
cells[((size.y - 1) * size.x) + idx] = .{
|
|
.style = .{
|
|
.fg = if (scrollbar_end_pos > 0 and idx <= scrollbar_end_pos) this.configuration.fg else this.configuration.bg,
|
|
.emphasis = &.{},
|
|
},
|
|
.cp = if (this.configuration.percent.enabled) switch (this.configuration.percent.alignment) {
|
|
.left => if (idx < percent_buf_len) percent_buf[idx] else this.configuration.cp,
|
|
.middle => if (idx >= ((size.x - percent_buf_len) / 2) and idx < ((size.x - percent_buf_len) / 2) + percent_buf_len) percent_buf[percent_buf_len - (((size.x - percent_buf_len) / 2) + percent_buf_len - idx - 1) - 1] else this.configuration.cp,
|
|
.right => if (idx >= size.x - percent_buf_len) percent_buf[percent_buf_len - ((size.x - idx + percent_buf_len - 1) % percent_buf_len) - 1] else this.configuration.cp,
|
|
} else this.configuration.cp,
|
|
};
|
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
|
if (idx == cells.len - 1) break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return progress_struct.progress_fn;
|
|
} // Progress(Model: type, Event: type, Queue: type)
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const meta = std.meta;
|
|
const build_options = @import("build_options");
|
|
const input = @import("input.zig");
|
|
const Container = @import("container.zig").Container;
|
|
const Cell = @import("cell.zig");
|
|
const Color = @import("color.zig").Color;
|
|
const Error = @import("error.zig").Error;
|
|
const Mouse = input.Mouse;
|
|
const Point = @import("point.zig").Point;
|
|
|
|
// TODO nested scrollable `Container`s?'
|
|
|
|
test "scrollable vertical" {
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
|
|
const allocator = std.testing.allocator;
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var box: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.sides = .all,
|
|
.color = .red,
|
|
},
|
|
.layout = .{
|
|
.separator = .{
|
|
.enabled = true,
|
|
.color = .red,
|
|
},
|
|
.direction = .vertical,
|
|
.padding = .all(1),
|
|
},
|
|
.size = .{
|
|
.dim = .{ .y = size.y + 15 },
|
|
},
|
|
}, .{});
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
defer box.deinit();
|
|
|
|
var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .disabled);
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.color = .green,
|
|
.sides = .vertical,
|
|
},
|
|
}, scrollable.element());
|
|
defer container.deinit();
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
|
|
|
|
// scroll down 15 times (exactly to the end) (with both mouse and key inputs)
|
|
for (0..7) |_| try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_down,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
for (7..15) |_| try container.handle(&model, .{
|
|
.key = .{ .cp = 'j' },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
|
|
|
// further scrolling down will not change anything
|
|
try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_down,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
|
|
|
// scrolling up and down again
|
|
for (0..5) |_| try container.handle(&model, .{
|
|
.key = .{ .cp = 'k' },
|
|
});
|
|
for (0..5) |_| try container.handle(&model, .{
|
|
.key = .{ .cp = 'j' },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
|
|
|
// scrolling half page up and down again
|
|
try container.handle(&model, .{
|
|
.key = .{ .cp = 'u', .mod = .{ .ctrl = true } },
|
|
});
|
|
try container.handle(&model, .{
|
|
.key = .{ .cp = 'd', .mod = .{ .ctrl = true } },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
|
|
|
// scrolling page up
|
|
try container.handle(&model, .{
|
|
.key = .{ .cp = 'b', .mod = .{ .ctrl = true } },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
|
|
|
|
// and down again
|
|
try container.handle(&model, .{
|
|
.key = .{ .cp = 'f', .mod = .{ .ctrl = true } },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
|
|
}
|
|
|
|
test "scrollable vertical with scrollbar" {
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
|
|
const allocator = std.testing.allocator;
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var box: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.sides = .all,
|
|
.color = .red,
|
|
},
|
|
.layout = .{
|
|
.separator = .{
|
|
.enabled = true,
|
|
.color = .red,
|
|
},
|
|
.direction = .vertical,
|
|
.padding = .all(1),
|
|
},
|
|
.size = .{
|
|
.dim = .{ .y = size.y + 15 },
|
|
},
|
|
}, .{});
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
defer box.deinit();
|
|
|
|
var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .enabled(.white, true));
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.color = .green,
|
|
.sides = .vertical,
|
|
},
|
|
}, scrollable.element());
|
|
defer container.deinit();
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen);
|
|
|
|
// scroll down 15 times (exactly to the end)
|
|
for (0..15) |_| try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_down,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
|
|
|
|
// further scrolling down will not change anything
|
|
try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_down,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
|
|
}
|
|
|
|
test "scrollable horizontal" {
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
|
|
const allocator = std.testing.allocator;
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var box: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.sides = .all,
|
|
.color = .red,
|
|
},
|
|
.layout = .{
|
|
.separator = .{
|
|
.enabled = true,
|
|
.color = .red,
|
|
},
|
|
.direction = .horizontal,
|
|
.padding = .all(1),
|
|
},
|
|
.size = .{
|
|
.dim = .{ .x = size.x + 15 },
|
|
},
|
|
}, .{});
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
defer box.deinit();
|
|
|
|
var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .disabled);
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.color = .green,
|
|
.sides = .horizontal,
|
|
},
|
|
}, scrollable.element());
|
|
defer container.deinit();
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
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(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_right,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
|
|
|
// further scrolling right will not change anything
|
|
try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_right,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
|
|
}
|
|
|
|
test "scrollable horizontal with scrollbar" {
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
|
|
const allocator = std.testing.allocator;
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var box: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.sides = .all,
|
|
.color = .red,
|
|
},
|
|
.layout = .{
|
|
.separator = .{
|
|
.enabled = true,
|
|
.color = .red,
|
|
},
|
|
.direction = .horizontal,
|
|
.padding = .all(1),
|
|
},
|
|
.size = .{
|
|
.dim = .{ .x = size.x + 15 },
|
|
},
|
|
}, .{});
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
try box.append(try .init(allocator, .{
|
|
.rectangle = .{ .fill = .grey },
|
|
}, .{}));
|
|
defer box.deinit();
|
|
|
|
var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .enabled(.white, true));
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.border = .{
|
|
.color = .green,
|
|
.sides = .horizontal,
|
|
},
|
|
}, scrollable.element());
|
|
defer container.deinit();
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen);
|
|
|
|
// scroll right 15 times (exactly to the end)
|
|
for (0..15) |_| try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_right,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
|
|
|
|
// further scrolling right will not change anything
|
|
try container.handle(&model, .{
|
|
.mouse = .{
|
|
.button = .wheel_right,
|
|
.kind = .press,
|
|
.x = 5,
|
|
.y = 5,
|
|
},
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
|
|
}
|
|
|
|
test "alignment center" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
const Model = struct {};
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 5 },
|
|
.grow = .fixed,
|
|
},
|
|
}, .{});
|
|
defer aligned_container.deinit();
|
|
|
|
var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .center);
|
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
|
|
|
try testing.expectContainerScreen(.{
|
|
.x = 30,
|
|
.y = 20,
|
|
}, @TypeOf(container), &container, Model, @import("test/element/alignment.center.zon"));
|
|
}
|
|
|
|
test "alignment left" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
const Model = struct {};
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 5 },
|
|
.grow = .fixed,
|
|
},
|
|
}, .{});
|
|
defer aligned_container.deinit();
|
|
|
|
var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
|
|
.h = .start,
|
|
.v = .center,
|
|
});
|
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
|
|
|
try testing.expectContainerScreen(.{
|
|
.x = 30,
|
|
.y = 20,
|
|
}, @TypeOf(container), &container, Model, @import("test/element/alignment.left.zon"));
|
|
}
|
|
|
|
test "alignment right" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
const Model = struct {};
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 5 },
|
|
.grow = .fixed,
|
|
},
|
|
}, .{});
|
|
defer aligned_container.deinit();
|
|
|
|
var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
|
|
.h = .end,
|
|
.v = .center,
|
|
});
|
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
|
|
|
try testing.expectContainerScreen(.{
|
|
.x = 30,
|
|
.y = 20,
|
|
}, @TypeOf(container), &container, Model, @import("test/element/alignment.right.zon"));
|
|
}
|
|
|
|
test "alignment top" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
const Model = struct {};
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 5 },
|
|
.grow = .fixed,
|
|
},
|
|
}, .{});
|
|
defer aligned_container.deinit();
|
|
|
|
var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
|
|
.h = .center,
|
|
.v = .start,
|
|
});
|
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
|
|
|
try testing.expectContainerScreen(.{
|
|
.x = 30,
|
|
.y = 20,
|
|
}, @TypeOf(container), &container, Model, @import("test/element/alignment.top.zon"));
|
|
}
|
|
|
|
test "alignment bottom" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const testing = @import("testing.zig");
|
|
const Model = struct {};
|
|
|
|
var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 5 },
|
|
.grow = .fixed,
|
|
},
|
|
}, .{});
|
|
defer aligned_container.deinit();
|
|
|
|
var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
|
|
.h = .center,
|
|
.v = .end,
|
|
});
|
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
|
|
|
try testing.expectContainerScreen(.{
|
|
.x = 30,
|
|
.y = 20,
|
|
}, @TypeOf(container), &container, Model, @import("test/element/alignment.bottom.zon"));
|
|
}
|
|
|
|
test "input element" {
|
|
// FIX correctly generate the `.zon` files for the cell equivalence test (see below)
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
|
|
accept: []u21,
|
|
});
|
|
const testing = @import("testing.zig");
|
|
const Queue = @import("queue.zig").Queue(Event, 256);
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var container: Container(Model, Event) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
var queue: Queue = .{};
|
|
|
|
var input_element: Input(Model, Event, Queue)(.accept) = .init(allocator, &queue, .init(.black));
|
|
defer input_element.deinit();
|
|
|
|
const input_container: Container(Model, Event) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .green },
|
|
.size = .{
|
|
.dim = .{ .x = 12, .y = 2 },
|
|
.grow = .fixed,
|
|
},
|
|
}, input_element.element());
|
|
|
|
try container.append(input_container);
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen);
|
|
|
|
// press 'a' 15 times
|
|
for (0..15) |_| try container.handle(&model, .{
|
|
.key = .{ .cp = 'a' },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen);
|
|
|
|
// press 'a' 15 times
|
|
for (0..15) |_| try container.handle(&model, .{
|
|
.key = .{ .cp = 'a' },
|
|
});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen);
|
|
|
|
// test the accepting of the `Element`
|
|
try container.handle(&model, .{
|
|
.key = .{ .cp = input.Enter },
|
|
});
|
|
const accept_event = queue.pop();
|
|
try std.testing.expectEqual(.accept, std.meta.activeTag(accept_event));
|
|
try std.testing.expectEqual(30, switch (accept_event) {
|
|
.accept => |input_content| input_content.len,
|
|
else => unreachable,
|
|
});
|
|
|
|
// free allocated resources
|
|
switch (accept_event) {
|
|
.accept => |slice| allocator.free(slice),
|
|
else => unreachable,
|
|
}
|
|
}
|
|
|
|
test "button" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
|
|
accept,
|
|
});
|
|
const testing = @import("testing.zig");
|
|
const Queue = @import("queue.zig").Queue(Event, 256);
|
|
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
var queue: Queue = .{};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var container: Container(Model, Event) = try .init(allocator, .{}, .{});
|
|
defer container.deinit();
|
|
|
|
var button: Button(Model, Event, Queue)(.accept) = .init(&queue, .init(.default, "Button"));
|
|
const button_container: Container(Model, Event) = try .init(allocator, .{
|
|
.rectangle = .{ .fill = .blue },
|
|
}, button.element());
|
|
try container.append(button_container);
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen);
|
|
|
|
// test the accepting of the `Element`
|
|
try container.handle(&model, .{
|
|
.mouse = .{
|
|
.x = 5,
|
|
.y = 3,
|
|
.button = .left,
|
|
.kind = .release,
|
|
},
|
|
});
|
|
try std.testing.expect(switch (queue.pop()) {
|
|
.accept => true,
|
|
else => false,
|
|
});
|
|
}
|
|
|
|
// TODO add test cases for `RadioButton` and `Selection`
|
|
|
|
test "progress" {
|
|
const allocator = std.testing.allocator;
|
|
const event = @import("event.zig");
|
|
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
|
|
progress: u8,
|
|
});
|
|
const testing = @import("testing.zig");
|
|
const Queue = @import("queue.zig").Queue(Event, 256);
|
|
|
|
const size: Point = .{
|
|
.x = 30,
|
|
.y = 20,
|
|
};
|
|
var queue: Queue = .{};
|
|
const Model = struct {};
|
|
var model: Model = .{};
|
|
|
|
var container: Container(Model, Event) = try .init(allocator, .{
|
|
.layout = .{
|
|
.padding = .all(1),
|
|
},
|
|
.rectangle = .{ .fill = .white },
|
|
}, .{});
|
|
defer container.deinit();
|
|
|
|
var progress: Progress(Model, Event, Queue)(.progress) = .init(&queue, .{
|
|
.percent = .{ .enabled = true },
|
|
.fg = .green,
|
|
.bg = .grey,
|
|
});
|
|
const progress_container: Container(Model, Event) = try .init(allocator, .{}, progress.element());
|
|
try container.append(progress_container);
|
|
|
|
var renderer: testing.Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen);
|
|
|
|
// test the progress of the `Element`
|
|
try container.handle(&model, .{
|
|
.progress = 25,
|
|
});
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen);
|
|
|
|
// test the progress of the `Element`
|
|
try container.handle(&model, .{
|
|
.progress = 50,
|
|
});
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen);
|
|
|
|
// test the progress of the `Element`
|
|
try container.handle(&model, .{
|
|
.progress = 75,
|
|
});
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen);
|
|
|
|
// test the progress of the `Element`
|
|
try container.handle(&model, .{
|
|
.progress = 100,
|
|
});
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_hundred.zon"), renderer.screen);
|
|
}
|