975 lines
27 KiB
C
975 lines
27 KiB
C
|
|
per_thread UI_State* ui_state;
|
|
|
|
global F32 ui_g_dt;
|
|
|
|
////////////////////////////////
|
|
//~ "Generated"/meta functions
|
|
#include "ui_meta.c"
|
|
|
|
////////////////////////////////
|
|
//~ Basic type functions
|
|
|
|
//- Boxes
|
|
root_function B32
|
|
UI_box_is_nil(UI_Box *box)
|
|
{
|
|
return box == 0 || box == &ui_g_nil_box;
|
|
}
|
|
|
|
root_function UI_BoxRec
|
|
UI_box_recurse_depth_first(UI_Box *box, UI_Box *stopper, MemberOffset sib, MemberOffset child)
|
|
{
|
|
UI_BoxRec rec = {0};
|
|
rec.next = &ui_g_nil_box;
|
|
// We check what we get from the child offset.
|
|
// If it is not nil we set the next pointer to that box.
|
|
if(!UI_box_is_nil(MemberFromOff(box, UI_Box *, child)))
|
|
{
|
|
rec.next = MemberFromOff(box, UI_Box *, child);
|
|
rec.push_count = 1;
|
|
}
|
|
else
|
|
{
|
|
// If the child is nil, we loop over all boxes going up the parent chain,
|
|
// until we hit stopper.
|
|
// As soon as we hit a sibling that is non-nil, we put that in next and return.
|
|
for(UI_Box *b = box; !UI_box_is_nil(b) && b != stopper; b = b->parent)
|
|
{
|
|
if(!UI_box_is_nil(MemberFromOff(b, UI_Box *, sib)))
|
|
{
|
|
rec.next = MemberFromOff(b, UI_Box *, sib);
|
|
break;
|
|
}
|
|
rec.pop_count += 1;
|
|
}
|
|
}
|
|
|
|
return rec;
|
|
}
|
|
|
|
//- sizes
|
|
root_function UI_Size
|
|
UI_size_make(UI_SizeKind kind, F32 value, F32 strictness)
|
|
{
|
|
UI_Size result = {0};
|
|
result.kind = kind;
|
|
result.value = value;
|
|
result.strictness = strictness;
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
//- ID strings
|
|
root_function String8
|
|
UI_hash_part_from_box_string(String8 string)
|
|
{
|
|
// TODO(anton): Implement ryans stuff here with substrings
|
|
return string;
|
|
}
|
|
|
|
//- Keys
|
|
root_function UI_Key
|
|
UI_key_zero(void)
|
|
{
|
|
UI_Key key = {0};
|
|
return key;
|
|
}
|
|
|
|
root_function UI_Key
|
|
UI_key_from_string(UI_Key seed, String8 string)
|
|
{
|
|
UI_Key key = {0};
|
|
if(string.size > 0)
|
|
{
|
|
MemoryCopyStruct(&key, &seed);
|
|
for(U64 i = 0; i < string.size; i += 1)
|
|
{
|
|
key.u64[0] = ((key.u64[0] << 5) + key.u64[0]) + string.str[i];
|
|
}
|
|
}
|
|
return key;
|
|
}
|
|
|
|
root_function B32
|
|
UI_key_match(UI_Key a, UI_Key b)
|
|
{
|
|
return a.u64[0] == b.u64[0];
|
|
}
|
|
|
|
|
|
root_function UI_Signal UI_signal_from_box(UI_Box *box)
|
|
{
|
|
UI_Signal sig = {box};
|
|
// TODO(anton): possibly clipped box rect
|
|
Rng2_F32 rect = box->rect;
|
|
|
|
|
|
B32 ctx_menu_is_ancestor = 0;
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(OS_Event *event = ui_state->events->first, *next = 0; event !=0; event = next)
|
|
{
|
|
B32 taken = 0; // flag for consume
|
|
next = event->next;
|
|
|
|
//- unpack event
|
|
Vec2_F32 event_mouse = event->position;
|
|
B32 event_mouse_in_bounds = rng2_contains_vec2_F32(rect, event_mouse);
|
|
UI_MouseButtonKind event_mouse_button_kind = (event->key == OS_Key_MouseLeft ? UI_MouseButtonKind_Left :
|
|
event->key == OS_Key_MouseRight ? UI_MouseButtonKind_Right :
|
|
event->key == OS_Key_MouseMiddle ? UI_MouseButtonKind_Middle :
|
|
UI_MouseButtonKind_Left);
|
|
B32 event_key_is_mouse = (event->key == OS_Key_MouseLeft ||
|
|
event->key == OS_Key_MouseRight ||
|
|
event->key == OS_Key_MouseMiddle);
|
|
//- Mouse presses
|
|
if(box->flags & UI_BoxFlag_MouseClickable &&
|
|
event->kind == OS_EventKind_Press &&
|
|
event_mouse_in_bounds &&
|
|
event_key_is_mouse)
|
|
{
|
|
ui_state->hot_box_key = box->key;
|
|
ui_state->active_box_key[event_mouse_button_kind] = box->key;
|
|
sig.flag |= (UI_SignalFlag_LeftPressed<<event_mouse_button_kind);
|
|
|
|
taken = 1;
|
|
}
|
|
|
|
if(taken)
|
|
{
|
|
OS_consume_event(ui_state->events, event);
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////
|
|
//- mouse is over this box's rect -> always mark mouse-over
|
|
// But the rect may be non-visible
|
|
Vec2_F32 mouse_pos = ui_state->mouse;
|
|
B32 rect_contains_mouse = rng2_contains_vec2_F32(rect, mouse_pos);
|
|
if(rect_contains_mouse)
|
|
{
|
|
sig.flag |= UI_SignalFlag_MouseOver;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- mouse is over this box's rect, no other hot key? -> set hot key, mark hovering
|
|
//
|
|
if(box->flags & UI_BoxFlag_MouseClickable &&
|
|
rect_contains_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.flag |= UI_SignalFlag_Hovering;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- rjf: clicking on something outside the context menu kills the context menu
|
|
//
|
|
if(!ctx_menu_is_ancestor && sig.flag & (UI_SignalFlag_LeftPressed|UI_SignalFlag_RightPressed|UI_SignalFlag_MiddlePressed))
|
|
{
|
|
UI_ctx_menu_close();
|
|
}
|
|
|
|
return sig;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ UI State functions
|
|
root_function UI_State*
|
|
UI_state_alloc(void)
|
|
{
|
|
ui_g_nil_box.hash_next = &ui_g_nil_box;
|
|
ui_g_nil_box.hash_prev = &ui_g_nil_box;
|
|
ui_g_nil_box.first = &ui_g_nil_box;
|
|
ui_g_nil_box.last = &ui_g_nil_box;
|
|
ui_g_nil_box.next = &ui_g_nil_box;
|
|
ui_g_nil_box.prev = &ui_g_nil_box;
|
|
ui_g_nil_box.parent = &ui_g_nil_box;
|
|
|
|
Arena* arena = m_make_arena_reserve(Gigabytes(1));
|
|
UI_State *state = PushArrayZero(arena, UI_State, 1);
|
|
state->arena = arena;
|
|
state->box_table_size = 4096;
|
|
state->box_table = PushArrayZero(arena, UI_BoxSlot, state->box_table_size);
|
|
|
|
// TODO(anton): make some better system for setting and getting theme colors
|
|
state->colors[UI_Color_Null] = vec4_F32(0, 0, 0, 1);
|
|
state->colors[UI_Color_PlainBackground] = vec4_F32(0.3f, 0.2f, 0.2f, 1);
|
|
state->colors[UI_Color_PlainBorder] = vec4_F32(0.4f, 0.4f, 0.4f, 1);
|
|
state->colors[UI_Color_PlainText] = vec4_F32(0.85f, 0.85f, 0.85f, 1);
|
|
state->colors[UI_Color_PlainOverlay] = vec4_F32(0.9f, 0.6f, 0.3f, 1);
|
|
|
|
for(U64 index = 0; index < ArrayCount(state->frame_arenas); index += 1)
|
|
{
|
|
state->frame_arenas[index] = m_make_arena_reserve(Gigabytes(1));
|
|
}
|
|
state->last_frame_arena_index = 1;
|
|
state->current_frame_arena_index = 0;
|
|
// TODO(anton): drag data arena
|
|
UI_init_stack_nils(state);
|
|
return state;
|
|
}
|
|
|
|
root_function void
|
|
UI_state_set(UI_State *ui)
|
|
{
|
|
ui_state = ui;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Build phase
|
|
root_function void
|
|
UI_build_begin(OS_Handle window, OS_EventList *events)
|
|
{
|
|
|
|
//- Reset per frame state
|
|
{
|
|
UI_init_stacks(ui_state);
|
|
ui_state->build_gen += 1;
|
|
m_arena_clear(UI_frame_arena());
|
|
ui_state->root = &ui_g_nil_box;
|
|
ui_state->ctx_menu_touched_this_frame = 0;
|
|
ui_state->ctx_menu_changed = 0;
|
|
|
|
}
|
|
|
|
//- Fill per build parameters
|
|
{
|
|
ui_state->events = events;
|
|
ui_state->window = window;
|
|
ui_state->mouse = OS_mouse_from_window(window);// TODO(anton): window focused and last time moved stuff.
|
|
|
|
}
|
|
|
|
//- "Prune stale boxes"
|
|
|
|
for(U64 slot = 0; slot < ui_state->box_table_size; slot += 1)
|
|
{
|
|
for(UI_Box *box = ui_state->box_table[slot].first, *next = 0;
|
|
!UI_box_is_nil(box);
|
|
box = next)
|
|
{
|
|
// Set the next box
|
|
next = box->hash_next;
|
|
// Any boxes existing in the hash table, that has a zero key or has a last_gen_touched+1 that is less
|
|
// than the current build gen, are removed from the table. The resulting free slots in the table are pushed
|
|
// onto the free list.
|
|
if(UI_key_match(box->key, UI_key_zero()) || box->last_gen_touched+1 < ui_state->build_gen)
|
|
{
|
|
DLLRemove_NPZ(ui_state->box_table[slot].first, ui_state->box_table[slot].last,
|
|
box, hash_next, hash_prev,
|
|
UI_box_is_nil, UI_box_set_nil);
|
|
StackPush(ui_state->first_free_box, box);
|
|
ui_state->free_box_list_count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//- Build root
|
|
// This is a UI box that extends the whole of the window.
|
|
|
|
{
|
|
Rng2_F32 client_rect = OS_client_rect_from_window(window);
|
|
Vec2_F32 client_rect_size = dim2_F32(client_rect);
|
|
UI_set_next_pref_width(UI_pixels(client_rect_size.x, 1));
|
|
UI_set_next_pref_height(UI_pixels(client_rect_size.y, 1));
|
|
UI_set_next_child_layout_axis(Axis2_Y);
|
|
String8 root_name = str8_lit("root_box");
|
|
UI_Box *root = UI_box_make(0 /* zero box flags for root box */, root_name);
|
|
UI_push_parent(root);
|
|
ui_state->root = root;
|
|
}
|
|
//- Context menu setup
|
|
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;
|
|
}
|
|
Vec2_F32 anchor = add2_F32(ui_state->ctx_menu_anchor_box_last_pos, ui_state->ctx_menu_anchor_offset);
|
|
|
|
UI_fixed_x(anchor.x) UI_fixed_y(anchor.y)
|
|
UI_pref_width(UI_size_by_children(0, 1)) UI_pref_height(UI_size_by_children(0, 1))
|
|
{
|
|
UI_set_next_child_layout_axis(Axis2_Y);
|
|
|
|
ui_state->ctx_menu_root = UI_box_make(UI_BoxFlag_FloatingX|UI_BoxFlag_FloatingY, str8_lit("ctx_menu"));
|
|
}
|
|
|
|
}
|
|
|
|
// 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 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();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
root_function void
|
|
UI_build_end(void)
|
|
{
|
|
|
|
ui_state->ctx_menu_touched_this_frame = 1;
|
|
if(ui_state->ctx_menu_open != 0 || ui_state->ctx_menu_touched_this_frame == 0)
|
|
{
|
|
//UI_ctx_menu_close();
|
|
}
|
|
|
|
UI_layout();
|
|
|
|
// TODO(anton): When I need this and understand why
|
|
//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))
|
|
//{
|
|
//Rng2_F32 root_rect = ui_state->ctx_menu_root->rect;
|
|
//Vec2_F32 pos =
|
|
//{
|
|
//anchor_box->rect.x0 + ui_state->ctx_menu_anchor_offset.x,
|
|
//anchor_box->rect.y0 + ui_state->ctx_menu_anchor_offset.y,
|
|
//};
|
|
//Vec2_F32 shift = sub2_F32(pos, root_rect.p0);
|
|
//Rng2_F32 new_root_rect = shift2_F32(root_rect, shift);
|
|
//ui_state->ctx_menu_root->fixed_position = new_root_rect.p0;
|
|
//ui_state->ctx_menu_root->fixed_size = dim2_F32(new_root_rect);
|
|
//ui_state->ctx_menu_root->rect = new_root_rect;
|
|
//}
|
|
//}
|
|
//
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Context menu
|
|
root_function void
|
|
UI_ctx_menu_open(UI_Key key, UI_Key anchor_key, Vec2_F32 anchor_offset)
|
|
{
|
|
anchor_offset.x = (F32)(int)anchor_offset.x;
|
|
anchor_offset.y = (F32)(int)anchor_offset.y;
|
|
ui_state->next_ctx_menu_open = 1;
|
|
ui_state->ctx_menu_changed = 1;
|
|
// TODO(anton): time parameter for ctx menu animation
|
|
ui_state->ctx_menu_key = key;
|
|
ui_state->next_ctx_menu_anchor_key = anchor_key;
|
|
ui_state->ctx_menu_anchor_offset = anchor_offset;
|
|
ui_state->ctx_menu_touched_this_frame = 1;
|
|
ui_state->ctx_menu_anchor_box_last_pos = vec2_F32(100, 100);
|
|
}
|
|
|
|
root_function void
|
|
UI_ctx_menu_close(void)
|
|
{
|
|
ui_state->next_ctx_menu_open = 0;
|
|
}
|
|
|
|
root_function B32
|
|
UI_begin_ctx_menu(UI_Key key)
|
|
{
|
|
UI_push_parent(ui_state->root);
|
|
UI_push_parent(ui_state->ctx_menu_root);
|
|
B32 result = UI_key_match(key, ui_state->ctx_menu_key) && ui_state->ctx_menu_open;
|
|
if(result)
|
|
{
|
|
ui_state->ctx_menu_touched_this_frame = 1;
|
|
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DrawBackground;
|
|
ui_state->ctx_menu_root->flags |= UI_BoxFlag_DrawDropShadow;
|
|
UI_push_pref_width(UI_pixels(100, 0));
|
|
UI_push_pref_height(UI_pixels(200, 0));
|
|
}
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
root_function B32
|
|
UI_ctx_menu_is_open(UI_Key key)
|
|
{
|
|
return ui_state->ctx_menu_open && UI_key_match(key, ui_state->ctx_menu_key);
|
|
}
|
|
|
|
root_function void
|
|
UI_end_ctx_menu(void)
|
|
{
|
|
//UI_Box* top_parent = UI_top_parent();
|
|
UI_pop_pref_width();
|
|
UI_pop_pref_height();
|
|
UI_pop_parent();
|
|
//top_parent = UI_top_parent();
|
|
UI_pop_parent();
|
|
//top_parent = UI_top_parent();
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ UI Frame
|
|
root_function Arena *
|
|
UI_frame_arena(void)
|
|
{
|
|
return ui_state->frame_arenas[ui_state->current_frame_arena_index];
|
|
}
|
|
|
|
root_function void
|
|
UI_frame_begin(F32 delta_time)
|
|
{
|
|
ui_g_dt = delta_time;
|
|
}
|
|
|
|
root_function void
|
|
UI_frame_end(void)
|
|
{
|
|
U32 last = ui_state->last_frame_arena_index;
|
|
ui_state->last_frame_arena_index = ui_state->current_frame_arena_index;
|
|
ui_state->current_frame_arena_index = last;
|
|
}
|
|
|
|
|
|
////////////////////////////////
|
|
//~ Box hierarchy construction
|
|
|
|
root_function UI_Box *
|
|
UI_box_from_key(UI_Key key)
|
|
{
|
|
UI_Box *result = &ui_g_nil_box;
|
|
// The box table contains all the hashed boxes, so we mod by the size to get the slot in the table.
|
|
U64 slot = key.u64[0] % ui_state->box_table_size;
|
|
if(!UI_key_match(key, UI_key_zero()))
|
|
{
|
|
// We check if we have a box corresponding to the key in the hash table, and return that if we find it.
|
|
for(UI_Box *b = ui_state->box_table[slot].first; !UI_box_is_nil(b); b = b->hash_next)
|
|
{
|
|
if(UI_key_match(b->key, key))
|
|
{
|
|
result = b;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
root_function UI_Box *
|
|
UI_box_make_from_key(UI_BoxFlags flags, UI_Key key)
|
|
{
|
|
// Get the nil box or a box from the hash table.
|
|
UI_Box *box = UI_box_from_key(key);
|
|
|
|
// If the key has already been used, we "trample" over the key/box pair to make an id-less box.
|
|
if(box->last_gen_touched == ui_state->build_gen)
|
|
{
|
|
box = &ui_g_nil_box;
|
|
key = UI_key_zero();
|
|
}
|
|
|
|
// Allocate the box if it's not allocated, or if it's a duplicate key
|
|
B32 first_frame = 0;
|
|
if(UI_box_is_nil(box))
|
|
{
|
|
U64 slot = key.u64[0] % ui_state->box_table_size;
|
|
first_frame = 1;
|
|
box = ui_state->first_free_box;
|
|
// If the first free box is nil we push a new box on the arena.
|
|
if(UI_box_is_nil(box))
|
|
{
|
|
box = PushArrayZero(ui_state->arena, UI_Box, 1);
|
|
}
|
|
else
|
|
{
|
|
// If the first free box is non-nil, we pop it from the stack.
|
|
// This macro works since the first_free_box is a UI_Box type, which has a next member.
|
|
// So this will but the ui_state->first_free_box pointer to the next member of the previous
|
|
// element pointed to by ui_state->first_free_box.
|
|
StackPop(ui_state->first_free_box);
|
|
MemoryZeroStruct(box);
|
|
ui_state->free_box_list_count -= 1;
|
|
}
|
|
// We push back the box to the doubly linked list in the table slot, using custom functions for
|
|
// zero check and zero set.
|
|
DLLPushBack_NPZ(ui_state->box_table[slot].first,
|
|
ui_state->box_table[slot].last, box, hash_next, hash_prev,
|
|
UI_box_is_nil, UI_box_set_nil);
|
|
box->key = key;
|
|
}
|
|
|
|
//- Link to the tree by getting the parent. If the parent is nil, the current box should actually be the root.
|
|
UI_Box *parent = UI_top_parent();
|
|
if(UI_box_is_nil(parent))
|
|
{
|
|
ui_state->root = box;
|
|
}
|
|
else
|
|
{
|
|
DLLPushBack_NPZ(parent->first, parent->last, box, next, prev, UI_box_is_nil, UI_box_set_nil);
|
|
parent->child_count += 1;
|
|
box->parent = parent;
|
|
}
|
|
|
|
//- Fill the state of the current box
|
|
if(!UI_box_is_nil(box))
|
|
{
|
|
|
|
if(first_frame)
|
|
{
|
|
box->first_gen_touched = ui_state->build_gen;
|
|
}
|
|
|
|
box->child_count = 0;
|
|
box->first = box->last = &ui_g_nil_box;
|
|
box->flags = flags | UI_top_flags();
|
|
|
|
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_top_pref_width();
|
|
}
|
|
|
|
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_top_pref_height();
|
|
}
|
|
|
|
|
|
box->calc_rel_pos.x = UI_top_fixed_x();
|
|
box->calc_rel_pos.y = UI_top_fixed_y();
|
|
|
|
|
|
box->child_layout_axis = UI_top_child_layout_axis();
|
|
box->last_gen_touched = ui_state->build_gen;
|
|
|
|
// TODO(anton): Text and color drawing properties here
|
|
box->background_color = ui_state->colors[UI_Color_PlainBackground];
|
|
box->text_color = ui_state->colors[UI_Color_PlainText];
|
|
box->border_color = ui_state->colors[UI_Color_PlainBorder];
|
|
box->overlay_color = ui_state->colors[UI_Color_PlainOverlay];
|
|
|
|
}
|
|
|
|
UI_auto_pop_stacks(ui_state);
|
|
|
|
return box;
|
|
}
|
|
|
|
root_function UI_Box *
|
|
UI_box_make(UI_BoxFlags flags, String8 string)
|
|
{
|
|
UI_Key seed = UI_top_seed_key();
|
|
|
|
String8 string_hash_part = UI_hash_part_from_box_string(string);
|
|
UI_Key key = UI_key_from_string(seed, string_hash_part);
|
|
|
|
UI_Box *box = UI_box_make_from_key(flags, key);
|
|
|
|
box->string = str8_copy(UI_frame_arena(), string);
|
|
|
|
return box;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ layout
|
|
|
|
root_function void
|
|
UI_solve_independent_sizes(UI_Box *root, Axis2 axis)
|
|
{
|
|
switch(root->pref_size[axis].kind)
|
|
{
|
|
default:break;
|
|
case UI_SizeKind_Pixels:
|
|
{
|
|
root->calc_size.v[axis] = root->pref_size[axis].value;
|
|
root->calc_size.v[axis] = floor_F32(root->calc_size.v[axis]);
|
|
} break;
|
|
}
|
|
|
|
// Recurse
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
UI_solve_independent_sizes(child, axis);
|
|
}
|
|
|
|
}
|
|
|
|
root_function void
|
|
UI_solve_upward_dependent_sizes(UI_Box *root, Axis2 axis)
|
|
{
|
|
switch(root->pref_size[axis].kind)
|
|
{
|
|
default:break;
|
|
case UI_SizeKind_Percent:
|
|
{
|
|
UI_Box *ancestor = &ui_g_nil_box;
|
|
// Move up the parents and get the first ancestor that does not have
|
|
// size by children.
|
|
for(UI_Box *p = root->parent; !UI_box_is_nil(p); p = p->parent)
|
|
{
|
|
if(p->pref_size[axis].kind != UI_SizeKind_SizeByChildren)
|
|
{
|
|
ancestor = p;
|
|
break;
|
|
}
|
|
}
|
|
// The calculated size of the argument box to this function is then
|
|
// the ancestor calculated size, scaled by the preferred size of this box, which is
|
|
// assumed to be given as a percentage by the SizeKind
|
|
if(!UI_box_is_nil(ancestor))
|
|
{
|
|
root->calc_size.v[axis] = ancestor->calc_size.v[axis] * root->pref_size[axis].value;
|
|
root->calc_size.v[axis] = floor_F32(root->calc_size.v[axis]);
|
|
}
|
|
} break;
|
|
}
|
|
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
UI_solve_upward_dependent_sizes(child, axis);
|
|
}
|
|
}
|
|
|
|
root_function void
|
|
UI_solve_downward_dependent_sizes(UI_Box *root, Axis2 axis)
|
|
{
|
|
// Here we first recurse since we will depend on the result for this input.
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
UI_solve_downward_dependent_sizes(child, axis);
|
|
}
|
|
|
|
switch(root->pref_size[axis].kind)
|
|
{
|
|
default:break;
|
|
case UI_SizeKind_SizeByChildren:
|
|
{
|
|
F32 value = 0;
|
|
{
|
|
// We will calculate the size to be the sum of the child sizes on this axis,
|
|
// if we are on the layout axis.
|
|
// If we are not on the layout axis the size will just be the maximum size of any child on that axis.
|
|
if(axis == root->child_layout_axis)
|
|
{
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
value += child->calc_size.v[axis];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
value = Max(value, child->calc_size.v[axis]);
|
|
}
|
|
}
|
|
}
|
|
root->calc_size.v[axis] = value;
|
|
root->calc_size.v[axis] = floor_F32(root->calc_size.v[axis]);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
root_function void
|
|
UI_solve_size_violations(UI_Box *root, Axis2 axis)
|
|
{
|
|
// Determine maximum available space from the root size.
|
|
F32 available_space = root->calc_size.v[axis];
|
|
|
|
F32 taken_space = 0.0f;
|
|
F32 total_fix_budget = 0.0f;
|
|
B32 is_overflow_x_on_axis = (root->flags & (UI_BoxFlag_OverflowX<<axis));
|
|
if(!is_overflow_x_on_axis)
|
|
{
|
|
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)
|
|
{
|
|
taken_space += child->calc_size.v[axis];
|
|
}
|
|
else
|
|
{
|
|
taken_space = Max(taken_space, child->calc_size.v[axis]);
|
|
}
|
|
|
|
F32 fix_budget_this_child = child->calc_size.v[axis] * (1.0f - child->pref_size[axis].strictness);
|
|
total_fix_budget += fix_budget_this_child;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
//- Fix children as much as possible within the calculated budget
|
|
if(!is_overflow_x_on_axis)
|
|
{
|
|
F32 violation = taken_space - available_space;
|
|
if(violation > 0 && total_fix_budget > 0)
|
|
{
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
|
|
{
|
|
F32 fix_budget_this_child = child->calc_size.v[axis] * (1.0f - child->pref_size[axis].strictness);
|
|
F32 fix_size_this_child = 0.0f;
|
|
if(axis == root->child_layout_axis)
|
|
{
|
|
fix_size_this_child = fix_budget_this_child * (violation / total_fix_budget);
|
|
}
|
|
else
|
|
{
|
|
fix_size_this_child = child->calc_size.v[axis] - available_space;
|
|
}
|
|
fix_size_this_child = Clamp(0, fix_size_this_child, fix_budget_this_child);
|
|
child->calc_size.v[axis] -= fix_size_this_child;
|
|
child->calc_size.v[axis] = floor_F32(child->calc_size.v[axis]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//- Position all children after fixup
|
|
{
|
|
if(axis == root->child_layout_axis)
|
|
{
|
|
// Determine the relative offset by incrementing p by the size of a child on the relevant axis.
|
|
F32 p = 0.0f;
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
|
|
{
|
|
child->calc_rel_pos.v[axis] = p;
|
|
p += child->calc_size.v[axis];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we're not on the layout axis the relative position is just zero
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
|
|
{
|
|
child->calc_rel_pos.v[axis] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the actual position values and rectangles
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
Rng2_F32 last_rel_rect = child->rel_rect;
|
|
unused_variable(last_rel_rect);
|
|
// The relative rectangle starts at the relative position, and ends at the relative rectangle p0 + size.
|
|
// TODO(anton): What does the relative rectangle mean?
|
|
child->rel_rect.p0.v[axis] = child->calc_rel_pos.v[axis];
|
|
child->rel_rect.p1.v[axis] = child->rel_rect.p0.v[axis] + child->calc_size.v[axis];
|
|
// TODO(anton): Corner stuff here
|
|
// The actual rectangle is the root rect p0, plus th relative p0, minus view offset of the root.
|
|
// And the p1 is the p0 + the size of the child.
|
|
child->rect.p0.v[axis] = root->rect.p0.v[axis] + child->rel_rect.p0.v[axis] - root->view_offset.v[axis];
|
|
child->rect.p1.v[axis] = child->rect.p0.v[axis] + child->calc_size.v[axis];
|
|
if(!(child->flags & (UI_BoxFlag_FloatingX<<axis)))
|
|
{
|
|
// If we are not floating on this axis, we floor the rectangle? Because we want to align to pixel sizes?
|
|
child->rect.p0.v[axis] = floor_F32(child->rect.p0.v[axis]);
|
|
child->rect.p1.v[axis] = floor_F32(child->rect.p1.v[axis]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recurse
|
|
for(UI_Box *child = root->first; !UI_box_is_nil(child); child = child->next)
|
|
{
|
|
UI_solve_size_violations(child, axis);
|
|
}
|
|
|
|
}
|
|
|
|
root_function void
|
|
UI_layout_root(UI_Box *root, Axis2 axis)
|
|
{
|
|
UI_solve_independent_sizes(root, axis);
|
|
UI_solve_upward_dependent_sizes(root, axis);
|
|
UI_solve_downward_dependent_sizes(root, axis);
|
|
UI_solve_size_violations(root, axis);
|
|
}
|
|
|
|
root_function void UI_layout(void)
|
|
{
|
|
for(Axis2 axis = (Axis2)0; axis < Axis2_COUNT; axis = (Axis2)(axis+1))
|
|
{
|
|
UI_layout_root(ui_state->root, axis);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Compositions
|
|
root_function void
|
|
UI_push_pref_size(Axis2 axis, UI_Size v)
|
|
{
|
|
if(axis == Axis2_X)
|
|
{
|
|
UI_push_pref_width(v);
|
|
}
|
|
else
|
|
{
|
|
UI_push_pref_height(v);
|
|
}
|
|
}
|
|
|
|
root_function void UI_pop_pref_size(Axis2 axis)
|
|
{
|
|
if(axis == Axis2_X)
|
|
{
|
|
UI_pop_pref_width();
|
|
}
|
|
else
|
|
{
|
|
UI_pop_pref_height();
|
|
}
|
|
}
|
|
|
|
root_function void
|
|
UI_set_next_pref_size(Axis2 axis, UI_Size v)
|
|
{
|
|
if(axis == Axis2_X)
|
|
{
|
|
UI_set_next_pref_width(v);
|
|
}
|
|
else
|
|
{
|
|
UI_set_next_pref_height(v);
|
|
}
|
|
}
|
|
|
|
root_function void
|
|
UI_push_fixed_pos(Vec2_F32 v)
|
|
{
|
|
UI_push_fixed_x(v.x);
|
|
UI_push_fixed_y(v.y);
|
|
}
|
|
|
|
root_function void
|
|
UI_pop_fixed_pos()
|
|
{
|
|
UI_pop_fixed_x();
|
|
UI_pop_fixed_y();
|
|
}
|
|
|
|
root_function void
|
|
UI_set_next_fixed_pos(Vec2_F32 v)
|
|
{
|
|
UI_set_next_fixed_x(v.x);
|
|
UI_set_next_fixed_y(v.y);
|
|
}
|
|
|
|
root_function void
|
|
UI_push_fixed_rect(Rng2_F32 rect)
|
|
{
|
|
Vec2_F32 dim = dim2_F32(rect);
|
|
UI_push_fixed_pos(rect.p0);
|
|
UI_push_pref_size(Axis2_X, UI_pixels(dim.x, 1));
|
|
UI_push_pref_size(Axis2_Y, UI_pixels(dim.y, 1));
|
|
}
|
|
|
|
root_function void
|
|
UI_pop_fixed_rect()
|
|
{
|
|
UI_pop_fixed_pos();
|
|
UI_pop_pref_size(Axis2_X);
|
|
UI_pop_pref_size(Axis2_Y);
|
|
}
|
|
|
|
root_function void
|
|
UI_set_next_fixed_rect(Rng2_F32 rect)
|
|
{
|
|
Vec2_F32 dim = dim2_F32(rect);
|
|
UI_set_next_fixed_pos(rect.p0);
|
|
UI_set_next_pref_size(Axis2_X, UI_pixels(dim.x, 1));
|
|
UI_set_next_pref_size(Axis2_Y, UI_pixels(dim.y, 1));
|
|
}
|
|
|
|
root_function Rng2_F32
|
|
UI_push_rect(Rng2_F32 rect)
|
|
{
|
|
Rng2_F32 replaced = {0};
|
|
Vec2_F32 size = dim2_F32(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;
|
|
}
|
|
|
|
root_function Rng2_F32
|
|
UI_pop_rect(void)
|
|
{
|
|
Rng2_F32 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;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Drawing and text
|
|
root_function Vec2_F32
|
|
UI_text_pos_from_box(UI_Box *box)
|
|
{
|
|
// The stb assumes that the supplied start position will give
|
|
// the _BASELINE_ of the text that is to be produced.
|
|
// So the text position we give here is adjusted to x of the box rect's p0,
|
|
// and to the y of the box rect's p1.
|
|
F32 offset_x_pixels = 0.1f*(box->rect.x1-box->rect.x0);
|
|
F32 offset_y_pixels = 0.30*(box->rect.y1-box->rect.y0);
|
|
Vec2_F32 result = {0};
|
|
result.x = box->rect.x0 + offset_x_pixels;
|
|
result.y = box->rect.y1 - offset_y_pixels;
|
|
|
|
return result;
|
|
}
|
|
|
|
root_function String8
|
|
UI_display_string_from_box(UI_Box *box)
|
|
{
|
|
return box->string;
|
|
|
|
} |