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