From: Brendan Hansen Date: Tue, 8 Feb 2022 04:24:50 +0000 (-0600) Subject: started working on proper UI for editor X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=d4b5bdf88bac67f2f2bcb0ec855f0dfdda431829;p=bar-game.git started working on proper UI for editor --- diff --git a/src/build.onyx b/src/build.onyx index bc08fff..5fd8821 100644 --- a/src/build.onyx +++ b/src/build.onyx @@ -26,6 +26,7 @@ DEBUG :: false #load "gfx/mesh" #load "gfx/shader" #load "gfx/texture" +#load "gfx/ui" #load "utils/input" #load "utils/vecmath" diff --git a/src/entity/editor.onyx b/src/entity/editor.onyx index 6c71db0..6c2d5a3 100644 --- a/src/entity/editor.onyx +++ b/src/entity/editor.onyx @@ -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) { diff --git a/src/entity/manager.onyx b/src/entity/manager.onyx index 1d10e62..68aca33 100644 --- a/src/entity/manager.onyx +++ b/src/entity/manager.onyx @@ -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!"); diff --git a/src/gfx/font.onyx b/src/gfx/font.onyx index 3e32076..094760e 100644 --- a/src/gfx/font.onyx +++ b/src/gfx/font.onyx @@ -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 index 0000000..c0436f2 --- /dev/null +++ b/src/gfx/ui.onyx @@ -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 diff --git a/src/main.onyx b/src/main.onyx index d09836a..e7a80db 100644 --- a/src/main.onyx +++ b/src/main.onyx @@ -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(); diff --git a/src/utils/input.onyx b/src/utils/input.onyx index 0967995..2a5f217 100644 --- a/src/utils/input.onyx +++ b/src/utils/input.onyx @@ -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);