vn/code/vn_ui.cpp

886 lines
22 KiB
C++

#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<<Axis)))
{
for(ui_box *Child = Box->First;
Child != 0;
Child = Child->Next)
{
if(!(Child->Flags & (UI_BoxFlag_FloatingX<<Axis)))
{
if(Axis == Box->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<<Axis)))
{
r32 ChildFixupBudget = Child->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<<Axis)))
{
Child->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<<Axis)))
{
Child->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;
}