855 lines
28 KiB
Zig
855 lines
28 KiB
Zig
// 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);
|
|
}
|
|
}
|
|
}
|