From a201f2b653e8d8282c6be55bbce1bf4a0f368000 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 10 Nov 2024 15:53:28 +0100 Subject: [PATCH] mod: change widget interface `Widget.content` replaced with `Widget.render` The .resize `Event` has been adapted to include an _anchor_, which provide the full necessary information for each widget where to render on the screen with what requested size. Each Widget can then dynamically decide how and what to render (i.e. provide placeholder text in case the size is too small, etc.). --- src/app.zig | 2 +- src/layout/Framing.zig | 2 +- src/layout/HStack.zig | 2 +- src/layout/Padding.zig | 2 +- src/layout/VStack.zig | 20 +++++++++++++++----- src/terminal.zig | 1 + src/widget.zig | 37 ++++++++++++++++++++++--------------- src/widget/RawText.zig | 13 ++++++++----- src/widget/Spacer.zig | 7 ++++--- 9 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/app.zig b/src/app.zig index 39e3c20..c64dd61 100644 --- a/src/app.zig +++ b/src/app.zig @@ -47,7 +47,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls pub const Event = mergeTaggedUnions(event.SystemEvent, E); pub const Renderer = R(fullscreen); pub const Layout = @import("layout.zig").Layout(Event, Renderer); - pub const Widget = @import("widget.zig").Widget(Event); + pub const Widget = @import("widget.zig").Widget(Event, Renderer); queue: Queue(Event, 256) = .{}, thread: ?std.Thread = null, diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index 70a9718..ce17fd9 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -17,7 +17,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { } const Element = union(enum) { layout: @import("../layout.zig").Layout(Event, Renderer), - widget: @import("../widget.zig").Widget(Event), + widget: @import("../widget.zig").Widget(Event, Renderer), }; const Events = std.ArrayList(Event); return struct { diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig index e3a0856..2c1ea46 100644 --- a/src/layout/HStack.zig +++ b/src/layout/HStack.zig @@ -15,7 +15,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } - const Widget = @import("../widget.zig").Widget(Event); + const Widget = @import("../widget.zig").Widget(Event, Renderer); const Lay = @import("../layout.zig").Layout(Event, Renderer); const Element = union(enum) { layout: Lay, diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig index 388af0a..bb8e702 100644 --- a/src/layout/Padding.zig +++ b/src/layout/Padding.zig @@ -17,7 +17,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { } const Element = union(enum) { layout: @import("../layout.zig").Layout(Event, Renderer), - widget: @import("../widget.zig").Widget(Event), + widget: @import("../widget.zig").Widget(Event, Renderer), }; const Events = std.ArrayList(Event); return struct { diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig index 0d04ab9..88e532e 100644 --- a/src/layout/VStack.zig +++ b/src/layout/VStack.zig @@ -15,7 +15,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } - const Widget = @import("../widget.zig").Widget(Event); + const Widget = @import("../widget.zig").Widget(Event, Renderer); const Lay = @import("../layout.zig").Layout(Event, Renderer); const Element = union(enum) { layout: Lay, @@ -80,10 +80,16 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { switch (event) { .resize => |size| { this.size = size; - log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows }); + log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + size.anchor.col, + size.anchor.row, + size.cols, + size.rows, + }); const len: u16 = @truncate(this.elements.items.len); this.element_rows = @divTrunc(size.rows, len); var overflow = this.size.rows % len; + var offset: u16 = 0; // adjust size according to the containing elements for (this.elements.items) |*element| { var rows = this.element_rows; @@ -93,10 +99,15 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { } const sub_event: Event = .{ .resize = .{ + .anchor = .{ + .col = size.anchor.col, + .row = size.anchor.row + offset, + }, .cols = size.cols, .rows = rows, }, }; + offset += rows; switch (element.*) { .layout => |*layout| { const events = try layout.handle(sub_event); @@ -140,8 +151,8 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { overflow -|= 1; row += 1; } + // TODO: that's the anchor of each component (only necessary for each widget rendering) const pos: terminal.Position = .{ .col = 1, .row = row }; - log.debug("using position: .{{ .cols = {d}, .rows = {d} }}", .{ pos.col, pos.row }); // TODO: do this using the renderer try terminal.setCursorPosition(pos); switch (element.*) { @@ -150,8 +161,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { }, .widget => |*widget| { // TODO: clear per widget if necesary (i.e. can I query that?) - const content = try widget.content(); - _ = try terminal.write(content); + try widget.render(renderer); }, } } diff --git a/src/terminal.zig b/src/terminal.zig index fdff6c1..c1061f9 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -5,6 +5,7 @@ pub const code_point = @import("code_point"); const log = std.log.scoped(.terminal); pub const Size = struct { + anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default cols: u16, rows: u16, }; diff --git a/src/widget.zig b/src/widget.zig index f8aa50d..514d294 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,7 +1,7 @@ //! Dynamic dispatch for widget implementations. //! Each widget should at last implement these functions: //! - handle(this: *@This(), event: Event) ?Event {} -//! - content(this: *@This()) ![]u8 {} +//! - render(this: *@This(), renderer: Renderer) !void {} //! - deinit(this: *@This()) void {} //! //! Create a `Widget` using `createFrom(object: anytype)` and use them through @@ -10,12 +10,14 @@ //! //! Each `Widget` may cache its content and should if the contents will not //! change for a long time. -const std = @import("std"); +//! +//! When `Widget.render` is called the provided `Renderer` type is expected +//! which handles how contents are rendered for a given widget. const isTaggedUnion = @import("event.zig").isTaggedUnion; -const log = std.log.scoped(.widget); +const log = @import("std").log.scoped(.widget); -pub fn Widget(comptime Event: type) type { +pub fn Widget(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } @@ -25,7 +27,7 @@ pub fn Widget(comptime Event: type) type { const VTable = struct { handle: *const fn (this: *WidgetType, event: Event) ?Event, - content: *const fn (this: *WidgetType) anyerror![]u8, + render: *const fn (this: *WidgetType, renderer: Renderer) anyerror!void, deinit: *const fn (this: *WidgetType) void, }; @@ -36,16 +38,21 @@ pub fn Widget(comptime Event: type) type { pub fn handle(this: *WidgetType, event: Event) ?Event { switch (event) { .resize => |size| { - log.debug("received size: .{{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows }); + log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + size.anchor.col, + size.anchor.row, + size.cols, + size.rows, + }); }, else => {}, } return this.vtable.handle(this, event); } - // Return the entire content of this `Widget`. - pub fn content(this: *WidgetType) ![]u8 { - return try this.vtable.content(this); + // Render the content of this `Widget` given the `Size` of the widget (.resize System`Event`). + pub fn render(this: *WidgetType, renderer: Renderer) !void { + try this.vtable.render(this, renderer); } pub fn deinit(this: *WidgetType) void { @@ -64,13 +71,13 @@ pub fn Widget(comptime Event: type) type { return widget.handle(event); } }.handle, - .content = struct { + .render = struct { // Return the entire content of this `Widget`. - fn content(this: *WidgetType) ![]u8 { + fn render(this: *WidgetType, renderer: Renderer) !void { const widget: @TypeOf(object) = @ptrFromInt(this.object); - return try widget.content(); + try widget.render(renderer); } - }.content, + }.render, .deinit = struct { fn deinit(this: *WidgetType) void { const widget: @TypeOf(object) = @ptrFromInt(this.object); @@ -82,7 +89,7 @@ pub fn Widget(comptime Event: type) type { } // TODO: import and export of `Widget` implementations (with corresponding initialization using `Event`) - pub const RawText = @import("widget/RawText.zig").Widget(Event); - pub const Spacer = @import("widget/Spacer.zig").Widget(Event); + pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer); + pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer); }; } diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index dcc714f..36f3ec2 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -8,7 +8,7 @@ const Style = terminal.Style; const log = std.log.scoped(.widget_rawtext); -pub fn Widget(comptime Event: type) type { +pub fn Widget(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } @@ -75,20 +75,23 @@ pub fn Widget(comptime Event: type) type { return null; } - pub fn content(this: *@This()) ![]u8 { + pub fn render(this: *@This(), renderer: Renderer) !void { + try terminal.setCursorPosition(this.size.anchor); + // TODO: render `this.contents` + _ = renderer; if (this.size.rows >= this.line_index.items.len) { - return this.contents.items; + _ = try terminal.write(this.contents.items); } else { // more rows than we can display const i = this.line_index.items[this.line]; const e = this.size.rows + this.line + 1; if (e >= this.line_index.items.len) { - return this.contents.items[i..]; + _ = try terminal.write(this.contents.items[i..]); } // last line should not end with the last character (likely a newline character) // FIX: what about files which do not end with a newline? const x = this.line_index.items[e] - 1; - return this.contents.items[i..x]; + _ = try terminal.write(this.contents.items[i..x]); } } }; diff --git a/src/widget/Spacer.zig b/src/widget/Spacer.zig index 888eaca..b347a24 100644 --- a/src/widget/Spacer.zig +++ b/src/widget/Spacer.zig @@ -6,7 +6,7 @@ const Error = @import("../event.zig").Error; const log = std.log.scoped(.widget_spacer); -pub fn Widget(comptime Event: type) type { +pub fn Widget(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } @@ -25,9 +25,10 @@ pub fn Widget(comptime Event: type) type { return null; } - pub fn content(this: *@This()) ![]u8 { + pub fn render(this: *@This(), renderer: Renderer) !void { + // FIX: this should rather clear the `terminal.Size` provided through the event system using the _renderer_. _ = this; - return &[0]u8{}; + _ = renderer; } }; }