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, ...) { 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_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(); 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.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 idx = 0; idx < countof(prefetch); ++idx) { prefetch[idx] = idx; } 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_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; 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, 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_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->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 - 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->rect.p0; Vec2 p1 = box->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; } 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->rect.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_rect = box->rect; } 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; 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)); ////////////////////////////// //- 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 */ if (box->desc.text.len > 0) { box->glyph_run = GC_RunFromString(frame->arena, box->desc.text, box->desc.font, box->desc.font_size); } } 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_slice_uv = cmd.set_raw_texture.slice_uv; } } 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) { text_size = box->glyph_run.baseline_length; } else { text_size = box->glyph_run.font_ascent + box->glyph_run.font_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->rect.p0, offset); if (!AnyBit(box->desc.flags, UI_BoxFlag_NoFloatingClamp)) { { f32 overshoot = MaxF32(0, (final_pos.x + dims_vec.x) - parent->rect.p1.x); final_pos.x = MaxF32(parent->rect.p0.x, final_pos.x - overshoot); } { f32 overshoot = MaxF32((final_pos.y + dims_vec.y) - parent->rect.p1.y, 0); final_pos.y = MaxF32(parent->rect.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->rect.p0.x + offset[0]; final_pos.y = parent->rect.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->rect.p0 = FloorVec2(floored_final_pos); box->rect.p1 = AddVec2(floored_final_pos, ceiled_dims); } /* Rounding */ { UI_Round rounding = box->desc.rounding; Vec2 half_dims = MulVec2(SubVec2(box->rect.p1, box->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; } if (parent && !AllBits(box->desc.flags, UI_BoxFlag_Floating | UI_BoxFlag_NoFloatingClamp)) { Vec2 vtl = SubVec2(VEC2(parent->rect.p0.x, parent->rect.p0.y), VEC2(box->rect.p0.x, box->rect.p0.y)); Vec2 vtr = SubVec2(VEC2(parent->rect.p1.x, parent->rect.p0.y), VEC2(box->rect.p1.x, box->rect.p0.y)); Vec2 vbr = SubVec2(VEC2(parent->rect.p1.x, parent->rect.p1.y), VEC2(box->rect.p1.x, box->rect.p1.y)); Vec2 vbl = SubVec2(VEC2(parent->rect.p0.x, parent->rect.p1.y), VEC2(box->rect.p0.x, box->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)); } /* 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->rect.p1.x > box->rect.p0.x); is_visible = is_visible && (box->rect.p1.y > box->rect.p0.y); if (is_visible || AnyBit(frame->frame_flags, UI_FrameFlag_Debug)) { /* Box rect */ { UI_DRect *rect = PushStruct(frame->rects_arena, UI_DRect); rect->bounds = box->rect; 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_slice_uv = box->raw_texture_slice_uv; } /* Text rects */ GC_Run raw_run = box->glyph_run; if (AnyBit(box->desc.flags, UI_BoxFlag_DrawText) && raw_run.ready) { f32 max_baseline = DimsFromRng2(box->rect).x; b32 should_truncate = raw_run.baseline_length > max_baseline && !AnyBit(box->desc.flags, UI_BoxFlag_NoTextTruncation); /* Truncate run */ u64 final_rects_count = 0; GC_RunRect *final_rects = 0; if (should_truncate) { /* Get elipses run */ GC_Run elipses_run = GC_RunFromString(scratch.arena, Lit("..."), box->desc.font, box->desc.font_size); f32 elipses_start_pos = max_baseline - elipses_run.baseline_length; /* Append non-overflowed rects */ 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) <= elipses_start_pos) { final_rects[final_rects_count] = rr; final_rects_count += 1; } } /* 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_start_pos; rr.bounds = AddRng2Vec2(rr.bounds, VEC2(elipses_start_pos, 0)); if ((rr.baseline_pos + rr.advance) <= max_baseline) { final_rects[final_rects_count] = rr; final_rects_count += 1; } } } else { final_rects = raw_run.rects; final_rects_count = raw_run.rects_count; } 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.font_ascent; f32 font_descent = raw_run.font_descent; f32 cap = raw_run.font_cap; f32 baseline_width = raw_run.baseline_length; f32 baseline_height = ascent + font_descent; Vec2 box_dims = DimsFromRng2(box->rect); Vec2 baseline = ZI; switch (x_alignment) { case UI_AxisAlignment_Start: { baseline.x = box->rect.p0.x; } break; case UI_AxisAlignment_End: { baseline.x = box->rect.p1.x; baseline.x -= baseline_width; } break; case UI_AxisAlignment_Center: { baseline.x = box->rect.p0.x; baseline.x += (box_dims.x - baseline_width) / 2; } break; } switch (y_alignment) { case UI_AxisAlignment_Start: { baseline.y = box->rect.p0.y; baseline.y += ascent; } break; case UI_AxisAlignment_End: { baseline.y = box->rect.p1.y; baseline.y -= font_descent; } break; case UI_AxisAlignment_Center: { baseline.y = box->rect.p0.y; baseline.y += box_dims.y / 2; baseline.y += cap / 2; } break; } baseline = CeilVec2(baseline); /* Push text rects */ 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_DRect *rect = PushStruct(frame->rects_arena, UI_DRect); rect->debug_lin = LinearFromSrgb(box->desc.debug_color); rect->tint_lin = LinearFromSrgb(box->desc.tint); 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, 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 */ u64 rects_count = ArenaCount(frame->rects_arena, UI_DRect); 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(); params.cursor_pos = frame->cursor_pos; } G_ResourceHandle params_buff = G_PushBufferFromString(frame->gpu_arena, frame->cl, StringFromStruct(¶ms)); 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(0, 0, 0, 1)); } //- Rect pass G_DumbMemoryLayoutSync(frame->cl, draw_target, G_Layout_DirectQueue_RenderTargetWrite); if (rects_count > 0) { /* Render rects */ G_Rasterize(frame->cl, UI_DRectVS, UI_DRectPS, rects_count, 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, rects_count, 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, 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); }