diff --git a/code/grime/hashmap_chained.odin b/code/grime/hashmap_chained.odin index c2a6c3d..94c494a 100644 --- a/code/grime/hashmap_chained.odin +++ b/code/grime/hashmap_chained.odin @@ -25,8 +25,9 @@ HMapChainedSlot :: struct( $Type : typeid ) { } HMapChainedHeader :: struct( $ Type : typeid ) { - pool : Pool, - lookup : [] ^HMapChainedSlot(Type), + pool : Pool, + lookup : [] ^HMapChainedSlot(Type), + dbg_name : string, } HMapChained :: struct( $ Type : typeid) { @@ -55,7 +56,7 @@ hmap_chained_init :: proc( $HMapChainedType : typeid/HMapChained($Type), lookup_ pool_bucket_cap : uint = 1 * Kilo, pool_bucket_reserve_num : uint = 0, pool_alignment : uint = mem.DEFAULT_ALIGNMENT, - // dbg_name : string = "" + dbg_name : string = "" ) -> (table : HMapChained(Type), error : AllocatorError) { header_size := size_of(HMapChainedHeader(Type)) @@ -73,10 +74,11 @@ hmap_chained_init :: proc( $HMapChainedType : typeid/HMapChained($Type), lookup_ bucket_reserve_num = pool_bucket_reserve_num, alignment = pool_alignment, allocator = allocator, - // dbg_name = str_intern(str_fmt("%v: pool", dbg_name)).str + dbg_name = str_intern(str_fmt("%v: pool", dbg_name)).str ) - data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChainedHeader(Type)) table.header)[1:] - table.lookup = slice_ptr( data, int(lookup_capacity) ) + data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChainedHeader(Type)) table.header)[1:] + table.lookup = slice_ptr( data, int(lookup_capacity) ) + table.dbg_name = dbg_name return } diff --git a/code/sectr/grime/hashmap_zpl.odin b/code/grime/hashmap_zpl.odin similarity index 79% rename from code/sectr/grime/hashmap_zpl.odin rename to code/grime/hashmap_zpl.odin index 37a6804..7117c3c 100644 --- a/code/sectr/grime/hashmap_zpl.odin +++ b/code/grime/hashmap_zpl.odin @@ -18,7 +18,7 @@ Each entry contains a next field, which is an index pointing to the next entry i Growing this hashtable is destructive, so it should usually be kept to a fixed-size unless the populating operations only occur in one place and from then on its read-only. */ -package sectr +package grime import "core:slice" @@ -69,7 +69,7 @@ hmap_zpl_init :: proc return result, AllocatorError.None } -hamp_zpl_clear :: proc( using self : ^ HMapZPL( $ Type ) ) { +hmap_zpl_clear :: proc( using self : ^ HMapZPL( $ Type ) ) { for id := 0; id < table.num; id += 1 { table[id] = -1 } @@ -78,17 +78,17 @@ hamp_zpl_clear :: proc( using self : ^ HMapZPL( $ Type ) ) { array_clear( entries ) } -hamp_zpl_destroy :: proc( using self : ^ HMapZPL( $ Type ) ) { +hmap_zpl_destroy :: proc( using self : ^ HMapZPL( $ Type ) ) { if table.data != nil && table.capacity > 0 { array_free( table ) array_free( entries ) } } -hamp_zpl_get :: proc ( using self : ^ HMapZPL( $ Type ), key : u64 ) -> ^ Type +hmap_zpl_get :: proc ( using self : ^ HMapZPL( $ Type ), key : u64 ) -> ^ Type { // profile(#procedure) - id := hamp_zpl_find( self, key ).entry_index + id := hmap_zpl_find( self, key ).entry_index if id >= 0 { return & entries.data[id].value } @@ -96,26 +96,26 @@ hamp_zpl_get :: proc ( using self : ^ HMapZPL( $ Type ), key : u64 ) -> ^ Type return nil } -hamp_zpl_map :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapProc ) { +hmap_zpl_map :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapProc ) { ensure( map_proc != nil, "Mapping procedure must not be null" ) for id := 0; id < entries.num; id += 1 { map_proc( Type, entries[id].key, entries[id].value ) } } -hamp_zpl_map_mut :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapMutProc ) { +hmap_zpl_map_mut :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapMutProc ) { ensure( map_proc != nil, "Mapping procedure must not be null" ) for id := 0; id < entries.num; id += 1 { map_proc( Type, entries[id].key, & entries[id].value ) } } -hamp_zpl_grow :: proc( using self : ^ HMapZPL( $ Type ) ) -> AllocatorError { +hmap_zpl_grow :: proc( using self : ^ HMapZPL( $ Type ) ) -> AllocatorError { new_num := array_grow_formula( entries.num ) - return hamp_zpl_rehash( self, new_num ) + return hmap_zpl_rehash( self, new_num ) } -hamp_zpl_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorError +hmap_zpl_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorError { profile(#procedure) // For now the prototype should never allow this to happen. @@ -124,7 +124,7 @@ hamp_zpl_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorE new_ht, init_result := hmap_zpl_init( HMapZPL(Type), new_num, ht.table.backing, ht.table.dbg_name ) if init_result != AllocatorError.None { - ensure( false, "New hamp_zpl failed to allocate" ) + ensure( false, "New hmap_zpl failed to allocate" ) return init_result } @@ -132,8 +132,8 @@ hamp_zpl_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorE find_result : HMapZPL_FindResult entry := & ht.entries.data[id] - find_result = hamp_zpl_find( & new_ht, entry.key ) - last_added_index = hamp_zpl_add_entry( & new_ht, entry.key ) + find_result = hmap_zpl_find( & new_ht, entry.key ) + last_added_index = hmap_zpl_add_entry( & new_ht, entry.key ) if find_result.prev_index < 0 { new_ht.table.data[ find_result.hash_index ] = last_added_index @@ -146,13 +146,13 @@ hamp_zpl_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorE new_ht.entries.data[ last_added_index ].value = entry.value } - hamp_zpl_destroy( ht ) + hmap_zpl_destroy( ht ) (ht ^) = new_ht return AllocatorError.None } -hamp_zpl_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) ) +hmap_zpl_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) ) { for id := 0; id < entries.num; id += 1 { entries[id].Next = -1; @@ -162,7 +162,7 @@ hamp_zpl_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) ) } for id := 0; id < entries.num; id += 1 { entry := & entries[id] - find_result := hamp_zpl_find( entry.key ) + find_result := hmap_zpl_find( entry.key ) if find_result.prev_index < 0 { table[ find_result.hash_index ] = id @@ -174,45 +174,45 @@ hamp_zpl_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) ) } // Used when the address space of the allocator changes and the backing reference must be updated -hamp_zpl_reload :: proc( using self : ^HMapZPL($Type), new_backing : Allocator ) { +hmap_zpl_reload :: proc( using self : ^HMapZPL($Type), new_backing : Allocator ) { table.backing = new_backing entries.backing = new_backing } -hamp_zpl_remove :: proc( self : ^ HMapZPL( $ Type ), key : u64 ) { - find_result := hamp_zpl_find( key ) +hmap_zpl_remove :: proc( self : ^ HMapZPL( $ Type ), key : u64 ) { + find_result := hmap_zpl_find( key ) if find_result.entry_index >= 0 { array_remove_at( & entries, find_result.entry_index ) - hamp_zpl_rehash_fast( self ) + hmap_zpl_rehash_fast( self ) } } -hamp_zpl_remove_entry :: proc( using self : ^ HMapZPL( $ Type ), id : i64 ) { +hmap_zpl_remove_entry :: proc( using self : ^ HMapZPL( $ Type ), id : i64 ) { array_remove_at( & entries, id ) } -hamp_zpl_set :: proc( using self : ^ HMapZPL( $ Type), key : u64, value : Type ) -> (^ Type, AllocatorError) +hmap_zpl_set :: proc( using self : ^ HMapZPL( $ Type), key : u64, value : Type ) -> (^ Type, AllocatorError) { // profile(#procedure) id : i64 = 0 find_result : HMapZPL_FindResult - if hamp_zpl_full( self ) + if hmap_zpl_full( self ) { - grow_result := hamp_zpl_grow( self ) + grow_result := hmap_zpl_grow( self ) if grow_result != AllocatorError.None { return nil, grow_result } } - find_result = hamp_zpl_find( self, key ) + find_result = hmap_zpl_find( self, key ) if find_result.entry_index >= 0 { id = find_result.entry_index } else { - id = hamp_zpl_add_entry( self, key ) + id = hmap_zpl_add_entry( self, key ) if find_result.prev_index >= 0 { entries.data[ find_result.prev_index ].next = id } @@ -223,15 +223,15 @@ hamp_zpl_set :: proc( using self : ^ HMapZPL( $ Type), key : u64, value : Type ) entries.data[id].value = value - if hamp_zpl_full( self ) { - alloc_error := hamp_zpl_grow( self ) + if hmap_zpl_full( self ) { + alloc_error := hmap_zpl_grow( self ) return & entries.data[id].value, alloc_error } return & entries.data[id].value, AllocatorError.None } -hamp_zpl_slot :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 { +hmap_zpl_slot :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 { for id : i64 = 0; id < table.num; id += 1 { if table.data[id] == key { return id @@ -240,14 +240,14 @@ hamp_zpl_slot :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 { return -1 } -hamp_zpl_add_entry :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 { +hmap_zpl_add_entry :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 { entry : HMapZPL_Entry(Type) = { key, -1, {} } id := cast(i64) entries.num array_append( & entries, entry ) return id } -hamp_zpl_find :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> HMapZPL_FindResult +hmap_zpl_find :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> HMapZPL_FindResult { // profile(#procedure) result : HMapZPL_FindResult = { -1, -1, -1 } @@ -271,7 +271,7 @@ hamp_zpl_find :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> HMapZPL_F return result } -hamp_zpl_full :: proc( using self : ^ HMapZPL( $ Type) ) -> b32 { +hmap_zpl_full :: proc( using self : ^ HMapZPL( $ Type) ) -> b32 { critical_load := u64(HMapZPL_CritialLoadScale * cast(f64) table.num) result : b32 = entries.num > critical_load return result diff --git a/code/grime/mappings.odin b/code/grime/mappings.odin index 3bb02ef..f0b221b 100644 --- a/code/grime/mappings.odin +++ b/code/grime/mappings.odin @@ -145,8 +145,8 @@ is_power_of_two :: proc { make :: proc { array_init, - // hmap_chained_init, - // hmap_zpl_init, + hmap_chained_init, + hmap_zpl_init, // Usual make_slice, @@ -161,6 +161,10 @@ push :: proc { stack_push, } +to_runes :: proc { + string_to_runes, +} + to_string :: proc { runes_to_string, str_builder_to_string, diff --git a/code/sectr/grime/string_interning.odin b/code/grime/string_cache.odin similarity index 65% rename from code/sectr/grime/string_interning.odin rename to code/grime/string_cache.odin index abe5ecd..931b3e4 100644 --- a/code/sectr/grime/string_interning.odin +++ b/code/grime/string_cache.odin @@ -8,7 +8,7 @@ TODO(Ed): Move the string cache to its own virtual arena? Its going to be used heavily and we can better utilize memory that way. The arena can deal with alignment just fine or we can pad in a min amount per string. */ -package sectr +package grime import "base:runtime" import "core:mem" @@ -34,7 +34,11 @@ StringCache :: struct { table : HMapZPL(StrRunesPair), } -str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) { +// This is the default string cache for the runtime module. +Module_String_Cache : ^StringCache + +str_cache_init :: proc( table_allocator, slabs_allocator : Allocator ) -> (cache : StringCache) +{ alignment := uint(mem.DEFAULT_ALIGNMENT) policy : SlabPolicy @@ -60,54 +64,47 @@ str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) { @static dbg_name := "StringCache slab" - state := get_state() - alloc_error : AllocatorError - cache.slab, alloc_error = slab_init( & policy, dbg_name = dbg_name, allocator = persistent_allocator() ) + cache.slab, alloc_error = slab_init( & policy, allocator = slabs_allocator, dbg_name = dbg_name ) verify(alloc_error == .None, "Failed to initialize the string cache" ) - cache.table, alloc_error = make( HMapZPL(StrRunesPair), 4 * Megabyte, persistent_allocator(), dbg_name ) + cache.table, alloc_error = make( HMapZPL(StrRunesPair), 4 * Megabyte, table_allocator, dbg_name ) return } -str_intern_key :: #force_inline proc( content : string ) -> StringKey { return cast(StringKey) crc32( transmute([]byte) content ) } -str_intern_lookup :: #force_inline proc( key : StringKey ) -> (^StrRunesPair) { return hamp_zpl_get( & get_state().string_cache.table, transmute(u64) key ) } +str_cache_reload :: #force_inline proc ( cache : ^StringCache, table_allocator, slabs_allocator : Allocator ) { + slab_reload( cache.slab, table_allocator ) + hmap_zpl_reload( & cache.table, slabs_allocator ) +} + +str_cache_set_module_ctx :: #force_inline proc "contextless" ( cache : ^StringCache ) { Module_String_Cache = cache } +str_intern_key :: #force_inline proc( content : string ) -> StringKey { return cast(StringKey) crc32( transmute([]byte) content ) } +str_intern_lookup :: #force_inline proc( key : StringKey ) -> (^StrRunesPair) { return hmap_zpl_get( & Module_String_Cache.table, transmute(u64) key ) } str_intern :: proc( content : string ) -> StrRunesPair { // profile(#procedure) - cache := & get_state().string_cache - + cache := Module_String_Cache key := str_intern_key(content) - result := hamp_zpl_get( & cache.table, transmute(u64) key ) + result := hmap_zpl_get( & cache.table, transmute(u64) key ) if result != nil { return (result ^) } - // profile_begin("new entry") - { - length := len(content) - // str_mem, alloc_error := alloc( length, mem.DEFAULT_ALIGNMENT ) - str_mem, alloc_error := slab_alloc( cache.slab, uint(length), uint(mem.DEFAULT_ALIGNMENT), zero_memory = false ) - verify( alloc_error == .None, "String cache had a backing allocator error" ) + length := len(content) + str_mem, alloc_error := slab_alloc( cache.slab, uint(length), uint(mem.DEFAULT_ALIGNMENT), zero_memory = false ) + verify( alloc_error == .None, "String cache had a backing allocator error" ) - // copy_non_overlapping( str_mem, raw_data(content), length ) - copy_non_overlapping( raw_data(str_mem), raw_data(content), length ) + copy_non_overlapping( raw_data(str_mem), raw_data(content), length ) - runes : []rune - // runes, alloc_error = to_runes( content, persistent_allocator() ) - runes, alloc_error = to_runes( content, slab_allocator(cache.slab) ) - verify( alloc_error == .None, "String cache had a backing allocator error" ) + runes : []rune + runes, alloc_error = to_runes( content, slab_allocator(cache.slab) ) + verify( alloc_error == .None, "String cache had a backing allocator error" ) + // slab_validate_pools( cache.slab.backing ) - slab_validate_pools( get_state().persistent_slab ) - - // result, alloc_error = hamp_zpl_set( & cache.table, key, StrRunesPair { transmute(string) byte_slice(str_mem, length), runes } ) - result, alloc_error = hamp_zpl_set( & cache.table, transmute(u64) key, StrRunesPair { transmute(string) str_mem, runes } ) - verify( alloc_error == .None, "String cache had a backing allocator error" ) - - slab_validate_pools( get_state().persistent_slab ) - } - // profile_end() + result, alloc_error = hmap_zpl_set( & cache.table, transmute(u64) key, StrRunesPair { transmute(string) str_mem, runes } ) + verify( alloc_error == .None, "String cache had a backing allocator error" ) + // slab_validate_pools( cache.slab.backing ) return (result ^) } diff --git a/code/sectr/engine/client_api.odin b/code/sectr/engine/client_api.odin index 72b2bd3..1509050 100644 --- a/code/sectr/engine/client_api.odin +++ b/code/sectr/engine/client_api.odin @@ -99,7 +99,8 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem transient_clear_time = 120 // Seconds, 2 Minutes - string_cache = str_cache_init() + string_cache = str_cache_init( persistent_allocator(), persistent_allocator() ) + str_cache_set_module_ctx( & string_cache ) } // Setup input frame poll references @@ -424,8 +425,8 @@ reload :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, hmap_chained_reload( font_provider_data.font_cache, persistent_allocator()) - slab_reload( string_cache.slab, persistent_allocator() ) - hamp_zpl_reload( & string_cache.table, persistent_slab_allocator()) + str_cache_reload( & string_cache, persistent_allocator(), persistent_allocator() ) + str_cache_set_module_ctx( & string_cache ) slab_reload( frame_slab, frame_allocator()) slab_reload( transient_slab, transient_allocator()) diff --git a/code/sectr/grime/Readme.md b/code/sectr/grime/Readme.md new file mode 100644 index 0000000..7fa0a88 --- /dev/null +++ b/code/sectr/grime/Readme.md @@ -0,0 +1,3 @@ +# Sectr's Grime + +This just contains the mappings file and some stuff I haven't felt like lifting to the grime package yet. diff --git a/code/sectr/grime/mappings.odin b/code/sectr/grime/mappings.odin index ee259be..e3d8a24 100644 --- a/code/sectr/grime/mappings.odin +++ b/code/sectr/grime/mappings.odin @@ -174,6 +174,13 @@ import "codebase:grime" hmap_chained_set :: grime.hmap_chained_set hmap_chained_reload :: grime.hmap_chained_reload + HMapZPL :: grime.HMapZPL + + hmap_zpl_init :: grime.hmap_zpl_init + hmap_zpl_get :: grime.hmap_zpl_get + hmap_zpl_reload :: grime.hmap_zpl_reload + hmap_zpl_set :: grime.hmap_zpl_set + Pool :: grime.Pool Slab :: grime.Slab @@ -234,12 +241,25 @@ import "codebase:grime" memtracker_register_auto_name_slice :: grime.memtracker_register_auto_name_slice memtracker_unregister :: grime.memtracker_unregister - calc_padding_with_header :: grime.calc_padding_with_header memory_after_header :: grime.memory_after_header memory_after :: grime.memory_after swap :: grime.swap + // strings + StrRunesPair :: grime.StrRunesPair + StringCache :: grime.StringCache + + str_cache_init :: grime.str_cache_init + str_cache_reload :: grime.str_cache_reload + str_cache_set_module_ctx :: grime.str_cache_set_module_ctx + // str_intern_key :: grime.str_intern_key + // str_intern_lookup :: grime.str_intern_lookup + str_intern :: grime.str_intern + str_intern_fmt :: grime.str_intern_fmt + + to_str_runes_pair_via_string :: grime.to_str_runes_pair_via_string + to_str_runes_pair_via_runes :: grime.to_str_runes_pair_via_runes // profiler SpallProfiler :: grime.SpallProfiler diff --git a/code/sectr/ui/core/base.odin b/code/sectr/ui/core/base.odin index 7abced0..76d43f0 100644 --- a/code/sectr/ui/core/base.odin +++ b/code/sectr/ui/core/base.odin @@ -151,7 +151,7 @@ ui_reload :: proc( ui : ^ UI_State, cache_allocator : Allocator ) { // We need to repopulate Allocator references for & cache in ui.caches { - hamp_zpl_reload( & cache, cache_allocator) + hmap_zpl_reload( & cache, cache_allocator) } ui.render_queue.backing = cache_allocator } diff --git a/code/sectr/ui/core/box.odin b/code/sectr/ui/core/box.odin index 4c888df..b10f72d 100644 --- a/code/sectr/ui/core/box.odin +++ b/code/sectr/ui/core/box.odin @@ -70,7 +70,7 @@ ui_box_equal :: #force_inline proc "contextless" ( a, b : ^ UI_Box ) -> b32 { } ui_box_from_key :: #force_inline proc ( cache : ^HMapZPL(UI_Box), key : UI_Key ) -> (^UI_Box) { - return hamp_zpl_get( cache, cast(u64) key ) + return hmap_zpl_get( cache, cast(u64) key ) } ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) @@ -80,7 +80,7 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) key := ui_key_from_string( label ) curr_box : (^ UI_Box) - prev_box := hamp_zpl_get( prev_cache, cast(u64) key ) + prev_box := hmap_zpl_get( prev_cache, cast(u64) key ) { // profile("Assigning current box") set_result : ^ UI_Box @@ -88,16 +88,16 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) if prev_box != nil { // Previous history was found, copy over previous state. - set_result, set_error = hamp_zpl_set( curr_cache, cast(u64) key, (prev_box ^) ) + set_result, set_error = hmap_zpl_set( curr_cache, cast(u64) key, (prev_box ^) ) } else { box : UI_Box box.key = key box.label = str_intern( label ) - set_result, set_error = hamp_zpl_set( curr_cache, cast(u64) key, box ) + set_result, set_error = hmap_zpl_set( curr_cache, cast(u64) key, box ) } - verify( set_error == AllocatorError.None, "Failed to set hamp_zpl due to allocator error" ) + verify( set_error == AllocatorError.None, "Failed to set hmap_zpl due to allocator error" ) curr_box = set_result curr_box.first_frame = prev_box == nil curr_box.flags = flags @@ -122,7 +122,7 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) return curr_box } -ui_prev_cached_box :: #force_inline proc( box : ^UI_Box ) -> ^UI_Box { return hamp_zpl_get( ui_context().prev_cache, cast(u64) box.key ) } +ui_prev_cached_box :: #force_inline proc( box : ^UI_Box ) -> ^UI_Box { return hmap_zpl_get( ui_context().prev_cache, cast(u64) box.key ) } ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box) { diff --git a/code/sectr/ui/core/signal.odin b/code/sectr/ui/core/signal.odin index 6f2227e..ba2af5e 100644 --- a/code/sectr/ui/core/signal.odin +++ b/code/sectr/ui/core/signal.odin @@ -156,7 +156,7 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas prev := ui_box_from_key( ui.curr_cache, ui.hot ) prev.hot_delta = 0 } - // prev_hot := hamp_zpl_get( ui.prev_cache, u64(ui.hot) ) + // prev_hot := hmap_zpl_get( ui.prev_cache, u64(ui.hot) ) // prev_hot_label := prev_hot != nil ? prev_hot.label.str : "" // log( str_fmt_tmp("Detected HOT via CURSOR OVER: %v is_hot: %v is_active: %v prev_hot: %v", box.label.str, is_hot, is_active, prev_hot_label )) ui.hot = box.key