1797 lines
55 KiB
C
1797 lines
55 KiB
C
UI_Ctx UI = Zi;
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Key helpers
|
|
|
|
b32 UI_MatchKey(UI_Key a, UI_Key b)
|
|
{
|
|
return a.v == b.v;
|
|
}
|
|
|
|
b32 UI_IsKeyNil(UI_Key key)
|
|
{
|
|
return key.v == 0;
|
|
}
|
|
|
|
UI_Key UI_KeyFromString(String str)
|
|
{
|
|
u64 top_tag = UI_Top(Tag);
|
|
UI_Key key = Zi;
|
|
key.v = HashStringEx(top_tag, str);
|
|
return key;
|
|
}
|
|
|
|
UI_Key UI_KeyF_(String fmt, ...)
|
|
{
|
|
UI_Key key = Zi;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
String name = FormatString(scratch.arena, fmt, FmtArgsFromVaList(scratch.arena, args));
|
|
key = UI_KeyFromString(name);
|
|
va_end(args);
|
|
}
|
|
EndScratch(scratch);
|
|
return key;
|
|
}
|
|
|
|
UI_Key UI_RandKey(void)
|
|
{
|
|
UI_Key key = Zi;
|
|
key.v = RandU64FromState(&UI.rand);
|
|
return key;
|
|
}
|
|
|
|
UI_Box *UI_BoxFromKey(UI_Key key)
|
|
{
|
|
UI_Box *box = 0;
|
|
if (key.v != 0)
|
|
{
|
|
UI_BoxBin *bin = &UI.box_bins[key.v % countof(UI.box_bins)];
|
|
for (box = bin->first; box; box = box->next_in_bin)
|
|
{
|
|
if (UI_MatchKey(box->key, key))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return box;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Anchor helpers
|
|
|
|
UI_Region UI_RegionFromPair(UI_RegionPair pair)
|
|
{
|
|
UI_Region result = 0;
|
|
switch(pair.y)
|
|
{
|
|
case UI_AxisRegion_Start:
|
|
{
|
|
switch(pair.x)
|
|
{
|
|
case UI_AxisRegion_Start: { result = UI_Region_TopLeft; } break;
|
|
case UI_AxisRegion_Center: { result = UI_Region_Top; } break;
|
|
case UI_AxisRegion_End: { result = UI_Region_TopRight; } break;
|
|
}
|
|
} break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
switch(pair.x)
|
|
{
|
|
case UI_AxisRegion_Start: { result = UI_Region_Left; } break;
|
|
case UI_AxisRegion_Center: { result = UI_Region_Center; } break;
|
|
case UI_AxisRegion_End: { result = UI_Region_Right; } break;
|
|
}
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
switch(pair.x)
|
|
{
|
|
case UI_AxisRegion_Start: { result = UI_Region_BottomLeft; } break;
|
|
case UI_AxisRegion_Center: { result = UI_Region_Bottom; } break;
|
|
case UI_AxisRegion_End: { result = UI_Region_BottomRight; } break;
|
|
}
|
|
} break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
UI_RegionPair UI_PairFromRegion(UI_Region region)
|
|
{
|
|
UI_RegionPair result = Zi;
|
|
// Horizontal
|
|
switch(region)
|
|
{
|
|
default: break;
|
|
case UI_Region_TopLeft:
|
|
case UI_Region_Left:
|
|
case UI_Region_BottomLeft:
|
|
{
|
|
result.x = UI_AxisRegion_Start;
|
|
} break;
|
|
case UI_Region_Top:
|
|
case UI_Region_Center:
|
|
case UI_Region_Bottom:
|
|
{
|
|
result.x = UI_AxisRegion_Center;
|
|
} break;
|
|
case UI_Region_TopRight:
|
|
case UI_Region_Right:
|
|
case UI_Region_BottomRight:
|
|
{
|
|
result.x = UI_AxisRegion_End;
|
|
} break;
|
|
}
|
|
// Vertical
|
|
switch(region)
|
|
{
|
|
default: break;
|
|
case UI_Region_TopLeft:
|
|
case UI_Region_Top:
|
|
case UI_Region_TopRight:
|
|
{
|
|
result.y = UI_AxisRegion_Start;
|
|
} break;
|
|
case UI_Region_Left:
|
|
case UI_Region_Center:
|
|
case UI_Region_Right:
|
|
{
|
|
result.y = UI_AxisRegion_Center;
|
|
} break;
|
|
case UI_Region_BottomLeft:
|
|
case UI_Region_Bottom:
|
|
case UI_Region_BottomRight:
|
|
{
|
|
result.y = UI_AxisRegion_End;
|
|
} break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Iteration helpers
|
|
|
|
UI_BoxIterResult UI_FirstBox(Arena *arena, UI_BoxIter *iter, UI_Key start_key)
|
|
{
|
|
// Free old stack
|
|
if (iter->first)
|
|
{
|
|
if (iter->last_free)
|
|
{
|
|
iter->last_free->next = iter->first;
|
|
}
|
|
else
|
|
{
|
|
iter->first_free = iter->first;
|
|
}
|
|
iter->last_free = iter->first;
|
|
iter->first = 0;
|
|
}
|
|
// Create root dfs node
|
|
UI_BoxIterDfsNode *dfs = iter->first_free;
|
|
if (dfs)
|
|
{
|
|
SllQueuePop(iter->first_free, iter->last_free);
|
|
ZeroStruct(dfs);
|
|
}
|
|
else
|
|
{
|
|
dfs = PushStruct(arena, UI_BoxIterDfsNode);
|
|
}
|
|
dfs->box = UI_BoxFromKey(start_key);
|
|
iter->first = dfs;
|
|
return UI_NextBox(arena, iter);
|
|
}
|
|
|
|
UI_BoxIterResult UI_NextBox(Arena *arena, UI_BoxIter *iter)
|
|
{
|
|
UI_BoxIterResult result = Zi;
|
|
if (iter->first)
|
|
{
|
|
UI_BoxIterDfsNode *dfs = iter->first;
|
|
UI_Box *box = dfs->box;
|
|
if (!dfs->visited)
|
|
{
|
|
// Pre order
|
|
dfs->visited = 1;
|
|
result.box = box;
|
|
result.pre = 1;
|
|
// Push floating children to dfs stack
|
|
for (UI_Box *child = box->last; child; child = child->prev)
|
|
{
|
|
if (AnyBit(child->desc.flags, UI_BoxFlag_Floating))
|
|
{
|
|
UI_BoxIterDfsNode *child_dfs = iter->first_free;
|
|
if (child_dfs)
|
|
{
|
|
SllQueuePop(iter->first_free, iter->last_free);
|
|
ZeroStruct(child_dfs);
|
|
}
|
|
else
|
|
{
|
|
child_dfs = PushStruct(arena, UI_BoxIterDfsNode);
|
|
}
|
|
child_dfs->box = child;
|
|
SllStackPush(iter->first, child_dfs);
|
|
}
|
|
}
|
|
// Push non-floating children to dfs stack
|
|
for (UI_Box *child = box->last; child; child = child->prev)
|
|
{
|
|
if (!AnyBit(child->desc.flags, UI_BoxFlag_Floating))
|
|
{
|
|
UI_BoxIterDfsNode *child_dfs = iter->first_free;
|
|
if (child_dfs)
|
|
{
|
|
SllQueuePop(iter->first_free, iter->last_free);
|
|
ZeroStruct(child_dfs);
|
|
}
|
|
else
|
|
{
|
|
child_dfs = PushStruct(arena, UI_BoxIterDfsNode);
|
|
}
|
|
child_dfs->box = child;
|
|
SllStackPush(iter->first, child_dfs);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Post order
|
|
result.box = box;
|
|
result.pre = 0;
|
|
SllStackPop(iter->first);
|
|
SllQueuePush(iter->first_free, iter->last_free, dfs);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ String helpers
|
|
|
|
String UI_StringF_(String fmt, ...)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
String result = Zi;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
String str = FormatString(frame->arena, fmt, FmtArgsFromVaList(scratch.arena, args));
|
|
va_end(args);
|
|
}
|
|
EndScratch(scratch);
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Checkpoint helpers
|
|
|
|
UI_Checkpoint UI_PushCP(UI_Key parent)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
stack->top_checkpoint.v += 1;
|
|
if (parent.v != 0)
|
|
{
|
|
UI_Push(Parent, parent);
|
|
}
|
|
return stack->top_checkpoint;
|
|
}
|
|
|
|
void UI_PopCP(UI_Checkpoint cp)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
for (UI_StyleKind kind = UI_StyleKind_None; kind < UI_StyleKind_COUNT; ++kind)
|
|
{
|
|
UI_StyleNode *n = stack->style_tops[kind];
|
|
while (n && n->checkpoint.v >= cp.v)
|
|
{
|
|
UI_StyleNode *next = n->next;
|
|
n->next = frame->first_free_style_node;
|
|
frame->first_free_style_node = n;
|
|
stack->style_tops[kind] = next;
|
|
n = next;
|
|
}
|
|
}
|
|
stack->top_checkpoint.v = cp.v - 1;
|
|
}
|
|
|
|
UI_Checkpoint UI_TopCP(void)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
return stack->top_checkpoint;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Style stack helpers
|
|
|
|
void UI_PushDefaults(void)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
UI_Checkpoint checkpoint = stack->top_checkpoint;
|
|
{
|
|
for (UI_StyleKind kind = UI_StyleKind_None; kind < UI_StyleKind_COUNT; ++kind)
|
|
{
|
|
UI_StyleDesc desc = Zi;
|
|
desc.style.kind = kind;
|
|
switch (kind)
|
|
{
|
|
default: break;
|
|
case UI_StyleKind_Parent: { desc.style.Parent = UI_RootKey; } break;
|
|
case UI_StyleKind_Width: { desc.style.Width = UI_GROW(1, 0); } break;
|
|
case UI_StyleKind_Height: { desc.style.Height = UI_GROW(1, 0); } break;
|
|
case UI_StyleKind_Scale: { desc.style.Scale = VEC2(1, 1); } break;
|
|
case UI_StyleKind_Font: { desc.style.Font = UI_BuiltinTextFont(); } break;
|
|
case UI_StyleKind_FontSize: { desc.style.FontSize = 16.0f; } break;
|
|
case UI_StyleKind_Tint: { desc.style.Tint = Color_White; } break;
|
|
case UI_StyleKind_TextColor: { desc.style.TextColor = Color_White; } break;
|
|
case UI_StyleKind_Tag: { desc.style.Tag = HashString(Lit("root")); } break;
|
|
case UI_StyleKind_DebugColor: { desc.style.DebugColor = Rgba(1, 0, 1, 0.5); } break;
|
|
case UI_StyleKind_InvisibleDebugColor: { desc.style.InvisibleDebugColor = Rgba(0, 1, 1, 0.25); } break;
|
|
case UI_StyleKind_BackgroundTextureSliceUv: { desc.style.BackgroundTextureSliceUv = RNG2(VEC2(0, 0), VEC2(1, 1)); } break;
|
|
};
|
|
UI_PushStyle(desc);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UI_PushStyle(UI_StyleDesc desc)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
UI_StyleKind kind = desc.style.kind;
|
|
if (kind >= UI_StyleKind_BeginVirtualStyles_)
|
|
{
|
|
switch(kind)
|
|
{
|
|
default: break;
|
|
|
|
case UI_StyleKind_AxisSize:
|
|
{
|
|
if (desc.axis == Axis_X)
|
|
{
|
|
UI_PushCopy(Width, desc, desc.style.AxisSize);
|
|
}
|
|
else
|
|
{
|
|
UI_PushCopy(Height, desc, desc.style.AxisSize);
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UI_StyleNode *n = stack->style_tops[kind];
|
|
if (!(n && n->override))
|
|
{
|
|
{
|
|
if (n && n->pop_when_used)
|
|
{
|
|
UI_StyleNode *next = n->next;
|
|
ZeroStruct(n);
|
|
n->next = next;
|
|
}
|
|
else
|
|
{
|
|
n = frame->first_free_style_node;
|
|
if (n)
|
|
{
|
|
frame->first_free_style_node = n->next;
|
|
ZeroStruct(n);
|
|
}
|
|
else
|
|
{
|
|
n = PushStruct(frame->arena, UI_StyleNode);
|
|
}
|
|
n->next = stack->style_tops[kind];
|
|
stack->style_tops[kind] = n;
|
|
}
|
|
n->style = desc.style;
|
|
n->pop_when_used = desc.pop_when_used;
|
|
n->override = desc.override;
|
|
n->checkpoint = stack->top_checkpoint;
|
|
}
|
|
|
|
// Initialize style data from desc
|
|
switch (kind)
|
|
{
|
|
default: break;
|
|
|
|
case UI_StyleKind_Tag:
|
|
{
|
|
if (n->next != 0)
|
|
{
|
|
n->style.Tag = MixU64s(n->next->style.Tag, n->style.Tag);
|
|
}
|
|
} break;
|
|
|
|
case UI_StyleKind_Text:
|
|
{
|
|
n->style.Text = PushString(frame->arena, n->style.Text);
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UI_Style UI_PopStyle(UI_StyleDesc desc)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_Stack *stack = frame->top_stack;
|
|
UI_Style result = Zi;
|
|
UI_StyleKind kind = desc.style.kind;
|
|
result.kind = kind;
|
|
if (kind >= UI_StyleKind_BeginVirtualStyles_)
|
|
{
|
|
switch(kind)
|
|
{
|
|
default: break;
|
|
case UI_StyleKind_AxisSize:
|
|
{
|
|
if (desc.axis == Axis_X)
|
|
{
|
|
desc.style.kind = UI_StyleKind_Width;
|
|
result = UI_PopStyle(desc);
|
|
}
|
|
else
|
|
{
|
|
desc.style.kind = UI_StyleKind_Height;
|
|
result = UI_PopStyle(desc);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UI_StyleNode *n = stack->style_tops[kind];
|
|
result = n->style;
|
|
if (desc.force_pop || (desc.use && n->pop_when_used))
|
|
{
|
|
stack->style_tops[kind] = n->next;
|
|
n->next = frame->first_free_style_node;
|
|
frame->first_free_style_node = n;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Command helpers
|
|
|
|
UI_Key UI_BuildBoxEx(UI_Key semantic_key)
|
|
{
|
|
UI_Key key = semantic_key;
|
|
b32 is_transient = 0;
|
|
if (UI_IsKeyNil(key))
|
|
{
|
|
key = UI_RandKey();
|
|
is_transient = 1;
|
|
}
|
|
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_CmdNode *n = PushStruct(frame->arena, UI_CmdNode);
|
|
n->cmd.kind = UI_CmdKind_BuildBox;
|
|
{
|
|
n->cmd.box.key = key;
|
|
n->cmd.box.is_transient = is_transient;
|
|
n->cmd.box.parent = UI_Top(Parent);
|
|
n->cmd.box.flags = (UI_Top(Flags) | UI_Top(OrFlags)) & ~UI_Top(OmitFlags);
|
|
n->cmd.box.pref_semantic_dims[Axis_X] = UI_Top(Width);
|
|
n->cmd.box.pref_semantic_dims[Axis_Y] = UI_Top(Height);
|
|
n->cmd.box.scale = UI_Top(Scale);
|
|
n->cmd.box.child_alignment = UI_Top(ChildAlignment);
|
|
n->cmd.box.child_layout_axis = UI_Top(ChildLayoutAxis);
|
|
n->cmd.box.background_color = UI_Top(BackgroundColor);
|
|
n->cmd.box.border_color = UI_Top(BorderColor);
|
|
n->cmd.box.debug_color = UI_Top(DebugColor);
|
|
n->cmd.box.invisible_debug_color = UI_Top(InvisibleDebugColor);
|
|
n->cmd.box.tint = UI_Top(Tint);
|
|
n->cmd.box.border_size = UI_Top(BorderSize);
|
|
n->cmd.box.font = UI_Top(Font);
|
|
n->cmd.box.font_size = UI_Top(FontSize);
|
|
n->cmd.box.rounding = UI_Top(Rounding);
|
|
n->cmd.box.text_color = UI_Top(TextColor);
|
|
n->cmd.box.text = UI_Top(Text);
|
|
n->cmd.box.icon = UI_Top(Icon);
|
|
n->cmd.box.anchor = UI_Top(Anchor);
|
|
n->cmd.box.floating_pos = UI_Top(FloatingPos);
|
|
n->cmd.box.sprite_sheet = UI_Top(SpriteSheet);
|
|
n->cmd.box.sprite_span = UI_Top(SpriteSpan);
|
|
n->cmd.box.sprite_seq = UI_Top(SpriteSeq);
|
|
n->cmd.box.misc = UI_Top(Misc);
|
|
}
|
|
++frame->cmds_count;
|
|
SllQueuePush(frame->first_cmd_node, frame->last_cmd_node, n);
|
|
return key;
|
|
}
|
|
|
|
void UI_SetRawTexture(UI_Key key, G_TextureRef tex, Rng2 uv)
|
|
{
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
UI_CmdNode *n = PushStruct(frame->arena, UI_CmdNode);
|
|
n->cmd.kind = UI_CmdKind_SetRawTexture;
|
|
{
|
|
n->cmd.set_raw_texture.key = key;
|
|
n->cmd.set_raw_texture.tex = tex;
|
|
n->cmd.set_raw_texture.slice_uv = uv;
|
|
}
|
|
++frame->cmds_count;
|
|
SllQueuePush(frame->first_cmd_node, frame->last_cmd_node, n);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Report
|
|
|
|
UI_BoxReports UI_ReportsFromKey(UI_Key key)
|
|
{
|
|
UI_BoxReports result = Zi;
|
|
UI_Box *box = UI_BoxFromKey(key);
|
|
if (box)
|
|
{
|
|
result = box->reports;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Begin frame
|
|
|
|
UI_Frame *UI_BeginFrame(UI_FrameFlag frame_flags)
|
|
{
|
|
|
|
//////////////////////////////
|
|
//- Init persistent state
|
|
|
|
if (!UI.box_arena)
|
|
{
|
|
UI.box_arena = AcquireArena(Gibi(64));
|
|
UI.gpu_frame_arena = G_AcquireArena();
|
|
// Init frames
|
|
for (u64 i = 0; i < countof(UI.frames); ++i)
|
|
{
|
|
UI_Frame *frame = &UI.frames[i];
|
|
frame->arena = AcquireArena(Gibi(64));
|
|
frame->rects_arena = AcquireArena(Gibi(64));
|
|
}
|
|
// Init root box
|
|
{
|
|
UI_Box *box = PushStruct(UI.box_arena, UI_Box);
|
|
box->key = UI_RootKey;
|
|
box->gen = 1;
|
|
UI.boxes_count += 1;
|
|
UI_BoxBin *bin = &UI.box_bins[box->key.v % countof(UI.box_bins)];
|
|
bin->first = box;
|
|
bin->last = box;
|
|
UI.root_box = box;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Begin frame
|
|
|
|
UI.cur_frame_tick += 1;
|
|
UI_Frame *prev_frame = UI_PrevFrame();
|
|
UI_Frame *frame = UI_CurrentFrame();
|
|
|
|
{
|
|
Arena *old_arena = frame->arena;
|
|
Arena *old_rects_arena = frame->rects_arena;
|
|
ZeroStruct(frame);
|
|
frame->arena = old_arena;
|
|
frame->rects_arena = old_rects_arena;
|
|
}
|
|
frame->window_frame = WND_BeginFrame(G_Format_R16G16B16A16_Float, WND_BackbufferSizeMode_MatchMonitor);
|
|
UI.cl = G_PrepareCommandList(G_QueueKind_Direct);
|
|
ResetArena(frame->arena);
|
|
ResetArena(frame->rects_arena);
|
|
G_ResetArena(UI.cl, UI.gpu_frame_arena);
|
|
|
|
{
|
|
i64 now_ns = TimeNs();
|
|
i64 dt_ns = now_ns - prev_frame->time_ns;
|
|
frame->time_ns = now_ns;
|
|
frame->dt_ns = dt_ns;
|
|
frame->dt = SecondsFromNs(frame->dt_ns);
|
|
frame->tick = UI.cur_frame_tick;
|
|
}
|
|
|
|
// Init style stack
|
|
{
|
|
frame->top_stack = PushStruct(frame->arena, UI_Stack);
|
|
UI_PushDefaults();
|
|
}
|
|
|
|
frame->frame_flags = frame_flags;
|
|
|
|
//////////////////////////////
|
|
//- Process controller events
|
|
|
|
frame->cursor_pos = prev_frame->cursor_pos;
|
|
frame->drag_cursor_pos = prev_frame->drag_cursor_pos;
|
|
|
|
if (prev_frame->boxes_pre != 0)
|
|
{
|
|
ControllerEventsArray controller_events = frame->window_frame.controller_events;
|
|
|
|
//- Locate boxes
|
|
UI_Box *top_hovered_box = 0;
|
|
UI_Box *active_box = UI_BoxFromKey(prev_frame->active_box);
|
|
|
|
//- Update cursor pos
|
|
for (u64 cev_index = 0; cev_index < controller_events.count; ++cev_index)
|
|
{
|
|
ControllerEvent cev = controller_events.events[cev_index];
|
|
if (cev.kind == ControllerEventKind_CursorMove)
|
|
{
|
|
frame->cursor_pos = Vec2FromVec(cev.cursor_pos);
|
|
}
|
|
}
|
|
|
|
//- Init box reports
|
|
for (u64 pre_index = UI.boxes_count; pre_index-- > 0;)
|
|
{
|
|
UI_Box *box = prev_frame->boxes_pre[pre_index];
|
|
UI_BoxReport *report = &box->reports.draw;
|
|
b32 is_cursor_in_box = 0;
|
|
{
|
|
// TODO: More efficient test. This logic is just copied from the renderer's SDF function for now.
|
|
Vec2 p0 = box->screen_rect.p0;
|
|
Vec2 p1 = box->screen_rect.p1;
|
|
Vec2 point = frame->cursor_pos;
|
|
b32 is_corner = 0;
|
|
f32 non_corner_edge_dist = MinF32(MinF32(point.x - p0.x, p1.x - point.x), MinF32(point.y - p0.y, p1.y - point.y));
|
|
f32 corner_edge_dist = non_corner_edge_dist;
|
|
if (non_corner_edge_dist >= 0)
|
|
{
|
|
f32 tl_radius = box->rounding_tl;
|
|
f32 tr_radius = box->rounding_tr;
|
|
f32 br_radius = box->rounding_br;
|
|
f32 bl_radius = box->rounding_bl;
|
|
Vec2 tl = VEC2(p0.x + tl_radius, p0.y + tl_radius);
|
|
Vec2 tr = VEC2(p1.x - tr_radius, p0.y + tr_radius);
|
|
Vec2 br = VEC2(p1.x - br_radius, p1.y - br_radius);
|
|
Vec2 bl = VEC2(p0.x + bl_radius, p1.y - bl_radius);
|
|
if (point.x < tl.x && point.y < tl.y) corner_edge_dist = MinF32(corner_edge_dist, tl_radius - Vec2Len(SubVec2(tl, point)));
|
|
if (point.x > tr.x && point.y < tr.y) corner_edge_dist = MinF32(corner_edge_dist, tr_radius - Vec2Len(SubVec2(tr, point)));
|
|
if (point.x > br.x && point.y > br.y) corner_edge_dist = MinF32(corner_edge_dist, br_radius - Vec2Len(SubVec2(br, point)));
|
|
if (point.x < bl.x && point.y > bl.y) corner_edge_dist = MinF32(corner_edge_dist, bl_radius - Vec2Len(SubVec2(bl, point)));
|
|
}
|
|
is_cursor_in_box = non_corner_edge_dist >= 0 && corner_edge_dist >= 0;
|
|
}
|
|
report->is_hovered = is_cursor_in_box;
|
|
if (top_hovered_box == 0 && box->desc.flags & UI_BoxFlag_CaptureMouse && is_cursor_in_box)
|
|
{
|
|
top_hovered_box = box;
|
|
}
|
|
report->m1.ups = 0;
|
|
report->m1.downs = 0;
|
|
report->m1.presses = 0;
|
|
report->m2.ups = 0;
|
|
report->m2.downs = 0;
|
|
report->m2.presses = 0;
|
|
report->m3.ups = 0;
|
|
report->m3.downs = 0;
|
|
report->m3.presses = 0;
|
|
}
|
|
|
|
//- Update state from controller events
|
|
i32 mouse_downs = 0;
|
|
for (u64 cev_index = 0; cev_index < controller_events.count; ++cev_index)
|
|
{
|
|
ControllerEvent cev = controller_events.events[cev_index];
|
|
switch (cev.kind)
|
|
{
|
|
default: break;
|
|
|
|
case ControllerEventKind_ButtonDown:
|
|
{
|
|
if (cev.button == Button_M1 || cev.button == Button_M2 || cev.button == Button_M3)
|
|
{
|
|
mouse_downs += 1;
|
|
if (top_hovered_box && active_box == 0)
|
|
{
|
|
if (cev.button == Button_M1)
|
|
{
|
|
++top_hovered_box->reports.draw.m1.downs;
|
|
top_hovered_box->reports.draw.m1.held = 1;
|
|
}
|
|
else if (cev.button == Button_M2)
|
|
{
|
|
++top_hovered_box->reports.draw.m2.downs;
|
|
top_hovered_box->reports.draw.m2.held = 1;
|
|
}
|
|
else if (cev.button == Button_M3)
|
|
{
|
|
++top_hovered_box->reports.draw.m3.downs;
|
|
top_hovered_box->reports.draw.m3.held = 1;
|
|
}
|
|
active_box = top_hovered_box;
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case ControllerEventKind_ButtonUp:
|
|
{
|
|
if (cev.button == Button_M1 || cev.button == Button_M2 || cev.button == Button_M3)
|
|
{
|
|
if (active_box)
|
|
{
|
|
if (cev.button == Button_M1)
|
|
{
|
|
if (active_box == top_hovered_box)
|
|
{
|
|
++active_box->reports.draw.m1.presses;
|
|
}
|
|
++active_box->reports.draw.m1.ups;
|
|
if (active_box->reports.draw.m1.held)
|
|
{
|
|
active_box->reports.draw.m1.held = 0;
|
|
active_box = 0;
|
|
}
|
|
}
|
|
else if (cev.button == Button_M2)
|
|
{
|
|
if (active_box == top_hovered_box)
|
|
{
|
|
++active_box->reports.draw.m2.presses;
|
|
}
|
|
++active_box->reports.draw.m2.ups;
|
|
if (active_box->reports.draw.m2.held)
|
|
{
|
|
active_box->reports.draw.m2.held = 0;
|
|
active_box = 0;
|
|
}
|
|
}
|
|
else if (cev.button == Button_M3)
|
|
{
|
|
if (active_box == top_hovered_box)
|
|
{
|
|
++active_box->reports.draw.m3.presses;
|
|
}
|
|
++active_box->reports.draw.m3.ups;
|
|
if (active_box->reports.draw.m3.held)
|
|
{
|
|
active_box->reports.draw.m3.held = 0;
|
|
active_box = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case ControllerEventKind_Quit:
|
|
{
|
|
SignalExit(0);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
UI_Box *hot_box = active_box;
|
|
if (top_hovered_box && !active_box)
|
|
{
|
|
hot_box = top_hovered_box;
|
|
}
|
|
|
|
//- Update box reports
|
|
{
|
|
f32 lower_target = TweakFloat("UI lower blend target", -0.05, -1, 0);
|
|
f32 upper_target = TweakFloat("UI upper blend target", 1.05, 1, 10);
|
|
for (u64 pre_index = 0; pre_index < UI.boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = prev_frame->boxes_pre[pre_index];
|
|
UI_BoxReport *report = &box->reports.draw;
|
|
report->is_hot = box == hot_box;
|
|
|
|
f32 target_exists = (box->last_build_tick >= (frame->tick - 1)) ? upper_target : lower_target;
|
|
f32 target_hovered = report->is_hovered ? Inf : lower_target;
|
|
f32 target_hot = report->is_hot ? Inf : lower_target;
|
|
f32 target_active = box == active_box ? Inf : lower_target;
|
|
f64 target_misc = box->desc.misc;
|
|
|
|
// TODO: Configurable per-box blend rates
|
|
f32 exists_blend_rate = (30 * frame->dt);
|
|
f32 hot_blend_rate = (15 * frame->dt);
|
|
f32 active_blend_rate = (15 * frame->dt);
|
|
f32 hovered_blend_rate = (15 * frame->dt);
|
|
// f64 misc_blend_rate = (30 * frame->dt);
|
|
f64 misc_blend_rate = 1;
|
|
|
|
report->exists = SaturateF32(LerpF32(report->exists, target_exists, exists_blend_rate));
|
|
report->hot = SaturateF32(LerpF32(report->hot, target_hot, hot_blend_rate));
|
|
report->active = SaturateF32(LerpF32(report->active, target_active, active_blend_rate));
|
|
report->hovered = SaturateF32(LerpF32(report->hovered, target_hovered, hovered_blend_rate));
|
|
report->misc = SaturateF32(LerpF32(report->misc, target_misc, misc_blend_rate));
|
|
|
|
report->screen_rect = box->screen_rect;
|
|
report->screen_anchor = box->screen_anchor;
|
|
if (mouse_downs > 0)
|
|
{
|
|
box->reports.drag = *report;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mouse_downs > 0)
|
|
{
|
|
frame->drag_cursor_pos = frame->cursor_pos;
|
|
}
|
|
|
|
frame->top_hovered_box = top_hovered_box ? top_hovered_box->key : UI_NilKey;
|
|
frame->hot_box = hot_box ? hot_box->key : UI_NilKey;
|
|
frame->active_box = active_box ? active_box->key : UI_NilKey;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Build root box
|
|
|
|
{
|
|
UI_SetNext(Width, UI_PIX(frame->window_frame.draw_size.x, 1));
|
|
UI_SetNext(Height, UI_PIX(frame->window_frame.draw_size.y, 1));
|
|
UI_SetNext(Parent, UI_NilKey);
|
|
UI_BuildBoxEx(UI_RootKey);
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Frame helpers
|
|
|
|
UI_Frame *UI_CurrentFrame(void)
|
|
{
|
|
return &UI.frames[UI.cur_frame_tick % countof(UI.frames)];
|
|
};
|
|
|
|
UI_Frame *UI_PrevFrame(void)
|
|
{
|
|
return &UI.frames[(UI.cur_frame_tick - 1) % countof(UI.frames)];
|
|
};
|
|
|
|
Arena *UI_FrameArena(void)
|
|
{
|
|
return UI_CurrentFrame()->arena;
|
|
}
|
|
|
|
Vec2 UI_CursorPos(void)
|
|
{
|
|
return UI_CurrentFrame()->cursor_pos;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Layout helpers
|
|
|
|
GC_Run UI_ScaleRun(Arena *arena, GC_Run unscaled_run, Vec2 scale)
|
|
{
|
|
GC_Run result = Zi;
|
|
result = unscaled_run;
|
|
|
|
result.font_size *= scale.y;
|
|
result.font_ascent *= scale.y;
|
|
result.font_descent *= scale.y;
|
|
result.font_cap *= scale.y;
|
|
result.baseline_length *= scale.x;
|
|
|
|
result.rects_count = unscaled_run.rects_count;
|
|
result.rects = PushStructsNoZero(arena, GC_RunRect, result.rects_count);
|
|
for (u64 rect_idx = 0; rect_idx < result.rects_count; ++rect_idx)
|
|
{
|
|
GC_RunRect *src = &unscaled_run.rects[rect_idx];
|
|
GC_RunRect *dst = &result.rects[rect_idx];
|
|
*dst = *src;
|
|
dst->bounds = MulRng2Vec2(dst->bounds, scale);
|
|
dst->advance *= scale.x;
|
|
dst->baseline_pos *= scale.x;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ End frame
|
|
|
|
void UI_EndFrame(UI_Frame *frame, i32 vsync)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
UI_BoxIter box_iter = Zi;
|
|
|
|
Vec2I32 monitor_size = frame->window_frame.monitor_size;
|
|
Rng3 monitor_viewport = RNG3(VEC3(0, 0, 0), VEC3(monitor_size.x, monitor_size.y, 1));
|
|
Rng2 monitor_scissor = RNG2(VEC2(0, 0), VEC2(monitor_size.x, monitor_size.y));
|
|
|
|
Vec2I32 draw_size = frame->window_frame.draw_size;
|
|
Rng3 draw_viewport = RNG3(VEC3(0, 0, 0), VEC3(draw_size.x, draw_size.y, 1));
|
|
Rng2 draw_scissor = RNG2(VEC2(0, 0), VEC2(draw_size.x, draw_size.y));
|
|
|
|
//////////////////////////////
|
|
//- Create boxes from build cmds
|
|
|
|
for (UI_CmdNode *cmd_node = frame->first_cmd_node; cmd_node; cmd_node = cmd_node->next)
|
|
{
|
|
UI_Cmd cmd = cmd_node->cmd;
|
|
if (cmd.kind == UI_CmdKind_BuildBox)
|
|
{
|
|
UI_Key key = cmd.box.key;
|
|
UI_Box *box = 0;
|
|
{
|
|
UI_BoxBin *bin = &UI.box_bins[key.v % countof(UI.box_bins)];
|
|
for (box = bin->first; box; box = box->next_in_bin)
|
|
{
|
|
if (UI_MatchKey(box->key, key))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
// Allocate new box
|
|
if (box == 0)
|
|
{
|
|
// Allocate new box
|
|
box = UI.first_free_box;
|
|
i64 old_gen = 0;
|
|
if (box)
|
|
{
|
|
old_gen = box->gen;
|
|
SllStackPop(UI.first_free_box);
|
|
ZeroStruct(box);
|
|
}
|
|
else
|
|
{
|
|
box = PushStruct(UI.box_arena, UI_Box);
|
|
}
|
|
box->key = key;
|
|
box->old_gen = old_gen;
|
|
box->gen = old_gen + 1;
|
|
DllQueuePushNP(bin->first, bin->last, box, next_in_bin, prev_in_bin);
|
|
++UI.boxes_count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Update boxes from cmds
|
|
|
|
for (UI_CmdNode *cmd_node = frame->first_cmd_node; cmd_node; cmd_node = cmd_node->next)
|
|
{
|
|
UI_Cmd cmd = cmd_node->cmd;
|
|
switch (cmd.kind)
|
|
{
|
|
case UI_CmdKind_BuildBox:
|
|
{
|
|
UI_Key key = cmd.box.key;
|
|
if (UI_MatchKey(key, UI_NilKey))
|
|
{
|
|
key = UI_RandKey();
|
|
}
|
|
UI_Box *box = UI_BoxFromKey(key);
|
|
|
|
UI_Box *parent = 0;
|
|
if (box != UI.root_box)
|
|
{
|
|
parent = UI_BoxFromKey(cmd.box.parent);
|
|
}
|
|
|
|
// Update parent
|
|
if (box->parent)
|
|
{
|
|
// Remove from old parent
|
|
DllQueueRemove(box->parent->first, box->parent->last, box);
|
|
--box->parent->count;
|
|
}
|
|
if (parent)
|
|
{
|
|
// Add to new parent
|
|
DllQueuePush(parent->first, parent->last, box);
|
|
++parent->count;
|
|
}
|
|
box->parent = parent;
|
|
|
|
// Update box from cmd
|
|
{
|
|
box->desc = cmd.box;
|
|
String32 codepoints = Zi;
|
|
if (box->desc.icon != UI_Icon_None)
|
|
{
|
|
codepoints.len = 1;
|
|
codepoints.text = (u32 *)&box->desc.icon;
|
|
}
|
|
else
|
|
{
|
|
codepoints = String32FromString(scratch.arena, box->desc.text);
|
|
}
|
|
|
|
if (AnyBit(box->desc.flags, UI_BoxFlag_DrawText) && codepoints.len > 0)
|
|
{
|
|
box->glyph_run = GC_RunFromString32(frame->arena, codepoints, box->desc.font, box->desc.font_size);
|
|
}
|
|
|
|
if (!SPR_IsSheetKeyNil(box->desc.sprite_sheet))
|
|
{
|
|
box->sprite = SPR_SpriteFromSheet(box->desc.sprite_sheet, box->desc.sprite_span, box->desc.sprite_seq);
|
|
}
|
|
}
|
|
box->last_build_tick = frame->tick;
|
|
} break;
|
|
|
|
case UI_CmdKind_SetRawTexture:
|
|
{
|
|
UI_Key key = cmd.set_raw_texture.key;
|
|
UI_Box *box = UI_BoxFromKey(key);
|
|
if (box)
|
|
{
|
|
box->raw_texture = cmd.set_raw_texture.tex;
|
|
box->raw_texture_slice_uv = cmd.set_raw_texture.slice_uv;
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Prune cached boxes
|
|
|
|
{
|
|
u64 prunes_count = 0;
|
|
UI_Box **prunes = PushStructsNoZero(scratch.arena, UI_Box *, UI.boxes_count);
|
|
for (UI_BoxIterResult ir = UI_FirstBox(scratch.arena, &box_iter, UI_RootKey); ir.box; ir = UI_NextBox(scratch.arena, &box_iter))
|
|
{
|
|
if (ir.pre)
|
|
{
|
|
UI_Box *box = ir.box;
|
|
if (box->last_build_tick < frame->tick)
|
|
{
|
|
// Cause children to prune
|
|
for (UI_Box *child = box->first; child; child = child->next)
|
|
{
|
|
child->last_build_tick = box->last_build_tick;
|
|
}
|
|
// Push box to prunes array
|
|
prunes[prunes_count++] = box;
|
|
}
|
|
}
|
|
}
|
|
for (u64 prune_idx = 0; prune_idx < prunes_count; ++prune_idx)
|
|
{
|
|
UI_Box *box = prunes[prune_idx];
|
|
UI_Box *parent = box->parent;
|
|
UI_BoxBin *bin = &UI.box_bins[box->key.v % countof(UI.box_bins)];
|
|
// Re-parent children
|
|
if (box->first)
|
|
{
|
|
for (UI_Box *child = box->first; child; child = child->next)
|
|
{
|
|
child->parent = parent;
|
|
}
|
|
if (parent->last)
|
|
{
|
|
parent->last->next = box->first;
|
|
box->first->prev = parent->last;
|
|
}
|
|
else
|
|
{
|
|
parent->first = box->first;
|
|
}
|
|
parent->last = box->last;
|
|
parent->count += box->count;
|
|
}
|
|
// Remove from parent
|
|
DllQueueRemove(parent->first, parent->last, box);
|
|
--parent->count;
|
|
// Remove from lookup table
|
|
DllQueueRemoveNP(bin->first, bin->last, box, next_in_bin, prev_in_bin);
|
|
// Add to free list
|
|
SllStackPush(UI.first_free_box, box);
|
|
--UI.boxes_count;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Layout
|
|
|
|
// Prepare layout data
|
|
u64 boxes_count = UI.boxes_count;
|
|
UI_Box **boxes_pre = PushStructsNoZero(frame->arena, UI_Box *, boxes_count);
|
|
UI_Box **boxes_post = PushStructsNoZero(frame->arena, UI_Box *, boxes_count);
|
|
frame->boxes_pre = boxes_pre;
|
|
frame->boxes_post = boxes_post;
|
|
{
|
|
u64 pre_index = 0;
|
|
u64 post_index = 0;
|
|
for (UI_BoxIterResult ir = UI_FirstBox(scratch.arena, &box_iter, UI_RootKey); ir.box; ir = UI_NextBox(scratch.arena, &box_iter))
|
|
{
|
|
UI_Box *box = ir.box;
|
|
if (ir.pre)
|
|
{
|
|
box->pre_index = pre_index;
|
|
boxes_pre[pre_index] = box;
|
|
pre_index += 1;
|
|
|
|
// Reset layout data
|
|
box->cursor = 0;
|
|
box->final_children_size_accum = VEC2(0, 0);
|
|
box->solved_dims = VEC2(0, 0);
|
|
|
|
// Solve scale
|
|
{
|
|
UI_Box *parent = box->parent;
|
|
box->solved_scale = box->desc.scale;
|
|
if (parent)
|
|
{
|
|
box->solved_scale = MulVec2Vec2(parent->solved_scale, box->solved_scale);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
box->post_index = post_index;
|
|
boxes_post[post_index] = box;
|
|
post_index += 1;
|
|
}
|
|
}
|
|
Assert(pre_index == boxes_count);
|
|
Assert(post_index == boxes_count);
|
|
}
|
|
|
|
// Solve independent sizes
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
for (Axis axis = 0; axis < Axis_COUNTXY; ++axis)
|
|
{
|
|
UI_Size size = box->desc.pref_semantic_dims[axis];
|
|
if (size.kind == UI_SizeKind_Pixel)
|
|
{
|
|
box->solved_dims.v[axis] = size.v;
|
|
}
|
|
else if (size.kind == UI_SizeKind_Shrink)
|
|
{
|
|
if (AnyBit(box->desc.flags, UI_BoxFlag_DrawText))
|
|
{
|
|
f32 text_size = 0;
|
|
if (axis == Axis_X)
|
|
{
|
|
text_size = box->glyph_run.baseline_length;
|
|
}
|
|
else
|
|
{
|
|
text_size = box->glyph_run.font_ascent + box->glyph_run.font_descent;
|
|
}
|
|
box->solved_dims.v[axis] = text_size + (size.v * 2);
|
|
}
|
|
else if (!SPR_IsSheetKeyNil(box->desc.sprite_sheet))
|
|
{
|
|
box->solved_dims.v[axis] = box->sprite.tex_rect.p1.v[axis] - box->sprite.tex_rect.p0.v[axis] + (size.v * 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solve upwards-dependent sizes along layout axis
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
if (box->parent)
|
|
{
|
|
Axis axis = box->parent->desc.child_layout_axis;
|
|
UI_Size size = box->desc.pref_semantic_dims[axis];
|
|
if (size.kind == UI_SizeKind_Grow)
|
|
{
|
|
f32 match_size = 0;
|
|
b32 found_match = 0;
|
|
for (UI_Box *ancestor = box->parent; ancestor != 0 && !found_match; ancestor = ancestor->parent)
|
|
{
|
|
UI_Size ancestor_size = ancestor->desc.pref_semantic_dims[axis];
|
|
if (
|
|
ancestor_size.kind == UI_SizeKind_Pixel || (
|
|
ancestor_size.kind == UI_SizeKind_Shrink && (
|
|
AnyBit(box->desc.flags, UI_BoxFlag_DrawText) ||
|
|
!SPR_IsSheetKeyNil(box->desc.sprite_sheet)
|
|
)
|
|
)
|
|
)
|
|
{
|
|
// Match independent ancestor
|
|
match_size = ancestor->solved_dims.v[axis];
|
|
found_match = 1;
|
|
}
|
|
}
|
|
box->solved_dims.v[axis] = match_size * size.v;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solve downwards-dependent sizes
|
|
for (u64 post_index = 0; post_index < boxes_count; ++post_index)
|
|
{
|
|
UI_Box *box = boxes_post[post_index];
|
|
for (Axis axis = 0; axis < Axis_COUNTXY; ++axis)
|
|
{
|
|
UI_Size size = box->desc.pref_semantic_dims[axis];
|
|
if (size.kind == UI_SizeKind_Shrink && !(AnyBit(box->desc.flags, UI_BoxFlag_DrawText) || !SPR_IsSheetKeyNil(box->desc.sprite_sheet)))
|
|
{
|
|
f32 accum = 0;
|
|
for (UI_Box *child = box->first; child; child = child->next)
|
|
{
|
|
if (!AnyBit(child->desc.flags, UI_BoxFlag_Floating))
|
|
{
|
|
f32 child_size = child->solved_dims.v[axis];
|
|
if (axis == box->desc.child_layout_axis)
|
|
{
|
|
accum += child_size;
|
|
}
|
|
else
|
|
{
|
|
accum = MaxF32(child_size, accum);
|
|
}
|
|
}
|
|
}
|
|
box->solved_dims.v[axis] = CeilF32(accum + (size.v * 2));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solve upwards-dependent sizes along non-layout axis
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
if (box->parent)
|
|
{
|
|
Axis axis = !box->parent->desc.child_layout_axis;
|
|
UI_Size size = box->desc.pref_semantic_dims[axis];
|
|
if (size.kind == UI_SizeKind_Grow)
|
|
{
|
|
box->solved_dims.v[axis] = box->parent->solved_dims.v[axis] * size.v;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solve violations
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
for (Axis axis = 0; axis < Axis_COUNTXY; ++axis)
|
|
{
|
|
f32 box_size = box->solved_dims.v[axis];
|
|
{
|
|
// Accumulate non-floating sizes
|
|
f32 unconstrained_size_accum = 0;
|
|
f32 flex_accum = 0;
|
|
f32 violation = 0;
|
|
{
|
|
for (UI_Box *child = box->first; child; child = child->next)
|
|
{
|
|
b32 is_floating = AnyBit(child->desc.flags, UI_BoxFlag_Floating);
|
|
if (!is_floating)
|
|
{
|
|
f32 size = child->solved_dims.v[axis];
|
|
f32 strictness = child->desc.pref_semantic_dims[axis].strictness;
|
|
f32 flex = size * (1.0 - strictness);
|
|
if (axis == box->desc.child_layout_axis)
|
|
{
|
|
unconstrained_size_accum += size;
|
|
flex_accum += flex;
|
|
}
|
|
else
|
|
{
|
|
unconstrained_size_accum = MaxF32(unconstrained_size_accum, size);
|
|
flex_accum = MaxF32(flex_accum, flex);
|
|
}
|
|
}
|
|
}
|
|
unconstrained_size_accum = FloorF32(unconstrained_size_accum);
|
|
violation = unconstrained_size_accum - box_size;
|
|
}
|
|
{
|
|
f32 size_accum = 0;
|
|
for (UI_Box *child = box->first; child; child = child->next)
|
|
{
|
|
b32 is_floating = AnyBit(child->desc.flags, UI_BoxFlag_Floating);
|
|
f32 unconstrained_size = child->solved_dims.v[axis];
|
|
f32 strictness = child->desc.pref_semantic_dims[axis].strictness;
|
|
f32 flex = unconstrained_size * (1.0 - strictness);
|
|
f32 new_size = unconstrained_size;
|
|
// Solve non-floating violation
|
|
if (!is_floating && violation > 0 && flex_accum > 0)
|
|
{
|
|
if (axis == box->desc.child_layout_axis)
|
|
{
|
|
f32 chopoff = MinF32(flex, violation * (flex / flex_accum));
|
|
new_size = new_size - chopoff;
|
|
}
|
|
else if (new_size > box_size)
|
|
{
|
|
new_size = MaxF32(new_size - flex, box_size);
|
|
}
|
|
}
|
|
// Solve floating violation
|
|
if (is_floating && new_size > box_size && !AnyBit(child->desc.flags, UI_BoxFlag_NoFloatingClamp))
|
|
{
|
|
new_size = MaxF32(new_size - flex, box_size);
|
|
}
|
|
if (!is_floating)
|
|
{
|
|
size_accum += new_size;
|
|
}
|
|
child->solved_dims.v[axis] = new_size;
|
|
}
|
|
box->final_children_size_accum.v[axis] = size_accum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Solve final positions
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
UI_Box *parent = box->parent;
|
|
|
|
// TODO: Distinguish between baseline alignment & visual alignment
|
|
UI_RegionPair child_alignment = UI_PairFromRegion(box->desc.child_alignment);
|
|
UI_RegionPair alignment_in_parent = UI_PairFromRegion(parent ? parent->desc.child_alignment : UI_Region_TopLeft);
|
|
|
|
Axis child_layout_axis = box->desc.child_layout_axis;
|
|
Axis layout_axis_in_parent = parent ? parent->desc.child_layout_axis : Axis_X;
|
|
|
|
b32 is_floating = AnyBit(box->desc.flags, UI_BoxFlag_Floating);
|
|
|
|
// Apply scale
|
|
for (Axis axis = 0; axis < Axis_COUNTXY; ++axis)
|
|
{
|
|
f32 unscaled_size = box->solved_dims.v[axis];
|
|
f32 scaled_size = 0;
|
|
if (box->solved_scale.v[axis] == 1)
|
|
{
|
|
// scaled_size = RoundF32(unscaled_size);
|
|
scaled_size = unscaled_size;
|
|
}
|
|
else
|
|
{
|
|
scaled_size = unscaled_size * box->solved_scale.v[axis];
|
|
}
|
|
box->solved_dims.v[axis] = scaled_size;
|
|
}
|
|
|
|
// Compute anchor offset
|
|
Vec2 anchor_offset = Zi;
|
|
{
|
|
UI_RegionPair anchor_region = UI_PairFromRegion(box->desc.anchor);
|
|
for (Axis axis = 0; axis < Axis_COUNTXY; ++axis)
|
|
{
|
|
UI_AxisRegion anchor = anchor_region.v[axis];
|
|
switch (anchor)
|
|
{
|
|
default: break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
anchor_offset.v[axis] = box->solved_dims.v[axis] * 0.5;
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
anchor_offset.v[axis] = box->solved_dims.v[axis];
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize layout cursor based on alignment
|
|
{
|
|
Axis axis = child_layout_axis;
|
|
UI_AxisRegion alignment = child_alignment.v[axis];
|
|
f32 box_size = box->solved_dims.v[axis];
|
|
f32 size_accum = box->final_children_size_accum.v[axis];
|
|
switch(alignment)
|
|
{
|
|
default: break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
box->cursor = box_size / 2 - size_accum / 2;
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
box->cursor = box_size - size_accum;
|
|
} break;
|
|
}
|
|
// box->cursor = FloorF32(box->cursor);
|
|
}
|
|
|
|
// Solve screen rect
|
|
{
|
|
// Compute offset
|
|
Vec2 offset = Zi;
|
|
{
|
|
// Floating box offset
|
|
if (is_floating)
|
|
{
|
|
offset = box->desc.floating_pos;
|
|
offset = SubVec2(offset, anchor_offset);
|
|
}
|
|
// Non-floating box offset
|
|
else if (parent)
|
|
{
|
|
// Compute offset in layout direction (based on parent cursor)
|
|
offset.v[layout_axis_in_parent] = parent->cursor;
|
|
// Compute offset in non-layout direction (based on alignment)
|
|
{
|
|
Axis axis = !layout_axis_in_parent;
|
|
UI_AxisRegion alignment = alignment_in_parent.v[axis];
|
|
switch(alignment)
|
|
{
|
|
default: break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
f32 parent_size = parent->solved_dims.v[axis];
|
|
f32 box_size = box->solved_dims.v[axis];
|
|
offset.v[axis] = parent_size / 2 - box_size / 2;
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
f32 parent_size = parent->solved_dims.v[axis];
|
|
f32 box_size = box->solved_dims.v[axis];
|
|
offset.v[axis] = parent_size - box_size;
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
if (box->solved_scale.x == 1)
|
|
{
|
|
offset.x = RoundF32(offset.x);
|
|
}
|
|
if (box->solved_scale.y == 1)
|
|
{
|
|
offset.y = RoundF32(offset.y);
|
|
}
|
|
}
|
|
|
|
// Compute rect
|
|
Vec2 screen_pos = parent ? AddVec2(parent->screen_rect.p0, offset) : VEC2(0, 0);
|
|
if (is_floating && !AnyBit(box->desc.flags, UI_BoxFlag_NoFloatingClamp))
|
|
{
|
|
Vec2 overshoot = Zi;
|
|
overshoot.x = MaxF32(0, (screen_pos.x + box->solved_dims.x) - parent->screen_rect.p1.x);
|
|
overshoot.y = MaxF32((screen_pos.y + box->solved_dims.y) - parent->screen_rect.p1.y, 0);
|
|
screen_pos.x = MaxF32(parent->screen_rect.p0.x, screen_pos.x - overshoot.x);
|
|
screen_pos.y = MaxF32(parent->screen_rect.p0.y, screen_pos.y - overshoot.y);
|
|
}
|
|
box->screen_rect.p0 = screen_pos;
|
|
box->screen_rect.p1 = AddVec2(box->screen_rect.p0, box->solved_dims);
|
|
box->screen_anchor = AddVec2(box->screen_rect.p0, anchor_offset);
|
|
|
|
// Update parent cursor
|
|
if (parent && !is_floating)
|
|
{
|
|
parent->cursor += box->solved_dims.v[layout_axis_in_parent];
|
|
}
|
|
}
|
|
|
|
// Solve screen rounding
|
|
{
|
|
UI_Round rounding = box->desc.rounding;
|
|
Vec2 half_dims = MulVec2(SubVec2(box->screen_rect.p1, box->screen_rect.p0), 0.5);
|
|
f32 min_half_dims = MinF32(half_dims.x, half_dims.y);
|
|
f32 final_rounding_tl = 0;
|
|
f32 final_rounding_tr = 0;
|
|
f32 final_rounding_br = 0;
|
|
f32 final_rounding_bl = 0;
|
|
|
|
switch(rounding.kind)
|
|
{
|
|
default: break;
|
|
case UI_RoundKind_Pixel:
|
|
{
|
|
final_rounding_tl = rounding.v;
|
|
final_rounding_tr = final_rounding_tl;
|
|
final_rounding_br = final_rounding_tl;
|
|
final_rounding_bl = final_rounding_tl;
|
|
} break;
|
|
case UI_RoundKind_Grow:
|
|
{
|
|
final_rounding_tl = rounding.v * min_half_dims;
|
|
final_rounding_tr = final_rounding_tl;
|
|
final_rounding_br = final_rounding_tl;
|
|
final_rounding_bl = final_rounding_tl;
|
|
} break;
|
|
}
|
|
|
|
// Clamp rounding based on parent rounding
|
|
if (parent && !AllBits(box->desc.flags, UI_BoxFlag_Floating | UI_BoxFlag_NoFloatingClamp))
|
|
{
|
|
Vec2 vtl = SubVec2(VEC2(parent->screen_rect.p0.x, parent->screen_rect.p0.y), VEC2(box->screen_rect.p0.x, box->screen_rect.p0.y));
|
|
Vec2 vtr = SubVec2(VEC2(parent->screen_rect.p1.x, parent->screen_rect.p0.y), VEC2(box->screen_rect.p1.x, box->screen_rect.p0.y));
|
|
Vec2 vbr = SubVec2(VEC2(parent->screen_rect.p1.x, parent->screen_rect.p1.y), VEC2(box->screen_rect.p1.x, box->screen_rect.p1.y));
|
|
Vec2 vbl = SubVec2(VEC2(parent->screen_rect.p0.x, parent->screen_rect.p1.y), VEC2(box->screen_rect.p0.x, box->screen_rect.p1.y));
|
|
final_rounding_tl = MaxF32(final_rounding_tl, parent->rounding_tl - Vec2Len(vtl));
|
|
final_rounding_tr = MaxF32(final_rounding_tr, parent->rounding_tr - Vec2Len(vtr));
|
|
final_rounding_br = MaxF32(final_rounding_br, parent->rounding_br - Vec2Len(vbr));
|
|
final_rounding_bl = MaxF32(final_rounding_bl, parent->rounding_bl - Vec2Len(vbl));
|
|
}
|
|
|
|
box->rounding_tl = final_rounding_tl;
|
|
box->rounding_tr = final_rounding_tr;
|
|
box->rounding_br = final_rounding_br;
|
|
box->rounding_bl = final_rounding_bl;
|
|
}
|
|
|
|
box->gen = box->old_gen;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Render
|
|
|
|
G_TextureRef draw_target = Zi;
|
|
{
|
|
//////////////////////////////
|
|
//- Build render data
|
|
|
|
// Build rect instance data
|
|
for (u64 pre_index = 0; pre_index < boxes_count; ++pre_index)
|
|
{
|
|
UI_Box *box = boxes_pre[pre_index];
|
|
GC_Run raw_run_unscaled = box->glyph_run;
|
|
GC_Run raw_run = UI_ScaleRun(frame->arena, raw_run_unscaled, box->solved_scale);
|
|
|
|
b32 is_visible = 1;
|
|
is_visible = is_visible && (box->desc.tint.w >= 0.0025);
|
|
is_visible = is_visible && (box->screen_rect.p1.x - box->screen_rect.p0.x > 0.0025);
|
|
is_visible = is_visible && (box->screen_rect.p1.y - box->screen_rect.p0.y > 0.0025);
|
|
is_visible = is_visible && (
|
|
!G_IsRefNil(box->raw_texture) ||
|
|
!SPR_IsSheetKeyNil(box->desc.sprite_sheet) ||
|
|
(AnyBit(box->desc.flags, UI_BoxFlag_DrawText) && raw_run.ready) ||
|
|
box->desc.tint.a * box->desc.background_color.a >= 0.0025 ||
|
|
box->desc.tint.a * box->desc.border_color.a >= 0.0025
|
|
);
|
|
|
|
if (is_visible || AnyBit(frame->frame_flags, UI_FrameFlag_Debug))
|
|
{
|
|
Vec4 debug_lin = is_visible ? LinearFromSrgb(box->desc.debug_color) : LinearFromSrgb(box->desc.invisible_debug_color);
|
|
Vec4 tint_lin = LinearFromSrgb(box->desc.tint);
|
|
Vec2 box_dims = DimsFromRng2(box->screen_rect);
|
|
UI_RegionPair child_alignment = UI_PairFromRegion(box->desc.child_alignment);
|
|
UI_AxisRegion x_alignment = child_alignment.v[Axis_X];
|
|
UI_AxisRegion y_alignment = child_alignment.v[Axis_Y];
|
|
|
|
// Box rect
|
|
{
|
|
UI_GpuRect *rect = PushStruct(frame->rects_arena, UI_GpuRect);
|
|
rect->bounds = box->screen_rect;
|
|
rect->background_lin = LinearFromSrgb(box->desc.background_color);
|
|
rect->border_lin = LinearFromSrgb(box->desc.border_color);
|
|
rect->debug_lin = debug_lin;
|
|
rect->tint_lin = tint_lin;
|
|
rect->border_size = box->desc.border_size;
|
|
rect->tl_rounding = box->rounding_tl;
|
|
rect->tr_rounding = box->rounding_tr;
|
|
rect->br_rounding = box->rounding_br;
|
|
rect->bl_rounding = box->rounding_bl;
|
|
if (!G_IsRefNil(box->raw_texture))
|
|
{
|
|
rect->tex = box->raw_texture;
|
|
rect->tex_slice_uv = box->raw_texture_slice_uv;
|
|
}
|
|
else if (!SPR_IsSheetKeyNil(box->desc.sprite_sheet))
|
|
{
|
|
rect->tex = box->sprite.tex;
|
|
rect->tex_slice_uv = DivRng2Vec2(box->sprite.tex_rect, box->sprite.tex_dims);
|
|
}
|
|
}
|
|
|
|
if (AnyBit(box->desc.flags, UI_BoxFlag_DrawText) && raw_run.ready)
|
|
{
|
|
f32 max_baseline_length = CeilF32(DimsFromRng2(box->screen_rect).x);
|
|
b32 should_truncate = FloorF32(raw_run.baseline_length) > max_baseline_length && !AnyBit(box->desc.flags, UI_BoxFlag_NoTextTruncation);
|
|
|
|
// Truncate run
|
|
u64 final_rects_count = 0;
|
|
GC_RunRect *final_rects = 0;
|
|
f32 final_baseline_length = 0;
|
|
if (should_truncate)
|
|
{
|
|
// Get elipses run
|
|
GC_Run elipses_run_unscaled = GC_RunFromString32(scratch.arena, String32FromString(scratch.arena, Lit("...")), box->desc.font, box->desc.font_size);
|
|
GC_Run elipses_run = UI_ScaleRun(frame->arena, elipses_run_unscaled, box->solved_scale);
|
|
f32 truncation_offset = max_baseline_length - elipses_run.baseline_length;
|
|
|
|
// Append non-overflowed rects
|
|
f32 elipses_offset = 0;
|
|
final_rects = PushStructsNoZero(scratch.arena, GC_RunRect, raw_run.rects_count);
|
|
for (u64 rect_idx = 0; rect_idx < raw_run.rects_count; ++rect_idx)
|
|
{
|
|
GC_RunRect rr = raw_run.rects[rect_idx];
|
|
if (rr.baseline_pos + rr.advance <= truncation_offset)
|
|
{
|
|
final_rects[final_rects_count] = rr;
|
|
final_rects_count += 1;
|
|
elipses_offset = MaxF32(elipses_offset, rr.baseline_pos + rr.advance);
|
|
final_baseline_length = MaxF32(final_baseline_length, rr.baseline_pos + rr.advance);
|
|
}
|
|
}
|
|
|
|
// Append elipses
|
|
for (u64 rect_idx = 0; rect_idx < elipses_run.rects_count; ++rect_idx)
|
|
{
|
|
GC_RunRect rr = elipses_run.rects[rect_idx];
|
|
rr.baseline_pos += elipses_offset;
|
|
if (rr.baseline_pos + rr.advance <= max_baseline_length)
|
|
{
|
|
final_rects[final_rects_count] = rr;
|
|
final_rects_count += 1;
|
|
final_baseline_length = MaxF32(final_baseline_length, rr.baseline_pos + rr.advance);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
final_rects = raw_run.rects;
|
|
final_rects_count = raw_run.rects_count;
|
|
final_baseline_length = raw_run.baseline_length;
|
|
}
|
|
|
|
// Compute baseline
|
|
f32 ascent = raw_run.font_ascent;
|
|
f32 font_descent = raw_run.font_descent;
|
|
f32 cap = raw_run.font_cap;
|
|
f32 baseline_height = ascent + font_descent;
|
|
Vec2 baseline = Zi;
|
|
switch (x_alignment)
|
|
{
|
|
case UI_AxisRegion_Start:
|
|
{
|
|
baseline.x = box->screen_rect.p0.x;
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
baseline.x = box->screen_rect.p1.x;
|
|
baseline.x -= final_baseline_length;
|
|
} break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
baseline.x = box->screen_rect.p0.x;
|
|
baseline.x += (box_dims.x - final_baseline_length) / 2;
|
|
} break;
|
|
}
|
|
switch (y_alignment)
|
|
{
|
|
case UI_AxisRegion_Start:
|
|
{
|
|
baseline.y = box->screen_rect.p0.y;
|
|
baseline.y += ascent;
|
|
} break;
|
|
case UI_AxisRegion_End:
|
|
{
|
|
baseline.y = box->screen_rect.p1.y;
|
|
baseline.y -= font_descent;
|
|
} break;
|
|
case UI_AxisRegion_Center:
|
|
{
|
|
baseline.y = box->screen_rect.p0.y;
|
|
baseline.y += box_dims.y / 2;
|
|
baseline.y += cap / 2;
|
|
} break;
|
|
}
|
|
baseline = CeilVec2(baseline);
|
|
|
|
// Push text rects
|
|
Vec4 text_color_lin = LinearFromSrgb(box->desc.text_color);
|
|
text_color_lin = MulVec4Vec4(text_color_lin, tint_lin);
|
|
for (u64 rect_idx = 0; rect_idx < final_rects_count; ++rect_idx)
|
|
{
|
|
GC_RunRect rr = final_rects[rect_idx];
|
|
Vec2 glyph_dims = DimsFromRng2(rr.bounds);
|
|
if (glyph_dims.x != 0 || glyph_dims.y != 0)
|
|
{
|
|
UI_GpuRect *rect = PushStruct(frame->rects_arena, UI_GpuRect);
|
|
rect->debug_lin = debug_lin;
|
|
rect->tint_lin = text_color_lin;
|
|
rect->tex = rr.tex;
|
|
rect->tex_slice_uv = rr.tex_slice_uv;
|
|
rect->bounds = rr.bounds;
|
|
rect->bounds = AddRng2Vec2(rect->bounds, baseline);
|
|
rect->bounds = AddRng2Vec2(rect->bounds, VEC2(rr.baseline_pos, 0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Upload data to GPU
|
|
|
|
UI_GpuParams params = Zi;
|
|
|
|
// Target
|
|
draw_target = G_PushTexture2D(
|
|
UI.cl, UI.gpu_frame_arena,
|
|
G_TextureLayout_Family,
|
|
G_Format_R16G16B16A16_Float,
|
|
monitor_size,
|
|
.flags = G_MemoryFlag_AllowTextureDraw,
|
|
.name = Lit("UI draw target")
|
|
);
|
|
|
|
// Rects
|
|
UI_GpuRect *rects = ArenaFirst(frame->rects_arena, UI_GpuRect);
|
|
u64 rects_count = ArenaCount(frame->rects_arena, UI_GpuRect);
|
|
params.rects = G_PushStructsFromCpu(
|
|
UI.cl, UI.gpu_frame_arena,
|
|
rects, rects_count,
|
|
.name = Lit("UI rects")
|
|
);
|
|
|
|
// Params
|
|
params.target = draw_target;
|
|
params.target_size = draw_size;
|
|
params.sampler = G_BasicSamplerFromKind(G_BasicSamplerKind_PointClamp);
|
|
params.cursor_pos = frame->cursor_pos;
|
|
params.anti_aliasing = TweakFloat("UI anti-aliasing", 1, 0, 1);
|
|
G_BufferRef params_buff = G_PushStructFromCpu(
|
|
UI.cl, UI.gpu_frame_arena,
|
|
¶ms,
|
|
.name = Lit("UI gpu params")
|
|
);
|
|
|
|
// Init constants
|
|
G_SetConstant(UI.cl, UI_GpuConst_Params, params_buff);
|
|
|
|
// Sync
|
|
G_Sync(UI.cl);
|
|
|
|
//////////////////////////////
|
|
//- Dispatch shaders
|
|
|
|
//- Clear pass
|
|
|
|
G_ZoneDF(UI.cl, "UI clear")
|
|
{
|
|
G_ClearRenderTarget(UI.cl, draw_target, VEC4(0, 0, 0, 0), 0);
|
|
}
|
|
|
|
//- Rect pass
|
|
|
|
// Render rects
|
|
G_ZoneDF(UI.cl, "UI rects")
|
|
{
|
|
G_Draw(
|
|
UI.cl,
|
|
UI_DRectVS, UI_DRectPS,
|
|
rects_count, G_QuadIndices(),
|
|
1, &G_RT(draw_target, G_BlendMode_CompositePremultipliedAlpha),
|
|
draw_viewport, draw_scissor,
|
|
G_DrawMode_TriangleList
|
|
);
|
|
}
|
|
|
|
// Render rect wireframes
|
|
if (AnyBit(frame->frame_flags, UI_FrameFlag_Debug))
|
|
{
|
|
G_ZoneDF(UI.cl, "UI debug rects")
|
|
{
|
|
G_SetConstant(UI.cl, UI_GpuConst_DebugDraw, 1);
|
|
G_Draw(
|
|
UI.cl,
|
|
UI_DRectVS, UI_DRectPS,
|
|
rects_count, G_QuadIndices(),
|
|
1, &G_RT(draw_target, G_BlendMode_CompositePremultipliedAlpha),
|
|
draw_viewport, draw_scissor,
|
|
G_DrawMode_WireTriangleList
|
|
);
|
|
G_SetConstant(UI.cl, UI_GpuConst_DebugDraw, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- End frame
|
|
|
|
G_CommitCommandList(UI.cl);
|
|
WND_EndFrame(frame->window_frame, VEC2I32(0, 0), draw_target, RNG2I32(VEC2I32(0, 0), draw_size), vsync);
|
|
|
|
EndScratch(scratch);
|
|
}
|