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