LOTS OF STUFF

* Move all global variables to UI_Context struct
* Add string formatting to API
* Allocate formatted (and const too, gross) strings via a frame allocator
* Add higher level Grid component

still lots of jank and built-in assumptions
master
Patrick Cleavelin 2023-07-11 16:06:40 -05:00
parent 8e7a5bb3a8
commit ad8ddae790
2 changed files with 841 additions and 675 deletions

View File

@ -3,45 +3,58 @@ const ui = @import("lib.zig");
const raylib = ui.raylib; const raylib = ui.raylib;
pub const Grid = struct { pub const Grid = struct {
fn MakeColumnHeader(columns: [][:0]const u8) !void { fn MakeColumnHeader(ctx: *ui.UIContext, columns: [][]const u8) !void {
_ = try ui.PushBox("GridColumnHeader", .{ .drawBackground = true, .drawBorder = false }, .leftToRight, .fitToChildren); _ = try ctx.PushBox("GridColumnHeader", .{ .drawBackground = true, .drawBorder = false }, .leftToRight, .fitToChildren);
defer ui.PopBox(); defer ctx.PopBox();
try ui.PushStyle(.{ // try ctx.PushStyle(.{ //
.color = raylib.WHITE, // .color = raylib.WHITE, //
.text_color = .{ .r = 0x18, .g = 0x90, .b = 0xff, .a = 0xff }, // .text_color = .{ .r = 0x18, .g = 0x90, .b = 0xff, .a = 0xff }, //
.text_size = 12, // .text_size = 12, //
.text_padding = 12, // .text_padding = 12, //
}); });
defer ui.PopStyle(); defer ctx.PopStyle();
for (columns) |column| { for (columns) |column| {
try MakeLabel(column, @as(i32, @intCast(columns.len))); try MakeLabel(ctx, column, @as(i32, @intCast(columns.len)));
} }
} }
pub fn MakeButton(label: [:0]const u8, items: i32) !bool { pub fn MakeButton(ctx: *ui.UIContext, label: []const u8, items: i32) !bool {
return try ui.MakeBox(label, .{ return try ctx.MakeBox(label, .{
.clickable = true, .clickable = true,
.drawText = true, .drawText = true,
}, .leftToRight, .{ .percentOfParent = ui.Vec2{ .x = 1.0 / @as(f32, @floatFromInt(items)), .y = 1.0 } }); }, .leftToRight, .{ .percentOfParent = ui.Vec2{ .x = 1.0 / @as(f32, @floatFromInt(items)), .y = 1.0 } });
} }
pub fn MakeLabel(label: [:0]const u8, items: i32) !void { pub fn MakeFormattedLabel(ctx: *ui.UIContext, comptime str: []const u8, args: anytype, items: i32) !void {
_ = try ui.MakeLabelWithLayout(label, .{ .percentOfParent = ui.Vec2{ .x = 1.0 / @as(f32, @floatFromInt(items)), .y = 1.0 } }); _ = try ctx.MakeFormattedLabelWithLayout(str, args, .{ .percentOfParent = ui.Vec2{ .x = 1.0 / @as(f32, @floatFromInt(items)), .y = 1.0 } });
} }
pub fn MakeGrid(comptime T: type, columns: [][:0]const u8, data: []T, MakeBody: *const fn (data: *const T, size: i32) anyerror!void) !void { pub fn MakeLabel(ctx: *ui.UIContext, label: []const u8, items: i32) !void {
_ = try ui.PushBox("GridContainer", .{ .drawBackground = true }, .topToBottom, .fill); _ = try ctx.MakeLabelWithLayout(label, .{ .percentOfParent = ui.Vec2{ .x = 1.0 / @as(f32, @floatFromInt(items)), .y = 1.0 } });
defer ui.PopBox(); }
try MakeColumnHeader(columns); pub fn MakeGrid(ctx: *ui.UIContext, comptime T: type, columns: [][]const u8, data: []T, MakeBody: *const fn (ctx: *ui.UIContext, data: *const T, size: i32) anyerror!void) !void {
_ = try ctx.PushBox("GridContainer", .{ .drawBackground = true }, .topToBottom, .fill);
defer ctx.PopBox();
try MakeColumnHeader(ctx, columns);
{ {
_ = try ui.PushBox("Grid", .{ .drawBackground = true, .scrollable = true }, .topToBottom, .fill); try ctx.PushStyle(.{ //
defer ui.PopBox(); .color = raylib.WHITE, //
.hover_color = .{ .r = 0x1a, .g = 0x7c, .b = 0xd3, .a = 0xff }, //
.text_color = raylib.BLACK, //
.text_size = 12, //
.text_padding = 12, //
});
defer ctx.PopStyle();
try ui.PushStyle(.{ // _ = try ctx.PushBox("Grid", .{ .drawBackground = true, .scrollable = true }, .topToBottom, .fill);
defer ctx.PopBox();
try ctx.PushStyle(.{ //
.color = raylib.WHITE, // .color = raylib.WHITE, //
.hover_color = raylib.LIGHTGRAY, // .hover_color = raylib.LIGHTGRAY, //
.text_color = raylib.BLACK, // .text_color = raylib.BLACK, //
@ -49,12 +62,12 @@ pub const Grid = struct {
.text_padding = 12, // .text_padding = 12, //
}); });
for (data) |item| { for (data) |item| {
_ = try ui.PushBox("GridItem", .{ .drawBackground = true, .drawBorder = false, .hoverable = true }, .leftToRight, .fitToChildren); _ = try ctx.PushBox("GridItem", .{ .drawBackground = true, .drawBorder = false, .hoverable = true }, .leftToRight, .fitToChildren);
defer ui.PopBox(); defer ctx.PopBox();
try MakeBody(&item, @as(i32, @intCast(columns.len))); try MakeBody(ctx, &item, @as(i32, @intCast(columns.len)));
} }
defer ui.PopStyle(); ctx.PopStyle();
} }
} }
}; };

View File

@ -5,102 +5,81 @@ const std = @import("std");
pub const raylib = @import("raylib"); pub const raylib = @import("raylib");
pub const components = @import("components.zig"); pub const components = @import("components.zig");
// TODO: don't just make these public pub const UIContext = struct {
pub var box_allocator: std.mem.Allocator = undefined; const Self = @This();
pub var root_box: ?*UI_Box = null;
pub var current_box: ?*UI_Box = null;
pub var current_style: std.ArrayList(UI_Style) = undefined;
pub var pushing_box: bool = false;
pub var popping_box: bool = false;
// PLEASE DON'T DO THIS
pub var font20: raylib.Font = undefined;
pub var font10: raylib.Font = undefined;
pub var mouse_x: i32 = 0;
pub var mouse_y: i32 = 0;
// TODO: do this better
pub var mouse_scroll: f32 = 0;
pub var mouse_released: bool = false;
pub var mouse_hovering_clickable: bool = false;
const scroll_speed: f32 = 1.125; const scroll_speed: f32 = 1.125;
pub const UI_Flags = packed struct(u6) { box_allocator: std.mem.Allocator,
clickable: bool = false,
hoverable: bool = false, // Double-buffered allocator to hold per-frame heap-data
scrollable: bool = false, current_frame_allocator: usize = 0,
drawText: bool = false, frame_allocators: [2]std.heap.ArenaAllocator,
drawBorder: bool = false,
drawBackground: bool = false, root_box: ?*UI_Box = null,
current_box: ?*UI_Box = null,
current_style: std.ArrayList(UI_Style) = undefined,
hot: ?*UI_Box = null,
active: ?*UI_Box = null,
pushing_box: bool = false,
popping_box: bool = false,
// PLEASE DON'T DO THIS
font20: raylib.Font = undefined,
font10: raylib.Font = undefined,
// TODO: do this better
mouse_x: i32 = 0,
mouse_y: i32 = 0,
mouse_scroll: f32 = 0,
mouse_released: bool = false,
mouse_hovering_clickable: bool = false,
pub fn init(allocator: std.mem.Allocator, font20: raylib.Font, font10: raylib.Font) Self {
return .{
.box_allocator = allocator,
.frame_allocators = .{
std.heap.ArenaAllocator.init(allocator),
std.heap.ArenaAllocator.init(allocator),
},
.current_style = std.ArrayList(UI_Style).init(allocator),
.font20 = font20,
.font10 = font10,
}; };
}
pub const UI_Layout = union(enum) { fn frame_allocator(self: *Self) std.mem.Allocator {
fitToText, return self.frame_allocators[self.current_frame_allocator].allocator();
fitToChildren, }
fill,
percentOfParent: Vec2,
exactSize: Vec2,
};
pub const UI_Direction = enum { fn swap_frame_allocators(self: *Self) void {
leftToRight, self.current_frame_allocator = (self.current_frame_allocator + 1) % self.frame_allocators.len;
rightToLeft, }
topToBottom,
bottomToTop,
};
// TODO: don't couple to raylib pub fn NewFrame(self: *Self, width: i32, height: i32, mouse_x: i32, mouse_y: i32, mouse_scroll: f32, mouse_released: bool) !void {
pub const UI_Style = struct { self.current_box = self.root_box;
color: raylib.Color = raylib.LIGHTGRAY, self.pushing_box = false;
hover_color: raylib.Color = raylib.WHITE, self.popping_box = false;
border_color: raylib.Color = raylib.DARKGRAY, self.mouse_hovering_clickable = false;
self.current_style.clearRetainingCapacity();
// TODO: really shouldn't be necessary?
try self.current_style.append(.{});
text_color: raylib.Color = raylib.BLACK, if (self.root_box) |box| {
text_size: i32 = 20, box.computed_size.x = @floatFromInt(width);
text_padding: i32 = 8, box.computed_size.y = @floatFromInt(height);
}; }
pub const Vec2 = struct { self.mouse_x = mouse_x;
x: f32, self.mouse_y = mouse_y;
y: f32, self.mouse_scroll = mouse_scroll;
}; self.mouse_released = mouse_released;
pub const UI_Box = struct { //std.debug.print("frame allocator[0] capacity: {d} - ", .{self.frame_allocators[0].queryCapacity()});
/// the first child //std.debug.print("frame allocator[1] capacity: {d}\n", .{self.frame_allocators[1].queryCapacity()});
first: ?*UI_Box, _ = self.frame_allocators[self.current_frame_allocator].reset(.{ .retain_with_limit = 500_000 });
last: ?*UI_Box, }
/// the next sibling
next: ?*UI_Box,
prev: ?*UI_Box,
parent: ?*UI_Box,
/// the assigned features
flags: UI_Flags,
direction: UI_Direction,
style: UI_Style,
layout: UI_Layout,
/// the label
label: [:0]u8,
/// the final computed position and size of this primitive (in pixels)
computed_pos: Vec2,
computed_size: Vec2,
// whether or not this primitive is currently being interacted with
active: bool = false,
// whether or not this primitive is *about* to be interacted with
hot: bool = false,
// specific scrollable settings
scroll_fract: f32 = 0,
scroll_top: ?*UI_Box = null,
};
fn CountChildren(box: *UI_Box) u32 { fn CountChildren(box: *UI_Box) u32 {
var count: u32 = 0; var count: u32 = 0;
@ -139,23 +118,23 @@ fn CountSiblings(box: *UI_Box) u32 {
return count; return count;
} }
fn TestBoxHover(box: *UI_Box) bool { fn TestBoxHover(self: *Self, box: *UI_Box) bool {
return @as(f32, @floatFromInt(mouse_x)) >= box.computed_pos.x and @as(f32, @floatFromInt(mouse_x)) <= box.computed_pos.x + box.computed_size.x and @as(f32, @floatFromInt(mouse_y)) >= box.computed_pos.y and @as(f32, @floatFromInt(mouse_y)) <= box.computed_pos.y + box.computed_size.y; return @as(f32, @floatFromInt(self.mouse_x)) >= box.computed_pos.x and @as(f32, @floatFromInt(self.mouse_x)) <= box.computed_pos.x + box.computed_size.x and @as(f32, @floatFromInt(self.mouse_y)) >= box.computed_pos.y and @as(f32, @floatFromInt(self.mouse_y)) <= box.computed_pos.y + box.computed_size.y;
} }
fn TestBoxClick(box: *UI_Box) bool { fn TestBoxClick(self: *Self, box: *UI_Box) bool {
return mouse_released and TestBoxHover(box); return self.mouse_released and self.TestBoxHover(box);
} }
fn ScrollBox(box: *UI_Box) void { fn ScrollBox(self: *Self, box: *UI_Box) void {
if (TestBoxHover(box)) { if (self.TestBoxHover(box)) {
if (box.scroll_top == null) { if (box.scroll_top == null) {
box.scroll_top = box.first; box.scroll_top = box.first;
} }
if (box.scroll_top) |top| { if (box.scroll_top) |top| {
if ((mouse_scroll > 0 and top.prev != null) or (mouse_scroll < 0 and top.next != null) or box.scroll_fract != 0) { if ((self.mouse_scroll > 0 and top.prev != null) or (self.mouse_scroll < 0 and top.next != null) or box.scroll_fract != 0) {
box.scroll_fract -= mouse_scroll * scroll_speed; box.scroll_fract -= self.mouse_scroll * scroll_speed;
} }
} }
@ -196,50 +175,76 @@ fn ScrollBox(box: *UI_Box) void {
} }
} }
pub fn DeleteBoxChildren(box: *UI_Box, should_destroy: bool) void { fn TestBoxInteraction(self: *Self, box: *UI_Box) bool {
if (box.first) |child| { if (box.flags.clickable) {
DeleteBoxChildren(child, true); return self.TestBoxClick(box);
} else if (should_destroy) {
box_allocator.destroy(box);
} }
if (box.flags.scrollable) {
self.ScrollBox(box);
}
return false;
}
pub fn DeleteBoxChildren(self: *Self, box: *UI_Box, should_destroy: bool) void {
var child = box.first;
while (child) |c| {
child = c.next;
self.DeleteBoxChildren(c, true);
}
if (should_destroy) {
self.box_allocator.destroy(box);
}
}
pub fn MakeBox(self: *Self, str: []const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool {
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{s}", .{str});
return try self._MakeBox(label, flags, direction, layout);
}
pub fn PushBox(self: *Self, str: []const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool {
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{s}", .{str});
return try self._PushBox(label, flags, direction, layout);
} }
// TODO: remove all footguns by compressing code // TODO: remove all footguns by compressing code
pub fn MakeBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool { fn _MakeBox(self: *Self, label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool {
//std.debug.print("making box '{s}'...", .{label}); //std.debug.print("making box '{s}'...", .{label});
// TODO: Please remove this state machine, there should be a way to do it without it // TODO: Please remove this state machine, there should be a way to do it without it
popping_box = false; self.popping_box = false;
if (pushing_box) { if (self.pushing_box) {
const box = try PushBox(label, flags, direction, layout); const box = try self._PushBox(label, flags, direction, layout);
pushing_box = false; self.pushing_box = false;
return box; return box;
} }
if (current_box) |box| { if (self.current_box) |box| {
if (box.next) |next| { if (box.next) |next| {
// Attempt to re-use cache // Attempt to re-use cache
if (std.mem.eql(u8, next.label, label)) { if (std.mem.eql(u8, next.label, label)) {
//std.debug.print("using cache for '{s}'\n", .{next.label}); //std.debug.print("using cache for '{s}'\n", .{next.label});
next.flags = flags; next.flags = flags;
next.direction = direction; next.direction = direction;
next.label = @constCast(label);
if (next.parent) |parent| { if (next.parent) |parent| {
parent.last = next; parent.last = next;
} }
current_box = next; self.current_box = next;
} else { } else {
// Invalid cache, delete next sibling while retaining the following one // Invalid cache, delete next sibling while retaining the following one
//std.debug.print("make_box: invalidating cache for '{s}' when making new box '{s}'\n", .{ next.label, label }); //std.debug.print("make_box: invalidating cache for '{s}' when making new box '{s}'\n", .{ next.label, label });
const following_sibling = next.next; const following_sibling = next.next;
DeleteBoxChildren(next, false); self.DeleteBoxChildren(next, false);
next.* = UI_Box{ next.* = UI_Box{
.label = @constCast(label), .label = @constCast(label),
.flags = flags, .flags = flags,
.direction = direction, .direction = direction,
.style = current_style.getLast(), .style = self.current_style.getLast(),
.layout = layout, .layout = layout,
.first = null, .first = null,
@ -251,7 +256,7 @@ pub fn MakeBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
.computed_size = Vec2{ .x = 0, .y = 0 }, .computed_size = Vec2{ .x = 0, .y = 0 },
}; };
current_box = next; self.current_box = next;
if (next.parent) |parent| { if (next.parent) |parent| {
parent.last = next; parent.last = next;
} }
@ -259,12 +264,12 @@ pub fn MakeBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
} else { } else {
// No existing cache, create new box // No existing cache, create new box
//std.debug.print("make_box: allocating new box: {s}\n", .{label}); //std.debug.print("make_box: allocating new box: {s}\n", .{label});
var new_box = try box_allocator.create(UI_Box); var new_box = try self.box_allocator.create(UI_Box);
new_box.* = UI_Box{ new_box.* = UI_Box{
.label = @constCast(label), .label = @constCast(label),
.flags = flags, .flags = flags,
.direction = direction, .direction = direction,
.style = current_style.getLast(), .style = self.current_style.getLast(),
.layout = layout, .layout = layout,
.first = null, .first = null,
@ -277,19 +282,19 @@ pub fn MakeBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
}; };
box.next = new_box; box.next = new_box;
current_box = new_box; self.current_box = new_box;
if (new_box.parent) |parent| { if (new_box.parent) |parent| {
parent.last = new_box; parent.last = new_box;
} }
} }
} else { } else {
//std.debug.print("make_box: allocating new box: {s}\n", .{label}); //std.debug.print("make_box: allocating new box: {s}\n", .{label});
var new_box = try box_allocator.create(UI_Box); var new_box = try self.box_allocator.create(UI_Box);
new_box.* = UI_Box{ new_box.* = UI_Box{
.label = @constCast(label), .label = @constCast(label),
.flags = flags, .flags = flags,
.direction = direction, .direction = direction,
.style = current_style.getLast(), .style = self.current_style.getLast(),
.layout = layout, .layout = layout,
.first = null, .first = null,
@ -301,36 +306,31 @@ pub fn MakeBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
.computed_size = Vec2{ .x = 0, .y = 0 }, .computed_size = Vec2{ .x = 0, .y = 0 },
}; };
current_box = new_box; self.current_box = new_box;
} }
if (current_box) |box| { if (self.current_box) |box| {
if (box.flags.clickable) { return self.TestBoxInteraction(box);
return TestBoxClick(box);
}
if (box.flags.scrollable) {
ScrollBox(box);
}
} }
return false; return false;
} }
pub fn PushBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool { pub fn _PushBox(self: *Self, label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool {
//std.debug.print("pushing box '{s}'...", .{label}); //std.debug.print("pushing box '{s}'...", .{label});
// TODO: Please remove this state machine, there should be a way to do it without it // TODO: Please remove this state machine, there should be a way to do it without it
if (popping_box) { if (self.popping_box) {
const box = try MakeBox(label, flags, direction, layout); const box = try self._MakeBox(label, flags, direction, layout);
pushing_box = true; self.pushing_box = true;
return box; return box;
} }
if (!pushing_box) { if (!self.pushing_box) {
pushing_box = true; self.pushing_box = true;
} }
if (current_box) |box| { if (self.current_box) |box| {
// Attempt to re-use cache // Attempt to re-use cache
if (box.first) |first| { if (box.first) |first| {
// check if the same // check if the same
@ -338,7 +338,8 @@ pub fn PushBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
//std.debug.print("using cache for '{s}'\n", .{first.label}); //std.debug.print("using cache for '{s}'\n", .{first.label});
first.flags = flags; first.flags = flags;
first.direction = direction; first.direction = direction;
current_box = first; first.label = @constCast(label);
self.current_box = first;
if (first.parent) |parent| { if (first.parent) |parent| {
parent.last = first; parent.last = first;
@ -347,96 +348,96 @@ pub fn PushBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, la
// Invalid cache // Invalid cache
//std.debug.print("push_box: invalidating cache for '{s}' when making new box '{s}'\n", .{ first.label, label }); //std.debug.print("push_box: invalidating cache for '{s}' when making new box '{s}'\n", .{ first.label, label });
const following_sibling = first.next; const following_sibling = first.next;
DeleteBoxChildren(first, false); self.DeleteBoxChildren(first, false);
first.* = UI_Box{ first.* = UI_Box{
.label = @constCast(label), .label = @constCast(label),
.flags = flags, .flags = flags,
.direction = direction, .direction = direction,
.style = current_style.getLast(), .style = self.current_style.getLast(),
.layout = layout, .layout = layout,
.first = null, .first = null,
.last = null, .last = null,
.next = following_sibling, .next = following_sibling,
.prev = null, .prev = null,
.parent = current_box, .parent = self.current_box,
.computed_pos = Vec2{ .x = 0, .y = 0 }, .computed_pos = Vec2{ .x = 0, .y = 0 },
.computed_size = Vec2{ .x = 0, .y = 0 }, .computed_size = Vec2{ .x = 0, .y = 0 },
}; };
current_box = first; self.current_box = first;
if (first.parent) |parent| { if (first.parent) |parent| {
parent.last = first; parent.last = first;
} }
} }
} else { } else {
//std.debug.print("push_box: allocating new box: {s}\n", .{label}); //std.debug.print("push_box: allocating new box: {s}\n", .{label});
var new_box = try box_allocator.create(UI_Box); var new_box = try self.box_allocator.create(UI_Box);
new_box.* = UI_Box{ new_box.* = UI_Box{
.label = @constCast(label), .label = @constCast(label),
.flags = flags, .flags = flags,
.direction = direction, .direction = direction,
.style = current_style.getLast(), .style = self.current_style.getLast(),
.layout = layout, .layout = layout,
.first = null, .first = null,
.last = null, .last = null,
.next = null, .next = null,
.prev = null, .prev = null,
.parent = current_box, .parent = self.current_box,
.computed_pos = Vec2{ .x = 0, .y = 0 }, .computed_pos = Vec2{ .x = 0, .y = 0 },
.computed_size = Vec2{ .x = 0, .y = 0 }, .computed_size = Vec2{ .x = 0, .y = 0 },
}; };
box.first = new_box; box.first = new_box;
current_box = new_box; self.current_box = new_box;
if (new_box.parent) |parent| { if (new_box.parent) |parent| {
parent.last = new_box; parent.last = new_box;
} }
} }
} else { } else {
pushing_box = false; self.pushing_box = false;
return try MakeBox(label, flags, direction, layout); return try self._MakeBox(label, flags, direction, layout);
} }
if (current_box) |box| { if (self.current_box) |box| {
if (box.flags.clickable) { return self.TestBoxInteraction(box);
return TestBoxClick(box);
}
} }
return false; return false;
} }
pub fn PopBox() void { pub fn PopBox(self: *Self) void {
//std.debug.print("popping box...", .{}); //std.debug.print("popping box...", .{});
if (current_box) |box| { if (self.current_box) |box| {
//if (box.parent) |parent| { //if (box.parent) |parent| {
//current_box = parent.last; //current_box = parent.last;
//return; //return;
//} //}
if (box.parent) |p| { if (box.parent) |p| {
p.last = current_box; p.last = self.current_box;
} }
current_box = box.parent; self.current_box = box.parent;
popping_box = true; self.popping_box = true;
return; return;
} }
//std.debug.print("couldn't pop box\n", .{}); //std.debug.print("couldn't pop box\n", .{});
} }
pub fn PushStyle(style: UI_Style) !void { pub fn PushStyle(self: *Self, style: UI_Style) !void {
try current_style.append(style); try self.current_style.append(style);
} }
pub fn PopStyle() void { pub fn PopStyle(self: *Self) void {
_ = current_style.popOrNull(); _ = self.current_style.popOrNull();
} }
pub fn MakeButtonWithLayout(label: [:0]const u8, layout: UI_Layout) !bool { pub fn MakeButtonWithLayout(self: *Self, str: []const u8, layout: UI_Layout) !bool {
return try MakeBox(label, .{ // TODO: replace with frame allocator
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{s}", .{str});
return try self._MakeBox(label, .{
.clickable = true, .clickable = true,
.hoverable = true, .hoverable = true,
.drawText = true, .drawText = true,
@ -444,18 +445,36 @@ pub fn MakeButtonWithLayout(label: [:0]const u8, layout: UI_Layout) !bool {
}, .leftToRight, layout); }, .leftToRight, layout);
} }
pub fn MakeButton(label: [:0]const u8) !bool { pub fn MakeButton(self: *Self, label: []const u8) !bool {
return try MakeButtonWithLayout(label, .fitToText); return try self.MakeButtonWithLayout(label, .fitToText);
} }
pub fn MakeLabelWithLayout(label: [:0]const u8, layout: UI_Layout) !void { pub fn MakeFormattedLabelWithLayout(self: *Self, comptime str: []const u8, args: anytype, layout: UI_Layout) !void {
_ = try MakeBox(label, .{ // TODO: replace with frame allocator
const label = try std.fmt.allocPrintZ(self.frame_allocator(), str, args);
_ = try self._MakeBox(label, .{
.drawText = true, .drawText = true,
}, .leftToRight, layout); }, .leftToRight, layout);
} }
pub fn MakeLabel(label: [:0]const u8) !void { pub fn MakeLabelWithLayout(self: *Self, str: []const u8, layout: UI_Layout) !void {
_ = try MakeLabelWithLayout(label, .fitToText); // TODO: replace with frame allocator
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{s}", .{str});
_ = try self._MakeBox(label, .{
.drawText = true,
}, .leftToRight, layout);
}
pub fn MakeLabelInt(self: *Self, value: anytype) !void {
// TODO: replace with frame allocator
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{d}", .{value});
_ = try self.MakeLabelWithLayout(label, .fitToText);
}
pub fn MakeLabel(self: *Self, str: []const u8) !void {
// TODO: replace with frame allocator
const label = try std.fmt.allocPrintZ(self.frame_allocator(), "{s}", .{str});
_ = try self.MakeLabelWithLayout(label, .fitToText);
} }
fn ComputeChildrenSize(box: *UI_Box) Vec2 { fn ComputeChildrenSize(box: *UI_Box) Vec2 {
@ -503,10 +522,12 @@ fn ComputeSiblingSize(box: *UI_Box) Vec2 {
// get siblings size so we know to big to get // get siblings size so we know to big to get
var total_sibling_size = Vec2{ .x = 0, .y = 0 }; var total_sibling_size = Vec2{ .x = 0, .y = 0 };
var n = box.next; var n = box.next;
const direction = if (box.parent) |p| p.direction else box.direction;
while (n) |next| { while (n) |next| {
const sibling_size = ComputeLayout(next); const sibling_size = ComputeLayout(next);
switch (box.direction) { switch (direction) {
.leftToRight => { .leftToRight => {
total_sibling_size.x += sibling_size.x; total_sibling_size.x += sibling_size.x;
if (sibling_size.y > total_sibling_size.y) { if (sibling_size.y > total_sibling_size.y) {
@ -571,23 +592,30 @@ pub fn ComputeLayout(box: *UI_Box) Vec2 {
}, },
.fill => { .fill => {
const total_sibling_size = ComputeSiblingSize(box); const total_sibling_size = ComputeSiblingSize(box);
if (box.parent) |p| { if (box.parent) |p| {
if (p.layout == .fitToChildren) {
//box.layout = .fitToText;
}
if (std.mem.eql(u8, "GridContainer", box.label)) {
const calculated_size = p.computed_size.y - total_sibling_size.y - (box.computed_pos.y - p.computed_pos.y);
std.debug.print("Grid sibling size: {d}, {d}, '{s}'_height: {d}, calculated height: {d}\n", .{ total_sibling_size.x, total_sibling_size.y, p.label, p.computed_size.y, calculated_size });
}
box.computed_size = Vec2{ box.computed_size = Vec2{
.x = switch (p.direction) { .x = switch (p.direction) {
.leftToRight => p.computed_size.x - total_sibling_size.x - box.computed_pos.x, .leftToRight => p.computed_size.x - total_sibling_size.x - (box.computed_pos.x - p.computed_pos.x),
.topToBottom => if (total_sibling_size.x == 0) p.computed_size.x else total_sibling_size.x, .topToBottom => if (total_sibling_size.x == 0) p.computed_size.x else total_sibling_size.x,
//.topToBottom => 0, //p.computed_size.x,
.rightToLeft, .bottomToTop => unreachable, .rightToLeft, .bottomToTop => unreachable,
}, },
.y = switch (p.direction) { .y = switch (p.direction) {
.leftToRight => if (total_sibling_size.y == 0) p.computed_size.y else total_sibling_size.y, .leftToRight => if (total_sibling_size.y == 0) p.computed_size.y else total_sibling_size.y,
.topToBottom => p.computed_size.y - total_sibling_size.y - box.computed_pos.y, //.leftToRight => 0, //p.computed_size.y,
.topToBottom => p.computed_size.y - total_sibling_size.y - (box.computed_pos.y - p.computed_pos.y),
.rightToLeft, .bottomToTop => unreachable, .rightToLeft, .bottomToTop => unreachable,
}, },
}; };
} else {
// TODO: somehow need to get these values
//box.computed_size = Vec2{ .x = 1280, .y = 720 };
} }
//_ = ComputeChildrenSize(box); //_ = ComputeChildrenSize(box);
@ -615,7 +643,30 @@ pub fn ComputeLayout(box: *UI_Box) Vec2 {
//_ = ComputeChildrenSize(box); //_ = ComputeChildrenSize(box);
break :blk true; break :blk true;
}, },
.exactSize => |_| unreachable, .exactSize => |size| {
box.computed_size = size;
break :blk true;
},
.floating => |layout| {
box.computed_pos = .{ .x = 0, .y = 0 };
switch (layout) {
.fitToText => {
box.computed_size = Vec2{
.x = @floatFromInt(raylib.MeasureText(box.label, box.style.text_size) + box.style.text_padding * 2),
.y = @floatFromInt(box.style.text_size + box.style.text_padding * 2),
};
},
.fitToChildren => {
box.computed_size = ComputeChildrenSize(box);
},
.exactSize => |size| {
box.computed_size = size;
},
}
_ = ComputeChildrenSize(box);
return .{ .x = 0, .y = 0 };
},
} }
}; };
@ -640,17 +691,25 @@ pub fn ComputeLayout(box: *UI_Box) Vec2 {
return box.computed_size; return box.computed_size;
} }
pub fn DrawUI(box: *UI_Box) void { pub fn DrawUI(self: *Self, box: *UI_Box) void {
const is_hovering = TestBoxHover(box); const is_hovering = self.TestBoxHover(box);
if (box.flags.clickable and is_hovering) { if (box.flags.clickable and is_hovering) {
mouse_hovering_clickable = true; self.mouse_hovering_clickable = true;
} }
const pos_x: i32 = @intFromFloat(box.computed_pos.x); const pos_x: i32 = @intFromFloat(box.computed_pos.x);
const pos_y: i32 = @intFromFloat(box.computed_pos.y); const pos_y: i32 = @intFromFloat(box.computed_pos.y);
const size_x: i32 = @intFromFloat(box.computed_size.x);
const size_y: i32 = @intFromFloat(box.computed_size.y);
if (box.layout != .floating) {
if (box.parent) |p| {
const parent_pos_x: i32 = @intFromFloat(p.computed_pos.x);
const parent_pos_y: i32 = @intFromFloat(p.computed_pos.y);
const parent_size_x: i32 = @intFromFloat(p.computed_size.x);
const parent_size_y: i32 = @intFromFloat(p.computed_size.y);
raylib.BeginScissorMode(parent_pos_x, parent_pos_y, parent_size_x, parent_size_y);
}
}
if (box.flags.drawBackground) { if (box.flags.drawBackground) {
const color = if (box.flags.hoverable and is_hovering) box.style.hover_color else box.style.color; const color = if (box.flags.hoverable and is_hovering) box.style.hover_color else box.style.color;
@ -682,7 +741,7 @@ pub fn DrawUI(box: *UI_Box) void {
} }
} }
raylib.DrawTextEx( // raylib.DrawTextEx( //
if (box.style.text_size == 20) font20 else if (box.style.text_size == 12) font10 else font10, // if (box.style.text_size == 20) self.font20 else if (box.style.text_size == 12) self.font10 else self.font10, //
box.label, // box.label, //
.{ .{
.x = box.computed_pos.x + @as(f32, @floatFromInt(box.style.text_padding)), // .x = box.computed_pos.x + @as(f32, @floatFromInt(box.style.text_padding)), //
@ -692,6 +751,11 @@ pub fn DrawUI(box: *UI_Box) void {
1.0, color // 1.0, color //
); );
} }
if (box.layout != .floating) {
if (box.parent) |_| {
raylib.EndScissorMode();
}
}
// draw children // draw children
const children = CountChildren(box); const children = CountChildren(box);
@ -708,9 +772,8 @@ pub fn DrawUI(box: *UI_Box) void {
var child_size: f32 = 0; var child_size: f32 = 0;
// TODO: replace with non-raylib function, also figure out why this doesn't clip text drawn with `DrawText` // TODO: replace with non-raylib function, also figure out why this doesn't clip text drawn with `DrawText`
raylib.BeginScissorMode(pos_x, pos_y, size_x, size_y);
while (child) |c| { while (child) |c| {
DrawUI(c); self.DrawUI(c);
if (child == box.last) break; if (child == box.last) break;
@ -719,19 +782,109 @@ pub fn DrawUI(box: *UI_Box) void {
.leftToRight => { .leftToRight => {
child_size += c.computed_size.x; child_size += c.computed_size.x;
// TODO: don't multiply this by two, use the last child size or something // TODO: don't multiply this by two, use the last child size or something
if (child_size > box.computed_size.x * 2) break; if (child_size > box.computed_size.x + c.computed_size.x) break;
}, },
.rightToLeft, .bottomToTop => unreachable, .rightToLeft, .bottomToTop => unreachable,
.topToBottom => { .topToBottom => {
child_size += c.computed_size.y; child_size += c.computed_size.y;
// TODO: don't multiply this by two, use the last child size or something // TODO: don't multiply this by two, use the last child size or something
if (child_size > box.computed_size.y * 2) break; if (child_size > box.computed_size.y + c.computed_size.y) break;
}, },
} }
} }
child = c.next; child = c.next;
} }
raylib.EndScissorMode();
} }
} }
pub fn Draw(self: *Self) void {
if (self.root_box) |box| {
_ = ComputeLayout(box);
self.DrawUI(box);
}
self.swap_frame_allocators();
}
};
pub const UI_Flags = packed struct {
clickable: bool = false,
hoverable: bool = false,
scrollable: bool = false,
drawText: bool = false,
drawBorder: bool = false,
drawBackground: bool = false,
};
pub const FloatingLayout = union(enum) {
fitToText,
fitToChildren,
exactSize: Vec2,
};
pub const UI_Layout = union(enum) {
fitToText,
fitToChildren,
fill,
percentOfParent: Vec2,
exactSize: Vec2,
floating: FloatingLayout,
};
pub const UI_Direction = enum {
leftToRight,
rightToLeft,
topToBottom,
bottomToTop,
};
// TODO: don't couple to raylib
pub const UI_Style = struct {
color: raylib.Color = raylib.LIGHTGRAY,
hover_color: raylib.Color = raylib.WHITE,
border_color: raylib.Color = raylib.DARKGRAY,
text_color: raylib.Color = raylib.BLACK,
text_size: i32 = 20,
text_padding: i32 = 8,
};
pub const Vec2 = struct {
x: f32,
y: f32,
};
pub const UI_Box = struct {
/// the first child
first: ?*UI_Box,
last: ?*UI_Box,
/// the next sibling
next: ?*UI_Box,
prev: ?*UI_Box,
parent: ?*UI_Box,
/// the assigned features
flags: UI_Flags,
direction: UI_Direction,
style: UI_Style,
layout: UI_Layout,
/// the label
label: [:0]u8,
/// the final computed position and size of this primitive (in pixels)
computed_pos: Vec2,
computed_size: Vec2,
// whether or not this primitive is currently being interacted with
//active: bool = false,
// whether or not this primitive is *about* to be interacted with
//hot: bool = false,
// specific scrollable settings
scroll_fract: f32 = 0,
scroll_top: ?*UI_Box = null,
};