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