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
464 // x and y are supposed to be 0
465 Graphics()->MapScreenToSize(Width: pScreen->w, Height: pScreen->h);
466}
467
468float CUi::PixelSize()
469{
470 return Screen()->w / Graphics()->ScreenWidth();
471}
472
473void CUi::ClipEnable(const CUIRect *pRect)
474{
475 if(IsClipped())
476 {
477 const CUIRect *pOldRect = ClipArea();
478 CUIRect Intersection;
479 Intersection.x = std::max(a: pRect->x, b: pOldRect->x);
480 Intersection.y = std::max(a: pRect->y, b: pOldRect->y);
481 Intersection.w = std::min(a: pRect->x + pRect->w, b: pOldRect->x + pOldRect->w) - pRect->x;
482 Intersection.h = std::min(a: pRect->y + pRect->h, b: pOldRect->y + pOldRect->h) - pRect->y;
483 m_vClips.push_back(x: Intersection);
484 }
485 else
486 {
487 m_vClips.push_back(x: *pRect);
488 }
489 UpdateClipping();
490}
491
492void CUi::ClipDisable()
493{
494 dbg_assert(IsClipped(), "no clip region");
495 m_vClips.pop_back();
496 UpdateClipping();
497}
498
499const CUIRect *CUi::ClipArea() const
500{
501 dbg_assert(IsClipped(), "no clip region");
502 return &m_vClips.back();
503}
504
505void CUi::UpdateClipping()
506{
507 if(IsClipped())
508 {
509 const CUIRect *pRect = ClipArea();
510 const float XScale = Graphics()->ScreenWidth() / Screen()->w;
511 const float YScale = Graphics()->ScreenHeight() / Screen()->h;
512
513 const float ScaledX = pRect->x * XScale;
514 const float ScaledY = pRect->y * YScale;
515 const float RoundX = std::round(x: ScaledX);
516 const float RoundY = std::round(x: ScaledY);
517 Graphics()->ClipEnable(x: RoundX, y: RoundY, w: std::round(x: pRect->w * XScale + (ScaledX - RoundX)), h: std::round(x: pRect->h * YScale + (ScaledY - RoundY)));
518 }
519 else
520 {
521 Graphics()->ClipDisable();
522 }
523}
524
525int CUi::DoButtonLogic(const void *pId, int Checked, const CUIRect *pRect, const unsigned Flags)
526{
527 int ReturnValue = 0;
528 const bool Inside = MouseHovered(pRect);
529
530 if(CheckActiveItem(pId))
531 {
532 dbg_assert(m_ActiveButtonLogicButton >= 0, "m_ActiveButtonLogicButton invalid");
533 if(!MouseButton(Index: m_ActiveButtonLogicButton))
534 {
535 if(Inside && Checked >= 0)
536 ReturnValue = 1 + m_ActiveButtonLogicButton;
537 SetActiveItem(nullptr);
538 m_ActiveButtonLogicButton = -1;
539 }
540 }
541
542 bool NoRelevantButtonsPressed = true;
543 for(int Button = 0; Button < 3; ++Button)
544 {
545 if((Flags & (BUTTONFLAG_LEFT << Button)) && MouseButton(Index: Button))
546 {
547 NoRelevantButtonsPressed = false;
548 if(HotItem() == pId)
549 {
550 SetActiveItem(pId);
551 m_ActiveButtonLogicButton = Button;
552 }
553 }
554 }
555
556 if(Inside && NoRelevantButtonsPressed)
557 SetHotItem(pId);
558
559 return ReturnValue;
560}
561
562int CUi::DoDraggableButtonLogic(const void *pId, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted)
563{
564 // logic
565 int ReturnValue = 0;
566 const bool Inside = MouseHovered(pRect);
567
568 if(pClicked != nullptr)
569 *pClicked = false;
570 if(pAbrupted != nullptr)
571 *pAbrupted = false;
572
573 if(CheckActiveItem(pId))
574 {
575 dbg_assert(m_ActiveDraggableButtonLogicButton >= 0, "m_ActiveDraggableButtonLogicButton invalid");
576 if(m_ActiveDraggableButtonLogicButton == 0)
577 {
578 if(Checked >= 0)
579 ReturnValue = 1 + m_ActiveDraggableButtonLogicButton;
580 if(!MouseButton(Index: m_ActiveDraggableButtonLogicButton))
581 {
582 if(pClicked != nullptr)
583 *pClicked = true;
584 SetActiveItem(nullptr);
585 m_ActiveDraggableButtonLogicButton = -1;
586 }
587 if(MouseButton(Index: 1))
588 {
589 if(pAbrupted != nullptr)
590 *pAbrupted = true;
591 SetActiveItem(nullptr);
592 m_ActiveDraggableButtonLogicButton = -1;
593 }
594 }
595 else if(!MouseButton(Index: m_ActiveDraggableButtonLogicButton))
596 {
597 if(Inside && Checked >= 0)
598 ReturnValue = 1 + m_ActiveDraggableButtonLogicButton;
599 if(pClicked != nullptr)
600 *pClicked = true;
601 SetActiveItem(nullptr);
602 m_ActiveDraggableButtonLogicButton = -1;
603 }
604 }
605 else if(HotItem() == pId)
606 {
607 for(int i = 0; i < 3; ++i)
608 {
609 if(MouseButton(Index: i))
610 {
611 SetActiveItem(pId);
612 m_ActiveDraggableButtonLogicButton = i;
613 }
614 }
615 }
616
617 if(Inside && !MouseButton(Index: 0) && !MouseButton(Index: 1) && !MouseButton(Index: 2))
618 SetHotItem(pId);
619
620 return ReturnValue;
621}
622
623bool CUi::DoDoubleClickLogic(const void *pId)
624{
625 if(m_DoubleClickState.m_pLastClickedId == pId &&
626 Client()->GlobalTime() - m_DoubleClickState.m_LastClickTime < 0.5f &&
627 distance(a: m_DoubleClickState.m_LastClickPos, b: MousePos()) <= 32.0f * Screen()->h / Graphics()->ScreenHeight())
628 {
629 m_DoubleClickState.m_pLastClickedId = nullptr;
630 return true;
631 }
632 m_DoubleClickState.m_pLastClickedId = pId;
633 m_DoubleClickState.m_LastClickTime = Client()->GlobalTime();
634 m_DoubleClickState.m_LastClickPos = MousePos();
635 return false;
636}
637
638EEditState CUi::DoPickerLogic(const void *pId, const CUIRect *pRect, float *pX, float *pY)
639{
640 if(MouseHovered(pRect))
641 SetHotItem(pId);
642
643 EEditState Res = EEditState::EDITING;
644
645 if(HotItem() == pId && MouseButtonClicked(Index: 0))
646 {
647 SetActiveItem(pId);
648 if(!m_pLastEditingItem)
649 {
650 m_pLastEditingItem = pId;
651 Res = EEditState::START;
652 }
653 }
654
655 if(CheckActiveItem(pId) && !MouseButton(Index: 0))
656 {
657 SetActiveItem(nullptr);
658 if(m_pLastEditingItem == pId)
659 {
660 m_pLastEditingItem = nullptr;
661 Res = EEditState::END;
662 }
663 }
664
665 if(!CheckActiveItem(pId) && Res == EEditState::EDITING)
666 return EEditState::NONE;
667
668 if(Input()->ShiftIsPressed())
669 m_MouseSlow = true;
670
671 if(pX)
672 *pX = std::clamp(val: MouseX() - pRect->x, lo: 0.0f, hi: pRect->w);
673 if(pY)
674 *pY = std::clamp(val: MouseY() - pRect->y, lo: 0.0f, hi: pRect->h);
675
676 return Res;
677}
678
679void CUi::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) const
680{
681 // reset scrolling if it's not necessary anymore
682 if(TotalSize < ViewPortSize)
683 {
684 *pScrollOffsetChange = -*pScrollOffset;
685 }
686
687 // instant scrolling if distance too long
688 if(absolute(a: *pScrollOffsetChange) > 2.0f * ViewPortSize)
689 {
690 *pScrollOffset += *pScrollOffsetChange;
691 *pScrollOffsetChange = 0.0f;
692 }
693
694 // smooth scrolling
695 if(*pScrollOffsetChange)
696 {
697 const float Delta = *pScrollOffsetChange * std::clamp(val: Client()->RenderFrameTime() * ScrollSpeed, lo: 0.0f, hi: 1.0f);
698 *pScrollOffset += Delta;
699 *pScrollOffsetChange -= Delta;
700 }
701
702 // clamp to first item
703 if(*pScrollOffset < 0.0f)
704 {
705 if(SmoothClamp && *pScrollOffset < -0.1f)
706 {
707 *pScrollOffsetChange = -*pScrollOffset;
708 }
709 else
710 {
711 *pScrollOffset = 0.0f;
712 *pScrollOffsetChange = 0.0f;
713 }
714 }
715
716 // clamp to last item
717 if(TotalSize > ViewPortSize && *pScrollOffset > TotalSize - ViewPortSize)
718 {
719 if(SmoothClamp && *pScrollOffset - (TotalSize - ViewPortSize) > 0.1f)
720 {
721 *pScrollOffsetChange = (TotalSize - ViewPortSize) - *pScrollOffset;
722 }
723 else
724 {
725 *pScrollOffset = TotalSize - ViewPortSize;
726 *pScrollOffsetChange = 0.0f;
727 }
728 }
729}
730
731struct SCursorAndBoundingBox
732{
733 vec2 m_TextSize;
734 float m_BiggestCharacterHeight;
735 int m_LineCount;
736};
737
738static SCursorAndBoundingBox CalcFontSizeCursorHeightAndBoundingBox(ITextRender *pTextRender, const char *pText, int Flags, float &Size, float MaxWidth, const SLabelProperties &LabelProps)
739{
740 const float MaxTextWidth = LabelProps.m_MaxWidth != -1.0f ? LabelProps.m_MaxWidth : MaxWidth;
741 const int FlagsWithoutStop = Flags & ~(TEXTFLAG_STOP_AT_END | TEXTFLAG_ELLIPSIS_AT_END);
742 const float MaxTextWidthWithoutStop = Flags == FlagsWithoutStop ? LabelProps.m_MaxWidth : -1.0f;
743
744 float TextBoundingHeight = 0.0f;
745 float TextHeight = 0.0f;
746 int LineCount = 0;
747 STextSizeProperties TextSizeProps{};
748 TextSizeProps.m_pHeight = &TextHeight;
749 TextSizeProps.m_pMaxCharacterHeightInLine = &TextBoundingHeight;
750 TextSizeProps.m_pLineCount = &LineCount;
751
752 float TextWidth;
753 do
754 {
755 Size = std::max(a: Size, b: LabelProps.m_MinimumFontSize);
756 // Only consider stop-at-end and ellipsis-at-end when minimum font size reached or font scaling disabled
757 if((Size == LabelProps.m_MinimumFontSize || !LabelProps.m_EnableWidthCheck) && Flags != FlagsWithoutStop)
758 TextWidth = pTextRender->TextWidth(Size, pText, StrLength: -1, LineWidth: LabelProps.m_MaxWidth, Flags, TextSizeProps);
759 else
760 TextWidth = pTextRender->TextWidth(Size, pText, StrLength: -1, LineWidth: MaxTextWidthWithoutStop, Flags: FlagsWithoutStop, TextSizeProps);
761 if(TextWidth <= MaxTextWidth + 0.001f || !LabelProps.m_EnableWidthCheck || Size == LabelProps.m_MinimumFontSize)
762 break;
763 Size--;
764 } while(true);
765
766 SCursorAndBoundingBox Res{};
767 Res.m_TextSize = vec2(TextWidth, TextHeight);
768 Res.m_BiggestCharacterHeight = TextBoundingHeight;
769 Res.m_LineCount = LineCount;
770 return Res;
771}
772
773static int GetFlagsForLabelProperties(const SLabelProperties &LabelProps, const CTextCursor *pReadCursor)
774{
775 if(pReadCursor != nullptr)
776 return pReadCursor->m_Flags & ~TEXTFLAG_RENDER;
777
778 int Flags = 0;
779 Flags |= LabelProps.m_StopAtEnd ? TEXTFLAG_STOP_AT_END : 0;
780 Flags |= LabelProps.m_EllipsisAtEnd ? TEXTFLAG_ELLIPSIS_AT_END : 0;
781 return Flags;
782}
783
784vec2 CUi::CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight)
785{
786 vec2 Cursor(pRect->x, pRect->y);
787
788 const int HorizontalAlign = Align & TEXTALIGN_MASK_HORIZONTAL;
789 if(HorizontalAlign == TEXTALIGN_CENTER)
790 {
791 Cursor.x += (pRect->w - TextSize.x) / 2.0f;
792 }
793 else if(HorizontalAlign == TEXTALIGN_RIGHT)
794 {
795 Cursor.x += pRect->w - TextSize.x;
796 }
797
798 const int VerticalAlign = Align & TEXTALIGN_MASK_VERTICAL;
799 if(VerticalAlign == TEXTALIGN_MIDDLE)
800 {
801 Cursor.y += pBiggestCharHeight != nullptr ? ((pRect->h - *pBiggestCharHeight) / 2.0f - (TextSize.y - *pBiggestCharHeight)) : (pRect->h - TextSize.y) / 2.0f;
802 }
803 else if(VerticalAlign == TEXTALIGN_BOTTOM)
804 {
805 Cursor.y += pRect->h - TextSize.y;
806 }
807
808 return Cursor;
809}
810
811CLabelResult CUi::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps) const
812{
813 const int Flags = GetFlagsForLabelProperties(LabelProps, pReadCursor: nullptr);
814 const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(pTextRender: TextRender(), pText, Flags, Size, MaxWidth: pRect->w, LabelProps);
815 const vec2 CursorPos = CalcAlignedCursorPos(pRect, TextSize: TextBounds.m_TextSize, Align, pBiggestCharHeight: TextBounds.m_LineCount == 1 ? &TextBounds.m_BiggestCharacterHeight : nullptr);
816
817 CTextCursor Cursor;
818 Cursor.SetPosition(CursorPos);
819 Cursor.m_FontSize = Size;
820 Cursor.m_Flags |= Flags;
821 Cursor.m_vColorSplits = LabelProps.m_vColorSplits;
822 Cursor.m_LineWidth = (float)LabelProps.m_MaxWidth;
823 TextRender()->TextEx(pCursor: &Cursor, pText, Length: -1);
824 return CLabelResult{.m_Truncated = Cursor.m_Truncated};
825}
826
827void CUi::DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const
828{
829 const int Flags = GetFlagsForLabelProperties(LabelProps, pReadCursor);
830 const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(pTextRender: TextRender(), pText, Flags, Size, MaxWidth: pRect->w, LabelProps);
831
832 CTextCursor Cursor;
833 if(pReadCursor)
834 {
835 Cursor = *pReadCursor;
836 }
837 else
838 {
839 Cursor.SetPosition(CalcAlignedCursorPos(pRect, TextSize: TextBounds.m_TextSize, Align));
840 Cursor.m_FontSize = Size;
841 Cursor.m_Flags |= Flags;
842 }
843 Cursor.m_LineWidth = LabelProps.m_MaxWidth;
844
845 RectEl.m_TextColor = TextRender()->GetTextColor();
846 RectEl.m_TextOutlineColor = TextRender()->GetTextOutlineColor();
847 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
848 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
849 TextRender()->CreateTextContainer(TextContainerIndex&: RectEl.m_UITextContainer, pCursor: &Cursor, pText, Length: StrLen);
850 TextRender()->TextColor(Color: RectEl.m_TextColor);
851 TextRender()->TextOutlineColor(Color: RectEl.m_TextOutlineColor);
852 RectEl.m_Cursor = Cursor;
853}
854
855void CUi::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const
856{
857 const int ReadCursorGlyphCount = pReadCursor == nullptr ? -1 : pReadCursor->m_GlyphCount;
858 bool NeedsRecreate = false;
859 bool ColorChanged = RectEl.m_TextColor != TextRender()->GetTextColor() || RectEl.m_TextOutlineColor != TextRender()->GetTextOutlineColor();
860 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)
861 {
862 NeedsRecreate = true;
863 }
864 else
865 {
866 if(StrLen <= -1)
867 {
868 if(str_comp(a: RectEl.m_Text.c_str(), b: pText) != 0)
869 NeedsRecreate = true;
870 }
871 else
872 {
873 if(StrLen != (int)RectEl.m_Text.size() || str_comp_num(a: RectEl.m_Text.c_str(), b: pText, num: StrLen) != 0)
874 NeedsRecreate = true;
875 }
876 }
877 RectEl.m_X = pRect->x;
878 RectEl.m_Y = pRect->y;
879 if(NeedsRecreate)
880 {
881 TextRender()->DeleteTextContainer(TextContainerIndex&: RectEl.m_UITextContainer);
882
883 RectEl.m_Width = pRect->w;
884 RectEl.m_Height = pRect->h;
885
886 if(StrLen > 0)
887 RectEl.m_Text = std::string(pText, StrLen);
888 else if(StrLen < 0)
889 RectEl.m_Text = pText;
890 else
891 RectEl.m_Text.clear();
892
893 RectEl.m_ReadCursorGlyphCount = ReadCursorGlyphCount;
894
895 CUIRect TmpRect;
896 TmpRect.x = 0;
897 TmpRect.y = 0;
898 TmpRect.w = pRect->w;
899 TmpRect.h = pRect->h;
900
901 DoLabel(RectEl, pRect: &TmpRect, pText, Size, Align: TEXTALIGN_TL, LabelProps, StrLen, pReadCursor);
902 }
903
904 if(RectEl.m_UITextContainer.Valid())
905 {
906 const vec2 CursorPos = CalcAlignedCursorPos(pRect, TextSize: vec2(RectEl.m_Cursor.m_LongestLineWidth, RectEl.m_Cursor.Height()), Align);
907 TextRender()->RenderTextContainer(TextContainerIndex: RectEl.m_UITextContainer, TextColor: RectEl.m_TextColor, TextOutlineColor: RectEl.m_TextOutlineColor, X: CursorPos.x, Y: CursorPos.y);
908 }
909}
910
911CLabelResult CUi::DoLabel_AutoLineSize(const char *pText, float FontSize, int Align, CUIRect *pRect, float LineSize, const SLabelProperties &LabelProps) const
912{
913 CUIRect LabelRect;
914 pRect->HSplitTop(Cut: LineSize, pTop: &LabelRect, pBottom: pRect);
915
916 return DoLabel(pRect: &LabelRect, pText, Size: FontSize, Align);
917}
918
919bool CUi::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector<STextColorSplit> &vColorSplits)
920{
921 const bool Inside = MouseHovered(pRect);
922 const bool Active = m_pLastActiveItem == pLineInput;
923 const bool Changed = pLineInput->WasChanged();
924 const bool CursorChanged = pLineInput->WasCursorChanged();
925
926 const float VSpacing = 2.0f;
927 CUIRect Textbox;
928 pRect->VMargin(Cut: VSpacing, pOtherRect: &Textbox);
929
930 bool JustGotActive = false;
931 if(CheckActiveItem(pId: pLineInput))
932 {
933 if(MouseButton(Index: 0))
934 {
935 if(pLineInput->IsActive() && (Input()->HasComposition() || Input()->GetCandidateCount()))
936 {
937 // Clear IME composition/candidates on mouse press
938 Input()->StopTextInput();
939 Input()->StartTextInput();
940 }
941 }
942 else
943 {
944 SetActiveItem(nullptr);
945 }
946 }
947 else if(HotItem() == pLineInput)
948 {
949 if(MouseButton(Index: 0))
950 {
951 if(!Active)
952 JustGotActive = true;
953 SetActiveItem(pLineInput);
954 }
955 }
956
957 if(Inside && !MouseButton(Index: 0))
958 SetHotItem(pLineInput);
959
960 if(Enabled() && Active && !JustGotActive)
961 pLineInput->Activate(Priority: EInputPriority::UI);
962 else
963 pLineInput->Deactivate();
964
965 float ScrollOffset = pLineInput->GetScrollOffset();
966 float ScrollOffsetChange = pLineInput->GetScrollOffsetChange();
967
968 // Update mouse selection information
969 CLineInput::SMouseSelection *pMouseSelection = pLineInput->GetMouseSelection();
970 if(Inside)
971 {
972 if(!pMouseSelection->m_Selecting && MouseButtonClicked(Index: 0))
973 {
974 pMouseSelection->m_Selecting = true;
975 pMouseSelection->m_PressMouse = MousePos();
976 pMouseSelection->m_Offset.x = ScrollOffset;
977 }
978 }
979 if(pMouseSelection->m_Selecting)
980 {
981 pMouseSelection->m_ReleaseMouse = MousePos();
982 if(!MouseButton(Index: 0))
983 {
984 pMouseSelection->m_Selecting = false;
985 if(Active)
986 {
987 Input()->EnsureScreenKeyboardShown();
988 }
989 }
990 }
991 if(ScrollOffset != pMouseSelection->m_Offset.x)
992 {
993 // When the scroll offset is changed, update the position that the mouse was pressed at,
994 // so the existing text selection still stays mostly the same.
995 // TODO: The selection may change by one character temporarily, due to different character widths.
996 // Needs text render adjustment: keep selection start based on character.
997 pMouseSelection->m_PressMouse.x -= ScrollOffset - pMouseSelection->m_Offset.x;
998 pMouseSelection->m_Offset.x = ScrollOffset;
999 }
1000
1001 // Render
1002 pRect->Draw(Color: ms_LightButtonColorFunction.GetColor(Active, Hovered: HotItem() == pLineInput), Corners, Rounding: 3.0f);
1003 ClipEnable(pRect);
1004 Textbox.x -= ScrollOffset;
1005 const STextBoundingBox BoundingBox = pLineInput->Render(pRect: &Textbox, FontSize, Align: TEXTALIGN_ML, Changed: Changed || CursorChanged, LineWidth: -1.0f, LineSpacing: 0.0f, vColorSplits);
1006 ClipDisable();
1007
1008 // Scroll left or right if necessary
1009 if(Active && !JustGotActive && (Changed || CursorChanged || Input()->HasComposition()))
1010 {
1011 const float CaretPositionX = pLineInput->GetCaretPosition().x - Textbox.x - ScrollOffset - ScrollOffsetChange;
1012 if(CaretPositionX > Textbox.w)
1013 ScrollOffsetChange += CaretPositionX - Textbox.w;
1014 else if(CaretPositionX < 0.0f)
1015 ScrollOffsetChange += CaretPositionX;
1016 }
1017
1018 DoSmoothScrollLogic(pScrollOffset: &ScrollOffset, pScrollOffsetChange: &ScrollOffsetChange, ViewPortSize: Textbox.w, TotalSize: BoundingBox.m_W, SmoothClamp: true);
1019
1020 pLineInput->SetScrollOffset(ScrollOffset);
1021 pLineInput->SetScrollOffsetChange(ScrollOffsetChange);
1022
1023 return Changed;
1024}
1025
1026bool CUi::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector<STextColorSplit> &vColorSplits)
1027{
1028 CUIRect EditBox, ClearButton;
1029 pRect->VSplitRight(Cut: pRect->h, pLeft: &EditBox, pRight: &ClearButton);
1030
1031 bool ReturnValue = DoEditBox(pLineInput, pRect: &EditBox, FontSize, Corners: Corners & ~IGraphics::CORNER_R, vColorSplits);
1032
1033 ClearButton.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f * ButtonColorMul(pId: pLineInput->GetClearButtonId())), Corners: Corners & ~IGraphics::CORNER_L, Rounding: 3.0f);
1034 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);
1035 DoLabel(pRect: &ClearButton, pText: "×", Size: ClearButton.h * CUi::ms_FontmodHeight * 0.8f, Align: TEXTALIGN_MC);
1036 TextRender()->SetRenderFlags(0);
1037 if(DoButtonLogic(pId: pLineInput->GetClearButtonId(), Checked: 0, pRect: &ClearButton, Flags: BUTTONFLAG_LEFT))
1038 {
1039 pLineInput->Clear();
1040 SetActiveItem(pLineInput);
1041 ReturnValue = true;
1042 }
1043
1044 return ReturnValue;
1045}
1046
1047bool CUi::DoEditBox_Search(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, bool HotkeyEnabled)
1048{
1049 CUIRect QuickSearch = *pRect;
1050 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1051 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);
1052 DoLabel(pRect: &QuickSearch, pText: FontIcon::MAGNIFYING_GLASS, Size: FontSize, Align: TEXTALIGN_ML);
1053 const float SearchWidth = TextRender()->TextWidth(Size: FontSize, pText: FontIcon::MAGNIFYING_GLASS);
1054 TextRender()->SetRenderFlags(0);
1055 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1056 QuickSearch.VSplitLeft(Cut: SearchWidth + 5.0f, pLeft: nullptr, pRight: &QuickSearch);
1057 if(HotkeyEnabled && Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
1058 {
1059 SetActiveItem(pLineInput);
1060 pLineInput->SelectAll();
1061 }
1062 pLineInput->SetEmptyText(Localize(pStr: "Search"));
1063 return DoClearableEditBox(pLineInput, pRect: &QuickSearch, FontSize);
1064}
1065
1066int CUi::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pId, const std::function<const char *()> &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props)
1067{
1068 CUIRect Text = *pRect, DropDownIcon;
1069 Text.HMargin(Cut: pRect->h >= 20.0f ? 2.0f : 1.0f, pOtherRect: &Text);
1070 Text.HMargin(Cut: (Text.h * Props.m_FontFactor) / 2.0f, pOtherRect: &Text);
1071 if(Props.m_ShowDropDownIcon)
1072 {
1073 Text.VSplitRight(Cut: pRect->h * 0.25f, pLeft: &Text, pRight: nullptr);
1074 Text.VSplitRight(Cut: pRect->h * 0.75f, pLeft: &Text, pRight: &DropDownIcon);
1075 }
1076
1077 if(!UIElement.AreRectsInit() || Props.m_HintRequiresStringCheck || Props.m_HintCanChangePositionOrSize || !UIElement.Rect(Index: 0)->m_UITextContainer.Valid())
1078 {
1079 bool NeedsRecalc = !UIElement.AreRectsInit() || !UIElement.Rect(Index: 0)->m_UITextContainer.Valid();
1080 if(Props.m_HintCanChangePositionOrSize)
1081 {
1082 if(UIElement.AreRectsInit())
1083 {
1084 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)
1085 {
1086 NeedsRecalc = true;
1087 }
1088 }
1089 }
1090 const char *pText = nullptr;
1091 if(Props.m_HintRequiresStringCheck)
1092 {
1093 if(UIElement.AreRectsInit())
1094 {
1095 pText = GetTextLambda();
1096 if(str_comp(a: UIElement.Rect(Index: 0)->m_Text.c_str(), b: pText) != 0)
1097 {
1098 NeedsRecalc = true;
1099 }
1100 }
1101 }
1102 if(NeedsRecalc)
1103 {
1104 if(!UIElement.AreRectsInit())
1105 {
1106 UIElement.InitRects(RequestedRectCount: 3);
1107 }
1108 ResetUIElement(UIElement);
1109
1110 for(int i = 0; i < 3; ++i)
1111 {
1112 ColorRGBA Color = Props.m_Color;
1113 if(i == 0)
1114 Color.a *= ButtonColorMulActive();
1115 else if(i == 1)
1116 Color.a *= ButtonColorMulHot();
1117 else if(i == 2)
1118 Color.a *= ButtonColorMulDefault();
1119 Graphics()->SetColor(Color);
1120
1121 CUIElement::SUIElementRect &NewRect = *UIElement.Rect(Index: i);
1122 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);
1123
1124 NewRect.m_X = pRect->x;
1125 NewRect.m_Y = pRect->y;
1126 NewRect.m_Width = pRect->w;
1127 NewRect.m_Height = pRect->h;
1128 NewRect.m_Rounding = Props.m_Rounding;
1129 NewRect.m_Corners = Props.m_Corners;
1130 if(i == 0)
1131 {
1132 if(pText == nullptr)
1133 pText = GetTextLambda();
1134 NewRect.m_Text = pText;
1135 if(Props.m_UseIconFont)
1136 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1137 DoLabel(RectEl&: NewRect, pRect: &Text, pText, Size: Text.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1138 if(Props.m_UseIconFont)
1139 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1140 }
1141 }
1142 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1143 }
1144 }
1145 // render
1146 size_t Index = 2;
1147 if(CheckActiveItem(pId))
1148 Index = 0;
1149 else if(HotItem() == pId)
1150 Index = 1;
1151 Graphics()->TextureClear();
1152 Graphics()->RenderQuadContainer(ContainerIndex: UIElement.Rect(Index)->m_UIRectQuadContainer, QuadDrawNum: -1);
1153 if(Props.m_ShowDropDownIcon)
1154 {
1155 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1156 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);
1157 DoLabel(pRect: &DropDownIcon, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Size: DropDownIcon.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MR);
1158 TextRender()->SetRenderFlags(0);
1159 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1160 }
1161 ColorRGBA ColorText(TextRender()->DefaultTextColor());
1162 ColorRGBA ColorTextOutline(TextRender()->DefaultTextOutlineColor());
1163 if(UIElement.Rect(Index: 0)->m_UITextContainer.Valid())
1164 TextRender()->RenderTextContainer(TextContainerIndex: UIElement.Rect(Index: 0)->m_UITextContainer, TextColor: ColorText, TextOutlineColor: ColorTextOutline);
1165 return DoButtonLogic(pId, Checked: Props.m_Checked, pRect, Flags: Props.m_Flags);
1166}
1167
1168int 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)
1169{
1170 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);
1171
1172 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1173 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1174 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
1175 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1176
1177 CUIRect Label;
1178 pRect->HMargin(Cut: 2.0f, pOtherRect: &Label);
1179 DoLabel(pRect: &Label, pText, Size: Label.h * ms_FontmodHeight, Align: TEXTALIGN_MC);
1180
1181 if(!Enabled)
1182 {
1183 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
1184 TextRender()->TextOutlineColor(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f));
1185 DoLabel(pRect: &Label, pText: FontIcon::SLASH, Size: Label.h * ms_FontmodHeight, Align: TEXTALIGN_MC);
1186 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
1187 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1188 }
1189
1190 TextRender()->SetRenderFlags(0);
1191 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1192
1193 return DoButtonLogic(pId: pButtonContainer, Checked, pRect, Flags);
1194}
1195
1196int 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)
1197{
1198 if(!TransparentInactive || CheckActiveItem(pId: pButtonContainer) || HotItem() == pButtonContainer)
1199 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);
1200
1201 CUIRect Label;
1202 pRect->Margin(Cut: Padding, pOtherRect: &Label);
1203 DoLabel(pRect: &Label, pText, Size, Align);
1204
1205 return Enabled ? DoButtonLogic(pId: pButtonContainer, Checked: 0, pRect, Flags: BUTTONFLAG_LEFT) : 0;
1206}
1207
1208int64_t CUi::DoValueSelector(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props)
1209{
1210 return DoValueSelectorWithState(pId, pRect, pLabel, Current, Min, Max, Props).m_Value;
1211}
1212
1213SEditResult<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)
1214{
1215 // logic
1216 const bool Inside = MouseInside(pRect);
1217 const int Base = Props.m_IsHex ? 16 : 10;
1218
1219 if(HotItem() == pId && m_ActiveValueSelectorState.m_Button >= 0 && !MouseButton(Index: m_ActiveValueSelectorState.m_Button))
1220 {
1221 DisableMouseLock();
1222 if(CheckActiveItem(pId))
1223 {
1224 SetActiveItem(nullptr);
1225 }
1226 if(Inside && ((m_ActiveValueSelectorState.m_Button == 0 && !m_ActiveValueSelectorState.m_DidScroll && DoDoubleClickLogic(pId)) || m_ActiveValueSelectorState.m_Button == 1))
1227 {
1228 m_ActiveValueSelectorState.m_pLastTextId = pId;
1229 m_ActiveValueSelectorState.m_NumberInput.SetInteger64(Number: Current, Base, HexPrefix: Props.m_HexPrefix);
1230 m_ActiveValueSelectorState.m_NumberInput.SelectAll();
1231 }
1232 m_ActiveValueSelectorState.m_Button = -1;
1233 }
1234
1235 if(m_ActiveValueSelectorState.m_pLastTextId == pId)
1236 {
1237 SetActiveItem(&m_ActiveValueSelectorState.m_NumberInput);
1238 DoEditBox(pLineInput: &m_ActiveValueSelectorState.m_NumberInput, pRect, FontSize: 10.0f);
1239
1240 if(ConsumeHotkey(Hotkey: HOTKEY_ENTER) || ((MouseButtonClicked(Index: 1) || MouseButtonClicked(Index: 0)) && !Inside))
1241 {
1242 Current = std::clamp(val: m_ActiveValueSelectorState.m_NumberInput.GetInteger64(Base), lo: Min, hi: Max);
1243 DisableMouseLock();
1244 SetActiveItem(nullptr);
1245 m_ActiveValueSelectorState.m_pLastTextId = nullptr;
1246 }
1247
1248 if(ConsumeHotkey(Hotkey: HOTKEY_ESCAPE))
1249 {
1250 DisableMouseLock();
1251 SetActiveItem(nullptr);
1252 m_ActiveValueSelectorState.m_pLastTextId = nullptr;
1253 }
1254 }
1255 else
1256 {
1257 if(CheckActiveItem(pId))
1258 {
1259 dbg_assert(m_ActiveValueSelectorState.m_Button >= 0, "m_ActiveValueSelectorState.m_Button invalid");
1260 if(Props.m_UseScroll && m_ActiveValueSelectorState.m_Button == 0 && MouseButton(Index: 0))
1261 {
1262 m_ActiveValueSelectorState.m_ScrollValue += MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f);
1263
1264 if(absolute(a: m_ActiveValueSelectorState.m_ScrollValue) > Props.m_Scale)
1265 {
1266 const int64_t Count = (int64_t)(m_ActiveValueSelectorState.m_ScrollValue / Props.m_Scale);
1267 m_ActiveValueSelectorState.m_ScrollValue = std::fmod(x: m_ActiveValueSelectorState.m_ScrollValue, y: Props.m_Scale);
1268 Current += Props.m_Step * Count;
1269 Current = std::clamp(val: Current, lo: Min, hi: Max);
1270 m_ActiveValueSelectorState.m_DidScroll = true;
1271
1272 // Constrain to discrete steps
1273 if(Count > 0)
1274 Current = Current / Props.m_Step * Props.m_Step;
1275 else
1276 Current = std::ceil(x: Current / (float)Props.m_Step) * Props.m_Step;
1277 }
1278 }
1279 }
1280 else if(HotItem() == pId)
1281 {
1282 if(MouseButton(Index: 0))
1283 {
1284 m_ActiveValueSelectorState.m_Button = 0;
1285 m_ActiveValueSelectorState.m_DidScroll = false;
1286 m_ActiveValueSelectorState.m_ScrollValue = 0.0f;
1287 SetActiveItem(pId);
1288 if(Props.m_UseScroll)
1289 EnableMouseLock(pId);
1290 }
1291 else if(MouseButton(Index: 1))
1292 {
1293 m_ActiveValueSelectorState.m_Button = 1;
1294 SetActiveItem(pId);
1295 }
1296 }
1297
1298 // render
1299 char aBuf[128];
1300 if(pLabel[0] != '\0')
1301 {
1302 if(Props.m_IsHex)
1303 str_format(aBuf, sizeof(aBuf), "%s #%0*" PRIX64, pLabel, Props.m_HexPrefix, Current);
1304 else
1305 str_format(aBuf, sizeof(aBuf), "%s %" PRId64, pLabel, Current);
1306 }
1307 else
1308 {
1309 if(Props.m_IsHex)
1310 str_format(aBuf, sizeof(aBuf), "#%0*" PRIX64, Props.m_HexPrefix, Current);
1311 else
1312 str_format(aBuf, sizeof(aBuf), "%" PRId64, Current);
1313 }
1314 pRect->Draw(Color: Props.m_Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
1315 DoLabel(pRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
1316 }
1317
1318 if(Inside && !MouseButton(Index: 0) && !MouseButton(Index: 1))
1319 SetHotItem(pId);
1320
1321 EEditState State = EEditState::NONE;
1322 if(m_pLastEditingItem == pId)
1323 {
1324 State = EEditState::EDITING;
1325 }
1326 if(((CheckActiveItem(pId) && CheckMouseLock()) || m_ActiveValueSelectorState.m_pLastTextId == pId) && m_pLastEditingItem != pId)
1327 {
1328 State = EEditState::START;
1329 m_pLastEditingItem = pId;
1330 }
1331 if(!CheckMouseLock() && m_ActiveValueSelectorState.m_pLastTextId != pId && m_pLastEditingItem == pId)
1332 {
1333 State = EEditState::END;
1334 m_pLastEditingItem = nullptr;
1335 }
1336
1337 return SEditResult<int64_t>{.m_State: State, .m_Value: Current};
1338}
1339
1340float CUi::DoScrollbarV(const void *pId, const CUIRect *pRect, float Current)
1341{
1342 Current = std::clamp(val: Current, lo: 0.0f, hi: 1.0f);
1343
1344 // layout
1345 CUIRect Rail;
1346 pRect->Margin(Cut: 5.0f, pOtherRect: &Rail);
1347
1348 CUIRect Handle;
1349 Rail.HSplitTop(Cut: std::clamp(val: 33.0f, lo: Rail.w, hi: Rail.h / 3.0f), pTop: &Handle, pBottom: nullptr);
1350 Handle.y = Rail.y + (Rail.h - Handle.h) * Current;
1351
1352 // logic
1353 const bool InsideRail = MouseHovered(pRect: &Rail);
1354 const bool InsideHandle = MouseHovered(pRect: &Handle);
1355 bool Grabbed = false; // whether to apply the offset
1356
1357 if(CheckActiveItem(pId))
1358 {
1359 if(MouseButton(Index: 0))
1360 {
1361 Grabbed = true;
1362 if(Input()->ShiftIsPressed())
1363 m_MouseSlow = true;
1364 }
1365 else
1366 {
1367 SetActiveItem(nullptr);
1368 }
1369 }
1370 else if(HotItem() == pId)
1371 {
1372 if(InsideHandle)
1373 {
1374 if(MouseButton(Index: 0))
1375 {
1376 SetActiveItem(pId);
1377 m_ActiveScrollbarOffset = MouseY() - Handle.y;
1378 Grabbed = true;
1379 }
1380 }
1381 else if(MouseButtonClicked(Index: 0))
1382 {
1383 SetActiveItem(pId);
1384 m_ActiveScrollbarOffset = Handle.h / 2.0f;
1385 Grabbed = true;
1386 }
1387 }
1388
1389 if(InsideRail && !MouseButton(Index: 0))
1390 {
1391 SetHotItem(pId);
1392 }
1393
1394 float ReturnValue = Current;
1395 if(Grabbed)
1396 {
1397 const float Min = Rail.y;
1398 const float Max = Rail.h - Handle.h;
1399 const float Cur = MouseY() - m_ActiveScrollbarOffset;
1400 ReturnValue = std::clamp(val: (Cur - Min) / Max, lo: 0.0f, hi: 1.0f);
1401 }
1402
1403 // render
1404 Rail.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: Rail.w / 2.0f);
1405 Handle.Draw(Color: ms_ScrollBarColorFunction.GetColor(Active: CheckActiveItem(pId), Hovered: HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding: Handle.w / 2.0f);
1406
1407 return ReturnValue;
1408}
1409
1410float CUi::DoScrollbarH(const void *pId, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner)
1411{
1412 Current = std::clamp(val: Current, lo: 0.0f, hi: 1.0f);
1413
1414 // layout
1415 CUIRect Rail;
1416 if(pColorInner)
1417 Rail = *pRect;
1418 else
1419 pRect->HMargin(Cut: 5.0f, pOtherRect: &Rail);
1420
1421 CUIRect Handle;
1422 Rail.VSplitLeft(Cut: pColorInner ? 8.0f : std::clamp(val: 33.0f, lo: Rail.h, hi: Rail.w / 3.0f), pLeft: &Handle, pRight: nullptr);
1423 Handle.x += (Rail.w - Handle.w) * Current;
1424
1425 CUIRect HandleArea = Handle;
1426 if(!pColorInner)
1427 {
1428 HandleArea.h = pRect->h * 0.9f;
1429 HandleArea.y = pRect->y + pRect->h * 0.05f;
1430 HandleArea.w += 6.0f;
1431 HandleArea.x -= 3.0f;
1432 }
1433
1434 // logic
1435 const bool InsideRail = MouseHovered(pRect: &Rail);
1436 const bool InsideHandle = MouseHovered(pRect: &HandleArea);
1437 bool Grabbed = false; // whether to apply the offset
1438
1439 if(CheckActiveItem(pId))
1440 {
1441 if(MouseButton(Index: 0))
1442 {
1443 Grabbed = true;
1444 if(Input()->ShiftIsPressed())
1445 m_MouseSlow = true;
1446 }
1447 else
1448 {
1449 SetActiveItem(nullptr);
1450 }
1451 }
1452 else if(HotItem() == pId)
1453 {
1454 if(InsideHandle)
1455 {
1456 if(MouseButton(Index: 0))
1457 {
1458 SetActiveItem(pId);
1459 m_pLastActiveScrollbar = pId;
1460 m_ActiveScrollbarOffset = MouseX() - Handle.x;
1461 Grabbed = true;
1462 }
1463 }
1464 else if(MouseButtonClicked(Index: 0))
1465 {
1466 SetActiveItem(pId);
1467 m_pLastActiveScrollbar = pId;
1468 m_ActiveScrollbarOffset = Handle.w / 2.0f;
1469 Grabbed = true;
1470 }
1471 }
1472
1473 if(!pColorInner && (InsideHandle || Grabbed) && (CheckActiveItem(pId) || HotItem() == pId))
1474 {
1475 Handle.h += 3.0f;
1476 Handle.y -= 1.5f;
1477 }
1478
1479 if(InsideRail && !MouseButton(Index: 0))
1480 {
1481 SetHotItem(pId);
1482 }
1483
1484 float ReturnValue = Current;
1485 if(Grabbed)
1486 {
1487 const float Min = Rail.x;
1488 const float Max = Rail.w - Handle.w;
1489 const float Cur = MouseX() - m_ActiveScrollbarOffset;
1490 ReturnValue = std::clamp(val: (Cur - Min) / Max, lo: 0.0f, hi: 1.0f);
1491 }
1492
1493 // render
1494 const ColorRGBA HandleColor = ms_ScrollBarColorFunction.GetColor(Active: CheckActiveItem(pId), Hovered: HotItem() == pId);
1495 if(pColorInner)
1496 {
1497 CUIRect Slider;
1498 Handle.VMargin(Cut: -2.0f, pOtherRect: &Slider);
1499 Slider.HMargin(Cut: -3.0f, pOtherRect: &Slider);
1500 Slider.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f).Multiply(Other: HandleColor), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1501 Slider.Margin(Cut: 2.0f, pOtherRect: &Slider);
1502 Slider.Draw(Color: pColorInner->Multiply(Other: HandleColor), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
1503 }
1504 else
1505 {
1506 Rail.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: Rail.h / 2.0f);
1507 Handle.Draw(Color: HandleColor, Corners: IGraphics::CORNER_ALL, Rounding: Rail.h / 2.0f);
1508 }
1509
1510 return ReturnValue;
1511}
1512
1513bool 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)
1514{
1515 const bool Infinite = Flags & CUi::SCROLLBAR_OPTION_INFINITE;
1516 const bool NoClampValue = Flags & CUi::SCROLLBAR_OPTION_NOCLAMPVALUE;
1517 const bool MultiLine = Flags & CUi::SCROLLBAR_OPTION_MULTILINE;
1518 const bool DelayUpdate = Flags & CUi::SCROLLBAR_OPTION_DELAYUPDATE;
1519
1520 int PrevValue = (DelayUpdate && m_pLastActiveScrollbar == pId && CheckActiveItem(pId)) ? m_ScrollbarValue : *pOption;
1521 int Value = PrevValue;
1522 if(Infinite)
1523 {
1524 Max += 1;
1525 if(Value == 0)
1526 Value = Max;
1527 }
1528
1529 char aBuf[256];
1530 if(!Infinite || Value != Max)
1531 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %i%s", pStr, Value, pSuffix);
1532 else
1533 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: ∞", pStr);
1534
1535 if(NoClampValue)
1536 {
1537 // clamp the value internally for the scrollbar
1538 Value = std::clamp(val: Value, lo: Min, hi: Max);
1539 }
1540
1541 CUIRect Label, ScrollBar;
1542 if(MultiLine)
1543 pRect->HSplitMid(pTop: &Label, pBottom: &ScrollBar);
1544 else
1545 pRect->VSplitMid(pLeft: &Label, pRight: &ScrollBar, Spacing: std::min(a: 10.0f, b: pRect->w * 0.05f));
1546
1547 const float FontSize = Label.h * CUi::ms_FontmodHeight * 0.8f;
1548 DoLabel(pRect: &Label, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
1549
1550 Value = pScale->ToAbsolute(RelativeValue: DoScrollbarH(pId, pRect: &ScrollBar, Current: pScale->ToRelative(AbsoluteValue: Value, Min, Max)), Min, Max);
1551 if(NoClampValue && ((Value == Min && PrevValue < Min) || (Value == Max && PrevValue > Max)))
1552 {
1553 Value = PrevValue; // use previous out of range value instead if the scrollbar is at the edge
1554 }
1555 else if(Infinite)
1556 {
1557 if(Value == Max)
1558 Value = 0;
1559 }
1560
1561 if(DelayUpdate && m_pLastActiveScrollbar == pId && CheckActiveItem(pId))
1562 {
1563 m_ScrollbarValue = Value;
1564 return false;
1565 }
1566
1567 if(*pOption != Value)
1568 {
1569 *pOption = Value;
1570 return true;
1571 }
1572 return false;
1573}
1574
1575void CUi::RenderProgressBar(CUIRect ProgressBar, float Progress)
1576{
1577 const float Rounding = std::min(a: 5.0f, b: ProgressBar.h / 2.0f);
1578 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding);
1579 ProgressBar.w = std::max(a: ProgressBar.w * Progress, b: 2 * Rounding);
1580 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
1581}
1582
1583void CCachedText::Update(ITextRender *pTextRender, const char *pText, float FontSize, float LineWidth, int CursorFlags)
1584{
1585 if(m_FontSize == FontSize && m_LineWidth == LineWidth && m_CursorFlags == CursorFlags && m_Text == pText)
1586 return;
1587
1588 // The render flags of a text container are fixed when it is created and depend on the
1589 // line width, so only text and font size changes can reuse the existing container and
1590 // upload the new quads into its buffer instead of allocating a new one.
1591 const bool ReuseContainer = m_TextContainerIndex.Valid() && m_LineWidth == LineWidth && m_CursorFlags == CursorFlags;
1592
1593 m_Text = pText;
1594 m_FontSize = FontSize;
1595 m_LineWidth = LineWidth;
1596 m_CursorFlags = CursorFlags;
1597
1598 CTextCursor Cursor;
1599 Cursor.m_FontSize = FontSize;
1600 Cursor.m_LineWidth = LineWidth;
1601 Cursor.m_Flags = CursorFlags;
1602
1603 // The color is applied when rendering, so it must not be baked into the quads.
1604 const ColorRGBA OldColor = pTextRender->GetTextColor();
1605 pTextRender->TextColor(Color: pTextRender->DefaultTextColor());
1606 if(ReuseContainer)
1607 pTextRender->RecreateTextContainerSoft(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_Text.c_str());
1608 else
1609 pTextRender->RecreateTextContainer(TextContainerIndex&: m_TextContainerIndex, pCursor: &Cursor, pText: m_Text.c_str());
1610 pTextRender->TextColor(Color: OldColor);
1611
1612 m_BoundingBox = Cursor.BoundingBox();
1613 m_MaxCharacterHeight = Cursor.m_MaxCharacterHeight;
1614}
1615
1616void CCachedText::Render(ITextRender *pTextRender, vec2 Pos, ColorRGBA Color) const
1617{
1618 if(!m_TextContainerIndex.Valid())
1619 return;
1620 // The quads are built with the default color, so the outline has to be faded here
1621 // instead of inheriting the alpha from the baked vertex color.
1622 pTextRender->RenderTextContainer(TextContainerIndex: m_TextContainerIndex, TextColor: Color, TextOutlineColor: pTextRender->DefaultTextOutlineColor().WithMultipliedAlpha(alpha: Color.a), X: Pos.x, Y: Pos.y);
1623}
1624
1625void CCachedText::Reset(ITextRender *pTextRender)
1626{
1627 pTextRender->DeleteTextContainer(TextContainerIndex&: m_TextContainerIndex);
1628 m_Text.clear();
1629 m_FontSize = -1.0f;
1630 m_LineWidth = -1.0f;
1631 m_CursorFlags = 0;
1632 m_BoundingBox = {.m_X: 0.0f, .m_Y: 0.0f, .m_W: 0.0f, .m_H: 0.0f};
1633 m_MaxCharacterHeight = 0.0f;
1634}
1635
1636void CUi::RenderTime(CUIRect TimeRect, float FontSize, int Seconds, bool NotFinished, int Millis, bool TrueMilliseconds, CCachedText &SecondsText, CCachedText &MillisText, ColorRGBA Color) const
1637{
1638 if(NotFinished)
1639 return;
1640
1641 char aBuf[128];
1642 str_time(centisecs: ((int64_t)absolute(a: Seconds)) * 100, format: ETimeFormat::HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1643 SecondsText.Update(pTextRender: TextRender(), pText: aBuf, FontSize);
1644
1645 // align in vertical middle
1646 vec2 Cursor = TimeRect.TopLeft();
1647 const float SecondsWidth = std::min(a: SecondsText.Width(), b: TimeRect.w);
1648 Cursor.x += TimeRect.w - SecondsWidth; // align right
1649 Cursor.y += ((TimeRect.h - SecondsText.MaxCharacterHeight()) / 2.0f - (FontSize - SecondsText.MaxCharacterHeight()));
1650
1651 // show milliseconds or centiseconds if we are under an hour
1652 if(Millis >= 0 && Seconds < 60 * 60)
1653 {
1654 constexpr float GoldenRatio = 0.61803398875f;
1655 const float CentisecondFontSize = FontSize * GoldenRatio;
1656
1657 // format 2 or 3 digits
1658 char aMillis[4];
1659 Millis %= 1000;
1660 if(!TrueMilliseconds)
1661 str_format(buffer: aMillis, buffer_size: sizeof(aMillis), format: "%02d", (int)std::round(x: Millis / 10));
1662 else
1663 str_format(buffer: aMillis, buffer_size: sizeof(aMillis), format: "%03d", Millis);
1664 MillisText.Update(pTextRender: TextRender(), pText: aMillis, FontSize: CentisecondFontSize);
1665
1666 const float MillisWidth = MillisText.Width();
1667
1668 // make space for millis, but put them 1/6th of a char tighter together
1669 Cursor.x -= MillisWidth - (TrueMilliseconds ? MillisWidth / (3 * 6) : MillisWidth / (2 * 6));
1670
1671 vec2 CursorMillis = TimeRect.TopLeft();
1672 CursorMillis.x += TimeRect.w - MillisWidth; // align right
1673 CursorMillis.y += ((TimeRect.h - MillisText.MaxCharacterHeight()) / 2.0f - (CentisecondFontSize - MillisText.MaxCharacterHeight()));
1674 CursorMillis.y -= (CursorMillis.y - Cursor.y) * GoldenRatio;
1675
1676 SecondsText.Render(pTextRender: TextRender(), Pos: Cursor, Color);
1677 MillisText.Render(pTextRender: TextRender(), Pos: CursorMillis, Color);
1678 }
1679 else
1680 {
1681 SecondsText.Render(pTextRender: TextRender(), Pos: Cursor, Color);
1682 }
1683}
1684
1685void CUi::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) const
1686{
1687 Graphics()->TextureClear();
1688 Graphics()->QuadsBegin();
1689
1690 // The filled and unfilled segments need to begin at the same angle offset
1691 // or the differences in pixel alignment will make the filled segments flicker.
1692 const float SegmentsAngle = 2.0f * pi / Props.m_Segments;
1693 const float InnerRadius = OuterRadius * 0.75f;
1694 const float AngleOffset = -0.5f * pi;
1695 Graphics()->SetColor(Props.m_Color.WithMultipliedAlpha(alpha: 0.5f));
1696 for(int i = 0; i < Props.m_Segments; ++i)
1697 {
1698 const vec2 Dir1 = direction(angle: AngleOffset + i * SegmentsAngle);
1699 const vec2 Dir2 = direction(angle: AngleOffset + (i + 1) * SegmentsAngle);
1700 IGraphics::CFreeformItem Item = IGraphics::CFreeformItem(
1701 Center + Dir1 * InnerRadius, Center + Dir2 * InnerRadius,
1702 Center + Dir1 * OuterRadius, Center + Dir2 * OuterRadius);
1703 Graphics()->QuadsDrawFreeform(pArray: &Item, Num: 1);
1704 }
1705
1706 const float FilledRatio = Props.m_Progress < 0.0f ? 0.333f : Props.m_Progress;
1707 const int FilledSegmentOffset = Props.m_Progress < 0.0f ? round_to_int(f: m_ProgressSpinnerOffset * Props.m_Segments) : 0;
1708 const int FilledNumSegments = std::min(a: (int)(Props.m_Segments * FilledRatio) + (Props.m_Progress < 0.0f ? 0 : 1), b: Props.m_Segments);
1709 Graphics()->SetColor(Props.m_Color);
1710 for(int i = 0; i < FilledNumSegments; ++i)
1711 {
1712 const float Angle1 = AngleOffset + (i + FilledSegmentOffset) * SegmentsAngle;
1713 const float Angle2 = AngleOffset + ((i + 1 == FilledNumSegments && Props.m_Progress >= 0.0f) ? (2.0f * pi * Props.m_Progress) : ((i + FilledSegmentOffset + 1) * SegmentsAngle));
1714 IGraphics::CFreeformItem Item = IGraphics::CFreeformItem(
1715 Center.x + std::cos(x: Angle1) * InnerRadius, Center.y + std::sin(x: Angle1) * InnerRadius,
1716 Center.x + std::cos(x: Angle2) * InnerRadius, Center.y + std::sin(x: Angle2) * InnerRadius,
1717 Center.x + std::cos(x: Angle1) * OuterRadius, Center.y + std::sin(x: Angle1) * OuterRadius,
1718 Center.x + std::cos(x: Angle2) * OuterRadius, Center.y + std::sin(x: Angle2) * OuterRadius);
1719 Graphics()->QuadsDrawFreeform(pArray: &Item, Num: 1);
1720 }
1721
1722 Graphics()->QuadsEnd();
1723}
1724
1725void CUi::DoBackButton()
1726{
1727 if(!g_Config.m_ClBackButton)
1728 return;
1729
1730 MapScreen();
1731 const CUIRect *pScreen = Screen();
1732 const float Size = pScreen->h * 0.1f;
1733 constexpr float PositionScale = 1000000.0f;
1734 const auto ClampPos = [&](vec2 Pos) {
1735 Pos.x = std::clamp(val: Pos.x, lo: 0.0f, hi: pScreen->w - Size);
1736 Pos.y = std::clamp(val: Pos.y, lo: 0.0f, hi: pScreen->h - Size);
1737 return Pos;
1738 };
1739
1740 vec2 ButtonPos = ClampPos({g_Config.m_ClBackButtonX / PositionScale * pScreen->w, g_Config.m_ClBackButtonY / PositionScale * pScreen->h});
1741 CUIRect ButtonRect{.x: ButtonPos.x, .y: ButtonPos.y, .w: Size, .h: Size};
1742
1743 bool Clicked = false;
1744 bool Abrupted = false;
1745 const int Result = DoDraggableButtonLogic(pId: &m_BackButtonId, Checked: 0, pRect: &ButtonRect, pClicked: &Clicked, pAbrupted: &Abrupted);
1746
1747 // Detect the press transition. DoDraggableButtonLogic sets the active item on the
1748 // press frame but returns 0 there, so check CheckActiveItem to catch it.
1749 if(m_BackButtonOp == EBackButtonOp::NONE && CheckActiveItem(pId: &m_BackButtonId))
1750 {
1751 m_BackButtonInitialMouse = MousePos();
1752 m_BackButtonDragOffset = ButtonPos - MousePos();
1753 m_BackButtonOp = EBackButtonOp::CLICKED;
1754 if(m_OnBackButtonPressedFunction)
1755 m_OnBackButtonPressedFunction();
1756 }
1757
1758 if(m_BackButtonOp == EBackButtonOp::CLICKED && length(a: MousePos() - m_BackButtonInitialMouse) > 5.0f)
1759 {
1760 m_BackButtonOp = EBackButtonOp::DRAGGING;
1761 }
1762
1763 if(m_BackButtonOp == EBackButtonOp::DRAGGING)
1764 {
1765 ButtonPos = ClampPos(MousePos() + m_BackButtonDragOffset);
1766 g_Config.m_ClBackButtonX = round_to_int(f: ButtonPos.x / pScreen->w * PositionScale);
1767 g_Config.m_ClBackButtonY = round_to_int(f: ButtonPos.y / pScreen->h * PositionScale);
1768 ButtonRect.x = ButtonPos.x;
1769 ButtonRect.y = ButtonPos.y;
1770 }
1771
1772 if(Result && Clicked)
1773 {
1774 if(m_BackButtonOp == EBackButtonOp::CLICKED && m_DispatchInputFunction)
1775 {
1776 IInput::CEvent Event;
1777 Event.m_Key = KEY_ESCAPE;
1778 Event.m_InputCount = 0;
1779 Event.m_aText[0] = '\0';
1780 Event.m_Flags = IInput::FLAG_PRESS;
1781 m_DispatchInputFunction(Event);
1782 Event.m_Flags = IInput::FLAG_RELEASE;
1783 m_DispatchInputFunction(Event);
1784 }
1785 m_BackButtonOp = EBackButtonOp::NONE;
1786 }
1787 else if(Result && Abrupted)
1788 {
1789 m_BackButtonOp = EBackButtonOp::NONE;
1790 }
1791
1792 m_BackButtonRect = ButtonRect;
1793}
1794
1795void CUi::RenderBackButton()
1796{
1797 if(!g_Config.m_ClBackButton)
1798 return;
1799
1800 MapScreen();
1801
1802 // Override hot/active claims made by UI rendered between DoBackButton and RenderBackButton.
1803 if(m_BackButtonOp != EBackButtonOp::NONE)
1804 SetActiveItem(&m_BackButtonId);
1805 else if(MouseHovered(pRect: &m_BackButtonRect) && !MouseButton(Index: 0) && !MouseButton(Index: 1) && !MouseButton(Index: 2))
1806 SetHotItem(&m_BackButtonId);
1807
1808 const bool Pressed = m_BackButtonOp != EBackButtonOp::NONE;
1809 const bool Hovered = !Pressed && HotItem() == &m_BackButtonId;
1810 const float Alpha = Pressed ? 0.9f : (Hovered ? 0.35f : 0.5f);
1811 m_BackButtonRect.Draw(Color: {0.0f, 0.0f, 0.0f, Alpha}, Corners: IGraphics::CORNER_ALL, Rounding: 12.0f);
1812
1813 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1814 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH |
1815 ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING |
1816 ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1817 DoLabel(pRect: &m_BackButtonRect, pText: FontIcon::CHEVRON_LEFT, Size: m_BackButtonRect.w * 0.5f, Align: TEXTALIGN_MC);
1818 TextRender()->SetRenderFlags(0);
1819 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1820}
1821