1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "ui.h"
4#include "ui_scrollregion.h"
5
6#include <base/math.h>
7#include <base/system.h>
8
9#include <engine/client.h>
10#include <engine/graphics.h>
11#include <engine/input.h>
12#include <engine/keys.h>
13#include <engine/shared/config.h>
14
15#include <game/localization.h>
16
17#include <limits>
18
19using namespace FontIcons;
20
21void CUIElement::Init(CUi *pUI, int RequestedRectCount)
22{
23 m_pUI = pUI;
24 pUI->AddUIElement(pElement: this);
25 if(RequestedRectCount > 0)
26 InitRects(RequestedRectCount);
27}
28
29void CUIElement::InitRects(int RequestedRectCount)
30{
31 dbg_assert(m_vUIRects.empty(), "UI rects can only be initialized once, create another ui element instead.");
32 m_vUIRects.resize(new_size: RequestedRectCount);
33 for(auto &Rect : m_vUIRects)
34 Rect.m_pParent = this;
35}
36
37CUIElement::SUIElementRect::SUIElementRect() { Reset(); }
38
39void CUIElement::SUIElementRect::Reset()
40{
41 m_UIRectQuadContainer = -1;
42 m_UITextContainer.Reset();
43 m_X = -1;
44 m_Y = -1;
45 m_Width = -1;
46 m_Height = -1;
47 m_Rounding = -1.0f;
48 m_Corners = -1;
49 m_Text.clear();
50 m_Cursor.Reset();
51 m_TextColor = ColorRGBA(-1, -1, -1, -1);
52 m_TextOutlineColor = ColorRGBA(-1, -1, -1, -1);
53 m_QuadColor = ColorRGBA(-1, -1, -1, -1);
54 m_ReadCursorGlyphCount = -1;
55}
56
57void CUIElement::SUIElementRect::Draw(const CUIRect *pRect, ColorRGBA Color, int Corners, float Rounding)
58{
59 bool NeedsRecreate = false;
60 if(m_UIRectQuadContainer == -1 || m_Width != pRect->w || m_Height != pRect->h || mem_comp(a: &m_QuadColor, b: &Color, size: sizeof(Color)) != 0)
61 {
62 m_pParent->Ui()->Graphics()->DeleteQuadContainer(ContainerIndex&: m_UIRectQuadContainer);
63 NeedsRecreate = true;
64 }
65 m_X = pRect->x;
66 m_Y = pRect->y;
67 if(NeedsRecreate)
68 {
69 m_Width = pRect->w;
70 m_Height = pRect->h;
71 m_QuadColor = Color;
72
73 m_pParent->Ui()->Graphics()->SetColor(Color);
74 m_UIRectQuadContainer = m_pParent->Ui()->Graphics()->CreateRectQuadContainer(x: 0, y: 0, w: pRect->w, h: pRect->h, r: Rounding, Corners);
75 m_pParent->Ui()->Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
76 }
77
78 m_pParent->Ui()->Graphics()->TextureClear();
79 m_pParent->Ui()->Graphics()->RenderQuadContainerEx(ContainerIndex: m_UIRectQuadContainer,
80 QuadOffset: 0, QuadDrawNum: -1, X: m_X, Y: m_Y, ScaleX: 1, ScaleY: 1);
81}
82
83/********************************************************
84 UI
85*********************************************************/
86
87const CLinearScrollbarScale CUi::ms_LinearScrollbarScale;
88const CLogarithmicScrollbarScale CUi::ms_LogarithmicScrollbarScale(25);
89const CDarkButtonColorFunction CUi::ms_DarkButtonColorFunction;
90const CLightButtonColorFunction CUi::ms_LightButtonColorFunction;
91const CScrollBarColorFunction CUi::ms_ScrollBarColorFunction;
92const float CUi::ms_FontmodHeight = 0.8f;
93
94CUi *CUIElementBase::s_pUI = nullptr;
95
96IClient *CUIElementBase::Client() const { return s_pUI->Client(); }
97IGraphics *CUIElementBase::Graphics() const { return s_pUI->Graphics(); }
98IInput *CUIElementBase::Input() const { return s_pUI->Input(); }
99ITextRender *CUIElementBase::TextRender() const { return s_pUI->TextRender(); }
100
101void CUi::Init(IKernel *pKernel)
102{
103 m_pClient = pKernel->RequestInterface<IClient>();
104 m_pGraphics = pKernel->RequestInterface<IGraphics>();
105 m_pInput = pKernel->RequestInterface<IInput>();
106 m_pTextRender = pKernel->RequestInterface<ITextRender>();
107 CUIRect::Init(pGraphics: m_pGraphics);
108 CLineInput::Init(pClient: m_pClient, pGraphics: m_pGraphics, pInput: m_pInput, pTextRender: m_pTextRender);
109 CUIElementBase::Init(pUI: this);
110}
111
112CUi::CUi()
113{
114 m_Enabled = true;
115
116 m_Screen.x = 0.0f;
117 m_Screen.y = 0.0f;
118}
119
120CUi::~CUi()
121{
122 for(CUIElement *&pEl : m_vpOwnUIElements)
123 {
124 delete pEl;
125 }
126 m_vpOwnUIElements.clear();
127}
128
129CUIElement *CUi::GetNewUIElement(int RequestedRectCount)
130{
131 CUIElement *pNewEl = new CUIElement(this, RequestedRectCount);
132
133 m_vpOwnUIElements.push_back(x: pNewEl);
134
135 return pNewEl;
136}
137
138void CUi::AddUIElement(CUIElement *pElement)
139{
140 m_vpUIElements.push_back(x: pElement);
141}
142
143void CUi::ResetUIElement(CUIElement &UIElement) const
144{
145 for(CUIElement::SUIElementRect &Rect : UIElement.m_vUIRects)
146 {
147 Graphics()->DeleteQuadContainer(ContainerIndex&: Rect.m_UIRectQuadContainer);
148 TextRender()->DeleteTextContainer(TextContainerIndex&: Rect.m_UITextContainer);
149 Rect.Reset();
150 }
151}
152
153void CUi::OnElementsReset()
154{
155 for(CUIElement *pEl : m_vpUIElements)
156 {
157 ResetUIElement(UIElement&: *pEl);
158 }
159}
160
161void CUi::OnWindowResize()
162{
163 OnElementsReset();
164}
165
166void CUi::OnCursorMove(float X, float Y)
167{
168 if(!CheckMouseLock())
169 {
170 m_UpdatedMousePos.x = clamp(val: m_UpdatedMousePos.x + X, lo: 0.0f, hi: Graphics()->WindowWidth() - 1.0f);
171 m_UpdatedMousePos.y = clamp(val: m_UpdatedMousePos.y + Y, lo: 0.0f, hi: Graphics()->WindowHeight() - 1.0f);
172 }
173
174 m_UpdatedMouseDelta += vec2(X, Y);
175}
176
177void CUi::Update(vec2 MouseWorldPos)
178{
179 unsigned MouseButtons = 0;
180 if(Enabled())
181 {
182 if(Input()->KeyIsPressed(Key: KEY_MOUSE_1))
183 MouseButtons |= 1;
184 if(Input()->KeyIsPressed(Key: KEY_MOUSE_2))
185 MouseButtons |= 2;
186 if(Input()->KeyIsPressed(Key: KEY_MOUSE_3))
187 MouseButtons |= 4;
188 }
189
190 const CUIRect *pScreen = Screen();
191 m_MousePos = m_UpdatedMousePos * vec2(pScreen->w / Graphics()->WindowWidth(), pScreen->h / Graphics()->WindowHeight());
192 m_MouseDelta = m_UpdatedMouseDelta;
193 m_UpdatedMouseDelta = vec2(0.0f, 0.0f);
194 m_MouseWorldPos = MouseWorldPos;
195 m_LastMouseButtons = m_MouseButtons;
196 m_MouseButtons = MouseButtons;
197
198 m_pHotItem = m_pBecomingHotItem;
199 if(m_pActiveItem)
200 m_pHotItem = m_pActiveItem;
201 m_pBecomingHotItem = nullptr;
202 m_pHotScrollRegion = m_pBecomingHotScrollRegion;
203 m_pBecomingHotScrollRegion = nullptr;
204
205 if(Enabled())
206 {
207 CLineInput *pActiveInput = CLineInput::GetActiveInput();
208 if(pActiveInput && m_pLastActiveItem && pActiveInput != m_pLastActiveItem)
209 pActiveInput->Deactivate();
210 }
211 else
212 {
213 m_pHotItem = nullptr;
214 m_pActiveItem = nullptr;
215 m_pHotScrollRegion = nullptr;
216 }
217
218 m_ProgressSpinnerOffset += Client()->RenderFrameTime() * 1.5f;
219 m_ProgressSpinnerOffset = std::fmod(x: m_ProgressSpinnerOffset, y: 1.0f);
220}
221
222void CUi::DebugRender()
223{
224 MapScreen();
225
226 char aBuf[128];
227 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "hot=%p nexthot=%p active=%p lastactive=%p", HotItem(), NextHotItem(), ActiveItem(), m_pLastActiveItem);
228 TextRender()->Text(x: 2.0f, y: Screen()->h - 12.0f, Size: 10.0f, pText: aBuf);
229}
230
231bool CUi::MouseInside(const CUIRect *pRect) const
232{
233 return pRect->Inside(Point: MousePos());
234}
235
236void CUi::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) const
237{
238 float Factor = 1.0f;
239 switch(CursorType)
240 {
241 case IInput::CURSOR_MOUSE:
242 Factor = g_Config.m_UiMousesens / 100.0f;
243 break;
244 case IInput::CURSOR_JOYSTICK:
245 Factor = g_Config.m_UiControllerSens / 100.0f;
246 break;
247 default:
248 dbg_msg(sys: "assert", fmt: "CUi::ConvertMouseMove CursorType %d", (int)CursorType);
249 dbg_break();
250 break;
251 }
252
253 if(m_MouseSlow)
254 Factor *= 0.05f;
255
256 *pX *= Factor;
257 *pY *= Factor;
258}
259
260bool CUi::ConsumeHotkey(EHotkey Hotkey)
261{
262 const bool Pressed = m_HotkeysPressed & Hotkey;
263 m_HotkeysPressed &= ~Hotkey;
264 return Pressed;
265}
266
267bool CUi::OnInput(const IInput::CEvent &Event)
268{
269 if(!Enabled())
270 return false;
271
272 CLineInput *pActiveInput = CLineInput::GetActiveInput();
273 if(pActiveInput && pActiveInput->ProcessInput(Event))
274 return true;
275
276 if(Event.m_Flags & IInput::FLAG_PRESS)
277 {
278 unsigned LastHotkeysPressed = m_HotkeysPressed;
279 if(Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER)
280 m_HotkeysPressed |= HOTKEY_ENTER;
281 else if(Event.m_Key == KEY_ESCAPE)
282 m_HotkeysPressed |= HOTKEY_ESCAPE;
283 else if(Event.m_Key == KEY_TAB && !Input()->AltIsPressed())
284 m_HotkeysPressed |= HOTKEY_TAB;
285 else if(Event.m_Key == KEY_DELETE)
286 m_HotkeysPressed |= HOTKEY_DELETE;
287 else if(Event.m_Key == KEY_UP)
288 m_HotkeysPressed |= HOTKEY_UP;
289 else if(Event.m_Key == KEY_DOWN)
290 m_HotkeysPressed |= HOTKEY_DOWN;
291 else if(Event.m_Key == KEY_MOUSE_WHEEL_UP)
292 m_HotkeysPressed |= HOTKEY_SCROLL_UP;
293 else if(Event.m_Key == KEY_MOUSE_WHEEL_DOWN)
294 m_HotkeysPressed |= HOTKEY_SCROLL_DOWN;
295 else if(Event.m_Key == KEY_PAGEUP)
296 m_HotkeysPressed |= HOTKEY_PAGE_UP;
297 else if(Event.m_Key == KEY_PAGEDOWN)
298 m_HotkeysPressed |= HOTKEY_PAGE_DOWN;
299 else if(Event.m_Key == KEY_HOME)
300 m_HotkeysPressed |= HOTKEY_HOME;
301 else if(Event.m_Key == KEY_END)
302 m_HotkeysPressed |= HOTKEY_END;
303 return LastHotkeysPressed != m_HotkeysPressed;
304 }
305 return false;
306}
307
308float CUi::ButtonColorMul(const void *pId)
309{
310 if(CheckActiveItem(pId))
311 return ButtonColorMulActive();
312 else if(HotItem() == pId)
313 return ButtonColorMulHot();
314 return ButtonColorMulDefault();
315}
316
317const CUIRect *CUi::Screen()
318{
319 m_Screen.h = 600.0f;
320 m_Screen.w = Graphics()->ScreenAspect() * m_Screen.h;
321 return &m_Screen;
322}
323
324void CUi::MapScreen()
325{
326 const CUIRect *pScreen = Screen();
327 Graphics()->MapScreen(TopLeftX: pScreen->x, TopLeftY: pScreen->y, BottomRightX: pScreen->w, BottomRightY: pScreen->h);
328}
329
330float CUi::PixelSize()
331{
332 return Screen()->w / Graphics()->ScreenWidth();
333}
334
335void CUi::ClipEnable(const CUIRect *pRect)
336{
337 if(IsClipped())
338 {
339 const CUIRect *pOldRect = ClipArea();
340 CUIRect Intersection;
341 Intersection.x = std::max(a: pRect->x, b: pOldRect->x);
342 Intersection.y = std::max(a: pRect->y, b: pOldRect->y);
343 Intersection.w = std::min(a: pRect->x + pRect->w, b: pOldRect->x + pOldRect->w) - pRect->x;
344 Intersection.h = std::min(a: pRect->y + pRect->h, b: pOldRect->y + pOldRect->h) - pRect->y;
345 m_vClips.push_back(x: Intersection);
346 }
347 else
348 {
349 m_vClips.push_back(x: *pRect);
350 }
351 UpdateClipping();
352}
353
354void CUi::ClipDisable()
355{
356 dbg_assert(IsClipped(), "no clip region");
357 m_vClips.pop_back();
358 UpdateClipping();
359}
360
361const CUIRect *CUi::ClipArea() const
362{
363 dbg_assert(IsClipped(), "no clip region");
364 return &m_vClips.back();
365}
366
367void CUi::UpdateClipping()
368{
369 if(IsClipped())
370 {
371 const CUIRect *pRect = ClipArea();
372 const float XScale = Graphics()->ScreenWidth() / Screen()->w;
373 const float YScale = Graphics()->ScreenHeight() / Screen()->h;
374 Graphics()->ClipEnable(x: (int)(pRect->x * XScale), y: (int)(pRect->y * YScale), w: (int)(pRect->w * XScale), h: (int)(pRect->h * YScale));
375 }
376 else
377 {
378 Graphics()->ClipDisable();
379 }
380}
381
382int CUi::DoButtonLogic(const void *pId, int Checked, const CUIRect *pRect)
383{
384 // logic
385 int ReturnValue = 0;
386 const bool Inside = MouseHovered(pRect);
387
388 if(CheckActiveItem(pId))
389 {
390 dbg_assert(m_ActiveButtonLogicButton >= 0, "m_ActiveButtonLogicButton invalid");
391 if(!MouseButton(Index: m_ActiveButtonLogicButton))
392 {
393 if(Inside && Checked >= 0)
394 ReturnValue = 1 + m_ActiveButtonLogicButton;
395 SetActiveItem(nullptr);
396 m_ActiveButtonLogicButton = -1;
397 }
398 }
399 else if(HotItem() == pId)
400 {
401 for(int i = 0; i < 3; ++i)
402 {
403 if(MouseButton(Index: i))
404 {
405 SetActiveItem(pId);
406 m_ActiveButtonLogicButton = i;
407 }
408 }
409 }
410
411 if(Inside && !MouseButton(Index: 0) && !MouseButton(Index: 1) && !MouseButton(Index: 2))
412 SetHotItem(pId);
413
414 return ReturnValue;
415}
416
417int CUi::DoDraggableButtonLogic(const void *pId, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted)
418{
419 // logic
420 int ReturnValue = 0;
421 const bool Inside = MouseHovered(pRect);
422
423 if(pClicked != nullptr)
424 *pClicked = false;
425 if(pAbrupted != nullptr)
426 *pAbrupted = false;
427
428 if(CheckActiveItem(pId))
429 {
430 dbg_assert(m_ActiveDraggableButtonLogicButton >= 0, "m_ActiveDraggableButtonLogicButton invalid");
431 if(m_ActiveDraggableButtonLogicButton == 0)
432 {
433 if(Checked >= 0)
434 ReturnValue = 1 + m_ActiveDraggableButtonLogicButton;
435 if(!MouseButton(Index: m_ActiveDraggableButtonLogicButton))
436 {
437 if(pClicked != nullptr)
438 *pClicked = true;
439 SetActiveItem(nullptr);
440 m_ActiveDraggableButtonLogicButton = -1;
441 }
442 if(MouseButton(Index: 1))
443 {
444 if(pAbrupted != nullptr)
445 *pAbrupted = true;
446 SetActiveItem(nullptr);
447 m_ActiveDraggableButtonLogicButton = -1;
448 }
449 }
450 else if(!MouseButton(Index: m_ActiveDraggableButtonLogicButton))
451 {
452 if(Inside && Checked >= 0)
453 ReturnValue = 1 + m_ActiveDraggableButtonLogicButton;
454 if(pClicked != nullptr)
455 *pClicked = true;
456 SetActiveItem(nullptr);
457 m_ActiveDraggableButtonLogicButton = -1;
458 }
459 }
460 else if(HotItem() == pId)
461 {
462 for(int i = 0; i < 3; ++i)
463 {
464 if(MouseButton(Index: i))
465 {
466 SetActiveItem(pId);
467 m_ActiveDraggableButtonLogicButton = i;
468 }
469 }
470 }
471
472 if(Inside && !MouseButton(Index: 0) && !MouseButton(Index: 1) && !MouseButton(Index: 2))
473 SetHotItem(pId);
474
475 return ReturnValue;
476}
477
478bool CUi::DoDoubleClickLogic(const void *pId)
479{
480 if(m_DoubleClickState.m_pLastClickedId == pId &&
481 Client()->GlobalTime() - m_DoubleClickState.m_LastClickTime < 0.5f &&
482 distance(a: m_DoubleClickState.m_LastClickPos, b: MousePos()) <= 32.0f * Screen()->h / Graphics()->ScreenHeight())
483 {
484 m_DoubleClickState.m_pLastClickedId = nullptr;
485 return true;
486 }
487 m_DoubleClickState.m_pLastClickedId = pId;
488 m_DoubleClickState.m_LastClickTime = Client()->GlobalTime();
489 m_DoubleClickState.m_LastClickPos = MousePos();
490 return false;
491}
492
493EEditState CUi::DoPickerLogic(const void *pId, const CUIRect *pRect, float *pX, float *pY)
494{
495 if(MouseHovered(pRect))
496 SetHotItem(pId);
497
498 EEditState Res = EEditState::EDITING;
499
500 if(HotItem() == pId && MouseButtonClicked(Index: 0))
501 {
502 SetActiveItem(pId);
503 if(!m_pLastEditingItem)
504 {
505 m_pLastEditingItem = pId;
506 Res = EEditState::START;
507 }
508 }
509
510 if(CheckActiveItem(pId) && !MouseButton(Index: 0))
511 {
512 SetActiveItem(nullptr);
513 if(m_pLastEditingItem == pId)
514 {
515 m_pLastEditingItem = nullptr;
516 Res = EEditState::END;
517 }
518 }
519
520 if(!CheckActiveItem(pId) && Res == EEditState::EDITING)
521 return EEditState::NONE;
522
523 if(Input()->ShiftIsPressed())
524 m_MouseSlow = true;
525
526 if(pX)
527 *pX = clamp(val: MouseX() - pRect->x, lo: 0.0f, hi: pRect->w);
528 if(pY)
529 *pY = clamp(val: MouseY() - pRect->y, lo: 0.0f, hi: pRect->h);
530
531 return Res;
532}
533
534void CUi::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) const
535{
536 // reset scrolling if it's not necessary anymore
537 if(TotalSize < ViewPortSize)
538 {
539 *pScrollOffsetChange = -*pScrollOffset;
540 }
541
542 // instant scrolling if distance too long
543 if(absolute(a: *pScrollOffsetChange) > 2.0f * ViewPortSize)
544 {
545 *pScrollOffset += *pScrollOffsetChange;
546 *pScrollOffsetChange = 0.0f;
547 }
548
549 // smooth scrolling
550 if(*pScrollOffsetChange)
551 {
552 const float Delta = *pScrollOffsetChange * clamp(val: Client()->RenderFrameTime() * ScrollSpeed, lo: 0.0f, hi: 1.0f);
553 *pScrollOffset += Delta;
554 *pScrollOffsetChange -= Delta;
555 }
556
557 // clamp to first item
558 if(*pScrollOffset < 0.0f)
559 {
560 if(SmoothClamp && *pScrollOffset < -0.1f)
561 {
562 *pScrollOffsetChange = -*pScrollOffset;
563 }
564 else
565 {
566 *pScrollOffset = 0.0f;
567 *pScrollOffsetChange = 0.0f;
568 }
569 }
570
571 // clamp to last item
572 if(TotalSize > ViewPortSize && *pScrollOffset > TotalSize - ViewPortSize)
573 {
574 if(SmoothClamp && *pScrollOffset - (TotalSize - ViewPortSize) > 0.1f)
575 {
576 *pScrollOffsetChange = (TotalSize - ViewPortSize) - *pScrollOffset;
577 }
578 else
579 {
580 *pScrollOffset = TotalSize - ViewPortSize;
581 *pScrollOffsetChange = 0.0f;
582 }
583 }
584}
585
586struct SCursorAndBoundingBox
587{
588 vec2 m_TextSize;
589 float m_BiggestCharacterHeight;
590 int m_LineCount;
591};
592
593static SCursorAndBoundingBox CalcFontSizeCursorHeightAndBoundingBox(ITextRender *pTextRender, const char *pText, int Flags, float &Size, float MaxWidth, const SLabelProperties &LabelProps)
594{
595 const float MinFontSize = 5.0f;
596 const float MaxTextWidth = LabelProps.m_MaxWidth != -1.0f ? LabelProps.m_MaxWidth : MaxWidth;
597 const int FlagsWithoutStop = Flags & ~(TEXTFLAG_STOP_AT_END | TEXTFLAG_ELLIPSIS_AT_END);
598 const float MaxTextWidthWithoutStop = Flags == FlagsWithoutStop ? LabelProps.m_MaxWidth : -1.0f;
599
600 float TextBoundingHeight = 0.0f;
601 float TextHeight = 0.0f;
602 int LineCount = 0;
603 STextSizeProperties TextSizeProps{};
604 TextSizeProps.m_pHeight = &TextHeight;
605 TextSizeProps.m_pMaxCharacterHeightInLine = &TextBoundingHeight;
606 TextSizeProps.m_pLineCount = &LineCount;
607
608 float TextWidth;
609 do
610 {
611 Size = maximum(a: Size, b: MinFontSize);
612 // Only consider stop-at-end and ellipsis-at-end when minimum font size reached or font scaling disabled
613 if((Size == MinFontSize || !LabelProps.m_EnableWidthCheck) && Flags != FlagsWithoutStop)
614 TextWidth = pTextRender->TextWidth(Size, pText, StrLength: -1, LineWidth: LabelProps.m_MaxWidth, Flags, TextSizeProps);
615 else
616 TextWidth = pTextRender->TextWidth(Size, pText, StrLength: -1, LineWidth: MaxTextWidthWithoutStop, Flags: FlagsWithoutStop, TextSizeProps);
617 if(TextWidth <= MaxTextWidth + 0.001f || !LabelProps.m_EnableWidthCheck || Size == MinFontSize)
618 break;
619 Size--;
620 } while(true);
621
622 SCursorAndBoundingBox Res{};
623 Res.m_TextSize = vec2(TextWidth, TextHeight);
624 Res.m_BiggestCharacterHeight = TextBoundingHeight;
625 Res.m_LineCount = LineCount;
626 return Res;
627}
628
629static int GetFlagsForLabelProperties(const SLabelProperties &LabelProps, const CTextCursor *pReadCursor)
630{
631 if(pReadCursor != nullptr)
632 return pReadCursor->m_Flags & ~TEXTFLAG_RENDER;
633
634 int Flags = 0;
635 Flags |= LabelProps.m_StopAtEnd ? TEXTFLAG_STOP_AT_END : 0;
636 Flags |= LabelProps.m_EllipsisAtEnd ? TEXTFLAG_ELLIPSIS_AT_END : 0;
637 return Flags;
638}
639
640vec2 CUi::CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight)
641{
642 vec2 Cursor(pRect->x, pRect->y);
643
644 const int HorizontalAlign = Align & TEXTALIGN_MASK_HORIZONTAL;
645 if(HorizontalAlign == TEXTALIGN_CENTER)
646 {
647 Cursor.x += (pRect->w - TextSize.x) / 2.0f;
648 }
649 else if(HorizontalAlign == TEXTALIGN_RIGHT)
650 {
651 Cursor.x += pRect->w - TextSize.x;
652 }
653
654 const int VerticalAlign = Align & TEXTALIGN_MASK_VERTICAL;
655 if(VerticalAlign == TEXTALIGN_MIDDLE)
656 {
657 Cursor.y += pBiggestCharHeight != nullptr ? ((pRect->h - *pBiggestCharHeight) / 2.0f - (TextSize.y - *pBiggestCharHeight)) : (pRect->h - TextSize.y) / 2.0f;
658 }
659 else if(VerticalAlign == TEXTALIGN_BOTTOM)
660 {
661 Cursor.y += pRect->h - TextSize.y;
662 }
663
664 return Cursor;
665}
666
667void CUi::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps) const
668{
669 const int Flags = GetFlagsForLabelProperties(LabelProps, pReadCursor: nullptr);
670 const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(pTextRender: TextRender(), pText, Flags, Size, MaxWidth: pRect->w, LabelProps);
671 const vec2 CursorPos = CalcAlignedCursorPos(pRect, TextSize: TextBounds.m_TextSize, Align, pBiggestCharHeight: TextBounds.m_LineCount == 1 ? &TextBounds.m_BiggestCharacterHeight : nullptr);
672
673 CTextCursor Cursor;
674 TextRender()->SetCursor(pCursor: &Cursor, x: CursorPos.x, y: CursorPos.y, FontSize: Size, Flags: TEXTFLAG_RENDER | Flags);
675 Cursor.m_vColorSplits = LabelProps.m_vColorSplits;
676 Cursor.m_LineWidth = (float)LabelProps.m_MaxWidth;
677 TextRender()->TextEx(pCursor: &Cursor, pText, Length: -1);
678}
679
680void CUi::DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const
681{
682 const int Flags = GetFlagsForLabelProperties(LabelProps, pReadCursor);
683 const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(pTextRender: TextRender(), pText, Flags, Size, MaxWidth: pRect->w, LabelProps);
684
685 CTextCursor Cursor;
686 if(pReadCursor)
687 {
688 Cursor = *pReadCursor;
689 }
690 else
691 {
692 const vec2 CursorPos = CalcAlignedCursorPos(pRect, TextSize: TextBounds.m_TextSize, Align);
693 TextRender()->SetCursor(pCursor: &Cursor, x: CursorPos.x, y: CursorPos.y, FontSize: Size, Flags: TEXTFLAG_RENDER | Flags);
694 }
695 Cursor.m_LineWidth = LabelProps.m_MaxWidth;
696
697 RectEl.m_TextColor = TextRender()->GetTextColor();
698 RectEl.m_TextOutlineColor = TextRender()->GetTextOutlineColor();
699 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
700 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
701 TextRender()->CreateTextContainer(TextContainerIndex&: RectEl.m_UITextContainer, pCursor: &Cursor, pText, Length: StrLen);
702 TextRender()->TextColor(rgb: RectEl.m_TextColor);
703 TextRender()->TextOutlineColor(rgb: RectEl.m_TextOutlineColor);
704 RectEl.m_Cursor = Cursor;
705}
706
707void CUi::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const
708{
709 const int ReadCursorGlyphCount = pReadCursor == nullptr ? -1 : pReadCursor->m_GlyphCount;
710 bool NeedsRecreate = false;
711 bool ColorChanged = RectEl.m_TextColor != TextRender()->GetTextColor() || RectEl.m_TextOutlineColor != TextRender()->GetTextOutlineColor();
712 if((!RectEl.m_UITextContainer.Valid() && pText[0] != '\0' && StrLen != 0) || RectEl.m_Width != pRect->w || RectEl.m_Height != pRect->h || ColorChanged || RectEl.m_ReadCursorGlyphCount != ReadCursorGlyphCount)
713 {
714 NeedsRecreate = true;
715 }
716 else
717 {
718 if(StrLen <= -1)
719 {
720 if(str_comp(a: RectEl.m_Text.c_str(), b: pText) != 0)
721 NeedsRecreate = true;
722 }
723 else
724 {
725 if(StrLen != (int)RectEl.m_Text.size() || str_comp_num(a: RectEl.m_Text.c_str(), b: pText, num: StrLen) != 0)
726 NeedsRecreate = true;
727 }
728 }
729 RectEl.m_X = pRect->x;
730 RectEl.m_Y = pRect->y;
731 if(NeedsRecreate)
732 {
733 TextRender()->DeleteTextContainer(TextContainerIndex&: RectEl.m_UITextContainer);
734
735 RectEl.m_Width = pRect->w;
736 RectEl.m_Height = pRect->h;
737
738 if(StrLen > 0)
739 RectEl.m_Text = std::string(pText, StrLen);
740 else if(StrLen < 0)
741 RectEl.m_Text = pText;
742 else
743 RectEl.m_Text.clear();
744
745 RectEl.m_ReadCursorGlyphCount = ReadCursorGlyphCount;
746
747 CUIRect TmpRect;
748 TmpRect.x = 0;
749 TmpRect.y = 0;
750 TmpRect.w = pRect->w;
751 TmpRect.h = pRect->h;
752
753 DoLabel(RectEl, pRect: &TmpRect, pText, Size, Align: TEXTALIGN_TL, LabelProps, StrLen, pReadCursor);
754 }
755
756 if(RectEl.m_UITextContainer.Valid())
757 {
758 const vec2 CursorPos = CalcAlignedCursorPos(pRect, TextSize: vec2(RectEl.m_Cursor.m_LongestLineWidth, RectEl.m_Cursor.Height()), Align);
759 TextRender()->RenderTextContainer(TextContainerIndex: RectEl.m_UITextContainer, TextColor: RectEl.m_TextColor, TextOutlineColor: RectEl.m_TextOutlineColor, X: CursorPos.x, Y: CursorPos.y);
760 }
761}
762
763bool CUi::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector<STextColorSplit> &vColorSplits)
764{
765 const bool Inside = MouseHovered(pRect);
766 const bool Active = m_pLastActiveItem == pLineInput;
767 const bool Changed = pLineInput->WasChanged();
768 const bool CursorChanged = pLineInput->WasCursorChanged();
769
770 const float VSpacing = 2.0f;
771 CUIRect Textbox;
772 pRect->VMargin(Cut: VSpacing, pOtherRect: &Textbox);
773
774 bool JustGotActive = false;
775 if(CheckActiveItem(pId: pLineInput))
776 {
777 if(MouseButton(Index: 0))
778 {
779 if(pLineInput->IsActive() && (Input()->HasComposition() || Input()->GetCandidateCount()))
780 {
781 // Clear IME composition/candidates on mouse press
782 Input()->StopTextInput();
783 Input()->StartTextInput();
784 }
785 }
786 else
787 {
788 SetActiveItem(nullptr);
789 }
790 }
791 else if(HotItem() == pLineInput)
792 {
793 if(MouseButton(Index: 0))
794 {
795 if(!Active)
796 JustGotActive = true;
797 SetActiveItem(pLineInput);
798 }
799 }
800
801 if(Inside && !MouseButton(Index: 0))
802 SetHotItem(pLineInput);
803
804 if(Enabled() && Active && !JustGotActive)
805 pLineInput->Activate(Priority: EInputPriority::UI);
806 else
807 pLineInput->Deactivate();
808
809 float ScrollOffset = pLineInput->GetScrollOffset();
810 float ScrollOffsetChange = pLineInput->GetScrollOffsetChange();
811
812 // Update mouse selection information
813 CLineInput::SMouseSelection *pMouseSelection = pLineInput->GetMouseSelection();
814 if(Inside)
815 {
816 if(!pMouseSelection->m_Selecting && MouseButtonClicked(Index: 0))
817 {
818 pMouseSelection->m_Selecting = true;
819 pMouseSelection->m_PressMouse = MousePos();
820 pMouseSelection->m_Offset.x = ScrollOffset;
821 }
822 }
823 if(pMouseSelection->m_Selecting)
824 {
825 pMouseSelection->m_ReleaseMouse = MousePos();
826 if(!MouseButton(Index: 0))
827 {
828 pMouseSelection->m_Selecting = false;
829 }
830 }
831 if(ScrollOffset != pMouseSelection->m_Offset.x)
832 {
833 // When the scroll offset is changed, update the position that the mouse was pressed at,
834 // so the existing text selection still stays mostly the same.
835 // TODO: The selection may change by one character temporarily, due to different character widths.
836 // Needs text render adjustment: keep selection start based on character.
837 pMouseSelection->m_PressMouse.x -= ScrollOffset - pMouseSelection->m_Offset.x;
838 pMouseSelection->m_Offset.x = ScrollOffset;
839 }
840
841 // Render
842 pRect->Draw(Color: ms_LightButtonColorFunction.GetColor(Active, Hovered: HotItem() == pLineInput), Corners, Rounding: 3.0f);
843 ClipEnable(pRect);
844 Textbox.x -= ScrollOffset;
845 const STextBoundingBox BoundingBox = pLineInput->Render(pRect: &Textbox, FontSize, Align: TEXTALIGN_ML, Changed: Changed || CursorChanged, LineWidth: -1.0f, LineSpacing: 0.0f, vColorSplits);
846 ClipDisable();
847
848 // Scroll left or right if necessary
849 if(Active && !JustGotActive && (Changed || CursorChanged || Input()->HasComposition()))
850 {
851 const float CaretPositionX = pLineInput->GetCaretPosition().x - Textbox.x - ScrollOffset - ScrollOffsetChange;
852 if(CaretPositionX > Textbox.w)
853 ScrollOffsetChange += CaretPositionX - Textbox.w;
854 else if(CaretPositionX < 0.0f)
855 ScrollOffsetChange += CaretPositionX;
856 }
857
858 DoSmoothScrollLogic(pScrollOffset: &ScrollOffset, pScrollOffsetChange: &ScrollOffsetChange, ViewPortSize: Textbox.w, TotalSize: BoundingBox.m_W, SmoothClamp: true);
859
860 pLineInput->SetScrollOffset(ScrollOffset);
861 pLineInput->SetScrollOffsetChange(ScrollOffsetChange);
862
863 return Changed;
864}
865
866bool CUi::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector<STextColorSplit> &vColorSplits)
867{
868 CUIRect EditBox, ClearButton;
869 pRect->VSplitRight(Cut: pRect->h, pLeft: &EditBox, pRight: &ClearButton);
870
871 bool ReturnValue = DoEditBox(pLineInput, pRect: &EditBox, FontSize, Corners: Corners & ~IGraphics::CORNER_R, vColorSplits);
872
873 ClearButton.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f * ButtonColorMul(pId: pLineInput->GetClearButtonId())), Corners: Corners & ~IGraphics::CORNER_L, Rounding: 3.0f);
874 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
875 DoLabel(pRect: &ClearButton, pText: "×", Size: ClearButton.h * CUi::ms_FontmodHeight * 0.8f, Align: TEXTALIGN_MC);
876 TextRender()->SetRenderFlags(0);
877 if(DoButtonLogic(pId: pLineInput->GetClearButtonId(), Checked: 0, pRect: &ClearButton))
878 {
879 pLineInput->Clear();
880 SetActiveItem(pLineInput);
881 ReturnValue = true;
882 }
883
884 return ReturnValue;
885}
886
887int CUi::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pId, const std::function<const char *()> &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props)
888{
889 CUIRect Text = *pRect, DropDownIcon;
890 Text.HMargin(Cut: pRect->h >= 20.0f ? 2.0f : 1.0f, pOtherRect: &Text);
891 Text.HMargin(Cut: (Text.h * Props.m_FontFactor) / 2.0f, pOtherRect: &Text);
892 if(Props.m_ShowDropDownIcon)
893 {
894 Text.VSplitRight(Cut: pRect->h * 0.25f, pLeft: &Text, pRight: nullptr);
895 Text.VSplitRight(Cut: pRect->h * 0.75f, pLeft: &Text, pRight: &DropDownIcon);
896 }
897
898 if(!UIElement.AreRectsInit() || Props.m_HintRequiresStringCheck || Props.m_HintCanChangePositionOrSize || !UIElement.Rect(Index: 0)->m_UITextContainer.Valid())
899 {
900 bool NeedsRecalc = !UIElement.AreRectsInit() || !UIElement.Rect(Index: 0)->m_UITextContainer.Valid();
901 if(Props.m_HintCanChangePositionOrSize)
902 {
903 if(UIElement.AreRectsInit())
904 {
905 if(UIElement.Rect(Index: 0)->m_X != pRect->x || UIElement.Rect(Index: 0)->m_Y != pRect->y || UIElement.Rect(Index: 0)->m_Width != pRect->w || UIElement.Rect(Index: 0)->m_Height != pRect->h || UIElement.Rect(Index: 0)->m_Rounding != Props.m_Rounding || UIElement.Rect(Index: 0)->m_Corners != Props.m_Corners)
906 {
907 NeedsRecalc = true;
908 }
909 }
910 }
911 const char *pText = nullptr;
912 if(Props.m_HintRequiresStringCheck)
913 {
914 if(UIElement.AreRectsInit())
915 {
916 pText = GetTextLambda();
917 if(str_comp(a: UIElement.Rect(Index: 0)->m_Text.c_str(), b: pText) != 0)
918 {
919 NeedsRecalc = true;
920 }
921 }
922 }
923 if(NeedsRecalc)
924 {
925 if(!UIElement.AreRectsInit())
926 {
927 UIElement.InitRects(RequestedRectCount: 3);
928 }
929 ResetUIElement(UIElement);
930
931 for(int i = 0; i < 3; ++i)
932 {
933 ColorRGBA Color = Props.m_Color;
934 if(i == 0)
935 Color.a *= ButtonColorMulActive();
936 else if(i == 1)
937 Color.a *= ButtonColorMulHot();
938 else if(i == 2)
939 Color.a *= ButtonColorMulDefault();
940 Graphics()->SetColor(Color);
941
942 CUIElement::SUIElementRect &NewRect = *UIElement.Rect(Index: i);
943 NewRect.m_UIRectQuadContainer = Graphics()->CreateRectQuadContainer(x: pRect->x, y: pRect->y, w: pRect->w, h: pRect->h, r: Props.m_Rounding, Corners: Props.m_Corners);
944
945 NewRect.m_X = pRect->x;
946 NewRect.m_Y = pRect->y;
947 NewRect.m_Width = pRect->w;
948 NewRect.m_Height = pRect->h;
949 NewRect.m_Rounding = Props.m_Rounding;
950 NewRect.m_Corners = Props.m_Corners;
951 if(i == 0)
952 {
953 if(pText == nullptr)
954 pText = GetTextLambda();
955 NewRect.m_Text = pText;
956 if(Props.m_UseIconFont)
957 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
958 DoLabel(RectEl&: NewRect, pRect: &Text, pText, Size: Text.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
959 if(Props.m_UseIconFont)
960 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
961 }
962 }
963 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
964 }
965 }
966 // render
967 size_t Index = 2;
968 if(CheckActiveItem(pId))
969 Index = 0;
970 else if(HotItem() == pId)
971 Index = 1;
972 Graphics()->TextureClear();
973 Graphics()->RenderQuadContainer(ContainerIndex: UIElement.Rect(Index)->m_UIRectQuadContainer, QuadDrawNum: -1);
974 if(Props.m_ShowDropDownIcon)
975 {
976 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
977 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
978 DoLabel(pRect: &DropDownIcon, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Size: DropDownIcon.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MR);
979 TextRender()->SetRenderFlags(0);
980 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
981 }
982 ColorRGBA ColorText(TextRender()->DefaultTextColor());
983 ColorRGBA ColorTextOutline(TextRender()->DefaultTextOutlineColor());
984 if(UIElement.Rect(Index: 0)->m_UITextContainer.Valid())
985 TextRender()->RenderTextContainer(TextContainerIndex: UIElement.Rect(Index: 0)->m_UITextContainer, TextColor: ColorText, TextOutlineColor: ColorTextOutline);
986 return DoButtonLogic(pId, Checked: Props.m_Checked, pRect);
987}
988
989int CUi::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pText, const CUIRect *pRect, float Size, int Align, float Padding, bool TransparentInactive, bool Enabled)
990{
991 if(!TransparentInactive || CheckActiveItem(pId: pButtonContainer) || HotItem() == pButtonContainer)
992 pRect->Draw(Color: Enabled ? ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * ButtonColorMul(pId: pButtonContainer)) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
993
994 CUIRect Label;
995 pRect->Margin(Cut: Padding, pOtherRect: &Label);
996 DoLabel(pRect: &Label, pText, Size, Align);
997
998 return Enabled ? DoButtonLogic(pId: pButtonContainer, Checked: 0, pRect) : 0;
999}
1000
1001int64_t CUi::DoValueSelector(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props)
1002{
1003 return DoValueSelectorWithState(pId, pRect, pLabel, Current, Min, Max, Props).m_Value;
1004}
1005
1006SEditResult<int64_t> CUi::DoValueSelectorWithState(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props)
1007{
1008 // logic
1009 const bool Inside = MouseInside(pRect);
1010 const int Base = Props.m_IsHex ? 16 : 10;
1011
1012 if(HotItem() == pId && m_ActiveValueSelectorState.m_Button >= 0 && !MouseButton(Index: m_ActiveValueSelectorState.m_Button))
1013 {
1014 DisableMouseLock();
1015 if(CheckActiveItem(pId))
1016 {
1017 SetActiveItem(nullptr);
1018 }
1019 if(Inside && ((m_ActiveValueSelectorState.m_Button == 0 && !m_ActiveValueSelectorState.m_DidScroll && DoDoubleClickLogic(pId)) || m_ActiveValueSelectorState.m_Button == 1))
1020 {
1021 m_ActiveValueSelectorState.m_pLastTextId = pId;
1022 m_ActiveValueSelectorState.m_NumberInput.SetInteger64(Number: Current, Base, HexPrefix: Props.m_HexPrefix);
1023 m_ActiveValueSelectorState.m_NumberInput.SelectAll();
1024 }
1025 m_ActiveValueSelectorState.m_Button = -1;
1026 }
1027
1028 if(m_ActiveValueSelectorState.m_pLastTextId == pId)
1029 {
1030 SetActiveItem(&m_ActiveValueSelectorState.m_NumberInput);
1031 DoEditBox(pLineInput: &m_ActiveValueSelectorState.m_NumberInput, pRect, FontSize: 10.0f);
1032
1033 if(ConsumeHotkey(Hotkey: HOTKEY_ENTER) || ((MouseButtonClicked(Index: 1) || MouseButtonClicked(Index: 0)) && !Inside))
1034 {
1035 Current = clamp(val: m_ActiveValueSelectorState.m_NumberInput.GetInteger64(Base), lo: Min, hi: Max);
1036 DisableMouseLock();
1037 SetActiveItem(nullptr);
1038 m_ActiveValueSelectorState.m_pLastTextId = nullptr;
1039 }
1040
1041 if(ConsumeHotkey(Hotkey: HOTKEY_ESCAPE))
1042 {
1043 DisableMouseLock();
1044 SetActiveItem(nullptr);
1045 m_ActiveValueSelectorState.m_pLastTextId = nullptr;
1046 }
1047 }
1048 else
1049 {
1050 if(CheckActiveItem(pId))
1051 {
1052 dbg_assert(m_ActiveValueSelectorState.m_Button >= 0, "m_ActiveValueSelectorState.m_Button invalid");
1053 if(Props.m_UseScroll && m_ActiveValueSelectorState.m_Button == 0 && MouseButton(Index: 0))
1054 {
1055 m_ActiveValueSelectorState.m_ScrollValue += MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f);
1056
1057 if(absolute(a: m_ActiveValueSelectorState.m_ScrollValue) > Props.m_Scale)
1058 {
1059 const int64_t Count = (int64_t)(m_ActiveValueSelectorState.m_ScrollValue / Props.m_Scale);
1060 m_ActiveValueSelectorState.m_ScrollValue = std::fmod(x: m_ActiveValueSelectorState.m_ScrollValue, y: Props.m_Scale);
1061 Current += Props.m_Step * Count;
1062 Current = clamp(val: Current, lo: Min, hi: Max);
1063 m_ActiveValueSelectorState.m_DidScroll = true;
1064
1065 // Constrain to discrete steps
1066 if(Count > 0)
1067 Current = Current / Props.m_Step * Props.m_Step;
1068 else
1069 Current = std::ceil(x: Current / (float)Props.m_Step) * Props.m_Step;
1070 }
1071 }
1072 }
1073 else if(HotItem() == pId)
1074 {
1075 if(MouseButton(Index: 0))
1076 {
1077 m_ActiveValueSelectorState.m_Button = 0;
1078 m_ActiveValueSelectorState.m_DidScroll = false;
1079 m_ActiveValueSelectorState.m_ScrollValue = 0.0f;
1080 SetActiveItem(pId);
1081 if(Props.m_UseScroll)
1082 EnableMouseLock(pId);
1083 }
1084 else if(MouseButton(Index: 1))
1085 {
1086 m_ActiveValueSelectorState.m_Button = 1;
1087 SetActiveItem(pId);
1088 }
1089 }
1090
1091 // render
1092 char aBuf[128];
1093 if(pLabel[0] != '\0')
1094 {
1095 if(Props.m_IsHex)
1096 str_format(aBuf, sizeof(aBuf), "%s #%0*" PRIX64, pLabel, Props.m_HexPrefix, Current);
1097 else
1098 str_format(aBuf, sizeof(aBuf), "%s %" PRId64, pLabel, Current);
1099 }
1100 else
1101 {
1102 if(Props.m_IsHex)
1103 str_format(aBuf, sizeof(aBuf), "#%0*" PRIX64, Props.m_HexPrefix, Current);
1104 else
1105 str_format(aBuf, sizeof(aBuf), "%" PRId64, Current);
1106 }
1107 pRect->Draw(Color: Props.m_Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
1108 DoLabel(pRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
1109 }
1110
1111 if(Inside && !MouseButton(Index: 0) && !MouseButton(Index: 1))
1112 SetHotItem(pId);
1113
1114 EEditState State = EEditState::NONE;
1115 if(m_pLastEditingItem == pId)
1116 {
1117 State = EEditState::EDITING;
1118 }
1119 if(((CheckActiveItem(pId) && CheckMouseLock()) || m_ActiveValueSelectorState.m_pLastTextId == pId) && m_pLastEditingItem != pId)
1120 {
1121 State = EEditState::START;
1122 m_pLastEditingItem = pId;
1123 }
1124 if(!CheckMouseLock() && m_ActiveValueSelectorState.m_pLastTextId != pId && m_pLastEditingItem == pId)
1125 {
1126 State = EEditState::END;
1127 m_pLastEditingItem = nullptr;
1128 }
1129
1130 return SEditResult<int64_t>{.m_State: State, .m_Value: Current};
1131}
1132
1133float CUi::DoScrollbarV(const void *pId, const CUIRect *pRect, float Current)
1134{
1135 Current = clamp(val: Current, lo: 0.0f, hi: 1.0f);
1136
1137 // layout
1138 CUIRect Rail;
1139 pRect->Margin(Cut: 5.0f, pOtherRect: &Rail);
1140
1141 CUIRect Handle;
1142 Rail.HSplitTop(Cut: clamp(val: 33.0f, lo: Rail.w, hi: Rail.h / 3.0f), pTop: &Handle, pBottom: 0);
1143 Handle.y = Rail.y + (Rail.h - Handle.h) * Current;
1144
1145 // logic
1146 const bool InsideRail = MouseHovered(pRect: &Rail);
1147 const bool InsideHandle = MouseHovered(pRect: &Handle);
1148 bool Grabbed = false; // whether to apply the offset
1149
1150 if(CheckActiveItem(pId))
1151 {
1152 if(MouseButton(Index: 0))
1153 {
1154 Grabbed = true;
1155 if(Input()->ShiftIsPressed())
1156 m_MouseSlow = true;
1157 }
1158 else
1159 {
1160 SetActiveItem(nullptr);
1161 }
1162 }
1163 else if(HotItem() == pId)
1164 {
1165 if(MouseButton(Index: 0))
1166 {
1167 SetActiveItem(pId);
1168 m_ActiveScrollbarOffset = MouseY() - Handle.y;
1169 Grabbed = true;
1170 }
1171 }
1172 else if(MouseButtonClicked(Index: 0) && !InsideHandle && InsideRail)
1173 {
1174 SetActiveItem(pId);
1175 m_ActiveScrollbarOffset = Handle.h / 2.0f;
1176 Grabbed = true;
1177 }
1178
1179 if(InsideHandle && !MouseButton(Index: 0))
1180 {
1181 SetHotItem(pId);
1182 }
1183
1184 float ReturnValue = Current;
1185 if(Grabbed)
1186 {
1187 const float Min = Rail.y;
1188 const float Max = Rail.h - Handle.h;
1189 const float Cur = MouseY() - m_ActiveScrollbarOffset;
1190 ReturnValue = clamp(val: (Cur - Min) / Max, lo: 0.0f, hi: 1.0f);
1191 }
1192
1193 // render
1194 Rail.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: Rail.w / 2.0f);
1195 Handle.Draw(Color: ms_ScrollBarColorFunction.GetColor(Active: CheckActiveItem(pId), Hovered: HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding: Handle.w / 2.0f);
1196
1197 return ReturnValue;
1198}
1199
1200float CUi::DoScrollbarH(const void *pId, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner)
1201{
1202 Current = clamp(val: Current, lo: 0.0f, hi: 1.0f);
1203
1204 // layout
1205 CUIRect Rail;
1206 if(pColorInner)
1207 Rail = *pRect;
1208 else
1209 pRect->HMargin(Cut: 5.0f, pOtherRect: &Rail);
1210
1211 CUIRect Handle;
1212 Rail.VSplitLeft(Cut: pColorInner ? 8.0f : clamp(val: 33.0f, lo: Rail.h, hi: Rail.w / 3.0f), pLeft: &Handle, pRight: 0);
1213 Handle.x += (Rail.w - Handle.w) * Current;
1214
1215 // logic
1216 const bool InsideRail = MouseHovered(pRect: &Rail);
1217 const bool InsideHandle = MouseHovered(pRect: &Handle);
1218 bool Grabbed = false; // whether to apply the offset
1219
1220 if(CheckActiveItem(pId))
1221 {
1222 if(MouseButton(Index: 0))
1223 {
1224 Grabbed = true;
1225 if(Input()->ShiftIsPressed())
1226 m_MouseSlow = true;
1227 }
1228 else
1229 {
1230 SetActiveItem(nullptr);
1231 }
1232 }
1233 else if(HotItem() == pId)
1234 {
1235 if(MouseButton(Index: 0))
1236 {
1237 SetActiveItem(pId);
1238 m_ActiveScrollbarOffset = MouseX() - Handle.x;
1239 Grabbed = true;
1240 }
1241 }
1242 else if(MouseButtonClicked(Index: 0) && !InsideHandle && InsideRail)
1243 {
1244 SetActiveItem(pId);
1245 m_ActiveScrollbarOffset = Handle.w / 2.0f;
1246 Grabbed = true;
1247 }
1248
1249 if(InsideHandle && !MouseButton(Index: 0))
1250 {
1251 SetHotItem(pId);
1252 }
1253
1254 float ReturnValue = Current;
1255 if(Grabbed)
1256 {
1257 const float Min = Rail.x;
1258 const float Max = Rail.w - Handle.w;
1259 const float Cur = MouseX() - m_ActiveScrollbarOffset;
1260 ReturnValue = clamp(val: (Cur - Min) / Max, lo: 0.0f, hi: 1.0f);
1261 }
1262
1263 // render
1264 if(pColorInner)
1265 {
1266 CUIRect Slider;
1267 Handle.VMargin(Cut: -2.0f, pOtherRect: &Slider);
1268 Slider.HMargin(Cut: -3.0f, pOtherRect: &Slider);
1269 Slider.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1270 Slider.Margin(Cut: 2.0f, pOtherRect: &Slider);
1271 Slider.Draw(Color: *pColorInner, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
1272 }
1273 else
1274 {
1275 Rail.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: Rail.h / 2.0f);
1276 Handle.Draw(Color: ms_ScrollBarColorFunction.GetColor(Active: CheckActiveItem(pId), Hovered: HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding: Handle.h / 2.0f);
1277 }
1278
1279 return ReturnValue;
1280}
1281
1282bool CUi::DoScrollbarOption(const void *pId, int *pOption, const CUIRect *pRect, const char *pStr, int Min, int Max, const IScrollbarScale *pScale, unsigned Flags, const char *pSuffix)
1283{
1284 const bool Infinite = Flags & CUi::SCROLLBAR_OPTION_INFINITE;
1285 const bool NoClampValue = Flags & CUi::SCROLLBAR_OPTION_NOCLAMPVALUE;
1286 const bool MultiLine = Flags & CUi::SCROLLBAR_OPTION_MULTILINE;
1287
1288 int Value = *pOption;
1289 if(Infinite)
1290 {
1291 Max += 1;
1292 if(Value == 0)
1293 Value = Max;
1294 }
1295
1296 char aBuf[256];
1297 if(!Infinite || Value != Max)
1298 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %i%s", pStr, Value, pSuffix);
1299 else
1300 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: ∞", pStr);
1301
1302 if(NoClampValue)
1303 {
1304 // clamp the value internally for the scrollbar
1305 Value = clamp(val: Value, lo: Min, hi: Max);
1306 }
1307
1308 CUIRect Label, ScrollBar;
1309 if(MultiLine)
1310 pRect->HSplitMid(pTop: &Label, pBottom: &ScrollBar);
1311 else
1312 pRect->VSplitMid(pLeft: &Label, pRight: &ScrollBar, Spacing: minimum(a: 10.0f, b: pRect->w * 0.05f));
1313
1314 const float FontSize = Label.h * CUi::ms_FontmodHeight * 0.8f;
1315 DoLabel(pRect: &Label, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
1316
1317 Value = pScale->ToAbsolute(RelativeValue: DoScrollbarH(pId, pRect: &ScrollBar, Current: pScale->ToRelative(AbsoluteValue: Value, Min, Max)), Min, Max);
1318 if(NoClampValue && ((Value == Min && *pOption < Min) || (Value == Max && *pOption > Max)))
1319 {
1320 Value = *pOption; // use previous out of range value instead if the scrollbar is at the edge
1321 }
1322 else if(Infinite)
1323 {
1324 if(Value == Max)
1325 Value = 0;
1326 }
1327
1328 if(*pOption != Value)
1329 {
1330 *pOption = Value;
1331 return true;
1332 }
1333 return false;
1334}
1335
1336void CUi::RenderProgressBar(CUIRect ProgressBar, float Progress)
1337{
1338 const float Rounding = minimum(a: 5.0f, b: ProgressBar.h / 2.0f);
1339 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding);
1340 ProgressBar.w = maximum(a: ProgressBar.w * Progress, b: 2 * Rounding);
1341 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
1342}
1343
1344void CUi::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) const
1345{
1346 Graphics()->TextureClear();
1347 Graphics()->QuadsBegin();
1348
1349 // The filled and unfilled segments need to begin at the same angle offset
1350 // or the differences in pixel alignment will make the filled segments flicker.
1351 const float SegmentsAngle = 2.0f * pi / Props.m_Segments;
1352 const float InnerRadius = OuterRadius * 0.75f;
1353 const float AngleOffset = -0.5f * pi;
1354 Graphics()->SetColor(Props.m_Color.WithMultipliedAlpha(alpha: 0.5f));
1355 for(int i = 0; i < Props.m_Segments; ++i)
1356 {
1357 const float Angle1 = AngleOffset + i * SegmentsAngle;
1358 const float Angle2 = AngleOffset + (i + 1) * SegmentsAngle;
1359 IGraphics::CFreeformItem Item = IGraphics::CFreeformItem(
1360 Center.x + std::cos(x: Angle1) * InnerRadius, Center.y + std::sin(x: Angle1) * InnerRadius,
1361 Center.x + std::cos(x: Angle2) * InnerRadius, Center.y + std::sin(x: Angle2) * InnerRadius,
1362 Center.x + std::cos(x: Angle1) * OuterRadius, Center.y + std::sin(x: Angle1) * OuterRadius,
1363 Center.x + std::cos(x: Angle2) * OuterRadius, Center.y + std::sin(x: Angle2) * OuterRadius);
1364 Graphics()->QuadsDrawFreeform(pArray: &Item, Num: 1);
1365 }
1366
1367 const float FilledRatio = Props.m_Progress < 0.0f ? 0.333f : Props.m_Progress;
1368 const int FilledSegmentOffset = Props.m_Progress < 0.0f ? round_to_int(f: m_ProgressSpinnerOffset * Props.m_Segments) : 0;
1369 const int FilledNumSegments = minimum<int>(a: Props.m_Segments * FilledRatio + (Props.m_Progress < 0.0f ? 0 : 1), b: Props.m_Segments);
1370 Graphics()->SetColor(Props.m_Color);
1371 for(int i = 0; i < FilledNumSegments; ++i)
1372 {
1373 const float Angle1 = AngleOffset + (i + FilledSegmentOffset) * SegmentsAngle;
1374 const float Angle2 = AngleOffset + ((i + 1 == FilledNumSegments && Props.m_Progress >= 0.0f) ? (2.0f * pi * Props.m_Progress) : ((i + FilledSegmentOffset + 1) * SegmentsAngle));
1375 IGraphics::CFreeformItem Item = IGraphics::CFreeformItem(
1376 Center.x + std::cos(x: Angle1) * InnerRadius, Center.y + std::sin(x: Angle1) * InnerRadius,
1377 Center.x + std::cos(x: Angle2) * InnerRadius, Center.y + std::sin(x: Angle2) * InnerRadius,
1378 Center.x + std::cos(x: Angle1) * OuterRadius, Center.y + std::sin(x: Angle1) * OuterRadius,
1379 Center.x + std::cos(x: Angle2) * OuterRadius, Center.y + std::sin(x: Angle2) * OuterRadius);
1380 Graphics()->QuadsDrawFreeform(pArray: &Item, Num: 1);
1381 }
1382
1383 Graphics()->QuadsEnd();
1384}
1385
1386void CUi::DoPopupMenu(const SPopupMenuId *pId, int X, int Y, int Width, int Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props)
1387{
1388 constexpr float Margin = SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN;
1389 if(X + Width > Screen()->w - Margin)
1390 X = maximum<float>(a: X - Width, b: Margin);
1391 if(Y + Height > Screen()->h - Margin)
1392 Y = maximum<float>(a: Y - Height, b: Margin);
1393
1394 m_vPopupMenus.emplace_back();
1395 SPopupMenu *pNewMenu = &m_vPopupMenus.back();
1396 pNewMenu->m_pId = pId;
1397 pNewMenu->m_Props = Props;
1398 pNewMenu->m_Rect.x = X;
1399 pNewMenu->m_Rect.y = Y;
1400 pNewMenu->m_Rect.w = Width;
1401 pNewMenu->m_Rect.h = Height;
1402 pNewMenu->m_pContext = pContext;
1403 pNewMenu->m_pfnFunc = pfnFunc;
1404}
1405
1406void CUi::RenderPopupMenus()
1407{
1408 for(size_t i = 0; i < m_vPopupMenus.size(); ++i)
1409 {
1410 const SPopupMenu &PopupMenu = m_vPopupMenus[i];
1411 const SPopupMenuId *pId = PopupMenu.m_pId;
1412 const bool Inside = MouseInside(pRect: &PopupMenu.m_Rect);
1413 const bool Active = i == m_vPopupMenus.size() - 1;
1414
1415 if(Active)
1416 {
1417 // Prevent UI elements below the popup menu from being activated.
1418 SetHotItem(pId);
1419 }
1420
1421 if(CheckActiveItem(pId))
1422 {
1423 if(!MouseButton(Index: 0))
1424 {
1425 if(!Inside)
1426 {
1427 ClosePopupMenu(pId);
1428 --i;
1429 continue;
1430 }
1431 SetActiveItem(nullptr);
1432 }
1433 }
1434 else if(HotItem() == pId)
1435 {
1436 if(MouseButton(Index: 0))
1437 SetActiveItem(pId);
1438 }
1439
1440 if(Inside)
1441 {
1442 // Prevent scroll regions directly behind popup menus from using the mouse scroll events.
1443 SetHotScrollRegion(nullptr);
1444 }
1445
1446 CUIRect PopupRect = PopupMenu.m_Rect;
1447 PopupRect.Draw(Color: PopupMenu.m_Props.m_BorderColor, Corners: PopupMenu.m_Props.m_Corners, Rounding: 3.0f);
1448 PopupRect.Margin(Cut: SPopupMenu::POPUP_BORDER, pOtherRect: &PopupRect);
1449 PopupRect.Draw(Color: PopupMenu.m_Props.m_BackgroundColor, Corners: PopupMenu.m_Props.m_Corners, Rounding: 3.0f);
1450 PopupRect.Margin(Cut: SPopupMenu::POPUP_MARGIN, pOtherRect: &PopupRect);
1451
1452 // The popup render function can open/close popups, which may resize the vector and thus
1453 // invalidate the variable PopupMenu. We therefore store pId in a separate variable.
1454 EPopupMenuFunctionResult Result = PopupMenu.m_pfnFunc(PopupMenu.m_pContext, PopupRect, Active);
1455 if(Result != POPUP_KEEP_OPEN || (Active && ConsumeHotkey(Hotkey: HOTKEY_ESCAPE)))
1456 ClosePopupMenu(pId, IncludeDescendants: Result == POPUP_CLOSE_CURRENT_AND_DESCENDANTS);
1457 }
1458}
1459
1460void CUi::ClosePopupMenu(const SPopupMenuId *pId, bool IncludeDescendants)
1461{
1462 auto PopupMenuToClose = std::find_if(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu PopupMenu) { return PopupMenu.m_pId == pId; });
1463 if(PopupMenuToClose != m_vPopupMenus.end())
1464 {
1465 if(IncludeDescendants)
1466 m_vPopupMenus.erase(first: PopupMenuToClose, last: m_vPopupMenus.end());
1467 else
1468 m_vPopupMenus.erase(position: PopupMenuToClose);
1469 SetActiveItem(nullptr);
1470 if(m_pfnPopupMenuClosedCallback)
1471 m_pfnPopupMenuClosedCallback();
1472 }
1473}
1474
1475void CUi::ClosePopupMenus()
1476{
1477 if(m_vPopupMenus.empty())
1478 return;
1479
1480 m_vPopupMenus.clear();
1481 SetActiveItem(nullptr);
1482 if(m_pfnPopupMenuClosedCallback)
1483 m_pfnPopupMenuClosedCallback();
1484}
1485
1486bool CUi::IsPopupOpen() const
1487{
1488 return !m_vPopupMenus.empty();
1489}
1490
1491bool CUi::IsPopupOpen(const SPopupMenuId *pId) const
1492{
1493 return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu PopupMenu) { return PopupMenu.m_pId == pId; });
1494}
1495
1496bool CUi::IsPopupHovered() const
1497{
1498 return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [this](const SPopupMenu PopupMenu) { return MouseHovered(pRect: &PopupMenu.m_Rect); });
1499}
1500
1501void CUi::SetPopupMenuClosedCallback(FPopupMenuClosedCallback pfnCallback)
1502{
1503 m_pfnPopupMenuClosedCallback = std::move(pfnCallback);
1504}
1505
1506void CUi::SMessagePopupContext::DefaultColor(ITextRender *pTextRender)
1507{
1508 m_TextColor = pTextRender->DefaultTextColor();
1509}
1510
1511void CUi::SMessagePopupContext::ErrorColor()
1512{
1513 m_TextColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f);
1514}
1515
1516CUi::EPopupMenuFunctionResult CUi::PopupMessage(void *pContext, CUIRect View, bool Active)
1517{
1518 SMessagePopupContext *pMessagePopup = static_cast<SMessagePopupContext *>(pContext);
1519 CUi *pUI = pMessagePopup->m_pUI;
1520
1521 pUI->TextRender()->TextColor(rgb: pMessagePopup->m_TextColor);
1522 pUI->TextRender()->Text(x: View.x, y: View.y, Size: SMessagePopupContext::POPUP_FONT_SIZE, pText: pMessagePopup->m_aMessage, LineWidth: View.w);
1523 pUI->TextRender()->TextColor(rgb: pUI->TextRender()->DefaultTextColor());
1524
1525 return (Active && pUI->ConsumeHotkey(Hotkey: HOTKEY_ENTER)) ? CUi::POPUP_CLOSE_CURRENT : CUi::POPUP_KEEP_OPEN;
1526}
1527
1528void CUi::ShowPopupMessage(float X, float Y, SMessagePopupContext *pContext)
1529{
1530 const float TextWidth = minimum(a: std::ceil(x: TextRender()->TextWidth(Size: SMessagePopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: -1.0f) + 0.5f), b: SMessagePopupContext::POPUP_MAX_WIDTH);
1531 float TextHeight = 0.0f;
1532 STextSizeProperties TextSizeProps{};
1533 TextSizeProps.m_pHeight = &TextHeight;
1534 TextRender()->TextWidth(Size: SMessagePopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: TextWidth, Flags: 0, TextSizeProps);
1535 pContext->m_pUI = this;
1536 DoPopupMenu(pId: pContext, X, Y, Width: TextWidth + 10.0f, Height: TextHeight + 10.0f, pContext, pfnFunc: PopupMessage);
1537}
1538
1539CUi::SConfirmPopupContext::SConfirmPopupContext()
1540{
1541 Reset();
1542}
1543
1544void CUi::SConfirmPopupContext::Reset()
1545{
1546 m_Result = SConfirmPopupContext::UNSET;
1547}
1548
1549void CUi::SConfirmPopupContext::YesNoButtons()
1550{
1551 str_copy(dst&: m_aPositiveButtonLabel, src: Localize(pStr: "Yes"));
1552 str_copy(dst&: m_aNegativeButtonLabel, src: Localize(pStr: "No"));
1553}
1554
1555void CUi::ShowPopupConfirm(float X, float Y, SConfirmPopupContext *pContext)
1556{
1557 const float TextWidth = minimum(a: std::ceil(x: TextRender()->TextWidth(Size: SConfirmPopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: -1.0f) + 0.5f), b: SConfirmPopupContext::POPUP_MAX_WIDTH);
1558 float TextHeight = 0.0f;
1559 STextSizeProperties TextSizeProps{};
1560 TextSizeProps.m_pHeight = &TextHeight;
1561 TextRender()->TextWidth(Size: SConfirmPopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: TextWidth, Flags: 0, TextSizeProps);
1562 const float PopupHeight = TextHeight + SConfirmPopupContext::POPUP_BUTTON_HEIGHT + SConfirmPopupContext::POPUP_BUTTON_SPACING + 10.0f;
1563 pContext->m_pUI = this;
1564 pContext->m_Result = SConfirmPopupContext::UNSET;
1565 DoPopupMenu(pId: pContext, X, Y, Width: TextWidth + 10.0f, Height: PopupHeight, pContext, pfnFunc: PopupConfirm);
1566}
1567
1568CUi::EPopupMenuFunctionResult CUi::PopupConfirm(void *pContext, CUIRect View, bool Active)
1569{
1570 SConfirmPopupContext *pConfirmPopup = static_cast<SConfirmPopupContext *>(pContext);
1571 CUi *pUI = pConfirmPopup->m_pUI;
1572
1573 CUIRect Label, ButtonBar, CancelButton, ConfirmButton;
1574 View.HSplitBottom(Cut: SConfirmPopupContext::POPUP_BUTTON_HEIGHT, pTop: &Label, pBottom: &ButtonBar);
1575 ButtonBar.VSplitMid(pLeft: &CancelButton, pRight: &ConfirmButton, Spacing: SConfirmPopupContext::POPUP_BUTTON_SPACING);
1576
1577 pUI->TextRender()->Text(x: Label.x, y: Label.y, Size: SConfirmPopupContext::POPUP_FONT_SIZE, pText: pConfirmPopup->m_aMessage, LineWidth: Label.w);
1578
1579 if(pUI->DoButton_PopupMenu(pButtonContainer: &pConfirmPopup->m_CancelButton, pText: pConfirmPopup->m_aNegativeButtonLabel, pRect: &CancelButton, Size: SConfirmPopupContext::POPUP_FONT_SIZE, Align: TEXTALIGN_MC))
1580 {
1581 pConfirmPopup->m_Result = SConfirmPopupContext::CANCELED;
1582 return CUi::POPUP_CLOSE_CURRENT;
1583 }
1584
1585 if(pUI->DoButton_PopupMenu(pButtonContainer: &pConfirmPopup->m_ConfirmButton, pText: pConfirmPopup->m_aPositiveButtonLabel, pRect: &ConfirmButton, Size: SConfirmPopupContext::POPUP_FONT_SIZE, Align: TEXTALIGN_MC) || (Active && pUI->ConsumeHotkey(Hotkey: HOTKEY_ENTER)))
1586 {
1587 pConfirmPopup->m_Result = SConfirmPopupContext::CONFIRMED;
1588 return CUi::POPUP_CLOSE_CURRENT;
1589 }
1590
1591 return CUi::POPUP_KEEP_OPEN;
1592}
1593
1594CUi::SSelectionPopupContext::SSelectionPopupContext()
1595{
1596 Reset();
1597}
1598
1599void CUi::SSelectionPopupContext::Reset()
1600{
1601 m_Props = SPopupMenuProperties();
1602 m_aMessage[0] = '\0';
1603 m_pSelection = nullptr;
1604 m_SelectionIndex = -1;
1605 m_vEntries.clear();
1606 m_vButtonContainers.clear();
1607 m_EntryHeight = 12.0f;
1608 m_EntryPadding = 0.0f;
1609 m_EntrySpacing = 5.0f;
1610 m_FontSize = 10.0f;
1611 m_Width = 300.0f + (SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN) * 2;
1612 m_AlignmentHeight = -1.0f;
1613 m_TransparentButtons = false;
1614}
1615
1616CUi::EPopupMenuFunctionResult CUi::PopupSelection(void *pContext, CUIRect View, bool Active)
1617{
1618 SSelectionPopupContext *pSelectionPopup = static_cast<SSelectionPopupContext *>(pContext);
1619 CUi *pUI = pSelectionPopup->m_pUI;
1620 CScrollRegion *pScrollRegion = pSelectionPopup->m_pScrollRegion;
1621
1622 vec2 ScrollOffset(0.0f, 0.0f);
1623 CScrollRegionParams ScrollParams;
1624 ScrollParams.m_ScrollbarWidth = 10.0f;
1625 ScrollParams.m_ScrollbarMargin = SPopupMenu::POPUP_MARGIN;
1626 ScrollParams.m_ScrollbarNoMarginRight = true;
1627 ScrollParams.m_ScrollUnit = 3 * (pSelectionPopup->m_EntryHeight + pSelectionPopup->m_EntrySpacing);
1628 pScrollRegion->Begin(pClipRect: &View, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
1629 View.y += ScrollOffset.y;
1630
1631 CUIRect Slot;
1632 if(pSelectionPopup->m_aMessage[0] != '\0')
1633 {
1634 const STextBoundingBox TextBoundingBox = pUI->TextRender()->TextBoundingBox(Size: pSelectionPopup->m_FontSize, pText: pSelectionPopup->m_aMessage, StrLength: -1, LineWidth: pSelectionPopup->m_Width);
1635 View.HSplitTop(Cut: TextBoundingBox.m_H, pTop: &Slot, pBottom: &View);
1636 if(pScrollRegion->AddRect(Rect: Slot))
1637 {
1638 pUI->TextRender()->Text(x: Slot.x, y: Slot.y, Size: pSelectionPopup->m_FontSize, pText: pSelectionPopup->m_aMessage, LineWidth: Slot.w);
1639 }
1640 }
1641
1642 pSelectionPopup->m_vButtonContainers.resize(new_size: pSelectionPopup->m_vEntries.size());
1643
1644 size_t Index = 0;
1645 for(const auto &Entry : pSelectionPopup->m_vEntries)
1646 {
1647 if(pSelectionPopup->m_aMessage[0] != '\0' || Index != 0)
1648 View.HSplitTop(Cut: pSelectionPopup->m_EntrySpacing, pTop: nullptr, pBottom: &View);
1649 View.HSplitTop(Cut: pSelectionPopup->m_EntryHeight, pTop: &Slot, pBottom: &View);
1650 if(pScrollRegion->AddRect(Rect: Slot))
1651 {
1652 if(pUI->DoButton_PopupMenu(pButtonContainer: &pSelectionPopup->m_vButtonContainers[Index], pText: Entry.c_str(), pRect: &Slot, Size: pSelectionPopup->m_FontSize, Align: TEXTALIGN_ML, Padding: pSelectionPopup->m_EntryPadding, TransparentInactive: pSelectionPopup->m_TransparentButtons))
1653 {
1654 pSelectionPopup->m_pSelection = &Entry;
1655 pSelectionPopup->m_SelectionIndex = Index;
1656 }
1657 }
1658 ++Index;
1659 }
1660
1661 pScrollRegion->End();
1662
1663 return pSelectionPopup->m_pSelection == nullptr ? CUi::POPUP_KEEP_OPEN : CUi::POPUP_CLOSE_CURRENT;
1664}
1665
1666void CUi::ShowPopupSelection(float X, float Y, SSelectionPopupContext *pContext)
1667{
1668 const STextBoundingBox TextBoundingBox = TextRender()->TextBoundingBox(Size: pContext->m_FontSize, pText: pContext->m_aMessage, StrLength: -1, LineWidth: pContext->m_Width);
1669 const float PopupHeight = minimum(a: (pContext->m_aMessage[0] == '\0' ? -pContext->m_EntrySpacing : TextBoundingBox.m_H) + pContext->m_vEntries.size() * (pContext->m_EntryHeight + pContext->m_EntrySpacing) + (SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN) * 2 + CScrollRegion::HEIGHT_MAGIC_FIX, b: Screen()->h * 0.4f);
1670 pContext->m_pUI = this;
1671 pContext->m_pSelection = nullptr;
1672 pContext->m_SelectionIndex = -1;
1673 pContext->m_Props.m_Corners = IGraphics::CORNER_ALL;
1674 if(pContext->m_AlignmentHeight >= 0.0f)
1675 {
1676 constexpr float Margin = SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN;
1677 if(X + pContext->m_Width > Screen()->w - Margin)
1678 {
1679 X = maximum<float>(a: X - pContext->m_Width, b: Margin);
1680 }
1681 if(Y + pContext->m_AlignmentHeight + PopupHeight > Screen()->h - Margin)
1682 {
1683 Y -= PopupHeight;
1684 pContext->m_Props.m_Corners = IGraphics::CORNER_T;
1685 }
1686 else
1687 {
1688 Y += pContext->m_AlignmentHeight;
1689 pContext->m_Props.m_Corners = IGraphics::CORNER_B;
1690 }
1691 }
1692 DoPopupMenu(pId: pContext, X, Y, Width: pContext->m_Width, Height: PopupHeight, pContext, pfnFunc: PopupSelection, Props: pContext->m_Props);
1693}
1694
1695int CUi::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Num, SDropDownState &State)
1696{
1697 if(!State.m_Init)
1698 {
1699 State.m_UiElement.Init(pUI: this, RequestedRectCount: -1);
1700 State.m_Init = true;
1701 }
1702
1703 const auto LabelFunc = [CurSelection, pStrs]() {
1704 return CurSelection > -1 ? pStrs[CurSelection] : "";
1705 };
1706
1707 SMenuButtonProperties Props;
1708 Props.m_HintRequiresStringCheck = true;
1709 Props.m_HintCanChangePositionOrSize = true;
1710 Props.m_ShowDropDownIcon = true;
1711 if(IsPopupOpen(pId: &State.m_SelectionPopupContext))
1712 Props.m_Corners = IGraphics::CORNER_ALL & (~State.m_SelectionPopupContext.m_Props.m_Corners);
1713 if(DoButton_Menu(UIElement&: State.m_UiElement, pId: &State.m_ButtonContainer, GetTextLambda: LabelFunc, pRect, Props))
1714 {
1715 State.m_SelectionPopupContext.Reset();
1716 State.m_SelectionPopupContext.m_Props.m_BorderColor = ColorRGBA(0.7f, 0.7f, 0.7f, 0.9f);
1717 State.m_SelectionPopupContext.m_Props.m_BackgroundColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f);
1718 for(int i = 0; i < Num; ++i)
1719 State.m_SelectionPopupContext.m_vEntries.emplace_back(args&: pStrs[i]);
1720 State.m_SelectionPopupContext.m_EntryHeight = pRect->h;
1721 State.m_SelectionPopupContext.m_EntryPadding = pRect->h >= 20.0f ? 2.0f : 1.0f;
1722 State.m_SelectionPopupContext.m_FontSize = (State.m_SelectionPopupContext.m_EntryHeight - 2 * State.m_SelectionPopupContext.m_EntryPadding) * CUi::ms_FontmodHeight;
1723 State.m_SelectionPopupContext.m_Width = pRect->w;
1724 State.m_SelectionPopupContext.m_AlignmentHeight = pRect->h;
1725 State.m_SelectionPopupContext.m_TransparentButtons = true;
1726 ShowPopupSelection(X: pRect->x, Y: pRect->y, pContext: &State.m_SelectionPopupContext);
1727 }
1728
1729 if(State.m_SelectionPopupContext.m_SelectionIndex >= 0)
1730 {
1731 const int NewSelection = State.m_SelectionPopupContext.m_SelectionIndex;
1732 State.m_SelectionPopupContext.Reset();
1733 return NewSelection;
1734 }
1735
1736 return CurSelection;
1737}
1738
1739CUi::EPopupMenuFunctionResult CUi::PopupColorPicker(void *pContext, CUIRect View, bool Active)
1740{
1741 SColorPickerPopupContext *pColorPicker = static_cast<SColorPickerPopupContext *>(pContext);
1742 CUi *pUI = pColorPicker->m_pUI;
1743 pColorPicker->m_State = EEditState::NONE;
1744
1745 CUIRect ColorsArea, HueArea, BottomArea, ModeButtonArea, HueRect, SatRect, ValueRect, HexRect, AlphaRect;
1746
1747 View.HSplitTop(Cut: 140.0f, pTop: &ColorsArea, pBottom: &BottomArea);
1748 ColorsArea.VSplitRight(Cut: 20.0f, pLeft: &ColorsArea, pRight: &HueArea);
1749
1750 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
1751 HueArea.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &HueArea);
1752
1753 BottomArea.HSplitTop(Cut: 20.0f, pTop: &HueRect, pBottom: &BottomArea);
1754 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
1755
1756 constexpr float ValuePadding = 5.0f;
1757 const float HsvValueWidth = (HueRect.w - ValuePadding * 2) / 3.0f;
1758 const float HexValueWidth = HsvValueWidth * 2 + ValuePadding;
1759
1760 HueRect.VSplitLeft(Cut: HsvValueWidth, pLeft: &HueRect, pRight: &SatRect);
1761 SatRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &SatRect);
1762 SatRect.VSplitLeft(Cut: HsvValueWidth, pLeft: &SatRect, pRight: &ValueRect);
1763 ValueRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &ValueRect);
1764
1765 BottomArea.HSplitTop(Cut: 20.0f, pTop: &HexRect, pBottom: &BottomArea);
1766 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
1767 HexRect.VSplitLeft(Cut: HexValueWidth, pLeft: &HexRect, pRight: &AlphaRect);
1768 AlphaRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &AlphaRect);
1769 BottomArea.HSplitTop(Cut: 20.0f, pTop: &ModeButtonArea, pBottom: &BottomArea);
1770
1771 const ColorRGBA BlackColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
1772
1773 HueArea.Draw(Color: BlackColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
1774 HueArea.Margin(Cut: 1.0f, pOtherRect: &HueArea);
1775
1776 ColorsArea.Draw(Color: BlackColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
1777 ColorsArea.Margin(Cut: 1.0f, pOtherRect: &ColorsArea);
1778
1779 ColorHSVA PickerColorHSV = pColorPicker->m_HsvaColor;
1780 ColorRGBA PickerColorRGB = pColorPicker->m_RgbaColor;
1781 ColorHSLA PickerColorHSL = pColorPicker->m_HslaColor;
1782
1783 // Color Area
1784 ColorRGBA TL, TR, BL, BR;
1785 TL = BL = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 0.0f, 1.0f));
1786 TR = BR = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 1.0f, 1.0f));
1787 ColorsArea.Draw4(ColorTopLeft: TL, ColorTopRight: TR, ColorBottomLeft: BL, ColorBottomRight: BR, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
1788
1789 TL = TR = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
1790 BL = BR = ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f);
1791 ColorsArea.Draw4(ColorTopLeft: TL, ColorTopRight: TR, ColorBottomLeft: BL, ColorBottomRight: BR, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
1792
1793 // Hue Area
1794 static const float s_aaColorIndices[7][3] = {
1795 {1.0f, 0.0f, 0.0f}, // red
1796 {1.0f, 0.0f, 1.0f}, // magenta
1797 {0.0f, 0.0f, 1.0f}, // blue
1798 {0.0f, 1.0f, 1.0f}, // cyan
1799 {0.0f, 1.0f, 0.0f}, // green
1800 {1.0f, 1.0f, 0.0f}, // yellow
1801 {1.0f, 0.0f, 0.0f}, // red
1802 };
1803
1804 const float HuePickerOffset = HueArea.h / 6.0f;
1805 CUIRect HuePartialArea = HueArea;
1806 HuePartialArea.h = HuePickerOffset;
1807
1808 for(size_t j = 0; j < std::size(s_aaColorIndices) - 1; j++)
1809 {
1810 TL = ColorRGBA(s_aaColorIndices[j][0], s_aaColorIndices[j][1], s_aaColorIndices[j][2], 1.0f);
1811 BL = ColorRGBA(s_aaColorIndices[j + 1][0], s_aaColorIndices[j + 1][1], s_aaColorIndices[j + 1][2], 1.0f);
1812
1813 HuePartialArea.y = HueArea.y + HuePickerOffset * j;
1814 HuePartialArea.Draw4(ColorTopLeft: TL, ColorTopRight: TL, ColorBottomLeft: BL, ColorBottomRight: BL, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
1815 }
1816
1817 const auto &&RenderAlphaSelector = [&](unsigned OldA) -> SEditResult<int64_t> {
1818 if(pColorPicker->m_Alpha)
1819 {
1820 return pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[3], pRect: &AlphaRect, pLabel: "A:", Current: OldA, Min: 0, Max: 255);
1821 }
1822 else
1823 {
1824 char aBuf[8];
1825 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "A: %d", OldA);
1826 pUI->DoLabel(pRect: &AlphaRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
1827 AlphaRect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.65f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
1828 return {.m_State: EEditState::NONE, .m_Value: OldA};
1829 }
1830 };
1831
1832 // Editboxes Area
1833 if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSVA)
1834 {
1835 const unsigned OldH = round_to_int(f: PickerColorHSV.h * 255.0f);
1836 const unsigned OldS = round_to_int(f: PickerColorHSV.s * 255.0f);
1837 const unsigned OldV = round_to_int(f: PickerColorHSV.v * 255.0f);
1838 const unsigned OldA = round_to_int(f: PickerColorHSV.a * 255.0f);
1839
1840 const auto [StateH, H] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "H:", Current: OldH, Min: 0, Max: 255);
1841 const auto [StateS, S] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "S:", Current: OldS, Min: 0, Max: 255);
1842 const auto [StateV, V] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "V:", Current: OldV, Min: 0, Max: 255);
1843 const auto [StateA, A] = RenderAlphaSelector(OldA);
1844
1845 if(OldH != H || OldS != S || OldV != V || OldA != A)
1846 {
1847 PickerColorHSV = ColorHSVA(H / 255.0f, S / 255.0f, V / 255.0f, A / 255.0f);
1848 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
1849 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
1850 }
1851
1852 for(auto State : {StateH, StateS, StateV, StateA})
1853 {
1854 if(State != EEditState::NONE)
1855 {
1856 pColorPicker->m_State = State;
1857 break;
1858 }
1859 }
1860 }
1861 else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_RGBA)
1862 {
1863 const unsigned OldR = round_to_int(f: PickerColorRGB.r * 255.0f);
1864 const unsigned OldG = round_to_int(f: PickerColorRGB.g * 255.0f);
1865 const unsigned OldB = round_to_int(f: PickerColorRGB.b * 255.0f);
1866 const unsigned OldA = round_to_int(f: PickerColorRGB.a * 255.0f);
1867
1868 const auto [StateR, R] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "R:", Current: OldR, Min: 0, Max: 255);
1869 const auto [StateG, G] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "G:", Current: OldG, Min: 0, Max: 255);
1870 const auto [StateB, B] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "B:", Current: OldB, Min: 0, Max: 255);
1871 const auto [StateA, A] = RenderAlphaSelector(OldA);
1872
1873 if(OldR != R || OldG != G || OldB != B || OldA != A)
1874 {
1875 PickerColorRGB = ColorRGBA(R / 255.0f, G / 255.0f, B / 255.0f, A / 255.0f);
1876 PickerColorHSL = color_cast<ColorHSLA>(rgb: PickerColorRGB);
1877 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
1878 }
1879
1880 for(auto State : {StateR, StateG, StateB, StateA})
1881 {
1882 if(State != EEditState::NONE)
1883 {
1884 pColorPicker->m_State = State;
1885 break;
1886 }
1887 }
1888 }
1889 else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSLA)
1890 {
1891 const unsigned OldH = round_to_int(f: PickerColorHSL.h * 255.0f);
1892 const unsigned OldS = round_to_int(f: PickerColorHSL.s * 255.0f);
1893 const unsigned OldL = round_to_int(f: PickerColorHSL.l * 255.0f);
1894 const unsigned OldA = round_to_int(f: PickerColorHSL.a * 255.0f);
1895
1896 const auto [StateH, H] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "H:", Current: OldH, Min: 0, Max: 255);
1897 const auto [StateS, S] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "S:", Current: OldS, Min: 0, Max: 255);
1898 const auto [StateL, L] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "L:", Current: OldL, Min: 0, Max: 255);
1899 const auto [StateA, A] = RenderAlphaSelector(OldA);
1900
1901 if(OldH != H || OldS != S || OldL != L || OldA != A)
1902 {
1903 PickerColorHSL = ColorHSLA(H / 255.0f, S / 255.0f, L / 255.0f, A / 255.0f);
1904 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
1905 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
1906 }
1907
1908 for(auto State : {StateH, StateS, StateL, StateA})
1909 {
1910 if(State != EEditState::NONE)
1911 {
1912 pColorPicker->m_State = State;
1913 break;
1914 }
1915 }
1916 }
1917 else
1918 {
1919 dbg_assert(false, "Color picker mode invalid");
1920 }
1921
1922 SValueSelectorProperties Props;
1923 Props.m_UseScroll = false;
1924 Props.m_IsHex = true;
1925 Props.m_HexPrefix = pColorPicker->m_Alpha ? 8 : 6;
1926 const unsigned OldHex = PickerColorRGB.PackAlphaLast(Alpha: pColorPicker->m_Alpha);
1927 auto [HexState, Hex] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[4], pRect: &HexRect, pLabel: "Hex:", Current: OldHex, Min: 0, Max: pColorPicker->m_Alpha ? 0xFFFFFFFFll : 0xFFFFFFll, Props);
1928 if(OldHex != Hex)
1929 {
1930 const float OldAlpha = PickerColorRGB.a;
1931 PickerColorRGB = ColorRGBA::UnpackAlphaLast<ColorRGBA>(Color: Hex, Alpha: pColorPicker->m_Alpha);
1932 if(!pColorPicker->m_Alpha)
1933 PickerColorRGB.a = OldAlpha;
1934 PickerColorHSL = color_cast<ColorHSLA>(rgb: PickerColorRGB);
1935 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
1936 }
1937
1938 if(HexState != EEditState::NONE)
1939 pColorPicker->m_State = HexState;
1940
1941 // Logic
1942 float PickerX, PickerY;
1943 EEditState ColorPickerRes = pUI->DoPickerLogic(pId: &pColorPicker->m_ColorPickerId, pRect: &ColorsArea, pX: &PickerX, pY: &PickerY);
1944 if(ColorPickerRes != EEditState::NONE)
1945 {
1946 PickerColorHSV.y = PickerX / ColorsArea.w;
1947 PickerColorHSV.z = 1.0f - PickerY / ColorsArea.h;
1948 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
1949 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
1950 pColorPicker->m_State = ColorPickerRes;
1951 }
1952
1953 EEditState HuePickerRes = pUI->DoPickerLogic(pId: &pColorPicker->m_HuePickerId, pRect: &HueArea, pX: &PickerX, pY: &PickerY);
1954 if(HuePickerRes != EEditState::NONE)
1955 {
1956 PickerColorHSV.x = 1.0f - PickerY / HueArea.h;
1957 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
1958 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
1959 pColorPicker->m_State = HuePickerRes;
1960 }
1961
1962 // Marker Color Area
1963 const float MarkerX = ColorsArea.x + ColorsArea.w * PickerColorHSV.y;
1964 const float MarkerY = ColorsArea.y + ColorsArea.h * (1.0f - PickerColorHSV.z);
1965
1966 const float MarkerOutlineInd = PickerColorHSV.z > 0.5f ? 0.0f : 1.0f;
1967 const ColorRGBA MarkerOutline = ColorRGBA(MarkerOutlineInd, MarkerOutlineInd, MarkerOutlineInd, 1.0f);
1968
1969 pUI->Graphics()->TextureClear();
1970 pUI->Graphics()->QuadsBegin();
1971 pUI->Graphics()->SetColor(MarkerOutline);
1972 pUI->Graphics()->DrawCircle(CenterX: MarkerX, CenterY: MarkerY, Radius: 4.5f, Segments: 32);
1973 pUI->Graphics()->SetColor(PickerColorRGB);
1974 pUI->Graphics()->DrawCircle(CenterX: MarkerX, CenterY: MarkerY, Radius: 3.5f, Segments: 32);
1975 pUI->Graphics()->QuadsEnd();
1976
1977 // Marker Hue Area
1978 CUIRect HueMarker;
1979 HueArea.Margin(Cut: -2.5f, pOtherRect: &HueMarker);
1980 HueMarker.h = 6.5f;
1981 HueMarker.y = (HueArea.y + HueArea.h * (1.0f - PickerColorHSV.x)) - HueMarker.h / 2.0f;
1982
1983 const ColorRGBA HueMarkerColor = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 1.0f, 1.0f, 1.0f));
1984 const float HueMarkerOutlineColor = PickerColorHSV.x > 0.75f ? 1.0f : 0.0f;
1985 const ColorRGBA HueMarkerOutline = ColorRGBA(HueMarkerOutlineColor, HueMarkerOutlineColor, HueMarkerOutlineColor, 1.0f);
1986
1987 HueMarker.Draw(Color: HueMarkerOutline, Corners: IGraphics::CORNER_ALL, Rounding: 1.2f);
1988 HueMarker.Margin(Cut: 1.2f, pOtherRect: &HueMarker);
1989 HueMarker.Draw(Color: HueMarkerColor, Corners: IGraphics::CORNER_ALL, Rounding: 1.2f);
1990
1991 pColorPicker->m_HsvaColor = PickerColorHSV;
1992 pColorPicker->m_RgbaColor = PickerColorRGB;
1993 pColorPicker->m_HslaColor = PickerColorHSL;
1994 if(pColorPicker->m_pHslaColor != nullptr)
1995 *pColorPicker->m_pHslaColor = PickerColorHSL.Pack(Alpha: pColorPicker->m_Alpha);
1996
1997 static const SColorPickerPopupContext::EColorPickerMode s_aModes[] = {SColorPickerPopupContext::MODE_HSVA, SColorPickerPopupContext::MODE_RGBA, SColorPickerPopupContext::MODE_HSLA};
1998 static const char *s_apModeLabels[std::size(s_aModes)] = {"HSVA", "RGBA", "HSLA"};
1999 for(SColorPickerPopupContext::EColorPickerMode Mode : s_aModes)
2000 {
2001 CUIRect ModeButton;
2002 ModeButtonArea.VSplitLeft(Cut: HsvValueWidth, pLeft: &ModeButton, pRight: &ModeButtonArea);
2003 ModeButtonArea.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &ModeButtonArea);
2004 if(pUI->DoButton_PopupMenu(pButtonContainer: &pColorPicker->m_aModeButtons[(int)Mode], pText: s_apModeLabels[Mode], pRect: &ModeButton, Size: 10.0f, Align: TEXTALIGN_MC, Padding: 2.0f, TransparentInactive: false, Enabled: pColorPicker->m_ColorMode != Mode))
2005 {
2006 pColorPicker->m_ColorMode = Mode;
2007 }
2008 }
2009
2010 return CUi::POPUP_KEEP_OPEN;
2011}
2012
2013void CUi::ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext)
2014{
2015 pContext->m_pUI = this;
2016 if(pContext->m_ColorMode == SColorPickerPopupContext::MODE_UNSET)
2017 pContext->m_ColorMode = SColorPickerPopupContext::MODE_HSVA;
2018 DoPopupMenu(pId: pContext, X, Y, Width: 160.0f + 10.0f, Height: 209.0f + 10.0f, pContext, pfnFunc: PopupColorPicker);
2019}
2020