ugui/lib/ugui.c3l/src/core.c3

391 lines
9.5 KiB
Plaintext

module ugui;
import mtree;
import cache;
import std::io;
import std::core::string;
import std::core::mem::allocator;
import std::collections::pair;
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,
ETYPE_SPRITE,
}
bitstruct ElemFlags : uint {
bool updated : 0;
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;
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;
int z_index;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
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;
// 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
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
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;
InputData input, current_input;
Id hover_id;
Id focus_id;
Rect div_scissor; // the current div bounds used for scissor test
int active_div; // tree node indicating the current active div
}
// return a pointer to the parent of the current active div
fn Elem*? Ctx.get_parent(&ctx)
{
Id parent_id = ctx.tree[ctx.active_div]!;
Elem* parent = ctx.cache.search(parent_id)!;
return parent;
}
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
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;
}
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
{
bool is_new;
Elem* parent;
Elem* elem;
parent = ctx.get_parent() ?? &&(Elem){};
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
elem.flags.shown = true;
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.z_index = parent.z_index;
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
return {elem, parent};
}
// 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)
{
Elem*? elem;
elem = ctx.cache.search(id);
if (catch elem) {
return &&(Elem){};
}
return elem;
}
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx, Allocator allocator)
{
ctx.tree.init(MAX_ELEMENTS, allocator);
defer catch { (void)ctx.tree.free(); }
ctx.cache.init(allocator)!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(allocator::mem, MAX_COMMANDS);
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator);
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
}
fn void? Ctx.frame_begin(&ctx)
{
// 1. Reset the active div
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
// down the element tree
elem.flags.updated = ctx.input.events.resize;
// 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
//elem.flags.has_focus = ctx.has_focus;
elem.bounds = {0, 0, ctx.width, ctx.height};
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);
ctx.div_scissor = elem.bounds;
}
fn void? Ctx.frame_end(&ctx)
{
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) {
return WRONG_ID?;
}
// 2. clear input fields
ctx.input = ctx.current_input;
ctx.current_input.events = {};
ctx.current_input.keyboard.text_len = 0;
// DO THE LAYOUT
ctx.layout_element_tree()!;
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);
// 1. clear the tree
ctx.tree.nuke();
// 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;
}
// 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):
// draw mouse position
Cmd cmd = {
.type = CMD_RECT,
.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,
},
.rect.color = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.push(cmd);
$endif
// foreach (i, c: ctx.cmd_queue) {
// io::printf("[%d]: ", i);
// io::printn(c);
// }
}
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)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
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);
}
if (hover) ctx.hover_id = elem.id;
if (focus) ctx.focus_id = elem.id;
}
// 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);
e.events = {
.has_focus = focus,
.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),
.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),
};
}