991 lines
26 KiB
C++
991 lines
26 KiB
C++
#include "generated/vn_ui.meta.c"
|
|
|
|
static ui_size UI_MakeSize(ui_size_kind Kind, r32 Value, r32 Strictness)
|
|
{
|
|
ui_size Result;
|
|
Result.Kind = Kind;
|
|
Result.Value = Value;
|
|
Result.Strictness = 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_HotKey(void)
|
|
{
|
|
return(UI_GetState()->Hot);
|
|
}
|
|
|
|
inline ui_key UI_ActiveKey(void)
|
|
{
|
|
return(UI_GetState()->Active);
|
|
}
|
|
|
|
inline platform_event_list *UI_EventList(void)
|
|
{
|
|
return(UI_GetState()->EventList);
|
|
}
|
|
|
|
inline arena *UI_FrameArena(void)
|
|
{
|
|
return(UI_GetState()->FrameArena);
|
|
}
|
|
|
|
inline v2_r32 UI_MouseP(void)
|
|
{
|
|
return(UI_GetState()->MouseP);
|
|
}
|
|
|
|
inline glyph_atlas *UI_GlyphAtlas(void)
|
|
{
|
|
return(UI_GetState()->GlyphAtlas);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static r64 UI_Time(void)
|
|
{
|
|
return(UI_GetState()->Time);
|
|
}
|
|
|
|
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_KeyFromString(string String)
|
|
{
|
|
ui_key Key;
|
|
if(String.Count)
|
|
{
|
|
Key.Value = HashString(String);
|
|
}
|
|
else
|
|
{
|
|
Key = UI_EmptyKey();
|
|
}
|
|
|
|
return(Key);
|
|
}
|
|
|
|
static string UI_BoxStringFromKey(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_BoxFromKey(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_KeyFromString(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_BoxFromKey(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;
|
|
}
|
|
}
|
|
|
|
static void UI_EquipBoxText(ui_box *Box, string String)
|
|
{
|
|
Box->String = PushString(UI_FrameArena(), String);
|
|
}
|
|
|
|
static void UI_EquipBoxCustomDrawCallback(ui_box *Box, ui_custom_draw_callback *DrawCallback, void *Data)
|
|
{
|
|
Box->DrawCallback = DrawCallback;
|
|
Box->DrawCallbackData = Data;
|
|
}
|
|
|
|
// sixten(NOTE): ClippingRect = Intersection(TrueClippingRect, Parent->Rect);
|
|
static b32 UI_ChildrenContainsP(ui_box *Parent, v2 P, range2_r32 Clip)
|
|
{
|
|
b32 Result = false;
|
|
|
|
for(ui_box *Child = Parent->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
range2_r32 IntersectionRect = Intersection(Clip, Child->Rect);
|
|
|
|
if(Child->Flags & UI_BoxFlag_Clickable && InRange(IntersectionRect, P))
|
|
{
|
|
Result = true;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if(Child->Flags & UI_BoxFlag_Clip)
|
|
{
|
|
Result = UI_ChildrenContainsP(Child, P, IntersectionRect);
|
|
}
|
|
else
|
|
{
|
|
Result = UI_ChildrenContainsP(Child, P, Clip);
|
|
}
|
|
|
|
if(Result)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(Child == Parent->Last)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return(Result);
|
|
}
|
|
|
|
static ui_signal UI_SignalFromBox(ui_box *Box)
|
|
{
|
|
ui *UI = UI_GetState();
|
|
|
|
ui_signal Signal = {};
|
|
|
|
// sixten: Gather the clipping rects of all the parents.
|
|
range2_r32 ClippedRect = Box->Rect;
|
|
for(ui_box *Parent = Box->Parent;
|
|
Parent != 0;
|
|
Parent = Parent->Parent)
|
|
{
|
|
if(Parent->Flags & UI_BoxFlag_Clip)
|
|
{
|
|
ClippedRect = Intersection(ClippedRect, Parent->Rect);
|
|
}
|
|
}
|
|
|
|
Signal.Hovering = InRange(ClippedRect, UI->MouseP);
|
|
|
|
// sixten: Make sure no children boxes overlap.
|
|
Signal.Hovering &= !UI_ChildrenContainsP(Box, UI->MouseP, ClippedRect);
|
|
|
|
// sixten: Make sure no previous boxes overlap.
|
|
if(Box->Prev)
|
|
{
|
|
ui_box PrevBoxParent = {};
|
|
PrevBoxParent.Rect = Box->Rect;
|
|
PrevBoxParent.First = Box->Parent->First;
|
|
PrevBoxParent.Last = Box->Prev;
|
|
|
|
Signal.Hovering &= !UI_ChildrenContainsP(&PrevBoxParent, UI->MouseP, ClippedRect);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
Signal.Hovering &= ~UI_ChildrenContainsP(UI->TooltipNode, UI->MouseP,
|
|
Range2R32(V2(0, 0), V2(InfinityR32, InfinityR32)));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(AreEqual(UI->Hot, Box->Key))
|
|
{
|
|
Platform.SetCursor(Box->HoverCursor);
|
|
}
|
|
|
|
Signal.MouseP = UI->MouseP;
|
|
Signal.dMouseP = UI->dMouseP;
|
|
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, Box->BackgroundColor, Box->CornerRadius, 0, 0);
|
|
}
|
|
|
|
if(Box->Flags & UI_BoxFlag_HotAnimation)
|
|
{
|
|
v4 Top = V4(1, 1, 1, 0.08F*Box->HotTransition);
|
|
v4 Bottom = V4(1, 1, 1, 0.0);
|
|
|
|
PushQuad(Group, Box->Rect, Top, Top, Bottom, Bottom, Box->CornerRadius, 0, 0);
|
|
}
|
|
|
|
if(Box->Flags & UI_BoxFlag_ActiveAnimation)
|
|
{
|
|
v4 Top = V4(0, 0, 0, 0.7F*Box->ActiveTransition);
|
|
v4 Bottom = V4(0, 0, 0, 0.1F*Box->ActiveTransition);
|
|
|
|
PushQuad(Group, Box->Rect, 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, Red, Red, Red, Red, 0, 1.8, 1.8);
|
|
}
|
|
#endif
|
|
|
|
for(ui_box *Child = Box->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
if(Child->Flags & UI_BoxFlag_DrawDropShadow)
|
|
{
|
|
r32 ShadowRadius = 10;
|
|
|
|
range2_r32 Rect = Range2R32(Child->Rect.Min - V2R32(ShadowRadius, ShadowRadius),
|
|
Child->Rect.Max + V2R32(ShadowRadius, ShadowRadius));
|
|
|
|
v4 ShadowColor = V4(0, 0, 0, 0.3);
|
|
|
|
PushQuad(Group, Rect, ShadowColor, 0, ShadowRadius, 0);
|
|
}
|
|
}
|
|
|
|
if(Box->Flags & UI_BoxFlag_Clip)
|
|
{
|
|
range2_r32 Rect = Intersection(Group->ClipStack[Group->ClipStackUsed], Box->Rect);
|
|
PushClip(Group, Rect);
|
|
}
|
|
|
|
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, Box->BorderColor, Box->CornerRadius, 0.8, Box->BorderThickness);
|
|
}
|
|
|
|
}
|
|
|
|
static r32 UI_CalculateChildrenSum(ui_box *Box, axis2 Axis, b32 ForceCalculate);
|
|
|
|
static r32 UI_CalculateBoxSize(ui_box *Box, axis2 Axis, b32 ForceCalculate = false)
|
|
{
|
|
r32 Result = 0;
|
|
|
|
ui *UI = UI_GetState();
|
|
ui_box *Parent = Box->Parent;
|
|
|
|
switch(Box->SemanticSize[Axis].Kind)
|
|
{
|
|
case UI_SizeKind_Pixels:
|
|
{
|
|
Result = Box->SemanticSize[Axis].Value;
|
|
} break;
|
|
|
|
case UI_SizeKind_TextContent:
|
|
{
|
|
glyph_atlas *Atlas = UI->GlyphAtlas;
|
|
|
|
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_SizeKind_PercentOfParent:
|
|
{
|
|
if(Parent && Parent->SemanticSize[Axis].Kind != UI_SizeKind_ChildrenSum)
|
|
{
|
|
r32 Size = Parent->ComputedDim.E[Axis];
|
|
// sixten: if the size is zero, try to find it.
|
|
if(Size == 0.0)
|
|
{
|
|
if(ForceCalculate)
|
|
{
|
|
Size = UI_CalculateBoxSize(Parent, Axis, ForceCalculate);
|
|
}
|
|
else
|
|
{
|
|
Size = DimOfRange(Parent->Rect).E[Axis];
|
|
}
|
|
}
|
|
Result = Size*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_SizeKind_ChildrenSum:
|
|
{
|
|
Result = UI_CalculateChildrenSum(Box, Axis, ForceCalculate)*Box->SemanticSize[Axis].Value;
|
|
} break;
|
|
|
|
InvalidDefaultCase;
|
|
}
|
|
|
|
return(Result);
|
|
}
|
|
|
|
static v2_r32 UI_CalculateBoxDim(ui_box *Box)
|
|
{
|
|
v2_r32 Result = V2R32(UI_CalculateBoxSize(Box, Axis2_X, true), UI_CalculateBoxSize(Box, Axis2_Y, true));
|
|
return(Result);
|
|
}
|
|
|
|
static r32 UI_CalculateChildrenSum(ui_box *Box, axis2 Axis, b32 ForceCalculate = false)
|
|
{
|
|
r32 Result = 0;
|
|
|
|
if(Box->LayoutAxis == Axis)
|
|
{
|
|
for(ui_box *Child = Box->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
Result += UI_CalculateBoxSize(Child, Axis, ForceCalculate);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(ui_box *Child = Box->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
Result = Max(Result, UI_CalculateBoxSize(Child, Axis, ForceCalculate));
|
|
}
|
|
}
|
|
return(Result);
|
|
}
|
|
|
|
static void UI_LayoutBox(ui_box *Box)
|
|
{
|
|
for(ui_box *Child = Box->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
Child->ComputedDim.E[Axis2_X] = UI_CalculateBoxSize(Child, Axis2_X);
|
|
Child->ComputedDim.E[Axis2_Y] = UI_CalculateBoxSize(Child, Axis2_Y);
|
|
}
|
|
|
|
UI_SolveSizeViolations(Box, Axis2_X);
|
|
UI_SolveSizeViolations(Box, Axis2_Y);
|
|
|
|
for(ui_box *Child = Box->First;
|
|
Child != 0;
|
|
Child = Child->Next)
|
|
{
|
|
if(Child->Flags & UI_BoxFlag_AnimatePosition)
|
|
{
|
|
Child->ApproachingRelativeP.x = AC_AnimateValueF(Child->ComputedRelativeP.x, Child->ComputedRelativeP.x, 0.5, "Box P.X %p", Child);
|
|
Child->ApproachingRelativeP.y = AC_AnimateValueF(Child->ComputedRelativeP.y, Child->ComputedRelativeP.y, 0.5, "Box P.Y %p", Child);
|
|
|
|
Child->Rect.Min = Box->Rect.Min + Child->ApproachingRelativeP + Box->Offset;
|
|
Child->Rect.Max = Child->Rect.Min + Child->ComputedDim;
|
|
}
|
|
else
|
|
{
|
|
Child->Rect.Min = Box->Rect.Min + Child->ComputedRelativeP + Box->Offset;
|
|
Child->Rect.Max = Child->Rect.Min + Child->ComputedDim;
|
|
}
|
|
UI_LayoutBox(Child);
|
|
}
|
|
}
|
|
|
|
static void UI_Init(ui *UI)
|
|
{
|
|
UI->Arena = ArenaAlloc(Kilobytes(4), true);
|
|
UI->FrameArena = ArenaAlloc(Kilobytes(16), true);
|
|
}
|
|
|
|
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_PushOffsetX(0);
|
|
UI_PushOffsetY(0);
|
|
UI_PushHoverCursor(PlatformCursor_Arrow);
|
|
|
|
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(void)
|
|
{
|
|
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_PopOffsetX();
|
|
UI_PopOffsetY();
|
|
UI_PopHoverCursor();
|
|
|
|
UI_LayoutBox(UI->RootNode);
|
|
}
|
|
|
|
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, r32 dtForFrame, glyph_atlas *GlyphAtlas)
|
|
{
|
|
UI_SetState(UI);
|
|
|
|
ArenaClear(UI->FrameArena);
|
|
|
|
UI->EventList = EventList;
|
|
UI->dMouseP = MouseP - UI->MouseP;
|
|
UI->MouseP = MouseP;
|
|
UI->Time += dtForFrame;
|
|
|
|
UI->GlyphAtlas = GlyphAtlas;
|
|
|
|
// 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;
|
|
}
|