#include "generated/vn_generated_ui.cpp" inline ui_size UI_Pixels(r32 Value, r32 Strictness) { ui_size Result = {UI_SizeType_Pixels, Value, Strictness}; return(Result); } inline ui_size UI_Em(r32 Value, r32 Strictness) { ui_size Result = {UI_SizeType_Pixels, Value*UI_TopFontSize(), Strictness}; return(Result); } inline ui_size UI_TextContent(r32 Value, r32 Strictness) { ui_size Result = {UI_SizeType_TextContent, Value, Strictness}; return(Result); } inline ui_size UI_Percent(r32 Value, r32 Strictness) { ui_size Result = {UI_SizeType_PercentOfParent, Value, Strictness}; return(Result); } inline ui_size UI_ChildrenSum(r32 Value, r32 Strictness) { ui_size Result = {UI_SizeType_ChildrenSum, Value, Strictness}; return(Result); } per_thread ui *ThreadLocal_UI; inline void UI_SetState(ui *UI) { ThreadLocal_UI = UI; } inline ui *UI_GetState(void) { return(ThreadLocal_UI); } inline ui_key UI_GetHot(void) { ui *UI = UI_GetState(); return(UI->Hot); } inline ui_key UI_GetActive(void) { ui *UI = UI_GetState(); return(UI->Active); } inline void UI_SetDragStartP(v2 P) { ui *UI = UI_GetState(); UI->DragStartP = P; } inline void UI_UpdateDragStartP(void) { ui *UI = UI_GetState(); UI->DragStartP = UI->MouseP; } inline v2 UI_GetDragStartP(void) { ui *UI = UI_GetState(); return(UI->DragStartP); } inline void UI_StoreDragV2(v2 DragData) { ui *UI = UI_GetState(); *(v2 *)&UI->DragData = DragData; } inline v2 UI_GetDragV2(void) { ui *UI = UI_GetState(); v2 Result = *(v2 *)UI->DragData; return(Result); } inline void UI_StoreDragR32(r32 DragData) { ui *UI = UI_GetState(); *(r32 *)&UI->DragData = DragData; } inline r32 UI_GetDragR32(void) { ui *UI = UI_GetState(); r32 Result = *(r32 *)UI->DragData; return(Result); } inline void UI_StoreDragPayload(void *Source) { ui *UI = UI_GetState(); Copy(UI->DragData, Source, sizeof(UI->DragData)); } inline void UI_GetDragPayload(void *Dest) { ui *UI = UI_GetState(); Copy(Dest, UI->DragData, sizeof(UI->DragData)); } inline void UI_StoreDragPointer(void *Data) { ui *UI = UI_GetState(); *(void **)&UI->DragData = Data; } inline void *UI_GetDragDataPointer(void) { ui *UI = UI_GetState(); void *Result = *(void **)UI->DragData; return(Result); } inline ui_key UI_EmptyKey(void) { ui_key Key = {}; return(Key); } inline ui_key UI_SeedKey(ui_key Key, ui_key Seed) { ui_key Result = {((Key.Value + Seed.Value) << 5) + Key.Value}; return(Result); } inline ui_key UI_GenerateKeyFromString(string String) { ui_key Key; if(String.Count) { Key.Value = HashString(String); } else { Key = UI_EmptyKey(); } return(Key); } static string UI_GetBoxNameByKey(ui_key Key) { ui *UI = UI_GetState(); string Result = StrLit("Empty or Not Found"); if(!AreEqual(Key, UI_EmptyKey())) { for(s32 BucketIndex = 0; BucketIndex < ArrayCount(UI->BoxBuckets); ++BucketIndex) { ui_box_bucket *Bucket = UI->BoxBuckets + BucketIndex; for(ui_box *Box = Bucket->First; Box != 0; Box = Box->HashNext) { if(AreEqual(Key, Box->Key)) { Result = Box->String; goto FoundName; } } } } FoundName: return(Result); } inline ui_box *UI_GetBoxByKey(ui *UI, ui_key Key) { u64 Hash = Key.Value; u64 Slot = Hash % ArrayCount(UI->BoxBuckets); ui_box_bucket *Bucket = UI->BoxBuckets + Slot; ui_box *Result = 0; if(!AreEqual(Key, UI_EmptyKey())) { for(ui_box *Box = Bucket->First; Box != 0; Box = Box->HashNext) { if(AreEqual(Box->Key, Key)) { Result = Box; break; } } } if(!Result) { // sixten: Check if we have already allocated a box if(DLLIsEmpty(UI->FirstFreeBox)) { // sixten: If not, simply allocate one Result = PushStruct(&UI->Arena, ui_box); } else { // sixten: If there exists an already allocated, remove it from the free list and use it. Result = UI->FirstFreeBox; DLLRemove_NP(UI->FirstFreeBox, UI->LastFreeBox, Result, HashNext, HashPrev); } // sixten: Insert the box into the hashmap. DLLInsertLast_NP(Bucket->First, Bucket->Last, Result, HashNext, HashPrev); } Result->Key = Key; return(Result); } inline ui_box *UI_MakeBox(ui_box_flags Flags, string String) { ui *UI = UI_GetState(); ui_box *Parent = UI_TopParent(); ui_key BaseKey = UI_GenerateKeyFromString(String); ui_key Seed = UI_SeedKey(BaseKey, Parent ? Parent->Seed : UI_EmptyKey()); ui_key Key = BaseKey.Value ? Seed : UI_EmptyKey(); // sixten: Check for duplicate keys. #if VN_SLOW if(Parent && !AreEqual(Key, UI_EmptyKey())) { for(ui_box *Child = Parent->First; Child != 0; Child = Child->Next) { Assert(!AreEqual(Child->Key, Key)); } } #endif ui_box *Box = UI_GetBoxByKey(UI, Key); Box->Seed = Seed; Box->First = Box->Last = Box->Next = Box->Prev = Box->Parent = 0; Box->ComputedRelativeP = V2(0, 0); Box->ComputedDim = V2(0, 0); Box->LastFrameTouched = UI->CurrentFrame; Box->Flags = Flags; Box->String = PushString(&UI->FrameArena, String); s64 HashIndex = LastIndexOf(Box->String, '#'); if(HashIndex != -1) { Box->String = Prefix(Box->String, HashIndex); } UI_ApplyStyles(Box); if(Parent) { DLLInsertLast(Parent->First, Parent->Last, Box); } return(Box); } inline ui_box *UI_MakeBoxF(ui_box_flags Flags, char *Format, ...) { ui *UI = UI_GetState(); // sixten(TODO): Allocate on scratch, copy to frame arena // (alternatively keep two versions of UI_MakeBox, to make sure everything works) va_list Arguments; va_start(Arguments, Format); string String = PushFormatVariadic(&UI->FrameArena, Format, Arguments); va_end(Arguments); ui_box *Box = UI_MakeBox(Flags, String); return(Box); } inline void UI_SetNextHot(ui_key Key) { ui *UI = UI_GetState(); if(!UI->NextHotSet) { UI->NextHot = Key; UI->NextHotSet = true; } } inline b32 UI_ChildrenContainsP(ui_box *Box, v2 P) { b32 Result = false; if(Box->Flags & UI_BoxFlag_Clickable && InRange(Box->Rect, P)) { Result = true; } else { if(Box->First) { Result = UI_ChildrenContainsP(Box->First, P); } if(!Result) { if(Box->Next) { Result = UI_ChildrenContainsP(Box->Next, P); } } } return(Result); } static ui_signal UI_SignalFromBox(ui_box *Box) { ui *UI = UI_GetState(); ui_signal Signal = {}; Signal.Hovering = InRange(Box->Rect, UI->MouseP) && !(Box->First && UI_ChildrenContainsP(Box->First, UI->MouseP)) && !(Box->Next && UI_ChildrenContainsP(Box->Next, UI->MouseP)); // sixten: Make sure the tooltip is not overlapping. { // sixten: Are we the tooltip? b32 FoundTooltip = false; for(ui_box *Parent = Box->Parent; Parent != 0; Parent = Parent->Parent) { if(Parent == UI->TooltipNode) { FoundTooltip = true; break; } } if(!FoundTooltip && UI->TooltipNode->First) { Signal.Hovering &= ~UI_ChildrenContainsP(UI->TooltipNode->First, UI->MouseP); } } if(Box->Flags & UI_BoxFlag_Clickable) { if(AreEqual(UI->Active, Box->Key)) { if(Platform_KeyRelease(UI->EventList, Key_MouseLeft)) { Signal.Clicked = Signal.Hovering; Signal.Released = true; UI->Active = UI_EmptyKey(); if(!Signal.Hovering) { UI_SetNextHot(UI_EmptyKey()); } } else { Signal.Dragging = true; } } else { if(AreEqual(UI->Hot, Box->Key)) { if(Platform_KeyPress(UI->EventList, Key_MouseLeft)) { UI->Active = Box->Key; UI->DragStartP = UI->MouseP; Signal.Dragging = true; Signal.Pressed = true; } else if(!Signal.Hovering) { UI_SetNextHot(UI_EmptyKey()); } if(Platform_KeyPress(UI->EventList, Key_MouseRight)) { Signal.PressedRight = true; } } else { if(AreEqual(UI->Hot, UI_EmptyKey()) && Signal.Hovering) { UI_SetNextHot(Box->Key); } } } } Signal.MouseP = UI->MouseP; Signal.DragDelta = UI->MouseP - UI->DragStartP; Signal.Box = Box; return(Signal); } static void UI_SolveSizeViolations(ui_box *Box, axis2 Axis) { r32 TotalSpace = 0; r32 FixupBudget = 0; if(!(Box->Flags & (UI_BoxFlag_OverflowX<First; Child != 0; Child = Child->Next) { if(!(Child->Flags & (UI_BoxFlag_FloatingX<LayoutAxis) { TotalSpace += Child->ComputedDim.E[Axis]; } else { TotalSpace = Maximum(TotalSpace, Child->ComputedDim.E[Axis]); } FixupBudget += Child->ComputedDim.E[Axis] * (1 - Child->SemanticSize[Axis].Strictness); } } r32 Violation = TotalSpace - Box->ComputedDim.E[Axis]; if(Violation > 0 && FixupBudget > 0) { for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { if(!(Child->Flags & (UI_BoxFlag_FloatingX<ComputedDim.E[Axis] * (1 - Child->SemanticSize[Axis].Strictness); r32 ChildFixupSize = 0; if(Axis == Box->LayoutAxis) { ChildFixupSize = ChildFixupBudget * (Violation / FixupBudget); } else { ChildFixupSize = Child->ComputedDim.E[Axis] - Box->ComputedDim.E[Axis]; } ChildFixupSize = Clamp(ChildFixupSize, 0, ChildFixupBudget); Child->ComputedDim.E[Axis] -= ChildFixupSize; } } } } if(Axis == Box->LayoutAxis) { r32 Position = 0; for(ui_box *Child = Box->First; Child; Child = Child->Next) { if((Child->Flags & (UI_BoxFlag_FloatingX<ComputedRelativeP.E[Axis] = Child->FixedP.E[Axis]; } else { Child->ComputedRelativeP.E[Axis] = Position; Position += Child->ComputedDim.E[Axis]; } } } else { for(ui_box *Child = Box->First; Child; Child = Child->Next) { if((Child->Flags & (UI_BoxFlag_FloatingX<ComputedRelativeP.E[Axis] = Child->FixedP.E[Axis]; } else { Child->ComputedRelativeP.E[Axis] = 0; } } } } inline void UI_BeginTooltip(void) { ui *UI = UI_GetState(); UI_PushParent(UI->TooltipNode); } inline void UI_EndTooltip(void) { UI_PopParent(); } inline void UI_SetNextTooltip(void) { ui *UI = UI_GetState(); UI_SetNextParent(UI->TooltipNode); } #define UI_Tooltip DeferLoop(UI_BeginTooltip(), UI_EndTooltip()) static void UI_DrawBox(ui_box *Box, render_group *Group, glyph_atlas *GlyphAtlas) { ui *UI = UI_GetState(); Box->HotTransition += ((r32)AreEqual(UI->Hot, Box->Key) - Box->HotTransition) * 0.6f; Box->ActiveTransition += ((r32)AreEqual(UI->Active, Box->Key) - Box->ActiveTransition) * 0.6f; if(Box->Flags & UI_BoxFlag_DrawBackground) { PushQuad(Group, Box->Rect.Min, Box->ComputedDim, Box->BackgroundColor, Box->CornerRadius, 0, 0); } if(Box->Flags & UI_BoxFlag_HotAnimation) { v4 Top = V4(1, 1, 1, 0.06F*Box->HotTransition); v4 Bottom = V4(1, 1, 1, 0.0); PushQuad(Group, Box->Rect.Min, Box->ComputedDim, Top, Top, Bottom, Bottom, Box->CornerRadius, 0, 0); } if(Box->Flags & UI_BoxFlag_ActiveAnimation) { v4 Top = V4(0, 0, 0, 0.5F*Box->ActiveTransition); v4 Bottom = V4(0, 0, 0, 0.1F*Box->ActiveTransition); PushQuad(Group, Box->Rect.Min, Box->ComputedDim, Top, Top, Bottom, Bottom, Box->CornerRadius, 0, 0); } if(Box->Flags & UI_BoxFlag_DrawText) { v2 TextDim = V2(CalculateRasterizedTextWidth(GlyphAtlas, Box->Font, Box->FontSize, Box->String), CalculateRasterizedTextHeight(GlyphAtlas, Box->Font, Box->FontSize, Box->String)); v2 P = Box->Rect.Min + (Box->ComputedDim - TextDim)*0.5; PushText(Group, GlyphAtlas, Box->Font, P, Box->FontSize, Box->TextColor, Box->String); } if(Box->DrawCallback) { Box->DrawCallback(Group, GlyphAtlas, Box, Box->DrawCallbackData); } #if VN_INTERNAL if(DEBUG_DebugSettings->RenderUIDebugRects) { // sixten: Render debug rects around boxes. r32 R = (((Box->Key.Value >> 0) & ((1 << 22) - 1)) / (r32)((1 << 22) - 1)); r32 G = (((Box->Key.Value >> 21) & ((1 << 22) - 1)) / (r32)((1 << 22) - 1)); r32 B = (((Box->Key.Value >> 42) & ((1 << 22) - 1)) / (r32)((1 << 22) - 1)); v4 Red = V4R32(R, G, B, 1); PushQuad(Group, Box->Rect.Min, Box->ComputedDim, Red, Red, Red, Red, 0, 1.8, 1.8); } #endif if(Box->Flags & UI_BoxFlag_Clip) { PushClip(Group, Box->Rect); } for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { if(Child->Flags & UI_BoxFlag_DrawDropShadow) { r32 ShadowRadius = 10; v2 P = Child->Rect.Min - V2(ShadowRadius, ShadowRadius); v2 Dim = Child->ComputedDim + V2(ShadowRadius, ShadowRadius)*2; v4 ShadowColor = V4(0, 0, 0, 0.7); PushQuad(Group, P, Dim, ShadowColor, 0, ShadowRadius, 0); } } for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { UI_DrawBox(Child, Group, GlyphAtlas); } if(Box->Flags & UI_BoxFlag_Clip) { PopClip(Group); } if(Box->Flags & UI_BoxFlag_DrawBorder) { PushQuad(Group, Box->Rect.Min, Box->ComputedDim, Box->BorderColor, Box->CornerRadius, 0.8, Box->BorderThickness); } } static r32 UI_CalculateChildrenSum(ui_box *Box, axis2 Axis, glyph_atlas *Atlas); static r32 UI_CalculateBoxSize(ui_box *Box, axis2 Axis, glyph_atlas *Atlas) { r32 Result = 0; ui_box *Parent = Box->Parent; switch(Box->SemanticSize[Axis].Type) { case UI_SizeType_Pixels: { Result = Box->SemanticSize[Axis].Value; } break; case UI_SizeType_TextContent: { Result = ((Axis == Axis2_X) ? CalculateRasterizedTextWidth(Atlas, Box->Font, Box->FontSize, Box->String) : CalculateRasterizedTextHeight(Atlas, Box->Font, Box->FontSize, Box->String)) + Box->SemanticSize[Axis].Value; } break; case UI_SizeType_PercentOfParent: { if(Parent && Parent->SemanticSize[Axis].Type != UI_SizeType_ChildrenSum) { Result = Parent->ComputedDim.E[Axis]*Box->SemanticSize[Axis].Value; } else { // sixten(NOTE): A box that depends on the size of the parent can not be allowed if the parent depends // on the children sum. InvalidCodepath; } } break; case UI_SizeType_ChildrenSum: { Result = UI_CalculateChildrenSum(Box, Axis, Atlas)*Box->SemanticSize[Axis].Value; } break; InvalidDefaultCase; } return(Result); } static r32 UI_CalculateChildrenSum(ui_box *Box, axis2 Axis, glyph_atlas *Atlas) { r32 Result = 0; if(Box->LayoutAxis == Axis) { for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { Result += UI_CalculateBoxSize(Child, Axis, Atlas); } } else { for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { Result = Max(Result, UI_CalculateBoxSize(Child, Axis, Atlas)); } } return(Result); } static void UI_LayoutBox(ui_box *Box, axis2 Axis, glyph_atlas *Atlas) { for(ui_box *Child = Box->First; Child != 0; Child = Child->Next) { Child->ComputedDim.E[Axis] = UI_CalculateBoxSize(Child, Axis, Atlas); } UI_SolveSizeViolations(Box, Axis); for(ui_box *Child = Box->First; Child; Child = Child->Next) { Child->Rect.Min.E[Axis] = Box->Rect.Min.E[Axis] + Child->ComputedRelativeP.E[Axis]; Child->Rect.Max.E[Axis] = Child->Rect.Min.E[Axis] + Child->ComputedDim.E[Axis]; UI_LayoutBox(Child, Axis, Atlas); } } static void UI_BeginBuild(v2 ScreenDim) { ui *UI = UI_GetState(); UI_PushParent(0); UI_PushWidth(UI_Pixels(ScreenDim.x, 1)); UI_PushHeight(UI_Pixels(ScreenDim.y, 1)); UI_PushFixedX(0); UI_PushFixedY(0); UI_PushTextColor(Theme_TextColor); UI_PushBackgroundColor(ColorFromHex(0x111111FF)); UI_PushBorderColor(Theme_BorderColor); UI_PushBorderThickness(1.8); UI_PushLayoutAxis(Axis2_Y); UI_PushCornerRadius(0); UI_PushFont(Font_Regular); UI_PushFontSize(15.0f); UI->RootNode = UI_MakeBox(UI_BoxFlag_DrawBackground, StrLit("UI Root Node")); UI->Stacks.ParentStack[0] = UI->RootNode; UI->ContainerNode = UI_MakeBox(UI_BoxFlag_Clickable, StrLit("UI Container Node")); UI->TooltipNode = UI_MakeBox(UI_BoxFlag_FloatingX|UI_BoxFlag_FloatingY, StrLit("UI Tooltip Node")); UI->Stacks.ParentStack[0] = UI->ContainerNode; UI->NextHotSet = false; } static void UI_EndBuild(glyph_atlas *GlyphAtlas) { ui *UI = UI_GetState(); UI_SignalFromBox(UI_GetState()->ContainerNode); if(UI->NextHotSet) { UI->Hot = UI->NextHot; } UI_PopParent(); UI_PopWidth(); UI_PopHeight(); UI_PopFixedX(); UI_PopFixedY(); UI_PopTextColor(); UI_PopBackgroundColor(); UI_PopBorderColor(); UI_PopBorderThickness(); UI_PopLayoutAxis(); UI_PopCornerRadius(); UI_PopFont(); UI_PopFontSize(); UI_LayoutBox(UI->RootNode, Axis2_X, GlyphAtlas); UI_LayoutBox(UI->RootNode, Axis2_Y, GlyphAtlas); } static void UI_RenderFrame(render_group *Group, glyph_atlas *GlyphAtlas) { ui *UI = UI_GetState(); UI_DrawBox(UI->RootNode, Group, GlyphAtlas); } inline void UI_ScanForHotAndActive(ui_box *Box, b32 *FoundHot, b32 *FoundActive) { ui *UI = UI_GetState(); if(AreEqual(UI->Hot, Box->Key) && (Box->Flags & UI_BoxFlag_Clickable)) { *FoundHot = true; } if(AreEqual(UI->Active, Box->Key) && (Box->Flags & UI_BoxFlag_Clickable)) { *FoundActive = true; } if(Box->First) { UI_ScanForHotAndActive(Box->First, FoundHot, FoundActive); } if(Box->Next) { UI_ScanForHotAndActive(Box->Next, FoundHot, FoundActive); } } static void UI_NewFrame(ui *UI, platform_event_list *EventList, v2 MouseP) { UI_SetState(UI); if(UI->FrameMemory.Arena) { EndTemporaryMemory(UI->FrameMemory); } UI->FrameMemory = BeginTemporaryMemory(&UI->FrameArena); UI->EventList = EventList; UI->MouseP = MouseP; // sixten: Make sure that the hot and active boxes are valid. if(UI->RootNode) { b32 FoundHot = false; b32 FoundActive = false; UI_ScanForHotAndActive(UI->RootNode, &FoundHot, &FoundActive); // sixten(TODO): Do we inform the builder code about this somehow? if(!FoundHot) { UI->Hot = UI_EmptyKey(); } if(!FoundActive) { UI->Active = UI_EmptyKey(); } } // sixten: Prune uncached boxes. ui_box_bucket *FirstBucket = UI->BoxBuckets; for(ui_box *Box = FirstBucket->First; Box != 0;) { if(AreEqual(Box->Key, UI_EmptyKey())) { ui_box *ToRemove = Box; Box = Box->HashNext; DLLRemove_NP(FirstBucket->First, FirstBucket->Last, ToRemove, HashNext, HashPrev); *ToRemove = {}; DLLInsertLast_NP(UI->FirstFreeBox, UI->LastFreeBox, ToRemove, HashNext, HashPrev); } else { Box = Box->HashNext; } } // sixten: Prune any unused boxes. for(s32 BucketIndex = 0; BucketIndex < ArrayCount(UI->BoxBuckets); ++BucketIndex) { ui_box_bucket *Bucket = UI->BoxBuckets + BucketIndex; for(ui_box *Box = Bucket->First; Box != 0;) { if(Box->LastFrameTouched != UI->CurrentFrame) { ui_box *ToRemove = Box; Box = Box->HashNext; DLLRemove_NP(Bucket->First, Bucket->Last, ToRemove, HashNext, HashPrev); *ToRemove = {}; DLLInsertLast_NP(UI->FirstFreeBox, UI->LastFreeBox, ToRemove, HashNext, HashPrev); } else { Box = Box->HashNext; } } } ++UI->CurrentFrame; }