From adc75f6977f7475e89f62b0e5c630c71103b3e3c Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 13 May 2024 16:07:49 -0400 Subject: [PATCH] Got support for persistent ordering of UI_Boxes using UI_FloatingManager --- code/engine_api.odin | 3 +- code/env_env.odin | 3 +- code/grime_grime.odin | 5 ++ code/tick_render.odin | 17 +--- code/ui_canvas.odin | 6 ++ code/ui_docking.odin | 12 +++ code/ui_floating.odin | 189 ++++++++++++++++++++++++++++++++++++++++++ code/ui_screen.odin | 41 +++++---- code/ui_signal.odin | 56 ------------- code/ui_ui.odin | 111 ++++++------------------- 10 files changed, 271 insertions(+), 172 deletions(-) create mode 100644 code/ui_canvas.odin create mode 100644 code/ui_docking.odin create mode 100644 code/ui_floating.odin diff --git a/code/engine_api.odin b/code/engine_api.odin index 358adaa..18d5ef5 100644 --- a/code/engine_api.odin +++ b/code/engine_api.odin @@ -193,6 +193,7 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem // Setup the screen ui state { ui_startup( & screen_ui.base, cache_allocator = persistent_slab_allocator() ) + ui_floating_startup( & screen_ui.floating, persistent_slab_allocator(), 16 * Kilobyte, 16 * Kilobyte, "screen ui floating manager" ) using screen_ui menu_bar.pos = { -60, 0 } @@ -353,7 +354,7 @@ tick :: proc( host_delta_time : f64, host_delta_ns : Duration ) -> b32 { // profile("Client tick timing processing") config.engine_refresh_hz = uint(monitor_refresh_hz) - // config.engine_refresh_hz = 10 + // config.engine_refresh_hz = 1 frametime_target_ms = 1.0 / f64(config.engine_refresh_hz) * S_To_MS sub_ms_granularity_required := frametime_target_ms <= Frametime_High_Perf_Threshold_MS diff --git a/code/env_env.odin b/code/env_env.odin index 3af391c..46213e4 100644 --- a/code/env_env.odin +++ b/code/env_env.odin @@ -242,7 +242,8 @@ State :: struct { // There are two potential UI contextes for this prototype so far, // the screen-space UI and the current workspace UI. // This is used so that the ui api doesn't need to have the user pass the context every single time. - ui_context : ^UI_State, + ui_context : ^UI_State, + ui_floating_context : ^UI_FloatingManager, // The camera is considered the "context" for coodrinate space operations in rendering cam_context : Camera, diff --git a/code/grime_grime.odin b/code/grime_grime.odin index 7f2d851..90034b6 100644 --- a/code/grime_grime.odin +++ b/code/grime_grime.odin @@ -274,6 +274,11 @@ to_writer :: proc { str_builder_to_writer, } +ui_floating :: proc { + ui_floating_just_builder, + ui_floating_with_capture, +} + ui_layout_push :: proc { ui_layout_push_layout, ui_layout_push_theme, diff --git a/code/tick_render.odin b/code/tick_render.odin index c04ddb2..ad8d630 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -116,15 +116,10 @@ render_mode_2d_workspace :: proc() state.ui_context = ui current := root.first - for ; current != nil; current = ui_box_tranverse_next( current, is_destructive = true ) + for ; current != nil; current = ui_box_tranverse_next( current ) { // profile("Box") parent := current.parent - if parent == ui.root && current.ancestors == -1 { - // This is a deceased rooted box - // Ignore it as its not constructed this frame - continue - } layout := current.layout style := current.style @@ -344,18 +339,10 @@ render_screen_ui :: proc() // Sort roots children by top-level order current := root.first - for ; current != nil; current = ui_box_tranverse_next( current, is_destructive = false ) + for ; current != nil; current = ui_box_tranverse_next( current ) { // profile("Box") parent := current.parent - if parent == ui.root && current.ancestors == -1 { - // current.parent = nil - // current.first = nil - // current.last = nil - // This is a deceased rooted box - // Ignore it as its not constructed this frame - continue - } style := current.style layout := current.layout diff --git a/code/ui_canvas.odin b/code/ui_canvas.odin new file mode 100644 index 0000000..94f3074 --- /dev/null +++ b/code/ui_canvas.odin @@ -0,0 +1,6 @@ +package sectr + +// Specialization of floating that allows panning across the viewport in the space (space isn't clampped to a specific size)) +UI_Canvas :: struct { + using floating : UI_Floating, +} diff --git a/code/ui_docking.odin b/code/ui_docking.odin new file mode 100644 index 0000000..d98acee --- /dev/null +++ b/code/ui_docking.odin @@ -0,0 +1,12 @@ +package sectr + +UI_DockedEntry :: struct { + placeholder : int, +} + +// Non-overlapping, Tiled, window/frame manager +// Has support for tabbing +// AKA: Tiled Window Manager +UI_Docking :: struct { + placeholder : int, +} diff --git a/code/ui_floating.odin b/code/ui_floating.odin new file mode 100644 index 0000000..4c094e7 --- /dev/null +++ b/code/ui_floating.odin @@ -0,0 +1,189 @@ +package sectr + +UI_Floating :: struct { + label : string, + using links : DLL_NodePN(UI_Floating), + captures : rawptr, + builder : UI_FloatingBuilder, + queued : b32, +} +UI_FloatingBuilder :: #type proc( captures : rawptr ) -> (became_active : b32) + +// Overlapping/Stacking window/frame manager +// AKA: Stacking or Floating Window Manager +UI_FloatingManager :: struct { + using links : DLL_NodeFL(UI_Floating), + build_queue : Array(UI_Floating), + tracked : HMapZPL(UI_Floating), +} + +ui_floating_startup :: proc( self : ^UI_FloatingManager, allocator : Allocator, build_queue_cap, tracked_cap : u64, dbg_name : string = "" ) -> AllocatorError +{ + error : AllocatorError + + queue_dbg_name := str_intern(str_fmt_tmp("%s: build_queue", dbg_name)) + self.build_queue, error = array_init_reserve( UI_Floating, allocator, build_queue_cap, dbg_name = queue_dbg_name.str ) + if error != AllocatorError.None + { + ensure(false, "Failed to allocate the build_queue") + return error + } + + tracked_dbg_name := str_intern(str_fmt_tmp("%s: tracked", dbg_name)) + self.tracked, error = zpl_hmap_init_reserve( UI_Floating, allocator, tracked_cap, dbg_name = tracked_dbg_name.str ) + if error != AllocatorError.None + { + ensure(false, "Failed to allocate tracking table") + return error + } + return error +} + +ui_floating_just_builder :: #force_inline proc( label : string, builder : UI_FloatingBuilder ) -> ^UI_Floating +{ + No_Captures : rawptr = nil + return ui_floating_with_capture(label, No_Captures, builder) +} + +ui_floating_with_capture :: proc( label : string, captures : rawptr = nil, builder : UI_FloatingBuilder ) -> ^UI_Floating +{ + entry := UI_Floating { + label = label, + captures = captures, + builder = builder, + } + + floating := get_state().ui_floating_context + array_append( & floating.build_queue, entry ) + return nil +} + +@(deferred_none = ui_floating_manager_end) +ui_floating_manager :: proc ( manager : ^UI_FloatingManager ) +{ + ui_floating_manager_begin(manager) +} + +ui_floating_manager_begin :: proc ( manager : ^UI_FloatingManager ) +{ + state := get_state() + state.ui_floating_context = manager +} + +ui_floating_manager_end :: proc() +{ + state := get_state() + floating := & state.ui_floating_context + ui_floating_build() + floating = nil +} + +ui_floating_build :: proc() +{ + ui := ui_context() + screen_ui := cast(^UI_ScreenState) ui + using floating := get_state().ui_floating_context + + for to_enqueue in array_to_slice_num( build_queue) + { + key := ui_key_from_string(to_enqueue.label) + lookup := zpl_hmap_get( & tracked, transmute(u64) key ) + + // Check if entry is already present + if lookup != nil && lookup.prev != nil && lookup.next != nil { + lookup.captures = to_enqueue.captures + lookup.builder = to_enqueue.builder + lookup.queued = true + continue + } + + if lookup == nil { + error : AllocatorError + lookup, error = zpl_hmap_set( & tracked, transmute(u64) key, to_enqueue ) + if error != AllocatorError.None { + ensure(false, "Failed to allocate entry to hashtable") + continue + } + lookup.queued = true + } + else { + lookup.captures = to_enqueue.captures + lookup.builder = to_enqueue.builder + lookup.queued = true + continue + } + + if first == nil { + first = lookup + last = lookup + continue + } + last.next = lookup + last = lookup + } + array_clear(build_queue) + + for entry := first; entry != nil; entry = entry.next + { + using entry + if ! queued + { + if entry == first + { + first = entry.next + entry.next = nil + continue + } + if entry == last + { + last = last.prev + last.prev = nil + entry.prev = nil + continue + } + + left := entry.prev + right := entry.next + + left.next = right + right.prev = left + entry.prev = nil + entry.next = nil + } + + if builder( captures ) && entry != last + { + PopEntry: + { + if first == nil { + first = entry + last = entry + break PopEntry + } + if entry == first + { + first = entry.next + entry.next = nil + break PopEntry + } + if entry == last + { + last = last.prev + last.prev = nil + entry.prev = nil + break PopEntry + } + + left := entry.prev + right := entry.next + + left.next = right + right.prev = left + } + + last.next = entry + last = entry + } + queued = false + } +} diff --git a/code/ui_screen.odin b/code/ui_screen.odin index d9f3a87..5ca4d76 100644 --- a/code/ui_screen.odin +++ b/code/ui_screen.odin @@ -3,6 +3,12 @@ package sectr UI_ScreenState :: struct { using base : UI_State, + + floating : UI_FloatingManager, + + // TODO(Ed): The docked should be the base, floating is should be nested within as a 'veiwport' to a 'desktop' or 'canvas' + // docked : UI_Docking, + menu_bar : struct { pos, size : Vec2, @@ -28,19 +34,25 @@ ui_screen_tick :: proc() { ui_graph_build( & screen_ui ) ui := ui_context - ui_screen_menu_bar() - ui_screen_settings_menu() + ui_floating_manager_begin( & screen_ui.floating ) + { + ui_floating("Menu Bar", ui_screen_menu_bar) + ui_floating("Settings Menu", ui_screen_settings_menu) + } + ui_floating_manager_end() } -ui_screen_menu_bar :: proc() +ui_screen_menu_bar :: proc( captures : rawptr = nil ) -> (should_raise : b32 ) { profile("App Menu Bar") fmt :: str_fmt_alloc using state := get_state() using screen_ui + // ui_floating("Menu Bar", No_Captures, proc( captures : rawptr = nil ) { - using menu_bar + using state := get_state(); + using screen_ui.menu_bar ui_layout( UI_Layout { flags = {.Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, .Origin_At_Anchor_Center}, // anchor = range2({0.5, 0.5}, {0.5, 0.5} ), @@ -48,8 +60,8 @@ ui_screen_menu_bar :: proc() border_width = 1.0, font_size = 12, // pos = {}, - pos = menu_bar.pos, - size = range2( menu_bar.size, {}), + pos = pos, + size = range2( size, {}), }) ui_style( UI_Style { bg_color = { 0, 0, 0, 30 }, @@ -57,9 +69,7 @@ ui_screen_menu_bar :: proc() font = default_font, text_color = Color_White, }) - // ui_hbox( & container, .Left_To_Right, "App Menu Bar", { .Mouse_Clickable} ) container = ui_hbox( .Left_To_Right, "Menu Bar" ) - // ui_parent(container) ui_layout( UI_Layout { flags = {}, @@ -81,7 +91,8 @@ ui_screen_menu_bar :: proc() { using move_box if active { - menu_bar.pos += input.mouse.delta + pos += input.mouse.delta + should_raise = true } layout.anchor.ratio.x = 0.2 } @@ -98,22 +109,21 @@ ui_screen_menu_bar :: proc() } settings_btn.layout.size.min.x = 100 if settings_btn.pressed { - settings_menu.is_open = true + screen_ui.settings_menu.is_open = true } spacer = ui_spacer("Menu Bar: End Spacer") spacer.layout.anchor.ratio.x = 1.0 - - // ui_hbox_end( container) } + return } -ui_screen_settings_menu :: proc() +ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b32) { profile("Settings Menu") using state := get_state() using state.screen_ui - if ! settings_menu.is_open do return + if ! settings_menu.is_open do return false using settings_menu if size.x < min_size.x do size.x = min_size.x @@ -171,6 +181,7 @@ ui_screen_settings_menu :: proc() layout.anchor.ratio.x = 1.0 if maximize_btn.pressed { settings_menu.is_maximized = ~settings_menu.is_maximized + should_raise = true } if settings_menu.is_maximized do text = str_intern("min") else do text = str_intern("max") @@ -195,6 +206,7 @@ ui_screen_settings_menu :: proc() } if frame_bar.active { pos += input.mouse.delta + should_raise = true } spacer := ui_spacer("Settings Menu: Spacer") @@ -209,4 +221,5 @@ ui_screen_settings_menu :: proc() } ui_resizable_handles( & container, & pos, & size) + return } diff --git a/code/ui_signal.odin b/code/ui_signal.odin index 9b37a07..a9df11c 100644 --- a/code/ui_signal.odin +++ b/code/ui_signal.odin @@ -72,62 +72,6 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas // Check to see if this box is active if mouse_clickable && signal.cursor_over && left_pressed && was_hot { - top_ancestor := ui_top_ancestor(box) - if ui.root.last != top_ancestor - { - // dll_full_pop(top_ancestor, top_ancestor.parent) - // dll_full_push_back( top_ancestor.parent, top_ancestor, nil ) - - left := top_ancestor.prev - right := top_ancestor.next - - if left != nil { - left.next = top_ancestor.next - } - else { - // We are the first box on root, - ui.root.first = right - } - // right should never be null since top_ancestor is not the last node - right.prev = left - - if ui.root.last != nil - { - prev_last := ui.root.last - ui.root.last = top_ancestor - - prev_last.next = top_ancestor - top_ancestor.prev = prev_last - top_ancestor.next = nil - if left == nil && right == prev_last - { - right.prev = nil - right.next = top_ancestor - } - } - else - { - // vvv - // ui.root.first - > ui.root.last - ui.root.last = top_ancestor - ui.root.first.next = top_ancestor - top_ancestor.prev = ui.root.first - top_ancestor.next = nil - ui.root.last = top_ancestor - } - - for curr := right; curr != nil; curr = curr.next { - curr.parent_index -= 1 - } - - // Fix up left & right references - // if left != nil && right != nil { - // right.prev = left - // left.next = right - // } - } - - // runtime.debug_trap() // ui.hot = box.key ui.active = box.key ui.active_mouse[MouseBtn.Left] = box.key diff --git a/code/ui_ui.odin b/code/ui_ui.odin index 9a46fc3..06259ef 100644 --- a/code/ui_ui.odin +++ b/code/ui_ui.odin @@ -119,9 +119,11 @@ UI_Box :: struct { flags : UI_BoxFlags, computed : UI_Computed, + // TODO(Ed): Move prev_layout out, this is no longer managed by the core UI, its managed by w/e is constructing the graph prev_layout : UI_Layout, layout : UI_Layout, + // TODO(Ed): Move prev_style out, this is no longer managed by the core UI, its managed by w/e is constructing the graph prev_style : UI_Style, style : UI_Style, @@ -243,14 +245,6 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) links_perserved : DLL_NodeFull( UI_Box ) curr_box : (^ UI_Box) - curr_box = zpl_hmap_get( curr_cache, cast(u64) key ) - if curr_box != nil && curr_box.ancestors == 1 { - // top_ancestor has had its neighboring links updated this frame - // preserve them from the refresh - links_perserved.prev = curr_box.links.prev - links_perserved.next = curr_box.links.next - } - prev_box := zpl_hmap_get( prev_cache, cast(u64) key ) { // profile("Assigning current box") @@ -285,88 +279,44 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) // If there is a parent, setup the relevant references parent := stack_peek( & parent_stack ) - if parent == nil && ! curr_box.first_frame - { - set_error : AllocatorError - if prev_box.first != nil { - curr_box.first, set_error = zpl_hmap_set( curr_cache, cast(u64) prev_box.first.key, prev_box.first ^ ) - verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" ) - } - if prev_box.last != nil { - curr_box.last, set_error = zpl_hmap_set( curr_cache, cast(u64) prev_box.last.key, prev_box.last ^ ) - verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" ) - } - curr_box.ancestors = 0 - } if parent != nil { - // There are three possible reasons to just add as usual: - // 1. Its not rooted, which means we don't track order - // 2. - if parent != ui.root || curr_box.first_frame //|| (prev_box.prev == nil && prev_box.next == nil) + dll_full_push_back( parent, curr_box, nil ) + when false { - // Only occurs when this is no prior history for rooted boxes - // Otherwise regular children always complete this - // dll_full_push_back( parent, curr_box, nil ) - when true - { - // | - // v - // parent.first - if parent.first == nil { - parent.first = curr_box - parent.last = curr_box - curr_box.next = nil - curr_box.prev = nil - } - else { - // Positin is set to last, insert at end - // curr_box - parent.last.next = curr_box - curr_box.prev = parent.last - parent.last = curr_box - curr_box.next = nil - } + // | + // v + // parent.first + if parent.first == nil { + parent.first = curr_box + parent.last = curr_box + curr_box.next = nil + curr_box.prev = nil } + else { + // Positin is set to last, insert at end + // curr_box + parent.last.next = curr_box + curr_box.prev = parent.last + parent.last = curr_box + curr_box.next = nil + } + } - curr_box.parent_index = parent.num_children - parent.num_children += 1 - curr_box.parent = parent - curr_box.ancestors = parent.ancestors + 1 - } - else if prev_box != nil - { - // Make only todo if links are properly wiped on current - set_error : AllocatorError - if curr_box.prev == nil && prev_box.prev != nil && prev_box.ancestors == 1 { - curr_box.prev, set_error = zpl_hmap_set( curr_cache, cast(u64) prev_box.prev.key, prev_box.prev ^ ) - verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" ) - } - if curr_box.next == nil && prev_box.next != nil && prev_box.ancestors == 1 { - curr_box.next, set_error = zpl_hmap_set( curr_cache, cast(u64) prev_box.next.key, prev_box.next ^ ) - verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" ) - } - curr_box.parent = parent - curr_box.ancestors = 1 - parent.num_children += 1 - } + curr_box.parent_index = parent.num_children + parent.num_children += 1 + curr_box.parent = parent + curr_box.ancestors = parent.ancestors + 1 } ui.built_box_count += 1 return curr_box } -ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box, is_destructive : b32 = false ) -> (^ UI_Box) +ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box) { parent := box.parent - // Marking this box as deceased with no position in the box graph - if is_destructive { - // box.parent = nil - box.num_children = -1 - box.ancestors = -1 - } - // Check to make sure parent is present on the screen, if its not don't bother. // If current has children, do them first using state := get_state() @@ -415,15 +365,6 @@ ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} ) get_state().ui_context = ui using get_state().ui_context - if root != nil - { - // Set all top-level widgets to a negative index - // This will be used for prunning the rooted_children order - // for box := ui.root.first; box != nil; box = box.next { - // box.parent_index = -1 - // } - } - temp := prev_cache prev_cache = curr_cache curr_cache = temp