From d4cc52082676778bd618b64c55c1b76fc48c8075 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 2 Nov 2025 16:05:53 +0100 Subject: [PATCH] fix(element/scrollable): display and user interaction Fix initial render to show scrollbar immediately if required. Show and hide scrollbar correctly when content size or terminal size changes. Add keybindings for scrolling similar to pager keybindings. --- README.md | 2 -- src/element.zig | 83 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9365c41..69fbccf 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ const zterm: *Dependency = b.dependency("zterm", .{ .target = target, .optimize = optimize, }); -// ... -exe.root_module.addImport("zterm", zterm.module("zterm")); ``` ### Documentation diff --git a/src/element.zig b/src/element.zig index 75c3af1..d96d5ad 100644 --- a/src/element.zig +++ b/src/element.zig @@ -267,8 +267,8 @@ pub fn Scrollable(Model: type, Event: type) type { this.size = size; if (this.configuration.scrollbar) { - if (this.container.properties.size.dim.x > this.size.x or this.container.size.x > this.size.x) this.configuration.y_axis = true; - if (this.container.properties.size.dim.y > this.size.y or this.container.size.y > this.size.y) this.configuration.x_axis = true; + this.configuration.y_axis = this.container.properties.size.dim.x > this.size.x or this.container.size.x > this.size.x; + this.configuration.x_axis = this.container.properties.size.dim.y > this.size.y or this.container.size.y > this.size.y; const min_size = this.container.element.minSize(size); this.container.resize(.{ @@ -295,7 +295,43 @@ pub fn Scrollable(Model: type, Event: type) type { fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { - // TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?) + .init => this.container.resize(this.container.element.minSize(this.size)), + // 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; @@ -1073,8 +1109,8 @@ test "scrollable vertical" { 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) - for (0..15) |_| try container.handle(&model, .{ + // 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, @@ -1082,6 +1118,9 @@ test "scrollable vertical" { .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); @@ -1096,6 +1135,40 @@ test "scrollable vertical" { }); 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" {