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; }