ref(event): split resize event into viewport and size event

Viewport event reflects the absolut position and size of a given
container (and propagates them to its children). While the size
event propagates the content size to its children (and sets their
corresponding member values accordingly).

Both events are currently only emitted by `Container`s meaning that they
don't need to be part of the event loop and that they might be removed
later.
This commit is contained in:
2025-02-15 09:55:30 +01:00
parent 01d121ef87
commit 09a659ba70
4 changed files with 250 additions and 139 deletions

View File

@@ -108,6 +108,17 @@ cells of the content (and may be overwritten by child elements contents).
The border of an element should be around independent of the scrolling of the
contents, just like padding.
### Positioning and Rendering
I need to find a way to render the contents of each container independently on the screen. Currently the application has two (yet still married) sizes which it keeps track of:
- **viewport**: The `Size` of the content on screen. It contains the information about the location (*anchor*) where on the screen and the size of the content to show.
- **size**: The actual size of the contents for a given container. This `Size` does contain information about the total content size (rows & cols which may not be equal to the screen space available (i.e. the *viewport*)) and the current *anchor* for the viewport of the contents.
Manipulating the *size* of a container has implications on the *viewport* of the child elements which actually causes problems when enlarging containers (children lose correct screen location information).
Should I split the *anchor* and the *size* information from another?
### Input
How is the user input handled in the containers? Should there be active

View File

@@ -32,14 +32,14 @@ pub fn main() !void {
},
.layout = .{
.padding = .all(5),
.direction = .vertical,
.direction = .horizontal,
},
});
var box = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.layout = .{
.gap = 1,
.direction = .horizontal,
.direction = .vertical,
.padding = .vertical(1),
},
});
@@ -92,9 +92,7 @@ pub fn main() !void {
});
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg });
},
.err => |err| log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}

View File

@@ -240,7 +240,7 @@ pub fn Container(comptime Event: type) type {
viewport: Size,
/// Size of the contents columns and rows used for the contents of this `Container`
/// The anchor of this `Size` corresponds to the contents inside of itself.
size: Size = .{},
size: Size,
properties: Properties,
elements: std.ArrayList(@This()),
@@ -258,6 +258,7 @@ pub fn Container(comptime Event: type) type {
return .{
.allocator = allocator,
.viewport = .{},
.size = .{},
.properties = properties,
.elements = std.ArrayList(@This()).init(allocator),
};
@@ -277,26 +278,34 @@ pub fn Container(comptime Event: type) type {
pub fn handle(this: *@This(), event: Event) !void {
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,
.resize => |size| {
// initial resize event (handled by the root container)
try this.content_layout(size);
try this.viewport_layout(size);
},
.size => |size| try this.content_layout(size),
.viewport => |size| try this.viewport_layout(size),
else => for (this.elements.items) |*element| try element.handle(event),
}
}
fn viewport_layout(this: *@This(), size: Size) anyerror!void {
// NOTE: do not manipulate the values here!
log.debug("Event .viewport: .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
this.viewport = s;
this.viewport = size;
const size: Size = .{
.cols = s.cols,
.rows = s.rows,
};
this.size = size;
if (this.elements.items.len == 0) break :resize;
// without child elements no size information needs to be propagated further
if (this.elements.items.len == 0) return;
const separator = this.properties.border.separator;
const sides = this.properties.border.sides;
const padding = this.properties.layout.padding;
var gap = this.properties.layout.gap;
if (separator.enabled) gap += 1; // the gap will be used for the rendering of the separator line
@@ -349,8 +358,8 @@ pub fn Container(comptime Event: type) type {
}
element_size = .{
.anchor = .{
.col = this.viewport.anchor.col + offset,
.row = this.viewport.anchor.row,
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
@@ -373,8 +382,8 @@ pub fn Container(comptime Event: type) type {
}
element_size = .{
.anchor = .{
.col = this.viewport.anchor.col,
.row = this.viewport.anchor.row + offset,
.col = size.anchor.col,
.row = size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
@@ -392,19 +401,115 @@ pub fn Container(comptime Event: type) type {
}
// border resizing
if (sides.top) {
element_size.anchor.row += 1;
if (sides.top) element_size.anchor.row += 1;
if (sides.left) element_size.anchor.col += 1;
try element.handle(.{ .viewport = element_size });
}
if (sides.left) {
element_size.anchor.col += 1;
}
// padding resizing
fn content_layout(this: *@This(), s: Size) anyerror!void {
// TODO: overwrite size with configuration values (i.e. fixed, min size, max size, etc.)
const size = s;
log.debug("Event .size: .{{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
this.size = size;
try element.handle(.{ .resize = element_size });
}
// without child elements no size information needs to be propagated further
if (this.elements.items.len == 0) return;
const separator = this.properties.border.separator;
const sides = this.properties.border.sides;
const padding = this.properties.layout.padding;
var gap = this.properties.layout.gap;
if (separator.enabled) gap += 1; // the gap will be used for the rendering of the separator line
const len: u16 = @truncate(this.elements.items.len);
const element_cols = blk: {
var cols = size.cols - gap * (len - 1);
if (sides.left) cols -= 1;
if (sides.right) cols -= 1;
cols -= padding.left + padding.right;
break :blk @divTrunc(cols, len);
};
const element_rows = blk: {
var rows = size.rows - gap * (len - 1);
if (sides.top) rows -= 1;
if (sides.bottom) rows -= 1;
rows -= padding.top + padding.bottom;
break :blk @divTrunc(rows, len);
};
var offset: u16 = switch (this.properties.layout.direction) {
.horizontal => padding.left,
.vertical => padding.top,
};
var overflow = switch (this.properties.layout.direction) {
.horizontal => blk: {
var cols = size.cols - gap * (len - 1);
if (sides.left) cols -= 1;
if (sides.right) cols -= 1;
cols -= padding.left + padding.right;
break :blk cols - element_cols * len;
},
else => for (this.elements.items) |*element| try element.handle(event),
.vertical => blk: {
var rows = size.rows - gap * (len - 1);
if (sides.top) rows -= 1;
if (sides.bottom) rows -= 1;
rows -= padding.top + padding.bottom;
break :blk rows - element_rows * len;
},
};
// TODO: make sure that items cannot underflow in size!
// - make their size and position still according (even if outside of the visible space!)
// - don't render them then accordingly -> avoid index out of bounce accesses!
for (this.elements.items) |*element| {
var element_size: Size = undefined;
switch (this.properties.layout.direction) {
.horizontal => {
var cols = element_cols;
if (overflow > 0) {
overflow -|= 1;
cols += 1;
}
element_size = .{
.cols = cols,
.rows = size.rows,
};
// border
if (sides.top) element_size.rows -= 1;
if (sides.bottom) element_size.rows -= 1;
// padding
element_size.rows -= padding.top + padding.bottom;
// gap
offset += gap;
offset += cols;
},
.vertical => {
var rows = element_rows;
if (overflow > 0) {
overflow -|= 1;
rows += 1;
}
element_size = .{
.cols = size.cols,
.rows = rows,
};
// border
if (sides.left) element_size.cols -= 1;
if (sides.right) element_size.cols -= 1;
// padding
element_size.cols -= padding.left + padding.right;
// gap
offset += gap;
offset += rows;
},
}
try element.handle(.{ .size = element_size });
}
}
@@ -419,13 +524,6 @@ pub fn Container(comptime Event: type) type {
this.properties.border.contents(content_cells, this.size, this.properties.layout, @truncate(this.elements.items.len));
this.properties.rectangle.contents(content_cells, this.size);
log.debug("Content::contents .scroll.size: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
this.size.anchor.col,
this.size.anchor.row,
this.size.cols,
this.size.rows,
});
const cols = blk: {
var cols: u16 = this.size.cols - this.size.anchor.col;
if (cols > this.viewport.cols) {

View File

@@ -21,6 +21,10 @@ pub const SystemEvent = union(enum) {
},
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
resize: Size,
/// `Size` event emitted by `Container`'s to provide child elements their appropriate viewport information
viewport: Size,
/// `Size` event emitted by `Container`'s to provide child elements with their available content size
size: Size,
/// Input key event received from the user
key: Key,
/// Focus event for mouse interaction