WIP: use viewport to allow sizes of scroll to extend further than renderable screen

This commit is contained in:
2025-02-12 22:33:03 +01:00
parent 98031dbd1a
commit bbe6f4741e
9 changed files with 119 additions and 51 deletions

View File

@@ -31,17 +31,19 @@ pub fn main() !void {
.separator = .{ .enabled = false },
},
.layout = .{
// .gap = 1,
.sizing = .{
.width = .{ .percent = 50 },
.height = .{ .percent = 50 },
},
.gap = 1,
.padding = .all(5),
.direction = .horizontal,
},
});
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.layout = .{
.sizing = .{
.width = .{ .fixed = 70 },
.height = .{ .fixed = 18 },
},
},
}));
try container.append(try App.Container.init(allocator, .{
.border = .{ .color = .light_blue, .corners = .squared },

View File

@@ -7,7 +7,7 @@ const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Key = @import("key.zig");
const Size = @import("size.zig");
const Size = @import("size.zig").Size;
const Queue = @import("queue.zig").Queue;
const log = std.log.scoped(.app);

View File

@@ -4,6 +4,7 @@ const Style = @import("style.zig");
pub const Cell = @This();
style: Style = .{ .attributes = &.{} },
// TODO: embrace `zg` dependency more due to utf-8 encoding
cp: u21 = ' ',
pub fn eql(this: Cell, other: Cell) bool {

View File

@@ -4,13 +4,13 @@ const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Size = @import("size.zig");
const Size = @import("size.zig").Size;
const Style = @import("style.zig");
const log = std.log.scoped(.container);
/// Border configuration struct
pub const Border = struct {
pub const Border = packed struct {
pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
/// Color to use for the border
@@ -40,10 +40,10 @@ pub const Border = struct {
}
} = .{},
/// Configure separator borders between child element to added to the layout
separator: struct {
separator: packed struct {
enabled: bool = false,
color: Color = .white,
line: enum {
line: enum(u1) {
line,
dotted,
// TODO: add more variations which could be used for the separator
@@ -52,7 +52,7 @@ pub const Border = struct {
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size, layout: Layout, len: u16) void {
std.debug.assert(cells.len == size.cols * size.rows);
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
const frame = switch (this.corners) {
.rounded => Border.rounded_border,
@@ -62,7 +62,7 @@ pub const Border = struct {
// render top and bottom border
for (0..size.cols) |col| {
const last_row = (size.rows - 1) * size.cols;
const last_row = @as(usize, size.rows - 1) * @as(usize, size.cols);
if (this.sides.left and col == 0) {
// top left corner
if (this.sides.top) cells[col].cp = frame[0];
@@ -170,7 +170,7 @@ pub const Border = struct {
};
/// Rectangle configuration struct
pub const Rectangle = struct {
pub const Rectangle = packed struct {
/// `Color` to use to fill the `Rectangle` with
/// NOTE: used as background color when rendering! such that it renders the
/// children accordingly without removing the coloring of the `Rectangle`
@@ -178,7 +178,7 @@ pub const Rectangle = struct {
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size) void {
std.debug.assert(cells.len == size.cols * size.rows);
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
for (0..size.rows) |row| {
for (0..size.cols) |col| {
@@ -200,8 +200,21 @@ pub const Scroll = packed struct {
// TODO: rendering enhancements:
// - render corresponding scroll-bars?
// TODO: does the `size` then actually need to be part of the `Scroll`, as the user should not tamper with the value anyway..
// TODO: should the container size be 'virtual' i.e. the complete
// - content size (which might be larger than the available screen)
// but how would a scrollable small portion of the screen work?
// -> this means that the size can remain and have its meaning, but the
// content needs another abstraction for the scroll
size: Size = .{},
// anchor => position in view of scrollable contents
// cols / rows => view port dimensions
// render => window in window
};
// TODO: can `Layout` become `packed`? -> for that the sizing cannot be a tagged enum!
/// Layout configuration struct
pub const Layout = struct {
/// control the direction in which child elements are laid out
@@ -240,8 +253,8 @@ pub const Layout = struct {
// NOTE: `sizing` cannot be *packed* because of the tagged unions? is this necessary -> I would need to measure the size differences
/// Sizing to be used for the width and height of this element to use
sizing: struct {
width: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit,
height: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit,
width: union(enum(u2)) { fit, grow, fixed: u16, percent: u16 } = .fit,
height: union(enum(u2)) { fit, grow, fixed: u16, percent: u16 } = .fit,
} = .{},
};
@@ -251,7 +264,8 @@ pub fn Container(comptime Event: type) type {
}
return struct {
allocator: std.mem.Allocator,
size: Size,
/// Size of actual columns and rows used to render this `Container`
viewport: Size,
properties: Properties,
elements: std.ArrayList(@This()),
@@ -268,7 +282,7 @@ pub fn Container(comptime Event: type) type {
pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() {
return .{
.allocator = allocator,
.size = .{ .cols = 0, .rows = 0 },
.viewport = .{},
.properties = properties,
.elements = std.ArrayList(@This()).init(allocator),
};
@@ -289,8 +303,16 @@ pub fn Container(comptime Event: type) type {
switch (event) {
.init => log.debug(".init event", .{}),
.resize => |s| resize: {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
s.anchor.col,
s.anchor.row,
s.cols,
s.rows,
});
this.viewport = s;
// sizing
var size = s;
size.anchor = .{}; // reset relative anchor of size!
const sizing = this.properties.layout.sizing;
switch (sizing.width) {
.fit => {
@@ -301,7 +323,7 @@ pub fn Container(comptime Event: type) type {
// NOTE: this is pretty much the current implementation
},
.fixed => |fix| {
std.debug.assert(fix <= size.cols);
// NOTE: fixed may now even define a larger column / row span for a container (but not everything might be rendered)
size.cols = fix;
},
.percent => |percent| {
@@ -312,13 +334,16 @@ pub fn Container(comptime Event: type) type {
switch (sizing.height) {
.fit => {
// use as much space as necessary (but nothing more than necessary)
// TODO: I need to work out the representation between the
// - `Size` and `Size.Position` with the actual screen size
// - the virtual screen may be larger than the actual screen (can I do this?)
},
.grow => {
// grow use as much space as available by the parent (i.e. the entire width)
// NOTE: this is pretty much the current implementation
},
.fixed => |fix| {
std.debug.assert(fix <= size.rows);
// NOTE: fixed may now even define a larger column / row span for a container (but not everything might be rendered)
size.rows = fix;
},
.percent => |percent| {
@@ -326,14 +351,7 @@ pub fn Container(comptime Event: type) type {
size.rows = @divTrunc(size.rows * percent, 100);
},
}
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
this.size = size;
this.properties.scroll.size = size;
if (this.elements.items.len == 0) break :resize;
@@ -446,10 +464,51 @@ pub fn Container(comptime Event: type) type {
}
pub fn contents(this: *const @This()) ![]const Cell {
const cells = try this.allocator.alloc(Cell, this.size.cols * this.size.rows);
@memset(cells, .{}); // reset all cells
this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len));
this.properties.rectangle.contents(cells, this.size);
const content = try this.allocator.alloc(Cell, @as(usize, this.properties.scroll.size.cols) * @as(usize, this.properties.scroll.size.rows));
defer this.allocator.free(content);
@memset(content, .{});
const cells = try this.allocator.alloc(Cell, @as(usize, this.viewport.cols) * @as(usize, this.viewport.rows));
@memset(cells, .{});
this.properties.border.contents(content, this.properties.scroll.size, this.properties.layout, @truncate(this.elements.items.len));
this.properties.rectangle.contents(content, this.properties.scroll.size);
const cols = blk: {
var cols: u16 = this.properties.scroll.size.cols;
if (this.properties.scroll.size.cols > this.viewport.cols) {
cols = this.viewport.cols;
}
break :blk cols;
};
const rows = blk: {
var rows: u16 = this.properties.scroll.size.rows;
if (this.properties.scroll.size.rows > this.viewport.rows) {
rows = this.viewport.rows;
}
break :blk rows;
};
const anchor = (this.properties.scroll.size.anchor.row * this.properties.scroll.size.cols) + this.properties.scroll.size.anchor.col;
log.debug("viewport = .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
this.viewport.anchor.col,
this.viewport.anchor.row,
this.viewport.cols,
this.viewport.rows,
});
log.debug("size = .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
this.properties.scroll.size.anchor.col,
this.properties.scroll.size.anchor.row,
this.properties.scroll.size.cols,
this.properties.scroll.size.rows,
});
for (0..rows) |row| {
for (0..cols) |col| {
// TODO: try to do this with @memcpy instead to improve performance
const cell = content[anchor + (row * this.properties.scroll.size.cols) + col];
cells[row * this.viewport.cols + col].cp = cell.cp;
cells[row * this.viewport.cols + col].style = cell.style;
}
}
return cells;
}
};

View File

@@ -3,7 +3,7 @@
const std = @import("std");
const terminal = @import("terminal.zig");
const Size = @import("size.zig");
const Size = @import("size.zig").Size;
const Key = @import("key.zig");
/// System events available to every `zterm.App`

View File

@@ -2,8 +2,8 @@ const std = @import("std");
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Size = @import("size.zig");
const Position = Size.Position;
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
/// Double-buffered intermediate rendering pipeline
pub const Buffered = struct {
@@ -62,17 +62,17 @@ pub const Buffered = struct {
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const size: Size = container.size;
const viewport: Size = container.viewport;
const cells: []const Cell = try container.contents();
if (cells.len == 0) return;
var idx: usize = 0;
var vs = this.virtual_screen;
const anchor = (size.anchor.row * this.size.cols) + size.anchor.col;
const anchor = (viewport.anchor.row * this.size.cols) + viewport.anchor.col;
blk: for (0..size.rows) |row| {
for (0..size.cols) |col| {
blk: for (0..viewport.rows) |row| {
for (0..viewport.cols) |col| {
const cell = cells[idx];
idx += 1;

View File

@@ -1,10 +1,10 @@
pub const Size = @This();
pub const Size = packed struct {
anchor: Position = .{},
cols: u16 = 0,
rows: u16 = 0,
};
pub const Position = struct {
pub const Position = packed struct {
col: u16 = 0,
row: u16 = 0,
};
anchor: Position = .{},
cols: u16,
rows: u16,

View File

@@ -2,7 +2,8 @@ const std = @import("std");
pub const code_point = @import("code_point");
const Key = @import("key.zig");
const Size = @import("size.zig");
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Cell = @import("cell.zig");
const log = std.log.scoped(.terminal);
@@ -78,7 +79,7 @@ pub fn writer() Writer {
return .{ .context = .{} };
}
pub fn setCursorPosition(pos: Size.Position) !void {
pub fn setCursorPosition(pos: Position) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);

View File

@@ -1,5 +1,9 @@
// private imports
const container = @import("container.zig");
const color = @import("color.zig");
const size = @import("size.zig");
// public exports
pub const App = @import("app.zig").App;
pub const Renderer = @import("render.zig");
@@ -10,18 +14,19 @@ pub const Scroll = container.Scroll;
pub const Layout = container.Layout;
pub const Cell = @import("cell.zig");
pub const Color = @import("color.zig").Color;
pub const Color = color.Color;
pub const Key = @import("key.zig");
pub const Size = @import("size.zig");
pub const Size = size.Size;
pub const Style = @import("style.zig");
test {
_ = @import("terminal.zig");
_ = @import("queue.zig");
_ = color;
_ = size;
_ = Cell;
_ = Color;
_ = Key;
_ = Size;
_ = Style;
}