sokoban-v2/src/ui.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;
}