diff --git a/README.md b/README.md index caefcf5..fd551ca 100644 --- a/README.md +++ b/README.md @@ -133,13 +133,18 @@ the primary use-case for myself to create this library in the first place. - [x] Button - [x] Text Input field - [ ] Popup-menu - - [ ] Scrollable Content (i.e. show long text of an except of something and other smaller `Container`) - - [ ] min size - - [ ] mouse scrolling aware of mouse position (i.e. through multiple different scrollable `Container`) + - [x] Scrollable Content (i.e. show long text of an except of something and other smaller `Container`) + - [x] min size + - [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) - [ ] Styles - [ ] Text styles - [ ] 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 - [ ] 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 diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index cc5276f..17f0977 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -23,7 +23,7 @@ pub const HelloWorldText = packed struct { // NOTE: error should only be returned here in case an in-recoverable exception has occurred const row = size.rows / 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| { cells[anchor + idx].style.fg = .black; @@ -52,7 +52,7 @@ pub fn main() !void { var element_wrapper: HelloWorldText = .{}; const element = element_wrapper.element(); - var box = try App.Container.init(allocator, .{ + var top_box = try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .layout = .{ .gap = 1, @@ -61,21 +61,36 @@ pub fn main() !void { }, .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 }, }, .{})); - try box.append(try App.Container.init(allocator, .{ + try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, }, element)); - try box.append(try App.Container.init(allocator, .{ + try top_box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, }, .{})); - defer box.deinit(); + defer top_box.deinit(); - var scrollable: App.Scrollable = .{ - .container = box, - .vertical = true, - }; + var bottom_box = try App.Container.init(allocator, .{ + .border = .{ .separator = .{ .enabled = 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, .{ .border = .{ @@ -87,21 +102,17 @@ pub fn main() !void { .layout = .{ .gap = 2, .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, .{ - .border = .{ - .color = .light_blue, - .sides = .vertical, - }, - }, .{})); - try container.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .blue }, - }, .{})); - defer container.deinit(); // also de-initializes the children + // place empty container containing the element of the scrollable Container. + var scrollable_top: App.Scrollable = .init(top_box); + try container.append(try App.Container.init(allocator, .{}, scrollable_top.element())); + + var scrollable_bottom: App.Scrollable = .init(bottom_box); + try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element())); try app.start(); 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)}); switch (event) { - .init => { - try container.handle(event); - continue; // do not render - }, + .init => continue, .quit => break, .resize => |size| try renderer.resize(size), - .key => |key| { - 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 + .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, } - // NOTE: returned errors should be propagated back to the application container.handle(event) catch |err| app.postEvent(.{ .err = .{ .err = err, diff --git a/src/element.zig b/src/element.zig index d60be3d..0cc992b 100644 --- a/src/element.zig +++ b/src/element.zig @@ -91,10 +91,6 @@ pub fn Scrollable(Event: type) type { anchor: Position = .{}, /// The actual container, that is scrollable. container: Container(Event), - /// Enable horizontal scrolling. This also renders a scrollbar (along the bottom of the viewport). - horizontal: bool = false, - /// Enable vertical scrolling. This also renders a scrollbar (along the right of the viewport). - vertical: bool = false, pub fn element(this: *@This()) Element(Event) { return .{ @@ -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 { const this: *@This() = @ptrCast(@alignCast(ctx)); 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| { this.size = size; // 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.?) .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; }, - 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; 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; }, - 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; this.anchor.col = @min(this.anchor.col + 1, max_anchor_col); },