Files
raddebugger/src/ui/ui_core.c
T

3464 lines
110 KiB
C

// Copyright (c) 2024 Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)
#undef MARKUP_LAYER_COLOR
#define MARKUP_LAYER_COLOR 0.80f, 0.40f, 0.35f
////////////////////////////////
//~ rjf: Globals
thread_static UI_State *ui_state = 0;
////////////////////////////////
//~ rjf: Basic Type Functions
#if !defined(XXH_IMPLEMENTATION)
# define XXH_IMPLEMENTATION
# define XXH_STATIC_LINKING_ONLY
# include "third_party/xxHash/xxhash.h"
#endif
internal U64
ui_hash_from_string(U64 seed, String8 string)
{
U64 result = XXH3_64bits_withSeed(string.str, string.size, seed);
return result;
}
internal String8
ui_hash_part_from_key_string(String8 string)
{
String8 result = string;
// rjf: look for ### patterns, which can replace the entirety of the part of
// the string that is hashed.
U64 hash_replace_signifier_pos = str8_find_needle(string, 0, str8_lit("###"), 0);
if(hash_replace_signifier_pos < string.size)
{
result = str8_skip(string, hash_replace_signifier_pos);
}
return result;
}
internal String8
ui_display_part_from_key_string(String8 string)
{
U64 hash_pos = str8_find_needle(string, 0, str8_lit("##"), 0);
string.size = hash_pos;
return string;
}
internal UI_Key
ui_key_zero(void)
{
UI_Key result = {0};
return result;
}
internal UI_Key
ui_key_make(U64 v)
{
UI_Key result = {v};
return result;
}
internal UI_Key
ui_key_from_string(UI_Key seed_key, String8 string)
{
ProfBeginFunction();
UI_Key result = {0};
if(string.size != 0)
{
String8 hash_part = ui_hash_part_from_key_string(string);
result.u64[0] = ui_hash_from_string(seed_key.u64[0], hash_part);
}
ProfEnd();
return result;
}
internal UI_Key
ui_key_from_stringf(UI_Key seed_key, char *fmt, ...)
{
Temp scratch = scratch_begin(0, 0);
va_list args;
va_start(args, fmt);
String8 string = push_str8fv(scratch.arena, fmt, args);
va_end(args);
UI_Key key = ui_key_from_string(seed_key, string);
scratch_end(scratch);
return key;
}
internal B32
ui_key_match(UI_Key a, UI_Key b)
{
return a.u64[0] == b.u64[0];
}
////////////////////////////////
//~ rjf: Event Type Functions
internal UI_EventNode *
ui_event_list_push(Arena *arena, UI_EventList *list, UI_Event *v)
{
UI_EventNode *n = push_array(arena, UI_EventNode, 1);
MemoryCopyStruct(&n->v, v);
n->v.string = push_str8_copy(arena, n->v.string);
DLLPushBack(list->first, list->last, n);
list->count += 1;
return n;
}
internal void
ui_eat_event_node(UI_EventList *list, UI_EventNode *node)
{
DLLRemove(list->first, list->last, node);
list->count -= 1;
}
////////////////////////////////
//~ rjf: Text Operation Functions
internal B32
ui_char_is_scan_boundary(U8 c)
{
return (char_is_alpha(c) || char_is_digit(c, 10) || c == '_');
}
internal S64
ui_scanned_column_from_column(String8 string, S64 start_column, Side side)
{
S64 new_column = start_column;
S64 delta = (!!side)*2 - 1;
B32 found_text = 0;
B32 found_non_space = 0;
S64 start_off = delta < 0 ? delta : 0;
for(S64 col = start_column+start_off; 1 <= col && col <= string.size+1; col += delta)
{
U8 byte = (col <= string.size) ? string.str[col-1] : 0;
B32 is_non_space = !char_is_space(byte);
B32 is_name = ui_char_is_scan_boundary(byte);
if(((side == Side_Min) && (col == 1)) ||
((side == Side_Max) && (col == string.size+1)) ||
(found_non_space && !is_non_space) ||
(found_text && !is_name))
{
new_column = col + (!side && col != 1);
break;
}
else if (!found_text && is_name)
{
found_text = 1;
}
else if (!found_non_space && is_non_space)
{
found_non_space = 1;
}
}
return new_column;
}
internal UI_TxtOp
ui_single_line_txt_op_from_event(Arena *arena, UI_Event *event, String8 string, TxtPt cursor, TxtPt mark)
{
TxtPt next_cursor = cursor;
TxtPt next_mark = mark;
TxtRng range = {0};
String8 replace = {0};
String8 copy = {0};
UI_TxtOpFlags flags = 0;
Vec2S32 delta = event->delta_2s32;
Vec2S32 original_delta = delta;
//- rjf: resolve high-level delta into byte delta, based on unit
switch(event->delta_unit)
{
default:{}break;
case UI_EventDeltaUnit_Char:
{
// TODO(rjf): this should account for multi-byte characters in UTF-8... for now, just assume ASCII and
// no-op
}break;
case UI_EventDeltaUnit_Word:
{
delta.x = (S32)ui_scanned_column_from_column(string, cursor.column, delta.x > 0 ? Side_Max : Side_Min) - cursor.column;
}break;
case UI_EventDeltaUnit_Line:
case UI_EventDeltaUnit_Whole:
case UI_EventDeltaUnit_Page:
{
S64 first_nonwhitespace_column = 1;
for(U64 idx = 0; idx < string.size; idx += 1)
{
if(!char_is_space(string.str[idx]))
{
first_nonwhitespace_column = (S64)idx + 1;
break;
}
}
S64 home_dest_column = (cursor.column == first_nonwhitespace_column) ? 1 : first_nonwhitespace_column;
delta.x = (delta.x > 0) ? ((S64)string.size+1 - cursor.column) : (home_dest_column - cursor.column);
}break;
}
//- rjf: zero delta
if(!txt_pt_match(cursor, mark) && event->flags & UI_EventFlag_ZeroDeltaOnSelect)
{
delta = v2s32(0, 0);
}
//- rjf: form next cursor
if(txt_pt_match(cursor, mark) || !(event->flags & UI_EventFlag_ZeroDeltaOnSelect))
{
next_cursor.column += delta.x;
}
//- rjf: cap at line
if(event->flags & UI_EventFlag_CapAtLine)
{
next_cursor.column = Clamp(1, next_cursor.column, (S64)(string.size+1));
}
//- rjf: in some cases, we want to pick a selection side based on the delta
if(!txt_pt_match(cursor, mark) && event->flags & UI_EventFlag_PickSelectSide)
{
if(original_delta.x < 0 || original_delta.y < 0)
{
next_cursor = next_mark = txt_pt_min(cursor, mark);
}
else if(original_delta.x > 0 || original_delta.y > 0)
{
next_cursor = next_mark = txt_pt_max(cursor, mark);
}
}
//- rjf: copying
if(event->flags & UI_EventFlag_Copy)
{
if(cursor.line == mark.line)
{
copy = str8_substr(string, r1u64(cursor.column-1, mark.column-1));
flags |= UI_TxtOpFlag_Copy;
}
else
{
flags |= UI_TxtOpFlag_Invalid;
}
}
//- rjf: pasting
if(event->flags & UI_EventFlag_Paste)
{
range = txt_rng(cursor, mark);
replace = os_get_clipboard_text(arena);
next_cursor = next_mark = txt_pt(cursor.line, cursor.column+replace.size);
}
//- rjf: deletion
if(event->flags & UI_EventFlag_Delete)
{
TxtPt new_pos = txt_pt_min(next_cursor, next_mark);
range = txt_rng(next_cursor, next_mark);
replace = str8_lit("");
next_cursor = next_mark = new_pos;
}
//- rjf: stick mark to cursor, when we don't want to keep it in the same spot
if(!(event->flags & UI_EventFlag_KeepMark))
{
next_mark = next_cursor;
}
//- rjf: insertion
if(event->string.size != 0)
{
range = txt_rng(cursor, mark);
replace = push_str8_copy(arena, event->string);
next_cursor = next_mark = txt_pt(range.min.line, range.min.column + event->string.size);
}
//- rjf: determine if this event should be taken, based on bounds of cursor
{
if(next_cursor.column > string.size+1 || 1 > next_cursor.column || event->delta_2s32.y != 0)
{
flags |= UI_TxtOpFlag_Invalid;
}
next_cursor.column = Clamp(1, next_cursor.column, string.size+replace.size+1);
next_mark.column = Clamp(1, next_mark.column, string.size+replace.size+1);
}
//- rjf: build+fill
UI_TxtOp op = {0};
{
op.flags = flags;
op.replace = replace;
op.copy = copy;
op.range = range;
op.cursor = next_cursor;
op.mark = next_mark;
}
return op;
}
internal String8
ui_push_string_replace_range(Arena *arena, String8 string, Rng1S64 col_range, String8 replace)
{
//- rjf: convert to offset range
Rng1U64 range =
{
(U64)(col_range.min-1),
(U64)(col_range.max-1),
};
//- rjf: clamp range
if(range.min > string.size)
{
range.min = 0;
}
if(range.max > string.size)
{
range.max = string.size;
}
//- rjf: calculate new size
U64 old_size = string.size;
U64 new_size = old_size - (range.max - range.min) + replace.size;
//- rjf: push+fill new string storage
U8 *push_base = push_array(arena, U8, new_size);
{
MemoryCopy(push_base+0, string.str, range.min);
MemoryCopy(push_base+range.min+replace.size, string.str+range.max, string.size-range.max);
if(replace.str != 0)
{
MemoryCopy(push_base+range.min, replace.str, replace.size);
}
}
String8 result = str8(push_base, new_size);
return result;
}
////////////////////////////////
//~ rjf: Sizes
internal UI_Size
ui_size(UI_SizeKind kind, F32 value, F32 strictness)
{
UI_Size size = {kind, value, strictness};
return size;
}
////////////////////////////////
//~ rjf: Scroll Point Type Functions
internal UI_ScrollPt
ui_scroll_pt(S64 idx, F32 off)
{
UI_ScrollPt pt = {idx, off};
return pt;
}
internal void
ui_scroll_pt_target_idx(UI_ScrollPt *v, S64 idx)
{
v->off = mod_f32(v->off, 1.f) + (F32)(v->idx+(S64)v->off - idx);
v->idx = idx;
}
internal void
ui_scroll_pt_clamp_idx(UI_ScrollPt *v, Rng1S64 range)
{
if(v->idx < range.min || range.max < v->idx)
{
S64 clamped = range.min;
ui_scroll_pt_target_idx(v, clamped);
}
}
////////////////////////////////
//~ rjf: Boxes
internal B32
ui_box_is_nil(UI_Box *box)
{
return box == 0 || box == &ui_nil_box;
}
internal UI_BoxRec
ui_box_rec_df(UI_Box *box, UI_Box *root, U64 sib_member_off, U64 child_member_off)
{
UI_BoxRec result = {0};
result.next = &ui_nil_box;
if(!ui_box_is_nil(*MemberFromOffset(UI_Box **, box, child_member_off)))
{
result.next = *MemberFromOffset(UI_Box **, box, child_member_off);
result.push_count = 1;
}
else for(UI_Box *p = box; !ui_box_is_nil(p) && p != root; p = p->parent)
{
if(!ui_box_is_nil(*MemberFromOffset(UI_Box **, p, sib_member_off)))
{
result.next = *MemberFromOffset(UI_Box **, p, sib_member_off);
break;
}
result.pop_count += 1;
}
return result;
}
internal void
ui_box_list_push(Arena *arena, UI_BoxList *list, UI_Box *box)
{
UI_BoxNode *n = push_array(arena, UI_BoxNode, 1);
n->box = box;
SLLQueuePush(list->first, list->last, n);
list->count += 1;
}
////////////////////////////////
//~ rjf: State Building / Selecting
internal UI_State *
ui_state_alloc(void)
{
Arena *arena = arena_alloc();
UI_State *ui = push_array(arena, UI_State, 1);
ui->arena = arena;
ui->external_key = ui_key_from_string(ui_key_zero(), str8_lit("###external_interaction_key###"));
ui->build_arenas[0] = arena_alloc();
ui->build_arenas[1] = arena_alloc();
ui->drag_state_arena = arena_alloc();
ui->string_hover_arena = arena_alloc();
ui->box_table_size = 4096;
ui->box_table = push_array(arena, UI_BoxHashSlot, ui->box_table_size);
ui->anim_slots_count = 4096;
ui->anim_slots = push_array(arena, UI_AnimSlot, ui->anim_slots_count);
UI_InitStackNils(ui);
return ui;
}
internal void
ui_state_release(UI_State *state)
{
arena_release(state->string_hover_arena);
arena_release(state->drag_state_arena);
for(int i = 0; i < ArrayCount(state->build_arenas); i += 1)
{
arena_release(state->build_arenas[i]);
}
arena_release(state->arena);
}
internal UI_Box *
ui_root_from_state(UI_State *state)
{
return state->root;
}
internal B32
ui_animating_from_state(UI_State *state)
{
return state->is_animating;
}
internal void
ui_select_state(UI_State *state)
{
ui_state = state;
}
internal UI_State *
ui_get_selected_state(void)
{
return ui_state;
}
////////////////////////////////
//~ rjf: Implicit State Accessors/Mutators
//- rjf: per-frame info
internal Arena *
ui_build_arena(void)
{
Arena *result = ui_state->build_arenas[ui_state->build_index%ArrayCount(ui_state->build_arenas)];
return result;
}
internal OS_Handle
ui_window(void)
{
return ui_state->window;
}
internal Vec2F32
ui_mouse(void)
{
return ui_state->mouse;
}
internal FNT_Tag
ui_icon_font(void)
{
return ui_state->icon_info.icon_font;
}
internal String8
ui_icon_string_from_kind(UI_IconKind icon_kind)
{
return ui_state->icon_info.icon_kind_text_map[icon_kind];
}
internal F32
ui_dt(void)
{
return ui_state->animation_dt;
}
//- rjf: event pumping
internal B32
ui_next_event(UI_Event **ev)
{
UI_EventList *events = ui_state->events;
UI_EventNode *start_node = events->first;
if(ev[0] != 0)
{
start_node = CastFromMember(UI_EventNode, v, ev[0]);
start_node = start_node->next;
ev[0] = 0;
}
if(start_node != 0)
{
UI_PermissionFlags perms = ui_top_permission_flags();
for(UI_EventNode *n = start_node; n != 0; n = n->next)
{
B32 good = 1;
if(!(perms & UI_PermissionFlag_ClicksLeft) &&
(n->v.kind == UI_EventKind_Press ||
n->v.kind == UI_EventKind_Release) &&
(n->v.key == OS_Key_LeftMouseButton))
{
good = 0;
}
if(!(perms & UI_PermissionFlag_ClicksMiddle) &&
(n->v.kind == UI_EventKind_Press ||
n->v.kind == UI_EventKind_Release) &&
(n->v.key == OS_Key_MiddleMouseButton))
{
good = 0;
}
if(!(perms & UI_PermissionFlag_ClicksRight) &&
(n->v.kind == UI_EventKind_Press ||
n->v.kind == UI_EventKind_Release) &&
(n->v.key == OS_Key_RightMouseButton))
{
good = 0;
}
if(!(perms & UI_PermissionFlag_ScrollX) && (n->v.kind == UI_EventKind_Scroll) && (n->v.delta_2f32.x != 0 || n->v.modifiers & OS_Modifier_Shift))
{
good = 0;
}
if(!(perms & UI_PermissionFlag_ScrollY) && (n->v.kind == UI_EventKind_Scroll) && n->v.delta_2f32.y != 0 && !(n->v.modifiers & OS_Modifier_Shift))
{
good = 0;
}
if(!(perms & UI_PermissionFlag_Keyboard) &&
(n->v.kind == UI_EventKind_Press ||
n->v.kind == UI_EventKind_Release) &&
(n->v.key != OS_Key_LeftMouseButton &&
n->v.key != OS_Key_MiddleMouseButton &&
n->v.key != OS_Key_RightMouseButton))
{
good = 0;
}
else if(!(perms & UI_PermissionFlag_Text) && (n->v.kind == UI_EventKind_Text))
{
good = 0;
}
if(good)
{
ev[0] = &n->v;
break;
}
}
}
B32 result = !!ev[0];
return result;
}
internal void
ui_eat_event(UI_Event *ev)
{
if(ev != 0)
{
UI_EventNode *n = CastFromMember(UI_EventNode, v, ev);
ui_eat_event_node(ui_state->events, n);
}
}
//- rjf: event consumption helpers
internal B32
ui_key_press(OS_Modifiers mods, OS_Key key)
{
B32 result = 0;
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
if(evt->kind == UI_EventKind_Press && evt->key == key && evt->modifiers == mods)
{
result = 1;
ui_eat_event(evt);
break;
}
}
return result;
}
internal B32
ui_key_release(OS_Modifiers mods, OS_Key key)
{
B32 result = 0;
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
if(evt->kind == UI_EventKind_Release && evt->key == key && evt->modifiers == mods)
{
result = 1;
ui_eat_event(evt);
break;
}
}
return result;
}
internal B32
ui_text(U32 character)
{
B32 result = 0;
Temp scratch = scratch_begin(0, 0);
String8 character_text = str8_from_32(scratch.arena, str32(&character, 1));
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
if(evt->kind == UI_EventKind_Text && str8_match(character_text, evt->string, 0))
{
result = 1;
ui_eat_event(evt);
break;
}
}
scratch_end(scratch);
return result;
}
internal B32
ui_slot_press(UI_EventActionSlot slot)
{
B32 result = 0;
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
if(evt->kind == UI_EventKind_Press && evt->slot == slot)
{
result = 1;
ui_eat_event(evt);
break;
}
}
return result;
}
//- rjf: drag data
internal Vec2F32
ui_drag_start_mouse(void)
{
return ui_state->drag_start_mouse;
}
internal Vec2F32
ui_drag_delta(void)
{
return sub_2f32(ui_mouse(), ui_state->drag_start_mouse);
}
internal void
ui_store_drag_data(String8 string)
{
arena_clear(ui_state->drag_state_arena);
ui_state->drag_state_data = push_str8_copy(ui_state->drag_state_arena, string);
}
internal String8
ui_get_drag_data(U64 min_required_size)
{
if(ui_state->drag_state_data.size < min_required_size)
{
Temp scratch = scratch_begin(0, 0);
String8 str = {push_array(scratch.arena, U8, min_required_size), min_required_size};
ui_store_drag_data(str);
scratch_end(scratch);
}
return ui_state->drag_state_data;
}
//- rjf: hovered string info
internal B32
ui_string_hover_active(void)
{
return (ui_state->build_index > 0 && ui_state->string_hover_build_index >= ui_state->build_index-1 &&
os_now_microseconds() >= ui_state->string_hover_begin_us + 500000);
}
internal U64
ui_string_hover_begin_time_us(void)
{
return ui_state->string_hover_begin_us;
}
internal DR_FStrList
ui_string_hover_fstrs(Arena *arena)
{
DR_FStrList result = dr_fstrs_copy(arena, &ui_state->string_hover_fstrs);
return result;
}
//- rjf: interaction keys
internal UI_Key
ui_hot_key(void)
{
return ui_state->hot_box_key;
}
internal UI_Key
ui_active_key(UI_MouseButtonKind button_kind)
{
return ui_state->active_box_key[button_kind];
}
internal UI_Key
ui_drop_hot_key(void)
{
return ui_state->drop_hot_box_key;
}
//- rjf: controls over interaction
internal void
ui_kill_action(void)
{
for EachEnumVal(UI_MouseButtonKind, k)
{
ui_state->active_box_key[k] = ui_key_zero();
}
}
//- rjf: box cache lookup
internal UI_Box *
ui_box_from_key(UI_Key key)
{
ProfBeginFunction();
UI_Box *result = &ui_nil_box;
if(!ui_key_match(key, ui_key_zero()))
{
U64 slot = key.u64[0] % ui_state->box_table_size;
for(UI_Box *b = ui_state->box_table[slot].hash_first; !ui_box_is_nil(b); b = b->hash_next)
{
if(ui_key_match(b->key, key))
{
result = b;
break;
}
}
}
ProfEnd();
return result;
}
////////////////////////////////
//~ rjf: Top-Level Building API
internal void
ui_begin_build(OS_Handle window, UI_EventList *events, UI_IconInfo *icon_info, UI_Theme *theme, UI_AnimationInfo *animation_info, F32 real_dt, F32 animation_dt)
{
//- rjf: reset per-build ui state
{
UI_InitStacks(ui_state);
ui_state->root = &ui_nil_box;
ui_state->ctx_menu_touched_this_frame = 0;
ui_state->is_animating = 0;
ui_state->clipboard_copy_key = ui_key_zero();
ui_state->last_build_box_count = ui_state->build_box_count;
ui_state->build_box_count = 0;
ui_state->tooltip_open = 0;
ui_state->ctx_menu_changed = 0;
ui_state->default_animation_rate = 1 - pow_f32(2, (-60.f * ui_state->animation_dt));
ui_state->tooltip_can_overflow_window = 0;
ui_state->tags_key_stack_top = ui_state->tags_key_stack_free = 0;
ui_state->tags_cache_slots_count = 512;
ui_state->tags_cache_slots = push_array(ui_build_arena(), UI_TagsCacheSlot, ui_state->tags_cache_slots_count);
ui_state->theme_pattern_cache_slots_count = 512;
ui_state->theme_pattern_cache_slots = push_array(ui_build_arena(), UI_ThemePatternCacheSlot, ui_state->theme_pattern_cache_slots_count);
}
//- rjf: prune unused animation nodes
ProfScope("ui prune unused animation nodes")
{
for(UI_AnimNode *n = ui_state->lru_anim_node, *next = &ui_nil_anim_node; n != &ui_nil_anim_node && n != 0; n = next)
{
next = n->lru_next;
if(n->last_touched_build_index+2 < ui_state->build_index)
{
U64 slot_idx = n->key.u64[0]%ui_state->anim_slots_count;
UI_AnimSlot *slot = &ui_state->anim_slots[slot_idx];
DLLRemove_NPZ(&ui_nil_anim_node, slot->first, slot->last, n, slot_next, slot_prev);;
DLLRemove_NPZ(&ui_nil_anim_node, ui_state->lru_anim_node, ui_state->mru_anim_node, n, lru_next, lru_prev);
SLLStackPush_N(ui_state->free_anim_node, n, slot_next);
}
else
{
break;
}
}
}
//- rjf: detect mouse-moves
for(UI_EventNode *n = events->first; n != 0; n = n->next)
{
if(n->v.kind == UI_EventKind_MouseMove)
{
ui_state->last_time_mousemoved_us = os_now_microseconds();
}
}
//- rjf: detect external press & holds
for EachEnumVal(UI_MouseButtonKind, k)
{
if(ui_key_match(ui_state->active_box_key[k], ui_key_zero()) && os_key_is_down(OS_Key_LeftMouseButton+k))
{
ui_state->active_box_key[k] = ui_state->external_key;
}
else if(ui_key_match(ui_state->active_box_key[k], ui_state->external_key) && !os_key_is_down(OS_Key_LeftMouseButton+k))
{
ui_state->active_box_key[k] = ui_key_zero();
}
}
//- rjf: fill build phase parameters
{
ui_state->theme = theme;
ui_state->events = events;
ui_state->window = window;
ui_state->mouse = (os_window_is_focused(window) || ui_state->last_time_mousemoved_us+500000 >= os_now_microseconds()) ? os_mouse_from_window(window) : v2f32(-100, -100);
ui_state->animation_dt = animation_dt;
MemoryZeroStruct(&ui_state->icon_info);
ui_state->icon_info.icon_font = icon_info->icon_font;
for(UI_IconKind icon_kind = UI_IconKind_Null;
icon_kind < UI_IconKind_COUNT;
icon_kind = (UI_IconKind)(icon_kind + 1))
{
ui_state->icon_info.icon_kind_text_map[icon_kind] = push_str8_copy(ui_build_arena(), icon_info->icon_kind_text_map[icon_kind]);
}
MemoryCopyStruct(&ui_state->animation_info, animation_info);
}
//- rjf: do default navigation
{
Temp scratch = scratch_begin(0, 0);
if(!ui_key_match(ui_state->default_nav_root_key, ui_key_zero()))
{
UI_Box *nav_root = ui_box_from_key(ui_state->default_nav_root_key);
if(!ui_box_is_nil(nav_root))
{
//- rjf: no child has the active focus -> do navigation at this layer
if(ui_key_match(ui_key_zero(), nav_root->default_nav_focus_active_key))
{
for(;;)
{
B32 moved = 0;
UI_Box *focus_box = ui_box_from_key(nav_root->default_nav_focus_next_hot_key);
UI_BoxList next_focus_box_candidates = {0};
// rjf: gather & consume events & nav actions
B32 nav_next = 0;
B32 nav_prev = 0;
Axis2 axis_lock = Axis2_Invalid;
if(ui_key_press(0, OS_Key_Tab))
{
nav_next = 1;
}
if(ui_key_press(OS_Modifier_Shift, OS_Key_Tab))
{
nav_prev = 1;
}
for(UI_EventNode *node = events->first, *next = 0; node != 0; node = next)
{
next = node->next;
B32 taken = 0;
if(node->v.delta_2s32.x == 0 && node->v.delta_2s32.y == 0)
{
continue;
}
if(((node->v.delta_2s32.x > 0 && nav_root->flags & UI_BoxFlag_DefaultFocusNavX) || node->v.delta_2s32.x == 0) &&
((node->v.delta_2s32.y > 0 && nav_root->flags & UI_BoxFlag_DefaultFocusNavY) || node->v.delta_2s32.y == 0))
{
taken = 1;
nav_next = 1;
}
if(((node->v.delta_2s32.x < 0 && nav_root->flags & UI_BoxFlag_DefaultFocusNavX) || node->v.delta_2s32.x == 0) &&
((node->v.delta_2s32.y < 0 && nav_root->flags & UI_BoxFlag_DefaultFocusNavY) || node->v.delta_2s32.y == 0))
{
taken = 1;
nav_prev = 1;
}
if(node->v.flags & UI_EventFlag_ExplicitDirectional)
{
axis_lock = node->v.delta_2s32.x != 0 ? Axis2_X : Axis2_Y;
}
if(taken)
{
ui_eat_event_node(events, node);
}
}
// rjf: [+] directional movement
if(nav_next)
{
UI_Box *search_start = ui_box_is_nil(focus_box) ? nav_root : focus_box;
U64 moved_in_axis[Axis2_COUNT] = {0};
moved = 1;
for(UI_Box *box = search_start;;)
{
if(box != search_start && !(box->flags & UI_BoxFlag_FocusNavSkip) && (box->flags & UI_BoxFlag_Clickable || ui_box_is_nil(box)) && (axis_lock == Axis2_Invalid || moved_in_axis[axis_lock] > 0))
{
ui_box_list_push(scratch.arena, &next_focus_box_candidates, box);
if(axis_lock == Axis2_Invalid || moved_in_axis[axis_lock] > 1)
{
break;
}
}
UI_Box *last_box = box;
if(!ui_box_is_nil(box->first))
{
moved_in_axis[box->child_layout_axis] += 1;
box = box->first;
}
else for(UI_Box *p = box; !ui_box_is_nil(p) && p != nav_root; p = p->parent)
{
if(!ui_box_is_nil(p->next))
{
moved_in_axis[p->parent->child_layout_axis] += 1;
box = p->next;
break;
}
}
if(last_box == box)
{
ui_box_list_push(scratch.arena, &next_focus_box_candidates, &ui_nil_box);
break;
}
}
}
// rjf: [-] directional movement
if(nav_prev)
{
UI_Box *search_start = ui_box_is_nil(focus_box) ? nav_root : focus_box;
U64 moved_in_axis[Axis2_COUNT] = {0};
moved = 1;
for(UI_Box *box = search_start;;)
{
if(box != search_start && !(box->flags & UI_BoxFlag_FocusNavSkip) && (box->flags & UI_BoxFlag_Clickable || ui_box_is_nil(box)) && (axis_lock == Axis2_Invalid || moved_in_axis[axis_lock] > 0))
{
ui_box_list_push(scratch.arena, &next_focus_box_candidates, box);
if(axis_lock == Axis2_Invalid || moved_in_axis[axis_lock] > 1)
{
break;
}
}
UI_Box *last_box = box;
UI_Box *root_descendant = &ui_nil_box;
if(box == nav_root && box == search_start)
{
for(UI_Box *d = box->last; !ui_box_is_nil(d); d = d->last)
{
moved_in_axis[d->parent->child_layout_axis] += 1;
root_descendant = d;
}
}
UI_Box *prev_descendant = &ui_nil_box;
for(UI_Box *d = box->prev; !ui_box_is_nil(d); d = d->last)
{
moved_in_axis[d->parent->child_layout_axis] += 1;
prev_descendant = d;
}
if(!ui_box_is_nil(root_descendant))
{
box = root_descendant;
}
else if(!ui_box_is_nil(prev_descendant))
{
box = prev_descendant;
}
else if(box->parent != nav_root)
{
moved_in_axis[box->parent->child_layout_axis] += 1;
box = box->parent;
}
if(box == last_box)
{
ui_box_list_push(scratch.arena, &next_focus_box_candidates, &ui_nil_box);
break;
}
}
}
// rjf: scan candidates and grab next focus box
UI_Box *next_focus_box = focus_box;
F32 best_distance_from_start = 1000000;
for(UI_BoxNode *n = next_focus_box_candidates.first; n != 0; n = n->next)
{
UI_Box *box = n->box;
F32 distance_from_start = 0;
if(axis_lock != Axis2_Invalid)
{
distance_from_start = abs_f32(center_2f32(box->rect).v[axis2_flip(axis_lock)] - center_2f32(focus_box->rect).v[axis2_flip(axis_lock)]);
}
if(distance_from_start < best_distance_from_start && box != focus_box)
{
next_focus_box = box;
best_distance_from_start = distance_from_start;
}
}
// rjf: commit next focus box
nav_root->default_nav_focus_next_hot_key = next_focus_box->key;
// rjf: no movement -> break
if(moved == 0)
{
break;
}
}
}
//- rjf: some child has the active focus -> accept escape keys to pop from the active key stack
if(!ui_key_match(ui_key_zero(), nav_root->default_nav_focus_active_key))
{
for(;ui_slot_press(UI_EventActionSlot_Cancel);)
{
UI_Box *prev_focus_root = nav_root;
for(UI_Box *focus_root = ui_box_from_key(nav_root->default_nav_focus_active_key);
!ui_box_is_nil(focus_root);)
{
UI_Box *next_focus_root = ui_box_from_key(focus_root->default_nav_focus_active_key);
if(ui_box_is_nil(next_focus_root))
{
prev_focus_root->default_nav_focus_next_active_key = ui_key_zero();
break;
}
else
{
prev_focus_root = focus_root;
focus_root = next_focus_root;
}
}
}
}
}
}
ui_state->default_nav_root_key = ui_key_zero();
scratch_end(scratch);
}
//- rjf: next-default-nav-focus keys -> current-default-nav-focus-keys
for(U64 slot_idx = 0; slot_idx < ui_state->box_table_size; slot_idx += 1)
{
for(UI_Box *box = ui_state->box_table[slot_idx].hash_first;
!ui_box_is_nil(box);
box = box->hash_next)
{
box->default_nav_focus_hot_key = box->default_nav_focus_next_hot_key;
box->default_nav_focus_active_key = box->default_nav_focus_next_active_key;
}
}
//- rjf: build top-level root
{
Rng2F32 window_rect = os_client_rect_from_window(window);
Vec2F32 window_rect_size = dim_2f32(window_rect);
ui_set_next_fixed_width(window_rect_size.x);
ui_set_next_fixed_height(window_rect_size.y);
ui_set_next_child_layout_axis(Axis2_X);
UI_Box *root = ui_build_box_from_stringf(0, "###%I64x", window.u64[0]);
ui_push_parent(root);
ui_state->root = root;
}
//- rjf: setup parent box for tooltip
UI_FixedX(ui_state->mouse.x+15.f) UI_FixedY(ui_state->mouse.y+15.f) UI_PrefWidth(ui_children_sum(1.f)) UI_PrefHeight(ui_children_sum(1.f))
{
ui_set_next_child_layout_axis(Axis2_Y);
ui_state->tooltip_root = ui_build_box_from_stringf(0, "###tooltip_%I64x", window.u64[0]);
}
//- rjf: setup parent box for context menu
ui_state->ctx_menu_open = ui_state->next_ctx_menu_open;
ui_state->ctx_menu_anchor_key = ui_state->next_ctx_menu_anchor_key;
{
UI_Box *anchor_box = ui_box_from_key(ui_state->ctx_menu_anchor_key);
if(!ui_box_is_nil(anchor_box))
{
ui_state->ctx_menu_anchor_box_last_pos = anchor_box->rect.p0;
}
Vec2F32 anchor = add_2f32(ui_state->ctx_menu_anchor_box_last_pos, ui_state->ctx_menu_anchor_off);
UI_FixedX(anchor.x) UI_FixedY(anchor.y) UI_PrefWidth(ui_children_sum(1.f)) UI_PrefHeight(ui_children_sum(1.f))
UI_Focus(UI_FocusKind_On)
UI_Squish(0.1f-ui_state->ctx_menu_open_t*0.1f)
UI_Transparency(1-ui_state->ctx_menu_open_t)
{
ui_set_next_child_layout_axis(Axis2_Y);
ui_state->ctx_menu_root = ui_build_box_from_stringf(UI_BoxFlag_Clickable|
UI_BoxFlag_SquishAnchored|
UI_BoxFlag_DrawDropShadow|
(ui_state->ctx_menu_open*UI_BoxFlag_DefaultFocusNavY),
"###ctx_menu_%I64x", window.u64[0]);
}
}
//- rjf: reset hot if we don't have an active widget
{
B32 has_active = 0;
for EachEnumVal(UI_MouseButtonKind, k)
{
if(!ui_key_match(ui_state->active_box_key[k], ui_key_zero()))
{
has_active = 1;
}
}
if(!has_active)
{
ui_state->hot_box_key = ui_key_zero();
}
}
//- rjf: reset drop-hot key
{
ui_state->drop_hot_box_key = ui_key_zero();
}
//- rjf: reset active if our active box is disabled
for EachEnumVal(UI_MouseButtonKind, k)
{
if(!ui_key_match(ui_state->active_box_key[k], ui_key_zero()))
{
UI_Box *box = ui_box_from_key(ui_state->active_box_key[k]);
if(!ui_box_is_nil(box) && box->flags & UI_BoxFlag_Disabled)
{
ui_state->active_box_key[k] = ui_key_zero();
}
}
}
//- rjf: reset active keys if they have been pruned
for EachEnumVal(UI_MouseButtonKind, k)
{
UI_Box *box = ui_box_from_key(ui_state->active_box_key[k]);
if(ui_box_is_nil(box))
{
ui_state->active_box_key[k] = ui_key_zero();
}
}
//- rjf: escape -> close context menu
if(ui_any_ctx_menu_is_open() && ui_slot_press(UI_EventActionSlot_Cancel))
{
ui_ctx_menu_close();
}
}
internal void
ui_end_build(void)
{
ProfBeginFunction();
//- rjf: prune untouched or transient boxes in the cache
ProfScope("ui prune unused boxes")
{
for(U64 slot_idx = 0; slot_idx < ui_state->box_table_size; slot_idx += 1)
{
for(UI_Box *box = ui_state->box_table[slot_idx].hash_first, *next = 0;
!ui_box_is_nil(box);
box = next)
{
next = box->hash_next;
if(box->last_touched_build_index < ui_state->build_index ||
ui_key_match(box->key, ui_key_zero()))
{
DLLRemove_NPZ(&ui_nil_box, ui_state->box_table[slot_idx].hash_first, ui_state->box_table[slot_idx].hash_last, box, hash_next, hash_prev);
SLLStackPush(ui_state->first_free_box, box);
}
}
}
}
//- rjf: layout box tree
ProfScope("ui box tree layout")
{
for(Axis2 axis = (Axis2)0; axis < Axis2_COUNT; axis = (Axis2)(axis + 1))
{
ui_layout_root(ui_state->root, axis);
}
}
//- rjf: close ctx menu if untouched
if(!ui_state->ctx_menu_touched_this_frame)
{
ui_ctx_menu_close();
}
//- rjf: stick ctx menu to anchor
if(ui_state->ctx_menu_touched_this_frame && !ui_state->ctx_menu_changed)
{
UI_Box *anchor_box = ui_box_from_key(ui_state->ctx_menu_anchor_key);
if(!ui_box_is_nil(anchor_box))
{
Rng2F32 root_rect = ui_state->ctx_menu_root->rect;
Vec2F32 pos =
{
anchor_box->rect.x0 + ui_state->ctx_menu_anchor_off.x,
anchor_box->rect.y0 + ui_state->ctx_menu_anchor_off.y,
};
Vec2F32 shift = sub_2f32(pos, root_rect.p0);
Rng2F32 new_root_rect = shift_2f32(root_rect, shift);
ui_state->ctx_menu_root->fixed_position = new_root_rect.p0;
ui_state->ctx_menu_root->fixed_size = dim_2f32(new_root_rect);
ui_state->ctx_menu_root->rect = new_root_rect;
}
}
//- rjf: ensure special floating roots are within screen bounds
UI_Box *floating_roots[] = {ui_state->tooltip_root, ui_state->ctx_menu_root};
B32 force_contain[] =
{
!ui_state->tooltip_can_overflow_window,
1,
};
for(U64 idx = 0; idx < ArrayCount(floating_roots); idx += 1)
{
UI_Box *root = floating_roots[idx];
if(!ui_box_is_nil(root))
{
Rng2F32 window_rect = os_client_rect_from_window(ui_window());
Vec2F32 window_dim = dim_2f32(window_rect);
Rng2F32 root_rect = root->rect;
Vec2F32 shift_down =
{
-ClampBot(0, root_rect.x1 - window_rect.x1) * (force_contain[idx]),
-ClampBot(0, root_rect.y1 - window_rect.y1) * (force_contain[idx]),
};
Rng2F32 new_root_rect = shift_2f32(root_rect, shift_down);
Vec2F32 shift_up =
{
ClampBot(0, window_rect.x0 - new_root_rect.x0) * (force_contain[idx]),
ClampBot(0, window_rect.y0 - new_root_rect.y0) * (force_contain[idx]),
};
new_root_rect = shift_2f32(new_root_rect, shift_up);
root->fixed_position = new_root_rect.p0;
root->fixed_size = dim_2f32(new_root_rect);
root->rect = new_root_rect;
for(Axis2 axis = (Axis2)0; axis < Axis2_COUNT; axis = (Axis2)(axis + 1))
{
ui_calc_sizes_standalone__in_place_rec(root, axis);
ui_calc_sizes_upwards_dependent__in_place_rec(root, axis);
ui_calc_sizes_downwards_dependent__in_place_rec(root, axis);
ui_layout_enforce_constraints__in_place_rec(root, axis);
ui_layout_position__in_place_rec(root, axis);
}
}
}
//- rjf: enforce child-rounding
{
for(U64 slot_idx = 0; slot_idx < ui_state->box_table_size; slot_idx += 1)
{
for(UI_Box *box = ui_state->box_table[slot_idx].hash_first;
!ui_box_is_nil(box);
box = box->hash_next)
{
if(box->flags & UI_BoxFlag_RoundChildrenByParent)
{
for(UI_Box *b = box; !ui_box_is_nil(b); b = ui_box_rec_df_pre(b, box).next)
{
if(floor_f32(b->rect.x0) <= floor_f32(box->rect.x0) &&
floor_f32(b->rect.y0) <= floor_f32(box->rect.y0))
{
b->corner_radii[Corner_00] = box->corner_radii[Corner_00];
}
if(floor_f32(b->rect.x1) >= floor_f32(box->rect.x1) &&
floor_f32(b->rect.y0) <= floor_f32(box->rect.y0))
{
b->corner_radii[Corner_10] = box->corner_radii[Corner_10];
}
if(floor_f32(b->rect.x0) <= floor_f32(box->rect.x0) &&
floor_f32(b->rect.y1) >= floor_f32(box->rect.y1))
{
b->corner_radii[Corner_01] = box->corner_radii[Corner_01];
}
if(floor_f32(b->rect.x1) >= floor_f32(box->rect.x1) &&
floor_f32(b->rect.y1) >= floor_f32(box->rect.y1))
{
b->corner_radii[Corner_11] = box->corner_radii[Corner_11];
}
}
box->first->corner_radii[Corner_00] = box->corner_radii[Corner_00];
box->first->corner_radii[Corner_10] = box->corner_radii[Corner_10];
box->last->corner_radii[Corner_01] = box->corner_radii[Corner_01];
box->last->corner_radii[Corner_11] = box->corner_radii[Corner_11];
}
}
}
}
//- rjf: animate
ProfScope("animate")
{
for(U64 slot_idx = 0; slot_idx < ui_state->anim_slots_count; slot_idx += 1)
{
for(UI_AnimNode *n = ui_state->anim_slots[slot_idx].first;
n != &ui_nil_anim_node && n != 0;
n = n->slot_next)
{
n->current += (n->params.target - n->current) * n->params.rate;
ui_state->is_animating = (ui_state->is_animating || abs_f32(n->params.target - n->current) > n->params.epsilon);
}
}
F32 fast_rate = ui_state->default_animation_rate;
F32 slow_rate = 1 - pow_f32(2, (-30.f * ui_state->animation_dt));
ui_state->ctx_menu_open_t += ((F32)!!ui_state->ctx_menu_open - ui_state->ctx_menu_open_t) * (ui_state->animation_info.flags & UI_AnimationInfoFlag_ContextMenuAnimations ? fast_rate : 1);
ui_state->is_animating = (ui_state->is_animating || abs_f32((F32)!!ui_state->ctx_menu_open - ui_state->ctx_menu_open_t) > 0.01f);
if(ui_state->ctx_menu_open_t >= 0.99f && ui_state->ctx_menu_open)
{
ui_state->ctx_menu_open_t = 1.f;
}
ui_state->tooltip_open_t += ((F32)!!ui_state->tooltip_open - ui_state->tooltip_open_t) * (ui_state->animation_info.flags & UI_AnimationInfoFlag_TooltipAnimations ? fast_rate : 1);
ui_state->is_animating = (ui_state->is_animating || abs_f32((F32)!!ui_state->tooltip_open - ui_state->tooltip_open_t) > 0.01f);
if(ui_state->tooltip_open_t >= 0.99f && ui_state->tooltip_open)
{
ui_state->tooltip_open_t = 1.f;
}
for(U64 slot_idx = 0; slot_idx < ui_state->box_table_size; slot_idx += 1)
{
for(UI_Box *box = ui_state->box_table[slot_idx].hash_first;
!ui_box_is_nil(box);
box = box->hash_next)
{
// rjf: grab states informing animation
B32 is_hot = (ui_key_match(box->key, ui_state->hot_box_key) ||
ui_key_match(box->key, ui_state->drop_hot_box_key));
B32 is_active = ui_key_match(box->key, ui_state->active_box_key[UI_MouseButtonKind_Left]);
B32 is_disabled = !!(box->flags & UI_BoxFlag_Disabled) && (box->first_disabled_build_index+2 < ui_state->build_index ||
box->first_touched_build_index == box->first_disabled_build_index);
B32 is_focus_hot = !!(box->flags & UI_BoxFlag_FocusHot) && !(box->flags & UI_BoxFlag_FocusHotDisabled);
B32 is_focus_active = !!(box->flags & UI_BoxFlag_FocusActive) && !(box->flags & UI_BoxFlag_FocusActiveDisabled);
B32 is_focus_active_disabled = !!(box->flags & UI_BoxFlag_FocusActiveDisabled);
// rjf: determine rates
F32 hot_rate = (ui_state->animation_info.flags & UI_AnimationInfoFlag_HotAnimations ? fast_rate : 1);
F32 active_rate = (ui_state->animation_info.flags & UI_AnimationInfoFlag_ActiveAnimations ? fast_rate : 1);
F32 disabled_rate = (ui_state->animation_info.flags & UI_AnimationInfoFlag_HotAnimations ? slow_rate : 1);
F32 focus_rate = (ui_state->animation_info.flags & UI_AnimationInfoFlag_FocusAnimations ? fast_rate : 1);
// rjf: determine animating status
B32 box_is_animating = 0;
box_is_animating = (box_is_animating || abs_f32((F32)is_hot - box->hot_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32((F32)is_active - box->active_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32((F32)is_disabled - box->disabled_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32((F32)is_focus_hot - box->focus_hot_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32((F32)is_focus_active - box->focus_active_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32((F32)is_focus_active_disabled - box->focus_active_disabled_t) > 0.01f);
box_is_animating = (box_is_animating || abs_f32(box->view_off_target.x - box->view_off.x) > 0.5f);
box_is_animating = (box_is_animating || abs_f32(box->view_off_target.y - box->view_off.y) > 0.5f);
if(box->flags & UI_BoxFlag_AnimatePosX)
{
box_is_animating = (box_is_animating || abs_f32(box->fixed_position_animated.x - box->fixed_position.x) > 0.5f);
}
if(box->flags & UI_BoxFlag_AnimatePosY)
{
box_is_animating = (box_is_animating || abs_f32(box->fixed_position_animated.y - box->fixed_position.y) > 0.5f);
}
ui_state->is_animating = (ui_state->is_animating || box_is_animating);
#if 0 // NOTE(rjf): enable to debug animation-causing-frames (or not)
if(box_is_animating)
{
box->overlay_color = v4f32(1, 0, 0, 0.1f);
box->flags |= UI_BoxFlag_DrawOverlay;
}
#endif
// rjf: animate interaction transition states
box->hot_t += hot_rate * ((F32)is_hot - box->hot_t);
box->active_t = is_active ? 1.f : box->active_t + (active_rate * ((F32)is_active - box->active_t));
box->disabled_t += disabled_rate * ((F32)is_disabled - box->disabled_t);
box->focus_hot_t += focus_rate * ((F32)is_focus_hot - box->focus_hot_t);
box->focus_active_t += focus_rate * ((F32)is_focus_active - box->focus_active_t);
box->focus_active_disabled_t += focus_rate * ((F32)is_focus_active_disabled - box->focus_active_disabled_t);
// rjf: animate positions
{
box->fixed_position_animated.x += fast_rate * (box->fixed_position.x - box->fixed_position_animated.x);
box->fixed_position_animated.y += fast_rate * (box->fixed_position.y - box->fixed_position_animated.y);
if(abs_f32(box->fixed_position.x - box->fixed_position_animated.x) < 1)
{
box->fixed_position_animated.x = box->fixed_position.x;
}
if(abs_f32(box->fixed_position.y - box->fixed_position_animated.y) < 1)
{
box->fixed_position_animated.y = box->fixed_position.y;
}
}
// rjf: clamp view
if(box->flags & UI_BoxFlag_ViewClamp)
{
Vec2F32 max_view_off_target =
{
ClampBot(0, box->view_bounds.x - box->fixed_size.x),
ClampBot(0, box->view_bounds.y - box->fixed_size.y),
};
if(box->flags & UI_BoxFlag_ViewClampX) { box->view_off_target.x = Clamp(0, box->view_off_target.x, max_view_off_target.x); }
if(box->flags & UI_BoxFlag_ViewClampY) { box->view_off_target.y = Clamp(0, box->view_off_target.y, max_view_off_target.y); }
}
// rjf: animate view offset
{
box->view_off.x += fast_rate * (box->view_off_target.x - box->view_off.x);
box->view_off.y += fast_rate * (box->view_off_target.y - box->view_off.y);
if(abs_f32(box->view_off.x - box->view_off_target.x) < 2)
{
box->view_off.x = box->view_off_target.x;
}
if(abs_f32(box->view_off.y - box->view_off_target.y) < 2)
{
box->view_off.y = box->view_off_target.y;
}
}
}
}
}
//- rjf: use group keys for box animation data if possible
for(UI_Box *b = ui_state->root; !ui_box_is_nil(b); b = ui_box_rec_df_pre(b, ui_state->root).next)
{
if(ui_key_match(b->key, ui_key_zero()) && !ui_key_match(b->group_key, ui_key_zero()))
{
UI_Box *group_box = ui_box_from_key(b->group_key);
b->hot_t = group_box->hot_t;
}
}
//- rjf: fall-through interact with context menu
if(ui_state->ctx_menu_open)
{
ui_signal_from_box(ui_state->ctx_menu_root);
}
//- rjf: close ctx menu if unconsumed clicks
{
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
if(evt->kind == UI_EventKind_Press &&
(evt->key == OS_Key_LeftMouseButton || evt->key == OS_Key_RightMouseButton))
{
ui_ctx_menu_close();
}
}
}
//- rjf: hover cursor
if(!ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Left], ui_state->external_key))
{
UI_Box *hot = ui_box_from_key(ui_state->hot_box_key);
UI_Box *active = ui_box_from_key(ui_state->active_box_key[UI_MouseButtonKind_Left]);
UI_Box *box = ui_box_is_nil(active) ? hot : active;
OS_Cursor cursor = box->hover_cursor;
if(box->flags & UI_BoxFlag_Disabled && box->flags & UI_BoxFlag_Clickable)
{
cursor = OS_Cursor_Disabled;
}
if(os_window_is_focused(ui_state->window) || !ui_box_is_nil(active))
{
os_set_cursor(cursor);
}
}
//- rjf: clipboard commits
{
UI_Box *box = ui_box_from_key(ui_state->clipboard_copy_key);
if(!ui_box_is_nil(box))
{
Temp scratch = scratch_begin(0, 0);
String8List strs = {0};
UI_BoxRec rec = {0};
for(UI_Box *b = box; !ui_box_is_nil(b); rec = ui_box_rec_df_pre(b, box), b = rec.next)
{
if(b->flags & UI_BoxFlag_DrawText && b->flags & UI_BoxFlag_HasDisplayString && !fnt_tag_match(b->font, ui_icon_font()))
{
String8 display_string = ui_box_display_string(b);
str8_list_push(scratch.arena, &strs, display_string);
}
}
if(strs.node_count != 0)
{
StringJoin join = {0};
join.sep = str8_lit(" ");
String8 string = str8_list_join(scratch.arena, &strs, &join);
os_set_clipboard_text(string);
}
scratch_end(scratch);
}
}
//- rjf: hovering possibly-truncated drawn text -> store text
{
B32 inactive = 1;
for EachEnumVal(UI_MouseButtonKind, k)
{
if(!ui_key_match(ui_key_zero(), ui_state->active_box_key[k]))
{
inactive = 0;
break;
}
}
if(inactive)
{
B32 found = 0;
for(UI_Box *box = ui_state->root, *next = 0; !ui_box_is_nil(box); box = next)
{
UI_BoxRec rec = ui_box_rec_df_pre(box, ui_state->root);
next = rec.next;
S32 pop_idx = 0;
for(UI_Box *b = box; !ui_box_is_nil(b) && pop_idx <= rec.pop_count; b = b->parent, pop_idx += 1)
{
if(b->flags & UI_BoxFlag_DrawText && !(b->flags & UI_BoxFlag_DisableTextTrunc))
{
Rng2F32 rect = b->rect;
for(UI_Box *p = b->parent; !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_Clip)
{
rect = intersect_2f32(rect, p->rect);
}
}
String8 box_display_string = ui_box_display_string(b);
Vec2F32 text_pos = ui_box_text_position(b);
Vec2F32 drawn_text_dim = {0};
{
Temp scratch = scratch_begin(0, 0);
DR_FRunList fruns = dr_fruns_from_fstrs(scratch.arena, b->tab_size, &b->display_fstrs);
drawn_text_dim = fruns.dim;
scratch_end(scratch);
}
B32 text_is_truncated = (drawn_text_dim.x + text_pos.x > rect.x1);
B32 mouse_is_hovering = contains_2f32(r2f32p(text_pos.x,
rect.y0,
Min(text_pos.x+drawn_text_dim.x, rect.x1),
rect.y1),
ui_state->mouse);
if(text_is_truncated && mouse_is_hovering && !(b->flags & UI_BoxFlag_DisableTruncatedHover))
{
if(!str8_match(box_display_string, ui_state->string_hover_string, 0) || box->font_size != ui_state->string_hover_size)
{
arena_clear(ui_state->string_hover_arena);
ui_state->string_hover_string = push_str8_copy(ui_state->string_hover_arena, box_display_string);
ui_state->string_hover_size = box->font_size;
ui_state->string_hover_fstrs = dr_fstrs_copy(ui_state->string_hover_arena, &b->display_fstrs);
ui_state->string_hover_begin_us = os_now_microseconds();
}
ui_state->string_hover_build_index = ui_state->build_index;
found = 1;
goto break_all_hover_string;
}
}
if(b != box && ui_key_match(b->key, ui_hot_key()))
{
goto break_all_hover_string;
}
if(b != box && contains_2f32(b->rect, ui_state->mouse) && b->flags & UI_BoxFlag_DrawText)
{
goto break_all_hover_string;
}
}
}
break_all_hover_string:;
if(!found)
{
arena_clear(ui_state->string_hover_arena);
ui_state->string_hover_build_index = 0;
MemoryZeroStruct(&ui_state->string_hover_string);
}
if(found && !ui_string_hover_active())
{
ui_state->is_animating = 1;
}
}
}
ui_state->build_index += 1;
arena_clear(ui_build_arena());
ProfEnd();
}
internal void
ui_calc_sizes_standalone__in_place_rec(UI_Box *root, Axis2 axis)
{
ProfBeginFunction();
switch(root->pref_size[axis].kind)
{
default:{}break;
case UI_SizeKind_Pixels:
{
root->fixed_size.v[axis] = root->pref_size[axis].value;
}break;
case UI_SizeKind_TextContent:
{
F32 padding = root->pref_size[axis].value;
F32 text_size = root->display_fruns.dim.x;
root->fixed_size.v[axis] = padding + text_size + root->text_padding*2;
}break;
}
//- rjf: recurse
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
ui_calc_sizes_standalone__in_place_rec(child, axis);
}
ProfEnd();
}
internal void
ui_calc_sizes_upwards_dependent__in_place_rec(UI_Box *root, Axis2 axis)
{
ProfBeginFunction();
//- rjf: solve for all kinds that are upwards-dependent
switch(root->pref_size[axis].kind)
{
default: break;
// rjf: if root has a parent percentage, figure out its size
case UI_SizeKind_ParentPct:
{
// rjf: find parent that has a fixed size
UI_Box *fixed_parent = &ui_nil_box;
for(UI_Box *p = root->parent; !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & (UI_BoxFlag_FixedWidth<<axis) ||
p->pref_size[axis].kind == UI_SizeKind_Pixels ||
p->pref_size[axis].kind == UI_SizeKind_TextContent ||
p->pref_size[axis].kind == UI_SizeKind_ParentPct)
{
fixed_parent = p;
break;
}
}
// rjf: figure out root's size on this axis
F32 size = fixed_parent->fixed_size.v[axis] * root->pref_size[axis].value;
// rjf: mutate root to have this size
root->fixed_size.v[axis] = size;
}break;
}
//- rjf: recurse
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
ui_calc_sizes_upwards_dependent__in_place_rec(child, axis);
}
ProfEnd();
}
internal void
ui_calc_sizes_downwards_dependent__in_place_rec(UI_Box *root, Axis2 axis)
{
ProfBeginFunction();
//- rjf: recurse first. we may depend on children that have
// the same property
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
ui_calc_sizes_downwards_dependent__in_place_rec(child, axis);
}
//- rjf: solve for all kinds that are downwards-dependent
switch(root->pref_size[axis].kind)
{
default: break;
// rjf: sum children
case UI_SizeKind_ChildrenSum:
{
F32 sum = 0;
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
if(axis == root->child_layout_axis)
{
sum += child->fixed_size.v[axis];
}
else
{
sum = Max(sum, child->fixed_size.v[axis]);
}
}
}
// rjf: figure out root's size on this axis
root->fixed_size.v[axis] = sum;
}break;
}
ProfEnd();
}
internal void
ui_layout_enforce_constraints__in_place_rec(UI_Box *root, Axis2 axis)
{
ProfBeginFunction();
Temp scratch = scratch_begin(0, 0);
// NOTE(rjf): The "layout axis" is the direction in which children
// of some node are intended to be laid out.
//- rjf: fixup children sizes (if we're solving along the *non-layout* axis)
if(axis != root->child_layout_axis && !(root->flags & (UI_BoxFlag_AllowOverflowX << axis)))
{
F32 allowed_size = root->fixed_size.v[axis];
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
F32 child_size = child->fixed_size.v[axis];
F32 violation = child_size - allowed_size;
F32 max_fixup = child_size;
F32 fixup = Clamp(0, violation, max_fixup);
if(fixup > 0)
{
child->fixed_size.v[axis] -= fixup;
}
}
}
}
//- rjf: fixup children sizes (in the direction of the layout axis)
if(axis == root->child_layout_axis && !(root->flags & (UI_BoxFlag_AllowOverflowX << axis)))
{
// rjf: figure out total allowed size & total size
F32 total_allowed_size = root->fixed_size.v[axis];
F32 total_size = 0;
F32 total_weighted_size = 0;
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
total_size += child->fixed_size.v[axis];
total_weighted_size += child->fixed_size.v[axis] * (1-child->pref_size[axis].strictness);
}
}
// rjf: if we have a violation, we need to subtract some amount from all children
F32 violation = total_size - total_allowed_size;
if(violation > 0 && total_weighted_size > 0)
{
// rjf: figure out how much we can take in totality
F32 child_fixup_sum = 0;
F32 *child_fixups = push_array(scratch.arena, F32, root->child_count);
{
U64 child_idx = 0;
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next, child_idx += 1)
{
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
F32 fixup_size_this_child = child->fixed_size.v[axis] * (1-child->pref_size[axis].strictness);
fixup_size_this_child = ClampBot(0, fixup_size_this_child);
child_fixups[child_idx] = fixup_size_this_child;
child_fixup_sum += fixup_size_this_child;
}
}
}
// rjf: fixup child sizes
{
U64 child_idx = 0;
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next, child_idx += 1)
{
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
F32 fixup_pct = (violation / total_weighted_size);
fixup_pct = Clamp(0, fixup_pct, 1);
child->fixed_size.v[axis] -= child_fixups[child_idx] * fixup_pct;
}
}
}
}
}
//- rjf: fixup upwards-relative sizes
if(root->flags & (UI_BoxFlag_AllowOverflowX << axis))
{
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
if(child->pref_size[axis].kind == UI_SizeKind_ParentPct)
{
child->fixed_size.v[axis] = root->fixed_size.v[axis] * child->pref_size[axis].value;
}
}
}
//- rjf: enforce clamps
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
child->fixed_size.v[axis] = Max(child->fixed_size.v[axis], child->min_size.v[axis]);
}
//- rjf: recurse
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
ui_layout_enforce_constraints__in_place_rec(child, axis);
}
scratch_end(scratch);
ProfEnd();
}
internal void
ui_layout_position__in_place_rec(UI_Box *root, Axis2 axis)
{
ProfBeginFunction();
F32 layout_position = 0;
//- rjf: lay out children
F32 bounds = 0;
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
// rjf: grab original position
F32 original_position = Min(child->rect.p0.v[axis], child->rect.p1.v[axis]);
// rjf: calculate fixed position & size
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
{
child->fixed_position.v[axis] = layout_position;
if(root->child_layout_axis == axis)
{
layout_position += child->fixed_size.v[axis];
bounds += child->fixed_size.v[axis];
}
else
{
bounds = Max(bounds, child->fixed_size.v[axis]);
}
}
// rjf: determine final rect for child, given fixed_position & size
if(child->flags & (UI_BoxFlag_AnimatePosX<<axis))
{
if(child->first_touched_build_index == child->last_touched_build_index)
{
child->fixed_position_animated = child->fixed_position;
}
child->rect.p0.v[axis] = root->rect.p0.v[axis] + child->fixed_position_animated.v[axis] - !(child->flags&(UI_BoxFlag_SkipViewOffX<<axis))*floor_f32(root->view_off.v[axis]);
}
else
{
child->rect.p0.v[axis] = root->rect.p0.v[axis] + child->fixed_position.v[axis] - !(child->flags&(UI_BoxFlag_SkipViewOffX<<axis))*floor_f32(root->view_off.v[axis]);
}
child->rect.p1.v[axis] = child->rect.p0.v[axis] + child->fixed_size.v[axis];
child->rect.p0.x = floor_f32(child->rect.p0.x);
child->rect.p0.y = floor_f32(child->rect.p0.y);
child->rect.p1.x = floor_f32(child->rect.p1.x);
child->rect.p1.y = floor_f32(child->rect.p1.y);
// rjf: grab new position
F32 new_position = Min(child->rect.p0.v[axis], child->rect.p1.v[axis]);
// rjf: store position delta
child->position_delta.v[axis] = new_position - original_position;
}
//- rjf: store view bounds
{
root->view_bounds.v[axis] = bounds;
}
//- rjf: recurse
for(UI_Box *child = root->first; !ui_box_is_nil(child); child = child->next)
{
ui_layout_position__in_place_rec(child, axis);
}
ProfEnd();
}
internal void
ui_layout_root(UI_Box *root, Axis2 axis)
{
ProfBegin("ui layout pass (%s)", axis == Axis2_X ? "x" : "y");
ui_calc_sizes_standalone__in_place_rec(root, axis);
ui_calc_sizes_upwards_dependent__in_place_rec(root, axis);
ui_calc_sizes_downwards_dependent__in_place_rec(root, axis);
ui_layout_enforce_constraints__in_place_rec(root, axis);
ui_layout_position__in_place_rec(root, axis);
ProfEnd();
}
////////////////////////////////
//~ rjf: Box Building API
//- rjf: spacers
internal UI_Signal
ui_spacer(UI_Size size)
{
UI_Box *parent = ui_top_parent();
ui_set_next_pref_size(parent->child_layout_axis, size);
UI_Box *box = ui_build_box_from_key(0, ui_key_zero());
UI_Signal interact = ui_signal_from_box(box);
return interact;
}
//- rjf: tooltips
internal void
ui_tooltip_begin_base(void)
{
ui_state->tooltip_open = 1;
ui_push_parent(ui_root_from_state(ui_state));
ui_push_parent(ui_state->tooltip_root);
ui_push_flags(0);
ui_push_text_raster_flags(ui_bottom_text_raster_flags());
ui_push_font_size(ui_bottom_font_size());
ui_push_tag(str8_lit("."));
ui_push_tag(str8_lit("floating"));
}
internal void
ui_tooltip_end_base(void)
{
ui_pop_tag();
ui_pop_tag();
ui_pop_font_size();
ui_pop_text_raster_flags();
ui_pop_flags();
ui_pop_parent();
ui_pop_parent();
}
internal void
ui_tooltip_begin(void)
{
ui_tooltip_begin_base();
ui_set_next_squish(0.1f-ui_state->tooltip_open_t*0.1f);
ui_set_next_transparency(1-ui_state->tooltip_open_t);
UI_Flags(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground|UI_BoxFlag_DrawBackgroundBlur|UI_BoxFlag_DrawDropShadow|UI_BoxFlag_SquishAnchored)
UI_PrefWidth(ui_children_sum(1))
UI_PrefHeight(ui_children_sum(1))
UI_CornerRadius(ui_top_font_size()*0.25f)
ui_column_begin();
UI_PrefWidth(ui_px(0, 1)) ui_spacer(ui_em(1.f, 1.f));
UI_PrefWidth(ui_children_sum(1))
UI_PrefHeight(ui_children_sum(1))
ui_row_begin();
UI_PrefHeight(ui_px(0, 1)) ui_spacer(ui_em(1.f, 1.f));
UI_PrefWidth(ui_children_sum(1))
UI_PrefHeight(ui_children_sum(1))
ui_column_begin();
ui_push_pref_width(ui_text_dim(10.f, 1.f));
ui_push_pref_height(ui_em(2.f, 1.f));
ui_push_text_alignment(UI_TextAlign_Center);
}
internal void
ui_tooltip_end(void)
{
ui_pop_text_alignment();
ui_pop_pref_width();
ui_pop_pref_height();
ui_column_end();
UI_PrefHeight(ui_px(0, 1)) ui_spacer(ui_em(1.f, 1.f));
ui_row_end();
UI_PrefWidth(ui_px(0, 1)) ui_spacer(ui_em(1.f, 1.f));
ui_column_end();
ui_tooltip_end_base();
}
//- rjf: context menus
internal void
ui_ctx_menu_open(UI_Key key, UI_Key anchor_box_key, Vec2F32 anchor_off)
{
anchor_off.x = (F32)(int)anchor_off.x;
anchor_off.y = (F32)(int)anchor_off.y;
ui_state->next_ctx_menu_open = 1;
ui_state->ctx_menu_changed = 1;
ui_state->ctx_menu_open_t = 0;
ui_state->ctx_menu_key = key;
ui_state->next_ctx_menu_anchor_key = anchor_box_key;
ui_state->ctx_menu_anchor_off = anchor_off;
ui_state->ctx_menu_touched_this_frame = 1;
ui_state->ctx_menu_anchor_box_last_pos = v2f32(0, 0);
ui_state->ctx_menu_root->default_nav_focus_active_key = ui_key_zero();
ui_state->ctx_menu_root->default_nav_focus_next_active_key = ui_key_zero();
}
internal void
ui_ctx_menu_close(void)
{
ui_state->next_ctx_menu_open = 0;
}
internal B32
ui_begin_ctx_menu(UI_Key key)
{
ui_push_parent(ui_root_from_state(ui_state));
ui_push_parent(ui_state->ctx_menu_root);
ui_push_pref_width(ui_bottom_pref_width());
ui_push_pref_height(ui_bottom_pref_height());
ui_push_focus_hot(UI_FocusKind_Root);
ui_push_focus_active(UI_FocusKind_Root);
ui_push_tag(str8_lit("."));
B32 is_open = ui_key_match(key, ui_state->ctx_menu_key) && ui_state->ctx_menu_open;
if(is_open != 0) UI_TagF("floating")
{
ui_state->ctx_menu_touched_this_frame = 1;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_RoundChildrenByParent;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DrawBackgroundBlur;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DrawBackground;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DisableFocusOverlay;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DrawBorder;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_Clip;
ui_state->ctx_menu_root->flags |= UI_BoxFlag_Clickable;
ui_state->ctx_menu_root->corner_radii[Corner_00] = ui_state->ctx_menu_root->corner_radii[Corner_01] = ui_state->ctx_menu_root->corner_radii[Corner_10] = ui_state->ctx_menu_root->corner_radii[Corner_11] = ui_top_font_size()*0.25f;
ui_state->ctx_menu_root->tags_key = ui_top_tags_key();
ui_state->ctx_menu_root->blur_size = ui_top_blur_size();
ui_state->ctx_menu_root->text_color = ui_color_from_name(str8_lit("text"));
ui_state->ctx_menu_root->background_color = ui_color_from_name(str8_lit("background"));
ui_spacer(ui_em(1.f, 1.f));
}
ui_state->is_in_open_ctx_menu = is_open;
return is_open;
}
internal void
ui_end_ctx_menu(void)
{
if(ui_state->is_in_open_ctx_menu)
{
ui_state->is_in_open_ctx_menu = 0;
ui_spacer(ui_em(1.f, 1.f));
}
ui_pop_tag();
ui_pop_focus_active();
ui_pop_focus_hot();
ui_pop_pref_width();
ui_pop_pref_height();
ui_pop_parent();
ui_pop_parent();
}
internal B32
ui_ctx_menu_is_open(UI_Key key)
{
return (ui_state->ctx_menu_open && ui_key_match(key, ui_state->ctx_menu_key));
}
internal B32
ui_any_ctx_menu_is_open(void)
{
return ui_state->ctx_menu_open;
}
//- rjf: focus tree coloring
internal B32
ui_is_focus_hot(void)
{
B32 result = (ui_state->focus_hot_stack.top->v == UI_FocusKind_On);
if(result)
{
for(UI_FocusHotNode *n = ui_state->focus_hot_stack.top; n != 0; n = n->next)
{
if(n->v == UI_FocusKind_Root)
{
break;
}
if(n->v == UI_FocusKind_Off)
{
result = 0;
break;
}
}
}
return result;
}
internal B32
ui_is_focus_active(void)
{
B32 result = (ui_state->focus_active_stack.top->v == UI_FocusKind_On);
if(result)
{
for(UI_FocusActiveNode *n = ui_state->focus_active_stack.top; n != 0; n = n->next)
{
if(n->v == UI_FocusKind_Root)
{
break;
}
if(n->v == UI_FocusKind_Off)
{
result = 0;
break;
}
}
}
return result;
}
//- rjf: implicit auto-managed tree-based focus state
internal B32
ui_is_key_auto_focus_active(UI_Key key)
{
B32 result = 0;
if(!ui_key_match(ui_key_zero(), key))
{
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_FocusActive && ui_key_match(key, p->default_nav_focus_active_key))
{
result = 1;
break;
}
}
}
return result;
}
internal B32
ui_is_key_auto_focus_hot(UI_Key key)
{
B32 result = 0;
if(!ui_key_match(ui_key_zero(), key))
{
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_FocusHot &&
((!(p->flags & UI_BoxFlag_FocusHotDisabled) &&
ui_key_match(key, p->default_nav_focus_hot_key)) ||
ui_key_match(key, p->default_nav_focus_active_key)))
{
result = 1;
break;
}
}
}
return result;
}
internal void
ui_set_auto_focus_active_key(UI_Key key)
{
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_DefaultFocusNav)
{
p->default_nav_focus_next_active_key = key;
break;
}
}
}
internal void
ui_set_auto_focus_hot_key(UI_Key key)
{
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_DefaultFocusNav)
{
p->default_nav_focus_next_hot_key = key;
break;
}
}
}
//- rjf: current style tags key
internal UI_Key
ui_top_tags_key(void)
{
UI_Key key = ui_key_zero();
if(ui_state->tags_key_stack_top != 0)
{
key = ui_state->tags_key_stack_top->key;
}
return key;
}
//- rjf: theme color lookups
internal Vec4F32
ui_color_from_name(String8 name)
{
Vec4F32 result = ui_color_from_tags_key_name(ui_top_tags_key(), name);
return result;
}
internal Vec4F32
ui_color_from_tags_key_name(UI_Key key, String8 name)
{
Vec4F32 result = {0};
{
//- rjf: compute final key, mixing (tags_key, name)
UI_Key final_key = ui_key_from_string(key, name);
//- rjf: map to existing node
U64 slot_idx = final_key.u64[0]%ui_state->theme_pattern_cache_slots_count;
UI_ThemePatternCacheSlot *slot = &ui_state->theme_pattern_cache_slots[slot_idx];
UI_ThemePatternCacheNode *node = 0;
for(UI_ThemePatternCacheNode *n = slot->first;
n != 0;
n = n->next)
{
if(ui_key_match(n->key, final_key))
{
node = n;
}
}
//- rjf: no node? create
if(node == 0)
{
// rjf: map tags_key (without name) -> full list of tags
String8Array tags = {0};
{
U64 tags_cache_slot_idx = key.u64[0]%ui_state->tags_cache_slots_count;
UI_TagsCacheSlot *tags_cache_slot = &ui_state->tags_cache_slots[tags_cache_slot_idx];
for(UI_TagsCacheNode *n = tags_cache_slot->first; n != 0; n = n->next)
{
if(ui_key_match(n->key, key))
{
tags = n->tags;
break;
}
}
}
// rjf: map tags to theme pattern
UI_Theme *theme = ui_state->theme;
UI_ThemePattern *pattern = 0;
U64 best_match_count = 0;
for(U64 idx = 0; idx < theme->patterns_count; idx += 1)
{
UI_ThemePattern *p = &theme->patterns[idx];
U64 match_count = 0;
B32 name_matches = 0;
for EachIndex(key_tags_idx, tags.count+1)
{
String8 key_string = key_tags_idx < tags.count ? tags.v[key_tags_idx] : name;
for EachIndex(p_tags_idx, p->tags.count)
{
if(str8_match(p->tags.v[p_tags_idx], key_string, 0))
{
name_matches = (key_tags_idx == tags.count);
match_count += 1;
break;
}
}
}
if(name_matches && match_count > best_match_count)
{
pattern = p;
best_match_count = match_count;
}
if(match_count == tags.count+1)
{
break;
}
}
// rjf: store in (key, name) -> (pattern) cache
node = push_array(ui_build_arena(), UI_ThemePatternCacheNode, 1);
SLLQueuePush(slot->first, slot->last, node);
node->key = final_key;
node->pattern = pattern;
}
//- rjf: grab resultant color
if(node != 0 && node->pattern != 0)
{
result = node->pattern->linear;
}
}
return result;
}
//- rjf: box node construction
internal UI_Box *
ui_build_box_from_key(UI_BoxFlags flags, UI_Key key)
{
ProfBeginFunction();
ui_state->build_box_count += 1;
//- rjf: grab active parent
UI_Box *parent = ui_top_parent();
//- rjf: try to get box
UI_BoxFlags last_flags = 0;
UI_Box *box = ui_box_from_key(key);
B32 box_first_frame = ui_box_is_nil(box);
last_flags = box->flags;
//- rjf: zero key on duplicate
if(!box_first_frame && box->last_touched_build_index == ui_state->build_index)
{
box = &ui_nil_box;
key = ui_key_zero();
box_first_frame = 1;
}
//- rjf: gather info from box
B32 box_is_transient = ui_key_match(key, ui_key_zero());
//- rjf: allocate box if it doesn't yet exist
if(box_first_frame)
{
box = !box_is_transient ? ui_state->first_free_box : 0;
ui_state->is_animating = ui_state->is_animating || !box_is_transient;
if(!ui_box_is_nil(box))
{
SLLStackPop(ui_state->first_free_box);
}
else
{
box = push_array_no_zero(box_is_transient ? ui_build_arena() : ui_state->arena, UI_Box, 1);
}
MemoryZeroStruct(box);
}
//- rjf: zero out per-frame state
{
box->first = box->last = box->next = box->prev = box->parent = &ui_nil_box;
box->child_count = 0;
box->flags = 0;
box->hover_cursor = OS_Cursor_Pointer;
MemoryZeroArray(box->pref_size);
MemoryZeroStruct(&box->draw_bucket);
}
//- rjf: hook into persistent state table
if(box_first_frame && !box_is_transient)
{
U64 slot = key.u64[0] % ui_state->box_table_size;
DLLInsert_NPZ(&ui_nil_box, ui_state->box_table[slot].hash_first, ui_state->box_table[slot].hash_last, ui_state->box_table[slot].hash_last, box, hash_next, hash_prev);
}
//- rjf: hook into per-frame tree structure
if(!ui_box_is_nil(parent))
{
DLLPushBack_NPZ(&ui_nil_box, parent->first, parent->last, box, next, prev);
parent->child_count += 1;
box->parent = parent;
}
//- rjf: fill box
{
box->key = key;
box->flags = (flags | ui_state->flags_stack.top->v) & ~ui_state->omit_flags_stack.top->v;
box->fastpath_codepoint = ui_state->fastpath_codepoint_stack.top->v;
box->group_key = ui_state->group_key_stack.top->v;
if(ui_is_focus_active() && (box->flags & UI_BoxFlag_DefaultFocusNav) && ui_key_match(ui_state->default_nav_root_key, ui_key_zero()))
{
ui_state->default_nav_root_key = box->key;
}
if(box_first_frame)
{
box->first_touched_build_index = ui_state->build_index;
box->disabled_t = (F32)!!(box->flags & UI_BoxFlag_Disabled);
}
box->last_touched_build_index = ui_state->build_index;
if(box->flags & UI_BoxFlag_Disabled && (!(last_flags & UI_BoxFlag_Disabled) || box_first_frame))
{
box->first_disabled_build_index = ui_state->build_index;
}
if(ui_state->fixed_x_stack.top != &ui_state->fixed_x_nil_stack_top)
{
box->flags |= UI_BoxFlag_FloatingX;
box->fixed_position.x = ui_state->fixed_x_stack.top->v;
}
if(ui_state->fixed_y_stack.top != &ui_state->fixed_y_nil_stack_top)
{
box->flags |= UI_BoxFlag_FloatingY;
box->fixed_position.y = ui_state->fixed_y_stack.top->v;
}
if(ui_state->fixed_width_stack.top != &ui_state->fixed_width_nil_stack_top)
{
box->flags |= UI_BoxFlag_FixedWidth;
box->fixed_size.x = ui_state->fixed_width_stack.top->v;
}
else
{
box->pref_size[Axis2_X] = ui_state->pref_width_stack.top->v;
}
if(ui_state->fixed_height_stack.top != &ui_state->fixed_height_nil_stack_top)
{
box->flags |= UI_BoxFlag_FixedHeight;
box->fixed_size.y = ui_state->fixed_height_stack.top->v;
}
else
{
box->pref_size[Axis2_Y] = ui_state->pref_height_stack.top->v;
}
box->min_size.v[Axis2_X] = ui_state->min_width_stack.top->v;
box->min_size.v[Axis2_Y] = ui_state->min_height_stack.top->v;
B32 is_auto_focus_active = ui_is_key_auto_focus_active(key);
B32 is_auto_focus_hot = ui_is_key_auto_focus_hot(key);
if(is_auto_focus_active)
{
ui_set_next_focus_active(UI_FocusKind_On);
}
if(is_auto_focus_hot)
{
ui_set_next_focus_hot(UI_FocusKind_On);
}
box->flags |= UI_BoxFlag_FocusHot * (ui_state->focus_hot_stack.top->v == UI_FocusKind_On);
box->flags |= UI_BoxFlag_FocusActive * (ui_state->focus_active_stack.top->v == UI_FocusKind_On);
if(box->flags & UI_BoxFlag_FocusHot && !ui_is_focus_hot())
{
box->flags |= UI_BoxFlag_FocusHotDisabled;
}
if(box->flags & UI_BoxFlag_FocusActive && !ui_is_focus_active())
{
box->flags |= UI_BoxFlag_FocusActiveDisabled;
}
box->text_align = ui_state->text_alignment_stack.top->v;
box->child_layout_axis = ui_state->child_layout_axis_stack.top->v;
box->font = ui_state->font_stack.top->v;
box->font_size = ui_state->font_size_stack.top->v;
box->tab_size = ui_state->tab_size_stack.top->v;
box->text_raster_flags = ui_state->text_raster_flags_stack.top->v;
box->corner_radii[Corner_00] = ui_state->corner_radius_00_stack.top->v;
box->corner_radii[Corner_01] = ui_state->corner_radius_01_stack.top->v;
box->corner_radii[Corner_10] = ui_state->corner_radius_10_stack.top->v;
box->corner_radii[Corner_11] = ui_state->corner_radius_11_stack.top->v;
box->blur_size = ui_state->blur_size_stack.top->v;
box->transparency = ui_state->transparency_stack.top->v;
box->squish = ui_state->squish_stack.top->v;
box->text_padding = ui_state->text_padding_stack.top->v;
box->hover_cursor = ui_state->hover_cursor_stack.top->v;
box->custom_draw = 0;
box->tags_key = ui_key_zero();
if(ui_state->tags_key_stack_top != 0)
{
box->tags_key = ui_state->tags_key_stack_top->key;
}
if(ui_state->background_color_stack.top != &ui_state->background_color_nil_stack_top)
{
box->background_color = ui_state->background_color_stack.top->v;
}
else
{
box->background_color = ui_color_from_name(str8_lit("background"));
}
if(ui_state->text_color_stack.top != &ui_state->text_color_nil_stack_top)
{
box->text_color = ui_state->text_color_stack.top->v;
}
else
{
box->text_color = ui_color_from_name(str8_lit("text"));
}
}
//- rjf: auto-pop all stacks
{
UI_AutoPopStacks(ui_state);
}
//- rjf: return
ProfEnd();
return box;
}
internal UI_Key
ui_active_seed_key(void)
{
UI_Box *keyed_ancestor = &ui_nil_box;
{
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(!ui_key_match(ui_key_zero(), p->key))
{
keyed_ancestor = p;
break;
}
}
}
return keyed_ancestor->key;
}
internal UI_Box *
ui_build_box_from_string(UI_BoxFlags flags, String8 string)
{
ProfBeginFunction();
//- rjf: grab active parent
UI_Box *parent = ui_top_parent();
//- rjf: figure out key
UI_Key key = ui_key_from_string(ui_active_seed_key(), string);
//- rjf: build box from key, equip passed string
UI_Box *box = ui_build_box_from_key(flags, key);
if(flags & UI_BoxFlag_DrawText)
{
ui_box_equip_display_string(box, string);
}
//- rjf: return
ProfEnd();
return box;
}
internal UI_Box *
ui_build_box_from_stringf(UI_BoxFlags flags, char *fmt, ...)
{
Temp scratch = scratch_begin(0, 0);
va_list args;
va_start(args, fmt);
String8 string = push_str8fv(scratch.arena, fmt, args);
va_end(args);
UI_Box *box = ui_build_box_from_string(flags, string);
scratch_end(scratch);
return box;
}
//- rjf: box node equipment
internal void
ui_box_equip_display_string(UI_Box *box, String8 string)
{
ProfBeginFunction();
box->string = push_str8_copy(ui_build_arena(), string);
box->flags |= UI_BoxFlag_HasDisplayString;
Vec4F32 text_color = box->text_color;
if(box->flags & UI_BoxFlag_DrawText && (box->fastpath_codepoint == 0 || !(box->flags & UI_BoxFlag_DrawTextFastpathCodepoint)))
{
String8 display_string = ui_box_display_string(box);
DR_FStrNode fstr_n = {0, {display_string, {box->font, box->text_raster_flags, text_color, box->font_size, 0, 0}}};
DR_FStrList fstrs = {&fstr_n, &fstr_n, 1};
box->display_fstrs = dr_fstrs_copy(ui_build_arena(), &fstrs);
box->display_fruns = dr_fruns_from_fstrs(ui_build_arena(), box->tab_size, &box->display_fstrs);
}
else if(box->flags & UI_BoxFlag_DrawText && box->flags & UI_BoxFlag_DrawTextFastpathCodepoint && box->fastpath_codepoint != 0)
{
Temp scratch = scratch_begin(0, 0);
String8 display_string = ui_box_display_string(box);
String32 fpcp32 = str32(&box->fastpath_codepoint, 1);
String8 fpcp = str8_from_32(scratch.arena, fpcp32);
U64 fpcp_pos = str8_find_needle(display_string, 0, fpcp, StringMatchFlag_CaseInsensitive);
if(fpcp_pos < display_string.size)
{
DR_FStrNode pst_fstr_n = {0, {str8_skip(display_string, fpcp_pos+fpcp.size), {box->font, box->text_raster_flags, text_color, box->font_size, 0, 0}}};
DR_FStrNode cdp_fstr_n = {&pst_fstr_n, {str8_substr(display_string, r1u64(fpcp_pos, fpcp_pos+fpcp.size)), {box->font, box->text_raster_flags, text_color, box->font_size, 3.f, 0}}};
DR_FStrNode pre_fstr_n = {&cdp_fstr_n, {str8_prefix(display_string, fpcp_pos), {box->font, box->text_raster_flags, text_color, box->font_size, 0, 0}}};
DR_FStrList fstrs = {&pre_fstr_n, &pst_fstr_n, 3};
box->display_fstrs = dr_fstrs_copy(ui_build_arena(), &fstrs);
box->display_fruns = dr_fruns_from_fstrs(ui_build_arena(), box->tab_size, &box->display_fstrs);
}
else
{
DR_FStrNode fstr_n = {0, {display_string, {box->font, box->text_raster_flags, text_color, box->font_size, 0, 0}}};
DR_FStrList fstrs = {&fstr_n, &fstr_n, 1};
box->display_fstrs = dr_fstrs_copy(ui_build_arena(), &fstrs);
box->display_fruns = dr_fruns_from_fstrs(ui_build_arena(), box->tab_size, &box->display_fstrs);
}
scratch_end(scratch);
}
ProfEnd();
}
internal void
ui_box_equip_display_fstrs(UI_Box *box, DR_FStrList *strings)
{
box->flags |= UI_BoxFlag_HasDisplayString;
box->string = dr_string_from_fstrs(ui_build_arena(), strings);
box->display_fstrs = dr_fstrs_copy(ui_build_arena(), strings);
box->display_fruns = dr_fruns_from_fstrs(ui_build_arena(), box->tab_size, &box->display_fstrs);
}
internal inline void
ui_box_equip_fuzzy_match_ranges(UI_Box *box, FuzzyMatchRangeList *matches)
{
box->flags |= UI_BoxFlag_HasFuzzyMatchRanges;
box->fuzzy_match_ranges = fuzzy_match_range_list_copy(ui_build_arena(), matches);
}
internal void
ui_box_equip_draw_bucket(UI_Box *box, DR_Bucket *bucket)
{
box->flags |= UI_BoxFlag_DrawBucket;
if(box->draw_bucket != 0)
{
DR_BucketScope(box->draw_bucket) dr_sub_bucket(bucket);
}
else
{
box->draw_bucket = bucket;
}
}
internal void
ui_box_equip_custom_draw(UI_Box *box, UI_BoxCustomDrawFunctionType *custom_draw, void *user_data)
{
box->custom_draw = custom_draw;
box->custom_draw_user_data = user_data;
}
//- rjf: box accessors / queries
internal String8
ui_box_display_string(UI_Box *box)
{
String8 result = box->string;
if(!(box->flags & UI_BoxFlag_DisableIDString))
{
result = ui_display_part_from_key_string(result);
}
return result;
}
internal Vec2F32
ui_box_text_position(UI_Box *box)
{
Vec2F32 result = {0};
FNT_Tag font = box->font;
F32 font_size = box->font_size;
FNT_Metrics font_metrics = fnt_metrics_from_tag_size(font, font_size);
result.y = floor_f32((box->rect.p0.y + box->rect.p1.y)/2.f) + font_metrics.capital_height/2.f - 1.f;
if(!fnt_tag_match(font, ui_icon_font()))
{
result.y += font_metrics.descent/2;
}
switch(box->text_align)
{
default:
case UI_TextAlign_Left:
{
result.x = box->rect.p0.x + box->text_padding;
}break;
case UI_TextAlign_Center:
{
Vec2F32 text_dim = box->display_fruns.dim;
result.x = round_f32((box->rect.p0.x + box->rect.p1.x)/2 - text_dim.x/2);
result.x = ClampBot(result.x, box->rect.x0);
}break;
case UI_TextAlign_Right:
{
Vec2F32 text_dim = box->display_fruns.dim;
result.x = round_f32((box->rect.p1.x) - text_dim.x - box->text_padding);
result.x = ClampBot(result.x, box->rect.x0);
}break;
}
result.x = floor_f32(result.x);
return result;
}
internal U64
ui_box_char_pos_from_xy(UI_Box *box, Vec2F32 xy)
{
FNT_Tag font = box->font;
F32 font_size = box->font_size;
String8 line = ui_box_display_string(box);
U64 result = fnt_char_pos_from_tag_size_string_p(font, font_size, 0, box->tab_size, line, xy.x - ui_box_text_position(box).x);
return result;
}
////////////////////////////////
//~ rjf: Box Interaction
internal UI_Signal
ui_signal_from_box(UI_Box *box)
{
ProfBeginFunction();
B32 is_focus_hot = box->flags & UI_BoxFlag_FocusHot && !(box->flags & UI_BoxFlag_FocusHotDisabled);
UI_Signal sig = {box};
sig.event_flags |= os_get_modifiers();
//////////////////////////////
//- rjf: calculate possibly-clipped box rectangle
//
Rng2F32 rect = box->rect;
for(UI_Box *b = box->parent; !ui_box_is_nil(b); b = b->parent)
{
if(b->flags & UI_BoxFlag_Clip)
{
rect = intersect_2f32(rect, b->rect);
}
}
//////////////////////////////
//- rjf: determine if we're under the context menu or not
//
B32 ctx_menu_is_ancestor = 0;
ProfScope("check context menu ancestor")
{
for(UI_Box *parent = box; !ui_box_is_nil(parent); parent = parent->parent)
{
if(parent == ui_state->ctx_menu_root)
{
ctx_menu_is_ancestor = 1;
break;
}
}
}
//////////////////////////////
//- rjf: calculate blacklist rectangles
//
Rng2F32 blacklist_rect = {0};
if(!ctx_menu_is_ancestor && ui_state->ctx_menu_open)
{
blacklist_rect = ui_state->ctx_menu_root->rect;
}
//////////////////////////////
//- rjf: process events related to this box
//
B32 view_scrolled = 0;
for(UI_Event *evt = 0; ui_next_event(&evt);)
{
B32 taken = 0;
//- rjf: unpack event
Vec2F32 evt_mouse = evt->pos;
B32 evt_mouse_in_bounds = !contains_2f32(blacklist_rect, evt_mouse) && contains_2f32(rect, evt_mouse);
UI_MouseButtonKind evt_mouse_button_kind = (evt->key == OS_Key_LeftMouseButton ? UI_MouseButtonKind_Left :
evt->key == OS_Key_MiddleMouseButton ? UI_MouseButtonKind_Middle :
evt->key == OS_Key_RightMouseButton ? UI_MouseButtonKind_Right :
UI_MouseButtonKind_Left);
B32 evt_key_is_mouse = (evt->key == OS_Key_LeftMouseButton ||
evt->key == OS_Key_MiddleMouseButton ||
evt->key == OS_Key_RightMouseButton);
sig.event_flags |= evt->modifiers;
//- rjf: mouse presses in box -> set hot/active; mark signal accordingly
if(box->flags & UI_BoxFlag_MouseClickable &&
evt->kind == UI_EventKind_Press &&
evt_mouse_in_bounds &&
evt_key_is_mouse)
{
ui_state->hot_box_key = box->key;
ui_state->active_box_key[evt_mouse_button_kind] = box->key;
sig.f |= (UI_SignalFlag_LeftPressed<<evt_mouse_button_kind);
ui_state->drag_start_mouse = evt->pos;
if(ui_key_match(box->key, ui_state->press_key_history[evt_mouse_button_kind][0]) &&
evt->timestamp_us-ui_state->press_timestamp_history_us[evt_mouse_button_kind][0] <= 1000000*os_get_gfx_info()->double_click_time)
{
sig.f |= (UI_SignalFlag_LeftDoubleClicked<<evt_mouse_button_kind);
}
if(ui_key_match(box->key, ui_state->press_key_history[evt_mouse_button_kind][0]) &&
ui_key_match(box->key, ui_state->press_key_history[evt_mouse_button_kind][1]) &&
evt->timestamp_us-ui_state->press_timestamp_history_us[evt_mouse_button_kind][0] <= 1000000*os_get_gfx_info()->double_click_time &&
ui_state->press_timestamp_history_us[evt_mouse_button_kind][0] - ui_state->press_timestamp_history_us[evt_mouse_button_kind][1] <= 1000000*os_get_gfx_info()->double_click_time)
{
sig.f |= (UI_SignalFlag_LeftTripleClicked<<evt_mouse_button_kind);
}
MemoryCopy(&ui_state->press_timestamp_history_us[evt_mouse_button_kind][1], &ui_state->press_timestamp_history_us[evt_mouse_button_kind][0],
sizeof(ui_state->press_timestamp_history_us[evt_mouse_button_kind][0]) * ArrayCount(ui_state->press_timestamp_history_us[evt_mouse_button_kind])-1);
MemoryCopy(&ui_state->press_key_history[evt_mouse_button_kind][1], &ui_state->press_key_history[evt_mouse_button_kind][0],
sizeof(ui_state->press_key_history[evt_mouse_button_kind][0]) * ArrayCount(ui_state->press_key_history[evt_mouse_button_kind])-1);
MemoryCopy(&ui_state->press_pos_history[evt_mouse_button_kind][1], &ui_state->press_pos_history[evt_mouse_button_kind][0],
sizeof(ui_state->press_pos_history[evt_mouse_button_kind][0]) * ArrayCount(ui_state->press_pos_history[evt_mouse_button_kind])-1);
ui_state->press_timestamp_history_us[evt_mouse_button_kind][0] = evt->timestamp_us;
ui_state->press_key_history[evt_mouse_button_kind][0] = box->key;
ui_state->press_pos_history[evt_mouse_button_kind][0] = evt_mouse;
taken = 1;
}
//- rjf: mouse releases in active box -> unset active; mark signal accordingly
if(box->flags & UI_BoxFlag_MouseClickable &&
evt->kind == UI_EventKind_Release &&
ui_key_match(ui_state->active_box_key[evt_mouse_button_kind], box->key) &&
evt_mouse_in_bounds &&
evt_key_is_mouse)
{
ui_state->active_box_key[evt_mouse_button_kind] = ui_key_zero();
sig.f |= (UI_SignalFlag_LeftReleased<<evt_mouse_button_kind);
sig.f |= (UI_SignalFlag_LeftClicked<<evt_mouse_button_kind);
taken = 1;
}
//- rjf: mouse releases outside active box -> unset hot/active
if(box->flags & UI_BoxFlag_MouseClickable &&
evt->kind == UI_EventKind_Release &&
ui_key_match(ui_state->active_box_key[evt_mouse_button_kind], box->key) &&
!evt_mouse_in_bounds &&
evt_key_is_mouse)
{
ui_state->hot_box_key = ui_key_zero();
ui_state->active_box_key[evt_mouse_button_kind] = ui_key_zero();
sig.f |= (UI_SignalFlag_LeftReleased<<evt_mouse_button_kind);
taken = 1;
}
//- rjf: focus is hot & keyboard click -> mark signal
if(box->flags & UI_BoxFlag_KeyboardClickable &&
is_focus_hot &&
evt->kind == UI_EventKind_Press &&
evt->slot == UI_EventActionSlot_Accept)
{
sig.f |= UI_SignalFlag_KeyboardPressed;
taken = 1;
}
//- rjf: focus is hot & copy event -> remember to copy this box tree's text content
if(is_focus_hot &&
evt->flags & UI_EventFlag_Copy &&
!ui_key_match(ui_key_zero(), box->key))
{
ui_state->clipboard_copy_key = box->key;
taken = 1;
}
//- rjf: ancestor is focused & fastpath codepoint pressed -> press
if(box->flags & UI_BoxFlag_Clickable && box->fastpath_codepoint != 0 && evt->string.size != 0)
{
B32 ancestor_is_focused = 0;
for(UI_Box *parent = box->parent; !ui_box_is_nil(parent); parent = parent->parent)
{
if(parent->flags & UI_BoxFlag_FocusActive)
{
ancestor_is_focused = 1;
if(parent->flags & UI_BoxFlag_FocusActiveDisabled ||
!ui_key_match(parent->default_nav_focus_active_key, ui_key_zero()))
{
ancestor_is_focused = 0;
break;
}
}
}
if(ancestor_is_focused)
{
Temp scratch = scratch_begin(0, 0);
String32 insertion32 = str32_from_8(scratch.arena, evt->string);
if(insertion32.size == 1 && insertion32.str[0] == box->fastpath_codepoint)
{
taken = 1;
sig.f |= UI_SignalFlag_Clicked|UI_SignalFlag_Pressed;
}
scratch_end(scratch);
}
}
//- rjf: scrolling
if(box->flags & UI_BoxFlag_Scroll &&
evt->kind == UI_EventKind_Scroll &&
evt->modifiers != OS_Modifier_Ctrl &&
evt_mouse_in_bounds)
{
Vec2F32 delta = evt->delta_2f32;
if(evt->modifiers & OS_Modifier_Shift)
{
Swap(F32, delta.x, delta.y);
}
Vec2S16 delta16 = v2s16((S16)(delta.x/30.f), (S16)(delta.y/30.f));
if(delta.x > 0 && delta16.x == 0) { delta16.x = +1; }
if(delta.x < 0 && delta16.x == 0) { delta16.x = -1; }
if(delta.y > 0 && delta16.y == 0) { delta16.y = +1; }
if(delta.y < 0 && delta16.y == 0) { delta16.y = -1; }
sig.scroll.x += delta16.x;
sig.scroll.y += delta16.y;
taken = 1;
}
//- rjf: view scrolling
if(box->flags & UI_BoxFlag_ViewScroll && box->first_touched_build_index != box->last_touched_build_index &&
evt->kind == UI_EventKind_Scroll &&
evt->modifiers != OS_Modifier_Ctrl &&
evt_mouse_in_bounds)
{
Vec2F32 delta = evt->delta_2f32;
if(evt->modifiers & OS_Modifier_Shift)
{
Swap(F32, delta.x, delta.y);
}
if(!(box->flags & UI_BoxFlag_ViewScrollX))
{
if(delta.y == 0)
{
delta.y = delta.x;
}
delta.x = 0;
}
if(!(box->flags & UI_BoxFlag_ViewScrollY))
{
if(delta.x == 0)
{
delta.x = delta.y;
}
delta.y = 0;
}
box->view_off_target.x += delta.x;
box->view_off_target.y += delta.y;
view_scrolled = 1;
taken = 1;
}
//- rjf: taken -> eat event
if(taken)
{
ui_eat_event(evt);
}
}
//////////////////////////////
//- rjf: clamp view scrolling
//
if(view_scrolled && box->flags & UI_BoxFlag_ViewClamp)
{
Vec2F32 max_view_off_target =
{
ClampBot(0, box->view_bounds.x - box->fixed_size.x),
ClampBot(0, box->view_bounds.y - box->fixed_size.y),
};
if(box->flags & UI_BoxFlag_ViewClampX) { box->view_off_target.x = Clamp(0, box->view_off_target.x, max_view_off_target.x); }
if(box->flags & UI_BoxFlag_ViewClampY) { box->view_off_target.y = Clamp(0, box->view_off_target.y, max_view_off_target.y); }
}
//////////////////////////////
//- rjf: active -> dragging
//
if(box->flags & UI_BoxFlag_MouseClickable)
{
for EachEnumVal(UI_MouseButtonKind, k)
{
if(ui_key_match(ui_state->active_box_key[k], box->key) ||
sig.f & (UI_SignalFlag_LeftPressed<<k))
{
sig.f |= (UI_SignalFlag_LeftDragging<<k);
}
}
}
//////////////////////////////
//- rjf: dragging started via double-click -> double-dragging
//
if(box->flags & UI_BoxFlag_MouseClickable)
{
for EachEnumVal(UI_MouseButtonKind, k)
{
if(sig.f & (UI_SignalFlag_LeftDragging<<k) &&
ui_key_match(ui_state->press_key_history[k][0], box->key) &&
ui_key_match(ui_state->press_key_history[k][1], box->key) &&
ui_state->press_timestamp_history_us[k][0] - ui_state->press_timestamp_history_us[k][1] <= 1000000*os_get_gfx_info()->double_click_time &&
length_2f32(sub_2f32(ui_state->press_pos_history[k][0], ui_state->press_pos_history[k][1])) < 10.f)
{
sig.f |= (UI_SignalFlag_LeftDoubleDragging<<k);
}
}
}
//////////////////////////////
//- rjf: dragging started via triple-click -> triple-dragging
//
if(box->flags & UI_BoxFlag_MouseClickable)
{
for EachEnumVal(UI_MouseButtonKind, k)
{
if(sig.f & (UI_SignalFlag_LeftDragging<<k) &&
ui_key_match(ui_state->press_key_history[k][0], box->key) &&
ui_key_match(ui_state->press_key_history[k][1], box->key) &&
ui_key_match(ui_state->press_key_history[k][2], box->key) &&
ui_state->press_timestamp_history_us[k][0] - ui_state->press_timestamp_history_us[k][1] <= 1000000*os_get_gfx_info()->double_click_time &&
ui_state->press_timestamp_history_us[k][1] - ui_state->press_timestamp_history_us[k][2] <= 1000000*os_get_gfx_info()->double_click_time &&
length_2f32(sub_2f32(ui_state->press_pos_history[k][0], ui_state->press_pos_history[k][1])) < 10.f &&
length_2f32(sub_2f32(ui_state->press_pos_history[k][1], ui_state->press_pos_history[k][2])) < 10.f)
{
sig.f |= (UI_SignalFlag_LeftTripleDragging<<k);
}
}
}
//////////////////////////////
//- rjf: mouse is over this box's rect -> always mark mouse-over
//
{
if(contains_2f32(rect, ui_state->mouse) &&
!contains_2f32(blacklist_rect, ui_state->mouse))
{
sig.f |= UI_SignalFlag_MouseOver;
}
}
//////////////////////////////
//- rjf: mouse is over this box's rect, no other hot key? -> set hot key, mark hovering
//
{
if(box->flags & UI_BoxFlag_MouseClickable &&
contains_2f32(rect, ui_state->mouse) &&
!contains_2f32(blacklist_rect, ui_state->mouse) &&
(ui_key_match(ui_state->hot_box_key, ui_key_zero()) || ui_key_match(ui_state->hot_box_key, box->key)) &&
(ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Left], ui_key_zero()) || ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Left], box->key)) &&
(ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Middle], ui_key_zero()) || ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Middle], box->key)) &&
(ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Right], ui_key_zero()) || ui_key_match(ui_state->active_box_key[UI_MouseButtonKind_Right], box->key)))
{
ui_state->hot_box_key = box->key;
sig.f |= UI_SignalFlag_Hovering;
}
}
//////////////////////////////
//- rjf: mouse is over this box's rect, currently-active-key has the same group key? -> set hot/active key
//
if(box->flags & UI_BoxFlag_MouseClickable &&
contains_2f32(rect, ui_state->mouse) &&
!contains_2f32(blacklist_rect, ui_state->mouse) &&
!ui_key_match(ui_key_zero(), box->group_key))
{
for EachEnumVal(UI_MouseButtonKind, k)
{
UI_Box *active_box = ui_box_from_key(ui_state->active_box_key[k]);
if(ui_key_match(box->group_key, active_box->group_key))
{
ui_state->hot_box_key = box->key;
ui_state->active_box_key[k] = box->key;
sig.f |= UI_SignalFlag_Hovering|(UI_SignalFlag_Dragging<<k);
}
}
}
//////////////////////////////
//- rjf: mouse is over this box's rect, drop site, no other drop hot key? -> set drop hot key
//
{
if(box->flags & UI_BoxFlag_DropSite &&
contains_2f32(rect, ui_state->mouse) &&
!contains_2f32(blacklist_rect, ui_state->mouse) &&
(ui_key_match(ui_state->drop_hot_box_key, ui_key_zero()) || ui_key_match(ui_state->drop_hot_box_key, box->key)))
{
ui_state->drop_hot_box_key = box->key;
}
}
//////////////////////////////
//- rjf: mouse is not over this box's rect, but this is the drop hot key? -> zero drop hot key
//
{
if(box->flags & UI_BoxFlag_DropSite &&
(!contains_2f32(rect, ui_state->mouse) ||
contains_2f32(blacklist_rect, ui_state->mouse)) &&
ui_key_match(ui_state->drop_hot_box_key, box->key))
{
ui_state->drop_hot_box_key = ui_key_zero();
}
}
//////////////////////////////
//- rjf: clicking on something outside the context menu kills the context menu
//
if(!ctx_menu_is_ancestor && sig.f & (UI_SignalFlag_LeftPressed|UI_SignalFlag_RightPressed|UI_SignalFlag_MiddlePressed))
{
ui_ctx_menu_close();
}
//////////////////////////////
//- rjf: get default nav ancestor
//
UI_Box *default_nav_parent = &ui_nil_box;
for(UI_Box *p = ui_top_parent(); !ui_box_is_nil(p); p = p->parent)
{
if(p->flags & UI_BoxFlag_DefaultFocusNav)
{
default_nav_parent = p;
break;
}
}
//////////////////////////////
//- rjf: clicking in default nav -> set navigation state to this box
//
if(box->flags & UI_BoxFlag_ClickToFocus && sig.f&UI_SignalFlag_Pressed && !ui_box_is_nil(default_nav_parent))
{
default_nav_parent->default_nav_focus_next_hot_key = box->key;
if(!ui_key_match(default_nav_parent->default_nav_focus_active_key, box->key))
{
default_nav_parent->default_nav_focus_next_active_key = ui_key_zero();
}
}
ProfEnd();
return sig;
}
////////////////////////////////
//~ rjf: Animation Cache Interaction API
internal F32
ui_anim_(UI_Key key, UI_AnimParams *params)
{
// rjf: get animation cache node
UI_AnimNode *node = &ui_nil_anim_node;
if(ui_state != 0)
{
U64 slot_idx = key.u64[0]%ui_state->anim_slots_count;
UI_AnimSlot *slot = &ui_state->anim_slots[slot_idx];
for(UI_AnimNode *n = slot->first; n != &ui_nil_anim_node && n != 0; n = n->slot_next)
{
if(ui_key_match(n->key, key))
{
node = n;
break;
}
}
if(node == &ui_nil_anim_node)
{
node = ui_state->free_anim_node;
if(node != 0)
{
SLLStackPop_N(ui_state->free_anim_node, slot_next);
}
else
{
node = push_array(ui_state->arena, UI_AnimNode, 1);
}
node->first_touched_build_index = ui_state->build_index;
node->key = key;
MemoryCopyStruct(&node->params, params);
node->current = params->initial;
DLLPushBack_NPZ(&ui_nil_anim_node, slot->first, slot->last, node, slot_next, slot_prev);
}
else
{
DLLRemove_NPZ(&ui_nil_anim_node, ui_state->lru_anim_node, ui_state->mru_anim_node, node, lru_next, lru_prev);
}
}
// rjf: touch node & update parameters - grab current
if(node != &ui_nil_anim_node)
{
node->last_touched_build_index = ui_state->build_index;
DLLPushBack_NPZ(&ui_nil_anim_node, ui_state->lru_anim_node, ui_state->mru_anim_node, node, lru_next, lru_prev);
if(params->reset)
{
node->current = params->initial;
}
MemoryCopyStruct(&node->params, params);
if(node->params.epsilon == 0)
{
node->params.epsilon = 0.005f;
}
if(node->params.rate == 1 || abs_f32(node->current - node->params.target) < abs_f32(node->params.epsilon))
{
node->current = node->params.target;
}
}
return node->current;
}
////////////////////////////////
//~ rjf: Stacks
#define UI_StackTopImpl(state, name_upper, name_lower) \
return state->name_lower##_stack.top->v;
#define UI_StackBottomImpl(state, name_upper, name_lower) \
return state->name_lower##_stack.bottom_val;
#define UI_StackPushImpl(state, name_upper, name_lower, type, new_value) \
UI_##name_upper##Node *node = state->name_lower##_stack.free;\
if(node != 0) {SLLStackPop(state->name_lower##_stack.free);}\
else {node = push_array(ui_build_arena(), UI_##name_upper##Node, 1);}\
type old_value = state->name_lower##_stack.top->v;\
node->v = new_value;\
SLLStackPush(state->name_lower##_stack.top, node);\
if(node->next == &state->name_lower##_nil_stack_top)\
{\
state->name_lower##_stack.bottom_val = (node->v);\
}\
state->name_lower##_stack.auto_pop = 0;\
state->name_lower##_stack.gen += 1;\
return old_value;
#define UI_StackPopImpl(state, name_upper, name_lower) \
UI_##name_upper##Node *popped = state->name_lower##_stack.top;\
if(popped != &state->name_lower##_nil_stack_top)\
{\
SLLStackPop(state->name_lower##_stack.top);\
SLLStackPush(state->name_lower##_stack.free, popped);\
state->name_lower##_stack.auto_pop = 0;\
state->name_lower##_stack.gen += 1;\
}\
return popped->v;\
#define UI_StackSetNextImpl(state, name_upper, name_lower, type, new_value) \
UI_##name_upper##Node *node = state->name_lower##_stack.free;\
if(node != 0) {SLLStackPop(state->name_lower##_stack.free);}\
else {node = push_array(ui_build_arena(), UI_##name_upper##Node, 1);}\
type old_value = state->name_lower##_stack.top->v;\
node->v = new_value;\
SLLStackPush(state->name_lower##_stack.top, node);\
state->name_lower##_stack.auto_pop = 1;\
state->name_lower##_stack.gen += 1;\
return old_value;
internal void
ui__push_tags_key_from_appended_string(String8 string)
{
// rjf: generate new key, by combining hash of this new string with the top
// of the tags key stack
UI_Key seed_key = {0};
if(ui_state->tags_key_stack_top != 0)
{
seed_key = ui_state->tags_key_stack_top->key;
}
UI_Key key = seed_key;
if(!str8_match(str8_lit("."), string, 0) && string.size > 0)
{
key = ui_key_from_string(seed_key, string);
}
// rjf: push this new key onto the stack
{
UI_TagsKeyStackNode *node = ui_state->tags_key_stack_free;
if(node != 0)
{
SLLStackPop(ui_state->tags_key_stack_free);
}
else
{
node = push_array(ui_build_arena(), UI_TagsKeyStackNode, 1);
}
SLLStackPush(ui_state->tags_key_stack_top, node);
node->key = key;
}
// rjf: store in tags cache
U64 slot_idx = key.u64[0] % ui_state->tags_cache_slots_count;
UI_TagsCacheSlot *slot = &ui_state->tags_cache_slots[slot_idx];
UI_TagsCacheNode *node = 0;
for(UI_TagsCacheNode *n = slot->first; n != 0; n = n->next)
{
if(ui_key_match(n->key, key))
{
node = n;
break;
}
}
if(node == 0)
{
Temp scratch = scratch_begin(0, 0);
String8List tags = {0};
if(!str8_match(string, str8_lit("."), 0))
{
if(string.size != 0)
{
str8_list_push(scratch.arena, &tags, push_str8_copy(ui_build_arena(), string));
}
for(UI_TagNode *n = ui_state->tag_stack.top; n != 0; n = n->next)
{
if(n->v.size == 1 && n->v.str[0] == '.')
{
break;
}
if(n->v.size != 0)
{
str8_list_push(scratch.arena, &tags, push_str8_copy(ui_build_arena(), n->v));
}
}
}
node = push_array(ui_build_arena(), UI_TagsCacheNode, 1);
SLLQueuePush(slot->first, slot->last, node);
node->key = key;
node->tags = str8_array_from_list(ui_build_arena(), &tags);
scratch_end(scratch);
}
}
internal void
ui__pop_tags_key(void)
{
if(ui_state->tags_key_stack_top != 0)
{
UI_TagsKeyStackNode *popped = ui_state->tags_key_stack_top;
SLLStackPop(ui_state->tags_key_stack_top);
SLLStackPush(ui_state->tags_key_stack_free, popped);
}
}
//- rjf: manual implementations
internal String8
ui_top_tag(void)
{
UI_StackTopImpl(ui_state, Tag, tag)
}
internal String8
ui_bottom_tag(void)
{
UI_StackBottomImpl(ui_state, Tag, tag)
}
internal String8
ui_push_tag(String8 v)
{
ui__push_tags_key_from_appended_string(v);
UI_StackPushImpl(ui_state, Tag, tag, String8, push_str8_copy(ui_build_arena(), v))
}
internal String8
ui_pop_tag(void)
{
ui__pop_tags_key();
UI_StackPopImpl(ui_state, Tag, tag)
}
internal String8
ui_set_next_tag(String8 v)
{
ui__push_tags_key_from_appended_string(v);
UI_StackSetNextImpl(ui_state, Tag, tag, String8, push_str8_copy(ui_build_arena(), v))
}
//- rjf: helpers
internal Rng2F32
ui_push_rect(Rng2F32 rect)
{
Rng2F32 replaced = {0};
Vec2F32 size = dim_2f32(rect);
replaced.x0 = ui_push_fixed_x(rect.x0);
replaced.y0 = ui_push_fixed_y(rect.y0);
replaced.x1 = replaced.x0 + ui_push_fixed_width(size.x);
replaced.y1 = replaced.y0 + ui_push_fixed_height(size.y);
return replaced;
}
internal Rng2F32
ui_pop_rect(void)
{
Rng2F32 popped = {0};
popped.x0 = ui_pop_fixed_x();
popped.y0 = ui_pop_fixed_y();
popped.x1 = popped.x0 + ui_pop_fixed_width();
popped.y1 = popped.y0 + ui_pop_fixed_height();
return popped;
}
internal void
ui_set_next_rect(Rng2F32 rect)
{
Vec2F32 size = dim_2f32(rect);
ui_set_next_fixed_x(rect.x0);
ui_set_next_fixed_y(rect.y0);
ui_set_next_fixed_width(size.x);
ui_set_next_fixed_height(size.y);
}
internal UI_Size
ui_push_pref_size(Axis2 axis, UI_Size v)
{
UI_Size result = zero_struct;
switch(axis)
{
default: break;
case Axis2_X: {result = ui_push_pref_width(v);}break;
case Axis2_Y: {result = ui_push_pref_height(v);}break;
}
return result;
}
internal UI_Size
ui_pop_pref_size(Axis2 axis)
{
UI_Size result = zero_struct;
switch(axis)
{
default: break;
case Axis2_X: {result = ui_pop_pref_width();}break;
case Axis2_Y: {result = ui_pop_pref_height();}break;
}
return result;
}
internal UI_Size
ui_set_next_pref_size(Axis2 axis, UI_Size v)
{
return (axis == Axis2_X ? ui_set_next_pref_width : ui_set_next_pref_height)(v);
}
internal void
ui_push_corner_radius(F32 v)
{
ui_push_corner_radius_00(v);
ui_push_corner_radius_01(v);
ui_push_corner_radius_10(v);
ui_push_corner_radius_11(v);
}
internal void
ui_pop_corner_radius(void)
{
ui_pop_corner_radius_00();
ui_pop_corner_radius_01();
ui_pop_corner_radius_10();
ui_pop_corner_radius_11();
}
internal void
ui_push_tagf(char *fmt, ...)
{
Temp scratch = scratch_begin(0, 0);
va_list args;
va_start(args, fmt);
String8 string = push_str8fv(scratch.arena, fmt, args);
ui_push_tag(string);
va_end(args);
scratch_end(scratch);
}
internal F32
ui_top_px_height(void)
{
F32 result = ui_top_font_size();
for(UI_PrefHeightNode *n = ui_state->pref_height_stack.top; n != 0; n = n->next)
{
if(n->v.kind == UI_SizeKind_Pixels)
{
result = n->v.value;
break;
}
}
return result;
}
////////////////////////////////
//~ rjf: Generated Code
#include "generated/ui.meta.c"