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