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