add(example/elements): distinct different scrollable containers
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s

This commit is contained in:
2025-02-21 15:58:42 +01:00
parent 44e92735cf
commit 16724f6a52
3 changed files with 51 additions and 60 deletions

View File

@@ -133,13 +133,18 @@ the primary use-case for myself to create this library in the first place.
- [x] Button - [x] Button
- [x] Text Input field - [x] Text Input field
- [ ] Popup-menu - [ ] Popup-menu
- [ ] Scrollable Content (i.e. show long text of an except of something and other smaller `Container`) - [x] Scrollable Content (i.e. show long text of an except of something and other smaller `Container`)
- [ ] min size - [x] min size
- [ ] mouse scrolling aware of mouse position (i.e. through multiple different scrollable `Container`) - [x] mouse scrolling aware of mouse position (i.e. through multiple different scrollable `Container`)
- [ ] Launch sub-applications (not inside of a `Container` but during the application workflow, like an editor) - [ ] Launch sub-applications (not inside of a `Container` but during the application workflow, like an editor)
- [ ] Styles - [ ] Styles
- [ ] Text styles - [ ] Text styles
- [ ] Color palette - [ ] Color palette
- [ ] Error Handling
- [ ] log and show error's without crashing the application
- [ ] show application error that will cause a crash
- [ ] Demo
- [ ] use another tui application to launch and come back to (showcase the interrupt behavior)
- [ ] Testability - [ ] Testability
- [ ] snapshot ability to safe current screen (from `Renderer`) to test against - [ ] snapshot ability to safe current screen (from `Renderer`) to test against
- [ ] try to integrate them into the library itself such that they also serve as examples on how to test - [ ] try to integrate them into the library itself such that they also serve as examples on how to test

View File

@@ -23,7 +23,7 @@ pub const HelloWorldText = packed struct {
// NOTE: error should only be returned here in case an in-recoverable exception has occurred // NOTE: error should only be returned here in case an in-recoverable exception has occurred
const row = size.rows / 2; const row = size.rows / 2;
const col = size.cols / 2 -| (text.len / 2); const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.rows) + col; const anchor = (row * size.cols) + col;
for (0.., text) |idx, char| { for (0.., text) |idx, char| {
cells[anchor + idx].style.fg = .black; cells[anchor + idx].style.fg = .black;
@@ -52,7 +52,7 @@ pub fn main() !void {
var element_wrapper: HelloWorldText = .{}; var element_wrapper: HelloWorldText = .{};
const element = element_wrapper.element(); const element = element_wrapper.element();
var box = try App.Container.init(allocator, .{ var top_box = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
.layout = .{ .layout = .{
.gap = 1, .gap = 1,
@@ -61,21 +61,36 @@ pub fn main() !void {
}, },
.min_size = .{ .rows = 50 }, .min_size = .{ .rows = 50 },
}, .{}); }, .{});
try box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
}, .{})); }, .{}));
try box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
}, element)); }, element));
try box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .light_green },
}, .{})); }, .{}));
defer box.deinit(); defer top_box.deinit();
var scrollable: App.Scrollable = .{ var bottom_box = try App.Container.init(allocator, .{
.container = box, .border = .{ .separator = .{ .enabled = true } },
.vertical = true, .rectangle = .{ .fill = .blue },
}; .layout = .{
.direction = .vertical,
.padding = .vertical(1),
},
.min_size = .{ .rows = 30 },
}, .{});
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, element));
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer bottom_box.deinit();
var container = try App.Container.init(allocator, .{ var container = try App.Container.init(allocator, .{
.border = .{ .border = .{
@@ -87,21 +102,17 @@ pub fn main() !void {
.layout = .{ .layout = .{
.gap = 2, .gap = 2,
.padding = .all(5), .padding = .all(5),
.direction = .horizontal, .direction = .vertical,
}, },
}, .{}); }, .{});
try container.append(try App.Container.init(allocator, .{}, scrollable.element())); defer container.deinit();
try container.append(try App.Container.init(allocator, .{ // place empty container containing the element of the scrollable Container.
.border = .{ var scrollable_top: App.Scrollable = .init(top_box);
.color = .light_blue, try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
.sides = .vertical,
}, var scrollable_bottom: App.Scrollable = .init(bottom_box);
}, .{})); try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start(); try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
@@ -112,37 +123,14 @@ pub fn main() !void {
log.debug("received event: {s}", .{@tagName(event)}); log.debug("received event: {s}", .{@tagName(event)});
switch (event) { switch (event) {
.init => { .init => continue,
try container.handle(event);
continue; // do not render
},
.quit => break, .quit => break,
.resize => |size| try renderer.resize(size), .resize => |size| try renderer.resize(size),
.key => |key| { .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
if (key.eql(.{ .cp = 'q' })) app.quit();
// corresponding element could even be changed from the 'outside' (not forced through the event system)
// event system however allows for cross element communication (i.e. broadcasting messages, etc.)
if (key.eql(.{ .cp = input.Escape })) element_wrapper.text_color = .black;
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
},
});
}
},
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {}, else => {},
} }
// NOTE: returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,

View File

@@ -91,10 +91,6 @@ pub fn Scrollable(Event: type) type {
anchor: Position = .{}, anchor: Position = .{},
/// The actual container, that is scrollable. /// The actual container, that is scrollable.
container: Container(Event), 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) { pub fn element(this: *@This()) Element(Event) {
return .{ return .{
@@ -106,11 +102,13 @@ pub fn Scrollable(Event: type) type {
}; };
} }
pub fn init(container: Container(Event)) @This() {
return .{ .container = container };
}
fn handle(ctx: *anyopaque, event: Event) !void { fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO: emit `.resize` event for the container to set the size for the scrollable `Container`
// - how would I determine the required or necessary `Size`?
.resize => |size| { .resize => |size| {
this.size = size; this.size = size;
// TODO: scrollbar space - depending on configuration and only if necessary? // TODO: scrollbar space - depending on configuration and only if necessary?
@@ -124,17 +122,17 @@ pub fn Scrollable(Event: type) type {
}, },
// TODO: other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?) // TODO: other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
.mouse => |mouse| switch (mouse.button) { .mouse => |mouse| switch (mouse.button) {
Mouse.Button.wheel_up => if (this.vertical) { Mouse.Button.wheel_up => if (this.container_size.rows > this.size.rows) {
this.anchor.row -|= 1; this.anchor.row -|= 1;
}, },
Mouse.Button.wheel_down => if (this.vertical) { Mouse.Button.wheel_down => if (this.container_size.rows > this.size.rows) {
const max_anchor_row = 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); this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
}, },
Mouse.Button.wheel_left => if (this.horizontal) { Mouse.Button.wheel_left => if (this.container_size.cols > this.size.cols) {
this.anchor.col -|= 1; this.anchor.col -|= 1;
}, },
Mouse.Button.wheel_right => if (this.horizontal) { Mouse.Button.wheel_right => if (this.container_size.cols > this.size.cols) {
const max_anchor_col = 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); this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
}, },