// 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<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<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<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<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) { // 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<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<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: 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<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<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<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<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_tag(str8_lit(".")); ui_push_tag(str8_lit("floating")); } internal void ui_tooltip_end_base(void) { ui_pop_tag(); ui_pop_tag(); 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; } 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<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<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<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< 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< 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< double-dragging // if(box->flags & UI_BoxFlag_MouseClickable) { for EachEnumVal(UI_MouseButtonKind, k) { if(sig.f & (UI_SignalFlag_LeftDragging<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< triple-dragging // if(box->flags & UI_BoxFlag_MouseClickable) { for EachEnumVal(UI_MouseButtonKind, k) { if(sig.f & (UI_SignalFlag_LeftDragging<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< 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< 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) { 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"