started working on proper UI for editor
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 8 Feb 2022 04:24:50 +0000 (22:24 -0600)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 8 Feb 2022 04:24:50 +0000 (22:24 -0600)
src/build.onyx
src/entity/editor.onyx
src/entity/manager.onyx
src/gfx/font.onyx
src/gfx/ui.onyx [new file with mode: 0644]
src/main.onyx
src/utils/input.onyx

index bc08fffa9a8ffb206e28b2e2ff956d1a15397ce5..5fd8821c3d3e9b636fbcd74ed44f523bd7bbba9b 100644 (file)
@@ -26,6 +26,7 @@ DEBUG :: false
 #load "gfx/mesh"
 #load "gfx/shader"
 #load "gfx/texture"
+#load "gfx/ui"
 
 #load "utils/input"
 #load "utils/vecmath"
index 6c71db020774bd5347734c72aaa6756011a938d1..6c2d5a3ed34af15ce52f13a68932b74ca7620557 100644 (file)
@@ -30,6 +30,9 @@ editor_toggle :: () {
 
 editor_update :: (dt: f32) {
     move_towards(^editor_openness, editor_target_openness, dt * 6);
+    
+    if is_key_just_up(GLFW_KEY_1) do clicked_tab = .Create;
+    if is_key_just_up(GLFW_KEY_2) do clicked_tab = .Edit;
 
     handle_clicking_tab(dt);
     switch active_tab {
@@ -139,26 +142,12 @@ editor_draw :: () {
             // Don't draw the "None" item;
             if it.value == 0 do continue;
 
-            contains_mouse := Rect.contains(.{x, y, w, h}, mouse_get_position_vector());
-            if contains_mouse && is_button_down(GLFW_MOUSE_BUTTON_LEFT) {
-                clicked_tab = ~~ it.value;
+            theme := Button_Theme.{};
+            theme.active = active_tab == ~~it.value;
+            if draw_button(.{x, y, w, h}, it.name, ^theme, increment=~~it.value) {
+                clicked_tab = ~~it.value;
             }
 
-            if ~~it.value == active_tab {
-                immediate_set_color(.{0.4, 0.4, 0.5, 1});
-            } else {
-                // This would be nice to have some kind of lerping, but that would require
-                // a lot more structure to this, which I don't think will be worth it.
-                if contains_mouse {
-                    immediate_set_color(.{0.3, 0.3, 0.3, 1});
-                } else {
-                    immediate_set_color(.{0.2, 0.2, 0.2, 1});
-                }
-            }
-            immediate_rectangle(x, y, w, h);
-
-            text_width := font_get_width(editor_big_font, it.name);
-            font_draw(editor_big_font, x + (w - text_width) / 2, y + h / 2 + 24 / 4, it.name);
             x += w + 2;
         }
     }
@@ -195,23 +184,24 @@ editor_draw :: () {
         type := it.key;
         info := cast(^type_info.Type_Info_Struct) type_info.get_type_info(type);
 
-        if active_index == i {
-            immediate_set_color(.{.5,.5,.3});
-        } elseif Rect.contains(.{x, y, w, 36.0f}, mouse_get_position_vector()) {
-            immediate_set_color(.{.4,.4,.4});
-
-            if is_button_just_down(GLFW_MOUSE_BUTTON_LEFT) {
-                active_index = i;
-            }
-        } else {
-            immediate_set_color(.{.3,.3,.3});
+        y += 40.0f;
+        theme := Button_Theme.{};
+        theme.active = active_index == i;
+        if draw_button(.{x + 4, y - 18, w, 36.0f}, info.name, ^theme, increment=i) {
+            active_index = i;
         }
+    }
 
-        immediate_rectangle(x, y, w, 36.0f);
-
-        y += 40.0f;
-        font_draw_centered(editor_font, x + 4, y - 18.0f, w, info.name);
+    #persist test : [..] u8;
+    if test.data == null {
+        test << #char "t";
+        test << #char "e";
+        test << #char "s";
+        test << #char "t";
     }
+
+    theme := Textbox_Theme.{};
+    draw_textbox(.{x, y + 72.0f, w, 36.0f}, ^test, theme=^theme);
 }
 
 #local render_entity_fields :: (entity: ^Entity, x, y, w, h: f32) {
index 1d10e622881f99cac1ffce678e522723c6507cc6..68aca33b0fafffc3ec15b81e0ff2e7cc76aaf5f2 100644 (file)
@@ -132,7 +132,7 @@ entity_manager_add :: (use this: ^Entity_Manager, entity: ^Entity) -> Entity_ID
         entity.id = next_entity_id;
         next_entity_id = ~~(cast(u32) next_entity_id + 1);
     } else {
-        next_entity_id = ~~(math.max(cast(u32) next_entity_id + 1, cast(u32) entity.id));
+        next_entity_id = ~~(math.max(cast(u32) next_entity_id, cast(u32) entity.id) + 1);
     }
 
     assert(cast(u32) entity.type != 0, "Adding an entity without a type!");
index 3e32076439c4a8935d2d8d26a8b345d40a535c74..094760ead05dc9e891dde9cab0c0963f7d3e0e48 100644 (file)
@@ -211,6 +211,26 @@ font_get_width :: (font: Font, msg: str) -> f32 {
     return math.max(x_, width);
 }
 
+font_get_height :: (font: Font, msg: str) -> f32 {
+    x_, y_ := 0.0f, 0.0f;
+    width := 0.0f;
+
+    quad: stbtt_aligned_quad;
+    for msg {
+        if it == #char "\n" {
+            width = math.max(width, x_);
+            x_ = 0;
+            y_ += font.em + 2;
+            continue;
+        }
+
+        stbtt_GetPackedQuad(font.chars.data, font.texture_width, font.texture_height,
+            ~~(it - #char " "), ^x_, ^y_, ^quad, false);
+    }
+
+    return y_ + font.em + 2;
+}
+
 
 FontDescriptor :: struct {
     path : str;
diff --git a/src/gfx/ui.onyx b/src/gfx/ui.onyx
new file mode 100644 (file)
index 0000000..c0436f2
--- /dev/null
@@ -0,0 +1,358 @@
+//
+// Very simple immediate mode UI
+//
+
+use package core
+use package opengles
+use package glfw3
+
+UI_Id :: u32
+
+ui_end_frame :: () {
+    hot_item_depth_needed = hot_item_depth;
+    if !hot_item_was_set do set_hot_item(0);
+    hot_item_depth = 0;
+    hot_item_was_set = false;
+
+    for^ animation_states.entries {
+        if !it.value.accessed_this_frame || (it.value.click_time == 0 && it.value.hover_time == 0)  {
+            map.delete(^animation_states, it.key);
+        }
+    }
+
+    for^ animation_states.entries {
+        it.value.accessed_this_frame = false;
+    }
+}
+
+//
+// Buttons
+//
+Button_Theme :: struct {
+    use text_theme := Text_Theme.{};
+    use animation_theme := Animation_Theme.{};
+
+    background_color := Color.{ 0.1, 0.1, 0.1 };
+    hover_color      := Color.{ 0.3, 0.3, 0.3 };
+    click_color      := Color.{ 0.5, 0.5, 0.7 };
+
+    border_color := Color.{0.2, 0.2, 0.2};
+    border_width := 2.0f;
+
+    active := false;
+}
+
+@CompilerBug // This should work...
+// default_button_theme := Button_Theme.{};
+
+draw_button  :: (use r: Rect, text: str, theme: ^Button_Theme, site := #callsite, increment := 0) -> bool {
+    // HMMM.... this should be gotten rid of as quick as possible.
+    __ASDF := Button_Theme.{};
+    if theme == null {
+        theme = ^__ASDF; 
+    }
+    result := false;
+
+    hash := get_site_hash(site, increment);
+    animation_state := get_animation(hash);
+    mx, my := mouse_get_position();
+
+    contains := Rect.contains(r, .{~~mx, ~~my});
+
+    if is_active_item(hash) {
+        if is_button_just_up(GLFW_MOUSE_BUTTON_LEFT) {
+            if is_hot_item(hash) && contains {
+                result = true;
+                animation_state.click_time = 1.0f;
+            }
+
+            set_active_item(0);
+        }
+    } elseif is_hot_item(hash) {
+        if is_button_down(GLFW_MOUSE_BUTTON_LEFT) {
+            set_active_item(hash);
+        }
+    }
+
+    if contains {
+        set_hot_item(hash);
+    }
+
+    if is_hot_item(hash) || theme.active {
+        move_towards(^animation_state.hover_time, 1.0f, theme.hover_speed);
+    } else {
+        move_towards(^animation_state.hover_time, 0.0f, theme.hover_speed);
+    }
+
+    border_width := theme.border_width;
+
+    immediate_set_color(theme.border_color);
+    immediate_rectangle(x, y, w, h);
+
+    surface_color := color_lerp(animation_state.hover_time, theme.background_color, theme.hover_color);
+    surface_color  = color_lerp(animation_state.click_time, surface_color, theme.click_color);
+    immediate_set_color(surface_color);
+    immediate_rectangle(x + border_width, y + border_width, w - border_width * 2, h - border_width * 2);
+
+    font : Font;
+    if theme.font == null do font = font_lookup(.{"./assets/fonts/calibri.ttf", 16});
+    else                  do font = *theme.font;
+    font_height := font_get_height(font, text);
+    font_set_color(theme.text_color);
+    font_draw_centered(font, x, y + (h - font_height) / 2 + font.em - 2, w, text);
+
+    move_towards(^animation_state.click_time, 0.0f, theme.click_decay_speed);
+
+    return result;
+}
+
+
+//
+// Textbox
+//
+Textbox_Theme :: struct {
+    use text_theme := Text_Theme.{
+        text_color = .{ 0, 0, 0 }
+    };
+
+    use animation_theme := Animation_Theme.{};
+
+    background_color := Color.{ 0.8, 0.8, 0.8 };
+    hover_color      := Color.{ 1.0, 1.0, 1.0 };
+    click_color      := Color.{ 0.5, 0.5, 0.7 };
+
+    border_color := Color.{ 0.2, 0.2, 0.2 };
+    border_width := 6.0f;   @InPixels
+
+    cursor_color := Color.{ 0.5, 0.5, 0.5 };
+    cursor_width := 4.0f;   @InPixels
+    cursor_blink_speed := 0.04f;   // Bigger is faster
+
+    placeholder_text_color := Color.{ 0.5, 0.5, 0.5 };
+}
+
+@CompilerBug
+// default_textbox_theme := Textbox_Theme.{};
+
+#local {
+    Textbox_Editing_State :: struct {
+        hash: UI_Id = 0;
+
+        cursor_position: i32 = 0;
+        cursor_animation := 0.0f;
+        cursor_animation_speed := 0.02f;
+    }
+
+    textbox_editing_state := Textbox_Editing_State.{};
+}
+
+draw_textbox :: (use r: Rect, text_buffer: ^[..] u8, placeholder := null_str, theme: ^Textbox_Theme = null, site := #callsite, increment := 0) -> bool {
+    result := false;
+
+    hash := get_site_hash(site, increment);
+    animation_state := get_animation(hash);
+    mx, my := mouse_get_position();
+
+    border_width := theme.border_width;
+    text_color := theme.text_color;
+    text := str.{text_buffer.data, text_buffer.count};
+    if text.count == 0 && placeholder.count >0 {
+        text = placeholder;
+        text_color = theme.placeholder_text_color;
+    }
+
+    font : Font;
+    if theme.font == null do font = font_lookup(.{"./assets/fonts/calibri.ttf", 16});
+    else                  do font = *theme.font;
+    text_width  := font_get_width(font, text);
+    text_height := font_get_height(font, text);
+
+    text_x := x + border_width;
+    text_y := y + font.em + (h - text_height) / 2;
+
+    contains := Rect.contains(r, .{~~mx, ~~my});
+
+    if is_hot_item(hash) && !is_active_item(hash) {
+        if is_button_down(GLFW_MOUSE_BUTTON_LEFT) && contains {
+            set_active_item(hash);
+            textbox_editing_state.hash = hash;
+            textbox_editing_state.cursor_animation_speed = theme.cursor_blink_speed;
+        }
+    }
+
+    if is_active_item(hash) {
+        if is_button_just_down(GLFW_MOUSE_BUTTON_LEFT) && !contains {
+            set_active_item(0);
+            textbox_editing_state.hash = 0;
+            textbox_editing_state.cursor_position = 0;
+        }
+    }
+
+    if contains {
+        set_hot_item(hash);
+    }
+
+    if textbox_editing_state.hash == hash {
+        move_towards(^textbox_editing_state.cursor_animation, 0.0f, textbox_editing_state.cursor_animation_speed);
+        if textbox_editing_state.cursor_animation <= 0.0f do textbox_editing_state.cursor_animation = 1.0f;
+
+        if is_button_down(GLFW_MOUSE_BUTTON_LEFT) && contains {
+            textbox_editing_state.cursor_animation = 1.0f;
+            // textbox_editing_state.cursor_position = get_cursor_position(text_buffer, text_x, text_y, theme.font_size, ~~mx, ~~my);
+        }
+
+        keys := input_get_keys_this_frame();
+        if keys.count > 0 {
+            for key: keys {
+                switch key {
+                    case GLFW_KEY_LEFT  do textbox_editing_state.cursor_position -= 1;
+                    case GLFW_KEY_RIGHT do textbox_editing_state.cursor_position += 1;
+                    case GLFW_KEY_END   do textbox_editing_state.cursor_position = text_buffer.count;
+                    case GLFW_KEY_HOME  do textbox_editing_state.cursor_position = 0;
+
+                    case GLFW_KEY_BACKSPACE {
+                        array.pop(text_buffer);
+                        textbox_editing_state.cursor_position = math.max(~~0, textbox_editing_state.cursor_position - 1);
+                    }
+
+                    case GLFW_KEY_DELETE {
+                        array.delete(text_buffer, 0);
+                    }
+
+                    case #default {
+                        if key >= #char " " && key <= 128 {
+                            array.push(text_buffer, ~~key);
+                            textbox_editing_state.cursor_position += 1;
+                        }
+                    }
+                }
+            }
+
+            textbox_editing_state.cursor_position = math.clamp(textbox_editing_state.cursor_position, 0, text_buffer.count);
+            textbox_editing_state.cursor_animation = 1.0f;
+        }
+    }
+
+    if is_hot_item(hash) {
+        move_towards(^animation_state.hover_time, 1.0f, theme.hover_speed);
+    } else {
+        move_towards(^animation_state.hover_time, 0.0f, theme.hover_speed);
+    }
+
+    // immediate_push_scissor(x, y, w, h);    
+    immediate_set_color(theme.border_color);
+    immediate_rectangle(x, y, w, h);
+
+    surface_color := color_lerp(animation_state.hover_time, theme.background_color, theme.hover_color);
+    surface_color  = color_lerp(animation_state.click_time, surface_color, theme.click_color);
+    immediate_set_color(surface_color);
+    immediate_rectangle(x + border_width, y + border_width, w - border_width * 2, h - border_width * 2);
+
+    font_set_color(theme.text_color);
+    font_draw(font, text_x, text_y, text); // This is technically a frame late for updating the text?
+
+    move_towards(^animation_state.click_time, 0.0f, theme.click_decay_speed);
+
+    // immediate_pop_scissor();
+    return result;
+}
+
+
+
+
+
+#local {
+    hot_item    : UI_Id = 0
+    active_item : UI_Id = 0
+    hot_item_was_set := false
+
+    hot_item_depth := 0;
+    hot_item_depth_needed := 0;
+
+    set_active_item :: (id: UI_Id) -> bool {
+        active_item = id;
+        return true;
+    }
+
+    set_hot_item :: (id: UI_Id, force := false) -> bool {
+        if active_item != 0 do return false;
+
+        if force {
+            hot_item_was_set = true;
+            hot_item = id;
+            return true;
+        }
+
+        hot_item_depth += 1;
+        if hot_item_depth >= hot_item_depth_needed {
+            hot_item_was_set = true;
+            hot_item = id;
+            return true;
+        }
+
+        return false;
+    }
+
+    is_active_item :: (id: UI_Id) -> bool {
+        return active_item == id;
+    }
+
+    is_hot_item :: (id: UI_Id) -> bool {
+        return hot_item == id;
+    }
+
+    Text_Theme :: struct {
+        text_color := Color.{1, 1, 1};
+        font       :  ^Font = null;
+    }
+
+    Animation_Theme :: struct {
+        hover_speed       := 0.1f;
+        click_decay_speed := 0.08f;
+    }
+
+    Animation_State :: struct {
+        hover_time := 0.0f;
+        click_time := 0.0f;
+
+        accessed_this_frame := false;
+    }
+
+    animation_states : Map(UI_Id, Animation_State);
+
+    get_animation :: (id: UI_Id) -> ^Animation_State {
+        retval := map.get_ptr(^animation_states, id);
+        if retval == null {
+            animation_states[id] = .{};
+            retval = ^animation_states[id];
+        }
+
+        retval.accessed_this_frame = true;
+        return retval;
+    }
+
+    has_active_animation :: () -> bool {
+        for^ animation_states.entries {
+            if it.value.hover_time != 0.0f || it.value.hover_time != 0.0f do return true;
+            if it.value.click_time != 0.0f || it.value.click_time != 0.0f do return true;
+        }
+
+        return false;
+    }
+
+    get_site_hash :: macro (site: CallSite, increment := 0) -> UI_Id {
+        hash :: package core.hash
+        file_hash   := hash.to_u32(site.file);
+        line_hash   := hash.to_u32(site.line);
+        column_hash := hash.to_u32(site.column);
+
+        return ~~ (file_hash * 0x472839 + line_hash * 0x6849210 + column_hash * 0x1248382 + increment);
+    }
+
+    color_lerp :: macro (t: f32, c1, c2: Color) => Color.{
+        r = c1.r * (1 - t) + c2.r * t,
+        g = c1.g * (1 - t) + c2.g * t,
+        b = c1.b * (1 - t) + c2.b * t,
+        a = c1.a * (1 - t) + c2.a * t,
+    };
+}
\ No newline at end of file
index d09836a981c9ffad24c48bc24fc2103c45ae3c48..e7a80db56ec9c4907a248d597272de8240348ac6 100644 (file)
@@ -90,6 +90,7 @@ update :: (dt: f32) {
 
 draw :: () {
     immediate_clear(.{0.1, 0.1, 0.1});
+    defer ui_end_frame();
     defer input_post_update();
     defer {
         immediate_flush();
index 09679958fd0e0bdd853ca2c054cbf88d00006f8b..2a5f217cf407d12573050905c3a7b3bcfc4f81b3 100644 (file)
@@ -2,18 +2,27 @@ use package core
 use package glfw3
 
 #local {
-       keys_this_frame: [..] u32
-       keys_last_frame: [..] u32 
+       keys_this_frame  : [..] u32   // Keys currently being pressed this frame
+       keys_pulse_frame : [..] u32   // Keys pressed during this frame, only set once per keypress
+       keys_last_frame  : [..] u32   // Keys being pressed in the last frame
 
-       buttons_this_frame: [8] bool
-       buttons_last_frame: [8] bool
+       buttons_this_frame: [8] bool  // Mouse buttons being pressed this frame
+       buttons_last_frame: [8] bool  // Mouse buttons being pressed last frame
 }
 
 input_update :: () {
     glfwGetCursorPos(window, ^mouse_x, ^mouse_y);
+
+       array.clear(^keys_pulse_frame);
+       for keys_this_frame {
+               if !array.contains(keys_last_frame, it) {
+                       keys_pulse_frame << it;
+               }
+       }
 }
 
 input_post_update :: () {
+       array.clear(^keys_pulse_frame);
        array.clear(^keys_last_frame);
        for keys_this_frame do keys_last_frame << it;
 
@@ -23,6 +32,10 @@ input_post_update :: () {
        last_mouse_y = mouse_y;
 }
 
+input_get_keys_this_frame :: () -> [] u32 {
+       return keys_pulse_frame;
+}
+
 is_key_down      :: (key) => array.contains(keys_this_frame, key);
 is_key_just_down :: (key) => array.contains(keys_this_frame, key) && !array.contains(keys_last_frame, key);
 is_key_just_up   :: (key) => !array.contains(keys_this_frame, key) && array.contains(keys_last_frame, key);