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/dbg.h>
7#include <base/math.h>
8#include <base/str.h>
9
10#include <engine/graphics.h>
11
12#include <game/localization.h>
13
14void CUi::DoPopupMenu(const SPopupMenuId *pId, float X, float Y, float Width, float Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props)
15{
16 constexpr float Margin = SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN;
17 if(X + Width > Screen()->w - Margin)
18 X = std::max(a: X - Width, b: Margin);
19 if(Y + Height > Screen()->h - Margin)
20 Y = std::max(a: Y - Height, b: Margin);
21
22 m_vPopupMenus.emplace_back();
23 SPopupMenu *pNewMenu = &m_vPopupMenus.back();
24 pNewMenu->m_pId = pId;
25 pNewMenu->m_Props = Props;
26 pNewMenu->m_Rect.x = X;
27 pNewMenu->m_Rect.y = Y;
28 pNewMenu->m_Rect.w = Width;
29 pNewMenu->m_Rect.h = Height;
30 pNewMenu->m_pContext = pContext;
31 pNewMenu->m_pfnFunc = pfnFunc;
32}
33
34void CUi::RenderPopupMenus()
35{
36 for(size_t i = 0; i < m_vPopupMenus.size(); ++i)
37 {
38 const SPopupMenu &PopupMenu = m_vPopupMenus[i];
39 const SPopupMenuId *pId = PopupMenu.m_pId;
40 const bool Inside = MouseInside(pRect: &PopupMenu.m_Rect);
41 const bool Active = i == m_vPopupMenus.size() - 1;
42
43 if(Active)
44 {
45 // Prevent UI elements below the popup menu from being activated.
46 SetHotItem(pId);
47 }
48
49 if(CheckActiveItem(pId))
50 {
51 if(!MouseButton(Index: 0))
52 {
53 if(!Inside)
54 {
55 ClosePopupMenu(pId);
56 --i;
57 continue;
58 }
59 SetActiveItem(nullptr);
60 }
61 }
62 else if(HotItem() == pId)
63 {
64 if(MouseButton(Index: 0))
65 SetActiveItem(pId);
66 }
67
68 if(Inside)
69 {
70 // Prevent scroll regions directly behind popup menus from using the mouse scroll events.
71 SetHotScrollRegion(nullptr);
72 }
73
74 CUIRect PopupRect = PopupMenu.m_Rect;
75 PopupRect.Draw(Color: PopupMenu.m_Props.m_BorderColor, Corners: PopupMenu.m_Props.m_Corners, Rounding: 3.0f);
76 PopupRect.Margin(Cut: SPopupMenu::POPUP_BORDER, pOtherRect: &PopupRect);
77 PopupRect.Draw(Color: PopupMenu.m_Props.m_BackgroundColor, Corners: PopupMenu.m_Props.m_Corners, Rounding: 3.0f);
78 PopupRect.Margin(Cut: SPopupMenu::POPUP_MARGIN, pOtherRect: &PopupRect);
79
80 // The popup render function can open/close popups, which may resize the vector and thus
81 // invalidate the variable PopupMenu. We therefore store pId in a separate variable.
82 EPopupMenuFunctionResult Result = PopupMenu.m_pfnFunc(PopupMenu.m_pContext, PopupRect, Active);
83 if(Result != POPUP_KEEP_OPEN || (Active && ConsumeHotkey(Hotkey: HOTKEY_ESCAPE)))
84 ClosePopupMenu(pId, IncludeDescendants: Result == POPUP_CLOSE_CURRENT_AND_DESCENDANTS);
85 }
86}
87
88void CUi::ClosePopupMenu(const SPopupMenuId *pId, bool IncludeDescendants)
89{
90 auto PopupMenuToClose = std::find_if(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu &PopupMenu) { return PopupMenu.m_pId == pId; });
91 if(PopupMenuToClose != m_vPopupMenus.end())
92 {
93 if(IncludeDescendants)
94 m_vPopupMenus.erase(first: PopupMenuToClose, last: m_vPopupMenus.end());
95 else
96 m_vPopupMenus.erase(position: PopupMenuToClose);
97 SetActiveItem(nullptr);
98 if(m_pfnPopupMenuClosedCallback)
99 m_pfnPopupMenuClosedCallback();
100 }
101}
102
103void CUi::ClosePopupMenus()
104{
105 if(m_vPopupMenus.empty())
106 return;
107
108 m_vPopupMenus.clear();
109 SetActiveItem(nullptr);
110 if(m_pfnPopupMenuClosedCallback)
111 m_pfnPopupMenuClosedCallback();
112}
113
114bool CUi::IsPopupOpen() const
115{
116 return !m_vPopupMenus.empty();
117}
118
119bool CUi::IsPopupOpen(const SPopupMenuId *pId) const
120{
121 return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu PopupMenu) { return PopupMenu.m_pId == pId; });
122}
123
124bool CUi::IsPopupHovered() const
125{
126 return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [this](const SPopupMenu PopupMenu) { return MouseHovered(pRect: &PopupMenu.m_Rect); });
127}
128
129void CUi::SetPopupMenuClosedCallback(FPopupMenuClosedCallback pfnCallback)
130{
131 m_pfnPopupMenuClosedCallback = std::move(pfnCallback);
132}
133
134void CUi::SMessagePopupContext::DefaultColor(ITextRender *pTextRender)
135{
136 m_TextColor = pTextRender->DefaultTextColor();
137}
138
139void CUi::SMessagePopupContext::ErrorColor()
140{
141 m_TextColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f);
142}
143
144CUi::EPopupMenuFunctionResult CUi::PopupMessage(void *pContext, CUIRect View, bool Active)
145{
146 SMessagePopupContext *pMessagePopup = static_cast<SMessagePopupContext *>(pContext);
147 CUi *pUI = pMessagePopup->m_pUI;
148
149 pUI->TextRender()->TextColor(Color: pMessagePopup->m_TextColor);
150 pUI->TextRender()->Text(x: View.x, y: View.y, Size: SMessagePopupContext::POPUP_FONT_SIZE, pText: pMessagePopup->m_aMessage, LineWidth: View.w);
151 pUI->TextRender()->TextColor(Color: pUI->TextRender()->DefaultTextColor());
152
153 return (Active && pUI->ConsumeHotkey(Hotkey: HOTKEY_ENTER)) ? CUi::POPUP_CLOSE_CURRENT : CUi::POPUP_KEEP_OPEN;
154}
155
156void CUi::ShowPopupMessage(float X, float Y, SMessagePopupContext *pContext)
157{
158 const float TextWidth = std::min(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);
159 float TextHeight = 0.0f;
160 STextSizeProperties TextSizeProps{};
161 TextSizeProps.m_pHeight = &TextHeight;
162 TextRender()->TextWidth(Size: SMessagePopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: TextWidth, Flags: 0, TextSizeProps);
163 pContext->m_pUI = this;
164 DoPopupMenu(pId: pContext, X, Y, Width: TextWidth + 10.0f, Height: TextHeight + 10.0f, pContext, pfnFunc: PopupMessage);
165}
166
167CUi::SConfirmPopupContext::SConfirmPopupContext()
168{
169 Reset();
170}
171
172void CUi::SConfirmPopupContext::Reset()
173{
174 m_Result = SConfirmPopupContext::UNSET;
175}
176
177void CUi::SConfirmPopupContext::YesNoButtons()
178{
179 str_copy(dst&: m_aPositiveButtonLabel, src: Localize(pStr: "Yes"));
180 str_copy(dst&: m_aNegativeButtonLabel, src: Localize(pStr: "No"));
181}
182
183void CUi::ShowPopupConfirm(float X, float Y, SConfirmPopupContext *pContext)
184{
185 const float TextWidth = std::min(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);
186 float TextHeight = 0.0f;
187 STextSizeProperties TextSizeProps{};
188 TextSizeProps.m_pHeight = &TextHeight;
189 TextRender()->TextWidth(Size: SConfirmPopupContext::POPUP_FONT_SIZE, pText: pContext->m_aMessage, StrLength: -1, LineWidth: TextWidth, Flags: 0, TextSizeProps);
190 const float PopupHeight = TextHeight + SConfirmPopupContext::POPUP_BUTTON_HEIGHT + SConfirmPopupContext::POPUP_BUTTON_SPACING + 10.0f;
191 pContext->m_pUI = this;
192 pContext->m_Result = SConfirmPopupContext::UNSET;
193 DoPopupMenu(pId: pContext, X, Y, Width: TextWidth + 10.0f, Height: PopupHeight, pContext, pfnFunc: PopupConfirm);
194}
195
196CUi::EPopupMenuFunctionResult CUi::PopupConfirm(void *pContext, CUIRect View, bool Active)
197{
198 SConfirmPopupContext *pConfirmPopup = static_cast<SConfirmPopupContext *>(pContext);
199 CUi *pUI = pConfirmPopup->m_pUI;
200
201 CUIRect Label, ButtonBar, CancelButton, ConfirmButton;
202 View.HSplitBottom(Cut: SConfirmPopupContext::POPUP_BUTTON_HEIGHT, pTop: &Label, pBottom: &ButtonBar);
203 ButtonBar.VSplitMid(pLeft: &CancelButton, pRight: &ConfirmButton, Spacing: SConfirmPopupContext::POPUP_BUTTON_SPACING);
204
205 pUI->TextRender()->Text(x: Label.x, y: Label.y, Size: SConfirmPopupContext::POPUP_FONT_SIZE, pText: pConfirmPopup->m_aMessage, LineWidth: Label.w);
206
207 if(pUI->DoButton_PopupMenu(pButtonContainer: &pConfirmPopup->m_CancelButton, pText: pConfirmPopup->m_aNegativeButtonLabel, pRect: &CancelButton, Size: SConfirmPopupContext::POPUP_FONT_SIZE, Align: TEXTALIGN_MC))
208 {
209 pConfirmPopup->m_Result = SConfirmPopupContext::CANCELED;
210 return CUi::POPUP_CLOSE_CURRENT;
211 }
212
213 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)))
214 {
215 pConfirmPopup->m_Result = SConfirmPopupContext::CONFIRMED;
216 return CUi::POPUP_CLOSE_CURRENT;
217 }
218
219 return CUi::POPUP_KEEP_OPEN;
220}
221
222CUi::SSelectionPopupContext::SSelectionPopupContext()
223{
224 Reset();
225}
226
227void CUi::SSelectionPopupContext::Reset()
228{
229 m_Props = SPopupMenuProperties();
230 m_aMessage[0] = '\0';
231 m_pSelection = nullptr;
232 m_SelectionIndex = -1;
233 m_vEntries.clear();
234 m_vButtonContainers.clear();
235 m_EntryHeight = 12.0f;
236 m_EntryPadding = 0.0f;
237 m_EntrySpacing = 5.0f;
238 m_FontSize = 10.0f;
239 m_Width = 300.0f + (SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN) * 2;
240 m_AlignmentHeight = -1.0f;
241 m_TransparentButtons = false;
242}
243
244CUi::EPopupMenuFunctionResult CUi::PopupSelection(void *pContext, CUIRect View, bool Active)
245{
246 SSelectionPopupContext *pSelectionPopup = static_cast<SSelectionPopupContext *>(pContext);
247 CUi *pUI = pSelectionPopup->m_pUI;
248 CScrollRegion *pScrollRegion = pSelectionPopup->m_pScrollRegion;
249
250 CScrollRegionParams ScrollParams;
251 ScrollParams.m_ScrollbarThickness = 10.0f;
252 ScrollParams.m_ScrollbarMargin = SPopupMenu::POPUP_MARGIN;
253 ScrollParams.m_ScrollbarNoOuterMargin = true;
254 ScrollParams.m_ScrollUnit = 3 * (pSelectionPopup->m_EntryHeight + pSelectionPopup->m_EntrySpacing);
255 pScrollRegion->Begin(pClipRect: &View, pParams: &ScrollParams);
256
257 CUIRect Slot;
258 if(pSelectionPopup->m_aMessage[0] != '\0')
259 {
260 const STextBoundingBox TextBoundingBox = pUI->TextRender()->TextBoundingBox(Size: pSelectionPopup->m_FontSize, pText: pSelectionPopup->m_aMessage, StrLength: -1, LineWidth: pSelectionPopup->m_Width);
261 View.HSplitTop(Cut: TextBoundingBox.m_H, pTop: &Slot, pBottom: &View);
262 if(pScrollRegion->AddRect(Rect: Slot))
263 {
264 pUI->TextRender()->Text(x: Slot.x, y: Slot.y, Size: pSelectionPopup->m_FontSize, pText: pSelectionPopup->m_aMessage, LineWidth: Slot.w);
265 }
266 }
267
268 pSelectionPopup->m_vButtonContainers.resize(sz: pSelectionPopup->m_vEntries.size());
269
270 size_t Index = 0;
271 for(const auto &Entry : pSelectionPopup->m_vEntries)
272 {
273 if(pSelectionPopup->m_aMessage[0] != '\0' || Index != 0)
274 View.HSplitTop(Cut: pSelectionPopup->m_EntrySpacing, pTop: nullptr, pBottom: &View);
275 View.HSplitTop(Cut: pSelectionPopup->m_EntryHeight, pTop: &Slot, pBottom: &View);
276 if(pScrollRegion->AddRect(Rect: Slot))
277 {
278 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))
279 {
280 pSelectionPopup->m_pSelection = &Entry;
281 pSelectionPopup->m_SelectionIndex = Index;
282 }
283 }
284 ++Index;
285 }
286
287 pScrollRegion->End();
288
289 return pSelectionPopup->m_pSelection == nullptr ? CUi::POPUP_KEEP_OPEN : CUi::POPUP_CLOSE_CURRENT;
290}
291
292void CUi::ShowPopupSelection(float X, float Y, SSelectionPopupContext *pContext)
293{
294 const STextBoundingBox TextBoundingBox = TextRender()->TextBoundingBox(Size: pContext->m_FontSize, pText: pContext->m_aMessage, StrLength: -1, LineWidth: pContext->m_Width);
295 const float PopupHeight = std::min(a: (pContext->m_aMessage[0] == '\0' ? -pContext->m_EntrySpacing : TextBoundingBox.m_H) + pContext->m_vEntries.size() * (pContext->m_EntryHeight + pContext->m_EntrySpacing) + (SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN) * 2, b: Screen()->h * 0.4f);
296 pContext->m_pUI = this;
297 pContext->m_pSelection = nullptr;
298 pContext->m_SelectionIndex = -1;
299 pContext->m_Props.m_Corners = IGraphics::CORNER_ALL;
300 if(pContext->m_AlignmentHeight >= 0.0f)
301 {
302 constexpr float Margin = SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN;
303 if(X + pContext->m_Width > Screen()->w - Margin)
304 {
305 X = std::max(a: X - pContext->m_Width, b: Margin);
306 }
307 if(Y + pContext->m_AlignmentHeight + PopupHeight > Screen()->h - Margin)
308 {
309 Y -= PopupHeight;
310 pContext->m_Props.m_Corners = IGraphics::CORNER_T;
311 }
312 else
313 {
314 Y += pContext->m_AlignmentHeight;
315 pContext->m_Props.m_Corners = IGraphics::CORNER_B;
316 }
317 }
318 DoPopupMenu(pId: pContext, X, Y, Width: pContext->m_Width, Height: PopupHeight, pContext, pfnFunc: PopupSelection, Props: pContext->m_Props);
319}
320
321int CUi::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Num, SDropDownState &State)
322{
323 if(!State.m_Init)
324 {
325 State.m_UiElement.Init(pUI: this, RequestedRectCount: -1);
326 State.m_Init = true;
327 }
328
329 const auto LabelFunc = [CurSelection, pStrs]() {
330 return CurSelection > -1 ? pStrs[CurSelection] : "";
331 };
332
333 SMenuButtonProperties Props;
334 Props.m_HintRequiresStringCheck = true;
335 Props.m_HintCanChangePositionOrSize = true;
336 Props.m_ShowDropDownIcon = true;
337 if(IsPopupOpen(pId: &State.m_SelectionPopupContext))
338 Props.m_Corners = IGraphics::CORNER_ALL & (~State.m_SelectionPopupContext.m_Props.m_Corners);
339 if(DoButton_Menu(UIElement&: State.m_UiElement, pId: &State.m_ButtonContainer, GetTextLambda: LabelFunc, pRect, Props))
340 {
341 State.m_SelectionPopupContext.Reset();
342 State.m_SelectionPopupContext.m_Props.m_BorderColor = ColorRGBA(0.7f, 0.7f, 0.7f, 0.9f);
343 State.m_SelectionPopupContext.m_Props.m_BackgroundColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f);
344 for(int i = 0; i < Num; ++i)
345 State.m_SelectionPopupContext.m_vEntries.emplace_back(args&: pStrs[i]);
346 State.m_SelectionPopupContext.m_EntryHeight = pRect->h;
347 State.m_SelectionPopupContext.m_EntryPadding = pRect->h >= 20.0f ? 2.0f : 1.0f;
348 State.m_SelectionPopupContext.m_FontSize = (State.m_SelectionPopupContext.m_EntryHeight - 2 * State.m_SelectionPopupContext.m_EntryPadding) * CUi::ms_FontmodHeight;
349 State.m_SelectionPopupContext.m_Width = pRect->w;
350 State.m_SelectionPopupContext.m_AlignmentHeight = pRect->h;
351 State.m_SelectionPopupContext.m_TransparentButtons = true;
352 ShowPopupSelection(X: pRect->x, Y: pRect->y, pContext: &State.m_SelectionPopupContext);
353 }
354
355 if(State.m_SelectionPopupContext.m_SelectionIndex >= 0)
356 {
357 const int NewSelection = State.m_SelectionPopupContext.m_SelectionIndex;
358 State.m_SelectionPopupContext.Reset();
359 return NewSelection;
360 }
361
362 return CurSelection;
363}
364
365CUi::EPopupMenuFunctionResult CUi::PopupColorPicker(void *pContext, CUIRect View, bool Active)
366{
367 SColorPickerPopupContext *pColorPicker = static_cast<SColorPickerPopupContext *>(pContext);
368 CUi *pUI = pColorPicker->m_pUI;
369 pColorPicker->m_State = EEditState::NONE;
370
371 CUIRect ColorsArea, HueArea, BottomArea, ModeButtonArea, HueRect, SatRect, ValueRect, HexRect, AlphaRect;
372
373 View.HSplitTop(Cut: 140.0f, pTop: &ColorsArea, pBottom: &BottomArea);
374 ColorsArea.VSplitRight(Cut: 20.0f, pLeft: &ColorsArea, pRight: &HueArea);
375
376 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
377 HueArea.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &HueArea);
378
379 BottomArea.HSplitTop(Cut: 20.0f, pTop: &HueRect, pBottom: &BottomArea);
380 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
381
382 constexpr float ValuePadding = 5.0f;
383 const float HsvValueWidth = (HueRect.w - ValuePadding * 2) / 3.0f;
384 const float HexValueWidth = HsvValueWidth * 2 + ValuePadding;
385
386 HueRect.VSplitLeft(Cut: HsvValueWidth, pLeft: &HueRect, pRight: &SatRect);
387 SatRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &SatRect);
388 SatRect.VSplitLeft(Cut: HsvValueWidth, pLeft: &SatRect, pRight: &ValueRect);
389 ValueRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &ValueRect);
390
391 BottomArea.HSplitTop(Cut: 20.0f, pTop: &HexRect, pBottom: &BottomArea);
392 BottomArea.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &BottomArea);
393 HexRect.VSplitLeft(Cut: HexValueWidth, pLeft: &HexRect, pRight: &AlphaRect);
394 AlphaRect.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &AlphaRect);
395 BottomArea.HSplitTop(Cut: 20.0f, pTop: &ModeButtonArea, pBottom: &BottomArea);
396
397 const ColorRGBA BlackColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
398
399 HueArea.Draw(Color: BlackColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
400 HueArea.Margin(Cut: 1.0f, pOtherRect: &HueArea);
401
402 ColorsArea.Draw(Color: BlackColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
403 ColorsArea.Margin(Cut: 1.0f, pOtherRect: &ColorsArea);
404
405 ColorHSVA PickerColorHSV = pColorPicker->m_HsvaColor;
406 ColorRGBA PickerColorRGB = pColorPicker->m_RgbaColor;
407 ColorHSLA PickerColorHSL = pColorPicker->m_HslaColor;
408
409 // Color Area
410 ColorRGBA TL, TR, BL, BR;
411 TL = BL = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 0.0f, 1.0f));
412 TR = BR = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 1.0f, 1.0f));
413 ColorsArea.Draw4(ColorTopLeft: TL, ColorTopRight: TR, ColorBottomLeft: BL, ColorBottomRight: BR, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
414
415 TL = TR = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
416 BL = BR = ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f);
417 ColorsArea.Draw4(ColorTopLeft: TL, ColorTopRight: TR, ColorBottomLeft: BL, ColorBottomRight: BR, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
418
419 // Hue Area
420 static const float s_aaColorIndices[7][3] = {
421 {1.0f, 0.0f, 0.0f}, // red
422 {1.0f, 0.0f, 1.0f}, // magenta
423 {0.0f, 0.0f, 1.0f}, // blue
424 {0.0f, 1.0f, 1.0f}, // cyan
425 {0.0f, 1.0f, 0.0f}, // green
426 {1.0f, 1.0f, 0.0f}, // yellow
427 {1.0f, 0.0f, 0.0f}, // red
428 };
429
430 const float HuePickerOffset = HueArea.h / 6.0f;
431 CUIRect HuePartialArea = HueArea;
432 HuePartialArea.h = HuePickerOffset;
433
434 for(size_t j = 0; j < std::size(s_aaColorIndices) - 1; j++)
435 {
436 TL = ColorRGBA(s_aaColorIndices[j][0], s_aaColorIndices[j][1], s_aaColorIndices[j][2], 1.0f);
437 BL = ColorRGBA(s_aaColorIndices[j + 1][0], s_aaColorIndices[j + 1][1], s_aaColorIndices[j + 1][2], 1.0f);
438
439 HuePartialArea.y = HueArea.y + HuePickerOffset * j;
440 HuePartialArea.Draw4(ColorTopLeft: TL, ColorTopRight: TL, ColorBottomLeft: BL, ColorBottomRight: BL, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
441 }
442
443 const auto &&RenderAlphaSelector = [&](unsigned OldA) -> SEditResult<int64_t> {
444 if(pColorPicker->m_Alpha)
445 {
446 return pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[3], pRect: &AlphaRect, pLabel: "A:", Current: OldA, Min: 0, Max: 255);
447 }
448 else
449 {
450 char aBuf[8];
451 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "A: %d", OldA);
452 pUI->DoLabel(pRect: &AlphaRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
453 AlphaRect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.65f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
454 return {.m_State: EEditState::NONE, .m_Value: OldA};
455 }
456 };
457
458 // Editboxes Area
459 if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSVA)
460 {
461 const unsigned OldH = round_to_int(f: PickerColorHSV.h * 255.0f);
462 const unsigned OldS = round_to_int(f: PickerColorHSV.s * 255.0f);
463 const unsigned OldV = round_to_int(f: PickerColorHSV.v * 255.0f);
464 const unsigned OldA = round_to_int(f: PickerColorHSV.a * 255.0f);
465
466 const auto [StateH, H] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "H:", Current: OldH, Min: 0, Max: 255);
467 const auto [StateS, S] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "S:", Current: OldS, Min: 0, Max: 255);
468 const auto [StateV, V] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "V:", Current: OldV, Min: 0, Max: 255);
469 const auto [StateA, A] = RenderAlphaSelector(OldA);
470
471 if(OldH != H || OldS != S || OldV != V || OldA != A)
472 {
473 PickerColorHSV = ColorHSVA(H / 255.0f, S / 255.0f, V / 255.0f, A / 255.0f);
474 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
475 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
476 }
477
478 for(auto State : {StateH, StateS, StateV, StateA})
479 {
480 if(State != EEditState::NONE)
481 {
482 pColorPicker->m_State = State;
483 break;
484 }
485 }
486 }
487 else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_RGBA)
488 {
489 const unsigned OldR = round_to_int(f: PickerColorRGB.r * 255.0f);
490 const unsigned OldG = round_to_int(f: PickerColorRGB.g * 255.0f);
491 const unsigned OldB = round_to_int(f: PickerColorRGB.b * 255.0f);
492 const unsigned OldA = round_to_int(f: PickerColorRGB.a * 255.0f);
493
494 const auto [StateR, R] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "R:", Current: OldR, Min: 0, Max: 255);
495 const auto [StateG, G] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "G:", Current: OldG, Min: 0, Max: 255);
496 const auto [StateB, B] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "B:", Current: OldB, Min: 0, Max: 255);
497 const auto [StateA, A] = RenderAlphaSelector(OldA);
498
499 if(OldR != R || OldG != G || OldB != B || OldA != A)
500 {
501 PickerColorRGB = ColorRGBA(R / 255.0f, G / 255.0f, B / 255.0f, A / 255.0f);
502 PickerColorHSL = color_cast<ColorHSLA>(rgb: PickerColorRGB);
503 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
504 }
505
506 for(auto State : {StateR, StateG, StateB, StateA})
507 {
508 if(State != EEditState::NONE)
509 {
510 pColorPicker->m_State = State;
511 break;
512 }
513 }
514 }
515 else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSLA)
516 {
517 const unsigned OldH = round_to_int(f: PickerColorHSL.h * 255.0f);
518 const unsigned OldS = round_to_int(f: PickerColorHSL.s * 255.0f);
519 const unsigned OldL = round_to_int(f: PickerColorHSL.l * 255.0f);
520 const unsigned OldA = round_to_int(f: PickerColorHSL.a * 255.0f);
521
522 const auto [StateH, H] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[0], pRect: &HueRect, pLabel: "H:", Current: OldH, Min: 0, Max: 255);
523 const auto [StateS, S] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[1], pRect: &SatRect, pLabel: "S:", Current: OldS, Min: 0, Max: 255);
524 const auto [StateL, L] = pUI->DoValueSelectorWithState(pId: &pColorPicker->m_aValueSelectorIds[2], pRect: &ValueRect, pLabel: "L:", Current: OldL, Min: 0, Max: 255);
525 const auto [StateA, A] = RenderAlphaSelector(OldA);
526
527 if(OldH != H || OldS != S || OldL != L || OldA != A)
528 {
529 PickerColorHSL = ColorHSLA(H / 255.0f, S / 255.0f, L / 255.0f, A / 255.0f);
530 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
531 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
532 }
533
534 for(auto State : {StateH, StateS, StateL, StateA})
535 {
536 if(State != EEditState::NONE)
537 {
538 pColorPicker->m_State = State;
539 break;
540 }
541 }
542 }
543 else
544 {
545 dbg_assert_failed("Color picker mode invalid: %d", (int)pColorPicker->m_ColorMode);
546 }
547
548 SValueSelectorProperties Props;
549 Props.m_UseScroll = false;
550 Props.m_IsHex = true;
551 Props.m_HexPrefix = pColorPicker->m_Alpha ? 8 : 6;
552 const unsigned OldHex = PickerColorRGB.PackAlphaLast(Alpha: pColorPicker->m_Alpha);
553 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);
554 if(OldHex != Hex)
555 {
556 const float OldAlpha = PickerColorRGB.a;
557 PickerColorRGB = ColorRGBA::UnpackAlphaLast<ColorRGBA>(Color: Hex, Alpha: pColorPicker->m_Alpha);
558 if(!pColorPicker->m_Alpha)
559 PickerColorRGB.a = OldAlpha;
560 PickerColorHSL = color_cast<ColorHSLA>(rgb: PickerColorRGB);
561 PickerColorHSV = color_cast<ColorHSVA>(hsl: PickerColorHSL);
562 }
563
564 if(HexState != EEditState::NONE)
565 pColorPicker->m_State = HexState;
566
567 // Logic
568 float PickerX, PickerY;
569 EEditState ColorPickerRes = pUI->DoPickerLogic(pId: &pColorPicker->m_ColorPickerId, pRect: &ColorsArea, pX: &PickerX, pY: &PickerY);
570 if(ColorPickerRes != EEditState::NONE)
571 {
572 PickerColorHSV.y = PickerX / ColorsArea.w;
573 PickerColorHSV.z = 1.0f - PickerY / ColorsArea.h;
574 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
575 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
576 pColorPicker->m_State = ColorPickerRes;
577 }
578
579 EEditState HuePickerRes = pUI->DoPickerLogic(pId: &pColorPicker->m_HuePickerId, pRect: &HueArea, pX: &PickerX, pY: &PickerY);
580 if(HuePickerRes != EEditState::NONE)
581 {
582 PickerColorHSV.x = 1.0f - PickerY / HueArea.h;
583 PickerColorHSL = color_cast<ColorHSLA>(hsv: PickerColorHSV);
584 PickerColorRGB = color_cast<ColorRGBA>(hsl: PickerColorHSL);
585 pColorPicker->m_State = HuePickerRes;
586 }
587
588 // Marker Color Area
589 const float MarkerX = ColorsArea.x + ColorsArea.w * PickerColorHSV.y;
590 const float MarkerY = ColorsArea.y + ColorsArea.h * (1.0f - PickerColorHSV.z);
591
592 const float MarkerOutlineInd = PickerColorHSV.z > 0.5f ? 0.0f : 1.0f;
593 const ColorRGBA MarkerOutline = ColorRGBA(MarkerOutlineInd, MarkerOutlineInd, MarkerOutlineInd, 1.0f);
594
595 pUI->Graphics()->TextureClear();
596 pUI->Graphics()->QuadsBegin();
597 pUI->Graphics()->SetColor(MarkerOutline);
598 pUI->Graphics()->DrawCircle(CenterX: MarkerX, CenterY: MarkerY, Radius: 4.5f, Segments: 32);
599 pUI->Graphics()->SetColor(PickerColorRGB);
600 pUI->Graphics()->DrawCircle(CenterX: MarkerX, CenterY: MarkerY, Radius: 3.5f, Segments: 32);
601 pUI->Graphics()->QuadsEnd();
602
603 // Marker Hue Area
604 CUIRect HueMarker;
605 HueArea.Margin(Cut: -2.5f, pOtherRect: &HueMarker);
606 HueMarker.h = 6.5f;
607 HueMarker.y = (HueArea.y + HueArea.h * (1.0f - PickerColorHSV.x)) - HueMarker.h / 2.0f;
608
609 const ColorRGBA HueMarkerColor = color_cast<ColorRGBA>(hsv: ColorHSVA(PickerColorHSV.x, 1.0f, 1.0f, 1.0f));
610 const float HueMarkerOutlineColor = PickerColorHSV.x > 0.75f ? 1.0f : 0.0f;
611 const ColorRGBA HueMarkerOutline = ColorRGBA(HueMarkerOutlineColor, HueMarkerOutlineColor, HueMarkerOutlineColor, 1.0f);
612
613 HueMarker.Draw(Color: HueMarkerOutline, Corners: IGraphics::CORNER_ALL, Rounding: 1.2f);
614 HueMarker.Margin(Cut: 1.2f, pOtherRect: &HueMarker);
615 HueMarker.Draw(Color: HueMarkerColor, Corners: IGraphics::CORNER_ALL, Rounding: 1.2f);
616
617 pColorPicker->m_HsvaColor = PickerColorHSV;
618 pColorPicker->m_RgbaColor = PickerColorRGB;
619 pColorPicker->m_HslaColor = PickerColorHSL;
620 if(pColorPicker->m_pHslaColor != nullptr)
621 *pColorPicker->m_pHslaColor = PickerColorHSL.Pack(Alpha: pColorPicker->m_Alpha);
622
623 static constexpr SColorPickerPopupContext::EColorPickerMode PICKER_MODES[] = {SColorPickerPopupContext::MODE_HSVA, SColorPickerPopupContext::MODE_RGBA, SColorPickerPopupContext::MODE_HSLA};
624 static constexpr const char *PICKER_MODE_LABELS[] = {"HSVA", "RGBA", "HSLA"};
625 static_assert(std::size(PICKER_MODES) == std::size(PICKER_MODE_LABELS));
626 for(SColorPickerPopupContext::EColorPickerMode Mode : PICKER_MODES)
627 {
628 CUIRect ModeButton;
629 ModeButtonArea.VSplitLeft(Cut: HsvValueWidth, pLeft: &ModeButton, pRight: &ModeButtonArea);
630 ModeButtonArea.VSplitLeft(Cut: ValuePadding, pLeft: nullptr, pRight: &ModeButtonArea);
631 if(pUI->DoButton_PopupMenu(pButtonContainer: &pColorPicker->m_aModeButtons[(int)Mode], pText: PICKER_MODE_LABELS[Mode], pRect: &ModeButton, Size: 10.0f, Align: TEXTALIGN_MC, Padding: 2.0f, TransparentInactive: false, Enabled: pColorPicker->m_ColorMode != Mode))
632 {
633 pColorPicker->m_ColorMode = Mode;
634 }
635 }
636
637 return CUi::POPUP_KEEP_OPEN;
638}
639
640void CUi::ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext)
641{
642 pContext->m_pUI = this;
643 if(pContext->m_ColorMode == SColorPickerPopupContext::MODE_UNSET)
644 pContext->m_ColorMode = SColorPickerPopupContext::MODE_HSVA;
645 DoPopupMenu(pId: pContext, X, Y, Width: 160.0f + 10.0f, Height: 209.0f + 10.0f, pContext, pfnFunc: PopupColorPicker);
646}
647