Compare commits

..

12 Commits

19 changed files with 963 additions and 393 deletions

View File

@ -2,12 +2,12 @@
"Debug": {
"build": [
{
"args": "",
"command": "scripts/compile_shaders.sh",
"args": "-C resources/shaders",
"command": "make",
"working_dir": ""
},
{
"args": "clean-run -g",
"args": "build -g",
"command": "c3c",
"working_dir": ""
}
@ -15,8 +15,8 @@
"build_types": [],
"clean": [
{
"args": "",
"command": "rm build/ugui",
"args": "clean",
"command": "c3c",
"working_dir": ""
}
],

2
TODO
View File

@ -64,6 +64,8 @@ to maintain focus until mouse release (fix scroll bars)
- border radius
[x] add a command to update an atlas
[ ] New window command, useful for popups
[ ] Text command returns the text bounds, this way we can avoid the pattern
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
## Atlas

View File

@ -7,72 +7,59 @@ struct ElemButton {
int filler;
}
// draw a button, return the events on that button
// FIXME: "state" should be renamed "active" to toggle between an usable button and
// an inactive (greyed-out) button
fn ElemEvents? Ctx.button(&ctx, String label, Rect size, bool state = false)
macro Ctx.button(&ctx, Rect size, bool state = false, ...)
=> ctx.button_id(@compute_id($vasplat), size, state);
fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
elem.bounds = ctx.position_element(parent, size, true);
elem.bounds = ctx.position_element(parent, size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Color col = 0x0000ffffu.to_rgba();
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
if (state) {
col = 0xff0000ffu.to_rgba();
} else if (ctx.elem_focus(elem) || elem.events.mouse_hover) {
col = 0xff00ffffu.to_rgba();
}
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
// Draw the button
ctx.push_rect(elem.bounds, col, parent.div.z_index, do_border: true, do_radius: true)!;
ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
return elem.events;
}
fn ElemEvents? Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool state = false)
macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
=> ctx.button_label_id(@compute_id($vasplat), label, size, active);
fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
// this resets the flags
elem.type = ETYPE_BUTTON;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
Rect text_size = ctx.get_text_bounds(label)!;
Rect btn_size = text_size.add({0,0,10,10});
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
// 2. Layout
elem.bounds = ctx.position_element(parent, btn_size, true);
elem.bounds = ctx.position_element(parent, btn_size, style_norm);
if (elem.bounds.is_null()) { return {}; }
Color col = 0x0000ffffu.to_rgba();
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
if (state) {
col = 0xff0000ffu.to_rgba();
} else if (ctx.elem_focus(elem) || elem.events.mouse_hover) {
col = 0xff00ffffu.to_rgba();
}
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
// Draw the button
text_size.x = elem.bounds.x;
@ -80,138 +67,121 @@ fn ElemEvents? Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,s
Point off = ctx.center_text(text_size, elem.bounds);
text_size.x += off.x;
text_size.y += off.y;
ctx.push_rect(elem.bounds, col, parent.div.z_index, do_border: true, do_radius: true)!;
ctx.push_string(text_size, label, parent.div.z_index)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
return elem.events;
}
fn ElemEvents? Ctx.button_icon(&ctx, String label, String icon, String on_icon = "", bool state = false)
macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
=> ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
Rect max_size = def_sprite.rect().max(on_sprite.rect());
elem.bounds = ctx.position_element(parent, max_size, true);
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
elem.bounds = ctx.position_element(parent, max_size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Color col = 0x0000ffffu.to_rgba();
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
Id tex_id = ctx.sprite_atlas.id;
if (state && on_icon != "") {
if (active && on_icon != "") {
ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
} else {
ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
}
// Draw the button
ctx.push_rect(elem.bounds, col, parent.div.z_index, do_border: true, do_radius: true)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
return elem.events;
}
// FIXME: this should be inside the style
const ushort DEFAULT_CHECKBOX_SIZE = 16;
fn void? Ctx.checkbox(&ctx, String label, String description, Point off, bool* state, String tick_sprite = {})
macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);
fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// FIXME: for now checkboxes and buttons have no members so the element types
// can be the same
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
Rect size = {off.x, off.y, DEFAULT_CHECKBOX_SIZE, DEFAULT_CHECKBOX_SIZE};
elem.bounds = ctx.position_element(parent, size, true);
elem.bounds = ctx.position_element(parent, size, style);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *state = !(*state);
Color col;
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
if (tick_sprite != {}) {
col = ctx.style.bgcolor;
ctx.push_rect(elem.bounds, col, parent.div.z_index, do_border: true, do_radius: true)!;
if (*state) {
ctx.draw_sprite_raw(tick_sprite, elem.bounds)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
if (*active) {
ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!;
}
} else {
if (*state) {
col = 0xff0000ffu.to_rgba();
} else {
col = 0xff00ffffu.to_rgba();
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
if (*active) {
ushort x = DEFAULT_CHECKBOX_SIZE / 4;
Rect check = elem.bounds.add({x, x, -x*2, -x*2});
Style s = *style;
s.bg = s.primary;
s.margin = s.border = s.padding = {};
ctx.push_rect(check, parent.div.z_index, &s)!;
}
// Draw the button
ctx.push_rect(elem.bounds, col, parent.div.z_index, do_border: true, do_radius: true)!;
}
}
// FIXME: this should be inside the style
const short DEFAULT_SWITCH_SIZE = 16;
fn void? Ctx.toggle(&ctx, String label, String description, Point off, bool* state)
macro Ctx.toggle(&ctx, String desc, Point off, bool* active)
=> ctx.toggle_id(@compute_id($vasplat), desc, off, active);
fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// FIXME: for now switches and buttons have no members so the element types
// can be the same
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("toggle"));
Rect size = {off.x, off.y, DEFAULT_SWITCH_SIZE*2, DEFAULT_SWITCH_SIZE};
elem.bounds = ctx.position_element(parent, size, true);
elem.bounds = ctx.position_element(parent, size, style);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *state = !(*state);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Color col;
if (*state) {
col = 0xff0000ffu.to_rgba();
} else {
col = 0xff00ffffu.to_rgba();
}
// Draw the button
// FIXME: THIS IS SHIT
ctx.push_rect(elem.bounds, ctx.style.bgcolor, parent.div.z_index, do_border: true, do_radius: true)!;
Rect t = elem.bounds.add({*state ? (DEFAULT_SWITCH_SIZE+3) : +3, +3, -DEFAULT_SWITCH_SIZE-6, -6});
ctx.push_rect(t, col, parent.div.z_index, do_border: false, do_radius: true)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
Rect t = elem.bounds.add({*active ? (DEFAULT_SWITCH_SIZE+3) : +3, +3, -DEFAULT_SWITCH_SIZE-6, -6});
Style s = *style;
s.bg = s.primary;
s.margin = s.border = s.padding = {};
ctx.push_rect(t, parent.div.z_index, &s)!;
}

View File

@ -90,21 +90,20 @@ fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
ctx.push_cmd(&sc, z_index)!;
}
// FIXME: is this really the best solution?
// "rect" is the bounding box of the element, which includes the border and the padding (so not just the content)
fn void? Ctx.push_rect(&ctx, Rect rect, Color color, int z_index, bool do_border = false, bool do_padding = false, bool do_radius = false)
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect border = ctx.style.border;
Rect padding = ctx.style.padding;
ushort radius = ctx.style.radius;
Color border_color = ctx.style.brcolor;
Rect border = style.border;
Rect padding = style.padding;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
if (do_border) {
if (!border.is_null()) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = do_radius ? radius : 0,
.rect.radius = radius,
};
ctx.push_cmd(&cmd, z_index)!;
}
@ -112,19 +111,18 @@ fn void? Ctx.push_rect(&ctx, Rect rect, Color color, int z_index, bool do_border
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + (do_border ? border.x : 0) + (do_padding ? padding.x : 0),
.y = rect.y + (do_border ? border.y : 0) + (do_padding ? padding.y : 0),
.w = rect.w - (do_border ? border.x+border.w : 0) - (do_padding ? padding.x+padding.w : 0),
.h = rect.h - (do_border ? border.y+border.h : 0) - (do_padding ? padding.y+padding.h : 0),
.x = rect.x + border.x + padding.x,
.y = rect.y + border.y + padding.y,
.w = rect.w - (border.x+border.w) - (padding.x+padding.w),
.h = rect.h - (border.y+border.h) - (padding.y+padding.h),
},
.rect.color = color,
.rect.radius = do_radius ? radius : 0,
.rect.color = bg,
.rect.radius = radius,
};
if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
ctx.push_cmd(&cmd, z_index)!;
}
// TODO: add texture id
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
{
Cmd cmd = {
@ -138,7 +136,7 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i
ctx.push_cmd(&cmd, z_index)!;
}
fn void? Ctx.push_string(&ctx, Rect bounds, String text, int z_index, Color hue = 0xffffffffu.to_rgba())
fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
{
if (text.len == 0) {
return;
@ -158,7 +156,7 @@ fn void? Ctx.push_string(&ctx, Rect bounds, String text, int z_index, Color hue
short line_len;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
Glyph* gp;
if (!ascii::is_cntrl((char)cp)) {

View File

@ -6,10 +6,11 @@ import fifo;
import std::io;
import std::core::string;
import std::core::mem::allocator;
// element ids are just long ints
alias Id = usz;
alias Id = uint;
enum ElemType {
ETYPE_NONE,
@ -34,11 +35,13 @@ bitstruct ElemEvents : uint {
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
bool text_input : 8;
}
// element structure
struct Elem {
Id id;
isz tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
@ -72,25 +75,13 @@ const uint MAX_CMDS = 2048;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bgcolor; // background color
Color fgcolor; // foreground color
Color brcolor; // border color
ushort radius;
}
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Style style;
Font font;
SpriteAtlas sprite_atlas;
@ -111,7 +102,7 @@ struct Ctx {
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys down;
ModKeys modkeys;
}
}
@ -135,10 +126,9 @@ const uint GOLDEN_RATIO = 0x9E3779B9;
// generate an id combining the hashes of the parent id and the label
// with the Cantor pairing function
macro Id? Ctx.gen_id(&ctx, String label)
fn Id? Ctx.gen_id(&ctx, Id id2)
{
Id id1 = ctx.tree.get(ctx.active_div)!;
Id id2 = label.hash();
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
@ -146,9 +136,19 @@ macro Id? Ctx.gen_id(&ctx, String label)
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 Elem*? Ctx.get_elem(&ctx, Id id)
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
{
Elem empty_elem;
bool is_new;
@ -156,8 +156,13 @@ fn Elem*? Ctx.get_elem(&ctx, Id id)
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
// FIXME: should this be here? or is it better to have the elements set the id?
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)!;
return elem;
}
@ -173,17 +178,7 @@ macro Elem* Ctx.find_elem(&ctx, Id id)
return elem;
}
// FIXME: Since ids are now keyed with the element's parent id, this function does not work
// outside of the element's div block.
// this searches an element in the cache by label, it does not create a new element
// if it does't find one
//macro Ctx.get_elem_by_label(&ctx, String label)
//{
// Id id = ctx.get_id(label);
// return ctx.cache.search(id);
//}
macro Ctx.get_elem_by_tree_idx(&ctx, isz idx) @private
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
@ -200,16 +195,10 @@ fn void? Ctx.init(&ctx)
ctx.cmd_queue.init(MAX_CMDS)!;
defer catch { (void)ctx.cmd_queue.free(); }
ctx.active_div = 0;
ctx.styles.init(allocator::heap());
defer catch { ctx.styles.free(); }
// TODO: add style config
ctx.style.margin = {2, 2, 2, 2};
ctx.style.border = {2, 2, 2, 2};
ctx.style.padding = {1, 1, 1, 1};
ctx.style.radius = 12;
ctx.style.bgcolor = 0x282828ffu.to_rgba();
ctx.style.fgcolor = 0xfbf1c7ffu.to_rgba();
ctx.style.brcolor = 0xd79921ffu.to_rgba();
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
@ -219,15 +208,18 @@ fn void Ctx.free(&ctx)
(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
Elem* elem = ctx.get_elem(ROOT_ID)!;
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frames, this propagates an element size recalculation
// 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
@ -235,28 +227,15 @@ fn void? Ctx.frame_begin(&ctx)
// other stuff
//elem.flags.has_focus = ctx.has_focus;
Elem def_root = {
.id = ROOT_ID,
.type = ETYPE_DIV,
.bounds = {
.w = ctx.width,
.h = ctx.height,
},
.div = {
.layout = LAYOUT_ROW,
.z_index = 0,
.children_bounds = {
.w = ctx.width,
.h = ctx.height,
}
},
.flags = elem.flags,
};
*elem = def_root;
// 3. Push the root element into the element tree
ctx.active_div = ctx.tree.add(ROOT_ID, 0)!;
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 = {};
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
@ -268,7 +247,8 @@ const int DEBUG = 1;
fn void? Ctx.frame_end(&ctx)
{
Elem* root = ctx.get_elem_by_tree_idx(0)!;
// 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()!;
root.div.layout = LAYOUT_ROW;
// 1. clear the tree
@ -343,6 +323,7 @@ fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
.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),
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
return ev;
}

View File

@ -29,22 +29,18 @@ struct ElemDiv {
// if the width or height are negative the width or height will be calculated based on the children size
// sort similar to a flexbox, and the minimum size is set by the negative of the width or height
// FIXME: there is a bug if the size.w or size.h == -0
fn void? Ctx.div_begin(&ctx, String label, Rect size, bool scroll_x = false, bool scroll_y = false)
macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...)
=> ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y);
fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id)!;
isz div_node = ctx.tree.add(id, ctx.active_div)!;
ctx.active_div = div_node;
Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
ctx.active_div = elem.tree_idx;
bool is_new = elem.flags.is_new;
Style* style = ctx.styles.get_style(0);
if (elem.flags.is_new) {
elem.type = ETYPE_DIV;
} else if (elem.type != ETYPE_DIV) {
return WRONG_ELEMENT_TYPE?;
}
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.div.z_index = parent.div.z_index + 1;
@ -56,7 +52,7 @@ fn void? Ctx.div_begin(&ctx, String label, Rect size, bool scroll_x = false, boo
.w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
.h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
};
elem.bounds = ctx.position_element(parent, wanted_size);
elem.bounds = ctx.position_element(parent, wanted_size, style);
elem.div.children_bounds = {};
// update the ctx scissor
@ -73,7 +69,7 @@ fn void? Ctx.div_begin(&ctx, String label, Rect size, bool scroll_x = false, boo
// Add the background to the draw stack
bool do_border = parent.div.layout == LAYOUT_FLOATING;
ctx.push_rect(elem.bounds, ctx.style.bgcolor, elem.div.z_index, do_border: do_border)!;
ctx.push_rect(elem.bounds, elem.div.z_index, style)!;
elem.events = ctx.get_elem_events(elem);
@ -85,7 +81,7 @@ fn void? Ctx.div_end(&ctx)
{
// swap the children bounds
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem_by_tree_idx(ctx.active_div)!;
Elem* elem = ctx.get_active_div()!;
elem.div.pcb = elem.div.children_bounds;
// FIXME: this causes all elements inside the div to loose focus since the mouse press happens
@ -110,10 +106,12 @@ fn void? Ctx.div_end(&ctx)
// vertical overflow
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
Id hsid = ctx.gen_id("div_scrollbar_horizontal")!;
Id vsid = ctx.gen_id("div_scrollbar_vertical")!;
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid || ctx.is_hovered(ctx.find_elem(vsid)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0;
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid || ctx.is_hovered(ctx.find_elem(hsid)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0;
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
Id vsid_raw = @str_hash("div_scrollbar_vertical");
Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0;
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
@ -128,7 +126,7 @@ fn void? Ctx.div_end(&ctx)
};
Layout prev_l = elem.div.layout;
elem.div.layout = LAYOUT_ABSOLUTE;
ctx.slider_ver("div_scrollbar_vertical", vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
elem.div.layout = prev_l;
}
@ -145,7 +143,7 @@ fn void? Ctx.div_end(&ctx)
};
Layout prev_l = elem.div.layout;
elem.div.layout = LAYOUT_ABSOLUTE;
ctx.slider_hor("div_scrollbar_horizontal", hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
elem.div.layout = prev_l;
}

View File

@ -211,6 +211,34 @@ fn Rect? Ctx.get_text_bounds(&ctx, String text)
return text_bounds;
}
fn Point? Ctx.get_cursor_position(&ctx, String text)
{
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
Glyph* gp;
// TODO: account for unicode codepoints
Point line;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
bool n;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
line.x += gp.adv;
} else if (cp == '\n'){
line.y += line_height + line_gap;
line.x = 0;
} else {
continue;
}
}
return line;
}
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
{
short dw = bounds.w - text_bounds.w;
@ -234,3 +262,5 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
return &ctx.font.atlas;
}
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

View File

@ -25,7 +25,7 @@ bitstruct MouseButtons : uint {
// FIXME: all of these names were prefixed with key_ idk if this is better,
// if it is remove the prefix on MouseButtons as well
// Modifier Keys, same as SDL
// Modifier Keys, intended as any key that is not text
bitstruct ModKeys : uint {
bool lshift : 0;
bool rshift : 1;
@ -39,12 +39,20 @@ bitstruct ModKeys : uint {
bool caps : 9;
bool mode : 10;
bool scroll : 11;
bool bkspc : 12;
bool del : 13;
// arrow keys
bool up : 14;
bool down : 15;
bool left : 16;
bool right : 17;
}
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true};
const ModKeys KMOD_ALT = {.lalt = true, .ralt = true};
const ModKeys KMOD_GUI = {.lgui = true, .rgui = true};
const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input
const ModKeys KMOD_NONE = {};
const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max);
@ -60,7 +68,7 @@ const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys)
{
bool is_mod = (bool)(ctx.input.keyboard.down & mod);
bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
bool is_keys = true;
String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len];
char[2] needle;
@ -187,9 +195,10 @@ fn void Ctx.input_char(&ctx, char c)
ctx.input_text_utf8(b[..]);
}
// Mouse Buttons down
// Modifier keys, like control or backspace
// TODO: make this call repetible to input modkeys one by one
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
{
ctx.input.keyboard.down = modkeys;
ctx.input.events.mod_key = (uint)ctx.input.keyboard.down != 0;
ctx.input.keyboard.modkeys = modkeys;
ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0;
}

View File

@ -106,7 +106,7 @@ macro Point Elem.get_view_off(&elem)
@require ctx != null
@require parent.type == ETYPE_DIV
*>
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, bool style = false)
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
{
ElemDiv* div = &parent.div;
@ -142,25 +142,25 @@ fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, bool style = false)
// offset placement and area
child_placement = child_placement.off(origin.add(rect.position()));
child_occupied = child_occupied.off(origin.add(rect.position()));
if (style) {
Rect margin = ctx.style.margin;
Rect border = ctx.style.border;
Rect padding = ctx.style.padding;
// padding, grows both the placement and occupied area
child_placement = child_placement.grow(padding.position().add(padding.size()));
child_occupied = child_occupied.grow(padding.position().add(padding.size()));
// border, grows both the placement and occupied area
child_placement = child_placement.grow(border.position().add(border.size()));
child_occupied = child_occupied.grow(border.position().add(border.size()));
// margin, offsets the placement and grows the occupied area
child_placement = child_placement.off(margin.position());
child_occupied = child_occupied.grow(margin.position().add(margin.size()));
Rect margin = style.margin;
Rect border = style.border;
Rect padding = style.padding;
// padding, grows both the placement and occupied area
child_placement = child_placement.grow(padding.position().add(padding.size()));
child_occupied = child_occupied.grow(padding.position().add(padding.size()));
// border, grows both the placement and occupied area
child_placement = child_placement.grow(border.position().add(border.size()));
child_occupied = child_occupied.grow(border.position().add(border.size()));
// margin, offsets the placement and grows the occupied area
child_placement = child_placement.off(margin.position());
child_occupied = child_occupied.grow(margin.position().add(margin.size()));
// oh yeah also adjust the rect if i was to grow
if (adapt_x) rect.w -= padding.x+padding.w + border.x+border.w + margin.x+margin.w;
if (adapt_y) rect.h -= padding.y+padding.h + border.y+border.h + margin.y+margin.h;
// oh yeah also adjust the rect if i was to grow
if (adapt_x) rect.w -= padding.x+padding.w + border.x+border.w + margin.x+margin.w;
if (adapt_y) rect.h -= padding.y+padding.h + border.y+border.h + margin.y+margin.h;
}
// set the size
child_placement = child_placement.grow(rect.size());
child_occupied = child_occupied.grow(rect.size());

View File

@ -203,6 +203,17 @@ macro Color uint.to_rgba(u)
};
}
macro Color uint.@to_rgba($u)
{
return {
.r = (char)(($u >> 24) & 0xff),
.g = (char)(($u >> 16) & 0xff),
.b = (char)(($u >> 8) & 0xff),
.a = (char)(($u >> 0) & 0xff)
};
}
macro uint Color.to_uint(c)
{
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);

View File

@ -8,38 +8,27 @@ struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor(&ctx,
String label,
Rect size,
float* value,
float hpercent = 0.25,
Color bgcolor = 0x0000ffffu.to_rgba(),
Color handlecolor = 0x0ff000ffu.to_rgba())
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
if (elem.flags.is_new) {
elem.type = ETYPE_SLIDER;
} else if (elem.type != ETYPE_SLIDER) {
return WRONG_ELEMENT_TYPE?;
}
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 2. Layout
elem.bounds = ctx.position_element(parent, size, true);
elem.bounds = ctx.position_element(parent, size, style);
// handle width
short hw = (short)(elem.bounds.w * hpercent);
@ -61,38 +50,36 @@ fn ElemEvents? Ctx.slider_hor(&ctx,
}
// Draw the slider background and handle
ctx.push_rect(elem.bounds, bgcolor, parent.div.z_index)!;
ctx.push_rect(elem.slider.handle, handlecolor, parent.div.z_index)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
Style s = *style;
s.bg = s.primary;
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}
/*
* +-+
* | |
* | |
* +-+
* |#| handle
* |#|
* +-+
* | |
* | |
* +-+
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
fn ElemEvents? Ctx.slider_ver(&ctx,
String label,
Rect size,
float* value,
float hpercent = 0.25,
Color bgcolor = 0x0000ffffu.to_rgba(),
Color handlecolor = 0x0ff000ffu.to_rgba())
macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 1. Fill the element fields
if (elem.flags.is_new) {
@ -102,7 +89,7 @@ fn ElemEvents? Ctx.slider_ver(&ctx,
}
// 2. Layout
elem.bounds = ctx.position_element(parent, size, true);
elem.bounds = ctx.position_element(parent, size, style);
// handle height
short hh = (short)(elem.bounds.h * hpercent);
@ -124,8 +111,10 @@ fn ElemEvents? Ctx.slider_ver(&ctx,
}
// Draw the slider background and handle
ctx.push_rect(elem.bounds, bgcolor, parent.div.z_index)!;
ctx.push_rect(elem.slider.handle, handlecolor, parent.div.z_index)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
Style s = *style;
s.bg = s.primary;
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}

View File

@ -110,27 +110,23 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
}
fn void? Ctx.draw_sprite(&ctx, String label, String name, Point off = {0,0})
macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...)
=> ctx.sprite_id(@compute_id($vasplat), name, off);
fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
if (elem.flags.is_new) {
elem.type = ETYPE_SPRITE;
} else if (elem.type != ETYPE_SPRITE) {
return WRONG_ELEMENT_TYPE?;
}
Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
Style* style = ctx.styles.get_style(0);
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h };
Rect bounds = { 0, 0, sprite.w, sprite.h };
elem.bounds = ctx.position_element(parent, bounds.off(off), true);
elem.bounds = ctx.position_element(parent, bounds.off(off), style);
elem.sprite.id = ctx.get_sprite_atlas_id(name);
// if the bounds are null the element is outside the div view,
@ -142,10 +138,16 @@ fn void? Ctx.draw_sprite(&ctx, String label, String name, Point off = {0,0})
return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!;
}
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds)
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false)
{
Elem *parent = ctx.get_parent()!;
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Id tex_id = ctx.sprite_atlas.id;
if (center) {
Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2};
bounds = bounds.off(off);
}
return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
}

View File

@ -0,0 +1,426 @@
module ugui;
import std::collections::map;
import std::core::mem::allocator;
import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bg; // background color
Color fg; // foreground color
Color primary; // primary color
Color secondary; // secondary color
Color accent; // accent color
ushort radius;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
.radius = 12,
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
alias StyleMap = map::HashMap{Id, Style};
// push or update a new style into the map
fn void StyleMap.register_style(&map, Style* style, Id id)
{
if (style == null) return;
map.set(id, *style);
}
// get a style from the map, if the style is not found then use a default style.
fn Style* StyleMap.get_style(&map, Id id)
{
Style*? s = map.get_ref(id);
if (catch e = s) {
return &DEFAULT_STYLE;
}
return s;
}
fn int StyleMap.import_style_string(&map, String text)
{
Parser p;
p.lex.text = text;
int added;
while (p.parse_style() == true) {
added++;
// set the default style correctly
if (p.style_id == @str_hash("default")) p.style_id = 0;
map.register_style(&p.style, p.style_id);
if (p.lex.peep_token().type == EOF) break;
}
return added;
}
fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
fn int Ctx.import_style_from_file(&ctx, String path)
{
char[] text;
usz size = file::get_size(path)!!;
text = mem::new_array(char, size);
file::load_buffer(path, text)!!;
defer mem::free(text);
int added = ctx.import_style_from_string((String)text);
return added;
}
// TODO: add a "size" property that controls the size of elements like checkboxes
/*
* Style can be serialized and deserialized with a subset of CSS
* <style name> {
* padding: left right top bottom;
* border: left right top bottom;
* margin: left right top bottoms;
* radius: uint;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* }
* The field "style name" will be hashed and the hash used as the id int the style map.
* Fields may be excluded, each excluded field is set to zero.
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
* factor that has to be obtained with the window manager functions.
*/
module ugui::css;
import std::ascii;
import std::io;
// CSS parser module
enum TokenType {
INVALID,
IDENTIFIER,
PUNCT,
NUMBER,
COLOR,
EOF,
}
enum Unit {
PIXELS,
MILLIMETERS
}
struct Token {
TokenType type;
usz line, col, off;
String text;
union {
struct {
float value;
Unit unit;
}
Color color;
}
}
fn short Token.to_px(&t, float mm_to_px)
{
if (t.type != NUMBER) {
unreachable("WFT you cannot convert to pixels a non-number");
}
if (t.unit == PIXELS) return (short)(t.value);
return (short)(t.value * mm_to_px);
}
struct Lexer {
String text;
usz line, col, off;
}
macro char Lexer.peep(&lex) => lex.text[lex.off];
fn char Lexer.advance(&lex)
{
if (lex.off >= lex.text.len) return '\0';
char c = lex.text[lex.off];
if (c == '\n') {
lex.col = 0;
lex.line++;
} else {
lex.col++;
}
lex.off++;
return c;
}
fn Token Lexer.next_token(&lex)
{
Token t;
t.type = INVALID;
t.off = lex.off;
t.col = lex.col;
t.line = lex.line;
if (lex.off >= lex.text.len) {
return {.type = EOF};
}
// skip whitespace
while (ascii::is_space_m(lex.peep())) {
if (lex.advance() == 0) return {.type = EOF};
if (lex.off >= lex.text.len) return {.type = EOF};
}
t.off = lex.off;
switch (true) {
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
t.type = PUNCT;
t.text = lex.text[lex.off:1];
if (lex.advance() == 0) { t.type = INVALID; break; }
case lex.peep() == '#': // color
t.type = COLOR;
if (lex.advance() == 0) { t.type = INVALID; break; }
usz hex_start = t.off+1;
while (ascii::is_alnum_m(lex.peep())) {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
if (lex.off - hex_start != 8) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2..] = lex.text[hex_start..lex.off-1];
uint? color_hex = ((String)hex_str[..]).to_uint();
if (catch color_hex) {
t.type = INVALID;
break;
}
t.color = color_hex.to_rgba();
case ascii::is_alpha_m(lex.peep()): // identifier
t.type = IDENTIFIER;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
t.text = lex.text[t.off..lex.off-1];
case ascii::is_digit_m(lex.peep()): // number
t.type = NUMBER;
t.unit = PIXELS;
// find the end of the number
usz end;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
end = lex.off;
if (end - t.off > 2) {
if (lex.text[end-2:2] == "px") {
t.unit = PIXELS;
end -= 2;
} else if (lex.text[end-2:2] == "mm") {
t.unit = MILLIMETERS;
end -= 2;
} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
io::eprintn("units 'em' or 'pt' are not supported at the moment");
t.type = INVALID;
break;
}
}
String number_str = lex.text[t.off..end-1];
float? value = number_str.to_float();
if (catch value) { t.type = INVALID; break; }
t.value = value;
t.text = lex.text[t.off..lex.off-1];
}
if (t.type == INVALID) {
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
}
return t;
}
fn Token Lexer.peep_token(&lex)
{
Lexer start_state = *lex;
Token t;
t = lex.next_token();
*lex = start_state;
return t;
}
struct Parser {
Lexer lex;
Style style;
Id style_id;
float mm_to_px;
}
macro bool Parser.expect_text(&p, Token* t, TokenType type, String text)
{
*t = p.lex.next_token();
if (t.type == type && t.text == text) {
return true;
}
io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
t.line, t.col, type, text, t.type, t.text);
return false;
}
macro bool Parser.expect(&p, Token* t, TokenType type)
{
*t = p.lex.next_token();
if (t.type == type) return true;
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
return false;
}
fn bool Parser.parse_style(&p)
{
Token t;
p.style = {};
p.style_id = 0;
// style name
if (p.expect(&t, IDENTIFIER) == false) return false;
p.style_id = t.text.hash();
// style body
if (p.expect_text(&t, PUNCT, "{") == false) return false;
while (true) {
if (p.parse_property() == false) return false;
t = p.lex.peep_token();
if (t.type != IDENTIFIER) break;
}
if (p.expect_text(&t, PUNCT, "}") == false) return false;
return true;
}
fn bool Parser.parse_property(&p)
{
Token t, prop;
if (p.expect(&prop, IDENTIFIER) == false) return false;
if (p.expect_text(&t, PUNCT, ":") == false) return false;
switch (prop.text) {
case "padding":
Rect padding;
if (p.parse_size(&padding) == false) return false;
p.style.padding = padding;
case "border":
Rect border;
if (p.parse_size(&border) == false) return false;
p.style.border = border;
case "margin":
Rect margin;
if (p.parse_size(&margin) == false) return false;
p.style.margin = margin;
case "bg":
Color bg;
if (p.parse_color(&bg) == false) return false;
p.style.bg = bg;
case "fg":
Color fg;
if (p.parse_color(&fg) == false) return false;
p.style.fg = fg;
case "primary":
Color primary;
if (p.parse_color(&primary) == false) return false;
p.style.primary = primary;
case "secondary":
Color secondary;
if (p.parse_color(&secondary) == false) return false;
p.style.secondary = secondary;
case "accent":
Color accent;
if (p.parse_color(&accent) == false) return false;
p.style.accent = accent;
case "radius":
short r;
if (p.parse_number(&r) == false) return false;
if (r < 0) {
io::eprintfn("CSS parsing error at %d:%d: radius must be a positive number, got %d", t.line, t.col, r);
return false;
}
p.style.radius = (ushort)r;
default:
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
return false;
}
if (p.expect_text(&t, PUNCT, ";") == false) return false;
return true;
}
fn bool Parser.parse_number(&p, short* n)
{
Token t;
if (p.expect(&t, NUMBER) == false) return false;
*n = t.to_px(p.mm_to_px);
return true;
}
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
fn bool Parser.parse_color(&p, Color* c)
{
Token t;
if (p.expect(&t, COLOR) == false) return false;
*c = t.color;
return true;
}
fn bool Parser.parse_size(&p, Rect* r)
{
short x;
Token t;
if (p.parse_number(&x) == false) return false;
t = p.lex.peep_token();
if (t.type == NUMBER) {
// we got another number so we expect three more
r.x = x;
if (p.parse_number(&x) == false) return false;
r.y = x;
if (p.parse_number(&x) == false) return false;
r.w = x;
if (p.parse_number(&x) == false) return false;
r.h = x;
return true;
} else if (t.type == PUNCT && t.text == ";") {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;
}
return false;
}

View File

@ -3,28 +3,88 @@ module ugui;
import std::io;
struct ElemText {
char* str;
char[] str;
usz cursor; // cursor offset
}
fn void? Ctx.text_unbounded(&ctx, String label, String text)
macro Ctx.text_unbounded(&ctx, String text, ...)
=> ctx.text_unbounded_id(@compute_id($vasplat), text);
fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
{
Id id = ctx.gen_id(label)!;
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(0);
// 1. Fill the element fields
// this resets the flags
elem.type = ETYPE_TEXT;
elem.text.str = text;
// if the element is new or the parent was updated then redo layout
Rect text_size = ctx.get_text_bounds(text)!;
// 2. Layout
elem.bounds = ctx.position_element(parent, text_size, true);
elem.bounds = ctx.position_element(parent, text_size, style);
if (elem.bounds.is_null()) { return; }
ctx.push_string(elem.bounds, text, parent.div.z_index)!;
ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
=> ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(0);
elem.text.str = text;
// layout the text box
elem.bounds = ctx.position_element(parent, size, style);
// check input and update the text
elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input) {
usz l = ctx.input.keyboard.text_len;
char[] t = ctx.input.keyboard.text[..l];
if (l != 0 && l < text.len - *text_len) {
text[*text_len..*text_len+l] = t[..];
*text_len += l;
}
if (ctx.input.keyboard.modkeys.bkspc) {
*text_len = *text_len > 0 ? *text_len-1 : 0;
}
}
elem.text.cursor = *text_len;
// draw the box
short line_height = (short)ctx.font.line_height();
Rect text_box = elem.bounds.sub({0,0,0,line_height});
Rect input_box = {
.x = elem.bounds.x,
.y = elem.bounds.y + elem.bounds.h - line_height,
.w = elem.bounds.w,
.h = line_height,
};
Rect cursor;
Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!;
cursor = {
.x = b.x,
.y = b.y,
.w = 3,
.h = line_height,
};
cursor = cursor.off(elem.bounds.position());
ctx.push_rect(text_box, parent.div.z_index, style)!;
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!;
ctx.push_rect(input_box, parent.div.z_index, style)!;
ctx.push_rect(cursor, parent.div.z_index, style)!;
return elem.events;
}

View File

@ -0,0 +1,33 @@
SOURCE_DIR := ./source
COMPILED_DIR := ./compiled
SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl)
COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES))
all: $(COMPILED_FILES)
@echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)"
$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl
@mkdir -p $(COMPILED_DIR)
@stage=$$(basename $< .glsl | cut -d. -f2); \
if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \
echo "$$stage $(notdir $<) > $(notdir $@)"; \
glslc -O0 -g -fshader-stage=$$stage $< -o $@; \
else \
echo "Skipping $<: unsupported stage $$stage"; \
fi
$(COMPILED_DIR):
mkdir -p $(COMPILED_DIR)
.PHONY: clean
clean:
rm -rf $(COMPILED_DIR)
.PHONY: tree
tree:
tree $(COMPILED_DIR)
.PHONY: compile_all
compile_all: clean all tree

View File

@ -1,41 +0,0 @@
#!/bin/sh
source_directory="./resources/shaders/source"
compiled_directory="./resources/shaders/compiled"
#vulkan_version="1.0"
mkdir -p "$compiled_directory"
rm -f "$compiled_directory"/*
echo "Compiling from $source_directory -> $compiled_directory"
for file in "$source_directory"/*; do
[ -f "$file" ] || continue # Skip non-files
filename=$(basename "$file")
# Extract filename parts using POSIX parameter expansion
shader_language="${filename##*.}"
stage_part="${filename%.*}" # Remove extension
base_name="${stage_part%.*}" # Remove stage
stage="${stage_part#"$base_name"}"
stage="${stage#.}" # Remove leading dot
# Skip if not in base.stage.glsl format
[ "$shader_language" = "glsl" ] && [ -n "$base_name" ] && [ -n "$stage" ] || continue
# Handle HLSL rejection
if [ "$shader_language" != "glsl" ]; then
echo "Error: Only GLSL shaders are supported" >&2
exit 1
fi
# Compile based on shader stage
case "$stage" in
frag|vert)
echo "$stage $filename > $base_name.$stage.spv"
glslc -O0 -g -fshader-stage="$stage" "$file" -o "$compiled_directory/$base_name.$stage.spv"
;;
esac
done
tree "$compiled_directory"

View File

@ -54,6 +54,77 @@ const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv";
const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv";
const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv";
const String STYLESHEET = `
default {
bg: #282828ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
button {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
radius: 10;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
button-active {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
radius: 10;
bg: #504945ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #cc241dff;
accent: #fabd2fff;
}
checkbox {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
radius: 10;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
toggle {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
radius: 10;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
slider {
margin: 2 2 2 2;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
`;
fn int main(String[] args)
{
@ -62,9 +133,9 @@ fn int main(String[] args)
defer ui.free();
ren::Renderer ren;
ren.init("Ugui Test", 640, 480, true);
ren.init("Ugui Test", 800, 600, true);
defer ren.free();
ui.input_window_size(640, 480)!!;
ui.input_window_size(800, 600)!!;
//
// FONT LOADING
@ -113,6 +184,9 @@ fn int main(String[] args)
ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
// CSS INPUT
io::printfn("imported %d styles", ui.import_style_from_string(STYLESHEET));
isz frame;
double fps;
bool toggle = true;
@ -145,6 +219,7 @@ fn int main(String[] args)
case EVENT_KEY_DOWN:
mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
// pressing ctrl+key or alt+key does not generate a character as such no
// TEXT_INPUT event is generated. When those keys are pressed we have to
@ -154,6 +229,8 @@ fn int main(String[] args)
ui.input_char((char)e.key.key);
}
}
if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
case EVENT_TEXT_INPUT:
ui.input_text_utf8(e.text.text.str_view());
@ -192,77 +269,72 @@ fn int main(String[] args)
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
ui.div_begin("main", {.w=-100})!!;
ui.div_begin({.w=-100})!!;
{
ui.layout_set_column()!!;
if (ui.button("button0", {0,0,30,30}, toggle)!!.mouse_press) {
if (ui.button({0,0,30,30}, toggle)!!.mouse_press) {
io::printn("press button0");
toggle = !toggle;
}
//ui.layout_next_column()!!;
if (ui.button("button1", {0,0,30,30})!!.mouse_press) {
if (ui.button({0,0,30,30})!!.mouse_press) {
io::printn("press button1");
}
//ui.layout_next_column()!!;
if (ui.button("button2", {0,0,30,30})!!.mouse_release) {
if (ui.button({0,0,30,30})!!.mouse_release) {
io::printn("release button2");
}
ui.layout_set_row()!!;
ui.layout_next_row()!!;
static float rf, gf, bf, af;
ui.slider_ver("slider_r", {0,0,30,100}, &rf)!!;
ui.slider_ver("slider_g", {0,0,30,100}, &gf)!!;
ui.slider_ver("slider_b", {0,0,30,100}, &bf)!!;
ui.slider_ver("slider_a", {0,0,30,100}, &af)!!;
ui.slider_ver({0,0,30,100}, &rf)!!;
ui.slider_ver({0,0,30,100}, &gf)!!;
ui.slider_ver({0,0,30,100}, &bf)!!;
ui.slider_ver({0,0,30,100}, &af)!!;
ui.layout_next_column()!!;
ui.text_unbounded("text1", "Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
ui.layout_next_column()!!;
ui.button_label("Continua!")!!;
ui.layout_next_row()!!;
static bool check;
ui.checkbox("check1", "", {}, &check, "tick")!!;
ui.toggle("toggle1", "", {}, &toggle)!!;
/*
ui.layout_set_column()!!;
ui.button_label(" A ")!!;
ui.button_label(" B ")!!;
ui.layout_next_column()!!;
ui.button_label(" C ")!!;
ui.button_label(" D ")!!;
ui.layout_next_row()!!;
ui.button_label(" E ")!!;
*/
ui.checkbox("", {}, &check, "tick")!!;
ui.checkbox("", {}, &check)!!;
ui.toggle("", {}, &toggle)!!;
};
ui.draw_sprite("sprite1", "tux")!!;
ui.sprite("tux")!!;
static char[128] text_box = "ciao mamma";
static usz text_len = "ciao mamma".len;
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
ui.div_end()!!;
ui.div_begin("second", ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
{
ui.layout_set_column()!!;
static float slider2 = 0.5;
if (ui.slider_ver("slider", {0,0,30,100}, &slider2)!!.update) {
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
io::printfn("other slider: %f", slider2);
}
ui.button("button0", {0,0,50,50})!!;
ui.button("button1", {0,0,50,50})!!;
ui.button("button2", {0,0,50,50})!!;
ui.button("button3", {0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
if (toggle) {
ui.button("button4", {0,0,50,50})!!;
ui.button("button5", {0,0,50,50})!!;
ui.button("button6", {0,0,50,50})!!;
ui.button("button7", {0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
}
ui.layout_next_column()!!;
ui.layout_set_row()!!;
static float f1, f2;
ui.slider_hor("hs1", {0,0,100,30}, &f1)!!;
ui.slider_hor("hs2", {0,0,100,30}, &f2)!!;
ui.slider_hor({0,0,100,30}, &f1)!!;
ui.slider_hor({0,0,100,30}, &f2)!!;
};
ui.div_end()!!;
@ -271,16 +343,16 @@ fn int main(String[] args)
TimeStats uts = ui_times.get_stats();
ui.layout_set_floating()!!;
ui.div_begin("fps", {0, ui.height-150, -300, 150})!!;
// FIXME: I cannot anchor shit to the bottom of the screen
ui.div_begin({0, ui.height-150, -300, 150})!!;
{
ui.layout_set_column()!!;
ui.text_unbounded("frame number", string::tformat("frame %d, fps = %.2f", frame, fps))!!;
ui.text_unbounded("draw times", string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
ui.text_unbounded("ui text input", string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
};
ui.div_end()!!;
ui.frame_end()!!;
/* End UI Handling */
ui_times.push(clock.mark());

View File

@ -522,18 +522,18 @@ enum TextureType : (GPUTextureFormat format) {
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
{
$switch $typeof(name_or_id):
$case usz: return self.new_texture_by_id(id, type, pixels, width, height);
$case uint: return self.new_texture_by_id(id, type, pixels, width, height);
$case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height);
$default: unreachable("texture must have a name (String) or an id (usz)");
$default: unreachable("texture must have a name (String) or an id (uint)");
$endswitch
}
macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
{
$switch $typeof(name_or_id):
$case usz: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
$case uint: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
$case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y);
$default: unreachable("texture must have a name (String) or an id (usz)");
$default: unreachable("texture must have a name (String) or an id (uint)");
$endswitch
}

30
test/test_idgen.c3 Normal file
View File

@ -0,0 +1,30 @@
import std::io;
alias Id = uint;
fn void foo_ex(Id id)
{
io::printfn("id = %d", id);
}
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;
}
macro foo(...) => foo_ex(@compute_id($vasplat));
fn int main()
{
foo_ex(1234);
foo();
foo();
foo();
return 0;
}