From 28817d468ad45f286187055df5be736144e590f4 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 12 Nov 2024 23:13:35 +0100 Subject: [PATCH] add(zig-interface): dependency to check interface contracts at comptime The described interfaces for `Widget` and `Layout` are now defined and correspondingly checked at comptime. --- build.zig | 10 +++++++++- build.zig.zon | 4 ++++ src/layout.zig | 27 ++++++++++++++++++++++----- src/layout/Framing.zig | 11 +++++------ src/layout/HStack.zig | 22 +++++++++++----------- src/layout/Padding.zig | 11 +++++------ src/layout/VStack.zig | 22 +++++++++++----------- src/widget.zig | 19 +++++++++++++------ 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/build.zig b/build.zig index 3cd44e9..3755ebd 100644 --- a/build.zig +++ b/build.zig @@ -15,13 +15,21 @@ pub fn build(b: *std.Build) void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - const zg = b.dependency("zg", .{}); + const zg = b.dependency("zg", .{ + .target = target, + .optimize = optimize, + }); + const interface = b.dependency("interface", .{ + .target = target, + .optimize = optimize, + }); const lib = b.addModule("zterm", .{ .root_source_file = b.path("src/zterm.zig"), .target = target, .optimize = optimize, }); + lib.addImport("interface", interface.module("interface")); lib.addImport("code_point", zg.module("code_point")); const exe = b.addExecutable(.{ diff --git a/build.zig.zon b/build.zig.zon index 7619cd0..050d4c1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -27,6 +27,10 @@ .url = "https://codeberg.org/dude_the_builder/zg/archive/v0.13.2.tar.gz", .hash = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40", }, + .interface = .{ + .url = "git+https://github.com/nilslice/zig-interface#c6ca205de75969fdcf04542f48d813d529196594", + .hash = "1220401627a97a7b429acd084bd3447fe5838122d71bbca57061906a2c3baf1c2e98", + }, }, .paths = .{ "build.zig", diff --git a/src/layout.zig b/src/layout.zig index 27495ee..2f16139 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -22,9 +22,18 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } const Events = std.ArrayList(Event); - return struct { + const Type = struct { const LayoutType = @This(); + const Element = union(enum) { + layout: LayoutType, + widget: @import("widget.zig").Widget(Event, Renderer), + }; const Ptr = usize; + pub const Interface = @import("interface").Interface(.{ + .handle = fn (anytype, Event) anyerror!*Events, + .render = fn (anytype, *Renderer) anyerror!void, + .deinit = fn (anytype) void, + }, .{}); const VTable = struct { handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events, @@ -79,9 +88,17 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { } // import and export of `Layout` implementations - pub const HStack = @import("layout/HStack.zig").Layout(Event, Renderer); - pub const VStack = @import("layout/VStack.zig").Layout(Event, Renderer); - pub const Padding = @import("layout/Padding.zig").Layout(Event, Renderer); - pub const Framing = @import("layout/Framing.zig").Layout(Event, Renderer); + pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer); + pub const VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer); + pub const Padding = @import("layout/Padding.zig").Layout(Event, Element, Renderer); + pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer); }; + // test widget implementation satisfies the interface + comptime Type.Interface.satisfiedBy(Type); + // TODO: there is a dependency loop (due to the necessary `Layout` type for the `Element`) + comptime Type.Interface.satisfiedBy(Type.HStack); + comptime Type.Interface.satisfiedBy(Type.VStack); + comptime Type.Interface.satisfiedBy(Type.Padding); + comptime Type.Interface.satisfiedBy(Type.Framing); + return Type; } diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index ff61031..72b4506 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -12,14 +12,13 @@ const Style = terminal.Cell.Style; const log = std.log.scoped(.layout_framing); -pub fn Layout(comptime Event: type, comptime Renderer: type) type { +pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); + @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); + } + if (!isTaggedUnion(Element)) { + @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } - const Element = union(enum) { - layout: @import("../layout.zig").Layout(Event, Renderer), - widget: @import("../widget.zig").Widget(Event, Renderer), - }; const Events = std.ArrayList(Event); return struct { size: terminal.Size = undefined, diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig index 7014824..6c47c67 100644 --- a/src/layout/HStack.zig +++ b/src/layout/HStack.zig @@ -11,17 +11,17 @@ const Key = terminal.Key; const log = std.log.scoped(.layout_hstack); -pub fn Layout(comptime Event: type, comptime Renderer: type) type { +pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); + @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); + } + if (!isTaggedUnion(Element)) { + @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } - const Widget = @import("../widget.zig").Widget(Event, Renderer); - const Lay = @import("../layout.zig").Layout(Event, Renderer); - const Element = union(enum) { - layout: Lay, - widget: Widget, - }; const Elements = std.ArrayList(Element); + // TODO: expect name of field to be corresponding to the type + const LayoutType = @typeInfo(Element).Union.fields[0].type; + const WidgetType = @typeInfo(Element).Union.fields[1].type; const Events = std.ArrayList(Event); return struct { // TODO: current focused `Element`? @@ -40,15 +40,15 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { inline for (comptime fields_info) |field| { const child = @field(children, field.name); const ChildType = @TypeOf(child); - if (ChildType == Widget) { + if (ChildType == WidgetType) { elements.append(.{ .widget = child }) catch {}; continue; } - if (ChildType == Lay) { + if (ChildType == LayoutType) { elements.append(.{ .layout = child }) catch {}; continue; } - @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType)); + @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType)); } return .{ .elements = elements, diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig index 03b9401..9068d0c 100644 --- a/src/layout/Padding.zig +++ b/src/layout/Padding.zig @@ -11,14 +11,13 @@ const Key = terminal.Key; const log = std.log.scoped(.layout_padding); -pub fn Layout(comptime Event: type, comptime Renderer: type) type { +pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); + @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); + } + if (!isTaggedUnion(Element)) { + @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } - const Element = union(enum) { - layout: @import("../layout.zig").Layout(Event, Renderer), - widget: @import("../widget.zig").Widget(Event, Renderer), - }; const Events = std.ArrayList(Event); return struct { size: terminal.Size = undefined, diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig index b948510..273fa9a 100644 --- a/src/layout/VStack.zig +++ b/src/layout/VStack.zig @@ -11,17 +11,17 @@ const Key = terminal.Key; const log = std.log.scoped(.layout_vstack); -pub fn Layout(comptime Event: type, comptime Renderer: type) type { +pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); + @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); + } + if (!isTaggedUnion(Element)) { + @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } - const Widget = @import("../widget.zig").Widget(Event, Renderer); - const Lay = @import("../layout.zig").Layout(Event, Renderer); - const Element = union(enum) { - layout: Lay, - widget: Widget, - }; const Elements = std.ArrayList(Element); + // TODO: expect name of field to be corresponding to the type + const LayoutType = @typeInfo(Element).Union.fields[0].type; + const WidgetType = @typeInfo(Element).Union.fields[1].type; const Events = std.ArrayList(Event); return struct { // TODO: current focused `Element`? @@ -40,15 +40,15 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { inline for (comptime fields_info) |field| { const child = @field(children, field.name); const ChildType = @TypeOf(child); - if (ChildType == Widget) { + if (ChildType == WidgetType) { elements.append(.{ .widget = child }) catch {}; continue; } - if (ChildType == Lay) { + if (ChildType == LayoutType) { elements.append(.{ .layout = child }) catch {}; continue; } - @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType)); + @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType)); } return .{ .elements = elements, diff --git a/src/widget.zig b/src/widget.zig index f5e902c..546434e 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,8 +1,5 @@ //! Dynamic dispatch for widget implementations. -//! Each widget should at least implement these functions: -//! - handle(this: *@This(), event: Event) ?Event {} -//! - render(this: *@This(), renderer: *Renderer) !void {} -//! - deinit(this: *@This()) void {} +//! Each `Widget` has to implement the `WidgetInterface` //! //! Create a `Widget` using `createFrom(object: anytype)` and use them through //! the defined interface. The widget will take care of calling the correct @@ -21,9 +18,14 @@ 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)`."); } - return struct { + const Type = struct { const WidgetType = @This(); const Ptr = usize; + pub const Interface = @import("interface").Interface(.{ + .handle = fn (anytype, Event) ?Event, + .render = fn (anytype, *Renderer) anyerror!void, + .deinit = fn (anytype) void, + }, .{}); const VTable = struct { handle: *const fn (this: *WidgetType, event: Event) ?Event, @@ -88,8 +90,13 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }; } - // TODO: import and export of `Widget` implementations (with corresponding initialization using `Event`) + // import and export of `Widget` implementations pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer); pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer); }; + // test widget implementation satisfies the interface + comptime Type.Interface.satisfiedBy(Type); + comptime Type.Interface.satisfiedBy(Type.RawText); + comptime Type.Interface.satisfiedBy(Type.Spacer); + return Type; }