391 lines
9.5 KiB
Plaintext
Raw Normal View History

2024-10-29 22:45:47 +01:00
module ugui;
import mtree;
import cache;
2025-10-05 23:14:16 +02:00
2024-10-29 22:45:47 +01:00
import std::io;
import std::core::string;
import std::core::mem::allocator;
2025-10-03 15:19:52 +02:00
import std::collections::pair;
2025-10-06 23:23:35 +02:00
import std::sort;
macro println(...)
{
$for var $i = 0; $i < $vacount; $i++:
io::print($vaexpr[$i]);
$endfor
io::printn();
}
// element ids are just long ints
alias Id = uint;
enum ElemType {
ETYPE_NONE,
ETYPE_DIV,
ETYPE_BUTTON,
ETYPE_SLIDER,
ETYPE_TEXT,
2025-01-30 18:36:47 +01:00
ETYPE_SPRITE,
}
bitstruct ElemFlags : uint {
bool updated : 0;
2025-10-08 22:16:17 +02:00
bool is_new : 1; // element is new in the cache
bool shown : 2; // element has been shown (drawn) this frame
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_repeat : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
2025-06-30 13:10:00 +02:00
bool text_input : 8;
bool has_focus : 9;
}
// element structure
struct Elem {
Id id;
int tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
Rect children_bounds;
ElemType type;
Layout layout;
2025-10-02 23:19:42 +02:00
int z_index;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
2025-01-30 18:36:47 +01:00
ElemSprite sprite;
}
}
const uint MAX_ELEMENTS = 256;
const uint MAX_COMMANDS = 2048;
const uint STACK_STEP = 10;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
2025-10-03 15:19:52 +02:00
// Tuple of Element pointers, used when it is useful to get both parent and child
alias PElemTuple = pair::Pair{Elem*, Elem*};
// relationships between elements are stored in a tree, it stores just the ids
alias IdTree = mtree::MTree{Id};
// elements themselves are kept in a cache
2025-05-05 16:23:26 +02:00
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
2025-07-14 13:05:30 +02:00
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
2025-10-10 22:31:28 +02:00
struct InputData {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Font font;
SpriteAtlas sprite_atlas;
bool has_focus;
2025-10-10 22:31:28 +02:00
InputData input, current_input;
2024-12-20 01:45:10 +01:00
Id hover_id;
Id focus_id;
2024-12-19 15:24:39 +01:00
Rect div_scissor; // the current div bounds used for scissor test
int active_div; // tree node indicating the current active div
}
2024-10-29 22:45:47 +01:00
// return a pointer to the parent of the current active div
2025-05-05 16:23:26 +02:00
fn Elem*? Ctx.get_parent(&ctx)
2024-10-29 22:45:47 +01:00
{
2025-10-03 15:19:52 +02:00
Id parent_id = ctx.tree[ctx.active_div]!;
Elem* parent = ctx.cache.search(parent_id)!;
2025-09-12 22:32:47 +02:00
return parent;
2024-10-29 22:45:47 +01:00
}
2024-12-25 12:30:35 +01:00
macro @bits(#a) => $typeof(#a).sizeof*8;
macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n));
const uint GOLDEN_RATIO = 0x9E3779B9;
// generate an id combining the hashes of the parent id and the label
// with the Cantor pairing function
fn Id? Ctx.gen_id(&ctx, Id id2)
{
// FIXME: this is SHIT
2025-10-03 15:19:52 +02:00
Id id1 = ctx.tree.get(ctx.active_div)!;
2024-12-25 12:30:35 +01:00
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
mixed += GOLDEN_RATIO;
return mixed;
}
// compute the id from arguments and the line of the call
macro Id @compute_id(...)
{
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
$for var $i = 0; $i < $vacount; $i++:
id ^= (Id)$vaconst[$i].hash();
$endfor
return id;
}
2024-10-31 00:04:49 +01:00
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
2025-10-03 15:19:52 +02:00
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
2024-10-31 00:04:49 +01:00
{
bool is_new;
2025-10-03 15:19:52 +02:00
Elem* parent;
2024-12-19 15:24:39 +01:00
Elem* elem;
2025-10-03 15:19:52 +02:00
parent = ctx.get_parent() ?? &&(Elem){};
2025-09-12 12:48:29 +02:00
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
2024-12-19 15:24:39 +01:00
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
2025-10-08 22:16:17 +02:00
elem.flags.shown = true;
2024-12-19 19:42:02 +01:00
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
2025-10-03 15:19:52 +02:00
elem.z_index = parent.z_index;
2025-10-04 19:58:34 +02:00
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
2025-10-03 15:19:52 +02:00
return {elem, parent};
2024-10-31 00:04:49 +01:00
}
2024-12-25 12:30:35 +01:00
// find an element, does not allocate a new one in cache
// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE
macro Elem* Ctx.find_elem(&ctx, Id id)
{
2025-05-05 16:23:26 +02:00
Elem*? elem;
2024-12-25 12:30:35 +01:00
elem = ctx.cache.search(id);
if (catch elem) {
2025-05-05 16:23:26 +02:00
return &&(Elem){};
2024-12-25 12:30:35 +01:00
}
return elem;
}
fn Elem*? Ctx.get_active_div(&ctx)
2024-11-14 23:42:20 +01:00
{
2025-10-03 15:19:52 +02:00
Id id = ctx.tree.get(ctx.active_div)!;
2024-11-14 23:42:20 +01:00
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx, Allocator allocator)
2024-10-29 22:45:47 +01:00
{
ctx.tree.init(MAX_ELEMENTS, allocator);
2024-10-29 22:45:47 +01:00
defer catch { (void)ctx.tree.free(); }
ctx.cache.init(allocator)!;
2024-10-29 22:45:47 +01:00
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(allocator::mem, MAX_COMMANDS);
2024-10-29 22:45:47 +01:00
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator);
2025-07-06 23:13:16 +02:00
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
2024-10-29 22:45:47 +01:00
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
2024-12-15 21:39:26 +01:00
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
2024-10-29 22:45:47 +01:00
}
2025-05-05 16:23:26 +02:00
fn void? Ctx.frame_begin(&ctx)
2024-10-29 22:45:47 +01:00
{
// 1. Reset the active div
2024-10-31 00:04:49 +01:00
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
2025-10-03 15:19:52 +02:00
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
ctx.active_div = elem.tree_idx;
2024-10-29 22:45:47 +01:00
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
2024-10-29 22:45:47 +01:00
// down the element tree
2024-12-19 15:24:39 +01:00
elem.flags.updated = ctx.input.events.resize;
2024-10-29 22:45:47 +01:00
// if the window has focus then the root element also has focus, no other
// computation needed, child elements need to check the mouse positon and
// other stuff
2024-12-20 01:45:10 +01:00
//elem.flags.has_focus = ctx.has_focus;
2024-10-31 00:04:49 +01:00
elem.bounds = {0, 0, ctx.width, ctx.height};
2025-10-02 23:19:42 +02:00
elem.z_index = 0;
elem.div.scroll_x.enabled = false;
elem.div.scroll_y.enabled = false;
elem.layout.dir = ROW;
elem.layout.anchor = TOP_LEFT;
elem.layout.w = @exact(ctx.width);
elem.layout.h = @exact(ctx.height);
2024-10-29 22:45:47 +01:00
ctx.div_scissor = elem.bounds;
2024-10-29 22:45:47 +01:00
}
2025-05-05 16:23:26 +02:00
fn void? Ctx.frame_end(&ctx)
2024-10-29 22:45:47 +01:00
{
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) {
return WRONG_ID?;
}
2025-10-10 22:31:28 +02:00
// 2. clear input fields
ctx.input = ctx.current_input;
ctx.current_input.events = {};
ctx.current_input.keyboard.text_len = 0;
// DO THE LAYOUT
2025-10-03 15:19:52 +02:00
ctx.layout_element_tree()!;
2024-12-16 17:06:16 +01:00
2025-10-10 22:31:28 +02:00
foreach (idx, id : ctx.tree.elem_vec) {
if (!ctx.tree.is_used((int)idx)) continue;
Elem* c = ctx.find_elem(id);
// reset events
c.events = {};
// reset shown flag
// TODO: use shown_last_frame to avoid this loop entirely
c.flags.shown = false;
}
// Propagate input events to the right elements
ctx.set_elem_events(ctx.hover_id);
ctx.set_elem_events(ctx.focus_id);
2024-10-29 22:45:47 +01:00
// 1. clear the tree
2024-12-17 11:26:59 +01:00
ctx.tree.nuke();
2024-10-29 22:45:47 +01:00
2024-12-15 21:39:26 +01:00
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
ctx.font.should_update = false;
}
if (ctx.sprite_atlas.should_update) {
ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!;
ctx.sprite_atlas.should_update = false;
}
2024-12-15 21:39:26 +01:00
2025-10-07 17:47:24 +02:00
// sort the command buffer by the z-index
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
// TODO: instead of sorting at the end perform ordered inserts into the command buffer
//sort::countingsort(ctx.cmd_queue, fn uint(Cmd c) => c.z_index+1);
ctx.cmd_queue.sort()!;
// debug
$if $feature(DEBUG_POINTER):
2024-12-15 21:39:26 +01:00
// draw mouse position
2024-10-29 22:45:47 +01:00
Cmd cmd = {
.type = CMD_RECT,
2025-06-03 22:03:14 +02:00
.z_index = int.max-1, // hopefully over everything else
.rect.rect = {
.x = ctx.input.mouse.pos.x - 2,
.y = ctx.input.mouse.pos.y - 2,
.w = 4,
.h = 4,
2024-10-29 22:45:47 +01:00
},
2024-12-28 16:59:12 +01:00
.rect.color = 0xff00ffffu.to_rgba()
2024-10-29 22:45:47 +01:00
};
ctx.cmd_queue.push(cmd);
2025-06-12 19:49:43 +02:00
$endif
2025-06-03 18:15:46 +02:00
// foreach (i, c: ctx.cmd_queue) {
// io::printf("[%d]: ", i);
// io::printn(c);
// }
2024-10-29 22:45:47 +01:00
}
2025-10-02 23:19:42 +02:00
2025-10-10 22:31:28 +02:00
macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds);
// Check if the element is hovered and/or focused, if it is update the context ids.
// The order in which the elements are passed to this function is not relevant
fn void Ctx.update_hover_and_focus(&ctx, Elem* elem)
{
2024-12-20 01:45:10 +01:00
bool hover = ctx.is_hovered(elem);
2025-10-10 22:31:28 +02:00
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
2025-01-30 18:36:47 +01:00
2025-10-10 22:31:28 +02:00
if (hover) {
Elem* prev_hover = ctx.find_elem(ctx.hover_id);
bool different = prev_hover.id != elem.id;
bool still_hovered = ctx.is_hovered(prev_hover);
bool shown = prev_hover.flags.shown;
bool above = prev_hover.z_index > elem.z_index;
hover = !(different && still_hovered && shown && above);
}
if (focus) {
Elem* prev_focus = ctx.find_elem(ctx.hover_id);
bool different = prev_focus.id != elem.id;
bool shown = prev_focus.flags.shown;
bool above = prev_focus.z_index > elem.z_index;
focus = !(different && shown && above);
2024-12-20 01:45:10 +01:00
}
2025-10-10 22:31:28 +02:00
if (hover) ctx.hover_id = elem.id;
if (focus) ctx.focus_id = elem.id;
}
2024-12-20 19:50:58 +01:00
2025-10-10 22:31:28 +02:00
// FIXME: this does not work with touch
fn void Ctx.set_elem_events(&ctx, Id id)
{
bool hover = id == ctx.hover_id;
bool focus = id == ctx.focus_id;
Elem* e = ctx.find_elem(id);
2025-01-30 18:36:47 +01:00
2025-10-10 22:31:28 +02:00
e.events = {
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
2024-12-20 01:45:10 +01:00
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.key_press = focus && ctx.input.events.key_press,
.key_release = focus && ctx.input.events.key_release,
.key_repeat = focus && ctx.input.events.key_repeat,
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
2024-12-20 01:45:10 +01:00
};
}