//- sixten: Rows and columns. inline void UI_RowBegin(u32 Flags, string Name) { UI_SetNextLayoutAxis(Axis2_X); ui_box *Box = UI_MakeBox(Flags, Name); UI_PushParent(Box); } inline void UI_RowEnd(void) { UI_PopParent(); } inline void UI_ColumnBegin(u32 Flags, string Name) { UI_SetNextLayoutAxis(Axis2_Y); ui_box *Box = UI_MakeBox(Flags, Name); UI_PushParent(Box); } inline void UI_ColumnEnd(void) { UI_PopParent(); } //- sixten: Compositions inline void UI_PushAxisSize(axis2 Axis, ui_size Size) { if(Axis == Axis2_X) { UI_PushWidth(Size); } else { UI_PushHeight(Size); } } inline void UI_PopAxisSize(axis2 Axis) { if(Axis == Axis2_X) { UI_PopWidth(); } else { UI_PopHeight(); } } inline void UI_SetNextAxisSize(axis2 Axis, ui_size Size) { if(Axis == Axis2_X) { UI_SetNextWidth(Size); } else { UI_SetNextHeight(Size); } } //- sixten: Spacing static ui_box *UI_NamedSpacer(ui_size Size, string String) { ui_box *Parent = UI_TopParent(); UI_SetNextAxisSize(Parent->LayoutAxis, Size); ui_box *Box = UI_MakeBox(0, String); return(Box); } static ui_box *UI_NamedSpacerF(ui_size Size, char *Format, ...) { temp Scratch = GetScratch(0, 0); va_list Arguments; va_start(Arguments, Format); string String = PushFormatVariadic(Scratch.Arena, Format, Arguments); va_end(Arguments); ui_box *Box = UI_NamedSpacer(Size, String); ReleaseScratch(Scratch); return(Box); } static ui_box *UI_Spacer(ui_size Size) { return(UI_NamedSpacer(Size, StrLit(""))); } //- sixten: Scrollable regions //- sixten: Common widgets static ui_box *UI_Label(string String) { ui_box *Box = UI_MakeBox(UI_BoxFlag_DrawText, String); return(Box); } static ui_box *UI_LabelF(char *Format, ...) { temp Scratch = GetScratch(0, 0); va_list Arguments; va_start(Arguments, Format); string String = PushFormatVariadic(Scratch.Arena, Format, Arguments); va_end(Arguments); ui_box *Box = UI_MakeBox(UI_BoxFlag_DrawText, String); ReleaseScratch(Scratch); return(Box); } static ui_signal UI_Button(string String) { UI_SetNextHoverCursor(PlatformCursor_Hand); ui_box *Box = UI_MakeBox(UI_BoxFlag_DrawText| UI_BoxFlag_DrawBackground| UI_BoxFlag_DrawBorder| UI_BoxFlag_HotAnimation| UI_BoxFlag_ActiveAnimation| UI_BoxFlag_Clickable, String); ui_signal Signal = UI_SignalFromBox(Box); return(Signal); } static ui_signal UI_ButtonF(char *Format, ...) { temp Scratch = GetScratch(0, 0); UI_SetNextHoverCursor(PlatformCursor_Hand); va_list Arguments; va_start(Arguments, Format); string String = PushFormatVariadic(Scratch.Arena, Format, Arguments); va_end(Arguments); ui_box *Box = UI_MakeBox(UI_BoxFlag_DrawText| UI_BoxFlag_DrawBackground| UI_BoxFlag_DrawBorder| UI_BoxFlag_HotAnimation| UI_BoxFlag_ActiveAnimation| UI_BoxFlag_Clickable, String); ui_signal Signal = UI_SignalFromBox(Box); ReleaseScratch(Scratch); return(Signal); } static ui_signal UI_Checkbox(b32 *Checked, string String) { UI_SetNextSize(UI_ChildrenSum(1, 1), UI_ChildrenSum(1, 1)); UI_SetNextLayoutAxis(Axis2_X); UI_SetNextHoverCursor(PlatformCursor_Hand); ui_box *ContainerBox = UI_MakeBox(UI_BoxFlag_Clickable, String); UI_Parent(ContainerBox) { r32 OpacityTransition = AC_AnimateValueF(*Checked, *Checked, 0.15, "UI Checkbox Transition %p", Checked); v4_r32 TextColor = UI_TopTextColor(); TextColor.a = OpacityTransition; UI_CornerRadius(2) UI_Size(UI_Em(1, 1), UI_Em(1, 1)) UI_Font(Font_Icons) UI_TextColor(TextColor) UI_MakeBoxF(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawText, "%U", FontIcon_Cancel); UI_Size(UI_TextContent(15, 1), UI_Em(1, 1)) UI_Label(String); } ui_signal Signal = UI_SignalFromBox(ContainerBox); if(Signal.Pressed) { *Checked = !*Checked; } return(Signal); } //- sixten: Scrollable regions static ui_signal UI_Scrollbar(axis2 Axis, string Name, r32 Size, r32 Offset) { ui_signal Signal; UI_SetNextAxisSize(Axis, UI_Percent(1, 0)); UI_SetNextAxisSize(Opposite(Axis), UI_Em(1, 1)); UI_SetNextBackgroundColor(Darken(UI_TopBackgroundColor(), 2)); UI_SetNextLayoutAxis(Axis); UI_Parent(UI_MakeBox(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground, Name)) { UI_SetNextSize(UI_Em(1, 1), UI_Em(1, 1)); UI_SetNextFont(Font_Icons); UI_MakeBoxF(UI_BoxFlag_DrawText, "%U", Axis?FontIcon_UpDir:FontIcon_LeftDir); Offset = Clamp(Offset, 0, DimOfRange(UI_TopParent()->Rect).E[Axis]-Size-UI_TopFontSize()*2); UI_Spacer(UI_Pixels(Offset, 1)); UI_SetNextCornerRadius(4.0f); UI_SetNextAxisSize(Axis, UI_Pixels(Size, 1)); UI_SetNextAxisSize(Opposite(Axis), UI_Percent(1, 1)); Signal = UI_SignalFromBox(UI_MakeBox(UI_BoxFlag_DrawBorder | UI_BoxFlag_DrawBackground | UI_BoxFlag_Clickable | UI_BoxFlag_HotAnimation | UI_BoxFlag_ActiveAnimation, StrLit("Slider"))); UI_Spacer(UI_Percent(1, 0)); UI_SetNextSize(UI_Em(1, 1), UI_Em(1, 1)); UI_SetNextFont(Font_Icons); UI_MakeBoxF(UI_BoxFlag_DrawText, "%U", Axis?FontIcon_DownDir:FontIcon_RightDir); } return(Signal); } static void UI_ScrollBegin(r32 *X, r32 *Y, ui_box_flags Flags, string Name) { b32 AllowOnX = (X != 0); b32 AllowOnY = (Y != 0); UI_RowBegin(Flags, Name); { UI_SetNextSize(UI_Percent(1, 0), UI_Percent(1, 0)); UI_ColumnBegin(); { u32 ScrollFlags = 0; if(AllowOnX) { ScrollFlags |= UI_BoxFlag_OverflowX; UI_SetNextOffsetX(-AC_AnimateValueF(*X, *X, 0.2f, "Scroll %S X", Name)); } if(AllowOnY) { ScrollFlags |= UI_BoxFlag_OverflowY; UI_SetNextOffsetY(-AC_AnimateValueF(*Y, *Y, 0.2f, "Scroll %S Y", Name)); } UI_SetNextSize(AllowOnX?UI_ChildrenSum(1, 1):UI_Percent(1, 0), AllowOnY?UI_ChildrenSum(1, 1):UI_Percent(1, 0)); ui_box *ScrollableBox = UI_MakeBox(ScrollFlags, StrLit("Scrollable Box")); UI_PushParent(ScrollableBox); } } } static void UI_ScrollEnd(r32 *X, r32 *Y, ui_box_flags Flags, string Name) { b32 AllowOnX = (X != 0); b32 AllowOnY = (Y != 0); r32 ScrollAmount = UI_TopFontSize()*4.0f; ui_box *ScrollableBox = UI_TopParent(); ui_signal ScrollableBoxSignal = UI_SignalFromBox(ScrollableBox); { { UI_PopParent(); if(AllowOnX) { r32 TotalWidth = UI_CalculateBoxSize(ScrollableBox, Axis2_X); r32 ViewWidth = UI_CalculateBoxSize(ScrollableBox->Parent->Parent, Axis2_X); if(ViewWidth / TotalWidth < 1) { r32 TotalScrollWidth = ViewWidth - ScrollableBox->FontSize*2; r32 ScrollScale = TotalScrollWidth / TotalWidth; // sixten(TODO): Add a max of 1 em, and figure out the associated algebra. r32 ScrollWidth = ViewWidth*ScrollScale; r32 ScrollOffset = (*X)*ScrollScale; ui_signal Signal = UI_Scrollbar(Axis2_X, StrLit("Scroll X"), ScrollWidth, ScrollOffset); { if(Signal.Dragging) { if(Signal.Pressed) { UI_StoreDragR32(*X); } r32 StartOffset = UI_GetDragR32(); r32 EndOffset = StartOffset + Signal.DragDelta.x/ScrollScale; *X = EndOffset; } else { for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if(Event->Type == PlatformEvent_MouseScroll && Event->Scroll.x != 0) { *X -= Event->Scroll.x*ScrollAmount; Platform_ConsumeEvent(UI_EventList(), Event); } } } } } *X = Clamp(*X, 0, Max(0.0, TotalWidth - ViewWidth)); } } UI_ColumnEnd(); if(AllowOnY) { UI_SetNextSize(UI_ChildrenSum(1, 1), UI_Percent(1, 0)); UI_Column() { r32 TotalHeight = UI_CalculateBoxSize(ScrollableBox, Axis2_Y); r32 ViewHeight = DimOfRange(ScrollableBox->Parent->Parent->Rect).y;//UI_CalculateBoxSize(ScrollableBox->Parent->Parent, Axis2_Y); if(ViewHeight / TotalHeight < 1) { r32 TotalScrollHeight = ViewHeight - ScrollableBox->FontSize*2; r32 ScrollScale = TotalScrollHeight / TotalHeight; // sixten(TODO): Add a max of 1 em, and figure out the associated algebra. r32 ScrollHeight = ViewHeight*ScrollScale; r32 ScrollOffset = (*Y)*ScrollScale; ui_signal Signal = UI_Scrollbar(Axis2_Y, StrLit("Scroll Y"), ScrollHeight, ScrollOffset); { if(Signal.Dragging) { if(Signal.Pressed) { UI_StoreDragR32(*Y); } r32 StartOffset = UI_GetDragR32(); r32 EndOffset = StartOffset + Signal.DragDelta.y/ScrollScale; *Y = EndOffset; } else { for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if(Event->Type == PlatformEvent_MouseScroll && Event->Scroll.y != 0) { *Y -= Event->Scroll.y*ScrollAmount; Platform_ConsumeEvent(UI_EventList(), Event); } } } } } *Y = Clamp(*Y, 0, Max(0.0, TotalHeight - ViewHeight)); } // sixten: Add padding if(AllowOnX && AllowOnY) { UI_SetNextSize(UI_Em(1, 1), UI_Em(1, 1)); UI_SetNextBackgroundColor(Darken(UI_TopBackgroundColor(), 2)); UI_MakeBox(UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground, StrLit("Padding")); } } } UI_RowEnd(); } static r32 UI_Slider(r32 Value, range1_r32 Range) { r32 Result = Value; UI_Column() { UI_Spacer(UI_Em(1, 1)); UI_Height(UI_Em(1, 1)) UI_Row() { UI_SetNextWidth(UI_Em(20, 1)); UI_SetNextCornerRadius(UI_TopFontSize()*0.5f); ui_box *Container = UI_MakeBoxF(UI_BoxFlag_DrawBackground|UI_BoxFlag_DrawBorder, "Scrollable"); UI_Parent(Container) { UI_SetNextCornerRadius(UI_TopFontSize()*0.5f); UI_SetNextSize(UI_Em(1, 1), UI_Em(1, 1)); UI_SetNextFixedX((DimOfRange(Container->Rect).x-UI_TopFontSize())*(Value-Range.Min)/DimOfRange(Range)); ui_box *Box = UI_MakeBoxF(UI_BoxFlag_DrawBackground| UI_BoxFlag_DrawBorder| UI_BoxFlag_HotAnimation| UI_BoxFlag_ActiveAnimation| UI_BoxFlag_Clickable| UI_BoxFlag_FloatingX| 0, "Dragable"); ui_signal Signal = UI_SignalFromBox(Box); if(Signal.Dragging) { if(Signal.Pressed) { UI_StoreDragR32(Value); } r32 StartT = UI_GetDragR32(); r32 EndT = StartT + Signal.DragDelta.x/(DimOfRange(Container->Rect).x-UI_TopFontSize()); Result = EndT*DimOfRange(Range)+Range.Min; } } } } return(Result); } static void UI_TooltipLabel(string Label, v2_r32 P) { UI_Tooltip { UI_SetNextSize(UI_TextContent(7, 1), UI_TextContent(3, 1)); UI_CornerRadius(4); UI_SetNextFixedP(P+V2R32(15.0f, 0.0f)); UI_MakeBox(UI_BoxFlag_DrawBorder | UI_BoxFlag_DrawBackground | UI_BoxFlag_DrawText | UI_BoxFlag_DrawDropShadow | UI_BoxFlag_FloatingX | UI_BoxFlag_FloatingY, Label); } } struct ui_line_edit_callback_data { text_edit_state State; b32 Focus; }; UI_CUSTOM_DRAW_CALLBACK(UI_LineEditCallback) { ui_line_edit_callback_data *EditData = (ui_line_edit_callback_data *)Data; s64 ClampedCursor = Clamp(0, Box->String.Count, EditData->State.Cursor); s64 ClampedMark = Clamp(0, Box->String.Count, EditData->State.Mark); string ToCursor = MakeString(Box->String.Data, ClampedCursor); string ToMark = MakeString(Box->String.Data, ClampedMark); r32 TargetCursorX = CalculateRasterizedTextWidth(Atlas, Box->Font, Box->FontSize, ToCursor); r32 TargetMarkerX = CalculateRasterizedTextWidth(Atlas, Box->Font, Box->FontSize, ToMark); r32 CursorX = AC_AnimateValueF(TargetCursorX, 0, 0.175, "UI Input Cursor %p", Box); r32 MarkerX = AC_AnimateValueF(TargetMarkerX, 0, 0.175, "UI Input Mark %p", Box); v2_r32 BoxDim = DimOfRange(Box->Rect); // sixten: Draw selection { v2_r32 Offset = V2R32(7.5, (BoxDim.y - Box->FontSize) / 2); v2_r32 Dim = V2R32(0, Box->FontSize); if(CursorX > MarkerX) { Offset.x += MarkerX; Dim.x = CursorX - MarkerX; } else { Offset.x += CursorX; Dim.x = MarkerX - CursorX; } v2_r32 P = Box->Rect.Min + Offset; v4_r32 Color = V4R32(0.4, 0.7, 0.8, 0.3); PushQuad(Group, Range2R32(P, P+Dim), Color, 0, 0, 0); } // sixten: Draw cursor { range1_r32 CursorSpan = Range1R32(CursorX, TargetCursorX); r32 Height = Box->FontSize + 4; v2_r32 Offset = V2R32(7.5F + CursorSpan.Min, (BoxDim.y - Height) / 2); v2_r32 Dim = V2R32(1.25F + CursorSpan.Max - CursorSpan.Min, Height); r32 FocusT = AC_AnimateValueF(EditData->Focus, 0, 0.175, "UI Input Focus %p", Box); v2_r32 P = Box->Rect.Min + Offset; v4_r32 Color = SetAlpha(V4R32(0.3, 1, 0.3, 0.7), UI_Blink()*FocusT); PushQuad(Group, Range2R32(P, P+Dim), Color, 0, 0, 0); } } // sixten(NOTE): this function only kinda works. it is meant for single line text, if that is any indication of its scope. static s64 UI_TextIndexFromP(ui_box *Box, v2_r32 P) { // sixten: yes, usually I don't like doing this, but hey there is only one glyph atlas used throughout the entire application, nobody will notice ._. glyph_atlas *Atlas = UI_GetState()->GlyphAtlas; s64 Result = 0; // sixten(NOTE): from now on, we ignore the y-axis r32 Dim = CalculateRasterizedTextWidth(UI_GetState()->GlyphAtlas, Box->Font, Box->FontSize, Box->String); r32 Offset = P.x - Box->Rect.Min.x - (Box->Rect.Max.x - Box->Rect.Min.x - Dim)*0.5; r32 Advance = 0; u8 *TextBegin = Box->String.Data; u8 *TextEnd = TextBegin+Box->String.Count; for(u8 *Byte = TextBegin; Byte < TextEnd;) { string_decode Decode = DecodeUTF8Codepoint(Byte, TextEnd-Byte); Byte += Decode.Size; u32 Codepoint = Decode.Codepoint; glyph *Glyph = F_GlyphFromAtlas(Atlas, Box->Font, Codepoint, Box->FontSize*Font_Oversample, F_SubpixelSegmentFromP(Advance*Font_Oversample)); Assert(Glyph); Advance += Glyph->Advance/Font_Oversample; if(Advance > Offset) { break; } else { Result += 1; } } return(Result); } static ui_signal UI_LineEdit(text_edit_state *State, u64 BufferSize, u8 *Buffer, s64 *BufferUsed, string String, b32 Focused) { temp Scratch = GetScratch(); if(Focused) { for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if((Event->Type == PlatformEvent_Press || Event->Type == PlatformEvent_Text) && (Event->Codepoint != '/' && Event->Codepoint != '\\')) { text_action Action = SingleLineTextActionFromEvent(Event); if(IsValid(&Action)) { text_op Op = TextOpFromAction(Scratch.Arena, MakeString(Buffer, *BufferUsed), State, &Action); if(Op.NewCursor >= 0 && Op.NewMark >= 0) { string Left = MakeString(Buffer, Op.Range.Min); string Right = MakeString(Buffer + Op.Range.Max, *BufferUsed - Op.Range.Max); u64 NewStringSize = Left.Count + Right.Count + Op.ReplaceString.Count; char *NewString = PushArray(Scratch.Arena, char, NewStringSize); Copy(NewString, Left.Data, Left.Count); Copy(NewString + Left.Count, Op.ReplaceString.Data, Op.ReplaceString.Count); Copy(NewString + Left.Count + Op.ReplaceString.Count, Right.Data, Right.Count); *BufferUsed = Minimum(BufferSize, NewStringSize); Copy(Buffer, NewString, *BufferUsed); State->Cursor = Minimum(Op.NewCursor, *BufferUsed); State->Mark = Minimum(Op.NewMark, *BufferUsed); Platform_ConsumeEvent(UI_EventList(), Event); UI_ResetBlink(); } } } } } UI_SetNextHoverCursor(PlatformCursor_IBeam); ui_box *Container = UI_MakeBox(UI_BoxFlag_Clip|UI_BoxFlag_DrawBorder|UI_BoxFlag_DrawBackground|UI_BoxFlag_Clickable, String); ui_box *InputBox = 0; UI_Parent(Container) UI_Height(UI_Percent(1, 1)) { UI_SetNextWidth(UI_TextContent(15, 1)); InputBox = UI_MakeBox(UI_BoxFlag_DrawText, StrLit("Text Input")); UI_EquipBoxText(InputBox, MakeString(Buffer, *BufferUsed)); ui_line_edit_callback_data *Data = PushStruct(UI_FrameArena(), ui_line_edit_callback_data); Data->State = *State; Data->Focus = Focused; UI_EquipBoxCustomDrawCallback(InputBox, UI_LineEditCallback, Data); UI_Spacer(UI_Percent(1, 0)); } ReleaseScratch(Scratch); //- sixten: handle mouse dragging ui_signal Signal = UI_SignalFromBox(Container); if(Signal.Dragging) { if(Signal.Pressed) { State->Cursor = State->Mark = UI_TextIndexFromP(InputBox, UI_MouseP()); } else { State->Cursor = UI_TextIndexFromP(InputBox, UI_MouseP()); } } return(Signal); }