diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index 4a96b54d5..132134d7a 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -68,6 +68,12 @@ constexpr wchar_t WSZ_GAME_TITLE1_b[] = L"C"; constexpr wchar_t WSZ_GAME_TITLE2[] = L"Hrebuild"; #define SZ_WEBSITE "https://neotokyorebuild.github.io" +enum ENeoPopup +{ + NEOPOPUP_ACTIONSERVER = NeoUI::INTERNALPOPUP_NIL + 1, + NEOPOPUP_ACTIONBLACKLIST, +}; + ConCommand neo_toggleconsole("neo_toggleconsole", NeoToggleconsole, "toggle the console", FCVAR_DONTRECORD); struct YMD @@ -158,68 +164,6 @@ static void AddToBlacklist(const gameserveritem_t *gameServer) g_blacklistedServers.AddToTail(sbInfo); } -static void RightClickRetServer(void *pData, const int iItem) -{ - CNeoRoot *pNeoRoot = (CNeoRoot *)(pData); - - Assert(pNeoRoot->m_iServerBrowserTab != GS_BLACKLIST && pNeoRoot->m_iSelectedServer >= 0); - if (pNeoRoot->m_iServerBrowserTab == GS_BLACKLIST || pNeoRoot->m_iSelectedServer < 0) - { - return; - } - - const auto *gameServer = &pNeoRoot->m_serverBrowser[pNeoRoot->m_iServerBrowserTab].m_filteredServers[pNeoRoot->m_iSelectedServer]; - - switch (iItem) - { - case CNeoRoot::RIGHTCLICKSERVER_FAV: - { - if (ISteamMatchmaking *smm = SteamMatchmaking()) - { - const servernetadr_t &netAdr = gameServer->m_NetAdr; - if (NetAdrIsFavorite(netAdr)) - { - smm->RemoveFavoriteGame( - engine->GetAppID(), - netAdr.GetIP(), - netAdr.GetConnectionPort(), - netAdr.GetQueryPort(), - k_unFavoriteFlagFavorite); - } - else - { - smm->AddFavoriteGame( - engine->GetAppID(), - netAdr.GetIP(), - netAdr.GetConnectionPort(), - netAdr.GetQueryPort(), - k_unFavoriteFlagFavorite, - 0); - } - } - } break; - case CNeoRoot::RIGHTCLICKSERVER_BLACKLIST: - AddToBlacklist(gameServer); - break; - default: - break; - } -} - -static void RightClickRetRemoveBlacklist(void *pData, [[maybe_unused]] const int iItem) -{ - CNeoRoot *pNeoRoot = (CNeoRoot *)(pData); - - Assert(pNeoRoot->m_iServerBrowserTab == GS_BLACKLIST && pNeoRoot->m_iSelectedServer >= 0); - if (pNeoRoot->m_iServerBrowserTab != GS_BLACKLIST || pNeoRoot->m_iSelectedServer < 0) - { - return; - } - - g_blacklistedServers.Remove(pNeoRoot->m_iSelectedServer); - pNeoRoot->m_iSelectedServer = -1; -} - CNeoRootInput::CNeoRootInput(CNeoRoot *rootPanel) : Panel(rootPanel, "NeoRootPanelInput") , m_pNeoRoot(rootPanel) @@ -1460,9 +1404,12 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) m_iSelectedServer = i; if (btn.bMouseRightPressed) { - static const wchar_t *PWSZ_BLACKLIST_RIGHTCLICK[] = { L"Remove from blacklist" }; - NeoUI::PopupMenu(PWSZ_BLACKLIST_RIGHTCLICK, ARRAYSIZE(PWSZ_BLACKLIST_RIGHTCLICK), - RightClickRetRemoveBlacklist, (void *)(this)); + NeoUI::OpenPopup(NEOPOPUP_ACTIONBLACKLIST, NeoUI::Dim{ + .x = g_uiCtx.iMouseAbsX, + .y = g_uiCtx.iMouseAbsY, + .wide = NeoUI::PopupWideByStr("Remove from blacklist"), + .tall = g_uiCtx.layout.iDefRowTall, + }); } } @@ -1533,10 +1480,18 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) m_iSelectedServer = i; if (btn.bMouseRightPressed) { - const bool bIsFav = NetAdrIsFavorite(server.m_NetAdr); - pwszRightclickServer[RIGHTCLICKSERVER_FAV] = bIsFav ? L"Unfavorite" : L"Favorite"; - NeoUI::PopupMenu(pwszRightclickServer, RIGHTCLICKSERVER__TOTAL, - RightClickRetServer, (void *)(this)); + NeoUI::OpenPopup(NEOPOPUP_ACTIONSERVER, NeoUI::Dim{ + .x = g_uiCtx.iMouseAbsX, + .y = g_uiCtx.iMouseAbsY, + .wide = NeoUI::PopupWideByStr("Add to blacklist"), + .tall = g_uiCtx.layout.iDefRowTall * 2, + }); + + // NetAdrIsFavorite cached here for the popup + const auto *gameServer = &m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer]; + const servernetadr_t &netAdr = gameServer->m_NetAdr; + m_bFavCacheIsFav = NetAdrIsFavorite(netAdr); + m_favCacheNetAdr = netAdr; } if (btn.bKeyPressed || btn.bMouseDoublePressed) { @@ -1718,8 +1673,71 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); + if (NeoUI::BeginPopup(NEOPOPUP_ACTIONSERVER, NeoUI::POPUPFLAG_COLORHOTASACTIVE)) + { + const bool bIsValid = (m_iServerBrowserTab != GS_BLACKLIST && m_iSelectedServer >= 0); + Assert(bIsValid); + const auto *pGameServer = bIsValid ? &m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer] : nullptr; + + if (NeoUI::Button(m_bFavCacheIsFav ? L"Unfavorite" : L"Favorite").bPressed) + { + ISteamMatchmaking *smm = SteamMatchmaking(); + if (smm && pGameServer) + { + const servernetadr_t &netAdr = pGameServer->m_NetAdr; + if (NetAdrIsFavorite(netAdr)) + { + smm->RemoveFavoriteGame( + engine->GetAppID(), + netAdr.GetIP(), + netAdr.GetConnectionPort(), + netAdr.GetQueryPort(), + k_unFavoriteFlagFavorite); + } + else + { + smm->AddFavoriteGame( + engine->GetAppID(), + netAdr.GetIP(), + netAdr.GetConnectionPort(), + netAdr.GetQueryPort(), + k_unFavoriteFlagFavorite, + 0); + } + } + NeoUI::ClosePopup(); + } + if (NeoUI::Button(L"Add to blacklist").bPressed) + { + if (pGameServer) + { + AddToBlacklist(pGameServer); + } + NeoUI::ClosePopup(); + } + + NeoUI::EndPopup(); + } + + if (NeoUI::BeginPopup(NEOPOPUP_ACTIONBLACKLIST, NeoUI::POPUPFLAG_COLORHOTASACTIVE)) + { + if (NeoUI::Button(L"Remove from blacklist").bPressed) + { + const bool bIsValid = (m_iServerBrowserTab == GS_BLACKLIST && m_iSelectedServer >= 0); + Assert(bIsValid); + if (bIsValid) + { + g_blacklistedServers.Remove(m_iSelectedServer); + m_iSelectedServer = -1; + } + NeoUI::ClosePopup(); + } + + NeoUI::EndPopup(); + } + + NeoUI::EndContext(); } static constexpr const wchar_t *CREDITSPEOPLELABEL_NAMES[] = { diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index 536d56263..47f38519a 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -231,15 +231,6 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener servernetadr_t m_favCacheNetAdr = {}; bool m_bFavCacheIsFav = false; bool m_bAutoRefreshFav = false; - - enum ERightClickServer - { - RIGHTCLICKSERVER_FAV = 0, - RIGHTCLICKSERVER_BLACKLIST, - - RIGHTCLICKSERVER__TOTAL, - }; - const wchar_t *pwszRightclickServer[RIGHTCLICKSERVER__TOTAL] = { L"", L"Add to blacklist" }; }; extern CNeoRoot *g_pNeoRoot; diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 5d93562a4..258cee5bf 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1355,10 +1355,11 @@ void NeoSettings_Crosshair(NeoSettings *ns) { NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT); NeoUI::RingBox(L"Crosshair style", CROSSHAIR_LABELS, CROSSHAIR_STYLE__TOTAL, &pCrosshair->info.iStyle); - NeoUI::SliderU8(L"Red", &pCrosshair->info.color[0], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Green", &pCrosshair->info.color[1], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Blue", &pCrosshair->info.color[2], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Alpha", &pCrosshair->info.color[3], 0, UCHAR_MAX); + NeoUI::ColorEdit(L"Crosshair color", + &pCrosshair->info.color[0], + &pCrosshair->info.color[1], + &pCrosshair->info.color[2], + &pCrosshair->info.color[3]); if (!bTextured) { NeoUI::RingBox(L"Size type", CROSSHAIR_SIZETYPE_LABELS, CROSSHAIR_SIZETYPE__TOTAL, &pCrosshair->info.iESizeType); @@ -1372,10 +1373,11 @@ void NeoSettings_Crosshair(NeoSettings *ns) NeoUI::SliderInt(L"Outline", &pCrosshair->info.iOutline, 0, CROSSHAIR_MAX_OUTLINE); if (pCrosshair->info.iOutline > 0) { - NeoUI::SliderU8(L"Outline color: Red", &pCrosshair->info.colorOutline[0], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Outline color: Green", &pCrosshair->info.colorOutline[1], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Outline color: Blue", &pCrosshair->info.colorOutline[2], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Outline color: Alpha", &pCrosshair->info.colorOutline[3], 0, UCHAR_MAX); + NeoUI::ColorEdit(L"Outline color", + &pCrosshair->info.colorOutline[0], + &pCrosshair->info.colorOutline[1], + &pCrosshair->info.colorOutline[2], + &pCrosshair->info.colorOutline[3]); } NeoUI::SliderInt(L"Center dot", &pCrosshair->info.iCenterDot, 0, CROSSHAIR_MAX_CENTER_DOT); if (pCrosshair->info.iCenterDot > 0) @@ -1383,16 +1385,18 @@ void NeoSettings_Crosshair(NeoSettings *ns) NeoUI::RingBoxBool(L"Separate dot color", &pCrosshair->info.bSeparateColorDot); if (pCrosshair->info.bSeparateColorDot) { - NeoUI::SliderU8(L"Dot color: Red", &pCrosshair->info.colorDot[0], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot color: Green", &pCrosshair->info.colorDot[1], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot color: Blue", &pCrosshair->info.colorDot[2], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot color: Alpha", &pCrosshair->info.colorDot[3], 0, UCHAR_MAX); + NeoUI::ColorEdit(L"Dot color", + &pCrosshair->info.colorDot[0], + &pCrosshair->info.colorDot[1], + &pCrosshair->info.colorDot[2], + &pCrosshair->info.colorDot[3]); if (pCrosshair->info.iOutline > 0) { - NeoUI::SliderU8(L"Dot outline color: Red", &pCrosshair->info.colorDotOutline[0], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot outline color: Green", &pCrosshair->info.colorDotOutline[1], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot outline color: Blue", &pCrosshair->info.colorDotOutline[2], 0, UCHAR_MAX); - NeoUI::SliderU8(L"Dot outline color: Alpha", &pCrosshair->info.colorDotOutline[3], 0, UCHAR_MAX); + NeoUI::ColorEdit(L"Dot outline", + &pCrosshair->info.colorDotOutline[0], + &pCrosshair->info.colorDotOutline[1], + &pCrosshair->info.colorDotOutline[2], + &pCrosshair->info.colorDotOutline[3]); } } } diff --git a/src/game/client/neo/ui/neo_theme.cpp b/src/game/client/neo/ui/neo_theme.cpp index 383d4b9cd..7fa00bce5 100644 --- a/src/game/client/neo/ui/neo_theme.cpp +++ b/src/game/client/neo/ui/neo_theme.cpp @@ -18,10 +18,7 @@ void SetupNTRETheme(NeoUI::Context *pNeoUICtx) pColors->cursor = COLOR_BLACK; pColors->textSelectionBg = COLOR_DARK_RED; pColors->divider = Color(220, 220, 220, 200); - pColors->menuFg = COLOR_WHITE; - pColors->menuBg = COLOR_BLACK; - pColors->menuHotFg = COLOR_BLACK; - pColors->menuHotBg = COLOR_WHITE; + pColors->popupBg = COLOR_BLACK; pColors->sectionBg = COLOR_BLACK_TRANSPARENT; pColors->scrollbarBg = COLOR_TRANSPARENT; pColors->scrollbarHandleNormalBg = COLOR_FADED_WHITE; diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index b00528b81..7534d209b 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -33,11 +33,6 @@ static constexpr float FL_BORDER_RATIO = 0.2f; static_assert(DEBUG_NEOUI == 0); #endif -static bool RightClickMenuActive() -{ - return (c->iRightClick >= 0 && c->iRightClickSection >= 0 && c->pwszRightClickList && c->ipwszRightClickListSize > 0); -} - void SwapFont(const EFont eFont, const bool bForce) { if (c->eMode != MODE_PAINT || (!bForce && (c->eFont == eFont))) return; @@ -139,7 +134,20 @@ void BeginMultiWidgetHighlighter(const int iTotalWidgets) } // Apply highlight if it's hot/active - const bool bHot = c->iHotSection == c->iSection && IN_BETWEEN_AR(c->iWidget, c->iHot, c->iWidget + iTotalWidgets); + bool bHot = c->iHotSection == c->iSection && IN_BETWEEN_AR(c->iWidget, c->iHot, c->iWidget + iTotalWidgets); + if (bHot && (c->dimPopup.wide > 0 && c->dimPopup.tall > 0) && + !(c->popupFlags & POPUPFLAG__INPOPUPSECTION)) + { + const Dim &dim = c->dimPopup; + bHot = !(IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) && + IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall)); + if (!bHot) + { + c->iHot = FOCUSOFF_NUM; + c->iHotSection = -1; + } + } + const bool bActive = c->iActiveSection == c->iSection && IN_BETWEEN_AR(c->iWidget, c->iActive, c->iWidget + iTotalWidgets); if (bHot || bActive) { @@ -222,13 +230,16 @@ static bool IsKeyDownWidget() { return c->eCode == KEY_DOWN || c->eCode == KEY_XBUTTON_DOWN || c->eCode == KEY_XSTICK1_DOWN || c->eCode == STEAMCONTROLLER_DPAD_DOWN || + (c->iCurPopupId != 0 && c->eCode == KEY_TAB) || ((c->iSectionFlags & SECTIONFLAG_ROWWIDGETS) && IsKeyRight()); } static bool IsKeyUpWidget() { + const bool bIsShiftDown = (vgui::input()->IsKeyDown(KEY_LSHIFT) || vgui::input()->IsKeyDown(KEY_RSHIFT)); return c->eCode == KEY_UP || c->eCode == KEY_XBUTTON_UP || c->eCode == KEY_XSTICK1_UP || c->eCode == STEAMCONTROLLER_DPAD_UP || + (c->iCurPopupId != 0 && c->eCode == KEY_TAB && bIsShiftDown) || ((c->iSectionFlags & SECTIONFLAG_ROWWIDGETS) && IsKeyLeft()); } @@ -244,7 +255,8 @@ bool BindKeyEnter() bool BindKeyBack() { - return c->eMode == MODE_KEYPRESSED && IsKeyBack(); + return c->eMode == MODE_KEYPRESSED && IsKeyBack() && + (false == (c->dimPopup.wide > 0 && c->dimPopup.tall > 0)); } void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar_t *wszTitle, const char *pSzCtxName) @@ -276,6 +288,14 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar c->htSliders.RemoveAll(); c->pSzCurCtxName = pSzCtxName; c->ibfSectionHasScroll = 0; + c->iCurPopupId = 0; + V_memset(&c->dimPopup, 0, sizeof(Dim)); + c->colorEditInfo.r = nullptr; + c->colorEditInfo.g = nullptr; + c->colorEditInfo.b = nullptr; + c->colorEditInfo.a = nullptr; + c->iPrePopupActive = c->iPrePopupHot = FOCUSOFF_NUM; + c->iPrePopupActiveSection = c->iPrePopupHotSection = -1; } switch (c->eMode) @@ -346,106 +366,37 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar void EndContext() { - if (RightClickMenuActive()) + if (BeginPopup(INTERNALPOPUP_COLOREDIT, POPUPFLAG_FOCUSONOPEN)) { - bool bDeInitRightClick = false; - switch (c->eMode) - { - case MODE_PAINT: - { - const Dim &dim = c->dimRightClick; - vgui::surface()->DrawSetColor(c->colors.menuBg); - vgui::surface()->DrawFilledRect( - dim.x, - dim.y, - dim.x + dim.wide, - dim.y + dim.tall); - for (int i = 0, iNextY = dim.y; - i < c->ipwszRightClickListSize; - ++i, iNextY += c->layout.iRowTall) - { - const bool bIsHot = c->iRightClickHotItem == i; - if (bIsHot) - { - vgui::surface()->DrawSetColor(c->colors.menuHotBg); - vgui::surface()->DrawFilledRect( - dim.x, - iNextY, - dim.x + dim.wide, - iNextY + c->layout.iRowTall); - } - const wchar *pwszItemLabel = c->pwszRightClickList[i]; - vgui::surface()->DrawSetTextPos(dim.x + c->iMarginX, - iNextY + c->iMarginY); - vgui::surface()->DrawSetColor(bIsHot ? c->colors.menuHotFg : c->colors.menuFg); - vgui::surface()->DrawPrintText(pwszItemLabel, V_wcslen(pwszItemLabel)); - } - } break; - case MODE_KEYPRESSED: - case MODE_KEYTYPED: - { - bDeInitRightClick = true; - } break; - case MODE_MOUSEPRESSED: + static const int ROWLAYOUT_COLORSPLIT[] = { 30, -1 }; + SetPerRowLayout(2, ROWLAYOUT_COLORSPLIT); + SliderU8(L"Red", c->colorEditInfo.r, 0, UCHAR_MAX); + SliderU8(L"Green", c->colorEditInfo.g, 0, UCHAR_MAX); + SliderU8(L"Blue", c->colorEditInfo.b, 0, UCHAR_MAX); + SliderU8(L"Alpha", c->colorEditInfo.a, 0, UCHAR_MAX); + + EndPopup(); + } + + if (BeginPopup(INTERNALPOPUP_COPYMENU, POPUPFLAG_COLORHOTASACTIVE)) + { + if (Button(L"Cut").bPressed) { - bDeInitRightClick = true; - // Wide/tall set to 0 indicates it's a newly initialized right-click menu - if (c->dimRightClick.wide == 0 || c->dimRightClick.tall == 0) - { - const auto *pFontI = &c->fonts[c->eFont]; - int iMinTextSize = 0; - for (int i = 0; i < c->ipwszRightClickListSize; ++i) - { - const wchar *pwszItemLabel = c->pwszRightClickList[i]; - iMinTextSize = Max(iMinTextSize, V_wcslen(pwszItemLabel)); - } - // Empty text size should never really happen, but just in-case, just don't - // init the menu - Assert(iMinTextSize > 0); - if (iMinTextSize > 0) - { - const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); - c->dimRightClick.wide = (c->iMarginX * 2) + (iMinTextSize * iChWidth); - c->dimRightClick.tall = c->ipwszRightClickListSize * c->layout.iRowTall; - bDeInitRightClick = false; - } - } - } break; - case MODE_MOUSEMOVED: + c->eRightClickCopyMenuRet = COPYMENU_CUT; + ClosePopup(); + } + if (Button(L"Copy").bPressed) { - const Dim &dim = c->dimRightClick; - if (dim.wide > 0 && dim.tall > 0 && - IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) && - IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall)) - { - const int iRelMenuPosY = c->iMouseAbsY - dim.y; - c->iRightClickHotItem = (iRelMenuPosY / static_cast(dim.tall)) * c->ipwszRightClickListSize; - } - else - { - c->iRightClickHotItem = -1; - } - } break; - default: - break; + c->eRightClickCopyMenuRet = COPYMENU_COPY; + ClosePopup(); } - - if (bDeInitRightClick) + if (Button(L"Paste").bPressed) { - if (c->fnRightClickRet && IN_BETWEEN_AR(0, c->iRightClickHotItem, c->ipwszRightClickListSize)) - { - c->fnRightClickRet(c->pData, c->iRightClickHotItem); - } - - // Already set wide/tall indicates right-click menu already initalized before - // and can now de-initialize/close - c->iRightClick = -1; - c->iRightClickSection = -1; - c->pwszRightClickList = nullptr; - c->ipwszRightClickListSize = 0; - c->fnRightClickRet = nullptr; - c->pData = nullptr; + c->eRightClickCopyMenuRet = COPYMENU_PASTE; + ClosePopup(); } + + EndPopup(); } if (c->eMode == MODE_MOUSEPRESSED && !c->iHasMouseInPanel) @@ -457,7 +408,7 @@ void EndContext() if (c->eMode == MODE_KEYPRESSED) { const bool bSwitchSectionController = c->eCode == KEY_XBUTTON_BACK || c->eCode == STEAMCONTROLLER_SELECT; - const bool bSwitchSectionKey = c->eCode == KEY_TAB; + const bool bSwitchSectionKey = c->iCurPopupId == 0 && c->eCode == KEY_TAB; if (IsKeyChangeWidgetFocus() || bSwitchSectionKey || bSwitchSectionController) { if (c->iActiveSection == -1) @@ -526,6 +477,15 @@ void BeginSection(const ISectionFlags iSectionFlags) c->iSectionFlags = iSectionFlags; c->uMultiHighlightFlags = MULTIHIGHLIGHTFLAG_NONE; + if (iSectionFlags & SECTIONFLAG_POPUP) + { + c->popupFlags |= POPUPFLAG__INPOPUPSECTION; + } + else + { + c->popupFlags &= ~(POPUPFLAG__INPOPUPSECTION); + } + c->iMouseRelX = c->iMouseAbsX - c->dPanel.x; c->iMouseRelY = c->iMouseAbsY - c->dPanel.y; c->bMouseInPanel = IN_BETWEEN_EQ(0, c->iMouseRelX, c->dPanel.wide - 1) && IN_BETWEEN_EQ(0, c->iMouseRelY, c->dPanel.tall - 1); @@ -535,7 +495,9 @@ void BeginSection(const ISectionFlags iSectionFlags) switch (c->eMode) { case MODE_PAINT: - vgui::surface()->DrawSetColor(c->colors.sectionBg); + vgui::surface()->DrawSetColor((iSectionFlags & SECTIONFLAG_POPUP) + ? c->colors.popupBg + : c->colors.sectionBg); vgui::surface()->DrawFilledRect(c->dPanel.x, c->dPanel.y, c->dPanel.x + c->dPanel.wide, @@ -545,9 +507,14 @@ void BeginSection(const ISectionFlags iSectionFlags) vgui::surface()->DrawSetTextColor(c->colors.normalFg); break; case MODE_KEYPRESSED: - if ((iSectionFlags & SECTIONFLAG_DEFAULTFOCUS) && c->iActiveSection == -1 && IsKeyChangeWidgetFocus()) + if (IsKeyChangeWidgetFocus()) { - c->iActiveSection = c->iSection; + const bool bDefaultFocus = (iSectionFlags & SECTIONFLAG_DEFAULTFOCUS) && (c->iActiveSection == -1); + const bool bPopupFocus = (iSectionFlags & SECTIONFLAG_POPUP); + if (bDefaultFocus || bPopupFocus) + { + c->iActiveSection = c->iSection; + } } break; } @@ -558,8 +525,10 @@ void BeginSection(const ISectionFlags iSectionFlags) void EndSection() { + // If button pressed inside of the section, but after the final + // widget, then should also be treated like outside of section if (c->eMode == MODE_MOUSEPRESSED && c->bMouseInPanel && - c->irWidgetLayoutY < c->iMouseRelY) + (c->irWidgetLayoutY + c->irWidgetTall) < c->iMouseRelY) { c->iActive = FOCUSOFF_NUM; c->iActiveSection = -1; @@ -706,6 +675,77 @@ void EndSection() ++c->iSection; } +void OpenPopup(const int iPopupId, const Dim &dimPopupInit) +{ + if (c->iCurPopupId != 0) + { + ClosePopup(); + } + + c->iCurPopupId = iPopupId; + c->popupFlags |= POPUPFLAG__NEWOPENPOPUP; + + c->iPrePopupHot = c->iHot; + c->iPrePopupHotSection = c->iHotSection; + + c->iPrePopupActive = c->iActive; + c->iPrePopupActiveSection = c->iActiveSection; + + V_memcpy(&c->dimPopup, &dimPopupInit, sizeof(Dim)); +} + +void ClosePopup() +{ + c->iCurPopupId = 0; + + c->iHot = c->iPrePopupHot; + c->iHotSection = c->iPrePopupHotSection; + + c->iActive = c->iPrePopupActive; + c->iActiveSection = c->iPrePopupActiveSection; + + V_memset(&c->dimPopup, 0, sizeof(Dim)); +} + +bool BeginPopup(const int iPopupId, const PopupFlags flags) +{ + if (iPopupId != c->iCurPopupId) + { + return false; + } + + const Dim &dim = c->dimPopup; + const bool bPressOutsidePopup = (c->eMode == MODE_MOUSEPRESSED && c->eCode == MOUSE_LEFT && + !(c->popupFlags & POPUPFLAG__NEWOPENPOPUP) && + !( IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) + && IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall))); + if (bPressOutsidePopup || (c->eMode == MODE_KEYPRESSED && IsKeyBack())) + { + ClosePopup(); + return false; + } + + c->popupFlags &= ~(POPUPFLAG__EXTERNAL); + c->popupFlags |= (flags & POPUPFLAG__EXTERNAL); + + V_memcpy(&c->dPanel, &dim, sizeof(Dim)); + BeginSection(SECTIONFLAG_POPUP); + return true; +} + +void EndPopup() +{ + EndSection(); + c->popupFlags &= ~(POPUPFLAG__NEWOPENPOPUP); +} + +int PopupWideByStr(const char *pszStr) +{ + const auto *pFontI = &c->fonts[c->eFont]; + const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); + return (c->iMarginX * 2) + (V_strlen(pszStr) * iChWidth); +} + void SetPerRowLayout(const int iColTotal, const int *iColProportions, const int iRowTall) { // Bump y-axis with previous row layout before applying new row layout @@ -873,22 +913,37 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) c->iHotSection = c->iSection; } bHot = (c->iHot == c->iWidget && c->iHotSection == c->iSection); - if (bHot && RightClickMenuActive()) + if (bHot && (c->dimPopup.wide > 0 && c->dimPopup.tall > 0) && + !(c->popupFlags & POPUPFLAG__INPOPUPSECTION)) { - const Dim &dim = c->dimRightClick; + const Dim &dim = c->dimPopup; bHot = !(IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) && IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall)); + if (!bHot) + { + c->iHot = FOCUSOFF_NUM; + c->iHotSection = -1; + } } bActive = (c->iWidget == c->iActive && c->iSection == c->iActiveSection); } - if (eWidgetFlag & WIDGETFLAG_FORCEACTIVE) + static const PopupFlags POPUPFLAGS_FIRSTACTIVE = POPUPFLAG_FOCUSONOPEN | POPUPFLAG__NEWOPENPOPUP | POPUPFLAG__INPOPUPSECTION; + const bool bIsPopupFirstActive = + (((c->popupFlags & POPUPFLAGS_FIRSTACTIVE) == POPUPFLAGS_FIRSTACTIVE) + && (eWidgetFlag & WIDGETFLAG_MARKACTIVE)); + + if ((eWidgetFlag & WIDGETFLAG_FORCEACTIVE) || bIsPopupFirstActive) { c->iActive = c->iHot = c->iWidget; c->iActiveSection = c->iHotSection = c->iSection; bHot = true; bActive = true; + if (bIsPopupFirstActive) + { + c->popupFlags &= ~(POPUPFLAG_FOCUSONOPEN); + } } // Mark this section this widget under as able to be active @@ -902,8 +957,10 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) } // Deal with inital widget colors, could also color active/hot by mutli-highlighting + static const PopupFlags POPUPFLAGS_COLORHOTASACTIVE = POPUPFLAG_COLORHOTASACTIVE | POPUPFLAG__INPOPUPSECTION; const bool bColorHot = bHot || (c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_HOT); - const bool bColorActive = bActive || (c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_ACTIVE); + const bool bColorActive = ((bActive || (c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_ACTIVE)) || + (((c->popupFlags & POPUPFLAGS_COLORHOTASACTIVE) == POPUPFLAGS_COLORHOTASACTIVE) && bColorHot)); if (bColorActive) { vgui::surface()->DrawSetColor(c->colors.activeBg); @@ -1979,6 +2036,62 @@ void SliderU8(const wchar_t *wszLeftLabel, uint8 *ucValue, const uint8 iMin, con } } +void ColorEdit(uint8 *r, uint8 *g, uint8 *b, uint8 *a) +{ + const auto wdgState = BeginWidget(WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE); + + if (wdgState.bInView) + { + bool bShowPopup = false; + + switch (c->eMode) + { + case MODE_PAINT: + { + vgui::surface()->DrawSetColor(*r, *g, *b, *a); + vgui::surface()->DrawFilledRectArray(&c->rWidgetArea, 1); + } + break; + case MODE_MOUSEPRESSED: + { + bShowPopup = (wdgState.bHot && c->eCode == MOUSE_LEFT); + } + break; + case MODE_KEYPRESSED: + { + bShowPopup = (wdgState.bActive && IsKeyEnter()); + } + break; + default: + break; + } + + if (bShowPopup) + { + OpenPopup(INTERNALPOPUP_COLOREDIT, Dim{ + .x = c->rWidgetArea.x0, + .y = c->rWidgetArea.y1, + .wide = c->irWidgetWide, + .tall = c->layout.iDefRowTall * 4, + }); + c->colorEditInfo.r = r; + c->colorEditInfo.g = g; + c->colorEditInfo.b = b; + c->colorEditInfo.a = a; + } + } + + EndWidget(wdgState); +} + +void ColorEdit(const wchar_t *wszLeftLabel, uint8 *r, uint8 *g, uint8 *b, uint8 *a) +{ + BeginMultiWidgetHighlighter(2); + Label(wszLeftLabel); + ColorEdit(r, g, b, a); + EndMultiWidgetHighlighter(); +} + static int TextEditChIdxFromMouse(const int iWszTextSize) { const int iMouseOnXWidth = c->iMouseAbsX - (c->rWidgetArea.x0 + c->iMarginX); @@ -2067,7 +2180,15 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f if (wdgState.bInView) { - int iCopyAction = -1; + ECopyMenu iCopyAction = COPYMENU_NIL; + + if (IN_BETWEEN_AR(COPYMENU_CUT, c->eRightClickCopyMenuRet, COPYMENU__TOTAL) && + c->iPrePopupActive == c->iWidget && + c->iPrePopupActiveSection == c->iSection) + { + iCopyAction = c->eRightClickCopyMenuRet; + c->eRightClickCopyMenuRet = COPYMENU_NIL; + } const bool bHasTextSel = wdgState.bActive; const bool bCurRightStart = (c->iTextSelCur >= c->iTextSelStart); @@ -2174,14 +2295,7 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f break; case MODE_MOUSEPRESSED: { - if (c->eCode == MOUSE_LEFT && - c->iRightClick == c->iWidget && - c->iRightClickSection == c->iSection) - { - iCopyAction = c->iRightClickHotItem; - } - - if (!IN_BETWEEN_AR(0, iCopyAction, c->ipwszRightClickListSize) && wdgState.bHot) + if (!IN_BETWEEN_AR(COPYMENU_CUT, iCopyAction, COPYMENU__TOTAL) && wdgState.bHot) { c->iActive = c->iWidget; c->iActiveSection = c->iSection; @@ -2202,11 +2316,13 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f case MOUSE_RIGHT: { // Open right-click menu - static const wchar *PWSZ_TEXTEDIT_RIGHTCLICK[COPYMENU__TOTAL] = { - L"Cut", L"Copy", L"Paste", - }; - PopupMenu(PWSZ_TEXTEDIT_RIGHTCLICK, ARRAYSIZE(PWSZ_TEXTEDIT_RIGHTCLICK), - nullptr, nullptr); + OpenPopup(INTERNALPOPUP_COPYMENU, Dim{ + .x = c->iMouseAbsX, + .y = c->iMouseAbsY, + .wide = PopupWideByStr("Paste"), + .tall = c->layout.iDefRowTall * 3, + }); + c->eRightClickCopyMenuRet = COPYMENU_NIL; } break; default: break; @@ -2241,7 +2357,7 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f break; default: break; } } - if (!IN_BETWEEN_AR(0, iCopyAction, c->ipwszRightClickListSize) && wdgState.bActive) + if (!IN_BETWEEN_AR(COPYMENU_CUT, iCopyAction, COPYMENU__TOTAL) && wdgState.bActive) { const bool bIsShiftDown = (vgui::input()->IsKeyDown(KEY_LSHIFT) || vgui::input()->IsKeyDown(KEY_RSHIFT)); switch (c->eCode) @@ -2541,19 +2657,4 @@ const wchar_t *HintAlt(const wchar *wszKey, const wchar *wszController) return (c->eKeyHints == NeoUI::KEYHINTS_KEYBOARD) ? wszKey : wszController; } -void PopupMenu(const wchar **pwszRightClickList, const int ipwszRightClickListSize, - void (*fnRightClickRet)(void *, int), void *pData) -{ - c->dimRightClick.x = c->iMouseAbsX; - c->dimRightClick.y = c->iMouseAbsY; - c->dimRightClick.wide = 0; - c->dimRightClick.tall = 0; - c->iRightClick = c->iWidget; - c->iRightClickSection = c->iSection; - c->pwszRightClickList = pwszRightClickList; - c->ipwszRightClickListSize = ipwszRightClickListSize; - c->fnRightClickRet = fnRightClickRet; - c->pData = pData; -} - } // namespace NeoUI diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index a37fd5bbd..b7f3d00d0 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -90,7 +90,7 @@ enum TextStyle }; static constexpr int FOCUSOFF_NUM = -1000; -static constexpr int MAX_SECTIONS = 5; +static constexpr int MAX_SECTIONS = 6; static constexpr int SIZEOF_SECTIONS = sizeof(int) * MAX_SECTIONS; static constexpr int MAX_TEXTINPUT_U8BYTES_LIMIT = 256; @@ -126,6 +126,7 @@ enum EFont enum ECopyMenu { + COPYMENU_NIL = 0, COPYMENU_CUT, COPYMENU_COPY, COPYMENU_PASTE, @@ -154,6 +155,21 @@ struct DynWidgetInfos bool bCannotActive; }; +enum EInternalPopup +{ + INTERNALPOPUP_COLOREDIT = -2, + INTERNALPOPUP_COPYMENU = -1, + INTERNALPOPUP_NIL = 0, +}; + +struct ColorEditInfo +{ + uint8 *r; + uint8 *g; + uint8 *b; + uint8 *a; +}; + enum ESectionFlag { SECTIONFLAG_NONE = 0, @@ -168,6 +184,8 @@ enum ESectionFlag SECTIONFLAG_EXCLUDECONTROLLER = 1 << 2, // Allow the hover and button click sound to play on buttons SECTIONFLAG_PLAYBUTTONSOUNDS = 1 << 3, + // Internal use only: Mark this section as a popup + SECTIONFLAG_POPUP = 1 << 4, }; typedef int ISectionFlags; @@ -185,6 +203,21 @@ enum EMultiHighlightFlag : uint8 MULTIHIGHLIGHTFLAG_ACTIVE = 1 << 2, }; +// This is utilized as both lower BeginPopup options flags and upper internal bool flags +enum PopupFlag_ +{ + // Public flags, used as BeginPopup options + POPUPFLAG_NIL = 0, + POPUPFLAG_FOCUSONOPEN = 1 << 0, // Direct focus to this popup when it opens + POPUPFLAG_COLORHOTASACTIVE = 1 << 1, // Color hot widgets as active, mainly for menu-style popups + + // Internal only flags + POPUPFLAG__EXTERNAL = ((1 << 8) - 1), // Mask of all external/options flags below start of internal + POPUPFLAG__INPOPUPSECTION = 1 << 8, // Inside a Begin/EndPopup section + POPUPFLAG__NEWOPENPOPUP = 1 << 9, // The popup just initialized +}; +typedef int PopupFlags; + struct Colors { Color normalFg; @@ -199,10 +232,7 @@ struct Colors Color cursor; Color textSelectionBg; Color divider; - Color menuFg; - Color menuBg; - Color menuHotFg; - Color menuHotBg; + Color popupBg; Color sectionBg; Color scrollbarBg; Color scrollbarHandleNormalBg; @@ -307,21 +337,34 @@ struct Context int iActiveSection; bool bValueEdited; + // Saved hot + active when OpenPopup activates + // Restored when ClosePopup + int iPrePopupHot; + int iPrePopupHotSection; + + int iPrePopupActive; + int iPrePopupActiveSection; + MouseStart eMousePressedStart; const char *pSzCurCtxName; CUtlHashtable htSliders; CUtlHashtable htTexMap; - // Right click menu - int iRightClick = -1; - int iRightClickSection = -1; - int iRightClickHotItem = -1; - int ipwszRightClickListSize = 0; - const wchar **pwszRightClickList = nullptr; - Dim dimRightClick = {}; - void (*fnRightClickRet)(void *, int) = nullptr; - void *pData = nullptr; + // # Popup mechanism variables + + // Cached popup size to readahead for mouse area checking + Dim dimPopup = {}; + PopupFlags popupFlags = POPUPFLAG_NIL; + + // 0 = reserved for nil ID, negative numbers = reserved for + // internal popups, only start using from 1 onwards for + // popups done externally + int iCurPopupId = 0; + + // # Internal managed popups variables + ECopyMenu eRightClickCopyMenuRet = COPYMENU_NIL; // Right-click copy menu + ColorEditInfo colorEditInfo; // Color editor popup // TextEdit text selection int iTextSelStart = -1; @@ -379,6 +422,15 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar void EndContext(); void BeginSection(const ISectionFlags iSectionFlags = SECTIONFLAG_NONE); void EndSection(); + +void OpenPopup(const int iPopupId, const Dim &dimPopupInit); +void ClosePopup(); +[[nodiscard]] bool BeginPopup(const int iPopupId, const PopupFlags flags = POPUPFLAG_NIL); +void EndPopup(); + +// Get a suitable wide size for a popup by the longest text in the popup +int PopupWideByStr(const char *pszStr); + [[nodiscard]] CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag = WIDGETFLAG_NONE); void EndWidget(const CurrentWidgetState &wdgState); @@ -429,6 +481,8 @@ typedef int TabsFlags; const wchar_t *wszSpecialText = nullptr); /*2W*/ void SliderU8(const wchar_t *wszLeftLabel, uint8 *ucValue, const uint8 iMin, const uint8 iMax, const uint8 iStep = 1, const wchar_t *wszSpecialText = nullptr); +/*1W*/ void ColorEdit(uint8 *r, uint8 *g, uint8 *b, uint8 *a); +/*2W*/ void ColorEdit(const wchar_t *wszLeftLabel, uint8 *r, uint8 *g, uint8 *b, uint8 *a); enum TextEditFlag_ { @@ -451,10 +505,6 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt const char *szTextureGroup = "", const TextureOptFlags texFlags = TEXTUREOPTFLAGS_NONE); void ResetTextures(); -// Right-click menu -void PopupMenu(const wchar **pwszRightClickList, const int ipwszRightClickListSize, - void (*fnRightClickRet)(void *, int), void *pData); - // Non-widgets/convenience functions bool Bind(const ButtonCode_t eCode); bool Bind(const ButtonCode_t *peCode, const int ieCodeSize);