| 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 | |
| 14 | void CUi::(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 * = &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 | |
| 34 | void CUi::() |
| 35 | { |
| 36 | for(size_t i = 0; i < m_vPopupMenus.size(); ++i) |
| 37 | { |
| 38 | const SPopupMenu & = 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 = 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 | |
| 88 | void CUi::(const SPopupMenuId *pId, bool IncludeDescendants) |
| 89 | { |
| 90 | auto = std::find_if(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu &) { 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 | |
| 103 | void CUi::() |
| 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 | |
| 114 | bool CUi::() const |
| 115 | { |
| 116 | return !m_vPopupMenus.empty(); |
| 117 | } |
| 118 | |
| 119 | bool CUi::(const SPopupMenuId *pId) const |
| 120 | { |
| 121 | return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [pId](const SPopupMenu ) { return PopupMenu.m_pId == pId; }); |
| 122 | } |
| 123 | |
| 124 | bool CUi::() const |
| 125 | { |
| 126 | return std::any_of(first: m_vPopupMenus.begin(), last: m_vPopupMenus.end(), pred: [this](const SPopupMenu ) { return MouseHovered(pRect: &PopupMenu.m_Rect); }); |
| 127 | } |
| 128 | |
| 129 | void CUi::(FPopupMenuClosedCallback pfnCallback) |
| 130 | { |
| 131 | m_pfnPopupMenuClosedCallback = std::move(pfnCallback); |
| 132 | } |
| 133 | |
| 134 | void CUi::SMessagePopupContext::(ITextRender *pTextRender) |
| 135 | { |
| 136 | m_TextColor = pTextRender->DefaultTextColor(); |
| 137 | } |
| 138 | |
| 139 | void CUi::SMessagePopupContext::() |
| 140 | { |
| 141 | m_TextColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f); |
| 142 | } |
| 143 | |
| 144 | CUi::EPopupMenuFunctionResult CUi::(void *pContext, CUIRect View, bool Active) |
| 145 | { |
| 146 | SMessagePopupContext * = 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 | |
| 156 | void CUi::(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 | |
| 167 | CUi::SConfirmPopupContext::() |
| 168 | { |
| 169 | Reset(); |
| 170 | } |
| 171 | |
| 172 | void CUi::SConfirmPopupContext::() |
| 173 | { |
| 174 | m_Result = SConfirmPopupContext::UNSET; |
| 175 | } |
| 176 | |
| 177 | void CUi::SConfirmPopupContext::() |
| 178 | { |
| 179 | str_copy(dst&: m_aPositiveButtonLabel, src: Localize(pStr: "Yes" )); |
| 180 | str_copy(dst&: m_aNegativeButtonLabel, src: Localize(pStr: "No" )); |
| 181 | } |
| 182 | |
| 183 | void CUi::(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 = 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 | |
| 196 | CUi::EPopupMenuFunctionResult CUi::(void *pContext, CUIRect View, bool Active) |
| 197 | { |
| 198 | SConfirmPopupContext * = 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 | |
| 222 | CUi::SSelectionPopupContext::() |
| 223 | { |
| 224 | Reset(); |
| 225 | } |
| 226 | |
| 227 | void CUi::SSelectionPopupContext::() |
| 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 | |
| 244 | CUi::EPopupMenuFunctionResult CUi::(void *pContext, CUIRect View, bool Active) |
| 245 | { |
| 246 | SSelectionPopupContext * = 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 | |
| 292 | void CUi::(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 = 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 | |
| 321 | int 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 | |
| 365 | CUi::EPopupMenuFunctionResult CUi::(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 | |
| 640 | void CUi::(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 | |