diff --git a/src/Element.zig b/src/Element.zig new file mode 100644 index 0000000..f7430b1 --- /dev/null +++ b/src/Element.zig @@ -0,0 +1,50 @@ +pub const Card = @import("./Element/Card.zig"); +pub const HBox = @import("./Element/HBox.zig"); +pub const Link = @import("./Element/Link.zig"); +pub const Page = @import("./Element/Page.zig"); +pub const Pile = @import("./Element/Pile.zig"); +pub const Text = @import("./Element/Text.zig"); +pub const View = @import("./Element/View.zig"); + +pointer: ?*anyopaque, +interface: *const Interface, + +pub const Element = @This(); + +pub const Error = error{ + OutOfMemory, +}; + +pub const Interface = struct { + destroy: *const fn (?*anyopaque) void, + minimum_size: *const fn (?*anyopaque, RenderResources) [2]f32, + render: *const fn (?*anyopaque, *seizer.Canvas, RenderResources, [2]f32, [2]f32) void, + get_actions: *const fn (?*anyopaque, *std.ArrayList(Action), RenderResources, [2]f32, [2]f32) Error!void, +}; + +pub const RenderResources = struct { + hovered: ?Element.Command, + deck: DeckSprites, + font: seizer.Canvas.Font, +}; + +pub const Action = struct { + center: [2]f32, + command: Command, +}; + +pub const Command = union(enum) { + goto_reference: protocol.Reference, + callback: struct { + pointer: ?*anyopaque, + function: *const fn (?*anyopaque) void, + }, +}; + +const DeckSprites = assets.DeckSprites; + +const protocol = @import("./protocol.zig"); +const assets = @import("./assets.zig"); +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/Card.zig b/src/Element/Card.zig new file mode 100644 index 0000000..132fd8e --- /dev/null +++ b/src/Element/Card.zig @@ -0,0 +1,101 @@ +allocator: std.mem.Allocator, +visual: Visual, +command: ?[]const u8, +marked: bool, + +const Visual = union(enum) { + back, + card: assets.Card, +}; + +pub fn create(allocator: std.mem.Allocator, options: struct { visual: Visual, command: ?[]const u8, marked: ?bool = null }) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + this.* = .{ + .allocator = allocator, + .visual = options.visual, + .command = options.command, + .marked = options.marked orelse false, + }; + return this; +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + _ = this; + + return .{ + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])), + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])), + }; +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const mark_offset = if (this.marked) + [2]f32{ + 0, + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) * -0.4, + } + else + [2]f32{ 0, 0 }; + + switch (this.visual) { + .back => render_resources.deck.tilesheet.renderTile( + canvas, + render_resources.deck.back, + .{ min[0] + mark_offset[0], min[1] + mark_offset[1] }, + .{}, + ), + .card => |card| render_resources.deck.tilesheet.renderTile( + canvas, + render_resources.deck.getTileForCard(card), + .{ @floor(min[0] + mark_offset[0]), @floor(min[1] + mark_offset[1]) }, + .{}, + ), + } + + if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) { + canvas.rect( + .{ min[0] + mark_offset[0], min[1] + mark_offset[1] }, + .{ max[0] - min[0], max[1] - min[1] }, + .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, + ); + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + _ = render_resources; + + if (this.command) |command| { + try actions.append(.{ + .center = [2]f32{ + (min[0] + max[0]) / 2, + (min[1] + max[1]) / 2, + }, + .command = command, + }); + } +} + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/HBox.zig b/src/Element/HBox.zig new file mode 100644 index 0000000..0770aee --- /dev/null +++ b/src/Element/HBox.zig @@ -0,0 +1,130 @@ +allocator: std.mem.Allocator, +children: std.ArrayListUnmanaged(Element), + +pub fn create(allocator: std.mem.Allocator) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + this.* = .{ + .allocator = allocator, + .children = .{}, + }; + return this; +} + +pub fn addElement(this: *@This(), child_element: Element) !void { + try this.children.append(this.allocator, child_element); +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + for (this.children.items) |child| { + child.interface.destroy(child.pointer); + } + this.children.deinit(this.allocator); + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + var minimum_size = [2]f32{ 0, 0 }; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + minimum_size = .{ + minimum_size[0] + child_size[0], + @max(minimum_size[1], child_size[1]), + }; + } + + return minimum_size; +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + if (this.children.items.len == 0) return; + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + var filled_space: f32 = 0; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + filled_space += child_size[0]; + } + + const empty_space = parent_size[0] - filled_space; + + const num_spaces = if (empty_space > 0) this.children.items.len + 1 else this.children.items.len - 1; + + const space_around = empty_space / @as(f32, @floatFromInt(num_spaces)); + + var x: f32 = min[0] + @max(space_around, 0); + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + const child_min = [2]f32{ x, min[1] }; + const child_max = [2]f32{ x + child_size[0], max[1] }; + + child.interface.render(child.pointer, canvas, render_resources, child_min, child_max); + + x += child_size[0] + space_around; + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + if (this.children.items.len == 0) return; + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + var filled_space: f32 = 0; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + filled_space += child_size[0]; + } + + const empty_space = parent_size[0] - filled_space; + + const space_around = empty_space / @as(f32, @floatFromInt((this.children.items.len + 1))); + + var x: f32 = min[0] + space_around; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + const child_min = [2]f32{ x, min[1] }; + const child_max = [2]f32{ x + child_size[0], max[1] }; + + try child.interface.get_actions(child.pointer, actions, render_resources, child_min, child_max); + + x += child_size[0] + space_around; + } +} + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/Link.zig b/src/Element/Link.zig new file mode 100644 index 0000000..5e62361 --- /dev/null +++ b/src/Element/Link.zig @@ -0,0 +1,76 @@ +allocator: std.mem.Allocator, +child: Element, +reference: protocol.Reference, + +pub fn create(allocator: std.mem.Allocator, child: Element, reference: protocol.Reference) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + this.* = .{ + .allocator = allocator, + .child = child, + .reference = reference, + }; + return this; +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + this.child.interface.destroy(this.child.pointer); + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + return this.child.interface.minimum_size(this.child.pointer, render_resources); +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + // render child element + this.child.interface.render(this.child.pointer, canvas, render_resources, min, max); + + // render selection indicator on top if this link is selected + if (render_resources.hovered != null and std.meta.eql(render_resources.hovered.?, Element.Command{ .goto_reference = this.reference })) { + canvas.rect( + .{ min[0], min[1] }, + .{ max[0] - min[0], max[1] - min[1] }, + .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, + ); + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + _ = render_resources; + + try actions.append(.{ + .center = [2]f32{ + (min[0] + max[0]) / 2, + (min[1] + max[1]) / 2, + }, + .command = .{ .goto_reference = this.reference }, + }); +} + +const Element = @import("../Element.zig"); +const protocol = @import("../protocol.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/Page.zig b/src/Element/Page.zig new file mode 100644 index 0000000..3522608 --- /dev/null +++ b/src/Element/Page.zig @@ -0,0 +1,150 @@ +allocator: std.mem.Allocator, +children: std.ArrayListUnmanaged(Child), + +pub const Child = struct { + /// Where is the child attached on the parent? Imagine it as a pin going through + /// both the parent and child element. This defines where on the parent that pin + /// passes through. + /// + /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless + /// the numbers are negative. + anchor_in_parent: [2]f32, + /// Where is the child attached on the parent? Imagine it as a pin going through + /// both the parent and child element. This defines where on the child that pin + /// passes through. + /// + /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless + /// the numbers are negative. + anchor_in_child: [2]f32, + element: Element, +}; + +pub fn create(allocator: std.mem.Allocator) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + this.* = .{ + .allocator = allocator, + .children = .{}, + }; + return this; +} + +pub fn addElement(this: *@This(), anchor_in_parent: [2]f32, anchor_in_child: [2]f32, child_element: Element) !void { + try this.children.append(this.allocator, Child{ + .anchor_in_parent = anchor_in_parent, + .anchor_in_child = anchor_in_child, + .element = child_element, + }); +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + for (this.children.items) |child| { + child.element.interface.destroy(child.element.pointer); + } + this.children.deinit(this.allocator); + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + var minimum_size = [2]f32{ 0, 0 }; + for (this.children.items) |child| { + const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); + + minimum_size = .{ + @max(minimum_size[0], child_size[0]), + @max(minimum_size[1], child_size[1]), + }; + } + + return minimum_size; +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + for (this.children.items) |child| { + const pos_in_parent = [2]f32{ + child.anchor_in_parent[0] * parent_size[0], + child.anchor_in_parent[1] * parent_size[1], + }; + + const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); + + const pos_in_child = [2]f32{ + child.anchor_in_child[0] * child_size[0], + child.anchor_in_child[1] * child_size[1], + }; + + const child_min = [2]f32{ + @max(min[0], pos_in_parent[0] - pos_in_child[0]), + @max(min[1], pos_in_parent[1] - pos_in_child[1]), + }; + const child_max = [2]f32{ + @min(max[0], pos_in_parent[0] + (child_size[0] - pos_in_child[0])), + @min(max[1], pos_in_parent[1] + (child_size[1] - pos_in_child[1])), + }; + + child.element.interface.render(child.element.pointer, canvas, render_resources, child_min, child_max); + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + for (this.children.items) |child| { + const pos_in_parent = [2]f32{ + child.anchor_in_parent[0] * parent_size[0], + child.anchor_in_parent[1] * parent_size[1], + }; + + const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); + + const pos_in_child = [2]f32{ + child.anchor_in_child[0] * child_size[0], + child.anchor_in_child[1] * child_size[1], + }; + + const child_min = [2]f32{ + pos_in_parent[0] - pos_in_child[0], + pos_in_parent[1] - pos_in_child[1], + }; + const child_max = [2]f32{ + pos_in_parent[0] + (child_size[0] - pos_in_child[0]), + pos_in_parent[1] + (child_size[1] - pos_in_child[1]), + }; + + try child.element.interface.get_actions(child.element.pointer, actions, render_resources, child_min, child_max); + } +} + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/Pile.zig b/src/Element/Pile.zig new file mode 100644 index 0000000..8f083b7 --- /dev/null +++ b/src/Element/Pile.zig @@ -0,0 +1,93 @@ +allocator: std.mem.Allocator, +cards: []Card, +hidden: bool = false, +command: ?[]const u8 = null, + +pub fn create(allocator: std.mem.Allocator, cards: []const Card) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + const cards_owned = try allocator.dupe(Card, cards); + errdefer allocator.free(cards_owned); + + this.* = .{ + .allocator = allocator, + .cards = cards_owned, + }; + return this; +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + return .{ + @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), + @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) + @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) * @as(f32, @floatFromInt(this.cards.len)) / 52.0 * 0.25, + }; +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const start_y: f32 = max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])); + + for (this.cards, 0..) |card, i| { + const oy = -@as(f32, @floatFromInt(i)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4)); + if (this.hidden) { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.back, .{ + min[0], + start_y + oy, + }, .{}); + } else { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.getTileForCard(card), .{ + min[0], + start_y + oy, + }, .{}); + } + } + + if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) { + const oy = -@as(f32, @floatFromInt(this.cards.len)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4)); + canvas.rect( + .{ min[0], start_y + oy }, + .{ @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), @floatFromInt(render_resources.deck.tilesheet.tile_size[1]) }, + .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, + ); + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + if (this.command) |command| { + const center = [2]f32{ + min[0] + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])) / 2, + max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) * (2.0 - @as(f32, @floatFromInt(this.cards.len)) / (52.0 * 4)), + }; + + try actions.append(.{ + .center = center, + .command = command, + }); + } +} + +const Card = assets.Card; + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/Text.zig b/src/Element/Text.zig new file mode 100644 index 0000000..5323c3b --- /dev/null +++ b/src/Element/Text.zig @@ -0,0 +1,62 @@ +allocator: std.mem.Allocator, +text: []const u8, + +pub fn create(allocator: std.mem.Allocator, text: []const u8) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + const text_owned = try allocator.dupe(u8, text); + errdefer allocator.free(text_owned); + + this.* = .{ + .allocator = allocator, + .text = text_owned, + }; + return this; +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + this.allocator.free(this.text); + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + return render_resources.font.textSize(this.text, 1); +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + _ = max; + _ = render_resources; + + _ = canvas.writeText(min, this.text, .{}); +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + _ = pointer; + _ = actions; + _ = render_resources; + _ = min; + _ = max; +} + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/Element/View.zig b/src/Element/View.zig new file mode 100644 index 0000000..26d900e --- /dev/null +++ b/src/Element/View.zig @@ -0,0 +1,179 @@ +allocator: std.mem.Allocator, +reference: protocol.Reference, +response: ?protocol.Response, + +link_actions_in_view: std.AutoHashMapUnmanaged(*LinkProxy, void) = .{}, + +const View = @This(); + +const LinkProxy = struct { + view: *View, + reference: protocol.Reference, + + fn callback(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + this.view.setReference(this.reference); + } +}; + +pub fn create(allocator: std.mem.Allocator, reference: protocol.Reference) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + this.* = .{ + .allocator = allocator, + .reference = reference, + .response = null, + }; + return this; +} + +pub fn setReference(this: *@This(), reference: protocol.Reference) void { + if (this.response) |response| { + response.arena.deinit(); + this.response = null; + } + + this.reference = reference; + + var action_proxies_iter = this.link_actions_in_view.keyIterator(); + while (action_proxies_iter.next()) |link_proxy| { + this.allocator.destroy(link_proxy.*); + } + this.link_actions_in_view.clearRetainingCapacity(); +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .get_actions = &element_get_actions, + }, + }; +} + +const LOADING_TEXT = "Loading..."; + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + if (this.response) |response| { + switch (response.body) { + .element => |root| root.interface.destroy(root.pointer), + } + response.arena.deinit(); + } + + var action_proxies_iter = this.link_actions_in_view.keyIterator(); + while (action_proxies_iter.next()) |link_proxy| { + this.allocator.destroy(link_proxy.*); + } + this.link_actions_in_view.deinit(this.allocator); + + switch (this.reference) { + .handler => |handler| if (handler.interface.deinit) |deinit| deinit(handler.pointer), + } + + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + if (this.response) |response| { + switch (response.body) { + .element => |root| return root.interface.minimum_size(root.pointer, render_resources), + } + } else { + return render_resources.font.textSize(LOADING_TEXT, 1); + } +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + if (this.response == null) { + this.response = this.reference.handler.interface.handle(this.reference.handler.pointer, .{ + .allocator = this.allocator, + }) catch response_error: { + std.log.warn("Could not get response", .{}); + break :response_error null; + }; + } + + if (this.response) |response| { + var child_render_resources = render_resources; + + if (render_resources.hovered) |hovered| { + switch (hovered) { + .callback => |cb| { + if (this.link_actions_in_view.getKey(@ptrCast(@alignCast(cb.pointer)))) |link_proxy| { + child_render_resources.hovered = .{ .goto_reference = link_proxy.reference }; + } + }, + else => {}, + } + } + + switch (response.body) { + .element => |root| root.interface.render(root.pointer, canvas, child_render_resources, min, max), + } + } else { + _ = canvas.writeText(min, LOADING_TEXT, .{}); + } +} + +pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Element.Action), render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + // clean up previous link actions + var action_proxies_iter = this.link_actions_in_view.keyIterator(); + while (action_proxies_iter.next()) |link_proxy| { + this.allocator.destroy(link_proxy.*); + } + this.link_actions_in_view.clearRetainingCapacity(); + + var children_actions = std.ArrayList(Element.Action).init(this.allocator); + defer children_actions.deinit(); + + if (this.response) |response| { + switch (response.body) { + .element => |root| { + try root.interface.get_actions(root.pointer, &children_actions, render_resources, min, max); + }, + } + } + + for (children_actions.items) |child_action| { + switch (child_action.command) { + .goto_reference => |ref| { + const link_proxy_action = try this.allocator.create(LinkProxy); + try this.link_actions_in_view.put(this.allocator, link_proxy_action, {}); + link_proxy_action.* = .{ + .view = this, + .reference = ref, + }; + try actions.append(.{ + .center = child_action.center, + .command = .{ + .callback = .{ + .pointer = link_proxy_action, + .function = &LinkProxy.callback, + }, + }, + }); + }, + .callback => try actions.append(child_action), + } + } +} + +const Element = @import("../Element.zig"); +const protocol = @import("../protocol.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/LocalUI.zig b/src/LocalUI.zig new file mode 100644 index 0000000..db6c22e --- /dev/null +++ b/src/LocalUI.zig @@ -0,0 +1,76 @@ +pub const main_menu = Handler{ + .pointer = null, + .interface = &.{ + .handle = &main_menu_handler, + .deinit = null, + }, +}; + +fn main_menu_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { + var arena = std.heap.ArenaAllocator.init(request.allocator); + errdefer arena.deinit(); + + var join_multiplayer_text = try Element.Text.create(arena.allocator(), "Join Multiplayer Game"); + var join_multiplayer_link = try Element.Link.create(arena.allocator(), join_multiplayer_text.element(), .{ .handler = join_multiplayer_game }); + + var host_multiplayer_text = try Element.Text.create(arena.allocator(), "Host Multiplayer Game"); + var host_multiplayer_link = try Element.Link.create(arena.allocator(), host_multiplayer_text.element(), .{ .handler = host_multiplayer_game }); + + var play_game_hbox = try Element.HBox.create(arena.allocator()); + try play_game_hbox.addElement(join_multiplayer_link.element()); + try play_game_hbox.addElement(host_multiplayer_link.element()); + + var page = try Element.Page.create(arena.allocator()); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, play_game_hbox.element()); + + return Response{ .arena = arena, .body = .{ .element = page.element() } }; +} + +pub const join_multiplayer_game = Handler{ + .pointer = null, + .interface = &.{ + .handle = &join_multiplayer_game_handler, + .deinit = null, + }, +}; + +fn join_multiplayer_game_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { + var arena = std.heap.ArenaAllocator.init(request.allocator); + errdefer arena.deinit(); + + var text = try Element.Text.create(arena.allocator(), "Joining Multiplayer Game"); + + var page = try Element.Page.create(arena.allocator()); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + + return Response{ .arena = arena, .body = .{ .element = page.element() } }; +} + +pub const host_multiplayer_game = Handler{ + .pointer = null, + .interface = &.{ + .handle = &host_multiplayer_game_handler, + .deinit = null, + }, +}; + +fn host_multiplayer_game_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { + var arena = std.heap.ArenaAllocator.init(request.allocator); + errdefer arena.deinit(); + + var text = try Element.Text.create(arena.allocator(), "Hosting Multiplayer Game"); + + var page = try Element.Page.create(arena.allocator()); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + + return Response{ .arena = arena, .body = .{ .element = page.element() } }; +} + +const Handler = protocol.Handler; +const Request = protocol.Request; +const Response = protocol.Response; + +const protocol = @import("./protocol.zig"); +const Element = @import("./Element.zig"); + +const std = @import("std"); diff --git a/src/main.zig b/src/main.zig index d347d07..03d4a1a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,36 +1,8 @@ var gl_binding: gl.Binding = undefined; -const HandlerError = error{OutOfMemory}; -const Handler = *const fn (std.mem.Allocator, Request) HandlerError!Response; - -const Request = struct { - game_state: GameState, - command: ?[]const u8, -}; -const Response = union(enum) { - page: Element, - transition: struct { - game_state: GameState, - history_type: HistoryType, - reset_selection: bool = false, - }, - - pub const HistoryType = enum { - /// This game state should act as the start of history, and all previous history should - /// be removed. - start, - /// This game state shouldn't be added to the history. - transient, - /// This game state should be added to the history, as a point in time that can be reverted - /// to. - important, - }; -}; - const GameState = struct { allocator: std.mem.Allocator, prng: std.rand.DefaultPrng, - handler: Handler, draw_pile: std.ArrayListUnmanaged(Card), discard_pile: std.ArrayListUnmanaged(Card), hands: []std.ArrayListUnmanaged(Card), @@ -69,7 +41,6 @@ const GameState = struct { return @This(){ .allocator = allocator, .prng = prng, - .handler = &drawCardHandler, .draw_pile = draw_pile, .discard_pile = discard_pile, .hands = hands, @@ -139,6 +110,12 @@ pub fn main() !void { } defer seizer.backend.glfw.c.glfwTerminate(); + // Initialize enet + if (c.enet_initialize() != 0) { + return error.EnetInitialize; + } + defer c.enet_deinitialize(); + // update joystick to gamepad mappings update_gamepad_mappings_file: { const sdl_controller_config_filepath = std.process.getEnvVarOwned(gpa.allocator(), "SDL_GAMECONTROLLERCONFIG_FILE") catch break :update_gamepad_mappings_file; @@ -206,26 +183,15 @@ pub fn main() !void { var canvas = try seizer.Canvas.init(gpa.allocator(), .{}); defer canvas.deinit(gpa.allocator()); - // game state - var history = std.ArrayList(GameState).init(gpa.allocator()); - defer { - for (history.items) |*state| { - state.deinit(); - } - history.deinit(); - } - try history.append(try GameState.init(gpa.allocator(), std.crypto.random.int(u64), 1)); - var history_type = Response.HistoryType.start; + // get main screen + const main_view = try Element.View.create(gpa.allocator(), .{ .handler = LocalUI.main_menu }); + defer main_view.element().interface.destroy(main_view.element().pointer); - var root_element: ?Element = null; - var response_arena = std.heap.ArenaAllocator.init(gpa.allocator()); - defer response_arena.deinit(); - - var actions = std.ArrayList(Action).init(gpa.allocator()); + var actions = std.ArrayList(Element.Action).init(gpa.allocator()); defer actions.deinit(); // TODO: Restore hovered_action when undoing - var hovered_action: ?Action = null; + var hovered_action: ?Element.Action = null; if (seizer.backend.glfw.c.glfwJoystickIsGamepad(seizer.backend.glfw.c.GLFW_JOYSTICK_1) != 0) { std.log.info("detected gamepad = \"{?s}\" {?s}", .{ seizer.backend.glfw.c.glfwGetGamepadName(seizer.backend.glfw.c.GLFW_JOYSTICK_1), seizer.backend.glfw.c.glfwGetJoystickGUID(seizer.backend.glfw.c.GLFW_JOYSTICK_1) }); @@ -310,26 +276,21 @@ pub fn main() !void { prev_controller_input_state = controller_input_state; } - var request_command: ?[]const u8 = null; - defer if (request_command) |command| gpa.allocator().free(command); - if (hovered_action) |hovered| { if (input_state.action) { - request_command = try gpa.allocator().dupe(u8, hovered.command); - root_element = null; + switch (hovered.command) { + .goto_reference => { + std.debug.panic("Got `.goto_reference` action at top-level; expected `main_view` to proxy them all into LinkProxy callbacks.", .{}); + }, + .callback => |cb| { + cb.function(cb.pointer); + }, + } } } else if (actions.items.len > 0) { hovered_action = actions.items[0]; } - if (input_state.undo and history.items.len > 1) { - var discarded_state = history.pop(); - history_type = .important; - discarded_state.deinit(); - root_element = null; - hovered_action = null; - } - if (hovered_action) |hovered| { var direction = [2]f32{ 0, 0 }; if (input_state.left) { @@ -348,12 +309,12 @@ pub fn main() !void { var new_distance: ?f32 = null; var new_action = hovered; for (actions.items) |action| { - if (std.mem.eql(u8, action.command, hovered.command)) { + if (std.meta.eql(action.command, hovered.command)) { continue; } if (distanceToAction(hovered.center, direction, action.center)) |distance| { if (new_distance == null or (new_distance != null and distance < new_distance.?)) { - new_action = Action{ + new_action = Element.Action{ .center = .{ if (input_state.left or input_state.right) action.center[0] else hovered.center[0], if (input_state.up or input_state.down) action.center[1] else hovered.center[1], @@ -368,46 +329,6 @@ pub fn main() !void { hovered_action = new_action; } - while (root_element == null) { - _ = response_arena.reset(.retain_capacity); - const current_state = history.items[history.items.len - 1]; - const response = try current_state.handler(response_arena.allocator(), Request{ - .game_state = current_state, - .command = request_command, - }); - - switch (response) { - .page => |page_root_element| root_element = page_root_element, - .transition => |transition| { - if (transition.reset_selection) { - hovered_action = null; - } - const new_game_state = try transition.game_state.clone(gpa.allocator()); - if (history_type == .transient) { - var transient_state = history.pop(); - transient_state.deinit(); - } - switch (transition.history_type) { - .start => { - for (history.items) |*state| { - state.deinit(); - } - history.clearRetainingCapacity(); - try history.append(new_game_state); - }, - .important => try history.append(new_game_state), - .transient => { - try history.append(new_game_state); - }, - } - history_type = transition.history_type; - - if (request_command) |command| gpa.allocator().free(command); - request_command = null; - }, - } - } - gl.clearColor(0.7, 0.5, 0.5, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -448,44 +369,42 @@ pub fn main() !void { else => unreachable, }; - if (root_element) |root| { - root.interface.render( - root.pointer, - &canvas, - .{ - .hovered = if (hovered_action) |ha| ha.command else null, - .deck = deck_sprites, - .font = canvas.font, - }, - .{ 0, 0 }, - .{ - @floatFromInt(window_size[0]), - @floatFromInt(window_size[1]), - }, - ); - } + main_view.element().interface.render( + main_view.element().pointer, + &canvas, + .{ + .hovered = if (hovered_action) |ha| ha.command else null, + .deck = deck_sprites, + .font = canvas.font, + }, + .{ 0, 0 }, + .{ + @floatFromInt(window_size[0]), + @floatFromInt(window_size[1]), + }, + ); + + _ = canvas.printText(.{ 0, 0 }, "#actions = {}", .{actions.items.len}, .{}); canvas.end(); seizer.backend.glfw.c.glfwSwapBuffers(window); - if (root_element) |root| { - actions.clearRetainingCapacity(); - try root.interface.get_actions( - root.pointer, - &actions, - .{ - .hovered = if (hovered_action) |ha| ha.command else null, - .deck = deck_sprites, - .font = canvas.font, - }, - .{ 0, 0 }, - .{ - @floatFromInt(window_size[0]), - @floatFromInt(window_size[1]), - }, - ); - } + actions.clearRetainingCapacity(); + try main_view.element().interface.get_actions( + main_view.element().pointer, + &actions, + .{ + .hovered = if (hovered_action) |ha| ha.command else null, + .deck = deck_sprites, + .font = canvas.font, + }, + .{ 0, 0 }, + .{ + @floatFromInt(window_size[0]), + @floatFromInt(window_size[1]), + }, + ); } } @@ -507,538 +426,8 @@ fn distanceToAction(origin: [2]f32, direction: [2]f32, other_pos: [2]f32) ?f32 { } } -const Action = struct { - center: [2]f32, - /// A string representing what should occur if this action is taken - command: []const u8, -}; - -const RenderResources = struct { - hovered: ?[]const u8, - deck: DeckSprites, - font: seizer.Canvas.Font, -}; - -pub const Element = struct { - pointer: ?*anyopaque, - interface: *const Interface, - - pub const Error = error{ - OutOfMemory, - }; - - pub const Interface = struct { - minimum_size: *const fn (?*anyopaque, RenderResources) [2]f32, - render: *const fn (?*anyopaque, *seizer.Canvas, RenderResources, [2]f32, [2]f32) void, - get_actions: *const fn (?*anyopaque, *std.ArrayList(Action), RenderResources, [2]f32, [2]f32) Error!void, - }; -}; - -pub const Page = struct { - allocator: std.mem.Allocator, - children: std.ArrayListUnmanaged(Child), - - pub const Child = struct { - /// Where is the child attached on the parent? Imagine it as a pin going through - /// both the parent and child element. This defines where on the parent that pin - /// passes through. - /// - /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless - /// the numbers are negative. - anchor_in_parent: [2]f32, - /// Where is the child attached on the parent? Imagine it as a pin going through - /// both the parent and child element. This defines where on the child that pin - /// passes through. - /// - /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless - /// the numbers are negative. - anchor_in_child: [2]f32, - element: Element, - }; - - pub fn create(allocator: std.mem.Allocator) !*@This() { - const this = try allocator.create(@This()); - errdefer allocator.destroy(this); - this.* = .{ - .allocator = allocator, - .children = .{}, - }; - return this; - } - - pub fn addElement(this: *@This(), anchor_in_parent: [2]f32, anchor_in_child: [2]f32, child_element: Element) !void { - try this.children.append(this.allocator, Child{ - .anchor_in_parent = anchor_in_parent, - .anchor_in_child = anchor_in_child, - .element = child_element, - }); - } - - pub fn element(this: *@This()) Element { - return Element{ - .pointer = this, - .interface = &Element.Interface{ - .minimum_size = &element_minimum_size, - .render = &element_render, - .get_actions = &element_get_actions, - }, - }; - } - - pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - var minimum_size = [2]f32{ 0, 0 }; - for (this.children.items) |child| { - const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); - - minimum_size = .{ - @max(minimum_size[0], child_size[0]), - @max(minimum_size[1], child_size[1]), - }; - } - - return minimum_size; - } - - pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - const parent_size = [2]f32{ - max[0] - min[0], - max[1] - min[1], - }; - - for (this.children.items) |child| { - const pos_in_parent = [2]f32{ - child.anchor_in_parent[0] * parent_size[0], - child.anchor_in_parent[1] * parent_size[1], - }; - - const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); - - const pos_in_child = [2]f32{ - child.anchor_in_child[0] * child_size[0], - child.anchor_in_child[1] * child_size[1], - }; - - const child_min = [2]f32{ - @max(min[0], pos_in_parent[0] - pos_in_child[0]), - @max(min[1], pos_in_parent[1] - pos_in_child[1]), - }; - const child_max = [2]f32{ - @min(max[0], pos_in_parent[0] + (child_size[0] - pos_in_child[0])), - @min(max[1], pos_in_parent[1] + (child_size[1] - pos_in_child[1])), - }; - - child.element.interface.render(child.element.pointer, canvas, render_resources, child_min, child_max); - } - } - - pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - const parent_size = [2]f32{ - max[0] - min[0], - max[1] - min[1], - }; - - for (this.children.items) |child| { - const pos_in_parent = [2]f32{ - child.anchor_in_parent[0] * parent_size[0], - child.anchor_in_parent[1] * parent_size[1], - }; - - const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); - - const pos_in_child = [2]f32{ - child.anchor_in_child[0] * child_size[0], - child.anchor_in_child[1] * child_size[1], - }; - - const child_min = [2]f32{ - pos_in_parent[0] - pos_in_child[0], - pos_in_parent[1] - pos_in_child[1], - }; - const child_max = [2]f32{ - pos_in_parent[0] + (child_size[0] - pos_in_child[0]), - pos_in_parent[1] + (child_size[1] - pos_in_child[1]), - }; - - try child.element.interface.get_actions(child.element.pointer, actions, render_resources, child_min, child_max); - } - } -}; - -pub const HBox = struct { - allocator: std.mem.Allocator, - children: std.ArrayListUnmanaged(Element), - - pub fn create(allocator: std.mem.Allocator) !*@This() { - const this = try allocator.create(@This()); - errdefer allocator.destroy(this); - this.* = .{ - .allocator = allocator, - .children = .{}, - }; - return this; - } - - pub fn addElement(this: *@This(), child_element: Element) !void { - try this.children.append(this.allocator, child_element); - } - - pub fn element(this: *@This()) Element { - return Element{ - .pointer = this, - .interface = &Element.Interface{ - .minimum_size = &element_minimum_size, - .render = &element_render, - .get_actions = &element_get_actions, - }, - }; - } - - pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - var minimum_size = [2]f32{ 0, 0 }; - for (this.children.items) |child| { - const child_size = child.interface.minimum_size(child.pointer, render_resources); - - minimum_size = .{ - minimum_size[0] + child_size[0], - @max(minimum_size[1], child_size[1]), - }; - } - - return minimum_size; - } - - pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - if (this.children.items.len == 0) return; - - const parent_size = [2]f32{ - max[0] - min[0], - max[1] - min[1], - }; - - var filled_space: f32 = 0; - for (this.children.items) |child| { - const child_size = child.interface.minimum_size(child.pointer, render_resources); - - filled_space += child_size[0]; - } - - const empty_space = parent_size[0] - filled_space; - - const num_spaces = if (empty_space > 0) this.children.items.len + 1 else this.children.items.len - 1; - - const space_around = empty_space / @as(f32, @floatFromInt(num_spaces)); - - var x: f32 = min[0] + @max(space_around, 0); - for (this.children.items) |child| { - const child_size = child.interface.minimum_size(child.pointer, render_resources); - - const child_min = [2]f32{ x, min[1] }; - const child_max = [2]f32{ x + child_size[0], max[1] }; - - child.interface.render(child.pointer, canvas, render_resources, child_min, child_max); - - x += child_size[0] + space_around; - } - } - - pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - if (this.children.items.len == 0) return; - - const parent_size = [2]f32{ - max[0] - min[0], - max[1] - min[1], - }; - - var filled_space: f32 = 0; - for (this.children.items) |child| { - const child_size = child.interface.minimum_size(child.pointer, render_resources); - - filled_space += child_size[0]; - } - - const empty_space = parent_size[0] - filled_space; - - const space_around = empty_space / @as(f32, @floatFromInt((this.children.items.len + 1))); - - var x: f32 = min[0] + space_around; - for (this.children.items) |child| { - const child_size = child.interface.minimum_size(child.pointer, render_resources); - - const child_min = [2]f32{ x, min[1] }; - const child_max = [2]f32{ x + child_size[0], max[1] }; - - try child.interface.get_actions(child.pointer, actions, render_resources, child_min, child_max); - - x += child_size[0] + space_around; - } - } -}; - -pub const Pile = struct { - allocator: std.mem.Allocator, - cards: []Card, - hidden: bool = false, - command: ?[]const u8 = null, - - pub fn create(allocator: std.mem.Allocator, cards: []const Card) !*@This() { - const this = try allocator.create(@This()); - errdefer allocator.destroy(this); - - const cards_owned = try allocator.dupe(Card, cards); - errdefer allocator.free(cards_owned); - - this.* = .{ - .allocator = allocator, - .cards = cards_owned, - }; - return this; - } - - pub fn element(this: *@This()) Element { - return Element{ - .pointer = this, - .interface = &Element.Interface{ - .minimum_size = &element_minimum_size, - .render = &element_render, - .get_actions = &element_get_actions, - }, - }; - } - - pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - return .{ - @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), - @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) + @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) * @as(f32, @floatFromInt(this.cards.len)) / 52.0 * 0.25, - }; - } - - pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - const start_y: f32 = max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])); - - for (this.cards, 0..) |card, i| { - const oy = -@as(f32, @floatFromInt(i)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4)); - if (this.hidden) { - render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.back, .{ - min[0], - start_y + oy, - }, .{}); - } else { - render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.getTileForCard(card), .{ - min[0], - start_y + oy, - }, .{}); - } - } - - if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) { - const oy = -@as(f32, @floatFromInt(this.cards.len)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4)); - canvas.rect( - .{ min[0], start_y + oy }, - .{ @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), @floatFromInt(render_resources.deck.tilesheet.tile_size[1]) }, - .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, - ); - } - } - - pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - if (this.command) |command| { - const center = [2]f32{ - min[0] + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])) / 2, - max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) * (2.0 - @as(f32, @floatFromInt(this.cards.len)) / (52.0 * 4)), - }; - - try actions.append(.{ - .center = center, - .command = command, - }); - } - } -}; - -pub const CardElement = struct { - allocator: std.mem.Allocator, - visual: Visual, - command: ?[]const u8, - marked: bool, - - const Visual = union(enum) { - back, - card: Card, - }; - - pub fn create(allocator: std.mem.Allocator, options: struct { visual: Visual, command: ?[]const u8, marked: ?bool = null }) !*@This() { - const this = try allocator.create(@This()); - errdefer allocator.destroy(this); - - this.* = .{ - .allocator = allocator, - .visual = options.visual, - .command = options.command, - .marked = options.marked orelse false, - }; - return this; - } - - pub fn element(this: *@This()) Element { - return Element{ - .pointer = this, - .interface = &Element.Interface{ - .minimum_size = &element_minimum_size, - .render = &element_render, - .get_actions = &element_get_actions, - }, - }; - } - - pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { - const this: *@This() = @ptrCast(@alignCast(pointer)); - _ = this; - - return .{ - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])), - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])), - }; - } - - pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - const mark_offset = if (this.marked) - [2]f32{ - 0, - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) * -0.4, - } - else - [2]f32{ 0, 0 }; - - switch (this.visual) { - .back => render_resources.deck.tilesheet.renderTile( - canvas, - render_resources.deck.back, - .{ min[0] + mark_offset[0], min[1] + mark_offset[1] }, - .{}, - ), - .card => |card| render_resources.deck.tilesheet.renderTile( - canvas, - render_resources.deck.getTileForCard(card), - .{ @floor(min[0] + mark_offset[0]), @floor(min[1] + mark_offset[1]) }, - .{}, - ), - } - - if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) { - canvas.rect( - .{ min[0] + mark_offset[0], min[1] + mark_offset[1] }, - .{ max[0] - min[0], max[1] - min[1] }, - .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, - ); - } - } - - pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - _ = render_resources; - - if (this.command) |command| { - try actions.append(.{ - .center = [2]f32{ - (min[0] + max[0]) / 2, - (min[1] + max[1]) / 2, - }, - .command = command, - }); - } - } -}; - -pub const TextElement = struct { - allocator: std.mem.Allocator, - text: []const u8, - command: ?[]const u8, - - pub fn create(allocator: std.mem.Allocator, options: struct { text: []const u8, command: ?[]const u8 }) !*@This() { - const this = try allocator.create(@This()); - errdefer allocator.destroy(this); - - this.* = .{ - .allocator = allocator, - .text = options.text, - .command = options.command, - }; - return this; - } - - pub fn element(this: *@This()) Element { - return Element{ - .pointer = this, - .interface = &Element.Interface{ - .minimum_size = &element_minimum_size, - .render = &element_render, - .get_actions = &element_get_actions, - }, - }; - } - - pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { - const this: *@This() = @ptrCast(@alignCast(pointer)); - return render_resources.font.textSize(this.text, 1); - } - - pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - _ = max; - - const text_size = canvas.writeText(min, this.text, .{ - .color = if (this.command != null) .{ 0xFF, 0xFF, 0xFF, 0xFF } else .{ 0xAA, 0xAA, 0xAA, 0xFF }, - }); - - if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) { - canvas.rect( - .{ min[0], min[1] }, - text_size, - .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }, - ); - } - } - - pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void { - const this: *@This() = @ptrCast(@alignCast(pointer)); - - _ = render_resources; - - if (this.command) |command| { - try actions.append(.{ - .center = [2]f32{ - (min[0] + max[0]) / 2, - (min[1] + max[1]) / 2, - }, - .command = command, - }); - } - } -}; - /// Handler at the start of a turn, while the player is drawing a card. -fn drawCardHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response { +fn drawCardHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { if (request.command) |command| { if (std.mem.eql(u8, command, "draw draw_pile")) { var new_game_state = try request.game_state.clone(arena); @@ -1059,34 +448,34 @@ fn drawCardHandler(arena: std.mem.Allocator, request: Request) HandlerError!Resp } } - var draw_pile = try Pile.create(arena, request.game_state.draw_pile.items); + var draw_pile = try Element.Pile.create(arena, request.game_state.draw_pile.items); draw_pile.hidden = true; draw_pile.command = "draw draw_pile"; - var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items); + var discard_pile = try Element.Pile.create(arena, request.game_state.discard_pile.items); discard_pile.command = "draw discard_pile"; - var hand = try HBox.create(arena); + var hand = try Element.HBox.create(arena); for (request.game_state.hands[0].items) |card| { - var card_element = try CardElement.create(arena, .{ + var card_element = try Element.CardElement.create(arena, .{ .visual = .{ .card = card }, .command = null, }); try hand.addElement(card_element.element()); } - var draw_discard_hbox = try HBox.create(arena); + var draw_discard_hbox = try Element.HBox.create(arena); try draw_discard_hbox.addElement(draw_pile.element()); try draw_discard_hbox.addElement(discard_pile.element()); - var page = try Page.create(arena); + var page = try Element.Page.create(arena); try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, draw_discard_hbox.element()); try page.addElement(.{ 0.5, 1 }, .{ 0.5, 1 }, hand.element()); return Response{ .page = page.element() }; } -fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response { +fn playerTurnHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { if (request.command) |command| handle_command: { if (std.mem.startsWith(u8, command, "mark ")) { var iter = std.mem.tokenizeScalar(u8, command, ' '); @@ -1141,7 +530,7 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re } } - var new_meld_text = try TextElement.create(arena, .{ + var new_meld_text = try Element.Text.create(arena, .{ .text = "New Meld", .command = null, }); @@ -1149,20 +538,20 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re new_meld_text.command = "new-meld"; } - var melds_hbox = try HBox.create(arena); + var melds_hbox = try Element.HBox.create(arena); try melds_hbox.addElement(new_meld_text.element()); - var draw_pile = try Pile.create(arena, request.game_state.draw_pile.items); + var draw_pile = try Element.Pile.create(arena, request.game_state.draw_pile.items); draw_pile.hidden = true; - var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items); + var discard_pile = try Element.Pile.create(arena, request.game_state.discard_pile.items); if (request.game_state.marked_cards.count() == 1) { discard_pile.command = "discard"; } - var hand = try HBox.create(arena); + var hand = try Element.HBox.create(arena); for (request.game_state.hands[0].items) |card| { - var card_element = try CardElement.create(arena, .{ + var card_element = try Element.Card.create(arena, .{ .visual = .{ .card = card }, .command = try std.fmt.allocPrint(arena, "mark {} of {s}", .{ card.rank, @tagName(card.suit) }), .marked = request.game_state.marked_cards.contains(card), @@ -1170,11 +559,11 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re try hand.addElement(card_element.element()); } - var draw_discard_hbox = try HBox.create(arena); + var draw_discard_hbox = try Element.HBox.create(arena); try draw_discard_hbox.addElement(draw_pile.element()); try draw_discard_hbox.addElement(discard_pile.element()); - var page = try Page.create(arena); + var page = try Element.Page.create(arena); try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, draw_discard_hbox.element()); try page.addElement(.{ 0.5, 1 }, .{ 0.5, 1 }, hand.element()); try page.addElement(.{ 0.5, 0 }, .{ 0.5, 0 }, melds_hbox.element()); @@ -1182,7 +571,7 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re return Response{ .page = page.element() }; } -fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response { +fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { if (request.command) |command| { if (std.mem.eql(u8, command, "end_turn")) { var new_game_state = try request.game_state.clone(arena); @@ -1195,16 +584,16 @@ fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) HandlerEr } } - var melds_hbox = try HBox.create(arena); + var melds_hbox = try Element.HBox.create(arena); - var draw_pile = try Pile.create(arena, request.game_state.draw_pile.items); + var draw_pile = try Element.Pile.create(arena, request.game_state.draw_pile.items); draw_pile.hidden = true; - var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items); + var discard_pile = try Element.Pile.create(arena, request.game_state.discard_pile.items); - var hand = try HBox.create(arena); + var hand = try Element.HBox.create(arena); for (request.game_state.hands[0].items) |card| { - var card_element = try CardElement.create(arena, .{ + var card_element = try Element.Card.create(arena, .{ .visual = .{ .card = card }, .command = null, .marked = false, @@ -1212,16 +601,16 @@ fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) HandlerEr try hand.addElement(card_element.element()); } - var draw_discard_hbox = try HBox.create(arena); + var draw_discard_hbox = try Element.HBox.create(arena); try draw_discard_hbox.addElement(draw_pile.element()); try draw_discard_hbox.addElement(discard_pile.element()); - var end_turn_text = try TextElement.create(arena, .{ + var end_turn_text = try Element.TextElement.create(arena, .{ .text = "End Turn", .command = "end_turn", }); - var page = try Page.create(arena); + var page = try Element.Page.create(arena); try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, draw_discard_hbox.element()); try page.addElement(.{ 0.5, 1 }, .{ 0.5, 1 }, hand.element()); try page.addElement(.{ 0.5, 0 }, .{ 0.5, 0 }, melds_hbox.element()); @@ -1230,6 +619,75 @@ fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) HandlerEr return Response{ .page = page.element() }; } +fn mainMenuHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { + if (request.command) |command| { + if (std.mem.eql(u8, command, "join-multiplayer-game")) { + var new_game_state = try request.game_state.clone(arena); + new_game_state.handler = &joinMultiplayerGameHandler; + + return Response{ .transition = .{ + .game_state = new_game_state, + .history_type = .start, + } }; + } else if (std.mem.eql(u8, command, "host-multiplayer-game")) { + var new_game_state = try request.game_state.clone(arena); + new_game_state.handler = &hostMultiplayerGameHandler; + + return Response{ .transition = .{ + .game_state = new_game_state, + .history_type = .start, + } }; + } + } + + var join_multiplayer_text = try Element.Textcreate(arena, .{ + .text = "Join Multiplayer Game", + .command = "join-multiplayer-game", + }); + + var host_multiplayer_text = try Element.Text.create(arena, .{ + .text = "Host Multiplayer Game", + .command = "host-multiplayer-game", + }); + + var play_game_hbox = try Element.HBox.create(arena); + try play_game_hbox.addElement(join_multiplayer_text.element()); + try play_game_hbox.addElement(host_multiplayer_text.element()); + + var page = try Element.Page.create(arena); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, play_game_hbox.element()); + + return Response{ .page = page.element() }; +} + +fn hostMultiplayerGameHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { + _ = request; + + var text = try Element.Text.create(arena, .{ + .text = "Hosting Multiplayer Game", + .command = null, + }); + + var page = try Element.Page.create(arena); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + + return Response{ .page = page.element() }; +} + +fn joinMultiplayerGameHandler(arena: std.mem.Allocator, request: Request) Handler.Error!Response { + _ = request; + + var text = try Element.Text.create(arena, .{ + .text = "Joining Multiplayer Game", + .command = null, + }); + + var page = try Element.Page.create(arena); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + + return Response{ .page = page.element() }; +} + fn isValidRummyMeld(cards: []const Card) bool { std.debug.assert(std.sort.isSorted(Card, cards, {}, rummyHandSort)); if (cards.len < 3) { @@ -1313,9 +771,17 @@ test { _ = @import("./assets.zig"); } +const Handler = protocol.Handler; +const Request = protocol.Request; +const Response = protocol.Response; const DeckSprites = assets.DeckSprites; const Card = assets.Card; +const LocalUI = @import("./LocalUI.zig"); +const Element = @import("./Element.zig"); + +const protocol = @import("./protocol.zig"); +const c = @import("./c.zig"); const assets = @import("./assets.zig"); const seizer = @import("seizer"); const gl = seizer.gl; diff --git a/src/protocol.zig b/src/protocol.zig new file mode 100644 index 0000000..31cf8e5 --- /dev/null +++ b/src/protocol.zig @@ -0,0 +1,31 @@ +pub const Handler = struct { + pointer: ?*anyopaque, + interface: *const Interface, + + pub const Error = error{OutOfMemory}; + pub const Fn = *const fn (Request) Error!Response; + pub const Interface = struct { + handle: *const fn (?*anyopaque, Request) Error!Response, + deinit: ?*const fn (?*anyopaque) void, + }; +}; + +pub const Reference = union(enum) { + handler: Handler, +}; + +pub const Request = struct { + allocator: std.mem.Allocator, + body: ?[]const u8 = null, +}; +pub const Response = struct { + arena: std.heap.ArenaAllocator, + body: Body, + + const Body = union(enum) { + element: Element, + }; +}; + +const Element = @import("./Element.zig"); +const std = @import("std");