mirror of https://github.com/pcleavelin/zooy
596 lines
18 KiB
Zig
596 lines
18 KiB
Zig
const std = @import("std");
|
|
|
|
// TODO: abstract raylib away to allow for consumers to use whatever they want.
|
|
// I'm also rexporting here because the zig build system hurts
|
|
pub const raylib = @import("raylib");
|
|
|
|
// TODO: don't just make these public
|
|
pub var box_allocator: std.mem.Allocator = undefined;
|
|
|
|
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;
|
|
|
|
pub var mouse_x: i32 = 0;
|
|
pub var mouse_y: i32 = 0;
|
|
pub var mouse_released: bool = false;
|
|
pub var mouse_hovering_clickable: bool = false;
|
|
|
|
pub const UI_Flags = packed struct(u5) {
|
|
clickable: bool = false,
|
|
hoverable: bool = false,
|
|
drawText: bool = false,
|
|
drawBorder: bool = false,
|
|
drawBackground: bool = false,
|
|
};
|
|
|
|
pub const UI_Layout = union(enum) {
|
|
fitToText,
|
|
fitToChildren,
|
|
fill,
|
|
percentOfParent: Vec2,
|
|
exactSize: Vec2,
|
|
};
|
|
|
|
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]const u8,
|
|
|
|
/// the final computed position and size of this primitive (in pixels)
|
|
computed_pos: Vec2,
|
|
computed_size: Vec2,
|
|
};
|
|
|
|
fn CountChildren(box: *UI_Box) u32 {
|
|
var count: u32 = 0;
|
|
var b = box.first;
|
|
|
|
while (b) |child| {
|
|
count += 1;
|
|
|
|
// TODO: um, somehow need to trim stale tree nodes
|
|
if (b == box.last) break;
|
|
b = child.next;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
fn CountSiblings(box: *UI_Box) u32 {
|
|
var count: u32 = 0;
|
|
var b = box;
|
|
if (b.parent) |p| {
|
|
if (b == p.last) return 0;
|
|
}
|
|
|
|
while (b.next) |next| {
|
|
count += 1;
|
|
|
|
if (b.parent) |p| {
|
|
if (b == p.last) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
b = next;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
fn TestBoxHover(box: *UI_Box) bool {
|
|
return @intToFloat(f32, mouse_x) >= box.computed_pos.x and @intToFloat(f32, mouse_x) <= box.computed_pos.x + box.computed_size.x and @intToFloat(f32, mouse_y) >= box.computed_pos.y and @intToFloat(f32, mouse_y) <= box.computed_pos.y + box.computed_size.y;
|
|
}
|
|
|
|
fn TestBoxClick(box: *UI_Box) bool {
|
|
return mouse_released and TestBoxHover(box);
|
|
}
|
|
|
|
pub fn DeleteBoxChildren(box: *UI_Box, should_destroy: bool) void {
|
|
if (box.first) |child| {
|
|
DeleteBoxChildren(child, true);
|
|
} else if (should_destroy) {
|
|
box_allocator.destroy(box);
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
//std.debug.print("making box '{s}'...", .{label});
|
|
|
|
// TODO: Please remove this state machine, there should be a way to do it without it
|
|
popping_box = false;
|
|
|
|
if (pushing_box) {
|
|
const box = try PushBox(label, flags, direction, layout);
|
|
pushing_box = false;
|
|
|
|
return box;
|
|
}
|
|
|
|
if (current_box) |box| {
|
|
if (box.next) |next| {
|
|
// Attempt to re-use cache
|
|
if (std.mem.eql(u8, next.label, label)) {
|
|
//std.debug.print("using cache for '{s}'\n", .{next.label});
|
|
next.flags = flags;
|
|
next.direction = direction;
|
|
if (next.parent) |parent| {
|
|
parent.last = next;
|
|
}
|
|
current_box = next;
|
|
} else {
|
|
// 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 });
|
|
const following_sibling = next.next;
|
|
DeleteBoxChildren(next, false);
|
|
|
|
next.* = UI_Box{
|
|
.label = label,
|
|
.flags = flags,
|
|
.direction = direction,
|
|
.style = current_style.getLast(),
|
|
.layout = layout,
|
|
|
|
.first = null,
|
|
.last = null,
|
|
.next = following_sibling,
|
|
.prev = box,
|
|
.parent = box.parent,
|
|
.computed_pos = Vec2{ .x = 0, .y = 0 },
|
|
.computed_size = Vec2{ .x = 0, .y = 0 },
|
|
};
|
|
|
|
current_box = next;
|
|
if (next.parent) |parent| {
|
|
parent.last = next;
|
|
}
|
|
}
|
|
} else {
|
|
// No existing cache, create new box
|
|
std.debug.print("make_box: allocating new box: {s}\n", .{label});
|
|
var new_box = try box_allocator.create(UI_Box);
|
|
new_box.* = UI_Box{
|
|
.label = label,
|
|
.flags = flags,
|
|
.direction = direction,
|
|
.style = current_style.getLast(),
|
|
.layout = layout,
|
|
|
|
.first = null,
|
|
.last = null,
|
|
.next = null,
|
|
.prev = box,
|
|
.parent = box.parent,
|
|
.computed_pos = Vec2{ .x = 0, .y = 0 },
|
|
.computed_size = Vec2{ .x = 0, .y = 0 },
|
|
};
|
|
|
|
box.next = new_box;
|
|
current_box = new_box;
|
|
if (new_box.parent) |parent| {
|
|
parent.last = new_box;
|
|
}
|
|
}
|
|
} else {
|
|
std.debug.print("make_box: allocating new box: {s}\n", .{label});
|
|
var new_box = try box_allocator.create(UI_Box);
|
|
new_box.* = UI_Box{
|
|
.label = label,
|
|
.flags = flags,
|
|
.direction = direction,
|
|
.style = current_style.getLast(),
|
|
.layout = layout,
|
|
|
|
.first = null,
|
|
.last = null,
|
|
.next = null,
|
|
.prev = null,
|
|
.parent = null,
|
|
.computed_pos = Vec2{ .x = 0, .y = 0 },
|
|
.computed_size = Vec2{ .x = 0, .y = 0 },
|
|
};
|
|
|
|
current_box = new_box;
|
|
}
|
|
|
|
if (current_box) |box| {
|
|
if (box.flags.clickable) {
|
|
return TestBoxClick(box);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn PushBox(label: [:0]const u8, flags: UI_Flags, direction: UI_Direction, layout: UI_Layout) anyerror!bool {
|
|
//std.debug.print("pushing box '{s}'...", .{label});
|
|
|
|
// TODO: Please remove this state machine, there should be a way to do it without it
|
|
if (popping_box) {
|
|
const box = try MakeBox(label, flags, direction, layout);
|
|
pushing_box = true;
|
|
|
|
return box;
|
|
}
|
|
if (!pushing_box) {
|
|
pushing_box = true;
|
|
}
|
|
|
|
if (current_box) |box| {
|
|
// Attempt to re-use cache
|
|
if (box.first) |first| {
|
|
// check if the same
|
|
if (std.mem.eql(u8, first.label, label)) {
|
|
//std.debug.print("using cache for '{s}'\n", .{first.label});
|
|
first.flags = flags;
|
|
first.direction = direction;
|
|
current_box = first;
|
|
|
|
if (first.parent) |parent| {
|
|
parent.last = first;
|
|
}
|
|
} else {
|
|
// Invalid cache
|
|
std.debug.print("push_box: invalidating cache for '{s}' when making new box '{s}'\n", .{ first.label, label });
|
|
const following_sibling = first.next;
|
|
DeleteBoxChildren(first, false);
|
|
|
|
first.* = UI_Box{
|
|
.label = label,
|
|
.flags = flags,
|
|
.direction = direction,
|
|
.style = current_style.getLast(),
|
|
.layout = layout,
|
|
|
|
.first = null,
|
|
.last = null,
|
|
.next = following_sibling,
|
|
.prev = null,
|
|
.parent = current_box,
|
|
.computed_pos = Vec2{ .x = 0, .y = 0 },
|
|
.computed_size = Vec2{ .x = 0, .y = 0 },
|
|
};
|
|
|
|
current_box = first;
|
|
if (first.parent) |parent| {
|
|
parent.last = first;
|
|
}
|
|
}
|
|
} else {
|
|
std.debug.print("push_box: allocating new box: {s}\n", .{label});
|
|
var new_box = try box_allocator.create(UI_Box);
|
|
new_box.* = UI_Box{
|
|
.label = label,
|
|
.flags = flags,
|
|
.direction = direction,
|
|
.style = current_style.getLast(),
|
|
.layout = layout,
|
|
|
|
.first = null,
|
|
.last = null,
|
|
.next = null,
|
|
.prev = null,
|
|
.parent = current_box,
|
|
.computed_pos = Vec2{ .x = 0, .y = 0 },
|
|
.computed_size = Vec2{ .x = 0, .y = 0 },
|
|
};
|
|
|
|
box.first = new_box;
|
|
current_box = new_box;
|
|
if (new_box.parent) |parent| {
|
|
parent.last = new_box;
|
|
}
|
|
}
|
|
} else {
|
|
pushing_box = false;
|
|
return try MakeBox(label, flags, direction, layout);
|
|
}
|
|
|
|
if (current_box) |box| {
|
|
if (box.flags.clickable) {
|
|
return TestBoxClick(box);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn PopBox() void {
|
|
//std.debug.print("popping box...", .{});
|
|
if (current_box) |box| {
|
|
//if (box.parent) |parent| {
|
|
//current_box = parent.last;
|
|
//return;
|
|
//}
|
|
if (box.parent) |p| {
|
|
p.last = current_box;
|
|
}
|
|
current_box = box.parent;
|
|
popping_box = true;
|
|
return;
|
|
}
|
|
|
|
//std.debug.print("couldn't pop box\n", .{});
|
|
}
|
|
|
|
pub fn PushStyle(style: UI_Style) !void {
|
|
try current_style.append(style);
|
|
}
|
|
|
|
pub fn PopStyle() void {
|
|
_ = current_style.popOrNull();
|
|
}
|
|
|
|
pub fn MakeButtonWithLayout(label: [:0]const u8, layout: UI_Layout) !bool {
|
|
return try MakeBox(label, .{
|
|
.clickable = true,
|
|
.hoverable = true,
|
|
.drawText = true,
|
|
.drawBackground = true,
|
|
}, .leftToRight, layout);
|
|
}
|
|
|
|
pub fn MakeButton(label: [:0]const u8) !bool {
|
|
return try MakeButtonWithLayout(label, .fitToText);
|
|
}
|
|
|
|
pub fn MakeLabelWithLayout(label: [:0]const u8, layout: UI_Layout) !void {
|
|
_ = try MakeBox(label, .{
|
|
.drawText = true,
|
|
}, .leftToRight, layout);
|
|
}
|
|
|
|
pub fn MakeLabel(label: [:0]const u8) !void {
|
|
_ = try MakeLabelWithLayout(label, .fitToText);
|
|
}
|
|
|
|
fn ComputeChildrenSize(box: *UI_Box) Vec2 {
|
|
var total_size = Vec2{ .x = 0, .y = 0 };
|
|
|
|
// TODO: make this block an iterator
|
|
const children = CountChildren(box);
|
|
if (children > 0) {
|
|
var child = box.first;
|
|
while (child) |c| {
|
|
const child_size = ComputeLayout(c);
|
|
|
|
switch (box.direction) {
|
|
.leftToRight => {
|
|
total_size.x += child_size.x;
|
|
|
|
// only grab max size for this direction
|
|
if (child_size.y > total_size.y) {
|
|
total_size.y = child_size.y;
|
|
}
|
|
},
|
|
.topToBottom => {
|
|
total_size.y += child_size.y;
|
|
|
|
// only grab max size for this direction
|
|
if (child_size.x > total_size.x) {
|
|
total_size.x = child_size.x;
|
|
}
|
|
},
|
|
.rightToLeft, .bottomToTop => {},
|
|
}
|
|
|
|
if (child == box.last) break;
|
|
|
|
child = c.next;
|
|
}
|
|
}
|
|
|
|
return total_size;
|
|
}
|
|
|
|
fn ComputeSiblingSize(box: *UI_Box) Vec2 {
|
|
// TODO: get _all_ sibling sizes
|
|
// (not just the _next_ ones, but also don't forget to not infinitly recurse)
|
|
// get siblings size so we know to big to get
|
|
var total_sibling_size = Vec2{ .x = 0, .y = 0 };
|
|
var n = box.next;
|
|
while (n) |next| {
|
|
const sibling_size = ComputeLayout(next);
|
|
|
|
switch (box.direction) {
|
|
.leftToRight => {
|
|
total_sibling_size.x += sibling_size.x;
|
|
if (sibling_size.y > total_sibling_size.y) {
|
|
total_sibling_size.y = sibling_size.y;
|
|
}
|
|
},
|
|
.topToBottom => {
|
|
total_sibling_size.y += sibling_size.y;
|
|
if (sibling_size.x > total_sibling_size.x) {
|
|
total_sibling_size.x = sibling_size.x;
|
|
}
|
|
},
|
|
.rightToLeft, .bottomToTop => {},
|
|
}
|
|
|
|
if (box.parent) |p| {
|
|
if (next == p.last) break;
|
|
}
|
|
|
|
n = next.next;
|
|
}
|
|
|
|
return total_sibling_size;
|
|
}
|
|
|
|
pub fn ComputeLayout(box: *UI_Box) Vec2 {
|
|
if (box.parent) |p| {
|
|
box.computed_size = p.computed_size;
|
|
|
|
if (box.prev) |prev| {
|
|
box.computed_pos = Vec2{ .x = switch (p.direction) {
|
|
.leftToRight => prev.computed_pos.x + prev.computed_size.x,
|
|
.topToBottom => prev.computed_pos.x,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
}, .y = switch (p.direction) {
|
|
.leftToRight => prev.computed_pos.y,
|
|
.topToBottom => prev.computed_pos.y + prev.computed_size.y,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
} };
|
|
} else {
|
|
box.computed_pos = p.computed_pos;
|
|
}
|
|
}
|
|
|
|
switch (box.layout) {
|
|
.fitToText => {
|
|
box.computed_size = Vec2{
|
|
.x = @intToFloat(f32, raylib.MeasureText(box.label, box.style.text_size) + box.style.text_padding * 2),
|
|
.y = @intToFloat(f32, box.style.text_size + box.style.text_padding * 2),
|
|
};
|
|
|
|
_ = ComputeChildrenSize(box);
|
|
},
|
|
.fitToChildren => {
|
|
// TODO: chicken before the egg :sigh:
|
|
box.computed_size = ComputeChildrenSize(box);
|
|
//box.computed_size = total_size;
|
|
},
|
|
.fill => {
|
|
const total_sibling_size = ComputeSiblingSize(box);
|
|
|
|
if (box.parent) |p| {
|
|
box.computed_size = Vec2{
|
|
.x = switch (p.direction) {
|
|
.leftToRight => p.computed_size.x - total_sibling_size.x - box.computed_pos.x,
|
|
.topToBottom => if (total_sibling_size.x == 0) p.computed_size.x else total_sibling_size.x,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
},
|
|
.y = switch (p.direction) {
|
|
.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,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
},
|
|
};
|
|
} else {
|
|
// TODO: somehow need to get these values
|
|
//box.computed_size = Vec2{ .x = 1280, .y = 720 };
|
|
}
|
|
|
|
_ = ComputeChildrenSize(box);
|
|
},
|
|
.percentOfParent => |size| {
|
|
//const total_sibling_size = ComputeSiblingSize(box);
|
|
|
|
// TODO: fix chicken and egg problem of needing to know the parent's computed size
|
|
if (box.parent) |p| {
|
|
box.computed_size = Vec2{
|
|
.x = switch (p.direction) { //
|
|
.leftToRight => p.computed_size.x * size.x,
|
|
.topToBottom => p.computed_size.x, //if (total_sibling_size.x == 0) @intToFloat(f32, raylib.MeasureText(box.label, box.style.text_size) + box.style.text_padding * 2) else total_sibling_size.x,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
},
|
|
.y = switch (p.direction) {
|
|
.leftToRight => @intToFloat(f32, box.style.text_size + box.style.text_padding * 2),
|
|
.topToBottom => p.computed_size.y * size.y,
|
|
.rightToLeft, .bottomToTop => unreachable,
|
|
},
|
|
};
|
|
}
|
|
|
|
_ = ComputeChildrenSize(box);
|
|
},
|
|
.exactSize => |_| unreachable,
|
|
}
|
|
|
|
return box.computed_size;
|
|
}
|
|
|
|
pub fn DrawUI(box: *UI_Box) void {
|
|
const is_hovering = TestBoxHover(box);
|
|
if (box.flags.clickable and is_hovering) {
|
|
mouse_hovering_clickable = true;
|
|
}
|
|
|
|
if (box.flags.drawBackground) {
|
|
const color = if (box.flags.hoverable and is_hovering) box.style.hover_color else box.style.color;
|
|
|
|
raylib.DrawRectangle( //
|
|
@floatToInt(i32, box.computed_pos.x), //
|
|
@floatToInt(i32, box.computed_pos.y), //
|
|
@floatToInt(i32, box.computed_size.x), //
|
|
@floatToInt(i32, box.computed_size.y), //
|
|
color //
|
|
);
|
|
}
|
|
if (box.flags.drawBorder) {
|
|
raylib.DrawRectangleLines( //
|
|
@floatToInt(i32, box.computed_pos.x), //
|
|
@floatToInt(i32, box.computed_pos.y), //
|
|
@floatToInt(i32, box.computed_size.x), //
|
|
@floatToInt(i32, box.computed_size.y), //
|
|
box.style.border_color //
|
|
);
|
|
}
|
|
if (box.flags.drawText) {
|
|
raylib.DrawText( //
|
|
box.label, //
|
|
@floatToInt(i32, box.computed_pos.x) + box.style.text_padding, //
|
|
@floatToInt(i32, box.computed_pos.y) + box.style.text_padding, //
|
|
box.style.text_size, //
|
|
box.style.text_color //
|
|
);
|
|
}
|
|
|
|
// draw children
|
|
const children = CountChildren(box);
|
|
if (children > 0) {
|
|
var child = box.first;
|
|
while (child) |c| {
|
|
DrawUI(c);
|
|
|
|
if (child == box.last) break;
|
|
|
|
child = c.next;
|
|
}
|
|
}
|
|
}
|