2862 lines
88 KiB
Zig
2862 lines
88 KiB
Zig
const std = @import("std");
|
|
const rl = @import("raylib");
|
|
const srcery = @import("./srcery.zig");
|
|
const Assets = @import("./assets.zig");
|
|
const rect_utils = @import("./rect-utils.zig");
|
|
const utils = @import("./utils.zig");
|
|
const builtin = @import("builtin");
|
|
const FontFace = @import("./font-face.zig");
|
|
const Platform = @import("./platform/root.zig");
|
|
const raylib_h = @cImport({
|
|
@cInclude("stdio.h");
|
|
@cInclude("raylib.h");
|
|
});
|
|
|
|
const assert = std.debug.assert;
|
|
pub const Vec2 = rl.Vector2;
|
|
pub const Rect = rl.Rectangle;
|
|
const clamp = std.math.clamp;
|
|
const log = std.log.scoped(.ui);
|
|
|
|
const Vec2Zero = Vec2{ .x = 0, .y = 0 };
|
|
|
|
const UI = @This();
|
|
|
|
const max_boxes = 512;
|
|
const max_events = 256;
|
|
const draw_debug = false; //builtin.mode == .Debug;
|
|
const default_font = Assets.FontId{ .variant = .regular, .size = 16 };
|
|
|
|
pub const Key = struct {
|
|
const StringHasher = std.hash.XxHash3;
|
|
pub const CombineHasher = std.hash.Fnv1a_64;
|
|
|
|
hash: u64 = 0,
|
|
|
|
pub fn init(hash: u64) Key {
|
|
return Key{
|
|
.hash = hash
|
|
};
|
|
}
|
|
|
|
pub fn initPtr(ptr: anytype) Key {
|
|
return Key.initUsize(@intFromPtr(ptr));
|
|
}
|
|
|
|
pub fn initUsize(num: usize) Key {
|
|
return Key{
|
|
.hash = @truncate(num)
|
|
};
|
|
}
|
|
|
|
pub fn initString(seed: u64, text: []const u8) Key {
|
|
return Key{
|
|
.hash = StringHasher.hash(seed, text)
|
|
};
|
|
}
|
|
|
|
pub fn combine(self: Key, other: Key) Key {
|
|
var hasher = CombineHasher.init();
|
|
hasher.update(std.mem.asBytes(&self.hash));
|
|
hasher.update(std.mem.asBytes(&other.hash));
|
|
|
|
return Key{
|
|
.hash = hasher.final()
|
|
};
|
|
}
|
|
|
|
pub fn initNil() Key {
|
|
return Key{ .hash = 0 };
|
|
}
|
|
|
|
pub fn eql(self: Key, other: Key) bool {
|
|
return self.hash == other.hash;
|
|
}
|
|
|
|
pub fn isNil(self: Key) bool {
|
|
return self.hash == 0;
|
|
}
|
|
|
|
pub fn format(
|
|
self: Key,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
|
|
try writer.print("{s}{{ 0x{x} }}", .{ @typeName(Key), self.hash });
|
|
}
|
|
};
|
|
|
|
pub const Event = union(enum) {
|
|
mouse_pressed: rl.MouseButton,
|
|
mouse_released: rl.MouseButton,
|
|
mouse_move: Vec2,
|
|
mouse_scroll: Vec2,
|
|
key_pressed: u32,
|
|
key_released: u32,
|
|
char_pressed: u32
|
|
};
|
|
|
|
pub const Signal = struct {
|
|
pub const Flag = enum {
|
|
left_pressed,
|
|
middle_pressed,
|
|
right_pressed,
|
|
|
|
left_released,
|
|
middle_released,
|
|
right_released,
|
|
|
|
left_clicked,
|
|
middle_clicked,
|
|
right_clicked,
|
|
|
|
left_dragging,
|
|
middle_dragging,
|
|
right_dragging,
|
|
|
|
scrolled
|
|
};
|
|
|
|
pub const Flags = std.EnumSet(Flag);
|
|
|
|
flags: Flags = .{},
|
|
drag: Vec2 = Vec2Zero,
|
|
scroll: Vec2 = Vec2Zero,
|
|
relative_mouse: Vec2 = Vec2Zero,
|
|
mouse: Vec2 = Vec2Zero,
|
|
is_mouse_inside: bool = false,
|
|
hot: bool = false,
|
|
active: bool = false,
|
|
shift_modifier: bool = false,
|
|
ctrl_modifier: bool = false,
|
|
clicked_outside: bool = false,
|
|
|
|
pub fn clicked(self: Signal) bool {
|
|
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked) or self.flags.contains(.middle_clicked);
|
|
}
|
|
|
|
pub fn dragged(self: Signal) bool {
|
|
return self.flags.contains(.left_dragging) or self.flags.contains(.right_dragging) or self.flags.contains(.middle_dragging);
|
|
}
|
|
|
|
pub fn scrolled(self: Signal) bool {
|
|
return self.flags.contains(.scrolled);
|
|
}
|
|
|
|
fn insertMousePressed(self: *Signal, mouse_button: rl.MouseButton) void {
|
|
if (mouse_button == .mouse_button_left) {
|
|
self.flags.insert(.left_pressed);
|
|
} else if (mouse_button == .mouse_button_right) {
|
|
self.flags.insert(.right_pressed);
|
|
} else if (mouse_button == .mouse_button_middle) {
|
|
self.flags.insert(.middle_pressed);
|
|
}
|
|
}
|
|
|
|
fn insertMouseReleased(self: *Signal, mouse_button: rl.MouseButton) void {
|
|
if (mouse_button == .mouse_button_left) {
|
|
self.flags.insert(.left_released);
|
|
} else if (mouse_button == .mouse_button_right) {
|
|
self.flags.insert(.right_released);
|
|
} else if (mouse_button == .mouse_button_middle) {
|
|
self.flags.insert(.middle_released);
|
|
}
|
|
}
|
|
|
|
fn insertMouseClicked(self: *Signal, mouse_button: rl.MouseButton) void {
|
|
if (mouse_button == .mouse_button_left) {
|
|
self.flags.insert(.left_clicked);
|
|
} else if (mouse_button == .mouse_button_right) {
|
|
self.flags.insert(.right_clicked);
|
|
} else if (mouse_button == .mouse_button_middle) {
|
|
self.flags.insert(.middle_clicked);
|
|
}
|
|
}
|
|
|
|
fn insertMouseDragged(self: *Signal, mouse_button: rl.MouseButton) void {
|
|
if (mouse_button == .mouse_button_left) {
|
|
self.flags.insert(.left_dragging);
|
|
} else if (mouse_button == .mouse_button_right) {
|
|
self.flags.insert(.right_dragging);
|
|
} else if (mouse_button == .mouse_button_middle) {
|
|
self.flags.insert(.middle_dragging);
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Axis = enum {
|
|
X, Y,
|
|
|
|
pub fn flip(self: Axis) Axis {
|
|
return switch (self) {
|
|
.X => .Y,
|
|
.Y => .X
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Unit = union(enum) {
|
|
pixels: f32,
|
|
parent_percent: f32,
|
|
text,
|
|
font_size: f32,
|
|
|
|
pub fn initPixels(pixels: f32) Unit {
|
|
return Unit{
|
|
.pixels = pixels
|
|
};
|
|
}
|
|
|
|
pub fn initParentPct(percent: f32) Unit {
|
|
return Unit{
|
|
.parent_percent = percent
|
|
};
|
|
}
|
|
|
|
fn get(self: Unit, box: *const Box, axis: Axis) f32 {
|
|
switch (self) {
|
|
.pixels => |pixels| {
|
|
return pixels;
|
|
},
|
|
.parent_percent => |pct| {
|
|
const parent_box = box.parent() orelse return 0;
|
|
return parent_box.availableChildrenSize(axis) * pct;
|
|
},
|
|
.text => {
|
|
const text = box.text orelse return 0;
|
|
const font_face = Assets.font(box.font);
|
|
|
|
const lines: []const []u8 = box.text_lines.constSlice();
|
|
var text_size: Vec2 = undefined;
|
|
if (lines.len == 0) {
|
|
text_size = font_face.measureText(text);
|
|
} else {
|
|
text_size = font_face.measureTextLines(lines);
|
|
}
|
|
|
|
return vec2ByAxis(&text_size, axis).*;
|
|
},
|
|
.font_size => |count| {
|
|
return box.font.size * count;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Sizing = union(enum) {
|
|
fixed: Unit,
|
|
grow: struct { min: Unit, max: Unit },
|
|
shrink: struct { min: Unit, max: Unit },
|
|
fit_children,
|
|
|
|
pub fn initFitChildren() Sizing {
|
|
return Sizing{
|
|
.fit_children = {}
|
|
};
|
|
}
|
|
|
|
pub fn initFixed(size: Unit) Sizing {
|
|
return Sizing{
|
|
.fixed = size
|
|
};
|
|
}
|
|
|
|
pub fn initFixedPixels(size: f32) Sizing {
|
|
return Sizing{
|
|
.fixed = Unit.initPixels(size)
|
|
};
|
|
}
|
|
|
|
pub fn initFixedText() Sizing {
|
|
return Sizing{
|
|
.fixed = .text
|
|
};
|
|
}
|
|
|
|
pub fn initGrowFull() Sizing {
|
|
return initGrow(
|
|
.{ .pixels = 0 },
|
|
.{ .parent_percent = 1 }
|
|
);
|
|
}
|
|
|
|
pub fn initGrowUpTo(size: Unit) Sizing {
|
|
return initGrow(
|
|
.{ .pixels = 0 },
|
|
size
|
|
);
|
|
}
|
|
|
|
pub fn initGrowFrom(size: Unit) Sizing {
|
|
return initGrow(
|
|
size,
|
|
.{ .parent_percent = 1 }
|
|
);
|
|
}
|
|
|
|
pub fn initGrow(min: Unit, max: Unit) Sizing {
|
|
return Sizing{
|
|
.grow = .{
|
|
.min = min,
|
|
.max = max
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn initShrinkFull() Sizing {
|
|
return initShrink(
|
|
.{ .pixels = 0 },
|
|
.{ .parent_percent = 1 }
|
|
);
|
|
}
|
|
|
|
pub fn initShrink(min: Unit, max: Unit) Sizing {
|
|
return Sizing{
|
|
.shrink = .{
|
|
.min = min,
|
|
.max = max
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn dependsOnParent(self: Sizing) bool {
|
|
return switch (self) {
|
|
.fixed => |fixed| fixed == .parent_percent,
|
|
.grow => |range| range.min == .parent_percent or range.max == .parent_percent,
|
|
.shrink => |range| range.min == .parent_percent or range.max == .parent_percent,
|
|
.fit_children => false
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Sizing2 = struct {
|
|
x: Sizing,
|
|
y: Sizing,
|
|
|
|
pub fn getAxis(self: Sizing2, axis: Axis) Sizing {
|
|
return switch (axis) {
|
|
.X => self.x,
|
|
.Y => self.y,
|
|
};
|
|
}
|
|
|
|
pub fn getAxisPtr(self: *Sizing2, axis: Axis) *Sizing {
|
|
return switch (axis) {
|
|
.X => &self.x,
|
|
.Y => &self.y,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const LayoutDirection = enum {
|
|
left_to_right,
|
|
top_to_bottom,
|
|
// TODO: right_to_left
|
|
// TODO: bottom_to_top
|
|
|
|
pub fn isAxis(self: LayoutDirection, axis: Axis) bool {
|
|
return switch (self) {
|
|
.left_to_right => (axis == .X),
|
|
.top_to_bottom => (axis == .Y)
|
|
};
|
|
}
|
|
};
|
|
|
|
// TODO: Use `Size` instead of f32 in `Padding`
|
|
// Not sure if it is worth implementing this now. This will really complicate layout because of .parent_percent
|
|
pub const Padding = struct {
|
|
top : f32 = 0,
|
|
bottom: f32 = 0,
|
|
left : f32 = 0,
|
|
right : f32 = 0,
|
|
|
|
pub fn all(size: f32) Padding {
|
|
return Padding{
|
|
.left = size,
|
|
.right = size,
|
|
.top = size,
|
|
.bottom = size
|
|
};
|
|
}
|
|
|
|
pub fn vertical(size: f32) Padding {
|
|
return Padding{
|
|
.top = size,
|
|
.bottom = size
|
|
};
|
|
}
|
|
|
|
pub fn horizontal(size: f32) Padding {
|
|
return Padding{
|
|
.left = size,
|
|
.right = size
|
|
};
|
|
}
|
|
|
|
pub fn byAxis(self: Padding, axis: Axis) f32 {
|
|
return switch (axis) {
|
|
.X => self.left + self.right,
|
|
.Y => self.top + self.bottom
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Alignment = enum {
|
|
start,
|
|
center,
|
|
end,
|
|
|
|
pub fn getCoefficient(self: Alignment) f32 {
|
|
return switch (self) {
|
|
.start => 0,
|
|
.center => 0.5,
|
|
.end => 1
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Alignment2 = struct {
|
|
x: Alignment,
|
|
y: Alignment,
|
|
|
|
pub fn getAxis(self: Alignment2, axis: Axis) Alignment {
|
|
return switch (axis) {
|
|
.X => self.x,
|
|
.Y => self.y,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const Border = struct {
|
|
size: f32 = 0,
|
|
color: rl.Color = rl.Color.magenta,
|
|
};
|
|
|
|
pub const Borders = struct {
|
|
left: Border = .{},
|
|
right: Border = .{},
|
|
top: Border = .{},
|
|
bottom: Border = .{},
|
|
|
|
pub fn all(border: Border) Borders {
|
|
return Borders{
|
|
.left = border,
|
|
.right = border,
|
|
.top = border,
|
|
.bottom = border,
|
|
};
|
|
}
|
|
|
|
pub fn vertical(border: Border) Borders {
|
|
return Borders{
|
|
.top = border,
|
|
.bottom = border,
|
|
};
|
|
}
|
|
|
|
pub fn horizontal(border: Border) Borders {
|
|
return Borders{
|
|
.left = border,
|
|
.right = border,
|
|
};
|
|
}
|
|
|
|
pub fn bottom(border: Border) Borders {
|
|
return Borders{
|
|
.bottom = border,
|
|
};
|
|
}
|
|
};
|
|
|
|
const BoxIndex = std.math.IntFittingRange(0, max_boxes);
|
|
pub const Box = struct {
|
|
pub const Persistent = struct {
|
|
position: Vec2 = Vec2Zero,
|
|
size: Vec2 = Vec2Zero,
|
|
sroll_offset: f32 = 0,
|
|
hot: f32 = 0,
|
|
active: f32 = 0,
|
|
};
|
|
|
|
pub const Flag = enum {
|
|
wrap_text,
|
|
float_x,
|
|
float_y,
|
|
clickable,
|
|
scrollable,
|
|
draggable,
|
|
draw_hot,
|
|
draw_active,
|
|
clip_view
|
|
};
|
|
|
|
pub const Flags = std.EnumSet(Flag);
|
|
|
|
pub const Draw = struct {
|
|
ctx: ?*anyopaque = null,
|
|
do: *const fn(ctx: ?*anyopaque, box: *Box) void
|
|
};
|
|
|
|
const max_wrapped_lines = 64;
|
|
|
|
ui: *UI,
|
|
allocator: std.mem.Allocator,
|
|
created: bool = false,
|
|
|
|
flags: Flags,
|
|
background: ?rl.Color,
|
|
layout_direction: LayoutDirection,
|
|
// TODO: Use `Size` instead of f32 in `layout_gap`. Not sure if this is needed at the moment.
|
|
layout_gap: f32,
|
|
persistent: Persistent,
|
|
alignment: Alignment2,
|
|
size: Sizing2,
|
|
// TODO: Add option for changing how padding interacts with size. Does it remove from the available size, or is it added on top
|
|
// Currently it is added on top
|
|
padding: Padding,
|
|
font: Assets.FontId,
|
|
text_color: rl.Color,
|
|
// TODO: Add option to specify where the border is drawn: outside, inside, center.
|
|
borders: Borders,
|
|
text: ?[]u8,
|
|
hot_cursor: ?rl.MouseCursor,
|
|
active_cursor: ?rl.MouseCursor,
|
|
view_offset: Vec2 = Vec2Zero,
|
|
texture: ?rl.Texture2D = null,
|
|
texture_size: ?Vec2 = null,
|
|
texture_color: ?rl.Color = null,
|
|
scientific_number: ?f64 = null,
|
|
scientific_precision: u32 = 1,
|
|
float_relative_to: ?*Box = null,
|
|
draw: ?Draw = null,
|
|
visual_hot: bool = false,
|
|
visual_active: bool = false,
|
|
tooltip: ?[]const u8 = null,
|
|
float_x: ?f32 = null,
|
|
float_y: ?f32 = null,
|
|
|
|
// Variables that you probably shouldn't be touching
|
|
last_used_frame: u64 = 0,
|
|
key: Key = Key.initNil(),
|
|
text_lines: std.BoundedArray([]u8, max_wrapped_lines) = .{ },
|
|
|
|
// Fields for maintaining tree data structure
|
|
tree: struct {
|
|
// Index of this box
|
|
index: BoxIndex,
|
|
// Go down the tree to the first child
|
|
first_child_index: ?BoxIndex = null,
|
|
// Go down the tree to the last child
|
|
last_child_index: ?BoxIndex = null,
|
|
// Go up the tree
|
|
parent_index: ?BoxIndex = null,
|
|
// Go the next node on the same level
|
|
next_sibling_index: ?BoxIndex = null,
|
|
// Go to previous node on the same level
|
|
prev_sibling_index: ?BoxIndex = null,
|
|
},
|
|
|
|
pub fn beginChildren(self: *Box) void {
|
|
self.ui.parent_stack.appendAssumeCapacity(self.tree.index);
|
|
}
|
|
|
|
pub fn endChildren(self: *Box) void {
|
|
const index = self.ui.parent_stack.pop();
|
|
assert(self.tree.index == index);
|
|
}
|
|
|
|
pub fn iterChildren(self: *const Box) BoxChildIterator {
|
|
return BoxChildIterator{
|
|
.boxes = self.ui.boxes.slice(),
|
|
.current_child = self.tree.first_child_index
|
|
};
|
|
}
|
|
|
|
pub fn iterParents(self: *const Box) BoxParentIterator {
|
|
return BoxParentIterator{
|
|
.boxes = self.ui.boxes.slice(),
|
|
.current_parent = self.tree.parent_index
|
|
};
|
|
}
|
|
|
|
pub fn rect(self: *const Box) Rect {
|
|
return Rect{
|
|
.x = self.persistent.position.x,
|
|
.y = self.persistent.position.y,
|
|
.width = self.persistent.size.x,
|
|
.height = self.persistent.size.y,
|
|
};
|
|
}
|
|
|
|
pub fn parent(self: *const Box) ?*Box {
|
|
if (self.tree.parent_index) |parent_index| {
|
|
return self.ui.getBoxByIndex(parent_index);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn setText(self: *Box, text: []const u8) void {
|
|
self.text = self.allocator.dupe(u8, text) catch return;
|
|
self.text_lines.len = 0;
|
|
}
|
|
|
|
pub fn setFmtText(self: *Box, comptime fmt: []const u8, args: anytype) void {
|
|
self.text = std.fmt.allocPrint(self.allocator, fmt, args) catch return;
|
|
self.text_lines.len = 0;
|
|
}
|
|
|
|
pub fn setFloatX(self: *Box, x: f32) void {
|
|
self.float_x = x;
|
|
}
|
|
|
|
pub fn setFloatY(self: *Box, y: f32) void {
|
|
self.float_y = y;
|
|
}
|
|
|
|
pub fn setFloatRect(self: *Box, float_rect: Rect) void {
|
|
self.setFloatX(float_rect.x);
|
|
self.setFloatY(float_rect.y);
|
|
self.size.x = .{ .fixed = .{ .pixels = float_rect.width } };
|
|
self.size.y = .{ .fixed = .{ .pixels = float_rect.height } };
|
|
}
|
|
|
|
pub fn wrapText(self: *Box, width: f32) void {
|
|
self.text_lines.len = 0;
|
|
|
|
const text = self.text orelse return;
|
|
const font_face = Assets.font(self.font);
|
|
|
|
var line_start: usize = 0;
|
|
var prev_word_end: usize = 0;
|
|
var word_iter = std.mem.tokenizeScalar(u8, text, ' ');
|
|
while (true) {
|
|
_ = word_iter.peek();
|
|
const word_start = word_iter.index;
|
|
if (word_iter.next() == null) break;
|
|
const word_end = word_iter.index;
|
|
|
|
const line = text[line_start..word_end];
|
|
const line_size = font_face.measureText(line);
|
|
if (line_size.x > width) {
|
|
const prev_line = text[line_start..prev_word_end];
|
|
self.text_lines.append(prev_line) catch return;
|
|
|
|
line_start = word_start;
|
|
}
|
|
|
|
prev_word_end = word_end;
|
|
}
|
|
|
|
if (line_start < text.len) {
|
|
self.text_lines.append(text[line_start..prev_word_end]) catch return;
|
|
}
|
|
}
|
|
|
|
fn isFloating(self: *const Box, axis: Axis) bool {
|
|
return switch (axis) {
|
|
.X => self.float_x != null,
|
|
.Y => self.float_y != null,
|
|
};
|
|
}
|
|
|
|
fn overflowEnabled(self: *const Box, axis: Axis) bool {
|
|
return switch (axis) {
|
|
.X => self.flags.contains(.overflow_x),
|
|
.Y => self.flags.contains(.overflow_y),
|
|
};
|
|
}
|
|
|
|
fn availableChildrenSize(self: *Box, axis: Axis) f32 {
|
|
const size = vec2ByAxis(&self.persistent.size, axis).*;
|
|
return @max(size - self.padding.byAxis(axis), 0);
|
|
}
|
|
|
|
pub fn appendChild(self: *Box, child: *Box) void {
|
|
child.tree.parent_index = self.tree.index;
|
|
|
|
if (self.tree.last_child_index) |last_child_index| {
|
|
const last_child = self.ui.getBoxByIndex(last_child_index);
|
|
|
|
last_child.tree.next_sibling_index = child.tree.index;
|
|
self.tree.last_child_index = child.tree.index;
|
|
self.tree.prev_sibling_index = last_child.tree.index;
|
|
} else {
|
|
self.tree.first_child_index = child.tree.index;
|
|
self.tree.last_child_index = child.tree.index;
|
|
}
|
|
}
|
|
|
|
pub fn removeChild(self: *Box, child: *Box) void {
|
|
assert(child.tree.parent_index == self.tree.index);
|
|
|
|
child.tree.parent_index = null;
|
|
defer child.tree.next_sibling_index = null;
|
|
defer child.tree.prev_sibling_index = null;
|
|
|
|
var next_sibling: ?*Box = null;
|
|
if (child.tree.next_sibling_index) |next_sibling_index| {
|
|
next_sibling = self.ui.getBoxByIndex(next_sibling_index);
|
|
}
|
|
|
|
var prev_sibling: ?*Box = null;
|
|
if (child.tree.prev_sibling_index) |prev_sibling_index| {
|
|
prev_sibling = self.ui.getBoxByIndex(prev_sibling_index);
|
|
}
|
|
|
|
if (next_sibling != null and prev_sibling != null) {
|
|
next_sibling.?.tree.prev_sibling_index = prev_sibling.?.tree.index;
|
|
prev_sibling.?.tree.next_sibling_index = next_sibling.?.tree.index;
|
|
} else if (next_sibling != null) {
|
|
next_sibling.?.tree.prev_sibling_index = null;
|
|
self.tree.first_child_index = next_sibling.?.tree.index;
|
|
} else if (prev_sibling != null) {
|
|
prev_sibling.?.tree.next_sibling_index = null;
|
|
self.tree.last_child_index = prev_sibling.?.tree.index;
|
|
}
|
|
}
|
|
|
|
fn hasChildren(self: *const Box) bool {
|
|
return self.tree.first_child_index != null;
|
|
}
|
|
|
|
pub fn bringChildToTop(self: *Box, child: *Box) void {
|
|
self.removeChild(child);
|
|
self.appendChild(child);
|
|
}
|
|
};
|
|
|
|
pub const BoxOptions = struct {
|
|
key: ?Key = null,
|
|
size_x: ?Sizing = null,
|
|
size_y: ?Sizing = null,
|
|
background: ?rl.Color = null,
|
|
layout_direction: ?LayoutDirection = null,
|
|
layout_gap: ?f32 = null,
|
|
padding: ?Padding = null,
|
|
align_x: ?Alignment = null,
|
|
align_y: ?Alignment = null,
|
|
text: ?[]const u8 = null,
|
|
font: ?Assets.FontId = null,
|
|
text_color: ?rl.Color = null,
|
|
flags: ?[]const Box.Flag = null,
|
|
position_x: ?f32 = null,
|
|
position_y: ?f32 = null,
|
|
borders: ?Borders = null,
|
|
hot_cursor: ?rl.MouseCursor = null,
|
|
active_cursor: ?rl.MouseCursor = null,
|
|
view_offset: ?Vec2 = null,
|
|
texture: ?rl.Texture2D = null,
|
|
texture_size: ?Vec2 = null,
|
|
float_rect: ?Rect = null,
|
|
scientific_number: ?f64 = null,
|
|
scientific_precision: ?u32 = null,
|
|
float_relative_to: ?*Box = null,
|
|
parent: ?*UI.Box = null,
|
|
texture_color: ?rl.Color = null,
|
|
draw: ?Box.Draw = null,
|
|
visual_hot: ?bool = null,
|
|
visual_active: ?bool = null
|
|
};
|
|
|
|
pub const root_box_key = Key.initString(0, "$root$");
|
|
pub const mouse_tooltip_box_key = Key.initString(0, "$mouse_tooltip$");
|
|
|
|
const BoxChildIterator = struct {
|
|
current_child: ?BoxIndex,
|
|
boxes: []Box,
|
|
|
|
pub fn next(self: *BoxChildIterator) ?*Box {
|
|
if (self.current_child) |child_index| {
|
|
const child = &self.boxes[child_index];
|
|
self.current_child = child.tree.next_sibling_index;
|
|
return child;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
const BoxParentIterator = struct {
|
|
current_parent: ?BoxIndex,
|
|
boxes: []Box,
|
|
|
|
pub fn next(self: *BoxParentIterator) ?*Box {
|
|
if (self.current_parent) |parent_index| {
|
|
const parent = &self.boxes[parent_index];
|
|
self.current_parent = parent.tree.parent_index;
|
|
return parent;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
const CharPressIterator = struct {
|
|
events: *Events,
|
|
index: usize = 0,
|
|
|
|
pub fn next(self: *CharPressIterator) ?u32 {
|
|
while (self.index < self.events.len) {
|
|
const event = self.events.get(self.index);
|
|
self.index += 1;
|
|
|
|
if (event == .char_pressed) {
|
|
return event.char_pressed;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn consume(self: *CharPressIterator) void {
|
|
self.index -= 1;
|
|
_ = self.events.swapRemove(self.index);
|
|
}
|
|
};
|
|
|
|
const Events = std.BoundedArray(Event, max_events);
|
|
|
|
arenas: [2]std.heap.ArenaAllocator,
|
|
|
|
// Retained structures. Used for tracking changes between frames
|
|
hot_box_key: ?Key = null,
|
|
active_box_keys: std.EnumMap(rl.MouseButton, Key) = .{},
|
|
frame_index: u64 = 0,
|
|
|
|
// Per frame fields
|
|
scissor_stack: std.BoundedArray(Rect, 16) = .{},
|
|
mouse: Vec2 = .{ .x = 0, .y = 0 },
|
|
mouse_delta: Vec2 = .{ .x = 0, .y = 0 },
|
|
mouse_buttons: std.EnumSet(rl.MouseButton) = .{},
|
|
events: Events = .{},
|
|
dt: f32 = 0,
|
|
|
|
// Per layout pass fields
|
|
font_stack: std.BoundedArray(Assets.FontId, 16) = .{},
|
|
boxes: std.BoundedArray(Box, max_boxes) = .{},
|
|
parent_stack: std.BoundedArray(BoxIndex, max_boxes) = .{},
|
|
|
|
pub fn init(allocator: std.mem.Allocator) UI {
|
|
return UI{
|
|
.arenas = .{
|
|
std.heap.ArenaAllocator.init(allocator),
|
|
std.heap.ArenaAllocator.init(allocator)
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *UI) void {
|
|
self.arenas[0].deinit();
|
|
self.arenas[1].deinit();
|
|
}
|
|
|
|
pub fn frameArena(self: *UI) *std.heap.ArenaAllocator {
|
|
return &self.arenas[@mod(self.frame_index, 2)];
|
|
}
|
|
|
|
pub fn frameAllocator(self: *UI) std.mem.Allocator {
|
|
return self.frameArena().allocator();
|
|
}
|
|
|
|
pub fn pullOsEvents(self: *UI) void {
|
|
self.events.len = 0;
|
|
|
|
const mouse = rl.getMousePosition();
|
|
self.mouse_delta = mouse.subtract(self.mouse);
|
|
self.dt = rl.getFrameTime();
|
|
self.mouse = mouse;
|
|
self.mouse_buttons = .{};
|
|
inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| {
|
|
const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value);
|
|
|
|
if (rl.isMouseButtonPressed(mouse_button)) {
|
|
self.events.appendAssumeCapacity(Event{ .mouse_pressed = mouse_button });
|
|
}
|
|
if (rl.isMouseButtonReleased(mouse_button)) {
|
|
self.events.appendAssumeCapacity(Event{ .mouse_released = mouse_button });
|
|
}
|
|
if (rl.isMouseButtonDown(mouse_button)) {
|
|
self.mouse_buttons.insert(mouse_button);
|
|
}
|
|
}
|
|
|
|
for (0..512) |key| {
|
|
if (raylib_h.IsKeyPressed(@intCast(key))) {
|
|
self.events.appendAssumeCapacity(Event{
|
|
.key_pressed = @intCast(key)
|
|
});
|
|
}
|
|
|
|
if (raylib_h.IsKeyReleased(@intCast(key))) {
|
|
self.events.appendAssumeCapacity(Event{
|
|
.key_released = @intCast(key)
|
|
});
|
|
}
|
|
}
|
|
|
|
while (true) {
|
|
const char = rl.getCharPressed();
|
|
if (char == 0) {
|
|
break;
|
|
}
|
|
|
|
self.events.appendAssumeCapacity(Event{
|
|
.char_pressed = @intCast(char)
|
|
});
|
|
}
|
|
|
|
const mouse_wheel = rl.getMouseWheelMoveV();
|
|
if (mouse_wheel.x != 0 or mouse_wheel.y != 0) {
|
|
self.events.appendAssumeCapacity(Event{ .mouse_scroll = mouse_wheel });
|
|
}
|
|
}
|
|
|
|
pub fn begin(self: *UI) void {
|
|
self.font_stack.len = 0;
|
|
self.parent_stack.len = 0;
|
|
|
|
// Remove boxes which haven't been used in the last frame
|
|
{
|
|
var i: usize = 0;
|
|
while (i < self.boxes.len) {
|
|
const box = &self.boxes.buffer[i];
|
|
if (box.last_used_frame != self.frame_index) {
|
|
if (self.boxes.len > 0) {
|
|
self.boxes.buffer[i] = self.boxes.buffer[self.boxes.len - 1];
|
|
}
|
|
self.boxes.len -= 1;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!self.isKeyActiveAny()) {
|
|
self.hot_box_key = null;
|
|
}
|
|
|
|
self.frame_index += 1;
|
|
_ = self.frameArena().reset(.retain_capacity);
|
|
|
|
self.pushFont(default_font);
|
|
|
|
_ = self.createBox(.{
|
|
.key = mouse_tooltip_box_key,
|
|
.size_x = Sizing.initFitChildren(),
|
|
.size_y = Sizing.initFitChildren(),
|
|
.padding = Padding.all(8),
|
|
.background = srcery.black,
|
|
.borders = Borders.all(.{
|
|
.color = srcery.hard_black,
|
|
.size = 4
|
|
})
|
|
});
|
|
|
|
const root_box = self.createBox(.{
|
|
.key = root_box_key,
|
|
.size_x = .{ .fixed = .{ .pixels = @floatFromInt(rl.getScreenWidth()) } },
|
|
.size_y = .{ .fixed = .{ .pixels = @floatFromInt(rl.getScreenHeight()) } },
|
|
});
|
|
root_box.beginChildren();
|
|
}
|
|
|
|
pub fn end(self: *UI) void {
|
|
const mouse_tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
|
|
|
// Add mouse tooltip to hot item
|
|
if (!mouse_tooltip.hasChildren() and self.hot_box_key != null) {
|
|
const box = self.getBoxByKey(self.hot_box_key.?).?;
|
|
if (box.tooltip) |tooltip| {
|
|
mouse_tooltip.beginChildren();
|
|
defer mouse_tooltip.endChildren();
|
|
|
|
_ = self.createBox(.{
|
|
.size_x = Sizing.initFixed(.text),
|
|
.size_y = Sizing.initFixed(.text),
|
|
.text = tooltip
|
|
});
|
|
}
|
|
}
|
|
|
|
const root_box = self.parentBox().?;
|
|
root_box.endChildren();
|
|
|
|
self.popFont();
|
|
|
|
// Update Animations
|
|
{
|
|
const fast_rate = 1 - std.math.pow(f32, 2, (-50 * self.dt));
|
|
|
|
for (self.boxes.slice()) |*_box| {
|
|
const box: *Box = _box;
|
|
if (box.key.isNil()) continue;
|
|
|
|
const is_hot = self.isKeyHot(box.key) or box.visual_hot;
|
|
const is_active = self.isKeyActive(box.key) or box.visual_active;
|
|
|
|
const is_hot_f32: f32 = @floatFromInt(@intFromBool(is_hot));
|
|
const is_active_f32: f32 = @floatFromInt(@intFromBool(is_active));
|
|
|
|
box.persistent.hot += fast_rate * (is_hot_f32 - box.persistent.hot );
|
|
box.persistent.active += fast_rate * (is_active_f32 - box.persistent.active);
|
|
}
|
|
}
|
|
|
|
// Mouse cursor
|
|
{
|
|
var cursor: ?rl.MouseCursor = null;
|
|
|
|
var active_iter = self.active_box_keys.iterator();
|
|
while (active_iter.next()) |active_box_key| {
|
|
if (self.getBoxByKey(active_box_key.value.*)) |active_box| {
|
|
cursor = active_box.active_cursor;
|
|
|
|
if (cursor != null) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cursor == null) {
|
|
if (self.hot_box_key) |hot_box_key| {
|
|
if (self.getBoxByKey(hot_box_key)) |hot_box| {
|
|
cursor = hot_box.hot_cursor;
|
|
}
|
|
}
|
|
}
|
|
|
|
rl.setMouseCursor(@intFromEnum(cursor orelse rl.MouseCursor.mouse_cursor_default));
|
|
}
|
|
|
|
// Reset sizes and positions, because it will be recalculated in layout pass
|
|
for (self.boxes.slice()) |*box| {
|
|
var position = Vec2{ .x = 0, .y = 0 };
|
|
if (box.float_x) |x| {
|
|
position.x = x;
|
|
}
|
|
if (box.float_y) |y| {
|
|
position.y = y;
|
|
}
|
|
box.persistent.size = Vec2Zero;
|
|
box.persistent.position = position;
|
|
}
|
|
|
|
self.layoutPass(root_box);
|
|
|
|
self.layoutSizesPass(mouse_tooltip);
|
|
// Position mouse tooltip so it does not go off screen
|
|
{
|
|
const window_rect = rect_utils.shrink(Rect{
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = @floatFromInt(rl.getScreenWidth()),
|
|
.height = @floatFromInt(rl.getScreenHeight())
|
|
}, 16);
|
|
|
|
const cursor_width = 12;
|
|
var tooltip_rect = Rect{
|
|
.x = self.mouse.x + cursor_width,
|
|
.y = self.mouse.y,
|
|
.width = mouse_tooltip.persistent.size.x,
|
|
.height = mouse_tooltip.persistent.size.y
|
|
};
|
|
|
|
tooltip_rect.x += @max(0, rect_utils.left(window_rect) - rect_utils.left(tooltip_rect));
|
|
tooltip_rect.x -= @max(0, rect_utils.right(tooltip_rect) - rect_utils.right(window_rect));
|
|
|
|
tooltip_rect.y += @max(0, rect_utils.top(window_rect) - rect_utils.top(tooltip_rect));
|
|
tooltip_rect.y -= @max(0, rect_utils.bottom(tooltip_rect) - rect_utils.bottom(window_rect));
|
|
|
|
mouse_tooltip.persistent.position = .{
|
|
.x = tooltip_rect.x,
|
|
.y = tooltip_rect.y
|
|
};
|
|
}
|
|
self.layoutPositionsPass(mouse_tooltip);
|
|
}
|
|
|
|
fn layoutSizesPass(self: *UI, root_box: *Box) void {
|
|
// TODO: Figure out a way for fit children to work without calling it twice.
|
|
|
|
self.layoutSizesInitial(root_box, .X);
|
|
self.layoutSizesShrink(root_box, .X);
|
|
self.layoutSizesFitChildren(root_box, .X);
|
|
self.layoutSizesGrow(root_box, .X);
|
|
self.layoutSizesFitChildren(root_box, .X);
|
|
|
|
self.layoutWrapText(root_box);
|
|
|
|
self.layoutSizesInitial(root_box, .Y);
|
|
self.layoutSizesShrink(root_box, .Y);
|
|
self.layoutSizesFitChildren(root_box, .Y);
|
|
self.layoutSizesGrow(root_box, .Y);
|
|
self.layoutSizesFitChildren(root_box, .Y);
|
|
}
|
|
|
|
fn layoutPositionsPass(self: *UI, root_box: *Box) void {
|
|
self.layoutPositions(root_box, .X);
|
|
self.layoutPositions(root_box, .Y);
|
|
self.layoutFloatingPositions(root_box, .X);
|
|
self.layoutFloatingPositions(root_box, .Y);
|
|
}
|
|
|
|
fn layoutPass(self: *UI, root_box: *Box) void {
|
|
self.layoutSizesPass(root_box);
|
|
self.layoutPositionsPass(root_box);
|
|
}
|
|
|
|
pub fn pushFont(self: *UI, font_id: Assets.FontId) void {
|
|
self.font_stack.appendAssumeCapacity(font_id);
|
|
}
|
|
|
|
pub fn popFont(self: *UI) void {
|
|
_ = self.font_stack.pop();
|
|
}
|
|
|
|
pub fn currentDefaultFont(self: *UI) Assets.FontId {
|
|
assert(self.font_stack.len > 0);
|
|
return self.font_stack.buffer[self.font_stack.len - 1];
|
|
}
|
|
|
|
pub fn rem(self: *UI, count: f32) f32 {
|
|
const font_id = self.currentDefaultFont();
|
|
return font_id.size * count;
|
|
}
|
|
|
|
inline fn vec2ByAxis(vec2: *Vec2, axis: Axis) *f32 {
|
|
return switch (axis) {
|
|
.X => &vec2.x,
|
|
.Y => &vec2.y
|
|
};
|
|
}
|
|
|
|
fn layoutWrapText(self: *UI, box: *Box) void {
|
|
if (box.flags.contains(.wrap_text)) {
|
|
box.wrapText(box.availableChildrenSize(.X));
|
|
}
|
|
|
|
{
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutWrapText(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn layoutSizesInitial(self: *UI, box: *Box, axis: Axis) void {
|
|
const axis_persistent_size = vec2ByAxis(&box.persistent.size, axis);
|
|
const sizing = box.size.getAxis(axis);
|
|
|
|
if (sizing == .fixed) {
|
|
axis_persistent_size.* = sizing.fixed.get(box, axis);
|
|
} else if (sizing == .shrink) {
|
|
axis_persistent_size.* = sizing.shrink.max.get(box, axis);
|
|
} else if (sizing == .grow) {
|
|
axis_persistent_size.* = sizing.grow.min.get(box, axis);
|
|
}
|
|
|
|
axis_persistent_size.* += box.padding.byAxis(axis);
|
|
|
|
{
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutSizesInitial(child, axis);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sumChildrenSize(box: *Box, axis: Axis) f32 {
|
|
var axis_size: f32 = 0;
|
|
|
|
if (box.layout_direction.isAxis(axis)) {
|
|
var children_count: f32 = 0;
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const axis_child_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
axis_size += axis_child_size.*;
|
|
|
|
children_count += 1;
|
|
}
|
|
|
|
if (children_count > 0) {
|
|
axis_size += box.layout_gap * (children_count - 1);
|
|
}
|
|
} else {
|
|
var max_child_size: f32 = 0;
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const axis_child_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
max_child_size = @max(max_child_size, axis_child_size.*);
|
|
}
|
|
|
|
axis_size += max_child_size;
|
|
}
|
|
|
|
return axis_size;
|
|
}
|
|
|
|
fn layoutSizesShrink(self: *UI, box: *Box, axis: Axis) void {
|
|
{
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutSizesShrink(child, axis);
|
|
}
|
|
}
|
|
|
|
if (box.layout_direction.isAxis(axis)) {
|
|
const available_children_size = box.availableChildrenSize(axis);
|
|
const used_axis_size = sumChildrenSize(box, axis);
|
|
|
|
var overflow_size = used_axis_size - available_children_size;
|
|
if (overflow_size > 0) {
|
|
const ShrinkableChild = struct {
|
|
box: *Box,
|
|
min_size: f32
|
|
};
|
|
var shrinkable_children: std.BoundedArray(ShrinkableChild, max_boxes) = .{};
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const child_axis_sizing = child.size.getAxis(axis);
|
|
if (child_axis_sizing == .shrink) {
|
|
shrinkable_children.appendAssumeCapacity(.{
|
|
.box = child,
|
|
.min_size = child_axis_sizing.shrink.min.get(child, axis)
|
|
});
|
|
}
|
|
}
|
|
|
|
while (overflow_size > 0) {
|
|
|
|
// Remove children that have reached minimum size
|
|
{
|
|
var i: usize = 0;
|
|
while (i < shrinkable_children.len) {
|
|
const shrinkable_child = shrinkable_children.get(i);
|
|
const child = shrinkable_child.box;
|
|
const child_min_size = shrinkable_child.min_size;
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
if (child_persistent_size.* <= child_min_size) {
|
|
_ = shrinkable_children.swapRemove(i);
|
|
continue;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shrinkable_children.len == 0) {
|
|
break;
|
|
}
|
|
|
|
var largest_child = shrinkable_children.buffer[0];
|
|
var largest_size: f32 = vec2ByAxis(&largest_child.box.persistent.size, axis).*;
|
|
var second_largest_size: ?f32 = null;
|
|
|
|
var largest_children: std.BoundedArray(*Box, max_boxes) = .{};
|
|
largest_children.appendAssumeCapacity(largest_child.box);
|
|
|
|
for (shrinkable_children.slice()[1..]) |shrinkable_child| {
|
|
const child = shrinkable_child.box;
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
if (child_persistent_size.* > largest_size) {
|
|
second_largest_size = largest_size;
|
|
largest_size = child_persistent_size.*;
|
|
largest_child = shrinkable_child;
|
|
largest_children.len = 0;
|
|
largest_children.appendAssumeCapacity(child);
|
|
} else if (child_persistent_size.* < largest_size) {
|
|
second_largest_size = @max(second_largest_size orelse 0, child_persistent_size.*);
|
|
} else {
|
|
largest_children.appendAssumeCapacity(child);
|
|
}
|
|
}
|
|
|
|
var shrink_largest_by = @max(overflow_size, largest_child.min_size);
|
|
if (second_largest_size != null) {
|
|
shrink_largest_by = @min(shrink_largest_by, largest_size - second_largest_size.?);
|
|
}
|
|
|
|
const largest_count_f32: f32 = @floatFromInt(largest_children.len);
|
|
shrink_largest_by = @min(shrink_largest_by * largest_count_f32, largest_size) / largest_count_f32;
|
|
|
|
for (largest_children.slice()) |child| {
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
child_persistent_size.* -= shrink_largest_by;
|
|
overflow_size -= shrink_largest_by;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn layoutSizesFitChildren(self: *UI, box: *Box, axis: Axis) void {
|
|
{
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutSizesFitChildren(child, axis);
|
|
}
|
|
}
|
|
|
|
if (box.size.getAxis(axis) == .fit_children) {
|
|
const children_size = sumChildrenSize(box, axis);
|
|
const persistent_size = vec2ByAxis(&box.persistent.size, axis);
|
|
persistent_size.* = children_size + box.padding.byAxis(axis);
|
|
}
|
|
}
|
|
|
|
fn layoutSizesGrow(self: *UI, box: *Box, axis: Axis) void {
|
|
const available_children_size = box.availableChildrenSize(axis);
|
|
|
|
// Hack to make .{ .fixed = { .parent_percent = 1} } work when parent is .grow
|
|
// I can't be bother to find a nice solutino for this.
|
|
// I need this for a scrollbar.
|
|
const sizing = box.size.getAxis(axis);
|
|
if (sizing.dependsOnParent() and sizing == .fixed) {
|
|
const persistent_size = vec2ByAxis(&box.persistent.size, axis);
|
|
persistent_size.* = sizing.fixed.get(box, axis);
|
|
}
|
|
|
|
if (box.layout_direction.isAxis(axis)) {
|
|
const used_axis_size = sumChildrenSize(box, axis);
|
|
var unused_size = available_children_size - used_axis_size;
|
|
|
|
if (unused_size > 0) {
|
|
const GrowableChild = struct {
|
|
box: *Box,
|
|
max_size: f32
|
|
};
|
|
var growable_children: std.BoundedArray(GrowableChild, max_boxes) = .{};
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const child_axis_sizing = child.size.getAxis(axis);
|
|
if (child_axis_sizing == .grow) {
|
|
growable_children.appendAssumeCapacity(.{
|
|
.box = child,
|
|
.max_size = child_axis_sizing.grow.max.get(child, axis)
|
|
});
|
|
}
|
|
}
|
|
|
|
while (unused_size > 0) {
|
|
|
|
// Remove children that have reached maximum size
|
|
{
|
|
var i: usize = 0;
|
|
while (i < growable_children.len) {
|
|
const growable_child = growable_children.get(i);
|
|
const child = growable_child.box;
|
|
const child_max_size = growable_child.max_size;
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
if (child_persistent_size.* >= child_max_size) {
|
|
_ = growable_children.swapRemove(i);
|
|
continue;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (growable_children.len == 0) {
|
|
break;
|
|
}
|
|
|
|
var smallest_child = growable_children.buffer[0];
|
|
var smallest_size: f32 = vec2ByAxis(&smallest_child.box.persistent.size, axis).*;
|
|
var second_smallest_size: ?f32 = null;
|
|
|
|
var smallest_children: std.BoundedArray(*Box, max_boxes) = .{};
|
|
smallest_children.appendAssumeCapacity(smallest_child.box);
|
|
|
|
for (growable_children.slice()[1..]) |growable_child| {
|
|
const child = growable_child.box;
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
if (child_persistent_size.* < smallest_size) {
|
|
second_smallest_size = smallest_size;
|
|
smallest_size = child_persistent_size.*;
|
|
smallest_child = growable_child;
|
|
smallest_children.len = 0;
|
|
smallest_children.appendAssumeCapacity(child);
|
|
} else if (child_persistent_size.* > smallest_size) {
|
|
second_smallest_size = @min(second_smallest_size orelse std.math.inf(f32), child_persistent_size.*);
|
|
} else {
|
|
smallest_children.appendAssumeCapacity(child);
|
|
}
|
|
}
|
|
|
|
var grow_smallest_by = @min(unused_size, smallest_child.max_size);
|
|
if (second_smallest_size != null) {
|
|
grow_smallest_by = @min(grow_smallest_by, second_smallest_size.? - smallest_size);
|
|
}
|
|
|
|
const smallest_count_f32: f32 = @floatFromInt(smallest_children.len);
|
|
grow_smallest_by = @min(grow_smallest_by * smallest_count_f32, unused_size) / smallest_count_f32;
|
|
|
|
for (smallest_children.slice()) |child| {
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
child_persistent_size.* += grow_smallest_by;
|
|
unused_size -= grow_smallest_by;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const child_axis_sizing = child.size.getAxis(axis);
|
|
if (child_axis_sizing == .grow) {
|
|
const child_max_size = child_axis_sizing.grow.max.get(child, axis);
|
|
const child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
child_persistent_size.* = @min(available_children_size, child_max_size);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutSizesGrow(child, axis);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn layoutPositions(self: *UI, box: *Box, axis: Axis) void {
|
|
const is_layout_axis = box.layout_direction.isAxis(axis);
|
|
|
|
{
|
|
const available_children_size = box.availableChildrenSize(axis);
|
|
const children_size = sumChildrenSize(box, axis);
|
|
|
|
const axis_alignment = box.alignment.getAxis(axis);
|
|
const alignment_offset_scalar = axis_alignment.getCoefficient();
|
|
|
|
var offset: f32 = 0;
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
if (child.isFloating(axis)) continue;
|
|
|
|
const axis_position = vec2ByAxis(&box.persistent.position, axis);
|
|
const axis_child_position = vec2ByAxis(&child.persistent.position, axis);
|
|
const axis_child_persistent_size = vec2ByAxis(&child.persistent.size, axis);
|
|
|
|
axis_child_position.* = axis_position.*;
|
|
|
|
if (axis == .X) {
|
|
axis_child_position.* += box.padding.left;
|
|
} else if (axis == .Y) {
|
|
axis_child_position.* += box.padding.top;
|
|
}
|
|
|
|
if (is_layout_axis) {
|
|
axis_child_position.* += offset;
|
|
offset += axis_child_persistent_size.*;
|
|
offset += box.layout_gap;
|
|
}
|
|
|
|
axis_child_position.* += @max(available_children_size - children_size, 0) * alignment_offset_scalar;
|
|
|
|
axis_child_position.* -= vec2ByAxis(&box.view_offset, axis).*;
|
|
}
|
|
}
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutPositions(child, axis);
|
|
}
|
|
}
|
|
|
|
fn layoutFloatingPositions(self: *UI, box: *Box, axis: Axis) void {
|
|
if (box.float_relative_to != null and box.isFloating(axis)) {
|
|
const target = box.float_relative_to.?;
|
|
|
|
const axis_position = vec2ByAxis(&box.persistent.position, axis);
|
|
const axis_position_target = vec2ByAxis(&target.persistent.position, axis);
|
|
|
|
axis_position.* += axis_position_target.*;
|
|
}
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.layoutFloatingPositions(child, axis);
|
|
}
|
|
}
|
|
|
|
pub fn createBox(self: *UI, opts: BoxOptions) *Box {
|
|
var box: *Box = undefined;
|
|
var box_index: ?BoxIndex = null;
|
|
var key = opts.key orelse Key.initNil();
|
|
var persistent = Box.Persistent{};
|
|
var created = false;
|
|
|
|
if (!key.isNil()) {
|
|
if (self.getBoxIndexByKey(key)) |last_frame_box_index| {
|
|
const last_frame_box = self.getBoxByIndex(last_frame_box_index);
|
|
|
|
box = last_frame_box;
|
|
persistent = last_frame_box.persistent;
|
|
box_index = last_frame_box_index;
|
|
|
|
// Safety guard for using a key multiple times in UI
|
|
assert(box.last_used_frame != self.frame_index);
|
|
}
|
|
}
|
|
|
|
if (box_index == null) {
|
|
box = self.boxes.addOneAssumeCapacity();
|
|
box_index = self.boxes.len - 1;
|
|
created = true;
|
|
}
|
|
|
|
var size = Sizing2{
|
|
.x = Sizing.initFixed(.{ .pixels = 0 }),
|
|
.y = Sizing.initFixed(.{ .pixels = 0 }),
|
|
};
|
|
if (opts.size_x) |size_x| {
|
|
size.x = size_x;
|
|
} else if (opts.text != null) {
|
|
size.x = Sizing.initFixed(.text);
|
|
}
|
|
|
|
if (opts.size_y) |size_y| {
|
|
size.y = size_y;
|
|
} else if (opts.text != null) {
|
|
size.y = Sizing.initFixed(.text);
|
|
}
|
|
|
|
var flags = Box.Flags.initEmpty();
|
|
if (opts.flags) |opts_flags| {
|
|
for (opts_flags) |flag| {
|
|
flags.insert(flag);
|
|
}
|
|
}
|
|
|
|
const default_alignment = if (opts.text != null) Alignment.center else Alignment.start;
|
|
const alignment = Alignment2{
|
|
.x = opts.align_x orelse default_alignment,
|
|
.y = opts.align_y orelse default_alignment,
|
|
};
|
|
|
|
box.* = Box{
|
|
.ui = self,
|
|
.allocator = self.frameAllocator(),
|
|
.created = created,
|
|
|
|
.persistent = persistent,
|
|
.flags = flags,
|
|
.background = opts.background,
|
|
.size = size,
|
|
.layout_direction = opts.layout_direction orelse LayoutDirection.left_to_right,
|
|
.layout_gap = opts.layout_gap orelse 0,
|
|
.padding = opts.padding orelse Padding{},
|
|
.alignment = alignment,
|
|
.font = opts.font orelse self.currentDefaultFont(),
|
|
.text = null,
|
|
.text_color = opts.text_color orelse srcery.bright_white,
|
|
.borders = opts.borders orelse Borders{},
|
|
.hot_cursor = opts.hot_cursor,
|
|
.active_cursor = opts.active_cursor,
|
|
.view_offset = opts.view_offset orelse Vec2Zero,
|
|
.texture = opts.texture,
|
|
.texture_size = opts.texture_size,
|
|
.scientific_number = opts.scientific_number,
|
|
.scientific_precision = opts.scientific_precision orelse 1,
|
|
.float_relative_to = opts.float_relative_to,
|
|
.texture_color = opts.texture_color,
|
|
.draw = opts.draw,
|
|
.visual_hot = opts.visual_hot orelse false,
|
|
.visual_active = opts.visual_active orelse false,
|
|
|
|
.last_used_frame = self.frame_index,
|
|
.key = key,
|
|
.tree = .{
|
|
.index = box_index.?
|
|
},
|
|
};
|
|
|
|
if (box.isFloating(.X)) {
|
|
box.persistent.position.x = 0;
|
|
}
|
|
|
|
if (box.isFloating(.Y)) {
|
|
box.persistent.position.y = 0;
|
|
}
|
|
|
|
if (opts.text) |text| {
|
|
box.setText(text);
|
|
}
|
|
|
|
if (opts.position_x) |x| {
|
|
box.setFloatX(x);
|
|
}
|
|
|
|
if (opts.position_y) |y| {
|
|
box.setFloatY(y);
|
|
}
|
|
|
|
if (opts.float_rect) |rect| {
|
|
box.setFloatRect(rect);
|
|
}
|
|
|
|
if (opts.parent orelse self.parentBox()) |parent| {
|
|
parent.appendChild(box);
|
|
}
|
|
|
|
return box;
|
|
}
|
|
|
|
pub fn parentBoxIndex(self: *UI) ?BoxIndex {
|
|
if (self.parent_stack.len > 0) {
|
|
return self.parent_stack.buffer[self.parent_stack.len - 1];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn parentBox(self: *UI) ?*Box {
|
|
if (self.parentBoxIndex()) |box_index| {
|
|
return self.getBoxByIndex(box_index);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn getBoxByIndex(self: *UI, box_index: BoxIndex) *Box {
|
|
return &self.boxes.buffer[box_index];
|
|
}
|
|
|
|
pub fn getBoxIndexByKey(self: *UI, key: Key) ?BoxIndex {
|
|
for (0.., self.boxes.slice()) |index, box| {
|
|
if (box.key.eql(key)) {
|
|
return @intCast(index);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn getBoxByKey(self: *UI, key: Key) ?*Box {
|
|
if (self.getBoxIndexByKey(key)) |box_index| {
|
|
return self.getBoxByIndex(box_index);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn draw(self: *UI) void {
|
|
defer assert(self.scissor_stack.len == 0);
|
|
|
|
const root_box = self.getBoxByKey(root_box_key).?;
|
|
const mouse_tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
|
|
|
self.drawBox(root_box);
|
|
|
|
if (mouse_tooltip.hasChildren()) {
|
|
self.drawBox(mouse_tooltip);
|
|
}
|
|
}
|
|
|
|
fn drawBox(self: *UI, box: *Box) void {
|
|
const box_rect = box.rect();
|
|
|
|
const do_scissor = box.flags.contains(.clip_view);
|
|
if (do_scissor) self.beginScissor(box_rect);
|
|
defer if (do_scissor) self.endScissor();
|
|
|
|
var value_shift: f32 = 0;
|
|
if (box.flags.contains(.draw_active) and box.persistent.active > 0.01) {
|
|
value_shift = -0.5 * box.persistent.active;
|
|
} else if (box.flags.contains(.draw_hot) and box.persistent.hot > 0.01) {
|
|
value_shift = 0.6 * box.persistent.hot;
|
|
}
|
|
|
|
if (box.background) |bg| {
|
|
rl.drawRectangleRec(box_rect, utils.shiftColorInHSV(bg, value_shift));
|
|
}
|
|
|
|
if (box.texture) |texture| {
|
|
const source = rl.Rectangle{
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = @floatFromInt(texture.width),
|
|
.height = @floatFromInt(texture.height)
|
|
};
|
|
var destination = box_rect;
|
|
if (box.texture_size) |texture_size| {
|
|
destination = rect_utils.initCentered(destination, texture_size.x, texture_size.y);
|
|
}
|
|
|
|
rl.drawTexturePro(
|
|
texture,
|
|
source,
|
|
destination,
|
|
rl.Vector2.zero(),
|
|
0,
|
|
box.texture_color orelse rl.Color.white
|
|
);
|
|
}
|
|
|
|
const borders_with_coords = .{
|
|
.{ box.borders.left , rl.Vector2.init(0, 0), rl.Vector2.init(0, 1), rl.Vector2.init( 1, 0) },
|
|
.{ box.borders.right , rl.Vector2.init(1, 0), rl.Vector2.init(1, 1), rl.Vector2.init(-1, 0) },
|
|
.{ box.borders.top , rl.Vector2.init(0, 0), rl.Vector2.init(1, 0), rl.Vector2.init( 0, 1) },
|
|
.{ box.borders.bottom, rl.Vector2.init(0, 1), rl.Vector2.init(1, 1), rl.Vector2.init( 0, -1) }
|
|
};
|
|
inline for (borders_with_coords) |border_with_coords| {
|
|
const border = border_with_coords[0];
|
|
const line_from = border_with_coords[1];
|
|
const line_to = border_with_coords[2];
|
|
const inset_direction: rl.Vector2 = border_with_coords[3];
|
|
|
|
if (border.size > 0) {
|
|
const inset = inset_direction.multiply(Vec2.init(border.size/2, border.size/2));
|
|
rl.drawLineEx(
|
|
rect_utils.positionAt(box_rect, line_from).add(inset),
|
|
rect_utils.positionAt(box_rect, line_to).add(inset),
|
|
border.size,
|
|
utils.shiftColorInHSV(border.color, value_shift)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (box.draw) |box_draw| {
|
|
box_draw.do(box_draw.ctx, box);
|
|
}
|
|
|
|
const alignment_x_coeff = box.alignment.x.getCoefficient();
|
|
const alignment_y_coeff = box.alignment.y.getCoefficient();
|
|
|
|
if (box.text) |text| {
|
|
const font_face = Assets.font(box.font);
|
|
var text_position = box.persistent.position;
|
|
text_position.x += box.padding.left;
|
|
text_position.y += box.padding.top;
|
|
|
|
const lines: [][]u8 = box.text_lines.slice();
|
|
|
|
const available_width = box.availableChildrenSize(.X);
|
|
const available_height = box.availableChildrenSize(.Y);
|
|
|
|
if (lines.len == 0) {
|
|
const text_size = font_face.measureText(text);
|
|
text_position.x += (available_width - text_size.x) * alignment_x_coeff;
|
|
text_position.y += (available_height - text_size.y) * alignment_y_coeff;
|
|
|
|
font_face.drawText(text, text_position, box.text_color);
|
|
} else {
|
|
// TODO: Don't call `measureTextLines`,
|
|
// Because in the end `measureText` will be called twice for each line
|
|
const text_size = font_face.measureTextLines(lines);
|
|
text_position.x += (available_width - text_size.x) * alignment_x_coeff;
|
|
text_position.y += (available_height - text_size.y) * alignment_y_coeff;
|
|
|
|
var offset_y: f32 = 0;
|
|
|
|
for (lines) |line| {
|
|
const line_size = font_face.measureText(line);
|
|
const offset_x = (text_size.x - line_size.x) * alignment_x_coeff;
|
|
|
|
font_face.drawText(
|
|
line,
|
|
text_position.add(.{ .x = offset_x, .y = offset_y }),
|
|
box.text_color
|
|
);
|
|
|
|
offset_y += font_face.getSize() * font_face.line_height;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (box.scientific_number) |scientific_number| {
|
|
const regular = Assets.font(box.font);
|
|
const superscript = Assets.font(.{
|
|
.size = box.font.size * 0.8,
|
|
.variant = box.font.variant
|
|
});
|
|
|
|
var text_position = box.persistent.position;
|
|
text_position.x += box.padding.left;
|
|
text_position.y += box.padding.top;
|
|
|
|
const available_width = box.availableChildrenSize(.X);
|
|
const available_height = box.availableChildrenSize(.Y);
|
|
|
|
const exponent = @floor(std.math.log10(scientific_number));
|
|
const multiplier = std.math.pow(f64, 10, exponent);
|
|
const coefficient = scientific_number / multiplier;
|
|
|
|
// const text = std.fmt.allocPrint(box.allocator, "{d:.2}x10{d}", .{coefficient, exponent}) catch "";
|
|
|
|
var coefficient_buff: [256]u8 = undefined;
|
|
const coefficient_str = std.fmt.formatFloat(&coefficient_buff, coefficient, .{ .mode = .decimal, .precision = box.scientific_precision }) catch "";
|
|
const exponent_str = std.fmt.allocPrint(box.allocator, "{d:.0}", .{exponent}) catch "";
|
|
|
|
var text_size = regular.measureText(coefficient_str);
|
|
text_size.x += regular.measureWidth("x10");
|
|
text_size.x += superscript.measureWidth(exponent_str);
|
|
|
|
text_position.x += (available_width - text_size.x) * alignment_x_coeff;
|
|
text_position.y += (available_height - text_size.y) * alignment_y_coeff;
|
|
|
|
var ctx = FontFace.DrawTextContext{
|
|
.font_face = regular,
|
|
.origin = text_position,
|
|
.tint = box.text_color
|
|
};
|
|
|
|
ctx.drawText(coefficient_str);
|
|
ctx.advanceY(-0.04);
|
|
ctx.advanceX(0.1);
|
|
ctx.drawText("x");
|
|
ctx.advanceY(0.04);
|
|
ctx.drawText("10");
|
|
|
|
ctx.font_face = superscript;
|
|
ctx.advanceY(-0.2);
|
|
ctx.drawText(exponent_str);
|
|
}
|
|
|
|
if (draw_debug) {
|
|
if (self.isKeyActive(box.key)) {
|
|
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.red);
|
|
} else if (self.isKeyHot(box.key)) {
|
|
rl.drawRectangleLinesEx(box_rect, 3, rl.Color.orange);
|
|
} else {
|
|
rl.drawRectangleLinesEx(box_rect, 1, rl.Color.magenta);
|
|
}
|
|
}
|
|
|
|
var child_iter = box.iterChildren();
|
|
while (child_iter.next()) |child| {
|
|
self.drawBox(child);
|
|
}
|
|
}
|
|
|
|
fn beginScissor(self: *UI, rect: Rect) void {
|
|
var intersected_rect = rect;
|
|
if (self.scissor_stack.len > 0) {
|
|
const top_scissor_rect = self.scissor_stack.buffer[self.scissor_stack.len - 1];
|
|
intersected_rect = rect_utils.intersect(top_scissor_rect, rect);
|
|
}
|
|
|
|
self.scissor_stack.appendAssumeCapacity(intersected_rect);
|
|
|
|
rl.beginScissorMode(
|
|
@intFromFloat(intersected_rect.x),
|
|
@intFromFloat(intersected_rect.y),
|
|
@intFromFloat(intersected_rect.width),
|
|
@intFromFloat(intersected_rect.height)
|
|
);
|
|
}
|
|
|
|
fn endScissor(self: *UI) void {
|
|
rl.endScissorMode();
|
|
_ = self.scissor_stack.pop();
|
|
|
|
if (self.scissor_stack.len > 0) {
|
|
const top_scissor_rect = self.scissor_stack.buffer[self.scissor_stack.len - 1];
|
|
|
|
rl.endScissorMode();
|
|
rl.beginScissorMode(
|
|
@intFromFloat(top_scissor_rect.x),
|
|
@intFromFloat(top_scissor_rect.y),
|
|
@intFromFloat(top_scissor_rect.width),
|
|
@intFromFloat(top_scissor_rect.height)
|
|
);
|
|
}
|
|
}
|
|
|
|
fn getKeySeed(self: *UI) u64 {
|
|
var maybe_current = self.parentBox();
|
|
while (maybe_current) |current| {
|
|
if (!current.key.isNil()) {
|
|
return current.key.hash;
|
|
}
|
|
|
|
maybe_current = null;
|
|
if (current.tree.parent_index) |parent_index| {
|
|
maybe_current = &self.boxes.buffer[parent_index];
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
pub fn keyFromString(self: *UI, text: []const u8) Key {
|
|
return Key.initString(self.getKeySeed(), text);
|
|
}
|
|
|
|
pub fn signal(self: *UI, box: *Box) Signal {
|
|
var result = Signal{};
|
|
|
|
if (box.key.isNil()) {
|
|
log.warn("Called signal() with nil key", .{});
|
|
return result;
|
|
}
|
|
|
|
const rect = box.rect();
|
|
var clipped_rect = rect;
|
|
{
|
|
var parent_iter = box.iterParents();
|
|
while (parent_iter.next()) |parent| {
|
|
if (parent.flags.contains(.clip_view)) {
|
|
clipped_rect = rect_utils.intersect(clipped_rect, parent.rect());
|
|
}
|
|
}
|
|
}
|
|
|
|
const key = box.key;
|
|
const clickable = box.flags.contains(.clickable);
|
|
const draggable = box.flags.contains(.draggable);
|
|
const scrollable = box.flags.contains(.scrollable);
|
|
const is_mouse_inside = rect_utils.isInsideVec2(clipped_rect, self.mouse);
|
|
|
|
var event_index: usize = 0;
|
|
while (event_index < self.events.len) {
|
|
var taken = false;
|
|
const event: Event = self.events.buffer[event_index];
|
|
const is_active = self.isKeyActive(key);
|
|
|
|
if (event == .mouse_pressed and clickable and is_mouse_inside) {
|
|
const mouse_button = event.mouse_pressed;
|
|
result.insertMousePressed(mouse_button);
|
|
|
|
self.active_box_keys.put(mouse_button, key);
|
|
taken = true;
|
|
}
|
|
|
|
if (event == .mouse_released and clickable and is_active and is_mouse_inside) {
|
|
const mouse_button = event.mouse_released;
|
|
result.insertMouseReleased(mouse_button);
|
|
result.insertMouseClicked(mouse_button);
|
|
|
|
self.active_box_keys.remove(mouse_button);
|
|
taken = true;
|
|
}
|
|
|
|
if (event == .mouse_released and clickable and is_active and !is_mouse_inside) {
|
|
const mouse_button = event.mouse_released;
|
|
result.insertMouseReleased(mouse_button);
|
|
|
|
self.hot_box_key = null;
|
|
self.active_box_keys.remove(mouse_button);
|
|
taken = true;
|
|
}
|
|
|
|
if (event == .mouse_released and clickable and !is_mouse_inside) {
|
|
result.clicked_outside = true;
|
|
}
|
|
|
|
if (event == .mouse_scroll and is_mouse_inside and scrollable) {
|
|
result.scroll = event.mouse_scroll;
|
|
result.flags.insert(.scrolled);
|
|
|
|
taken = true;
|
|
}
|
|
|
|
if (taken) {
|
|
_ = self.events.swapRemove(event_index);
|
|
} else {
|
|
event_index += 1;
|
|
}
|
|
}
|
|
|
|
if (draggable and self.mouse_delta.equals(Vec2.zero()) == 0) {
|
|
const mouse_buttons = [_]rl.MouseButton{ .mouse_button_left, .mouse_button_right, .mouse_button_middle };
|
|
inline for (mouse_buttons) |mouse_button| {
|
|
const active_box = self.active_box_keys.get(mouse_button);
|
|
|
|
if (active_box != null and active_box.?.eql(key)) {
|
|
result.insertMouseDragged(mouse_button);
|
|
result.drag = self.mouse_delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_mouse_inside and clickable) {
|
|
if (self.hot_box_key == null) {
|
|
self.hot_box_key = key;
|
|
}
|
|
}
|
|
|
|
result.hot = self.isKeyHot(box.key);
|
|
result.active = self.isKeyActive(box.key);
|
|
result.relative_mouse = self.mouse.subtract(rect_utils.position(rect));
|
|
result.mouse = self.mouse;
|
|
result.is_mouse_inside = is_mouse_inside;
|
|
result.shift_modifier = rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift);
|
|
result.ctrl_modifier = rl.isKeyDown(rl.KeyboardKey.key_left_control) or rl.isKeyDown(rl.KeyboardKey.key_right_control);
|
|
|
|
return result;
|
|
}
|
|
|
|
pub fn isKeyHot(self: *UI, key: Key) bool {
|
|
if (self.hot_box_key) |hot_box_key| {
|
|
return hot_box_key.eql(key);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
pub fn isKeyActive(self: *UI, key: Key) bool {
|
|
inline for (std.meta.fields(rl.MouseButton)) |mouse_button_field| {
|
|
const mouse_button: rl.MouseButton = @enumFromInt(mouse_button_field.value);
|
|
|
|
if (self.active_box_keys.get(mouse_button)) |active_box| {
|
|
if (active_box.eql(key)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn isKeyActiveAny(self: *UI) bool {
|
|
return self.active_box_keys.count() != 0;
|
|
}
|
|
|
|
pub fn isKeyboardPressed(self: *UI, key: rl.KeyboardKey) bool {
|
|
const key_u32: u32 = @intCast(@intFromEnum(key));
|
|
|
|
for (0.., self.events.slice()) |i, _event| {
|
|
const event: Event = _event;
|
|
if (event == .key_pressed and event.key_pressed == key_u32) {
|
|
_ = self.events.swapRemove(i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub fn isKeyboardPressedOrHeld(self: *UI, key: rl.KeyboardKey) bool {
|
|
return self.isKeyboardPressed(key) or rl.isKeyPressedRepeat(key);
|
|
}
|
|
|
|
pub fn hasKeyboardPressess(self: *UI) bool {
|
|
for (self.events.slice()) |_event| {
|
|
const event: Event = _event;
|
|
if (event == .key_pressed) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub fn iterCharacterPressess(self: *UI) CharPressIterator {
|
|
return CharPressIterator{
|
|
.events = &self.events,
|
|
};
|
|
}
|
|
|
|
pub fn isShiftDown(self: *UI) bool {
|
|
_ = self;
|
|
return rl.isKeyDown(rl.KeyboardKey.key_left_shift) or rl.isKeyDown(rl.KeyboardKey.key_right_shift);
|
|
}
|
|
|
|
pub fn isCtrlDown(self: *UI) bool {
|
|
_ = self;
|
|
return rl.isKeyDown(rl.KeyboardKey.key_left_control) or rl.isKeyDown(rl.KeyboardKey.key_right_control);
|
|
}
|
|
|
|
// --------------------------------- Widgets ----------------------------------------- //
|
|
|
|
pub const TextInputStorage = struct {
|
|
buffer: std.ArrayList(u8),
|
|
|
|
modified: bool = false,
|
|
editing: bool = false,
|
|
last_pressed_at_ns: i128 = 0,
|
|
cursor_start: usize = 0,
|
|
cursor_stop: usize = 0,
|
|
shown_slice_start: f32 = 0,
|
|
shown_slice_end: f32 = 0,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) TextInputStorage {
|
|
return TextInputStorage{
|
|
.buffer = std.ArrayList(u8).init(allocator)
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: TextInputStorage) void {
|
|
self.buffer.deinit();
|
|
}
|
|
|
|
pub fn setText(self: *TextInputStorage, text: []const u8) !void {
|
|
self.modified = true;
|
|
|
|
self.buffer.clearAndFree();
|
|
try self.buffer.appendSlice(text);
|
|
}
|
|
|
|
pub fn insertSingle(self: *TextInputStorage, index: usize, symbol: u8) !void {
|
|
self.modified = true;
|
|
|
|
try self.buffer.insert(index, symbol);
|
|
|
|
if (self.cursor_start >= index) {
|
|
self.cursor_start += 1;
|
|
}
|
|
|
|
if (self.cursor_stop >= index) {
|
|
self.cursor_stop += 1;
|
|
}
|
|
}
|
|
|
|
fn deleteSingle(self: *TextInputStorage, index: usize) void {
|
|
self.deleteMany(index, index+1);
|
|
}
|
|
|
|
/// If `from` and `to` are the same number, nothing will be deleted
|
|
fn deleteMany(self: *TextInputStorage, from: usize, to: usize) void {
|
|
const from_clamped = std.math.clamp(from, 0, self.buffer.items.len);
|
|
const to_clamped = std.math.clamp(to , 0, self.buffer.items.len);
|
|
if (from_clamped == to_clamped) return;
|
|
|
|
self.modified = true;
|
|
|
|
const lower = @min(from_clamped, to_clamped);
|
|
const upper = @max(from_clamped, to_clamped);
|
|
assert(lower < upper);
|
|
|
|
const deleted_count = (upper - lower);
|
|
|
|
std.mem.copyForwards(u8, self.buffer.items[lower..], self.buffer.items[upper..]);
|
|
self.buffer.items.len -= deleted_count;
|
|
|
|
if (self.cursor_start > upper) {
|
|
self.cursor_start -= deleted_count;
|
|
} else if (self.cursor_start > lower) {
|
|
self.cursor_start = lower;
|
|
}
|
|
|
|
if (self.cursor_stop > upper) {
|
|
self.cursor_stop -= deleted_count;
|
|
} else if (self.cursor_stop > lower) {
|
|
self.cursor_stop = lower;
|
|
}
|
|
}
|
|
|
|
fn getCharOffsetX(self: *const TextInputStorage, font: FontFace, index: usize) f32 {
|
|
const text = self.buffer.items;
|
|
return font.measureWidth(text[0..index]);
|
|
}
|
|
|
|
fn getCharIndex(self: *const TextInputStorage, font: FontFace, x: f32) usize {
|
|
var measure_opts = FontFace.MeasureOptions{
|
|
.up_to_width = x + self.shown_slice_start
|
|
};
|
|
const before_size = font.measureTextEx(self.buffer.items, &measure_opts).x;
|
|
|
|
const index = measure_opts.last_codepoint_index;
|
|
|
|
if (index+1 < self.buffer.items.len) {
|
|
const after_size = self.getCharOffsetX(font, index + 1);
|
|
if (@abs(before_size - x) > @abs(after_size - x)) {
|
|
return index + 1;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
fn nextJumpPoint(self: *const TextInputStorage, index: usize, step: isize) usize {
|
|
assert(step == 1 or step == -1); // Only tested for these cases
|
|
var i = index;
|
|
const text = self.buffer.items;
|
|
|
|
if (step > 0) {
|
|
var prev_whitespace = std.ascii.isWhitespace(text[i]);
|
|
while (true) {
|
|
const cur_whitespace = std.ascii.isWhitespace(text[i]);
|
|
if (!prev_whitespace and cur_whitespace) {
|
|
break;
|
|
}
|
|
|
|
prev_whitespace = cur_whitespace;
|
|
|
|
const next_i = @as(isize, @intCast(i)) + step;
|
|
if (next_i > text.len) {
|
|
break;
|
|
}
|
|
|
|
i = @intCast(next_i);
|
|
|
|
if (i == text.len) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
if (index == 0) return index;
|
|
|
|
i = @intCast(@as(isize, @intCast(i)) + step);
|
|
|
|
var cur_whitespace = std.ascii.isWhitespace(text[i]);
|
|
while (true) {
|
|
const next_i = @as(isize, @intCast(i)) + step;
|
|
if (next_i < 0) {
|
|
break;
|
|
}
|
|
|
|
const next_whitespace = std.ascii.isWhitespace(text[@intCast(next_i)]);
|
|
if (!cur_whitespace and next_whitespace) {
|
|
break;
|
|
}
|
|
|
|
cur_whitespace = next_whitespace;
|
|
|
|
i = @intCast(next_i);
|
|
}
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
pub fn textLength(self: *TextInputStorage) []const u8 {
|
|
return self.buffer.items;
|
|
}
|
|
|
|
fn cursorSlice(self: *const TextInputStorage) []const u8 {
|
|
if (self.cursor_start == self.cursor_stop) {
|
|
return self.buffer.items[self.cursor_start..(self.cursor_start+1)];
|
|
} else {
|
|
const lower = @min(self.cursor_start, self.cursor_stop);
|
|
const upper = @max(self.cursor_start, self.cursor_stop);
|
|
return self.buffer.items[lower..upper];
|
|
}
|
|
}
|
|
|
|
fn insertMany(self: *TextInputStorage, index: usize, text: []const u8) !void {
|
|
if (index > self.buffer.items.len) return;
|
|
|
|
self.modified = true;
|
|
|
|
try self.buffer.insertSlice(index, text);
|
|
|
|
if (self.cursor_start >= index) {
|
|
self.cursor_start += text.len;
|
|
}
|
|
|
|
if (self.cursor_stop >= index) {
|
|
self.cursor_stop += text.len;
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const TextInputOptions = struct {
|
|
key: Key,
|
|
storage: *TextInputStorage,
|
|
editable: bool = true,
|
|
|
|
initial: ?[]const u8 = null,
|
|
placeholder: ?[]const u8 = null,
|
|
text_color: rl.Color = srcery.black
|
|
};
|
|
|
|
pub const NumberInputOptions = struct {
|
|
key: Key,
|
|
storage: *TextInputStorage,
|
|
invalid: bool = false,
|
|
editable: bool = true,
|
|
|
|
initial: ?[]const u8 = null,
|
|
placeholder: ?[]const u8 = null,
|
|
text_color: rl.Color = srcery.black,
|
|
invalid_color: rl.Color = srcery.red
|
|
};
|
|
|
|
pub const CheckboxOptions = struct {
|
|
value: *bool,
|
|
label: ?[]const u8 = null
|
|
};
|
|
|
|
pub const FileInputOptions = struct {
|
|
key: Key,
|
|
allocator: std.mem.Allocator,
|
|
file_picker: *?Platform.FilePickerId,
|
|
open_dialog: bool = true,
|
|
path: ?[]const u8 = null
|
|
};
|
|
|
|
pub fn mouseTooltip(self: *UI) *Box {
|
|
const tooltip = self.getBoxByKey(mouse_tooltip_box_key).?;
|
|
|
|
return tooltip;
|
|
}
|
|
|
|
pub fn button(self: *UI, key: UI.Key) *Box {
|
|
return self.createBox(.{
|
|
.key = key,
|
|
.size_x = Sizing.initFixed(.text),
|
|
.size_y = Sizing.initFixed(.text),
|
|
.flags = &.{ .draw_hot, .draw_active, .clickable },
|
|
.padding = Padding{
|
|
.bottom = self.rem(0.5),
|
|
.top = self.rem(0.5),
|
|
.left = self.rem(1),
|
|
.right = self.rem(1)
|
|
},
|
|
.hot_cursor = .mouse_cursor_pointing_hand,
|
|
});
|
|
}
|
|
|
|
pub fn textButton(self: *UI, text: []const u8) *Box {
|
|
var box = self.button(self.keyFromString(text));
|
|
box.setText(text);
|
|
box.alignment.x = .center;
|
|
box.alignment.y = .center;
|
|
return box;
|
|
}
|
|
|
|
pub fn label(self: *UI, comptime fmt: []const u8, args: anytype) *Box {
|
|
const box = self.createBox(.{
|
|
.size_x = Sizing.initFixed(.text),
|
|
.size_y = Sizing.initFixed(.text),
|
|
.flags = &.{ .wrap_text }
|
|
});
|
|
|
|
box.setFmtText(fmt, args);
|
|
|
|
return box;
|
|
}
|
|
|
|
pub fn beginScrollbar(self: *UI, key: Key) *Box {
|
|
const wrapper = self.createBox(.{
|
|
.key = key,
|
|
.layout_direction = .left_to_right,
|
|
.flags = &.{ .clip_view },
|
|
.size_x = Sizing.initGrowFull(),
|
|
.size_y = Sizing.initGrowFull()
|
|
});
|
|
wrapper.beginChildren();
|
|
|
|
const content_area = self.createBox(.{
|
|
.key = self.keyFromString("Scrollable content area"),
|
|
.flags = &.{ .scrollable, .clip_view },
|
|
.size_x = Sizing.initGrowFull(),
|
|
.size_y = Sizing.initFitChildren(),
|
|
});
|
|
content_area.beginChildren();
|
|
|
|
const content_size = content_area.persistent.size.y;
|
|
const visible_percent = clamp(wrapper.persistent.size.y / content_size, 0, 1);
|
|
const sroll_offset = content_area.persistent.sroll_offset;
|
|
content_area.view_offset.y = sroll_offset * (1 - visible_percent) * content_size;
|
|
|
|
return content_area;
|
|
}
|
|
|
|
pub fn endScrollbar(self: *UI) void {
|
|
const content_area = self.parentBox().?;
|
|
content_area.endChildren();
|
|
|
|
const wrapper = self.parentBox().?;
|
|
|
|
const visible_percent = clamp(wrapper.persistent.size.y / content_area.persistent.size.y, 0, 1);
|
|
|
|
{
|
|
const scrollbar_area = self.createBox(.{
|
|
.key = self.keyFromString("Scrollbar area"),
|
|
.background = srcery.hard_black,
|
|
.flags = &.{ .scrollable },
|
|
.size_x = .{ .fixed = .{ .pixels = 24 } },
|
|
.size_y = Sizing.initGrowFull()
|
|
});
|
|
scrollbar_area.beginChildren();
|
|
defer scrollbar_area.endChildren();
|
|
|
|
const draggable = self.createBox(.{
|
|
.key = self.keyFromString("Scrollbar button"),
|
|
.background = srcery.black,
|
|
.flags = &.{ .draw_hot, .draw_active, .clickable, .draggable },
|
|
.borders = Borders.all(.{ .size = 4, .color = srcery.xgray3 }),
|
|
.size_x = Sizing.initFixed(.{ .parent_percent = 1 }),
|
|
.size_y = Sizing.initFixed(.{ .parent_percent = visible_percent }),
|
|
.hot_cursor = .mouse_cursor_pointing_hand
|
|
});
|
|
|
|
const sroll_offset = &content_area.persistent.sroll_offset;
|
|
const scrollbar_height = scrollbar_area.persistent.size.y;
|
|
const max_offset = scrollbar_height * (1 - visible_percent);
|
|
draggable.setFloatY(content_area.persistent.position.y + sroll_offset.* * max_offset);
|
|
|
|
const draggable_signal = self.signal(draggable);
|
|
if (draggable_signal.dragged()) {
|
|
sroll_offset.* += draggable_signal.drag.y / max_offset;
|
|
}
|
|
|
|
const scroll_speed = 16;
|
|
const scrollbar_signal = self.signal(scrollbar_area);
|
|
if (scrollbar_signal.scrolled()) {
|
|
sroll_offset.* -= scrollbar_signal.scroll.y / max_offset * scroll_speed;
|
|
}
|
|
|
|
const content_area_signal = self.signal(content_area);
|
|
if (content_area_signal.scrolled()) {
|
|
sroll_offset.* -= content_area_signal.scroll.y / max_offset * scroll_speed;
|
|
}
|
|
|
|
sroll_offset.* = std.math.clamp(sroll_offset.*, 0, 1);
|
|
}
|
|
|
|
wrapper.endChildren();
|
|
}
|
|
|
|
pub fn textInput(self: *UI, opts: TextInputOptions) !void {
|
|
const now = std.time.nanoTimestamp();
|
|
|
|
const container = self.createBox(.{
|
|
.key = opts.key,
|
|
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
|
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
|
.flags = &.{ .clickable, .clip_view, .draggable },
|
|
.background = srcery.bright_white,
|
|
.align_y = .center,
|
|
.padding = UI.Padding.all(self.rem(0.25)),
|
|
});
|
|
container.beginChildren();
|
|
defer container.endChildren();
|
|
|
|
const font = Assets.font(container.font);
|
|
const storage = opts.storage;
|
|
const storage_text = &storage.buffer;
|
|
|
|
if (opts.initial != null and container.created) {
|
|
storage_text.clearAndFree();
|
|
try storage_text.appendSlice(opts.initial.?);
|
|
}
|
|
|
|
const cursor_start_x = storage.getCharOffsetX(font, storage.cursor_start);
|
|
const cursor_stop_x = storage.getCharOffsetX(font, storage.cursor_stop);
|
|
|
|
{ // Text visuals
|
|
var text_color = opts.text_color;
|
|
var text: []const u8 = storage_text.items;
|
|
if (opts.placeholder != null and text.len == 0) {
|
|
text = opts.placeholder.?;
|
|
text_color = text_color.alpha(0.6);
|
|
}
|
|
|
|
const text_size = font.measureText(text);
|
|
const visible_text_width = container.persistent.size.x - container.padding.byAxis(.X);
|
|
|
|
const shown_window_size = @min(visible_text_width, text_size.x);
|
|
if (storage.shown_slice_end - storage.shown_slice_start != shown_window_size) {
|
|
const shown_slice_middle = (storage.shown_slice_end + storage.shown_slice_start) / 2;
|
|
storage.shown_slice_start = shown_slice_middle - shown_window_size/2;
|
|
storage.shown_slice_end = shown_slice_middle + shown_window_size/2;
|
|
}
|
|
if (storage.shown_slice_end > text_size.x) {
|
|
storage.shown_slice_end = shown_window_size;
|
|
storage.shown_slice_start = storage.shown_slice_end - shown_window_size;
|
|
} else if (storage.shown_slice_start < 0) {
|
|
storage.shown_slice_start = 0;
|
|
storage.shown_slice_end = shown_window_size;
|
|
}
|
|
|
|
if (cursor_stop_x > storage.shown_slice_end) {
|
|
storage.shown_slice_start = cursor_stop_x - shown_window_size;
|
|
storage.shown_slice_end = cursor_stop_x;
|
|
}
|
|
if (cursor_stop_x < storage.shown_slice_start) {
|
|
storage.shown_slice_start = cursor_stop_x;
|
|
storage.shown_slice_end = cursor_stop_x + shown_window_size;
|
|
}
|
|
|
|
_ = self.createBox(.{
|
|
.text_color = text_color,
|
|
.text = text,
|
|
.float_relative_to = container,
|
|
.float_rect = Rect{
|
|
.x = container.padding.left - storage.shown_slice_start,
|
|
.y = 0,
|
|
.width = visible_text_width,
|
|
.height = container.persistent.size.y
|
|
},
|
|
.align_y = .center,
|
|
.align_x = .start
|
|
});
|
|
}
|
|
|
|
const container_signal = self.signal(container);
|
|
if (opts.editable and container_signal.hot) {
|
|
container.borders = UI.Borders.all(.{
|
|
.color = srcery.red,
|
|
.size = 2
|
|
});
|
|
}
|
|
|
|
// Text editing visuals
|
|
if (storage.editing) {
|
|
const blink_period = std.time.ns_per_s;
|
|
const blink = @mod(now - storage.last_pressed_at_ns, blink_period) < 0.5 * blink_period;
|
|
|
|
const cursor_color = srcery.hard_black;
|
|
|
|
if (storage.cursor_start == storage.cursor_stop and blink) {
|
|
_ = self.createBox(.{
|
|
.background = cursor_color,
|
|
.float_relative_to = container,
|
|
.float_rect = Rect{
|
|
.x = container.padding.left + cursor_start_x - storage.shown_slice_start,
|
|
.y = container.padding.top,
|
|
.width = 2,
|
|
.height = container.persistent.size.y - container.padding.byAxis(.Y)
|
|
},
|
|
});
|
|
}
|
|
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
const lower_cursor_x = @min(cursor_start_x, cursor_stop_x);
|
|
const upper_cursor_x = @max(cursor_start_x, cursor_stop_x);
|
|
|
|
_ = self.createBox(.{
|
|
.background = cursor_color.alpha(0.25),
|
|
.float_relative_to = container,
|
|
.float_rect = Rect{
|
|
.x = container.padding.left + lower_cursor_x - storage.shown_slice_start,
|
|
.y = container.padding.top,
|
|
.width = upper_cursor_x - lower_cursor_x,
|
|
.height = container.persistent.size.y - container.padding.byAxis(.Y)
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (opts.editable and container_signal.active) {
|
|
storage.editing = true;
|
|
}
|
|
|
|
if (self.isKeyActiveAny() and !container_signal.active) {
|
|
storage.editing = false;
|
|
}
|
|
|
|
// Text input controls
|
|
if (storage.editing) {
|
|
const shift = container_signal.shift_modifier;
|
|
const ctrl = container_signal.ctrl_modifier;
|
|
|
|
var no_blinking = false;
|
|
|
|
{ // Cursor movement controls
|
|
var move_cursor_dir: i32 = 0;
|
|
if (self.isKeyboardPressedOrHeld(.key_left)) {
|
|
move_cursor_dir -= 1;
|
|
}
|
|
if (self.isKeyboardPressedOrHeld(.key_right)) {
|
|
move_cursor_dir += 1;
|
|
}
|
|
|
|
if (move_cursor_dir != 0) {
|
|
if (shift) {
|
|
if (ctrl) {
|
|
storage.cursor_stop = storage.nextJumpPoint(storage.cursor_stop, move_cursor_dir);
|
|
} else {
|
|
const cursor_stop: isize = @intCast(storage.cursor_stop);
|
|
storage.cursor_stop = @intCast(std.math.clamp(
|
|
cursor_stop + move_cursor_dir,
|
|
0,
|
|
@as(isize, @intCast(storage_text.items.len))
|
|
));
|
|
}
|
|
|
|
} else {
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
const lower = @min(storage.cursor_start, storage.cursor_stop);
|
|
const upper = @max(storage.cursor_start, storage.cursor_stop);
|
|
|
|
if (move_cursor_dir < 0) {
|
|
if (ctrl) {
|
|
storage.cursor_start = storage.nextJumpPoint(lower, -1);
|
|
} else {
|
|
storage.cursor_start = lower;
|
|
}
|
|
} else {
|
|
if (ctrl) {
|
|
storage.cursor_start = storage.nextJumpPoint(upper, 1);
|
|
} else {
|
|
storage.cursor_start = upper;
|
|
}
|
|
}
|
|
storage.cursor_stop = storage.cursor_start;
|
|
|
|
} else {
|
|
var cursor = storage.cursor_start;
|
|
|
|
if (ctrl) {
|
|
cursor = storage.nextJumpPoint(cursor, move_cursor_dir);
|
|
} else {
|
|
cursor = @intCast(std.math.clamp(
|
|
@as(isize, @intCast(cursor)) + move_cursor_dir,
|
|
0,
|
|
@as(isize, @intCast(storage_text.items.len))
|
|
));
|
|
}
|
|
|
|
storage.cursor_start = cursor;
|
|
storage.cursor_stop = cursor;
|
|
}
|
|
}
|
|
|
|
no_blinking = true;
|
|
}
|
|
}
|
|
|
|
{ // Cursor movement with mouse
|
|
var mouse = container_signal.relative_mouse;
|
|
mouse.x -= container.padding.left;
|
|
mouse.y -= container.padding.top;
|
|
|
|
const mouse_index = storage.getCharIndex(font, mouse.x);
|
|
|
|
if (container_signal.flags.contains(.left_pressed)) {
|
|
storage.cursor_start = mouse_index;
|
|
storage.cursor_stop = mouse_index;
|
|
no_blinking = true;
|
|
}
|
|
if (container_signal.flags.contains(.left_dragging)) {
|
|
storage.cursor_stop = mouse_index;
|
|
no_blinking = true;
|
|
}
|
|
}
|
|
|
|
// Deletion
|
|
if (self.isKeyboardPressedOrHeld(.key_backspace) and storage_text.items.len > 0) {
|
|
if (storage.cursor_start == storage.cursor_stop) {
|
|
if (storage.cursor_start > 0) {
|
|
storage.deleteMany(storage.cursor_start-1, storage.cursor_start);
|
|
}
|
|
} else {
|
|
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
|
|
}
|
|
|
|
no_blinking = true;
|
|
}
|
|
|
|
// Common commands (E.g. Ctrl+A, Ctrl+C, etc.)
|
|
if (ctrl) {
|
|
if (self.isKeyboardPressedOrHeld(.key_x)) {
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
const allocator = storage.buffer.allocator;
|
|
const text_z = try allocator.dupeZ(u8, storage.cursorSlice());
|
|
defer allocator.free(text_z);
|
|
|
|
rl.setClipboardText(text_z);
|
|
|
|
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
|
|
}
|
|
} else if (self.isKeyboardPressedOrHeld(.key_a)) {
|
|
storage.cursor_start = 0;
|
|
storage.cursor_stop = storage_text.items.len;
|
|
|
|
} else if (self.isKeyboardPressedOrHeld(.key_c)) {
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
const allocator = storage.buffer.allocator;
|
|
const text_z = try allocator.dupeZ(u8, storage.cursorSlice());
|
|
defer allocator.free(text_z);
|
|
|
|
rl.setClipboardText(text_z);
|
|
}
|
|
} else if (self.isKeyboardPressedOrHeld(.key_v)) {
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
|
|
}
|
|
|
|
const clipboard = rl.getClipboardText();
|
|
try storage.insertMany(storage.cursor_start, clipboard);
|
|
}
|
|
}
|
|
|
|
{ // Insertion
|
|
// TODO: Handle UTF8 characters
|
|
var char_iter = self.iterCharacterPressess();
|
|
while (char_iter.next()) |char| {
|
|
if (char <= 255 and std.ascii.isPrint(@intCast(char))) {
|
|
char_iter.consume();
|
|
|
|
if (storage.cursor_start != storage.cursor_stop) {
|
|
storage.deleteMany(storage.cursor_start, storage.cursor_stop);
|
|
}
|
|
try storage.insertSingle(storage.cursor_start, @intCast(char));
|
|
|
|
no_blinking = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (no_blinking) {
|
|
storage.last_pressed_at_ns = now;
|
|
}
|
|
|
|
if (self.isKeyboardPressed(.key_escape) or self.isKeyboardPressed(.key_enter)) {
|
|
storage.editing = false;
|
|
}
|
|
|
|
if (container_signal.clicked_outside and !container_signal.is_mouse_inside) {
|
|
storage.editing = false;
|
|
}
|
|
|
|
if (!opts.editable) {
|
|
storage.editing = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T {
|
|
const storage = opts.storage;
|
|
|
|
var text_opts = TextInputOptions{
|
|
.key = opts.key,
|
|
.storage = opts.storage,
|
|
.initial = opts.initial,
|
|
.text_color = opts.text_color,
|
|
.placeholder = opts.placeholder,
|
|
.editable = opts.editable
|
|
};
|
|
|
|
var is_invalid = opts.invalid;
|
|
if (storage.buffer.items.len > 0 and std.meta.isError(std.fmt.parseFloat(T, storage.buffer.items))) {
|
|
is_invalid = true;
|
|
}
|
|
|
|
if (is_invalid) {
|
|
text_opts.text_color = opts.invalid_color;
|
|
}
|
|
|
|
try self.textInput(text_opts);
|
|
|
|
if (std.fmt.parseFloat(T, storage.buffer.items)) |new_value| {
|
|
return new_value;
|
|
} else |_| {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn checkbox(self: *UI, opts: CheckboxOptions) void {
|
|
const container = self.createBox(.{
|
|
.key = UI.Key.initPtr(opts.value),
|
|
.size_x = UI.Sizing.initFitChildren(),
|
|
.size_y = UI.Sizing.initFitChildren(),
|
|
.flags = &.{ .draw_hot, .draw_active, .clickable },
|
|
.hot_cursor = .mouse_cursor_pointing_hand,
|
|
.layout_direction = .left_to_right,
|
|
.layout_gap = self.rem(0.25)
|
|
});
|
|
container.beginChildren();
|
|
defer container.endChildren();
|
|
|
|
const container_signal = self.signal(container);
|
|
|
|
const marker = self.createBox(.{
|
|
.key = self.keyFromString("checkbox marker"),
|
|
.size_x = UI.Sizing.initFixedPixels(self.rem(1)),
|
|
.size_y = UI.Sizing.initFixedPixels(self.rem(1)),
|
|
.background = srcery.bright_white,
|
|
.visual_hot = container_signal.hot,
|
|
.visual_active = container_signal.active,
|
|
.flags = &.{ .draw_hot, .draw_active },
|
|
});
|
|
|
|
if (opts.label) |text| {
|
|
_ = self.createBox(.{
|
|
.size_x = Sizing.initFixed(.text),
|
|
.size_y = Sizing.initFixed(.text),
|
|
.text = text
|
|
});
|
|
}
|
|
|
|
if (opts.value.*) {
|
|
marker.texture = Assets.checkbox_mark;
|
|
}
|
|
|
|
if (container_signal.clicked()) {
|
|
opts.value.* = !opts.value.*;
|
|
}
|
|
}
|
|
|
|
pub fn fileInput(self: *UI, opts: FileInputOptions) ?[]u8 {
|
|
var result: ?[]u8 = null;
|
|
|
|
const container = self.createBox(.{
|
|
.key = opts.key,
|
|
.size_x = Sizing.initGrowUpTo(.{ .pixels = 200 }),
|
|
.size_y = Sizing.initFixed(Unit.initPixels(self.rem(1))),
|
|
.flags = &.{ .clickable, .clip_view, .draw_hot, .draw_active },
|
|
.background = srcery.bright_white,
|
|
.align_y = .center,
|
|
.padding = UI.Padding.all(self.rem(0.25)),
|
|
.hot_cursor = .mouse_cursor_pointing_hand,
|
|
.layout_gap = self.rem(0.5)
|
|
});
|
|
container.beginChildren();
|
|
defer container.endChildren();
|
|
|
|
_ = self.createBox(.{
|
|
.texture = Assets.file,
|
|
.size_x = UI.Sizing.initFixed(.{ .pixels = 16 }),
|
|
.size_y = UI.Sizing.initFixed(.{ .pixels = 16 })
|
|
});
|
|
|
|
const path_box = self.createBox(.{
|
|
.size_x = UI.Sizing.initGrowFull(),
|
|
.size_y = UI.Sizing.initGrowFull(),
|
|
.text_color = srcery.black
|
|
});
|
|
|
|
if (opts.path) |path| {
|
|
path_box.setText(std.fs.path.basename(path));
|
|
container.tooltip = path;
|
|
} else {
|
|
path_box.setText("<none>");
|
|
}
|
|
|
|
if (opts.file_picker.* != null) {
|
|
if (Platform.waitUntilFilePickerDone(opts.allocator, opts.file_picker)) |path| {
|
|
result = path;
|
|
}
|
|
} else {
|
|
const container_signal = self.signal(container);
|
|
if (container_signal.clicked()) {
|
|
var file_open_options: Platform.OpenFileOptions = .{};
|
|
if (opts.open_dialog) {
|
|
file_open_options.style = .open;
|
|
file_open_options.file_must_exist = true;
|
|
} else {
|
|
file_open_options.style = .save;
|
|
file_open_options.prompt_overwrite = true;
|
|
}
|
|
file_open_options.appendFilter("All", "*") catch unreachable;
|
|
file_open_options.appendFilter("Binary", "*.bin") catch unreachable;
|
|
|
|
if (Platform.spawnFilePicker(&file_open_options)) |file_picker_id| {
|
|
opts.file_picker.* = file_picker_id;
|
|
} else |e| {
|
|
log.err("Failed to open file picker: {}", .{e});
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} |