From 16adfd7cc51af101ce398e0eda986f9c511a0e5e Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Wed, 25 Dec 2024 12:30:35 +0100 Subject: [PATCH] idk look at the changes --- TODO | 27 +++++++-- src/cache.c3 | 15 ++++- src/main.c3 | 30 ++++++---- src/ugui_core.c3 | 37 ++++++++++-- src/ugui_div.c3 | 13 ++++- src/ugui_font.c3 | 1 + src/ugui_input.c3 | 132 +++++++++++++++++++++++++++++++++--------- src/ugui_slider.c3 | 5 +- test/test_error.c3 | 72 +++++++++++++++++++++++ test/test_keyboard.c3 | 26 +++++++++ 10 files changed, 304 insertions(+), 54 deletions(-) create mode 100644 test/test_error.c3 create mode 100644 test/test_keyboard.c3 diff --git a/TODO b/TODO index 8c53143..556b6f5 100644 --- a/TODO +++ b/TODO @@ -7,18 +7,31 @@ [ ] Write a README.md [ ] Use an arena allocator for cache [ ] Do not redraw if there was no update (no layout and no draw) +[ ] Do command buffer damage tracking based on a context grid (see rxi writeup) [x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars) [x] Clip element bounds to parent div, specifically text -[ ] Keyboard input -[ ] Mouse scroll wheel -[ ] Touch input [ ] Resizeable divs -[ ] Implement a z index -[ ] Text reflow +[ ] Implement a z index and sort command buffer based on that [ ] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it -[ ] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division +[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division [ ] Animations, somehow +[ ] Maybe cache codepoint converted strings +[x] Fix scroll wheel when div is scrolled + +## Layout + +[ ] Text reflow +[ ] Flexbox +[ ] Center elements to the row/column + +## Input + +[x] Keyboard input +[x] Mouse scroll wheel +[ ] Touch input +[x] Do not set input event to true if the movement was zero (like no mouse movement) +[ ] Use input event flags, for example to consume the input event ## Commands @@ -49,3 +62,5 @@ _ border radius [x] Button with label [ ] Text Input box [ ] Icon Buttons +[ ] Switch + diff --git a/src/cache.c3 b/src/cache.c3 index 5b37d29..fac5cf2 100644 --- a/src/cache.c3 +++ b/src/cache.c3 @@ -62,8 +62,9 @@ fn Value*! Cache.search(&cache, Key id) // get_entry() faults on miss IdTableEntry* entry = cache.table.get_entry(id)!; - /* MISS */ + /* MISS, wrong key */ if (entry.key != id) { + cache.table.remove(id)!; return SearchResult.MISSING?; } @@ -79,6 +80,18 @@ fn Value*! Cache.search(&cache, Key id) return &(cache.pool[entry.value]); } +fn void Cache.remove(&cache, Key id) +{ + IdTableEntry*! entry = cache.table.get_entry(id); + if (catch entry) { + return; + } + + // found, remove it + cache.present[entry.value] = false; + (void)cache.table.remove(id); +} + /* Look for a free spot in the present bitmap and return its index */ /* If there is no free space left then just return the first position */ fn usz Cache.get_free_spot(&cache) @private diff --git a/src/main.c3 b/src/main.c3 index 0650e4e..be5c2f3 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -106,6 +106,7 @@ fn int main(String[] args) while (!rl::window_should_close()) { clock.mark(); +/* KeyboardKey k; do { k = rl::get_key_pressed(); @@ -115,7 +116,12 @@ fn int main(String[] args) if (rl::is_key_released(k)) { io::print("released "); } io::printfn("%s", k); } while (k != 0); - +*/ + for (int c; (c = rl::get_char_pressed()) != 0;) { + int[1] ts; + ts[0] = c; + ui.input_text_unicode(ts[..]); + } /* Start Input Handling */ if (rl::is_window_resized()) { @@ -128,11 +134,14 @@ fn int main(String[] args) rl::Vector2 mpos = rl::get_mouse_position(); ui.input_mouse_abs((short)mpos.x, (short)mpos.y); - - ugui::MouseButtons buttons; - buttons.btn_left = rl::is_mouse_button_down(rl::MOUSE_BUTTON_LEFT); - buttons.btn_right = rl::is_mouse_button_down(rl::MOUSE_BUTTON_RIGHT); - buttons.btn_middle = rl::is_mouse_button_down(rl::MOUSE_BUTTON_MIDDLE); + rl::Vector2 mwheel = rl::get_mouse_wheel_move_v(); + ui.input_mouse_wheel((short)mwheel.x, (short)mwheel.y); + + ugui::MouseButtons buttons = { + .btn_left = rl::is_mouse_button_down(rl::MOUSE_BUTTON_LEFT), + .btn_right = rl::is_mouse_button_down(rl::MOUSE_BUTTON_RIGHT), + .btn_middle = rl::is_mouse_button_down(rl::MOUSE_BUTTON_MIDDLE), + }; ui.input_mouse_button(buttons); /* End Input Handling */ @@ -196,16 +205,17 @@ fn int main(String[] args) ui.slider_hor("hs2", ugui::Rect{0,0,100,30}, &f2)!!; |}; ui.div_end()!!; - + // Timings counter TimeStats dts = draw_times.get_stats(); TimeStats uts = ui_times.get_stats(); ui.layout_set_floating()!!; - ui.div_begin("fps", ugui::Rect{0, ui.height-60, 200, 60})!!; + ui.div_begin("fps", ugui::Rect{0, ui.height-100, 200, 100})!!; {| - ui.layout_set_row()!!; - ui.text_unbounded("ui avg", string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!; + ui.layout_set_column()!!; + 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)ui.input.keyboard.text[..])!!; |}; ui.div_end()!!; diff --git a/src/ugui_core.c3 b/src/ugui_core.c3 index 3d7f50f..09c5348 100644 --- a/src/ugui_core.c3 +++ b/src/ugui_core.c3 @@ -93,6 +93,7 @@ const uint STACK_STEP = 10; const uint MAX_ELEMS = 128; const uint MAX_CMDS = 256; const uint ROOT_ID = 1; +const uint TEXT_MAX = 64; // global style, similar to the css box model struct Style { // css box model @@ -126,6 +127,13 @@ struct Ctx { // mouse_pressed = mouse_updated & mouse_down MouseButtons down; MouseButtons updated; + // scroll wheel + Point scroll; + } + struct keyboard { + char[TEXT_MAX] text; + usz text_len; + ModKeys down; } } @@ -182,18 +190,26 @@ fn Elem*! Ctx.get_parent(&ctx) return ctx.cache.search(parent_id); } +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 macro Id! Ctx.gen_id(&ctx, String label) { - Id a = ctx.tree.get(ctx.active_div)!; - Id b = label.hash(); - return (a + b) * (a + b + 1) / 2 + a; + 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); + mixed += GOLDEN_RATIO; + return mixed; } // get or push an element from the cache, return a pointer to it // resets all flags except is_new which is set accordingly -macro Ctx.get_elem(&ctx, Id id) +fn Elem*! Ctx.get_elem(&ctx, Id id) { Elem empty_elem; bool is_new; @@ -206,6 +222,18 @@ macro Ctx.get_elem(&ctx, Id id) return elem; } +// 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; +} + // 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 @@ -305,6 +333,7 @@ fn void! Ctx.frame_end(&ctx) // 2. clear input fields ctx.input.events = (InputEvents)0; + ctx.input.keyboard.text_len = 0; // send atlas updates if (ctx.font.should_update) { diff --git a/src/ugui_div.c3 b/src/ugui_div.c3 index f46eb15..1d02bdc 100644 --- a/src/ugui_div.c3 +++ b/src/ugui_div.c3 @@ -99,11 +99,14 @@ fn void! Ctx.div_end(&ctx) 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.hover_id == vsid ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0; - short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid || ctx.hover_id == hsid ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0; + 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; if (elem.div.scroll_y.on) { + if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { + elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f; + elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f); + } Rect vslider = { .x = elem.bounds.x + elem.bounds.w - wdim, .y = elem.bounds.y, @@ -117,6 +120,10 @@ fn void! Ctx.div_end(&ctx) } if (elem.div.scroll_x.on) { + if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { + elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f; + elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f); + } Rect hslider = { .x = elem.bounds.x, .y = elem.bounds.y + elem.bounds.h - hdim, diff --git a/src/ugui_font.c3 b/src/ugui_font.c3 index c9be65d..189fdef 100644 --- a/src/ugui_font.c3 +++ b/src/ugui_font.c3 @@ -76,6 +76,7 @@ fn void! Font.load(&font, String name, ZString path, uint height, float scale) font.sft.font = schrift::loadfile(path); if (font.sft.font == null) { + font.table.free(); return UgFontError.TTF_LOAD_FAILED?; } diff --git a/src/ugui_input.c3 b/src/ugui_input.c3 index a82240b..b01d383 100644 --- a/src/ugui_input.c3 +++ b/src/ugui_input.c3 @@ -1,25 +1,72 @@ module ugui; +import grapheme; import std::io; import std::math; -// TODO: this could be a bitstruct + bitstruct InputEvents : uint { - bool resize : 0; // window size was changed + bool resize : 0; // window size was changed bool change_focus : 1; // window focus changed - bool mouse_move : 2; // mouse was moved - bool mouse_btn : 3; // mouse button pressed or released + bool mouse_move : 2; // mouse was moved + bool mouse_btn : 3; // mouse button pressed or released + bool mouse_scroll : 4; // mouse scroll wheel. x or y + bool text_input : 5; + bool mod_key : 6; +} + +bitstruct MouseButtons : uint { + bool btn_left : 0; + bool btn_middle : 1; + bool btn_right : 2; + bool btn_4 : 3; + bool btn_5 : 4; +} + +// 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 +bitstruct ModKeys : uint { + bool lshift : 0; + bool rshift : 1; + bool lctrl : 2; + bool rctrl : 3; + bool lalt : 4; + bool ralt : 5; + bool lgui : 6; + bool rgui : 7; + bool num : 8; + bool caps : 9; + bool mode : 10; + bool scroll : 11; } +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_NONE = ModKeys{}; +const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max); + +const MouseButtons BTN_NONE = MouseButtons{}; +const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max); +const MouseButtons BTN_LEFT = {.btn_left = true}; +const MouseButtons BTN_MIDDLE = {.btn_middle = true}; +const MouseButtons BTN_RIGHT = {.btn_right = true}; +const MouseButtons BTN_4 = {.btn_4 = true}; +const MouseButtons BTN_5 = {.btn_5 = true}; + +const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max); + // Window size was changed fn void! Ctx.input_window_size(&ctx, short width, short height) { if (width <= 0 || height <= 0) { return UgError.INVALID_SIZE?; } + ctx.input.events.resize = ctx.width != width || ctx.height != height; ctx.width = width; ctx.height = height; - ctx.input.events.resize = true; } // Window gained/lost focus @@ -27,44 +74,26 @@ fn void Ctx.input_changefocus(&ctx, bool has_focus) { // FIXME: raylib only has an API to query the focus status so we have to // update the input flag only if the focus changed - if (ctx.has_focus != has_focus) { - ctx.input.events.change_focus = true; - } + ctx.input.events.change_focus = ctx.has_focus != has_focus; ctx.has_focus = has_focus; } -bitstruct MouseButtons : uint { - bool btn_left : 0; - bool btn_middle : 1; - bool btn_right : 2; - bool btn_4 : 3; - bool btn_5 : 4; -} - macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down; macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down; macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down; -const MouseButtons BTN_NONE = (MouseButtons)0u; -const MouseButtons BTN_ANY = (MouseButtons)(uint.max); -const MouseButtons BTN_LEFT = {.btn_left = true}; -const MouseButtons BTN_MIDDLE = {.btn_middle = true}; -const MouseButtons BTN_RIGHT = {.btn_right = true}; -const MouseButtons BTN_4 = {.btn_4 = true}; -const MouseButtons BTN_5 = {.btn_5 = true}; - // FIXME: hthis compairson could be done with a cast using MouseButtons.inner // property but I could not figure out how macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE; macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE; macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE; -// Mouse Button moved +// Mouse Buttons down fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons) { ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons; ctx.input.mouse.down = buttons; - ctx.input.events.mouse_btn = true; + ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0; } // Mouse was moved, report absolute position @@ -80,7 +109,7 @@ fn void Ctx.input_mouse_abs(&ctx, short x, short y) ctx.input.mouse.delta.x = dx; ctx.input.mouse.delta.y = dy; - ctx.input.events.mouse_move = true; + ctx.input.events.mouse_move = dx != 0 || dy != 0; } // Mouse was moved, report relative motion @@ -96,5 +125,52 @@ fn void Ctx.input_mouse_delta(&ctx, short dx, short dy) ctx.input.mouse.pos.x = math::clamp(mx, 0u16, ctx.width); ctx.input.mouse.pos.y = math::clamp(my, 0u16, ctx.height); - ctx.input.events.mouse_move = true; + ctx.input.events.mouse_move = dx != 0 || dy != 0; +} + +fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0) +{ + ctx.input.mouse.scroll.x = (short)((float)-x*scale); + ctx.input.mouse.scroll.y = (short)((float)-y*scale); + ctx.input.events.mouse_scroll = x !=0 || y != 0; +} + +// append utf-8 encoded text to the context text input +fn void Ctx.input_text_utf8(&ctx, char[] text) +{ + if (text.len == 0) { return; } + + usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; + usz len = text.len > remaining ? remaining : text.len; + char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; + s[..len-1] = text[..len-1]; + ctx.input.keyboard.text_len += len; + ctx.input.events.text_input = true; +} + +fn void Ctx.input_text_unicode(&ctx, int[] text) +{ + if (text.len == 0) { return; } + + char[32] tmp; + usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; + char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; + + usz off; + foreach (idx, cp: text) { + if (off >= remaining) { break; } + usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len); + s[off..off+enc] = tmp[..enc]; + off += enc; + } + ctx.input.keyboard.text_len += off; + + ctx.input.events.text_input = true; +} + +// Mouse Buttons down +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; } diff --git a/src/ugui_slider.c3 b/src/ugui_slider.c3 index 4641322..a51d952 100644 --- a/src/ugui_slider.c3 +++ b/src/ugui_slider.c3 @@ -130,5 +130,6 @@ fn ElemEvents! Ctx.slider_ver(&ctx, return elem.events; } -macro short calc_slider(ushort off, ushort dim, float value) => (short)off + (short)(dim * value); -macro float calc_value(ushort off, ushort mouse, ushort dim, ushort slider) => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f); +macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value); +macro float calc_value(short off, short mouse, short dim, short slider) + => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f); diff --git a/test/test_error.c3 b/test/test_error.c3 new file mode 100644 index 0000000..051c928 --- /dev/null +++ b/test/test_error.c3 @@ -0,0 +1,72 @@ +import std::io; + +struct FaultStack { + usz elem; + anyfault[16] v; +} + +fn void FaultStack.push(&fs, anyfault f) +{ + if (fs.elem < fs.v.len) { + fs.v[fs.elem++] = f; + } +} + +fn anyfault FaultStack.pop(&fs) +{ + return fs.elem > 0 ? fs.v[fs.elem-- - 1] : anyfault{}; +} + +FaultStack fs; + +fn int! err1() +{ + return IoError.OUT_OF_SPACE?; +} + +fn void! err2() +{ + return IoError.EOF?; +} + +/* +macro @unwrap(#f) +{ + $if ($typeof(#f).typeid == void!.typeid) { + if (catch err = #f) { fs.push(err); } + return; + } $else { + $typeof(#f) x = #f; + if (catch err = x) { + fs.push(err); + return $typeof(#f!!){}; + } else {return x;} + } +} +*/ + +<* +@require @typekind(#func) == OPTIONAL : `@unwrap requires an optional value` +*> +macro @unwrap(#func) +{ + anyfault exc = @catch(#func); + if (exc != anyfault{}) { + fs.push(exc); + $if $typeof(#func!!).typeid != void.typeid: + return $typeof(#func!!){}; + $else + return; + $endif + } else { + return #func!!; + } +} + +fn void main() +{ + @unwrap(err1()); + @unwrap(err2()); + + io::printfn("%s", fs.v); +} diff --git a/test/test_keyboard.c3 b/test/test_keyboard.c3 new file mode 100644 index 0000000..c9ab167 --- /dev/null +++ b/test/test_keyboard.c3 @@ -0,0 +1,26 @@ +import rl; +import std::io; + +fn int main(String[] args) +{ + short width = 800; + short height = 450; + rl::set_config_flags(rl::FLAG_WINDOW_RESIZABLE); + rl::init_window(width, height, "Ugui Test"); + rl::set_target_fps(60); + rl::enable_event_waiting(); + + // Main loop + KeyboardKey k; + while (!rl::window_should_close()) { + do { + k = rl::get_char_pressed(); + io::printfn("%s", k); + } while (k != 0); + } + + rl::close_window(); + + return 0; +} +