// zig fmt: off const std = @import("std"); const rl = @import("raylib"); const rect_utils = @import("./rect-utils.zig"); const FontFace = @import("./font-face.zig"); const builtin = @import("builtin"); const srcery = @import("./srcery.zig"); const rlgl_h = @cImport({ @cInclude("rlgl.h"); }); const Rect = rl.Rectangle; const Vec2 = rl.Vector2; const assert = std.debug.assert; const UI = @This(); pub const Interaction = struct { widget: Widget.Key, clicked: bool = false, hovering: bool = false, pressed: bool = false, released: bool = false, held_down: bool = false, }; const SemanticSize = union(enum) { pixels: f32, percent: f32, fit_children, text }; const SemanticVec2 = struct { x: SemanticSize, y: SemanticSize, }; pub const Widget = struct { const Flag = enum { clickable, passthrough }; const Flags = std.EnumSet(Flag); pub const Key = packed struct { hash: u16 = 0, extra: u16 = 0, pub fn fromUsize(number: usize) Key { return .{ .hash = @truncate(number), }; } pub fn eql(self: Key, other: Key) bool { return self.hash == other.hash and self.extra == other.extra; } pub fn indexOf(haystack: []const Key, needle: Key) ?usize { for (0.., haystack) |i, key| { if (key.eql(needle)) { return i; } } return null; } }; const Sides = struct { top: f32 = 0, bottom: f32 = 0, left: f32 = 0, right: f32 = 0, pub fn vertical(self: *Sides, amount: f32) void { self.top = amount; self.bottom = amount; } pub fn horizontal(self: *Sides, amount: f32) void { self.left = amount; self.right = amount; } pub fn all(self: *Sides, amount: f32) void { self.top = amount; self.bottom = amount; self.left = amount; self.right = amount; } }; key: Key = .{}, parent: Key = .{}, flags: Flags = .{}, layout: Layout = .{}, padding: Sides = .{}, corner_radius: f32 = 12, size: SemanticVec2 = .{ .x = .{ .pixels = 0 }, .y = .{ .pixels = 0 }, }, text: ?struct { content: []const u8, color: rl.Color = srcery.bright_white, } = null, background: ?union(enum) { color: rl.Color, blur_world, } = null, computed_relative_position: ?Vec2 = null, computed_content_size: ?Vec2 = null, computed_rect: ?Rect = null, fn computed_size(self: *const Widget) ?Vec2 { if (self.computed_content_size) |content_size| { return Vec2{ .x = content_size.x + self.padding.left + self.padding.right, .y = content_size.y + self.padding.top + self.padding.bottom, }; } else { return null; } } }; pub const Layout = struct { pub const Kind = enum { vertical_column }; kind: Kind = .vertical_column, gap: f32 = 0, key_extra: u16 = 0 }; const debug = false; const max_widgets = 64; const Widgets = std.BoundedArray(Widget, max_widgets); const random_seed = 0; const root_widget_key = Widget.Key{ .hash = 0 }; prev_widgets: Widgets = .{}, widgets: Widgets = .{}, widget_stack: std.BoundedArray(Widget.Key, max_widgets) = .{}, font_face: FontFace, random: std.Random.DefaultPrng, blur_background: ?rl.Texture = null, disable_mouse_interaction: bool = false, // Debug fields duplicate_keys: std.BoundedArray(Widget.Key, max_widgets) = .{}, pub fn init(font_face: FontFace) UI { const random = std.Random.DefaultPrng.init(random_seed); return UI{ .font_face = font_face, .random = random }; } pub fn begin(self: *UI) void { self.prev_widgets = self.widgets; self.widgets.len = 0; self.duplicate_keys.len = 0; self.random = std.Random.DefaultPrng.init(random_seed); self.widgets.appendAssumeCapacity(Widget{ .key = root_widget_key, .size = .{ .x = .{ .pixels = @floatFromInt(rl.getScreenWidth()) }, .y = .{ .pixels = @floatFromInt(rl.getScreenHeight()) }, }, .flags = Widget.Flags.initMany(&.{ .passthrough }) }); self.pushWidget(root_widget_key); } pub fn end(self: *UI) void { self.popWidget(); const widgets: []Widget = self.widgets.slice(); for (widgets) |*widget| { widget.computed_content_size = Vec2{ .x = 0, .y = 0 }; widget.computed_relative_position = Vec2{ .x = 0, .y = 0 }; } // (1) Calculate stadalone sizes for (widgets) |*widget| { var text_size = Vec2{ .x = 0, .y = 0 }; if (widget.text) |text| { text_size = self.font_face.measureText(text.content); } var content_size = &widget.computed_content_size.?; inline for (.{ "x", "y" }) |axis| { const semantic_size = @field(widget.size, axis); if (semantic_size == .pixels) { @field(content_size, axis) = semantic_size.pixels; } else if (semantic_size == .text) { @field(content_size, axis) = @field(text_size, axis); } } } // (2) Upward dependent sizes for (widgets) |*widget| { if (widget.key.eql(root_widget_key)) continue; const parent = self.getWidget(widget.parent); const parent_size = parent.computed_size().?; var content_size = &widget.computed_content_size.?; if (widget.size.x == .percent) { const percent = widget.size.x.percent; content_size.x = parent_size.x * percent; } if (widget.size.y == .percent) { const percent = widget.size.y.percent; content_size.y = parent_size.y * percent; } } for (0..widgets.len) |i| { const widget = &widgets[widgets.len - i - 1]; var children = self.listChildren(widget); // (3) Calculate relative position of each widget switch (widget.layout.kind) { .vertical_column => { var y_offset: f32 = 0; const children_slice: []*Widget = children.slice(); for (children_slice) |child| { child.computed_relative_position.?.y += y_offset; y_offset += child.computed_size().?.y; y_offset += widget.layout.gap; } } } // (4) Downward dependent sizes inline for (.{ "x", "y" }) |axis| { if (@field(widget.size, axis) == .fit_children) { var min = std.math.floatMax(f32); var max = std.math.floatMin(f32); for (children.constSlice()) |child| { const child_size = child.computed_size().?; const child_position = child.computed_relative_position.?; min = @min(min, @field(child_position, axis)); max = @max(max, @field(child_position, axis) + @field(child_size, axis)); } if (children.len > 0) { @field(widget.computed_content_size.?, axis) = max - min; } } } } for (widgets) |*widget| { const computed_size = widget.computed_size().?; const relative_position = widget.computed_relative_position.?; var computed_rect = Rect{ .x = relative_position.x, .y = relative_position.y, .width = computed_size.x, .height = computed_size.y, }; if (!widget.key.eql(root_widget_key)) { const parent = self.getWidget(widget.parent); computed_rect.x += parent.padding.left; computed_rect.y += parent.padding.top; if (parent.computed_rect) |parent_rect| { computed_rect.x += parent_rect.x; computed_rect.y += parent_rect.y; } } widget.computed_rect = computed_rect; } const duplicate_keys = self.duplicate_keys.constSlice(); for (widgets) |*widget|{ const rect = widget.computed_rect orelse continue; const padding = widget.padding; const content_rect = Rect{ .x = rect.x + padding.left, .y = rect.y + padding.top, .width = rect.width - padding.left - padding.right, .height = rect.height - padding.top - padding.bottom, }; if (widget.background) |background| { switch (background) { .color => |bg_color| { drawRectangleRoundedUV(rect, widget.corner_radius, bg_color); }, .blur_world => { const bg_color = srcery.xgray10; if (self.blur_background) |texture| { const border = 2.5; { const previous_texture = rl.getShapesTexture(); const previous_rect = rl.getShapesTextureRectangle(); defer rl.setShapesTexture(previous_texture, previous_rect); const texture_height: f32 = @floatFromInt(texture.height); const shape_rect = rl.Rectangle{ .x = rect.x, .y = texture_height - rect.y, .width = rect.width, .height = -rect.height, }; rl.setShapesTexture(texture, shape_rect); drawRectangleRoundedUV(rect, widget.corner_radius, bg_color); rl.gl.rlDrawRenderBatchActive(); } drawRectangleRoundedLinesEx( rect, widget.corner_radius, border, srcery.bright_white.alpha(0.25) ); rl.gl.rlDrawRenderBatchActive(); } } } } if (widget.text) |text| { self.font_face.drawTextCenter( text.content, rect_utils.center(content_rect), text.color ); } } if (builtin.mode == .Debug) { for (widgets) |widget|{ const rect = widget.computed_rect orelse continue; if (Widget.Key.indexOf(duplicate_keys, widget.key) != null) { const time: f32 = @floatCast(rl.getTime()); if (@rem(time, 0.4) < 0.2) { rl.drawRectangleLinesEx(rect, 3, rl.Color.purple); } else { rl.drawRectangleLinesEx(rect, 3, rl.Color.red); } } } } } pub fn pushWidget(self: *UI, key: Widget.Key) void { self.widget_stack.appendAssumeCapacity(key); } pub fn popWidget(self: *UI) void { assert(self.widget_stack.len >= 1); _ = self.widget_stack.pop(); } pub fn topWidget(self: *UI) *Widget { const top_key = self.widget_stack.buffer[self.widget_stack.len - 1]; return self.getWidget(top_key); } pub fn layout(self: *UI) *Layout { return &self.topWidget().layout; } pub fn getOrAppendWidget(self: *UI, key_hash: u16) *Widget { const parent = self.topWidget(); const key = Widget.Key{ .hash = key_hash, .extra = parent.layout.key_extra }; var found_prev_widget: ?Widget = null; for (self.prev_widgets.constSlice()) |widget| { if (widget.key.eql(key)) { found_prev_widget = widget; break; } } for (self.widgets.constSlice()) |widget| { if (widget.key.eql(key)) { self.duplicate_keys.appendAssumeCapacity(widget.key); break; } } if (found_prev_widget) |prev_widget| { self.widgets.appendAssumeCapacity(prev_widget); } else { self.widgets.appendAssumeCapacity(Widget{ .key = key }); } const widget = &self.widgets.buffer[self.widgets.len - 1]; assert(self.widget_stack.len >= 1); widget.parent = self.widget_stack.buffer[self.widget_stack.len-1]; return widget; } pub fn getWidget(self: *UI, key: Widget.Key) *Widget { for (self.widgets.slice()) |*widget| { if (widget.key.eql(key)) { return widget; } } @panic("Failed to find widget"); } pub fn getInteraction(self: *UI, widget: *const Widget) Interaction { var interaction = Interaction{ .widget = widget.key }; const rect = widget.computed_rect orelse return interaction; if (!self.disable_mouse_interaction) { const mouse = rl.getMousePosition(); if (!widget.flags.contains(.passthrough) and rect_utils.isInsideVec2(rect, mouse)) { interaction.hovering = true; if (rl.isMouseButtonPressed(.mouse_button_left)) { interaction.pressed = true; } if (rl.isMouseButtonReleased(.mouse_button_left)) { interaction.released = true; if (widget.flags.contains(.clickable)) { interaction.clicked = true; } } if (rl.isMouseButtonDown(.mouse_button_left)) { interaction.held_down = true; } } } return interaction; } pub fn isHoveringAnything(self: *UI) bool { const widgets: []Widget = self.widgets.slice(); for (widgets) |*widget|{ const interaction = self.getInteraction(widget); if (interaction.hovering) { return true; } } return false; } pub fn randomWidgetHash(self: *UI) u16 { const rng = self.random.random(); return rng.int(u16); } fn listChildren(self: *UI, parent: *Widget) std.BoundedArray(*Widget, max_widgets) { var children: std.BoundedArray(*Widget, max_widgets) = .{}; const widgets: []Widget = self.widgets.slice(); for (widgets) |*child| { if (child == parent) { continue; } if (child.parent.eql(parent.key)) { children.appendAssumeCapacity(child); } } return children; } // Modified version of `DrawRectangleRounded` where the UV texture coordiantes are consistent and align fn drawRectangleRoundedUV(rec: rl.Rectangle, radius: f32, color: rl.Color) void { if (radius <= 0 or rec.width <= 1 or rec.height <= 1) { rl.drawRectangleRec(rec, color); return; } if (radius <= 0.0) return; // Calculate the maximum angle between segments based on the error rate (usually 0.5f) const smooth_circle_error_rate = 0.5; const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1); var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0); segments = @max(segments, 4); const step_length = 90.0 / @as(f32, @floatFromInt(segments)); // Quick sketch to make sense of all of this, // there are 9 parts to draw, also mark the 12 points we'll use // // P0____________________P1 // /| |\ // /1| 2 |3\ // P7 /__|____________________|__\ P2 // | |P8 P9| | // | 8 | 9 | 4 | // | __|____________________|__ | // P6 \ |P11 P10| / P3 // \7| 6 |5/ // \|____________________|/ // P5 P4 // Coordinates of the 12 points that define the rounded rect const radius_u = radius / rec.width; const radius_v = radius / rec.height; const points = [_]rl.Vector2{ .{ .x = radius_u , .y = 0 }, // P0 .{ .x = 1 - radius_u , .y = 0 }, // P1 .{ .x = 1 , .y = radius_v }, // P2 .{ .x = 1 , .y = 1 - radius_v }, // P3 .{ .x = 1 - radius_u , .y = 1 }, // P4 .{ .x = radius_u , .y = 1 }, // P5 .{ .x = 0 , .y = 1 - radius_v }, // P6 .{ .x = 0 , .y = radius_v }, // P7 .{ .x = radius_u , .y = radius_v }, // P8 .{ .x = 1 - radius_u , .y = radius_v }, // P9 .{ .x = 1 - radius_u , .y = 1 - radius_v }, // P10 .{ .x = radius_u , .y = 1 - radius_v }, // P11 }; const texture = rl.getShapesTexture(); const shape_rect = rl.getShapesTextureRectangle(); const texture_width: f32 = @floatFromInt(texture.width); const texture_height: f32 = @floatFromInt(texture.height); rl.gl.rlBegin(rlgl_h.RL_TRIANGLES); defer rl.gl.rlEnd(); rl.gl.rlSetTexture(texture.id); defer rl.gl.rlSetTexture(0); // Draw all of the 4 corners: [1] Upper Left Corner, [3] Upper Right Corner, [5] Lower Right Corner, [7] Lower Left Corner const centers = [_]rl.Vector2{ points[8], points[9], points[10], points[11] }; const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 }; for (0..4) |k| { var angle = angles[k]; const center = centers[k]; for (0..@intCast(segments)) |_| { rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); const rad_per_deg = std.math.rad_per_deg; const triangle = .{ center, .{ .x = center.x + @cos(rad_per_deg*(angle + step_length))*radius_u, .y = center.y + @sin(rad_per_deg*(angle + step_length))*radius_v }, .{ .x = center.x + @cos(rad_per_deg * angle)*radius_u, .y = center.y + @sin(rad_per_deg * angle)*radius_v } }; inline for (triangle) |point| { rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } angle += step_length; } } // [2] Upper Rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 0, 8, 9, 1, 0, 9 }) |index| { const point = points[index]; rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } // [4] Right Rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 9, 10, 3, 2, 9, 3 }) |index| { const point = points[index]; rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } // [6] Bottom Rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 11, 5, 4, 10, 11, 4 }) |index| { const point = points[index]; rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } // [8] Left Rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 7, 6, 11, 8, 7, 11 }) |index| { const point = points[index]; rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } // [9] Middle Rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 8, 11, 10, 9, 8, 10 }) |index| { const point = points[index]; rl.gl.rlTexCoord2f( (shape_rect.x + shape_rect.width * point.x) / texture_width, (shape_rect.y + shape_rect.height * point.y) / texture_height ); rl.gl.rlVertex2f( rec.x + rec.width * point.x, rec.y + rec.height * point.y ); } } // Modified version of `DrawRectangleRoundedLinesEx` where the corner radius is provided fn drawRectangleRoundedLinesEx(rec: rl.Rectangle, radius: f32, _line_thick: f32, color: rl.Color) void { var line_thick = _line_thick; if (line_thick < 0) line_thick = 0; // Not a rounded rectangle if (radius <= 0.0) { rl.drawRectangleLinesEx(rl.Rectangle{ .x = rec.x, .y = rec.y, .width = rec.width, .height = rec.height }, line_thick, color); return; } // Calculate number of segments to use for the corners const smooth_circle_error_rate = 0.5; const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1); var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0); segments = @max(segments, 4); const step_length = 90.0/@as(f32, @floatFromInt(segments)); const outer_radius = radius; const inner_radius = radius - line_thick; // Quick sketch to make sense of all of this, // marks the 16 + 4(corner centers P16-19) points we'll use // // P0 ================== P1 // // P8 P9 \\ // // \\ // P7 // P15 P10 \\ P2 // || *P16 P17* || // || || // || P14 P11 || // P6 \\ *P19 P18* // P3 // \\ // // \\ P13 P12 // // P5 ================== P4 const points = [_]rl.Vector2{ .{ .x = outer_radius , .y = 0 }, // P0 .{ .x = rec.width - outer_radius , .y = 0 }, // P1 .{ .x = rec.width , .y = outer_radius }, // P2 .{ .x = rec.width , .y = rec.height - outer_radius }, // P3 .{ .x = rec.width - outer_radius , .y = rec.height }, // P4 .{ .x = outer_radius , .y = rec.height }, // P5 .{ .x = 0 , .y = rec.height - outer_radius }, // P6 .{ .x = 0 , .y = outer_radius }, // P7 .{ .x = outer_radius , .y = line_thick }, // P8 .{ .x = rec.width - outer_radius , .y = line_thick }, // P9 .{ .x = rec.width - line_thick , .y = outer_radius }, // P10 .{ .x = rec.width - line_thick , .y = rec.height - outer_radius }, // P11 .{ .x = rec.width - outer_radius , .y = rec.height - line_thick }, // P12 .{ .x = outer_radius , .y = rec.height - line_thick }, // P13 .{ .x = line_thick , .y = rec.height - outer_radius }, // P14 .{ .x = line_thick , .y = outer_radius }, // P15 }; const centers = [_]rl.Vector2{ .{ .x = outer_radius , .y = outer_radius }, // P16 .{ .x = rec.width - outer_radius , .y = outer_radius }, // P17 .{ .x = rec.width - outer_radius , .y = rec.height - outer_radius }, // P18 .{ .x = outer_radius , .y = rec.height - outer_radius }, // P18 }; const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 }; if (line_thick > 1) { rl.gl.rlBegin(rlgl_h.RL_TRIANGLES); defer rl.gl.rlEnd(); // Draw all of the 4 corners first: Upper Left Corner, Upper Right Corner, Lower Right Corner, Lower Left Corner for (centers, angles) |center, initialAngle| { var angle = initialAngle; for (0..@intCast(segments)) |_| { const rad_per_deg = std.math.rad_per_deg; const next_angle = angle + step_length; const rect_points = .{ rl.Vector2{ .x = @cos(rad_per_deg * angle) * inner_radius, .y = @sin(rad_per_deg * angle) * inner_radius }, rl.Vector2{ .x = @cos(rad_per_deg * next_angle) * inner_radius, .y = @sin(rad_per_deg * next_angle) * inner_radius }, rl.Vector2{ .x = @cos(rad_per_deg * angle) * outer_radius, .y = @sin(rad_per_deg * angle) * outer_radius }, rl.Vector2{ .x = @cos(rad_per_deg * next_angle) * outer_radius, .y = @sin(rad_per_deg * next_angle) * outer_radius } }; rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 0, 1, 2, 1, 3, 2 }) |i| { rl.gl.rlVertex2f( rec.x + center.x + rect_points[i].x, rec.y + center.y + rect_points[i].y ); } angle = next_angle; } } // Upper rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 0, 8, 9, 1, 0, 9 }) |i| { rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); } // Right rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 10, 11, 3, 2, 10, 3 }) |i| { rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); } // Lower rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 13, 5, 4, 12, 13, 4 }) |i| { rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); } // Left rectangle rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); inline for (.{ 7, 6, 14, 15, 7, 14 }) |i| { rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); } } else { // Use LINES to draw the outline rl.gl.rlBegin(rlgl_h.RL_LINES); defer rl.gl.rlEnd(); // Draw all the 4 corners first: Upper Left Corner, Upper Right Corner, Lower Right Corner, Lower Left Corner for (centers, angles) |center, initialAngle| { var angle = initialAngle; for (0..@intCast(segments)) |_| { const rad_per_deg = std.math.rad_per_deg; const next_angle = angle + step_length; rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); rl.gl.rlVertex2f( rec.x + center.x + @cos(rad_per_deg*angle)*outer_radius, rec.y + center.y + @sin(rad_per_deg*angle)*outer_radius ); rl.gl.rlVertex2f( rec.x + center.x + @cos(rad_per_deg*next_angle)*outer_radius, rec.y + center.y + @sin(rad_per_deg*next_angle)*outer_radius ); angle = next_angle; } } // And now the remaining 4 lines inline for (.{ 0, 2, 4, 6 }) |i| { rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); rl.gl.rlVertex2f(rec.x + points[i + 0].x, rec.y + points[i + 0].y); rl.gl.rlVertex2f(rec.x + points[i + 1].x, rec.y + points[i + 1].y); } } }