intermediate #1

Merged
yves-biener merged 31 commits from intermediate into main 2025-02-16 16:02:59 +01:00
9 changed files with 119 additions and 51 deletions
Showing only changes of commit bbe6f4741e - Show all commits

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ const Style = @import("style.zig");
pub const Cell = @This(); pub const Cell = @This();
style: Style = .{ .attributes = &.{} }, style: Style = .{ .attributes = &.{} },
// TODO: embrace `zg` dependency more due to utf-8 encoding
cp: u21 = ' ', cp: u21 = ' ',
pub fn eql(this: Cell, other: Cell) bool { 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 Cell = @import("cell.zig");
const Color = @import("color.zig").Color; const Color = @import("color.zig").Color;
const Size = @import("size.zig"); const Size = @import("size.zig").Size;
const Style = @import("style.zig"); const Style = @import("style.zig");
const log = std.log.scoped(.container); const log = std.log.scoped(.container);
/// Border configuration struct /// Border configuration struct
pub const Border = struct { pub const Border = packed struct {
pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
/// Color to use for the border /// 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 /// Configure separator borders between child element to added to the layout
separator: struct { separator: packed struct {
enabled: bool = false, enabled: bool = false,
color: Color = .white, color: Color = .white,
line: enum { line: enum(u1) {
line, line,
dotted, dotted,
// TODO: add more variations which could be used for the separator // 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` // 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 { 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) { const frame = switch (this.corners) {
.rounded => Border.rounded_border, .rounded => Border.rounded_border,
@@ -62,7 +62,7 @@ pub const Border = struct {
// render top and bottom border // render top and bottom border
for (0..size.cols) |col| { 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) { if (this.sides.left and col == 0) {
// top left corner // top left corner
if (this.sides.top) cells[col].cp = frame[0]; if (this.sides.top) cells[col].cp = frame[0];
@@ -170,7 +170,7 @@ pub const Border = struct {
}; };
/// Rectangle configuration struct /// Rectangle configuration struct
pub const Rectangle = struct { pub const Rectangle = packed struct {
/// `Color` to use to fill the `Rectangle` with /// `Color` to use to fill the `Rectangle` with
/// NOTE: used as background color when rendering! such that it renders the /// NOTE: used as background color when rendering! such that it renders the
/// children accordingly without removing the coloring of the `Rectangle` /// 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` // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size) void { 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.rows) |row| {
for (0..size.cols) |col| { for (0..size.cols) |col| {
@@ -200,8 +200,21 @@ pub const Scroll = packed struct {
// TODO: rendering enhancements: // TODO: rendering enhancements:
// - render corresponding scroll-bars? // - 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 /// Layout configuration struct
pub const Layout = struct { pub const Layout = struct {
/// control the direction in which child elements are laid out /// 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 // 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 to be used for the width and height of this element to use
sizing: struct { sizing: struct {
width: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit, width: union(enum(u2)) { fit, grow, fixed: u16, percent: u16 } = .fit,
height: union(enum) { 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 { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
size: Size, /// Size of actual columns and rows used to render this `Container`
viewport: Size,
properties: Properties, properties: Properties,
elements: std.ArrayList(@This()), 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() { pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.size = .{ .cols = 0, .rows = 0 }, .viewport = .{},
.properties = properties, .properties = properties,
.elements = std.ArrayList(@This()).init(allocator), .elements = std.ArrayList(@This()).init(allocator),
}; };
@@ -289,8 +303,16 @@ pub fn Container(comptime Event: type) type {
switch (event) { switch (event) {
.init => log.debug(".init event", .{}), .init => log.debug(".init event", .{}),
.resize => |s| resize: { .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 // sizing
var size = s; var size = s;
size.anchor = .{}; // reset relative anchor of size!
const sizing = this.properties.layout.sizing; const sizing = this.properties.layout.sizing;
switch (sizing.width) { switch (sizing.width) {
.fit => { .fit => {
@@ -301,7 +323,7 @@ pub fn Container(comptime Event: type) type {
// NOTE: this is pretty much the current implementation // NOTE: this is pretty much the current implementation
}, },
.fixed => |fix| { .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; size.cols = fix;
}, },
.percent => |percent| { .percent => |percent| {
@@ -312,13 +334,16 @@ pub fn Container(comptime Event: type) type {
switch (sizing.height) { switch (sizing.height) {
.fit => { .fit => {
// use as much space as necessary (but nothing more than necessary) // 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 => {
// grow use as much space as available by the parent (i.e. the entire width) // grow use as much space as available by the parent (i.e. the entire width)
// NOTE: this is pretty much the current implementation // NOTE: this is pretty much the current implementation
}, },
.fixed => |fix| { .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; size.rows = fix;
}, },
.percent => |percent| { .percent => |percent| {
@@ -326,14 +351,7 @@ pub fn Container(comptime Event: type) type {
size.rows = @divTrunc(size.rows * percent, 100); size.rows = @divTrunc(size.rows * percent, 100);
}, },
} }
this.properties.scroll.size = size;
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;
if (this.elements.items.len == 0) break :resize; 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 { pub fn contents(this: *const @This()) ![]const Cell {
const cells = try this.allocator.alloc(Cell, this.size.cols * this.size.rows); const content = try this.allocator.alloc(Cell, @as(usize, this.properties.scroll.size.cols) * @as(usize, this.properties.scroll.size.rows));
@memset(cells, .{}); // reset all cells defer this.allocator.free(content);
this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); @memset(content, .{});
this.properties.rectangle.contents(cells, this.size);
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; return cells;
} }
}; };

View File

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

View File

@@ -2,8 +2,8 @@ const std = @import("std");
const terminal = @import("terminal.zig"); const terminal = @import("terminal.zig");
const Cell = @import("cell.zig"); const Cell = @import("cell.zig");
const Size = @import("size.zig"); const Position = @import("size.zig").Position;
const Position = Size.Position; const Size = @import("size.zig").Size;
/// Double-buffered intermediate rendering pipeline /// Double-buffered intermediate rendering pipeline
pub const Buffered = struct { pub const Buffered = struct {
@@ -62,17 +62,17 @@ pub const Buffered = struct {
/// Render provided cells at size (anchor and dimension) into the *virtual screen*. /// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) !void { 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(); const cells: []const Cell = try container.contents();
if (cells.len == 0) return; if (cells.len == 0) return;
var idx: usize = 0; var idx: usize = 0;
var vs = this.virtual_screen; 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| { blk: for (0..viewport.rows) |row| {
for (0..size.cols) |col| { for (0..viewport.cols) |col| {
const cell = cells[idx]; const cell = cells[idx];
idx += 1; 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, col: u16 = 0,
row: 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"); pub const code_point = @import("code_point");
const Key = @import("key.zig"); 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 Cell = @import("cell.zig");
const log = std.log.scoped(.terminal); const log = std.log.scoped(.terminal);
@@ -78,7 +79,7 @@ pub fn writer() Writer {
return .{ .context = .{} }; return .{ .context = .{} };
} }
pub fn setCursorPosition(pos: Size.Position) !void { pub fn setCursorPosition(pos: Position) !void {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col }); const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
_ = try std.posix.write(std.posix.STDIN_FILENO, value); _ = try std.posix.write(std.posix.STDIN_FILENO, value);

View File

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