ugui.c3l/lib/ugui.c3l/src/ugui_core.c3

339 lines
7.9 KiB
Plaintext
Raw Normal View History

2024-10-29 22:45:47 +01:00
module ugui;
import vtree;
import cache;
import fifo;
2024-10-29 22:45:47 +01:00
import std::io;
import std::core::string;
import std::core::mem::allocator;
// 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;
2024-12-20 01:45:10 +01:00
bool is_new : 1;
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_hold : 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;
}
// element structure
struct Elem {
Id id;
isz tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
ElemType type;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
2025-01-30 18:36:47 +01:00
ElemSprite sprite;
}
}
// relationships between elements are stored in a tree, it stores just the ids
2025-05-05 16:23:26 +02:00
alias IdTree = vtree::VTree{Id};
// elements themselves are kept in a cache
2025-01-30 22:27:46 +01:00
const uint MAX_ELEMENTS = 256;
2025-05-05 16:23:26 +02:00
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
2025-05-05 16:23:26 +02:00
alias CmdQueue = fifo::Fifo{Cmd};
2025-07-14 13:05:30 +02:00
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
const uint STACK_STEP = 10;
const uint MAX_ELEMS = 128;
2025-06-15 18:54:35 +02:00
const uint MAX_CMDS = 2048;
const uint ROOT_ID = 1;
2024-12-25 12:30:35 +01:00
const uint TEXT_MAX = 64;
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;
struct input {
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;
2024-12-25 12:30:35 +01:00
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
2025-06-30 13:10:00 +02:00
ModKeys modkeys;
}
}
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
isz 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
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
2025-07-14 13:05:30 +02:00
Elem*? parent = ctx.cache.search(parent_id);
if (catch parent) return parent;
if (parent.type != ETYPE_DIV) return WRONG_ELEMENT_TYPE?;
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)
{
2024-12-25 12:30:35 +01:00
Id id1 = ctx.tree.get(ctx.active_div)!;
// 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
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
2024-10-31 00:04:49 +01:00
{
Elem empty_elem;
bool is_new;
2024-12-19 15:24:39 +01:00
Elem* elem;
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
2024-12-19 19:42:02 +01:00
elem.id = id;
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
2024-12-19 15:24:39 +01:00
return elem;
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
{
2024-12-01 00:26:56 +01:00
Id id = ctx.tree.get(ctx.active_div)!;
2024-11-14 23:42:20 +01:00
return ctx.cache.search(id);
}
2025-05-05 16:23:26 +02:00
fn void? Ctx.init(&ctx)
2024-10-29 22:45:47 +01:00
{
ctx.tree.init(MAX_ELEMENTS)!;
defer catch { (void)ctx.tree.free(); }
ctx.cache.init()!;
defer catch { (void)ctx.cache.free(); }
2025-06-15 18:54:35 +02:00
ctx.cmd_queue.init(MAX_CMDS)!;
2024-10-29 22:45:47 +01:00
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator::heap());
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;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!;
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};
elem.div.layout = LAYOUT_ROW;
elem.div.z_index = 0;
elem.div.children_bounds = elem.bounds;
elem.div.scroll_x.enabled = false;
elem.div.scroll_y.enabled = false;
elem.div.pcb = {};
elem.div.origin_c = {};
elem.div.origin_r = {};
2024-10-29 22:45:47 +01:00
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
2024-10-29 22:45:47 +01:00
// The root element does not push anything to the stack
// TODO: add a background color taken from a theme or config
}
2025-06-03 18:15:46 +02:00
const int DEBUG = 1;
2025-05-05 16:23:26 +02:00
fn void? Ctx.frame_end(&ctx)
2024-10-29 22:45:47 +01:00
{
// FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
Elem* root = ctx.get_active_div()!;
2025-07-14 12:57:53 +02:00
if (root.id != ROOT_ID) return WRONG_ID?;
2024-12-16 17:06:16 +01:00
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
// 2. clear input fields
ctx.input.events = (InputEvents)0;
2024-12-25 12:30:35 +01:00
ctx.input.keyboard.text_len = 0;
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-06-03 18:15:46 +02:00
// debug
$if DEBUG == 1:
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.enqueue(&cmd)!;
2025-06-12 19:49:43 +02:00
$endif
2025-06-03 18:15:46 +02:00
2025-06-12 19:49:43 +02:00
// sort the command buffer by the z-index
2025-06-03 22:03:14 +02:00
ctx.cmd_queue.sort()!;
// foreach (i, c: ctx.cmd_queue) {
// io::printf("[%d]: ", i);
// io::printn(c);
// }
2024-10-29 22:45:47 +01:00
}
2024-12-01 00:26:56 +01:00
<*
2024-10-31 00:04:49 +01:00
* @ensure elem != null
2024-12-20 01:45:10 +01:00
*>
macro bool Ctx.is_hovered(&ctx, Elem *elem)
2024-10-29 22:45:47 +01:00
{
2024-12-26 22:58:24 +01:00
return ctx.input.mouse.pos.in_rect(elem.bounds);
2024-10-29 22:45:47 +01:00
}
2024-12-20 01:45:10 +01:00
macro bool Ctx.elem_focus(&ctx, Elem *elem)
{
return ctx.focus_id == elem.id;
}
// TODO: add other events
// FIXME: this does not work with touch
2024-12-20 19:50:58 +01:00
// FIXME: hacked together, please do better
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
2024-12-20 01:45:10 +01:00
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
2025-01-30 18:36:47 +01:00
2024-12-20 19:50:58 +01:00
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
focus = false;
if (ctx.focus_id == elem.id) ctx.focus_id = 0;
2024-12-20 01:45:10 +01:00
}
2024-12-20 19:50:58 +01:00
if (hover) { ctx.hover_id = elem.id; }
if (focus) { ctx.focus_id = elem.id; }
2025-01-30 18:36:47 +01:00
2024-12-20 01:45:10 +01:00
ElemEvents ev = {
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
2025-06-30 13:10:00 +02:00
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
2024-12-20 01:45:10 +01:00
};
return ev;
}