//! By convention, root.zig is the root source file when making a library. const std = @import("std"); // base drawing functions // text, // rect, // texture, // sissor pub const VERSION_MAJOR = 0; pub const VERSION_MINOR = 1; pub const VERSION_PATCH = 0; pub const VERSION = std.fmt.comptimePrint("{}.{}.{}", .{ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, }); pub const Real = f32; const Spaceing = 4; pub fn TextType( comptime FontType: type, comptime getTextWidthFunc: fn (string: []const u8, FontType) Real, comptime getTextHeightFunc: fn (string: []const u8, FontType) Real, ) type { return struct { const Optional = struct { font: FontType, }; const Self = @This(); string: []const u8, colour: Colour, background: Colour, font: FontType, pub fn init(string: []const u8, font: FontType, colour: Colour) Self { return Self{ .string = string, .font = font, .colour = colour, .background = .{ .r = 0, .b = 0, .g = 0, .a = 0, }, }; } pub fn setBackground(self: Self, background: Colour) Self { var s = self; s.background = background; return s; } pub fn getTextWidth(self: Self) Real { return getTextWidthFunc(self.string, self.font); } pub fn getTextHeight(self: Self) Real { return getTextHeightFunc(self.string, self.font); } }; } pub fn TextureType(comptime TextureData: type) type { return struct { texture: TextureData, rect: Rect, }; } pub const Rect = struct { w: Real = 0, h: Real = 0, }; pub const Pos = struct { x: Real = 0, y: Real = 0, }; const Sizing = enum { fixed, grow, min, max, fit, }; pub const Padding = struct { top: Real = 0, bottom: Real = 0, left: Real = 0, right: Real = 0, pub fn all(padding: Real) Padding { return .{ .top = padding, .bottom = padding, .left = padding, .right = padding, }; } pub fn topf(padding: Real) Padding { return .{ .top = padding, }; } pub fn bottomf(padding: Real) Padding { return .{ .bottom = padding, }; } pub fn leftf(padding: Real) Padding { return .{ .left = padding, }; } pub fn rightf(padding: Real) Padding { return .{ .right = padding, }; } }; pub const Layout = enum { right_to_left, top_to_bottom, }; fn printWithLevel(writer: *std.Io.Writer, level: usize, comptime fmt: []const u8, args: anytype) !void { for (0..level * Spaceing) |_| try writer.writeByte(' '); try writer.print(fmt, args); } pub const Style = struct { size_x: Sizing = .fit, size_y: Sizing = .fit, background_colour: Colour = .{}, rounded: ?Real = null, // require re-draw padding: Padding = .{}, child_gap: Real = 0, layout: Layout = .top_to_bottom, pub fn format( self: @This(), writer: *std.Io.Writer, ) std.Io.Writer.Error!void { try printWithLevel(writer, 0, "style : .{{\n", .{}); try self.printStyle(writer, 0); } pub fn printStyle(style: Style, writer: *std.Io.Writer, level: usize) std.Io.Writer.Error!void { try printWithLevel(writer, level + 1, "size_x : {any},\n", .{style.size_x}); try printWithLevel(writer, level + 1, "size_y : {any},\n", .{style.size_y}); try printWithLevel(writer, level + 1, "background_colour : {any},\n", .{style.background_colour}); try printWithLevel(writer, level + 1, "rounded : {any},\n", .{style.rounded}); try printWithLevel(writer, level + 1, "padding : {any},\n", .{style.padding}); try printWithLevel(writer, level + 1, "child_gap : {any},\n", .{style.child_gap}); try printWithLevel(writer, level + 1, "layout : {any},\n", .{style.layout}); try printWithLevel(writer, level, "}},\n", .{}); } }; pub const Colour = struct { r: u8 = 0, g: u8 = 0, b: u8 = 0, a: u8 = 255, }; pub const MouseState = struct { pos: Pos, left: bool, right: bool, middle: bool, }; pub fn pointinRect(point: Pos, rect_pos: Pos, rect: Rect) bool { if (point.x > rect_pos.x and point.x < rect_pos.x + rect.w and point.y > rect_pos.y and point.y < rect_pos.y + rect.h) return true; return false; } // might add an easy conversion // comptime mouse_type: type, // comptime getMouseState: fn (mouse_type) MouseState, // comptime getKeyboardInput: fn(keyboard_type) []const u8, // comptime renderTextFunc: fn (text_type, Pos) void, // comptime renderRectFunc: fn (Rect, Pos, Colour, rounding: ?Real) void, // comptime renderTextureFunc: fn (texture_type, Pos) void, pub fn Shoots( comptime text_type: type, comptime texture_type: type, ) type { return struct { const Self = @This(); pub const Text = text_type; pub const Texture = texture_type; const NodeTypes = enum { element, text, texture, }; pub const Node = union(NodeTypes) { element: Ele, text: Text, texture: Texture, fn printTreeHelper(writer: *std.Io.Writer, node: Node, level: usize) !void { switch (node) { .element => { try printWithLevel(writer, level, "{s} : Ele{{\n", .{node.element.name}); try printWithLevel(writer, level + 1, "pos : {any},\n", .{node.element.pos}); try printWithLevel(writer, level + 1, "on_click : {?},\n", .{node.element.on_click}); try printWithLevel(writer, level + 1, "rect : {any},\n", .{node.element.rect}); try printWithLevel(writer, level + 1, "style : .{{\n", .{}); try node.element.style.printStyle(writer, level + 1); try printWithLevel(writer, level + 1, "children : {{\n", .{}); for (node.element.children, 0..) |_, i| { try printTreeHelper(writer, node.element.children[i], (level + 2)); } try printWithLevel(writer, level + 1, "}},\n", .{}); try printWithLevel(writer, level, "}},\n", .{}); }, .text => { try printWithLevel(writer, level, "text : {s}.\n", .{node.text.string}); return; }, .texture => { try printWithLevel(writer, level, "texture : {any},\n", .{node.texture}); return; }, } } pub fn format( self: Node, writer: *std.Io.Writer, ) std.Io.Writer.Error!void { try printTreeHelper(writer, self, 0); } }; pub const Ele = struct { const Self = @This(); name: []const u8 = "", z_index: usize = 0, pos: Pos = .{}, rect: Rect = .{}, style: Style = .{}, children: []const Node = &[_]Node{}, on_hover: ?Interact = null, on_click: ?Interact = null, // the on hover call back will be ran allow_on_hover_when_occluded: bool = true, allow_on_click_when_occluded: bool = false, }; pub const Interact = struct { // returns true if the ui must be drawn again func: *const fn (self: *Ele, mouse_state: MouseState, *anyopaque) bool, data: *anyopaque, }; const RenderCommandType = enum { rect, text, texture, }; pub const RenderCommand = union(RenderCommandType) { rect: struct { rect: Rect, pos: Pos, colour: Colour, rounding: ?Real, z_index: usize = 0, }, text: struct { text: Text, pos: Pos, z_index: usize = 0, }, texture: struct { texture: Texture, pos: Pos, z_index: usize = 0, }, }; // focused: *Node, // /// highly suggested that you use an area for the alloc // pub fn init() Self { // return Self{ // .alloc = alloc, // }; // } pub inline fn ElementWborder( boarder_width: Padding, boarder_colour: Colour, ele: Ele, ) Node { var internal = ele; internal.rect.h -= boarder_width.bottom + boarder_width.top; internal.rect.w -= boarder_width.right + boarder_width.left; return Node{ .element = .{ .name = ele.name ++ "_boarder", .style = .{ .padding = boarder_width, .background_colour = boarder_colour, .rounded = ele.style.rounded, }, .children = &[_]Node{ Element(internal), }, }, }; } pub inline fn Element(ele: Ele) Node { return Node{ .element = ele, }; } pub inline fn Txt(text: Text) Node { return Element( .{ .style = .{ .background_colour = text.background, }, .children = &[_]Node{.{ .text = text, }}, }, ); } pub fn closeElement(node: *Node, parent: *Node) void { switch (node.*) { .element => { // layout const child_gap: Real = parent.element.style.child_gap; switch (parent.element.style.layout) { .right_to_left => parent.element.rect.w += child_gap, .top_to_bottom => parent.element.rect.h += child_gap, } const padding = node.element.style.padding; node.element.rect.w += padding.left + padding.right; node.element.rect.h += padding.top + padding.bottom; switch (parent.element.style.layout) { .right_to_left => { parent.element.rect.h = @max(node.element.rect.h, parent.element.rect.h); parent.element.rect.w += node.element.rect.w; }, .top_to_bottom => { parent.element.rect.h += node.element.rect.h; parent.element.rect.w = @max(node.element.rect.w, parent.element.rect.w); }, } }, .text => { parent.element.rect.h += node.text.getTextHeight(); // std.debug.print("parent.element.rect.h : {}\n", .{parent.element.rect.h}); parent.element.rect.w += node.text.getTextWidth(); // std.debug.print("parent.element.rect.w : {}\n", .{parent.element.rect.w}); }, .texture => return, } } fn deepCloneHelper(node: Node, node_location: *Node, alloc: std.mem.Allocator) !void { // std.debug.print("nodeloc : {}\n", .{node_location}); node_location.* = node; // std.debug.print("nodeloc : {}\n", .{node_location}); switch (node) { .element => |ele| { // BASE CASE // node_location.element.name = "cloned"; if (ele.children.len == 0) return; const children = try alloc.alloc(Node, ele.children.len); // std.debug.print("children Alloc {x}\n", .{@intFromPtr(children.ptr)}); for (ele.children, 0..) |child, i| { try deepCloneHelper(child, &children[i], alloc); // std.debug.print("base node_loc {x}\n", .{@intFromPtr(node_location.element.children.ptr)}); node_location.*.element.children = children; // std.debug.print("up[date node_loc {x}\n", .{@intFromPtr(node_location.element.children.ptr)}); } }, else => return, } } // we deep clone the root node as this make it easier for the temp node fn deepClone(node: Node, alloc: std.mem.Allocator) !Node { var new_node = node; switch (new_node) { .element => |ele| { // BASE CASE if (ele.children.len == 0) return new_node; const children = try alloc.alloc(Node, ele.children.len); for (ele.children, 0..) |child, i| { try deepCloneHelper(child, &children[i], alloc); new_node.element.children = children; } }, else => return new_node, } return new_node; } pub fn getRenderCommandsHelper(node: Node, alloc: std.mem.Allocator, command_list: *std.ArrayList(RenderCommand)) !void { switch (node) { .element => { // std.debug.print("parent name : {s}\n", .{parent.element.name}); try command_list.append(alloc, .{ .rect = .{ .colour = node.element.style.background_colour, .pos = node.element.pos, .rect = node.element.rect, .rounding = node.element.style.rounded, .z_index = node.element.z_index, }, }); for (node.element.children, 0..) |_, i| { switch (node.element.children[i]) { .element => try getRenderCommandsHelper(node.element.children[i], alloc, command_list), .text => |text| try command_list.append(alloc, .{ .text = .{ .pos = node.element.pos, .text = text, .z_index = node.element.z_index, } }), .texture => |texture| try command_list.append(alloc, .{ .texture = .{ .pos = node.element.pos, .texture = texture, .z_index = node.element.z_index, } }), } } }, else => return, // the commands get created above, since the text and texture need a pos // but they dont have the pos one the parent has it } } pub fn getRenderCommands(node: Node, alloc: std.mem.Allocator) !std.ArrayList(RenderCommand) { var render_commands: std.ArrayList(RenderCommand) = .empty; try getRenderCommandsHelper(node, alloc, &render_commands); return render_commands; } fn computeSizes(node: *Node, parent: *Node) void { switch (node.*) { .element => { // std.debug.print("parent name : {s}\n", .{parent.element.name}); for (node.element.children, 0..) |_, i| { computeSizes(@constCast(&node.element.children[i]), node); } // cheaters way to beat the fence post problem // TODO fix this switch (node.element.style.layout) { .right_to_left => node.element.rect.w -= node.element.style.child_gap, .top_to_bottom => node.element.rect.h -= node.element.style.child_gap, } }, else => {}, } closeElement(node, parent); } fn filterGrowableChildren(index: *usize, node: Node) ?*Ele { switch (node) { .element => { if (node.element.children.len == 0) return null; var i = index.*; while (i < node.element.children.len - 1) : (i += 1) { switch (node.element.children[i]) { .element => |e| { if (e.style.size_x == .grow or e.style.size_y == .grow) { index.* = i + 1; return @constCast(&node.element.children[i].element); } }, else => continue, } } }, else => return null, } return null; } fn computeRemainingHeightAndWidth(node: Node) struct { Real, Real } { // growable code // std.debug.print("node.element.rect.w : {d}\n", .{node.element.rect.w}); var remaining_width = node.element.rect.w; var remaining_height = node.element.rect.h; // std.debug.print("remaining_width : {d}\n", .{remaining_width}); // std.debug.print("remaining_hieght : {d}\n", .{remaining_height}); remaining_width -= node.element.style.padding.left + node.element.style.padding.right; remaining_height -= node.element.style.padding.top + node.element.style.padding.bottom; if (node.element.children.len == 0) return .{ 0, 0, }; // std.debug.print("remaining_width : {d}\n", .{remaining_width}); // std.debug.print("remaining_hieght : {d}\n", .{remaining_height}); // are there too many indentaions here maybe for (node.element.children) |child| { // std.debug.print("child : {}\n", .{child}); switch (child) { .element => { remaining_height -= child.element.rect.h; remaining_width -= child.element.rect.w; // std.debug.print("remaining_width : {d}\n", .{remaining_width}); // std.debug.print("remaining_hieght : {d}\n", .{remaining_height}); }, .texture => { switch (node.element.style.layout) { .top_to_bottom => { remaining_height -= child.texture.rect.h; remaining_width = @max(remaining_width, child.texture.rect.w); }, .right_to_left => { remaining_width -= child.texture.rect.w; remaining_height = @max(remaining_height, child.texture.rect.h); }, } }, else => {}, } } // std.debug.print("remaining_width : {d}\n", .{remaining_width}); // std.debug.print("remaining_hieght : {d}\n", .{remaining_height}); remaining_width -= (@as(Real, @floatFromInt(node.element.children.len -| 1)) * node.element.style.child_gap); remaining_height -= (@as(Real, @floatFromInt(node.element.children.len -| 1)) * node.element.style.child_gap); // std.debug.print("remaining_width : {d}\n", .{remaining_width}); // std.debug.print("remaining_hieght : {d}\n", .{remaining_height}); return .{ remaining_height, remaining_width, }; } pub fn getHeight(element: *const Ele) Real { return element.rect.h; } pub fn getWidth(element: *const Ele) Real { return element.rect.w; } pub fn addHeight(element: *Ele, height: Real) void { element.rect.h += height; } pub fn addWidth(element: *Ele, width: Real) void { element.rect.w += width; } pub fn getSizingY(element: *const Ele) Sizing { return element.style.size_y; } pub fn getSizingX(element: *const Ele) Sizing { return element.style.size_x; } fn growChildIndependedElements( node: Node, axis_x: bool, ) void { const padding_x = node.element.style.padding.left + node.element.style.padding.right; const padding_y = node.element.style.padding.left + node.element.style.padding.right; for (node.element.children, 0..) |child, i| { switch (child) { .element => |e| { if (e.style.size_x == .grow and axis_x) { @constCast(&node.element.children[i]).element.rect.w = node.element.rect.w - padding_x; } if (e.style.size_y == .grow and !axis_x) { @constCast(&node.element.children[i]).element.rect.h = node.element.rect.h - padding_y; } }, else => {}, } } } fn growChildDependedElements( node: Node, remaining_disance: Real, total_growable: usize, getAxis: *const fn (*const Ele) Real, addToAxis: *const fn (*Ele, Real) void, getSizing: *const fn (*const Ele) Sizing, ) void { // this algorithm is based on the one used in clay : https://github.com/nicbarker/clay var remaining = remaining_disance; while (remaining > 0) { var find_next_smallest_index: usize = 0; var smallest = filterGrowableChildren(&find_next_smallest_index, node) orelse return; var second_smallest = &Ele{ .rect = .{ .h = std.math.floatMax(Real), .w = std.math.floatMax(Real), } }; var width_to_add = remaining; var next_smallest_index: usize = 0; while (filterGrowableChildren(&next_smallest_index, node)) |e| { if (getAxis(e) < getAxis(smallest)) { second_smallest = smallest; smallest = e; } if (getAxis(e) > getAxis(smallest)) { second_smallest = if (getAxis(second_smallest) < getAxis(e)) second_smallest else e; width_to_add = getAxis(second_smallest) - getAxis(smallest); } } width_to_add = @min(width_to_add, remaining / @as(Real, @floatFromInt(total_growable))); if (width_to_add == 0) return; var grow_em: usize = 0; // std.debug.print("smallest ; {}\n", .{smallest.rect}); while (filterGrowableChildren(&grow_em, node)) |growable| { if (getAxis(growable) == getAxis(smallest)) { // std.debug.print("{s} before growable : {} + width_to_add {}\n", .{ growable.name, growable.rect, width_to_add }); if (getSizing(growable) == .grow) addToAxis(growable, width_to_add); // std.debug.print("after growable : {}\n", .{growable.rect}); remaining -= width_to_add; } } } } pub fn growChildern(node: *Node) void { // check that there are growable children var total_growable_index: usize = 0; var total_growable: usize = 0; while (filterGrowableChildren(&total_growable_index, node.*)) |_| : (total_growable += 1) continue; if (total_growable == 0) return; const remaining_height, const remaining_width = computeRemainingHeightAndWidth(node.*); // std.debug.print("remaining_height remaining_width : {} {}\n", .{ remaining_height, remaining_width }); if (node.element.style.layout == .top_to_bottom) { growChildIndependedElements(node.*, true); if (remaining_height > 0) growChildDependedElements( node.*, remaining_height, total_growable, &getHeight, &addHeight, getSizingY, ); } if (node.element.style.layout == .right_to_left) { growChildIndependedElements(node.*, false); if (remaining_width > 0) growChildDependedElements( node.*, remaining_width, total_growable, &getWidth, &addWidth, getSizingX, ); } } pub fn computeGrowElements(node: *Node) void { switch (node.*) { .element => { growChildern(node); for (node.element.children, 0..) |_, i| { computeGrowElements(@constCast(&node.element.children[i])); } }, else => return, } } fn computeChildernsPostions(ele: *Ele) void { const layout_style = ele.style.layout; var children = @constCast(ele.children); switch (layout_style) { .right_to_left => { var off_set_left: Real = 0; for (ele.children, 0..) |_, i| { const child_gap: Real = ele.style.child_gap; switch (ele.children[i]) { .element => { // off set it by the parent children[i].element.pos.x += ele.pos.x + ele.style.padding.left + off_set_left; children[i].element.pos.y += ele.pos.y + ele.style.padding.top; off_set_left += children[i].element.rect.w + child_gap; // add }, else => continue, } } }, .top_to_bottom => { var offset_top: Real = 0; for (ele.children, 0..) |_, i| { const child_gap: Real = ele.style.child_gap; switch (ele.children[i]) { .element => { // off set it by the parent children[i].element.pos.x += ele.pos.x + ele.style.padding.left; children[i].element.pos.y += ele.pos.y + ele.style.padding.top + offset_top; offset_top += children[i].element.rect.h + child_gap; // add }, else => continue, } } }, } } pub fn computePostionsHelper(node: *Node, index: usize) void { switch (node.*) { .element => { computeChildernsPostions(&node.element); node.element.z_index = index; for (node.element.children, 0..) |_, i| { computePostionsHelper(@constCast(&node.element.children[i]), index + 1); } }, else => return, } } pub fn computePositions(node: *Node) void { computePostionsHelper(node, 0); } // this could be done real quick fn filterHoveredNodes(node: Node, pos: Pos) ?*Node { switch (node) { .element => { for (node.element.children, 0..) |value, i| { switch (value) { .element => |ele| { if (pointinRect(pos, ele.pos, ele.rect)) { return @constCast(&node.element.children[i]); } }, else => continue, } } }, else => return null, } return null; } pub fn processInteractions(root: *Node, mouse_state: MouseState) bool { var needs_redraw = false; needs_redraw = true; var current_node = root; const clicked = mouse_state.left or mouse_state.middle or mouse_state.right; // std.debug.print("state : {}\n", .{mouse_state}); while (filterHoveredNodes(current_node.*, mouse_state.pos)) |node| { switch (node.*) { .element => |e| { // std.debug.print("clicked : {} hovered node name : {s}\n", .{ clicked, e.name }); if (e.on_hover != null and e.allow_on_hover_when_occluded) { needs_redraw |= node.element.on_hover.?.func(&node.element, mouse_state, node.element.on_click.?.data); } if (e.on_click != null and clicked and e.allow_on_click_when_occluded) { needs_redraw |= node.element.on_click.?.func(&node.element, mouse_state, node.element.on_click.?.data); } }, // .text => |t| std.debug.print("hovered node name : {s}\n", .{t.string}), else => {}, } current_node = node; } switch (current_node.*) { .element => { if (current_node.element.on_hover != null and !current_node.element.allow_on_hover_when_occluded) { needs_redraw |= current_node.element.on_hover.?.func( ¤t_node.element, mouse_state, current_node.element.on_click.?.data, ); } if (current_node.element.on_click != null and clicked and !current_node.element.allow_on_click_when_occluded) { needs_redraw |= current_node.element.on_click.?.func( ¤t_node.element, mouse_state, current_node.element.on_click.?.data, ); } }, else => {}, } return needs_redraw; } pub fn resolveSizing(alloc: std.mem.Allocator, root: Node) !Node { var new_tree: Node = try deepClone(root, alloc); var base_node: Node = Node{ .element = Ele{ .children = &[_]Node{ root, }, } }; computeSizes(&new_tree, &base_node); computeGrowElements(&new_tree); computePositions(&new_tree); return new_tree; } }; }