337 lines
8.0 KiB
Zig
337 lines
8.0 KiB
Zig
const std = @import("std");
|
|
const log = std.log;
|
|
const assert = std.debug.assert;
|
|
const Font = @import("./font.zig");
|
|
|
|
const Platform = @import("platform");
|
|
const Gfx = Platform.Gfx;
|
|
const Math = Platform.Math;
|
|
const Color = Platform.Color;
|
|
const Vec2 = Platform.Vec2;
|
|
const Rect = Math.Rect;
|
|
|
|
const UI = @This();
|
|
|
|
pub const Key = enum(u64) {
|
|
_,
|
|
|
|
const StringHasher = std.hash.Wyhash;
|
|
pub const CombineHasher = std.hash.Fnv1a_64;
|
|
|
|
pub const nil = Key.init(0);
|
|
|
|
pub fn init(hash: u64) Key {
|
|
return @enumFromInt(hash);
|
|
}
|
|
|
|
pub fn initPtr(ptr: anytype) Key {
|
|
return Key.initUsize(@intFromPtr(ptr));
|
|
}
|
|
|
|
pub fn initUsize(num: usize) Key {
|
|
return Key.init(@truncate(num));
|
|
}
|
|
|
|
pub fn initString(seed: u64, text: []const u8) Key {
|
|
return Key.init(StringHasher.hash(seed, text));
|
|
}
|
|
|
|
pub fn combine(self: Key, other: Key) Key {
|
|
var hasher = CombineHasher.init();
|
|
hasher.update(std.mem.asBytes(&@intFromEnum(self)));
|
|
hasher.update(std.mem.asBytes(&@intFromEnum(other)));
|
|
|
|
return Key.init(hasher.final());
|
|
}
|
|
|
|
pub fn isNil(self: Key) bool {
|
|
return self == nil;
|
|
}
|
|
|
|
pub fn format(self: Key, writer: *std.io.Writer) std.io.Writer.Error!void {
|
|
try writer.print("{s}{{ 0x{x:016} }}", .{ @typeName(Key), @intFromEnum(self) });
|
|
}
|
|
};
|
|
|
|
pub const VerticalLayout = struct {
|
|
ui: *UI,
|
|
used: f32 = 0,
|
|
gap: f32 = 0,
|
|
|
|
pub fn advance(self: *VerticalLayout, height: f32) void {
|
|
self.used += height;
|
|
self.used += self.gap;
|
|
}
|
|
|
|
pub fn next(self: *VerticalLayout, height: f32) Rect {
|
|
const container = self.ui.top().container;
|
|
const result = Rect{
|
|
.pos = container.pos.add(.init(0, self.used)),
|
|
.size = .init(container.size.x, height)
|
|
};
|
|
|
|
self.advance(height);
|
|
return result;
|
|
}
|
|
|
|
pub fn textButton(self: *VerticalLayout, text: []const u8) bool {
|
|
const frame = self.ui.top();
|
|
const font = frame.font orelse return false;
|
|
|
|
var button_size = Vec2{
|
|
.x = font.measureText(text, frame.text_height).size.x,
|
|
.y = font.getAscent(frame.text_height)
|
|
};
|
|
button_size = button_size.add(frame.padding.toVec2());
|
|
|
|
const row_rect = self.next(button_size.y);
|
|
const button_rect = Rect{
|
|
.pos = row_rect.pos,
|
|
.size = button_size
|
|
};
|
|
|
|
return self.ui.textButton(button_rect, text);
|
|
}
|
|
};
|
|
|
|
pub const Sides = struct {
|
|
left: f32,
|
|
right: f32,
|
|
top: f32,
|
|
bottom: f32,
|
|
|
|
pub const empty = Sides{
|
|
.left = 0,
|
|
.right = 0,
|
|
.top = 0,
|
|
.bottom = 0
|
|
};
|
|
|
|
pub fn grow(self: Sides, rect: Rect) Rect {
|
|
return Rect{
|
|
.pos = rect.pos.sub(.init(self.left, self.top)),
|
|
.size = rect.size.add(.init(self.left+self.right, self.top+self.bottom))
|
|
};
|
|
}
|
|
|
|
pub fn shrink(self: Sides, rect: Rect) Rect {
|
|
return Rect{
|
|
.pos = rect.pos.add(.init(self.left, self.top)),
|
|
.size = rect.size.sub(.init(self.left+self.right, self.top+self.bottom))
|
|
};
|
|
}
|
|
|
|
pub fn add(lhs: Sides, rhs: Sides) Sides {
|
|
return Sides{
|
|
.left = lhs.left + rhs.left,
|
|
.right = lhs.right + rhs.right,
|
|
.top = lhs.top + rhs.top,
|
|
.bottom = lhs.bottom + rhs.bottom,
|
|
};
|
|
}
|
|
|
|
pub fn initVertical(amount: f32) Sides {
|
|
return Sides{
|
|
.top = amount/2,
|
|
.bottom = amount/2,
|
|
.left = 0,
|
|
.right = 0,
|
|
};
|
|
}
|
|
|
|
pub fn initHorizontal(amount: f32) Sides {
|
|
return Sides{
|
|
.left = amount/2,
|
|
.right = amount/2,
|
|
.top = 0,
|
|
.bottom = 0,
|
|
};
|
|
}
|
|
|
|
pub fn vertical(self: Sides) f32 {
|
|
return self.top + self.bottom;
|
|
}
|
|
|
|
pub fn horizontal(self: Sides) f32 {
|
|
return self.left + self.right;
|
|
}
|
|
|
|
pub fn addVertical(self: *Sides, amount: f32) void {
|
|
self.* = self.add(.initVertical(amount));
|
|
}
|
|
|
|
pub fn addHorizontal(self: *Sides, amount: f32) void {
|
|
self.* = self.add(.initHorizontal(amount));
|
|
}
|
|
|
|
pub fn toVec2(self: Sides) Vec2 {
|
|
return Vec2.init(self.horizontal(), self.vertical());
|
|
}
|
|
};
|
|
|
|
const Box = struct {
|
|
margin: Sides,
|
|
padding: Sides,
|
|
};
|
|
|
|
pub const Frame = struct {
|
|
container: Rect = .zero,
|
|
key: Key = .nil,
|
|
font: ?*Font = null,
|
|
text_color: Color = .white,
|
|
text_height: f32 = 64,
|
|
margin: Sides = .empty,
|
|
padding: Sides = .empty,
|
|
};
|
|
|
|
pub const State = struct {
|
|
hot: Key,
|
|
active: Key,
|
|
|
|
pub const empty = State{
|
|
.hot = .nil,
|
|
.active = .nil
|
|
};
|
|
};
|
|
|
|
state: *State,
|
|
input: *Platform.Input,
|
|
stack: [32]Frame,
|
|
stack_len: u32,
|
|
|
|
pub fn init(state: *State, input: *Platform.Input) UI {
|
|
var stack: [32]Frame = undefined;
|
|
stack[0] = Frame{};
|
|
|
|
return UI{
|
|
.state = state,
|
|
.input = input,
|
|
.stack = stack,
|
|
.stack_len = 1
|
|
};
|
|
}
|
|
|
|
pub fn beginFrame(self: *UI) void {
|
|
if (self.state.active == Key.nil) {
|
|
self.state.hot = .nil;
|
|
}
|
|
}
|
|
|
|
pub fn endFrame(self: *UI) void {
|
|
if (self.input.isMouseReleased(.left)) {
|
|
self.state.active = .nil;
|
|
}
|
|
}
|
|
|
|
pub fn push(self: *UI) void {
|
|
if (self.stack_len == self.stack.len) {
|
|
log.warn("UI stack capacity limit reached", .{});
|
|
return;
|
|
}
|
|
assert(self.stack_len >= 1);
|
|
self.stack[self.stack_len] = self.stack[self.stack_len-1];
|
|
self.stack_len += 1;
|
|
}
|
|
|
|
pub fn pop(self: *UI) void {
|
|
if (self.stack_len <= 1) {
|
|
log.warn("Attempt to pop from empty UI", .{});
|
|
return;
|
|
}
|
|
self.stack_len -= 1;
|
|
}
|
|
|
|
pub fn top(self: *UI) *Frame {
|
|
assert(self.stack_len > 0);
|
|
return &self.stack[self.stack_len-1];
|
|
}
|
|
|
|
pub fn measureText(self: *UI, text: []const u8) Rect {
|
|
const frame = self.top();
|
|
const font = frame.font orelse return .zero;
|
|
|
|
return font.measureText(
|
|
text,
|
|
frame.text_height,
|
|
);
|
|
}
|
|
|
|
pub fn drawText(self: *UI, pos: Vec2, text: []const u8) void {
|
|
const frame = self.top();
|
|
const font = frame.font orelse return;
|
|
|
|
font.drawText(.{
|
|
.pos = pos,
|
|
.text = text,
|
|
.height = frame.text_height,
|
|
.color = frame.text_color
|
|
});
|
|
}
|
|
|
|
pub fn getMousePosition(self: *UI) ?Vec2 {
|
|
if (self.input.mouse_position) |screen_mouse_position| {
|
|
return Gfx.getTransform().applyInverse(screen_mouse_position);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const ButtonState = enum {
|
|
normal,
|
|
hot,
|
|
active,
|
|
released,
|
|
};
|
|
|
|
fn buttonLogic(self: *UI, rect: Rect, key: Key) ButtonState {
|
|
if (self.getMousePosition()) |mouse_position| {
|
|
const is_inside = rect.isInside(mouse_position);
|
|
if (self.state.hot == Key.nil and is_inside) {
|
|
self.state.hot = key;
|
|
}
|
|
|
|
if (self.state.hot == key and self.input.isMousePressed(.left)) {
|
|
self.state.active = key;
|
|
}
|
|
|
|
if (self.state.active == key and self.input.isMouseReleased(.left) and is_inside) {
|
|
return .released;
|
|
}
|
|
}
|
|
|
|
|
|
if (self.state.active == key) {
|
|
return .active;
|
|
} else if (self.state.hot == key) {
|
|
return .hot;
|
|
} else {
|
|
return .normal;
|
|
}
|
|
}
|
|
|
|
pub fn textButton(self: *UI, rect: Rect, text: []const u8) bool {
|
|
const frame = self.top();
|
|
const text_bounds = self.measureText(text);
|
|
|
|
const button_state = self.buttonLogic(rect, Key.initString(@intFromEnum(frame.key), text));
|
|
|
|
if (button_state == .active) {
|
|
Gfx.drawRectangle(rect.pos, rect.size, .blue);
|
|
} else if (button_state == .hot) {
|
|
Gfx.drawRectangle(rect.pos, rect.size, .red);
|
|
} else {
|
|
Gfx.drawRectangle(rect.pos, rect.size, .white);
|
|
}
|
|
|
|
const content_size = rect.size.sub(frame.padding.toVec2());
|
|
|
|
self.drawText(
|
|
rect.pos
|
|
.sub(text_bounds.pos)
|
|
.add(.init(frame.padding.left, frame.padding.top))
|
|
.add(content_size.sub(text_bounds.size).multiplyScalar(0.5)),
|
|
text
|
|
);
|
|
|
|
return button_state == .released;
|
|
}
|