Got support for persistent ordering of UI_Boxes using UI_FloatingManager

This commit is contained in:
Edward R. Gonzalez 2024-05-13 16:07:49 -04:00
parent 0744069b0d
commit adc75f6977
10 changed files with 271 additions and 172 deletions

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

6
code/ui_canvas.odin Normal file
View File

@ -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,
}

12
code/ui_docking.odin Normal file
View File

@ -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,
}

189
code/ui_floating.odin Normal file
View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 <nil>
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
// <parent.last.prev> <parent.last> curr_box
parent.last.next = curr_box
curr_box.prev = parent.last
parent.last = curr_box
curr_box.next = nil
}
// |
// v
// parent.first <nil>
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
// <parent.last.prev> <parent.last> 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