power_play/src/ui/ui_core.c

1463 lines
54 KiB
C

UI_State UI_state = ZI;
////////////////////////////////////////////////////////////
//~ Bootstrap
void UI_Bootstrap(void)
{
}
////////////////////////////////////////////////////////////
//~ Font helpers
GC_FontKey UI_GetDefaultFont(void)
{
return GC_FontKeyFromResource(ResourceKeyFromStore(&UI_Resources, Lit("font/proggy.ttf")));
}
////////////////////////////////////////////////////////////
//~ Key helpers
UI_Key UI_KeyFromString(String str)
{
u64 top_tag = UI_UseTop(Tag);
UI_Key key = ZI;
key.hash = HashFnv64(top_tag, str);
return key;
}
UI_Key UI_KeyF_(String fmt, ...)
{
TempArena scratch = BeginScratchNoConflict();
UI_Key key = ZI;
va_list args;
va_start(args, fmt);
{
String name = FormatStringV(scratch.arena, fmt, args);
key = UI_KeyFromString(name);
}
va_end(args);
EndScratch(scratch);
return key;
}
UI_Key UI_TransKey(void)
{
UI_Frame *frame = UI_CurrentFrame();
u64 seed = ++frame->transient_key_seed;
UI_Key key = ZI;
key.hash = RandU64FromSeed(seed);
return key;
}
UI_Box *UI_BoxFromKey(UI_Key key)
{
UI_State *g = &UI_state;
UI_Box *box = 0;
if (key.hash != 0)
{
UI_BoxBin *bin = &g->box_bins[key.hash % UI_NumBoxLookupBins];
for (UI_Box *tmp = bin->first; tmp; tmp = tmp->next_in_bin)
{
if (tmp->key.hash == key.hash)
{
box = tmp;
break;
}
}
}
return box;
}
////////////////////////////////////////////////////////////
//~ String helpers
String UI_StringF_(String fmt, ...)
{
UI_Frame *frame = UI_CurrentFrame();
va_list args;
va_start(args, fmt);
String str = FormatStringV(frame->arena, fmt, args);
va_end(args);
return str;
}
////////////////////////////////////////////////////////////
//~ 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.hash != 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); }
case UI_StyleKind_Font: { desc.style.Font = UI_GetDefaultFont(); } break;
u8 prefetch[127] = ZI;
for (u64 i = 0; i < countof(prefetch); ++i)
{
prefetch[i] = i;
}
case UI_StyleKind_FontSize: { desc.style.FontSize = 16.0f; } break;
case UI_StyleKind_Tint: { desc.style.Tint = Color_White; } break;
case UI_StyleKind_Tag: { desc.style.Tag = HashFnv64(Fnv64Basis, Lit("root")); } break;
case UI_StyleKind_DebugColor: { desc.style.DebugColor = Rgba(1, 0, 1, 0.5); } break;
case UI_StyleKind_BackgroundTextureUv1: { desc.style.BackgroundTextureUv1 = 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;
case UI_StyleKind_ChildAlignment:
{
UI_Alignment alignment = desc.style.ChildAlignment;
/* Alignment -> horizontal alignment */
switch(alignment)
{
default: break;
case UI_Alignment_TopLeft:
case UI_Alignment_Left:
case UI_Alignment_BottomLeft:
{
UI_PushCopy(ChildAlignmentX, desc, UI_AxisAlignment_Start);
} break;
case UI_Alignment_Top:
case UI_Alignment_Center:
case UI_Alignment_Bottom:
{
UI_PushCopy(ChildAlignmentX, desc, UI_AxisAlignment_Center);
} break;
case UI_Alignment_TopRight:
case UI_Alignment_Right:
case UI_Alignment_BottomRight:
{
UI_PushCopy(ChildAlignmentX, desc, UI_AxisAlignment_End);
} break;
}
/* Alignment -> vertical alignment */
switch(alignment)
{
default: break;
case UI_Alignment_TopLeft:
case UI_Alignment_Top:
case UI_Alignment_TopRight:
{
UI_PushCopy(ChildAlignmentY, desc, UI_AxisAlignment_Start);
} break;
case UI_Alignment_Left:
case UI_Alignment_Center:
case UI_Alignment_Right:
{
UI_PushCopy(ChildAlignmentY, desc, UI_AxisAlignment_Center);
} break;
case UI_Alignment_BottomLeft:
case UI_Alignment_Bottom:
case UI_Alignment_BottomRight:
{
UI_PushCopy(ChildAlignmentY, desc, UI_AxisAlignment_End);
} break;
}
} 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_Text:
{
n->style.Text = PushString(frame->arena, desc.style.Text);
} break;
case UI_StyleKind_Tag:
{
if (n->next != 0)
{
n->style.Tag = RandU64FromSeeds(n->next->style.Tag, n->style.Tag);
}
} 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);
}
};
case UI_StyleKind_ChildAlignment:
{
UI_StyleDesc x_desc = desc;
UI_StyleDesc y_desc = desc;
x_desc.style.kind = UI_StyleKind_ChildAlignmentX;
y_desc.style.kind = UI_StyleKind_ChildAlignmentY;
UI_AxisAlignment x_alignment = UI_PopStyle(x_desc).ChildAlignmentX;
UI_AxisAlignment y_alignment = UI_PopStyle(y_desc).ChildAlignmentY;
switch(y_alignment)
{
case UI_AxisAlignment_Start:
{
switch(x_alignment)
{
case UI_AxisAlignment_Start: { result.ChildAlignment = UI_Alignment_TopLeft; } break;
case UI_AxisAlignment_Center: { result.ChildAlignment = UI_Alignment_Top; } break;
case UI_AxisAlignment_End: { result.ChildAlignment = UI_Alignment_TopRight; } break;
}
} break;
case UI_AxisAlignment_Center:
{
switch(x_alignment)
{
case UI_AxisAlignment_Start: { result.ChildAlignment = UI_Alignment_Left; } break;
case UI_AxisAlignment_Center: { result.ChildAlignment = UI_Alignment_Center; } break;
case UI_AxisAlignment_End: { result.ChildAlignment = UI_Alignment_Right; } break;
}
} break;
case UI_AxisAlignment_End:
{
switch(x_alignment)
{
case UI_AxisAlignment_Start: { result.ChildAlignment = UI_Alignment_BottomLeft; } break;
case UI_AxisAlignment_Center: { result.ChildAlignment = UI_Alignment_Bottom; } break;
case UI_AxisAlignment_End: { result.ChildAlignment = UI_Alignment_BottomRight; } break;
}
} break;
}
} break;
}
}
else
{
UI_StyleNode *n = stack->style_tops[kind];
result = n->style;
if (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 key)
{
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.parent = UI_UseTop(Parent);
n->cmd.box.flags = UI_UseTop(Flags);
n->cmd.box.pref_size[Axis_X] = UI_UseTop(Width);
n->cmd.box.pref_size[Axis_Y] = UI_UseTop(Height);
n->cmd.box.child_alignment[Axis_X] = UI_UseTop(ChildAlignmentX);
n->cmd.box.child_alignment[Axis_Y] = UI_UseTop(ChildAlignmentY);
n->cmd.box.child_layout_axis = UI_UseTop(ChildLayoutAxis);
n->cmd.box.background_color = UI_UseTop(BackgroundColor);
n->cmd.box.border_color = UI_UseTop(BorderColor);
n->cmd.box.debug_color = UI_UseTop(DebugColor);
n->cmd.box.tint = UI_UseTop(Tint);
n->cmd.box.border = UI_UseTop(Border);
n->cmd.box.font = UI_UseTop(Font);
n->cmd.box.font_size = UI_UseTop(FontSize);
n->cmd.box.rounding = UI_UseTop(Rounding);
n->cmd.box.text = UI_UseTop(Text);
n->cmd.box.floating_pos = UI_UseTop(FloatingPos);
}
++frame->cmds_count;
SllQueuePush(frame->first_cmd_node, frame->last_cmd_node, n);
return key;
}
void UI_SetRawTexture(UI_Key key, G_Texture2DRef tex, Vec2 uv0, Vec2 uv1)
{
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.uv0 = uv0;
n->cmd.set_raw_texture.uv1 = uv1;
}
++frame->cmds_count;
SllQueuePush(frame->first_cmd_node, frame->last_cmd_node, n);
}
////////////////////////////////////////////////////////////
//~ Report
UI_Report UI_ReportFromKey(UI_Key key)
{
UI_State *g = &UI_state;
UI_Report result = ZI;
UI_Box *box = UI_BoxFromKey(key);
if (box)
{
result = box->report;
}
return result;
}
////////////////////////////////////////////////////////////
//~ Begin frame
UI_Frame *UI_BeginFrame(UI_FrameFlag frame_flags, Vec4 swapchain_color)
{
UI_State *g = &UI_state;
//////////////////////////////
//- Init persistent state
if (!g->box_arena)
{
g->box_arena = AcquireArena(Gibi(64));
g->box_bins = PushStructs(g->box_arena, UI_BoxBin, UI_NumBoxLookupBins);
for (u64 i = 0; i < countof(g->frames); ++i)
{
UI_Frame *frame = &g->frames[i];
frame->arena = AcquireArena(Gibi(64));
frame->rects_arena = AcquireArena(Gibi(64));
frame->gpu_arena = G_AcquireArena();
}
}
//////////////////////////////
//- Begin frame
u64 last_frame_idx = g->current_frame_idx;
u64 frame_idx = last_frame_idx + 1;
if (frame_idx >= countof(g->frames))
{
frame_idx = 0;
}
g->current_frame_idx = frame_idx;
UI_Frame *last_frame = &g->frames[last_frame_idx];
UI_Frame *frame = &g->frames[frame_idx];
{
Arena *old_arena = frame->arena;
Arena *old_rects_arena = frame->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 - last_frame->time_ns;
frame->time_ns = now_ns;
frame->dt_ns = dt_ns;
frame->tick = last_frame->tick + 1;
}
/* Init style stack */
{
frame->top_stack = PushStruct(frame->arena, UI_Stack);
UI_PushDefaults();
}
frame->frame_flags = frame_flags;
frame->swapchain_color = swapchain_color;
//////////////////////////////
//- Process controller events
if (last_frame->boxes_pre != 0)
{
ControllerEventsArray controller_events = frame->window_frame.controller_events;
frame->cursor_pos = last_frame->cursor_pos;
f64 dt = SecondsFromNs(frame->dt_ns);
f64 inv_dt = 1.0 / dt;
/* Locate boxes */
UI_Box *hovered_box = 0;
UI_Box *active_box = UI_BoxFromKey(last_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 = g->boxes_count; pre_index-- > 0;)
{
UI_Box *box = last_frame->boxes_pre[pre_index];
if (hovered_box == 0 && box->desc.flags & UI_BoxFlag_Interactable)
{
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->p0;
Vec2 p1 = box->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;
}
if (is_cursor_in_box)
{
hovered_box = box;
}
}
box->report.m1_ups = 0;
box->report.m1_downs = 0;
box->report.m1_presses = 0;
}
/* Update state from controller events */
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)
{
if (hovered_box)
{
++hovered_box->report.m1_downs;
hovered_box->report.m1_held = 1;
hovered_box->report.last_m1_offset = SubVec2(frame->cursor_pos, hovered_box->p0);
active_box = hovered_box;
}
}
} break;
case ControllerEventKind_ButtonUp:
{
if (cev.button == Button_M1)
{
if (active_box)
{
if (active_box == hovered_box)
{
++active_box->report.m1_presses;
}
++active_box->report.m1_ups;
active_box->report.m1_held = 0;
active_box = 0;
}
}
} break;
case ControllerEventKind_Quit:
{
SignalExit(0);
} break;
}
}
/* Update box hot & active states */
for (u64 pre_index = 0; pre_index < g->boxes_count; ++pre_index)
{
UI_Box *box = last_frame->boxes_pre[pre_index];
UI_Report *report = &box->report;
f32 target_hot = box == active_box || (box == hovered_box && (box == active_box || active_box == 0));
f32 target_active = box == active_box;
f32 target_hovered = box == hovered_box;
f32 hot_blend_rate = target_hot == 1 ? 1 : (20 * dt);
f32 active_blend_rate = target_active == 1 ? 1 : (20 * dt);
f32 hovered_blend_rate = target_hovered == 1 ? 1 : (20 * dt);
report->hot = LerpF32(report->hot, target_hot, hot_blend_rate);
report->active = LerpF32(report->active, target_active, active_blend_rate);
report->hovered = LerpF32(report->hovered, target_hovered, hovered_blend_rate);
report->screen_p0 = box->p0;
report->screen_p1 = box->p1;
}
frame->hovered_box = hovered_box ? hovered_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)
{
UI_State *g = &UI_state;
return &g->frames[g->current_frame_idx];
};
Arena *UI_FrameArena(void)
{
return UI_CurrentFrame()->arena;
}
Vec2 UI_CursorPos(void)
{
return UI_CurrentFrame()->cursor_pos;
}
////////////////////////////////////////////////////////////
//~ End frame
void UI_EndFrame(UI_Frame *frame)
{
TempArena scratch = BeginScratchNoConflict();
UI_State *g = &UI_state;
Vec2I32 monitor_size = frame->window_frame.monitor_size;
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));
//////////////////////////////
//- Process commands
g->boxes_count = 0;
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 (key.hash == 0)
{
key = UI_TransKey();
}
b32 is_root = frame->root_box == 0;
UI_Box *parent = 0;
if (!is_root)
{
parent = UI_BoxFromKey(cmd.box.parent);
if (!parent)
{
parent = frame->root_box;
}
}
/* Allocate box */
UI_Box *box = 0;
{
UI_BoxBin *bin = &g->box_bins[key.hash % UI_NumBoxLookupBins];
for (UI_Box *tmp = bin->first; tmp && !box; tmp = tmp->next_in_bin)
{
if (tmp->key.hash == key.hash)
{
box = tmp;
}
}
if (box)
{
/* Remove box from old parent */
if (box->parent)
{
DllQueueRemove(box->parent->first, box->parent->last, box);
--box->parent->count;
}
}
else
{
box = g->first_free_box;
if (box)
{
SllStackPop(g->first_free_box);
ZeroStruct(box);
}
else
{
box = PushStruct(g->box_arena, UI_Box);
}
DllQueuePushNP(bin->first, bin->last, box, next_in_bin, prev_in_bin);
}
}
++g->boxes_count;
/* Reset box */
UI_Box old_box = *box;
{
ZeroStruct(box);
box->next_in_bin = old_box.next_in_bin;
box->prev_in_bin = old_box.prev_in_bin;
box->report = old_box.report;
}
box->key = key;
box->last_updated_tick = frame->tick;
/* Update box */
{
box->desc = cmd.box;
/* Insert box into parent */
if (parent)
{
DllQueuePush(parent->first, parent->last, box);
box->parent = parent;
++parent->count;
}
/* Fetch run */
box->glyph_run = &GC_NilRun;
if (box->desc.text.len > 0)
{
box->glyph_run = GC_RunFromString(frame->arena, box->desc.font, box->desc.text);
}
}
if (is_root)
{
frame->root_box = box;
}
} 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_uv0 = cmd.set_raw_texture.uv0;
box->raw_texture_uv1 = cmd.set_raw_texture.uv1;
}
} break;
}
}
//////////////////////////////
//- Prune boxes
// {
// u64 cur_tick = frame->tick;
// UI_BoxIter it = UI_ITER(g->root_box);
// UI_Box *box = UI_NextBox(&it);
// while (box != 0)
// {
// UI_Box *next = UI_NextBox(&it);
// box = next;
// }
// }
//////////////////////////////
//- Layout
/* Build pre-order & post-order box arrays */
u64 boxes_count = g->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;
{
Struct(BoxNode) { BoxNode *next; b32 visited; UI_Box *box; };
BoxNode *first_dfs = 0;
u64 pre_index = 0;
u64 post_index = 0;
{
BoxNode *n = PushStruct(scratch.arena, BoxNode);
n->box = frame->root_box;
SllStackPush(first_dfs, n);
}
while (first_dfs)
{
BoxNode *n = first_dfs;
UI_Box *box = n->box;
if (!n->visited)
{
/* Push floating children to dfs stack */
for (UI_Box *child = box->last; child; child = child->prev)
{
if (AnyBit(child->desc.flags, UI_BoxFlag_Floating))
{
BoxNode *child_n = PushStruct(scratch.arena, BoxNode);
child_n->box = child;
SllStackPush(first_dfs, child_n);
}
}
/* 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))
{
BoxNode *child_n = PushStruct(scratch.arena, BoxNode);
child_n->box = child;
SllStackPush(first_dfs, child_n);
}
}
box->pre_index = pre_index++;
boxes_pre[box->pre_index] = box;
n->visited = 1;
}
else
{
SllStackPop(first_dfs);
box->post_index = post_index++;
boxes_post[box->post_index] = box;
}
}
Assert(pre_index == boxes_count);
Assert(post_index == boxes_count);
}
/* Compute 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 pref_size = box->desc.pref_size[axis];
if (pref_size.kind == UI_SizeKind_Pixel)
{
box->solved_dims[axis] = pref_size.v;
}
else if (pref_size.kind == UI_SizeKind_Shrink && AnyBit(box->desc.flags, UI_BoxFlag_DrawText))
{
/* TODO: Distinguish between baseline alignment & visual alignment */
f32 text_size = 0;
if (axis == Axis_X)
{
GC_RunRect rr = box->glyph_run->rects[box->glyph_run->count - 1];
f32 baseline_length = rr.pos + rr.advance;
text_size = baseline_length;
}
else
{
text_size = box->glyph_run->ascent + box->glyph_run->descent;
}
box->solved_dims[axis] = text_size + (pref_size.v * 2);
}
}
}
/* Compute 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 pref_size = box->desc.pref_size[axis];
if (pref_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_size[axis];
if (ancestor_size.kind == UI_SizeKind_Pixel || (ancestor_size.kind == UI_SizeKind_Shrink && AnyBit(box->desc.flags, UI_BoxFlag_DrawText)))
{
/* Match independent ancestor */
match_size = ancestor->solved_dims[axis];
found_match = 1;
}
}
box->solved_dims[axis] = match_size * pref_size.v;
}
}
}
/* Compute 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 pref_size = box->desc.pref_size[axis];
if (pref_size.kind == UI_SizeKind_Shrink && !AnyBit(box->desc.flags, UI_BoxFlag_DrawText))
{
f32 accum = 0;
for (UI_Box *child = box->first; child; child = child->next)
{
if (!AnyBit(child->desc.flags, UI_BoxFlag_Floating))
{
if (axis == box->desc.child_layout_axis)
{
accum += child->solved_dims[axis];
}
else
{
accum = MaxF32(child->solved_dims[axis], accum);
}
}
}
box->solved_dims[axis] = accum + (pref_size.v * 2);
}
}
}
/* Compute 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 pref_size = box->desc.pref_size[axis];
if (pref_size.kind == UI_SizeKind_Grow)
{
box->solved_dims[axis] = box->parent->solved_dims[axis] * pref_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[axis];
/* Solve non-floating violations */
{
f32 size_accum = 0;
f32 flex_accum = 0;
for (UI_Box *child = box->first; child; child = child->next)
{
if (!AnyBit(child->desc.flags, UI_BoxFlag_Floating))
{
f32 size = child->solved_dims[axis];
f32 strictness = child->desc.pref_size[axis].strictness;
f32 flex = size * (1.0 - strictness);
if (axis == box->desc.child_layout_axis)
{
size_accum += size;
flex_accum += flex;
}
else
{
size_accum = MaxF32(size_accum, size);
flex_accum = MaxF32(flex_accum, flex);
}
}
}
f32 violation = size_accum - box_size;
if (violation > 0 && flex_accum > 0)
{
f32 adjusted_size_accum = 0;
for (UI_Box *child = box->first; child; child = child->next)
{
if (!AnyBit(child->desc.flags, UI_BoxFlag_Floating))
{
f32 size = child->solved_dims[axis];
f32 strictness = child->desc.pref_size[axis].strictness;
f32 flex = size * (1.0 - strictness);
f32 new_size = size;
if (axis == box->desc.child_layout_axis)
{
f32 chopoff = MinF32(flex, violation * (flex / flex_accum));
new_size = size - chopoff;
}
else
{
if (size > box_size)
{
new_size = MaxF32(size - flex, box_size);
}
}
adjusted_size_accum += new_size;
child->solved_dims[axis] = new_size;
}
}
size_accum = adjusted_size_accum;
}
box->final_children_size_accum[axis] = size_accum;
}
/* Solve floating violations */
for (UI_Box *child = box->first; child; child = child->next)
{
if (AnyBit(child->desc.flags, UI_BoxFlag_Floating) && !AnyBit(child->desc.flags, UI_BoxFlag_NoFloatingClamp))
{
f32 size = child->solved_dims[axis];
if (size > box_size)
{
f32 strictness = child->desc.pref_size[axis].strictness;
f32 flex = size * (1.0 - strictness);
child->solved_dims[axis] = MaxF32(size - flex, box_size);
}
}
}
}
}
/* Compute 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;
/* Initialize layout cursor based on alignment */
{
Axis axis = box->desc.child_layout_axis;
UI_AxisAlignment alignment = box->desc.child_alignment[axis];
f32 box_size = box->solved_dims[axis];
f32 size_accum = box->final_children_size_accum[axis];
switch(alignment)
{
default: break;
case UI_AxisAlignment_Center:
{
box->layout_cursor = box_size / 2 - size_accum / 2;
} break;
case UI_AxisAlignment_End:
{
box->layout_cursor = box_size - size_accum;
} break;
}
}
/* Position */
{
f32 *dims_arr = box->solved_dims;
Vec2 dims_vec = VEC2(dims_arr[0], dims_arr[1]);
Vec2 final_pos = ZI;
/* Floating box position */
if (AnyBit(box->desc.flags, UI_BoxFlag_Floating))
{
Vec2 offset = box->desc.floating_pos;
final_pos = AddVec2(parent->p0, offset);
if (!AnyBit(box->desc.flags, UI_BoxFlag_NoFloatingClamp))
{
{
f32 overshoot = MaxF32(0, (final_pos.x + dims_vec.x) - parent->p1.x);
final_pos.x = MaxF32(parent->p0.x, final_pos.x - overshoot);
}
{
f32 overshoot = MaxF32((final_pos.y + dims_vec.y) - parent->p1.y, 0);
final_pos.y = MaxF32(parent->p0.y, final_pos.y - overshoot);
}
}
}
/* Non-floating box position */
else if (parent)
{
f32 layout_cursor = parent->layout_cursor;
f32 offset[2] = ZI;
/* Compute offset in layout direction */
{
Axis axis = parent->desc.child_layout_axis;
offset[axis] = layout_cursor;
}
/* Compute offset in non-layout direction (based on alignment) */
{
Axis axis = !parent->desc.child_layout_axis;
UI_AxisAlignment alignment = parent->desc.child_alignment[axis];
switch(alignment)
{
default: break;
case UI_AxisAlignment_Center:
{
f32 parent_size = parent->solved_dims[axis];
f32 box_size = dims_arr[axis];
offset[axis] = parent_size / 2 - box_size / 2;
} break;
case UI_AxisAlignment_End:
{
f32 parent_size = parent->solved_dims[axis];
f32 box_size = dims_arr[axis];
offset[axis] = parent_size - box_size;
} break;
}
}
final_pos.x = parent->p0.x + offset[0];
final_pos.y = parent->p0.y + offset[1];
parent->layout_cursor += dims_arr[parent->desc.child_layout_axis];
}
/* Submit position */
Vec2 floored_final_pos = FloorVec2(final_pos);
Vec2 ceiled_dims = CeilVec2(dims_vec);
box->p0 = FloorVec2(floored_final_pos);
box->p1 = AddVec2(floored_final_pos, ceiled_dims);
}
/* Rounding */
{
UI_Round rounding = box->desc.rounding;
Vec2 half_dims = MulVec2(SubVec2(box->p1, box->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;
}
if (parent && !AllBits(box->desc.flags, UI_BoxFlag_Floating | UI_BoxFlag_NoFloatingClamp))
{
Vec2 vtl = SubVec2(VEC2(parent->p0.x, parent->p0.y), VEC2(box->p0.x, box->p0.y));
Vec2 vtr = SubVec2(VEC2(parent->p1.x, parent->p0.y), VEC2(box->p1.x, box->p0.y));
Vec2 vbr = SubVec2(VEC2(parent->p1.x, parent->p1.y), VEC2(box->p1.x, box->p1.y));
Vec2 vbl = SubVec2(VEC2(parent->p0.x, parent->p1.y), VEC2(box->p0.x, box->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));
}
/* Submit rounding */
box->rounding_tl = final_rounding_tl;
box->rounding_tr = final_rounding_tr;
box->rounding_br = final_rounding_br;
box->rounding_bl = final_rounding_bl;
}
}
//////////////////////////////
//- 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);
is_visible = is_visible && (box->p1.x > box->p0.x);
is_visible = is_visible && (box->p1.y > box->p0.y);
if (is_visible || AnyBit(frame->frame_flags, UI_FrameFlag_Debug))
{
/* Box rect */
{
UI_DRect *rect = PushStruct(frame->rects_arena, UI_DRect);
rect->p0 = box->p0;
rect->p1 = box->p1;
rect->tex_uv0 = VEC2(0, 0);
rect->tex_uv1 = VEC2(1, 1);
rect->background_lin = LinearFromSrgb(box->desc.background_color);
rect->border_lin = LinearFromSrgb(box->desc.border_color);
rect->debug_lin = LinearFromSrgb(box->desc.debug_color);
rect->tint_lin = LinearFromSrgb(box->desc.tint);
rect->border = box->desc.border;
rect->tl_rounding = box->rounding_tl;
rect->tr_rounding = box->rounding_tr;
rect->br_rounding = box->rounding_br;
rect->bl_rounding = box->rounding_bl;
rect->tex = box->raw_texture;
rect->tex_uv0 = box->raw_texture_uv0;
rect->tex_uv1 = box->raw_texture_uv1;
}
/* Text rects */
GC_Run *raw_run = box->glyph_run;
if (AnyBit(box->desc.flags, UI_BoxFlag_DrawText) && raw_run->loaded)
{
f32 max_baseline = box->p1.x - box->p0.x;
b32 should_truncate = raw_run->count > 0 && (raw_run->rects[raw_run->count - 1].pos + raw_run->rects[raw_run->count - 1].advance) > max_baseline;
/* Truncate run */
GC_RunRect *truncated_rects = raw_run->rects;
u32 truncated_rects_count = raw_run->count;
if (should_truncate && !AnyBit(box->desc.flags, UI_BoxFlag_NoTextTruncation))
{
/* Get elipses run */
GC_Run *elipses_run = GC_RunFromString(scratch.arena, box->desc.font, Lit("..."));
if (elipses_run->count > 0)
{
max_baseline -= elipses_run->rects[elipses_run->count - 1].pos + elipses_run->rects[elipses_run->count - 1].advance;
}
/* Subtract glyphs */
while (truncated_rects_count > 0)
{
GC_RunRect rr = raw_run->rects[truncated_rects_count - 1];
if (rr.pos + rr.advance <= max_baseline)
{
break;
}
--truncated_rects_count;
}
/* Merge trunc rects */
/* FIXME: Verify this stil works as expected */
{
truncated_rects_count += elipses_run->count;
truncated_rects = PushStructsNoZero(scratch.arena, GC_RunRect, truncated_rects_count + elipses_run->count);
CopyStructs(truncated_rects, raw_run->rects, raw_run->count);
f32 elipses_offset = truncated_rects_count > 0 ? (truncated_rects[truncated_rects_count - 1].pos + truncated_rects[truncated_rects_count - 1].advance) : 0;
for (u32 i = 0; i < elipses_run->count; ++i)
{
GC_RunRect *rr = &truncated_rects[i + truncated_rects_count];
*rr = elipses_run->rects[i];
rr->pos += elipses_offset;
}
}
}
UI_AxisAlignment x_alignment = box->desc.child_alignment[Axis_X];
UI_AxisAlignment y_alignment = box->desc.child_alignment[Axis_Y];
if (should_truncate)
{
x_alignment = UI_AxisAlignment_Start;
}
/* Compute baseline */
f32 ascent = raw_run->ascent;
f32 descent = raw_run->descent;
f32 cap = raw_run->cap;
f32 baseline_width = truncated_rects_count > 0 ? (truncated_rects[truncated_rects_count - 1].pos + truncated_rects[truncated_rects_count - 1].advance) : 0;
f32 baseline_height = ascent + descent;
f32 box_width = box->p1.x - box->p0.x;
f32 box_height = box->p1.y - box->p0.y;
Vec2 baseline = ZI;
switch (x_alignment)
{
case UI_AxisAlignment_Start:
{
baseline.x = box->p0.x;
} break;
case UI_AxisAlignment_End:
{
baseline.x = box->p1.x;
baseline.x -= baseline_width;
} break;
case UI_AxisAlignment_Center:
{
baseline.x = box->p0.x;
baseline.x += (box_width - baseline_width) / 2;
} break;
}
switch (y_alignment)
{
case UI_AxisAlignment_Start:
{
baseline.y = box->p0.y;
baseline.y += ascent;
} break;
case UI_AxisAlignment_End:
{
baseline.y = box->p1.y;
baseline.y -= descent;
} break;
case UI_AxisAlignment_Center:
{
baseline.y = box->p0.y;
baseline.y += box_height / 2;
baseline.y += cap / 2;
} break;
}
baseline = CeilVec2(baseline);
/* Push text rects */
for (u64 i = 0; i < truncated_rects_count; ++i)
{
GC_RunRect rr = truncated_rects[i];
Vec2 glyph_size = rr.size;
if (glyph_size.x != 0 || glyph_size.y != 0)
{
UI_DRect *rect = PushStruct(frame->rects_arena, UI_DRect);
rect->p0 = AddVec2(baseline, VEC2(rr.pos, 0));
rect->p0 = AddVec2(rect->p0, rr.offset);
rect->p1 = AddVec2(rect->p0, glyph_size);
rect->debug_lin = LinearFromSrgb(box->desc.debug_color);
rect->tint_lin = LinearFromSrgb(box->desc.tint);
rect->tex = rr.tex;
rect->tex_uv0 = rr.uv.p0;
rect->tex_uv1 = rr.uv.p1;
}
}
}
}
}
//////////////////////////////
//- Push data to GPU
/* Target */
G_ResourceHandle draw_target = G_PushTexture2D(
frame->gpu_arena,
G_Format_R16G16B16A16_Float,
monitor_size,
G_Layout_DirectQueue_RenderTargetWrite,
.flags = G_ResourceFlag_AllowRenderTarget
);
G_Texture2DRef draw_target_ro = G_PushTexture2DRef(frame->gpu_arena, draw_target);
/* Rects */
G_ResourceHandle rects_buff = G_PushBufferFromString(frame->gpu_arena, frame->cl, StringFromArena(frame->rects_arena));
G_StructuredBufferRef rects_ro = G_PushStructuredBufferRef(frame->gpu_arena, rects_buff, UI_DRect);
/* Params */
UI_DParams params = ZI;
{
params.target_size = draw_size;
params.target_ro = draw_target_ro;
params.rects = rects_ro;
params.sampler = G_BasicSampler();
}
G_ResourceHandle params_buff = G_PushBufferFromString(frame->gpu_arena, frame->cl, StringFromStruct(&params));
G_StructuredBufferRef params_ro = G_PushStructuredBufferRef(frame->gpu_arena, params_buff, UI_DParams);
/* Constants */
G_SetConstant(frame->cl, UI_ShaderConst_Params, params_ro);
//////////////////////////////
//- Dispatch shaders
G_DumbMemoryLayoutSync(frame->cl, draw_target, G_Layout_DirectQueue_RenderTargetWrite);
//- Clear pass
{
G_ClearRenderTarget(frame->cl, draw_target, VEC4(1, 0, 0, 1));
}
//- Rect pass
G_DumbMemoryLayoutSync(frame->cl, draw_target, G_Layout_DirectQueue_RenderTargetWrite);
if (G_CountBufferBytes(rects_buff) > 0)
{
/* Render rects */
G_Rasterize(frame->cl,
UI_DRectVS, UI_DRectPS,
1, G_QuadIndices(),
1, &draw_target,
draw_viewport, draw_scissor,
G_RasterMode_TriangleList);
/* Render rect wireframes */
if (AnyBit(frame->frame_flags, UI_FrameFlag_Debug))
{
G_SetConstant(frame->cl, UI_ShaderConst_DebugDraw, 1);
G_Rasterize(frame->cl,
UI_DRectVS, UI_DRectPS,
1, G_QuadIndices(),
1, &draw_target,
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, &backbuffer,
draw_viewport, draw_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);
}