power_play/src/ui/ui_core.c

1809 lines
56 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_Texture2DRef 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));
// 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));
frame->gpu_arena = G_AcquireArena();
}
// 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.current_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;
G_ArenaHandle old_gpu_arena = frame->gpu_arena;
ZeroStruct(frame);
frame->arena = old_arena;
frame->rects_arena = old_rects_arena;
frame->gpu_arena = old_gpu_arena;
}
frame->window_frame = WND_BeginFrame(G_Format_R16G16B16A16_Float, WND_BackbufferSizeMode_MatchMonitor);
frame->cl = G_PrepareCommandList(G_QueueKind_Direct);
ResetArena(frame->arena);
ResetArena(frame->rects_arena);
G_ResetArena(frame->cl, frame->gpu_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.current_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.current_frame_tick % countof(UI.frames)];
};
UI_Frame *UI_PrevFrame(void)
{
return &UI.frames[(UI.current_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_ResourceHandle backbuffer = frame->window_frame.backbuffer;
{
//////////////////////////////
//- 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];
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);
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);
}
}
// Text rects
GC_Run raw_run_unscaled = box->glyph_run;
GC_Run raw_run = UI_ScaleRun(frame->arena, raw_run_unscaled, box->solved_scale);
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));
}
}
}
}
}
//////////////////////////////
//- Push data to GPU
// Target
G_ResourceHandle draw_target = G_PushTexture2D(
frame->gpu_arena, frame->cl,
G_Format_R16G16B16A16_Float,
monitor_size,
G_Layout_DirectQueue_RenderTargetWrite,
.flags = G_ResourceFlag_AllowRenderTarget,
.name = Lit("UI draw target")
);
G_Texture2DRef draw_target_ro = G_PushTexture2DRef(frame->gpu_arena, draw_target);
// Rects
u64 rects_count = ArenaCount(frame->rects_arena, UI_GpuRect);
G_ResourceHandle rects_buff = G_PushBufferFromCpuCopy(
frame->gpu_arena, frame->cl,
StringFromArena(frame->rects_arena),
.name = Lit("UI rects")
);
G_StructuredBufferRef rects_ro = G_PushStructuredBufferRef(frame->gpu_arena, rects_buff, UI_GpuRect);
// Params
UI_GpuParams params = Zi;
{
params.target_size = draw_size;
params.target_ro = draw_target_ro;
params.rects = rects_ro;
params.sampler = G_BasicSamplerFromKind(G_BasicSamplerKind_PointClamp);
params.cursor_pos = frame->cursor_pos;
params.aa = TweakFloat("UI anti-aliasing", 1, 0, 1);
}
G_ResourceHandle params_buff = G_PushBufferFromCpuCopy(
frame->gpu_arena, frame->cl,
StringFromStruct(&params),
.name = Lit("UI gpu params")
);
G_StructuredBufferRef params_ro = G_PushStructuredBufferRef(frame->gpu_arena, params_buff, UI_GpuParams);
// Initial constants
G_SetConstant(frame->cl, UI_GpuConst_Params, params_ro);
// Sync
G_DumbGlobalMemorySync(frame->cl);
//////////////////////////////
//- Dispatch shaders
//- Clear pass
{
G_ClearRenderTarget(frame->cl, draw_target, VEC4(0, 0, 0, 0), 0);
}
//- Rect pass
if (rects_count > 0)
{
// Render rects
G_Rasterize(
frame->cl,
UI_DRectVS, UI_DRectPS,
rects_count, G_QuadIndices(),
1, &G_Rt(draw_target, G_BlendMode_CompositePremultipliedAlpha),
draw_viewport, draw_scissor,
G_RasterMode_TriangleList
);
// Render rect wireframes
if (AnyBit(frame->frame_flags, UI_FrameFlag_Debug))
{
G_SetConstant(frame->cl, UI_GpuConst_DebugDraw, 1);
G_Rasterize(
frame->cl,
UI_DRectVS, UI_DRectPS,
rects_count, G_QuadIndices(),
1, &G_Rt(draw_target, G_BlendMode_CompositePremultipliedAlpha),
draw_viewport, draw_scissor,
G_RasterMode_WireTriangleList
);
}
}
//- Backbuffer blit pass
G_DumbMemoryLayoutSync(frame->cl, draw_target, G_Layout_DirectQueue_ShaderRead);
G_DumbMemoryLayoutSync(frame->cl, backbuffer, G_Layout_DirectQueue_RenderTargetWrite);
{
G_Rasterize(
frame->cl,
UI_BlitVS, UI_BlitPS,
1, G_QuadIndices(),
1, &G_Rt(backbuffer, G_BlendMode_Opaque),
monitor_viewport, monitor_scissor,
G_RasterMode_TriangleList
);
}
G_DumbMemoryLayoutSync(frame->cl, backbuffer, G_Layout_AnyQueue_ShaderRead_CopyRead_CopyWrite_Present);
}
//////////////////////////////
//- End frame
G_CommitCommandList(frame->cl);
WND_EndFrame(frame->window_frame, vsync);
EndScratch(scratch);
}