diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index de9f6034e..d2e116dca 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -57,6 +57,10 @@ extern CNeoLoading *g_pNeoLoading; inline NeoUI::Context g_uiCtx; inline ConVar cl_neo_toggleconsole("cl_neo_toggleconsole", "1", FCVAR_ARCHIVE, "If the console can be toggled with the ` keybind or not.", true, 0.0f, true, 1.0f); +#ifdef DEBUG +ConVar cl_neo_autojoin_offset("cl_neo_autojoin_offset", "0", FCVAR_DEVELOPMENTONLY, + "Auto-join offset max-player requirement.", true, 0.0f, true, static_cast(MAX_PLAYERS-1)); +#endif inline int g_iRowsInScreen; namespace { @@ -69,10 +73,37 @@ constexpr wchar_t WSZ_GAME_TITLE1_b[] = L"C"; constexpr wchar_t WSZ_GAME_TITLE2[] = L"Hrebuild"; #define SZ_WEBSITE "https://neotokyorebuild.github.io" +const wchar_t *TABLE_HEADERS_SERVERBROWSER[GSIW__TOTAL] = { + L"Lock", L"VAC", L"Name", L"IP Address", L"Map", L"Players", L"Ping", L"Tags", +}; +const int TABLE_DEFPROP_SERVERBROWSER[GSIW__TOTAL] = { + 8, 8, 30, -18, 18, 12, 12, 12, // Last item need to set for hide/unhide +}; +const float TABLE_SCALEWIDE_SERVERBROWSER = 1.2f; + +const wchar_t *TABLE_HEADERS_SERVERBLACKLIST[SBLIST_COL__TOTAL] = { + L"Name", L"Type", L"Added on", +}; +const int TABLE_DEFPROP_SERVERBLACKLIST[SBLIST_COL__TOTAL] = { + 60, 10, -1 +}; + +const wchar_t *TABLE_HEADERS_PLAYER[GSPS__TOTAL] = { + L"Score", L"Name", L"Time" +}; +const int TABLE_DEFPROP_PLAYER[GSPS__TOTAL] = { + 15, 65, -1 +}; + +static_assert(sizeof(CNeoRoot::m_iColsWideServerBrowser) == sizeof(TABLE_DEFPROP_SERVERBROWSER)); +static_assert(sizeof(CNeoRoot::m_iColsWideServerBlacklist) == sizeof(TABLE_DEFPROP_SERVERBLACKLIST)); +static_assert(sizeof(CNeoRoot::m_iColsWideDetailedPlayerList) == sizeof(TABLE_DEFPROP_PLAYER)); + enum ENeoPopup { NEOPOPUP_ACTIONSERVER = NeoUI::INTERNALPOPUP_NIL + 1, NEOPOPUP_ACTIONBLACKLIST, + NEOPOPUP_ACTIONSBHEADER, }; ConCommand neo_toggleconsole("neo_toggleconsole", NeoToggleconsole, "toggle the console", FCVAR_DONTRECORD); @@ -115,6 +146,24 @@ void OverrideGameUI() } } +// Check if server is using SDR (Steam Datagram Relay) AKA Steam Networking +static bool NetAdrIsSDR(const servernetadr_t &netAdr) +{ + const uint32 u32IpAdr = netAdr.GetIP(); + const uint8 *u8IpBytes = (uint8 *)(&u32IpAdr); +#ifdef VALVE_BIG_ENDIAN + return (u8IpBytes[0] == 169 && u8IpBytes[1] == 254); +#else + return (u8IpBytes[3] == 169 && u8IpBytes[2] == 254); +#endif +} + +// SDR servers doesn't actually includes bots in their players count +static int PlayersCount(const gameserveritem_t *pServer) +{ + return pServer->m_nPlayers - (NetAdrIsSDR(pServer->m_NetAdr) ? 0 : pServer->m_nBotPlayers); +} + // Only use it rarely/cached static bool NetAdrIsFavorite(const servernetadr_t &netAdr) { @@ -479,6 +528,9 @@ void CNeoRoot::ApplySchemeSettings(IScheme *pScheme) m_flWideAs43 = static_cast(tall) * (4.0f / 3.0f); if (m_flWideAs43 > flWide) m_flWideAs43 = flWide; g_iRootSubPanelWide = static_cast(m_flWideAs43 * 0.9f); + m_tabsStateSettings = {}; + m_tabsStateServerBrowser = {}; + m_tabsStateIFF = {}; UpdateControls(); } @@ -523,7 +575,7 @@ void CNeoRoot::OnTick() { if (m_state == STATE_SERVERBROWSER) { - if (m_bSBFiltModified) + if (m_headerModFlagsServerBrowser) { // Pass modified over to the tabs so it doesn't trigger // the filter refresh immeditely @@ -531,7 +583,7 @@ void CNeoRoot::OnTick() { m_serverBrowser[i].m_bModified = true; } - m_bSBFiltModified = false; + m_headerModFlagsServerBrowser = 0; } auto *pSbTab = &m_serverBrowser[m_iServerBrowserTab]; @@ -543,10 +595,11 @@ void CNeoRoot::OnTick() } else if (m_state == STATE_SERVERDETAILS) { - if (m_bSPlayersSortModified) + // NEO TODO MAYBE (nullsystem): Can just reverse list if desending used change + if (m_headerModFlagsPlayers) { m_serverPlayers.UpdateSortedList(); - m_bSPlayersSortModified = false; + m_headerModFlagsPlayers = 0; } } } @@ -599,6 +652,60 @@ void CNeoRoot::OnRelayedKeyTyped(wchar_t unichar) OnMainLoop(NeoUI::MODE_KEYTYPED); } +// copy not ref/pointer gameserveritem_t +void CNeoRoot::OnEnterServer(const gameserveritem_t gameServer, const char *pszServerPassword) +{ + const int iPlayersCount = PlayersCount(&gameServer); + m_serverPingAutoJoin.m_serverInfo = +#ifdef DEBUG + (iPlayersCount < (gameServer.m_nMaxPlayers - cl_neo_autojoin_offset.GetInt())) +#else + (iPlayersCount < gameServer.m_nMaxPlayers) +#endif + ? gameserveritem_t{} + : gameServer; + m_flAutoJoinLastAttempt = gpGlobals->realtime; + if (m_serverPingAutoJoin.m_serverInfo.m_NetAdr.GetIP() == 0) + { + if (IsInGame()) + { + engine->ClientCmd_Unrestricted("disconnect"); + } + + ConVarRef("password").SetValue(pszServerPassword ? pszServerPassword : ""); + V_memset(m_wszServerPassword, 0, sizeof(m_wszServerPassword)); + + // NEO NOTE (nullsystem): Deal with password protected server + if (nullptr == pszServerPassword && gameServer.m_bPassword) + { + m_state = STATE_SERVERPASSWORD; + } + else + { + g_pNeoRoot->m_flTimeLoadingScreenTransition = gpGlobals->realtime; + + char connectCmd[256]; + const char *szAddress = gameServer.m_NetAdr.GetConnectionAddressString(); + V_sprintf_safe(connectCmd, "progress_enable; wait; connect %s", szAddress); + engine->ClientCmd_Unrestricted(connectCmd); + + if (g_pNeoLoading) + { + g_pVGuiLocalize->ConvertANSIToUnicode(gameServer.m_szMap, + g_pNeoLoading->m_wszLoadingMap, + sizeof(g_pNeoLoading->m_wszLoadingMap)); + } + + m_state = STATE_ROOT; + } + } + else if (pszServerPassword) + { + // If this is from the password screen, kick back to server browser + m_state = STATE_SERVERBROWSER; + } +} + void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) { int wide, tall; @@ -645,8 +752,65 @@ void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) &CNeoRoot::MainLoopPopup, // STATE_SPRAYDELETERCONFIRM &CNeoRoot::MainLoopPopup, // STATE_ADDCUSTOMBLACKLIST }; + // Each MainLoop... will have its own BeginContext (this->*P_FN_MAIN_LOOP[m_state])(MainLoopParam{.eMode = eMode, .wide = wide, .tall = tall}); + if (m_serverPingAutoJoin.m_serverInfo.m_NetAdr.GetIP() != 0) + { + g_uiCtx.dPanel.wide = wide; + g_uiCtx.dPanel.tall = g_uiCtx.layout.iDefRowTall; + g_uiCtx.dPanel.x = 0; + g_uiCtx.dPanel.y = tall - g_uiCtx.dPanel.tall - 1; + g_uiCtx.colors.sectionBg = COLOR_DARK_RED; + g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; + NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + NeoUI::BeginSection(NeoUI::SECTIONFLAG_PLAYBUTTONSOUNDS); + static constexpr float FL_AUTO_JOIN_WAIT = 15.0f; + + const auto &server = m_serverPingAutoJoin.m_serverInfo; + const int iPlayersCount = PlayersCount(&server); + wchar wszText[k_cbMaxGameServerName] = {}; + + static constexpr const int ROWLAYOUT_REFRESH[] = { 10, 15, 35, 20, -1 }; + NeoUI::SetPerRowLayout(5, ROWLAYOUT_REFRESH, g_uiCtx.layout.iDefRowTall); + if (NeoUI::Button(L"Cancel").bPressed) + { + m_serverPingAutoJoin.m_serverInfo = {}; // Zero-init + } + NeoUI::Label(L"Auto-joining:"); + + g_pVGuiLocalize->ConvertANSIToUnicode(server.GetName(), wszText, sizeof(wszText)); + NeoUI::Label(wszText); + + V_swprintf_safe(wszText, L"Players: %d/%d", iPlayersCount, server.m_nMaxPlayers); + NeoUI::Label(wszText); + + V_swprintf_safe(wszText, L"Refresh: %ds", static_cast((m_flAutoJoinLastAttempt + FL_AUTO_JOIN_WAIT) - gpGlobals->realtime)); + NeoUI::Label(wszText); + + if ((m_flAutoJoinLastAttempt + FL_AUTO_JOIN_WAIT) <= gpGlobals->realtime) + { + m_serverPingAutoJoin.RequestPing(); + m_flAutoJoinLastAttempt = gpGlobals->realtime; + } + else if (CNeoServerPing::PINGSTATE_NIL != m_serverPingAutoJoin.m_ePingState) + { +#ifdef DEBUG + if ((iPlayersCount < (server.m_nMaxPlayers - cl_neo_autojoin_offset.GetInt())) +#else + if ((iPlayersCount < server.m_nMaxPlayers) +#endif + && (CNeoServerPing::PINGSTATE_SUCCESS == m_serverPingAutoJoin.m_ePingState)) + { + OnEnterServer(m_serverPingAutoJoin.m_serverInfo, nullptr); + } + m_serverPingAutoJoin.m_ePingState = CNeoServerPing::PINGSTATE_NIL; + } + NeoUI::EndSection(); + } + + NeoUI::EndContext(); + // When the state changes, save some variables if (m_state != ePrevState) { @@ -678,13 +842,23 @@ void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) if (eMode == NeoUI::MODE_PAINT) { - // Draw version info (bottom left corner) - Always + // Draw version info - Always surface()->DrawSetTextColor(g_uiCtx.colors.normalFg); int textWidth, textHeight; surface()->DrawSetTextFont(g_uiCtx.fonts[NeoUI::FONT_NTNORMAL].hdl); surface()->GetTextSize(g_uiCtx.fonts[NeoUI::FONT_NTNORMAL].hdl, BUILD_DISPLAY, textWidth, textHeight); - surface()->DrawSetTextPos(g_uiCtx.iMarginX, tall - textHeight - g_uiCtx.iMarginY); + if (m_serverPingAutoJoin.m_serverInfo.m_NetAdr.GetIP() != 0) + { + // top right corner + surface()->DrawSetTextPos(wide - textWidth - g_uiCtx.iMarginX, g_uiCtx.iMarginY); + } + else + { + // bottom left corner + surface()->DrawSetTextPos(g_uiCtx.iMarginX, tall - textHeight - g_uiCtx.iMarginY); + } + surface()->DrawPrintText(BUILD_DISPLAY, V_wcslen(BUILD_DISPLAY)); } } @@ -940,7 +1114,7 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) NeoUI::EndSection(); #endif g_uiCtx.dPanel.x = param.wide - 128; - g_uiCtx.dPanel.y = param.tall - 96; + g_uiCtx.dPanel.y = param.tall - 96 - g_uiCtx.layout.iDefRowTall; g_uiCtx.dPanel.wide = 128; g_uiCtx.dPanel.tall = param.tall; @@ -957,8 +1131,6 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) } } NeoUI::EndSection(); - - NeoUI::EndContext(); } extern ConVar neo_fov; @@ -995,8 +1167,7 @@ void CNeoRoot::MainLoopSettings(const MainLoopParam param) { NeoUI::BeginSection(NeoUI::SECTIONFLAG_ROWWIDGETS | NeoUI::SECTIONFLAG_EXCLUDECONTROLLER); { - static int siTabsLabelWide = -1; - NeoUI::Tabs(WSZ_TABS_LABELS, ARRAYSIZE(WSZ_TABS_LABELS), &m_ns.iCurTab, NeoUI::TABFLAG_DEFAULT, &siTabsLabelWide); + NeoUI::Tabs(WSZ_TABS_LABELS, ARRAYSIZE(WSZ_TABS_LABELS), &m_ns.iCurTab, NeoUI::TABFLAG_DEFAULT, &m_tabsStateSettings); } NeoUI::EndSection(); if (!P_FN[m_ns.iCurTab].bUISectionManaged) @@ -1060,7 +1231,7 @@ void CNeoRoot::MainLoopSettings(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); + if (!m_ns.bModified && g_uiCtx.bValueEdited) { m_ns.bModified = true; @@ -1173,129 +1344,6 @@ void CNeoRoot::MainLoopNewGame(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); -} - -static constexpr const wchar_t *SBLABEL_NAMES[GSIW__TOTAL] = { - L"Lock", L"VAC", L"Name", L"Map", L"Players", L"Ping", -}; -static const int ROWLAYOUT_TABLESPLIT[GSIW__TOTAL] = { - 8, 8, 40, 20, 12, -1 -}; - -static constexpr const wchar_t *BLACKLISTLABEL_NAMES[SBLIST_COL__TOTAL] = { - L"Name", L"Type", L"Added on", -}; -static const int ROWLAYOUT_BLACKLIST[SBLIST_COL__TOTAL] = { - 60, 10, -1 -}; - - -static void ServerBrowserDrawRow(const gameserveritem_t &server) -{ - int xPos = 0; - const int fontStartYPos = g_uiCtx.fonts[g_uiCtx.eFont].iYOffset; - - if (server.m_bPassword) - { - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(L"P", 1); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_TABLESPLIT[GSIW_LOCKED] / 100.0f)); - - if (server.m_bSecure) - { - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(L"S", 1); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_TABLESPLIT[GSIW_VAC] / 100.0f)); - - { - wchar_t wszServerName[k_cbMaxGameServerName]; - const int iSize = g_pVGuiLocalize->ConvertANSIToUnicode(server.GetName(), wszServerName, sizeof(wszServerName)); - - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(wszServerName, iSize - 1); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_TABLESPLIT[GSIW_NAME] / 100.0f)); - - { - // In lower resolution, it may overlap from name, so paint a background here - vgui::surface()->DrawFilledRect(g_uiCtx.rWidgetArea.x0 + xPos, g_uiCtx.rWidgetArea.y0, - g_uiCtx.rWidgetArea.x1, g_uiCtx.rWidgetArea.y1); - - wchar_t wszMapName[k_cbMaxGameServerMapName]; - const int iSize = g_pVGuiLocalize->ConvertANSIToUnicode(server.m_szMap, wszMapName, sizeof(wszMapName)); - - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(wszMapName, iSize - 1); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_TABLESPLIT[GSIW_MAP] / 100.0f)); - - { - // In lower resolution, it may overlap from name, so paint a background here - vgui::surface()->DrawFilledRect(g_uiCtx.rWidgetArea.x0 + xPos, g_uiCtx.rWidgetArea.y0, - g_uiCtx.rWidgetArea.x1, g_uiCtx.rWidgetArea.y1); - - wchar_t wszPlayers[15]; - const int iSize = server.m_nBotPlayers ? V_swprintf_safe(wszPlayers, L"%d/%d (%d)", server.m_nPlayers - server.m_nBotPlayers, server.m_nMaxPlayers, server.m_nBotPlayers) - : V_swprintf_safe(wszPlayers, L"%d/%d", server.m_nPlayers, server.m_nMaxPlayers); - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(wszPlayers, iSize); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_TABLESPLIT[GSIW_PLAYERS] / 100.0f)); - - { - wchar_t wszPing[10]; - const int iSize = V_swprintf_safe(wszPing, L"%d", server.m_nPing); - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(wszPing, iSize); - } -} - -static void ServerBlacklistDrawRow(const ServerBlacklistInfo &blacklist) -{ - int xPos = 0; - const int fontStartYPos = g_uiCtx.fonts[g_uiCtx.eFont].iYOffset; - - { - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(blacklist.wszName, V_wcslen(blacklist.wszName)); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_BLACKLIST[SBLIST_COL_NAME] / 100.0f)); - - { - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - const wchar_t *pwszLabel = (blacklist.eType == SBLIST_TYPE_NETADR) ? L"IP" : L"NAME"; - vgui::surface()->DrawPrintText(pwszLabel, V_wcslen(pwszLabel)); - } - xPos += static_cast(g_uiCtx.irWidgetWide * (ROWLAYOUT_BLACKLIST[SBLIST_COL_TYPE] / 100.0f)); - - { - vgui::surface()->DrawSetTextPos(g_uiCtx.rWidgetArea.x0 + xPos + g_uiCtx.iMarginX, g_uiCtx.rWidgetArea.y0 + fontStartYPos); - vgui::surface()->DrawPrintText(blacklist.wszDateTimeAdded, V_wcslen(blacklist.wszDateTimeAdded)); - } -} - -static void DrawSortHint(const bool bDescending) -{ - if (g_uiCtx.eMode != NeoUI::MODE_PAINT || !IN_BETWEEN_AR(0, g_uiCtx.irWidgetLayoutY, g_uiCtx.dPanel.tall)) - { - return; - } - int iHintTall = g_uiCtx.iMarginY / 3; - vgui::surface()->DrawSetColor(COLOR_WHITE); - if (!bDescending) - { - vgui::surface()->DrawFilledRect(g_uiCtx.rWidgetArea.x0, g_uiCtx.rWidgetArea.y0, - g_uiCtx.rWidgetArea.x1, g_uiCtx.rWidgetArea.y0 + iHintTall); - } - else - { - vgui::surface()->DrawFilledRect(g_uiCtx.rWidgetArea.x0, g_uiCtx.rWidgetArea.y1 - iHintTall, - g_uiCtx.rWidgetArea.x1, g_uiCtx.rWidgetArea.y1); - } - vgui::surface()->DrawSetColor(COLOR_NEOPANELACCENTBG); } void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) @@ -1306,22 +1354,59 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) static const wchar_t *ANTICHEAT_LABELS[ANTICHEAT__TOTAL] = { L"", L"On", L"Off" }; + static const wchar_t *TAGS_FILTER_LABELS[TAGSFILTER__TOTAL] = { + L"Tags include", L"Tags exclude" + }; - bool bEnterServer = false; + enum EEnterServerState + { + ENTERSERVER_NIL = 0, + ENTERSERVER_PING, + }; + EEnterServerState eEnterServer = ENTERSERVER_NIL; const int iTallTotal = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 2); g_uiCtx.dPanel.wide = g_iRootSubPanelWide; g_uiCtx.dPanel.x = (param.wide / 2) - (g_iRootSubPanelWide / 2); g_uiCtx.dPanel.y = (param.tall / 2) - (iTallTotal / 2); g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * 2; g_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; + + const int iTotalHeadersWide = (g_uiCtx.dPanel.wide * TABLE_SCALEWIDE_SERVERBROWSER); + + // NEO TODO (nullsystem): Save and restore it cross sessions + if (NeoUI::MODE_PAINT == g_uiCtx.eMode && g_uiCtx.dPanel.wide > 0) + { + if (0 == m_iColsWideServerBrowser[0]) + { + int iAccX = 0; + for (int i = 0; i < (GSIW__TOTAL - 1); ++i) + { + m_iColsWideServerBrowser[i] = (TABLE_DEFPROP_SERVERBROWSER[i] / 100.0f) * iTotalHeadersWide; + iAccX += m_iColsWideServerBrowser[i]; + } + m_iColsWideServerBrowser[GSIW__TOTAL - 1] = iTotalHeadersWide - iAccX; + } + + if (0 == m_iColsWideServerBlacklist[0]) + { + int iAccX = 0; + for (int i = 0; i < (SBLIST_COL__TOTAL - 1); ++i) + { + m_iColsWideServerBlacklist[i] = (TABLE_DEFPROP_SERVERBLACKLIST[i] / 100.0f) * g_uiCtx.dPanel.wide; + iAccX += m_iColsWideServerBlacklist[i]; + } + m_iColsWideServerBlacklist[SBLIST_COL__TOTAL - 1] = g_uiCtx.dPanel.wide - iAccX; + } + } + NeoUI::BeginContext(&g_uiCtx, param.eMode, m_wszCachedTexts[MMBTN_FINDSERVER], "CtxServerBrowser"); { bool bForceRefresh = false; NeoUI::BeginSection(NeoUI::SECTIONFLAG_ROWWIDGETS); { const int iPrevTab = m_iServerBrowserTab; - static int siGsNamesWide = -1; - NeoUI::Tabs(GS_NAMES, ARRAYSIZE(GS_NAMES), &m_iServerBrowserTab, NeoUI::TABFLAG_DEFAULT, &siGsNamesWide); + NeoUI::Tabs(GS_NAMES, ARRAYSIZE(GS_NAMES), &m_iServerBrowserTab, + NeoUI::TABFLAG_DEFAULT, &m_tabsStateServerBrowser); if (iPrevTab != m_iServerBrowserTab) { m_iSelectedServer = -1; @@ -1339,49 +1424,38 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; int iColTotal = GSIW__TOTAL; - const int *pirLayout = ROWLAYOUT_TABLESPLIT; - const wchar_t * const*pwszNames = SBLABEL_NAMES; + int *pirLayout = m_iColsWideServerBrowser; + const wchar_t **pwszNames = TABLE_HEADERS_SERVERBROWSER; if (m_iServerBrowserTab == GS_BLACKLIST) { - iColTotal = ARRAYSIZE(ROWLAYOUT_BLACKLIST); - pirLayout = ROWLAYOUT_BLACKLIST; - pwszNames = BLACKLISTLABEL_NAMES; + iColTotal = SBLIST_COL__TOTAL; + pirLayout = m_iColsWideServerBlacklist; + pwszNames = TABLE_HEADERS_SERVERBLACKLIST; } - NeoUI::SetPerRowLayout(iColTotal, pirLayout); - for (int i = 0; i < iColTotal; ++i) + m_headerModFlagsServerBrowser |= NeoUI::TableHeader(pwszNames, iColTotal, + pirLayout, &m_sortCtx.col, &m_sortCtx.bDescending, 1); + if (m_iServerBrowserTab != GS_BLACKLIST + && g_uiCtx.eMode == NeoUI::MODE_MOUSEPRESSED + && g_uiCtx.eCode == MOUSE_RIGHT + && g_uiCtx.iHotSection == g_uiCtx.iSection) { - const bool isSortCol = (m_sortCtx.col == i); - vgui::surface()->DrawSetColor(isSortCol ? COLOR_NEOPANELACCENTBG : COLOR_BLACK_TRANSPARENT); - if (NeoUI::Button(pwszNames[i]).bPressed) - { - if (isSortCol) - { - m_sortCtx.bDescending = !m_sortCtx.bDescending; - } - else - { - m_sortCtx.col = i; - } - m_bSBFiltModified = true; - } - - if (isSortCol) - { - DrawSortHint(m_sortCtx.bDescending); - } + NeoUI::OpenPopup(NEOPOPUP_ACTIONSBHEADER, NeoUI::Dim{ + .x = g_uiCtx.iMouseAbsX, + .y = g_uiCtx.iMouseAbsY, + .wide = NeoUI::PopupWideByStr("__IP Address"), + .tall = g_uiCtx.layout.iDefRowTall * GSIW__TOTAL, + }); } - // TODO: Should give proper controls over colors through NeoUI - vgui::surface()->DrawSetColor(COLOR_NEOPANELACCENTBG); - vgui::surface()->DrawSetTextColor(COLOR_WHITE); g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; } NeoUI::EndSection(); - static constexpr int FILTER_ROWS = 5; + const bool bTabShowFilterPanel = m_bShowFilterPanel && (m_iServerBrowserTab != GS_BLACKLIST); + static constexpr int FILTER_ROWS = 8; g_uiCtx.dPanel.y += g_uiCtx.dPanel.tall; g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * (g_iRowsInScreen - 1); - if (m_bShowFilterPanel) g_uiCtx.dPanel.tall -= g_uiCtx.layout.iRowTall * FILTER_ROWS; + if (bTabShowFilterPanel) g_uiCtx.dPanel.tall -= g_uiCtx.layout.iRowTall * FILTER_ROWS; NeoUI::BeginSection(NeoUI::SECTIONFLAG_DEFAULTFOCUS); { NeoUI::SetPerRowLayout(1); @@ -1389,57 +1463,61 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) { if (g_blacklistedServers.IsEmpty()) { - wchar_t wszInfo[128]; - V_swprintf_safe(wszInfo, L"No servers blacklisted"); - NeoUI::HeadingLabel(wszInfo); + NeoUI::BeginIgnoreXOffset(); + { + NeoUI::HeadingLabel(L"No servers blacklisted"); + } + NeoUI::EndIgnoreXOffset(); + NeoUI::PadTableXScroll(m_iColsWideServerBlacklist, SBLIST_COL__TOTAL); } else { - for (int i = 0; i < g_blacklistedServers.Count(); ++i) + NeoUI::BeginTable(m_iColsWideServerBlacklist, SBLIST_COL__TOTAL); { - const ServerBlacklistInfo &blacklist = g_blacklistedServers[i]; - - const auto btn = NeoUI::Button(L""); - if (btn.bPressed || btn.bMouseRightPressed) // Dummy button, draw over it in paint + for (int i = 0; i < g_blacklistedServers.Count(); ++i) { - m_iSelectedServer = i; - if (btn.bMouseRightPressed) - { - 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, - }); - } - } + const ServerBlacklistInfo &blacklist = g_blacklistedServers[i]; - if (param.eMode == NeoUI::MODE_PAINT) - { - Color drawColor = g_uiCtx.colors.normalBg; - Color textColor = g_uiCtx.colors.normalFg; + NeoUI::NextTableRowFlags rowFlags = NeoUI::NEXTTABLEROWFLAG_SELECTABLE; if (m_iSelectedServer == i) { - drawColor = g_uiCtx.colors.activeBg; - textColor = g_uiCtx.colors.activeFg; + rowFlags |= NeoUI::NEXTTABLEROWFLAG_SELECTED; } - else if (btn.bMouseHover) + const auto btn = NeoUI::NextTableRow(rowFlags); { - drawColor = COLOR_BLACK_TRANSPARENT; + NeoUI::Label(blacklist.wszName); + NeoUI::Label((blacklist.eType == SBLIST_TYPE_NETADR) ? L"IP" : L"NAME"); + NeoUI::Label(blacklist.wszDateTimeAdded); + } + if (btn.bKeyUpPressed || btn.bKeyDownPressed) + { + m_iSelectedServer = LoopAroundInArray(m_iSelectedServer + ((btn.bKeyUpPressed) ? -1 : +1), g_blacklistedServers.Count()); + } + else if (btn.bPressed || btn.bMouseRightPressed) + { + m_iSelectedServer = i; + if (btn.bMouseRightPressed) + { + 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, + }); + } } - - vgui::surface()->DrawSetColor(drawColor); - vgui::surface()->DrawSetTextColor(textColor); - vgui::surface()->DrawFilledRectArray(&g_uiCtx.rWidgetArea, 1); - ServerBlacklistDrawRow(blacklist); - vgui::surface()->DrawSetColor(g_uiCtx.colors.normalBg); } } + const auto endBtn = NeoUI::EndTable(); + if (endBtn.bKeyUpPressed || endBtn.bKeyDownPressed) + { + m_iSelectedServer = (endBtn.bKeyUpPressed) ? g_blacklistedServers.Count() - 1 : 0; + } } } else { - if (m_serverBrowser[m_iServerBrowserTab].m_filteredServers.IsEmpty()) + if (m_serverBrowser[m_iServerBrowserTab].m_filteredServers.empty()) { wchar_t wszInfo[128]; if (m_serverBrowser[m_iServerBrowserTab].m_bSearching) @@ -1450,7 +1528,12 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) { V_swprintf_safe(wszInfo, L"No %ls queries found. Press Refresh to re-check", GS_NAMES[m_iServerBrowserTab]); } - NeoUI::HeadingLabel(wszInfo); + NeoUI::BeginIgnoreXOffset(); + { + NeoUI::HeadingLabel(wszInfo); + } + NeoUI::EndIgnoreXOffset(); + NeoUI::PadTableXScroll(m_iColsWideServerBrowser, GSIW__TOTAL); } else { @@ -1459,69 +1542,195 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) // expanded to give Y-offset in the form of filtered array range so it only loops // through what's needed instead. const auto *sbTab = &m_serverBrowser[m_iServerBrowserTab]; - for (int i = 0; i < sbTab->m_filteredServers.Size(); ++i) + int iShownServers = 0; + NeoUI::BeginTable(m_iColsWideServerBrowser, GSIW__TOTAL); { - const auto &server = sbTab->m_filteredServers[i]; - bool bSkipServer = false; - if (m_sbFilters.bServerNotFull && server.m_nPlayers == server.m_nMaxPlayers) bSkipServer = true; - else if (m_sbFilters.bHasUsersPlaying && server.m_nPlayers - server.m_nBotPlayers == 0) bSkipServer = true; - else if (m_sbFilters.bIsNotPasswordProtected && server.m_bPassword) bSkipServer = true; - else if (m_sbFilters.iAntiCheat == ANTICHEAT_OFF && server.m_bSecure) bSkipServer = true; - else if (m_sbFilters.iAntiCheat == ANTICHEAT_ON && !server.m_bSecure) bSkipServer = true; - else if (m_sbFilters.iMaxPing != 0 && server.m_nPing > m_sbFilters.iMaxPing) bSkipServer = true; - else if (ServerBlacklisted(server)) bSkipServer = true; - if (bSkipServer) + for (int i = 0; i < sbTab->m_filteredServers.size(); ++i) { - continue; - } + // NEO TODO (nullsystem): Some/most of those filters really should be + // done via the CNeoServerList::RequestList server request through the + // MatchMakingKeyValuePair_t mmFilters list + const auto &server = sbTab->m_filteredServers[i]; + const int iPlayersCount = PlayersCount(&server); + bool bSkipServer = false; + if (m_sbFilters.bServerNotFull && server.m_nPlayers == server.m_nMaxPlayers) bSkipServer = true; + else if (m_sbFilters.bHasUsersPlaying && iPlayersCount == 0) bSkipServer = true; + else if (m_sbFilters.bIsNotPasswordProtected && server.m_bPassword) bSkipServer = true; + else if (m_sbFilters.iAntiCheat == ANTICHEAT_OFF && server.m_bSecure) bSkipServer = true; + else if (m_sbFilters.iAntiCheat == ANTICHEAT_ON && !server.m_bSecure) bSkipServer = true; + else if (m_sbFilters.iMaxPing != 0 && server.m_nPing > m_sbFilters.iMaxPing) bSkipServer = true; + else if (ServerBlacklisted(server)) bSkipServer = true; + else if (m_sbFilters.iMaxPlayerCount > 0 && iPlayersCount > m_sbFilters.iMaxPlayerCount) bSkipServer = true; + + // NEO NOTE (nullsystem): All of those wchar_t[] strings + // need to be zero-initialized otherwise it can crash + // the client on some text! + wchar_t wszMapName[k_cbMaxGameServerMapName] = {}; + if (false == bSkipServer) + { + g_pVGuiLocalize->ConvertANSIToUnicode( + server.m_szMap, wszMapName, sizeof(wszMapName)); + if (m_sbFilters.wszMapFilter[0] && nullptr == wcsstr(wszMapName, m_sbFilters.wszMapFilter)) bSkipServer = true; + } - const auto btn = NeoUI::Button(L""); - if (btn.bPressed || btn.bMouseRightPressed) // Dummy button, draw over it in paint - { - m_iSelectedServer = i; - if (btn.bMouseRightPressed) + // Tags are separated by "," + wchar_t wszTags[k_cbMaxGameServerTags] = {}; + if (false == bSkipServer) + { + g_pVGuiLocalize->ConvertANSIToUnicode( + server.m_szGameTags, wszTags, sizeof(wszTags)); + if (m_sbFilters.wszTagsFilter[0]) + { + bool bInFilter = false; + + // wcstok will modify string in-place, so copy the strings over first + // and use it on mutWsz... variables instead + wchar_t mutWszTags[k_cbMaxGameServerTags] = {}; + V_wcscpy_safe(mutWszTags, wszTags); + + wchar_t *pwszBufTags = nullptr; + wchar_t *pwszTokenTag = wcstok(mutWszTags, L",", &pwszBufTags); + while (pwszTokenTag && !bInFilter) + { + wchar_t mutWszTagsFilter[k_cbMaxGameServerTags] = {}; + V_wcscpy_safe(mutWszTagsFilter, m_sbFilters.wszTagsFilter); + + wchar_t *pwszBufTagsFilter = nullptr; + wchar_t *pwszBufTokenTagFilter = wcstok(mutWszTagsFilter, L",", &pwszBufTagsFilter); + while (pwszBufTokenTagFilter && !bInFilter) + { + bInFilter = (0 == V_wcscmp(pwszTokenTag, pwszBufTokenTagFilter)); + pwszBufTokenTagFilter = wcstok(nullptr, L",", &pwszBufTagsFilter); + } + + pwszTokenTag = wcstok(nullptr, L",", &pwszBufTags); + } + + if (!bInFilter && m_sbFilters.iTagsFilterType == TAGSFILTER_INCLUDE) + { + bSkipServer = true; + } + else if (bInFilter) + { + bSkipServer = (m_sbFilters.iTagsFilterType == TAGSFILTER_EXCLUDE); + } + } + } + + if (bSkipServer) { - 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 (m_iSelectedServer == i && m_iUpDownDirection != 0 && m_iUpDownInitialServer >= 0) + { + if (m_iSelectedServer == m_iUpDownInitialServer) + { + // Unlikely it'll hit this case but just in-case + m_iUpDownDirection = 0; + m_iUpDownInitialServer = -1; + m_iSelectedServer = -1; + } + else + { + m_iSelectedServer = LoopAroundInArray(m_iSelectedServer + m_iUpDownDirection, static_cast(sbTab->m_filteredServers.size())); + } + } + continue; } - if (btn.bKeyPressed || btn.bMouseDoublePressed) + ++iShownServers; + m_iUpDownDirection = 0; + m_iUpDownInitialServer = -1; + + wchar_t wszServerName[k_cbMaxGameServerName] = {}; + wchar_t wszPlayers[15] = {}; + wchar_t wszPing[10] = {}; + wchar_t wszIPAddress[64] = {}; + + g_pVGuiLocalize->ConvertANSIToUnicode( + server.GetName(), wszServerName, sizeof(wszServerName)); + if (server.m_nBotPlayers > 0) { - bEnterServer = true; + V_swprintf_safe(wszPlayers, L"%d/%d (%d)", + iPlayersCount, + server.m_nMaxPlayers, + server.m_nBotPlayers); } - } + else + { + V_swprintf_safe(wszPlayers, L"%d/%d", + server.m_nPlayers, + server.m_nMaxPlayers); + } + V_swprintf_safe(wszPing, L"%d", server.m_nPing); + g_pVGuiLocalize->ConvertANSIToUnicode(server.m_NetAdr.GetConnectionAddressString(), wszIPAddress, sizeof(wszIPAddress)); - if (param.eMode == NeoUI::MODE_PAINT) - { - Color textColor = COLOR_WHITE; - Color drawColor = COLOR_BLACK_TRANSPARENT; + NeoUI::NextTableRowFlags rowFlags = NeoUI::NEXTTABLEROWFLAG_SELECTABLE; if (m_iSelectedServer == i) { - drawColor = COLOR_WHITE; - textColor = COLOR_BLACK; + rowFlags |= NeoUI::NEXTTABLEROWFLAG_SELECTED; } - else if (btn.bMouseHover) + const auto btn = NeoUI::NextTableRow(rowFlags); { - drawColor = COLOR_BLACK_TRANSPARENT; + NeoUI::Label((server.m_bPassword) ? L"P" : L""); + NeoUI::Label((server.m_bSecure) ? L"S" : L""); + NeoUI::Label(wszServerName); + NeoUI::Label(wszIPAddress); + NeoUI::Label(wszMapName); + NeoUI::Label(wszPlayers); + NeoUI::Label(wszPing); + NeoUI::Label(wszTags); } - - vgui::surface()->DrawSetColor(drawColor); - vgui::surface()->DrawSetTextColor(textColor); - vgui::surface()->DrawFilledRect(g_uiCtx.rWidgetArea.x0, g_uiCtx.rWidgetArea.y0, - g_uiCtx.rWidgetArea.x1, g_uiCtx.rWidgetArea.y1); - ServerBrowserDrawRow(server); - vgui::surface()->DrawSetColor(g_uiCtx.colors.normalBg); + if (btn.bKeyUpPressed || btn.bKeyDownPressed) + { + m_iUpDownDirection = (btn.bKeyUpPressed) ? -1 : +1; + m_iUpDownInitialServer = i; + m_iSelectedServer = LoopAroundInArray(m_iSelectedServer + m_iUpDownDirection, static_cast(sbTab->m_filteredServers.size())); + if (m_iSelectedServer == m_iUpDownInitialServer) + { + m_iUpDownDirection = 0; + m_iUpDownInitialServer = -1; + } + } + else if (btn.bPressed || btn.bMouseRightPressed) + { + m_iSelectedServer = i; + if (btn.bMouseRightPressed) + { + 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.bKeyEnterPressed || btn.bMouseDoublePressed) + { + eEnterServer = ENTERSERVER_PING; + } + } + } + } + const auto endBtn = NeoUI::EndTable(); + if (endBtn.bKeyUpPressed || endBtn.bKeyDownPressed) + { + m_iUpDownDirection = (endBtn.bKeyUpPressed) ? -1 : +1; + m_iSelectedServer = (endBtn.bKeyUpPressed) ? static_cast(sbTab->m_filteredServers.size()) - 1 : 0; + m_iUpDownInitialServer = m_iSelectedServer; + if (m_iSelectedServer == m_iUpDownInitialServer) + { + m_iUpDownDirection = 0; + m_iUpDownInitialServer = -1; } } + + if (iShownServers == 0) + { + NeoUI::HeadingLabel(L"There are no servers that pass your filter settings"); + } } } } @@ -1529,21 +1738,36 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::EndSection(); g_uiCtx.dPanel.y += g_uiCtx.dPanel.tall; g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall; - if (m_bShowFilterPanel) g_uiCtx.dPanel.tall += g_uiCtx.layout.iRowTall * FILTER_ROWS; + if (bTabShowFilterPanel) g_uiCtx.dPanel.tall += g_uiCtx.layout.iRowTall * FILTER_ROWS; NeoUI::BeginSection(NeoUI::SECTIONFLAG_ROWWIDGETS); { NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); + if (bTabShowFilterPanel) + { + NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT); + NeoUI::RingBoxBool(L"Server not full", &m_sbFilters.bServerNotFull); + NeoUI::RingBoxBool(L"Has users playing", &m_sbFilters.bHasUsersPlaying); + NeoUI::RingBoxBool(L"Is not password protected", &m_sbFilters.bIsNotPasswordProtected); + NeoUI::RingBox(L"Anti-cheat", ANTICHEAT_LABELS, ARRAYSIZE(ANTICHEAT_LABELS), &m_sbFilters.iAntiCheat); + NeoUI::SliderInt(L"Maximum ping", &m_sbFilters.iMaxPing, 0, 500, 10, L"No limit"); + NeoUI::TextEdit(L"Map", m_sbFilters.wszMapFilter, SZWSZ_LEN(m_sbFilters.wszMapFilter)); + NeoUI::SliderInt(L"Max player count", &m_sbFilters.iMaxPlayerCount, 0, MAX_PLAYERS-1, 1, L"No limit"); + + NeoUI::BeginMultiWidgetHighlighter(2); + { + NeoUI::RingBox(TAGS_FILTER_LABELS, ARRAYSIZE(TAGS_FILTER_LABELS), &m_sbFilters.iTagsFilterType); + NeoUI::TextEdit(m_sbFilters.wszTagsFilter, SZWSZ_LEN(m_sbFilters.wszTagsFilter)); + } + NeoUI::EndMultiWidgetHighlighter(); + } g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_CENTER; - NeoUI::SetPerRowLayout(6); + NeoUI::SetPerRowLayout(5); { if (NeoUI::Button(NeoUI::HintAlt(L"Back (ESC)", L"Back (B)")).bPressed || NeoUI::BindKeyBack()) { + m_serverPingEnter.m_serverInfo = {}; // Zero-init m_state = STATE_ROOT; } - if (NeoUI::Button(L"Legacy").bPressed) - { - GetGameUI()->SendMainMenuCommand("OpenServerBrowser"); - } if (m_iServerBrowserTab == GS_BLACKLIST) { if (NeoUI::Button(L"Import").bPressed) @@ -1611,8 +1835,8 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) m_iSelectedServer = -1; ISteamMatchmakingServers *steamMM = steamapicontext->SteamMatchmakingServers(); CNeoServerList *pServerBrowser = &m_serverBrowser[m_iServerBrowserTab]; - pServerBrowser->m_servers.RemoveAll(); - pServerBrowser->m_filteredServers.RemoveAll(); + pServerBrowser->m_servers.clear(); + pServerBrowser->m_filteredServers.clear(); if (pServerBrowser->m_hdlRequest) { steamMM->CancelQuery(pServerBrowser->m_hdlRequest); @@ -1623,54 +1847,42 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) } if (m_iSelectedServer >= 0) { - if (bEnterServer || NeoUI::Button(L"Enter").bPressed) + if (NeoUI::Button(L"Enter").bPressed) { - if (IsInGame()) - { - engine->ClientCmd_Unrestricted("disconnect"); - } - - ConVarRef("password").SetValue(""); - V_memset(m_wszServerPassword, 0, sizeof(m_wszServerPassword)); + eEnterServer = ENTERSERVER_PING; + } + } + if (m_iSelectedServer >= 0 + || m_serverPingEnter.m_serverInfo.m_NetAdr.GetIP() != 0) + { + const auto &gameServerSelected = m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer]; - // NEO NOTE (nullsystem): Deal with password protected server - const auto gameServer = m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer]; - if (gameServer.m_bPassword) - { - m_state = STATE_SERVERPASSWORD; - } - else + // NEO NOTE (nullsystem): When entering a server, must ping the + // server to check the player count just before properly entering. + // To catch out if it's been updated since the list refresh and + // go into auto-join state if server's full. + if (ENTERSERVER_PING == eEnterServer) + { + m_serverPingEnter.m_serverInfo = gameServerSelected; + m_serverPingEnter.RequestPing(); + eEnterServer = ENTERSERVER_NIL; + } + else if (m_serverPingEnter.m_serverInfo.m_NetAdr.GetIP() != 0 + && CNeoServerPing::PINGSTATE_NIL != m_serverPingEnter.m_ePingState) + { + // Regardless of success state or not, just refresh to NIL and try to enter + m_serverPingEnter.m_ePingState = CNeoServerPing::PINGSTATE_NIL; + // Check if nothing else changed since selected game-server submitted for ping-reply + // otherwise don't try + if (gameServerSelected.m_NetAdr.GetIP() == m_serverPingEnter.m_serverInfo.m_NetAdr.GetIP()) { - g_pNeoRoot->m_flTimeLoadingScreenTransition = gpGlobals->realtime; - - char connectCmd[256]; - const char *szAddress = gameServer.m_NetAdr.GetConnectionAddressString(); - V_sprintf_safe(connectCmd, "progress_enable; wait; connect %s", szAddress); - engine->ClientCmd_Unrestricted(connectCmd); - - if (g_pNeoLoading) - { - g_pVGuiLocalize->ConvertANSIToUnicode(gameServer.m_szMap, - g_pNeoLoading->m_wszLoadingMap, - sizeof(g_pNeoLoading->m_wszLoadingMap)); - } - - m_state = STATE_ROOT; + OnEnterServer(m_serverPingEnter.m_serverInfo, nullptr); } + m_serverPingEnter.m_serverInfo = {}; // Zero-init } } } } - NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); - if (m_bShowFilterPanel) - { - NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT); - NeoUI::RingBoxBool(L"Server not full", &m_sbFilters.bServerNotFull); - NeoUI::RingBoxBool(L"Has users playing", &m_sbFilters.bHasUsersPlaying); - NeoUI::RingBoxBool(L"Is not password protected", &m_sbFilters.bIsNotPasswordProtected); - NeoUI::RingBox(L"Anti-cheat", ANTICHEAT_LABELS, ARRAYSIZE(ANTICHEAT_LABELS), &m_sbFilters.iAntiCheat); - NeoUI::SliderInt(L"Maximum ping", &m_sbFilters.iMaxPing, 0, 500, 10, L"No limit"); - } } NeoUI::EndSection(); } @@ -1738,7 +1950,27 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::EndPopup(); } - NeoUI::EndContext(); + if (NeoUI::BeginPopup(NEOPOPUP_ACTIONSBHEADER, NeoUI::POPUPFLAG_COLORHOTASACTIVE)) + { + for (int i = 0; i < GSIW__TOTAL; ++i) + { + if (NeoUI::ButtonCheckbox(TABLE_HEADERS_SERVERBROWSER[i], (m_iColsWideServerBrowser[i] > 0)).bPressed) + { + if (m_iColsWideServerBrowser[i] == 0) + { + m_iColsWideServerBrowser[i] = abs(TABLE_DEFPROP_SERVERBROWSER[i] / 100.0f) * iTotalHeadersWide; + } + else + { + m_iColsWideServerBrowser[i] = -m_iColsWideServerBrowser[i]; + } + NeoUI::ClosePopup(); + break; + } + } + + NeoUI::EndPopup(); + } } static constexpr const wchar_t *CREDITSPEOPLELABEL_NAMES[] = { @@ -1840,7 +2072,6 @@ void CNeoRoot::MainLoopCredits(const MainLoopParam param) NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); NeoUI::EndSection(); } - NeoUI::EndContext(); } void CNeoRoot::MainLoopMapList(const MainLoopParam param) @@ -1882,7 +2113,6 @@ void CNeoRoot::MainLoopMapList(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); } void CNeoRoot::MainLoopSprayPicker(const MainLoopParam param) @@ -2004,7 +2234,6 @@ void CNeoRoot::MainLoopSprayPicker(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); } void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) @@ -2016,6 +2245,20 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) g_uiCtx.dPanel.y = (param.tall / 2) - (iTallTotal / 2); g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * 6; g_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; + + // NEO TODO (nullsystem): Save and restore it cross sessions + if (NeoUI::MODE_PAINT == g_uiCtx.eMode && g_uiCtx.dPanel.wide > 0 && + 0 == m_iColsWideDetailedPlayerList[0]) + { + int iAccX = 0; + for (int i = 0; i < (GSPS__TOTAL - 1); ++i) + { + m_iColsWideDetailedPlayerList[i] = (TABLE_DEFPROP_PLAYER[i] / 100.0f) * g_uiCtx.dPanel.wide; + iAccX += m_iColsWideDetailedPlayerList[i]; + } + m_iColsWideDetailedPlayerList[GSPS__TOTAL - 1] = g_uiCtx.dPanel.wide - iAccX; + } + NeoUI::BeginContext(&g_uiCtx, param.eMode, L"Server details", "CtxServerDetail"); { NeoUI::BeginSection(NeoUI::SECTIONFLAG_DEFAULTFOCUS); @@ -2032,8 +2275,20 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) } if (bP) g_pVGuiLocalize->ConvertANSIToUnicode(gameServer->m_szMap, wszText, sizeof(wszText)); NeoUI::Label(L"Map:", wszText); - if (bP) gameServer->m_nBotPlayers ? V_swprintf_safe(wszText, L"%d/%d (%d)", gameServer->m_nPlayers - gameServer->m_nBotPlayers , gameServer->m_nMaxPlayers, gameServer->m_nBotPlayers) - : V_swprintf_safe(wszText, L"%d/%d", gameServer->m_nPlayers, gameServer->m_nMaxPlayers); + if (bP) + { + if (gameServer->m_nBotPlayers) + { + V_swprintf_safe(wszText, L"%d/%d (%d)", + PlayersCount(gameServer), + gameServer->m_nMaxPlayers, + gameServer->m_nBotPlayers); + } + else + { + V_swprintf_safe(wszText, L"%d/%d", gameServer->m_nPlayers, gameServer->m_nMaxPlayers); + } + } NeoUI::Label(L"Players:", wszText); if (bP) V_swprintf_safe(wszText, L"%ls", gameServer->m_bSecure ? L"Enabled" : L"Disabled"); NeoUI::Label(L"VAC:", wszText); @@ -2071,39 +2326,14 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) } else { - static constexpr const int PLAYER_ROW_PROP[] = { 15, 65, -1 }; - const int iInfoTall = g_uiCtx.dPanel.tall; g_uiCtx.dPanel.y += iInfoTall; g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall; NeoUI::BeginSection(); { - NeoUI::SetPerRowLayout(ARRAYSIZE(PLAYER_ROW_PROP), PLAYER_ROW_PROP); - // Headers - static constexpr const wchar_t *PLAYER_HEADERS[GSPS__TOTAL] = { - L"Score", L"Name", L"Time" - }; - for (int i = 0; i < GSPS__TOTAL; ++i) - { - vgui::surface()->DrawSetColor((m_serverPlayers.m_sortCtx.col == i) ? COLOR_NEOPANELACCENTBG : COLOR_BLACK_TRANSPARENT); - if (NeoUI::Button(PLAYER_HEADERS[i]).bPressed) - { - m_bSPlayersSortModified = true; - if (m_serverPlayers.m_sortCtx.col == i) - { - m_serverPlayers.m_sortCtx.bDescending = !m_serverPlayers.m_sortCtx.bDescending; - } - else - { - m_serverPlayers.m_sortCtx.col = static_cast(i); - } - } - - if (m_serverPlayers.m_sortCtx.col == i) - { - DrawSortHint(m_serverPlayers.m_sortCtx.bDescending); - } - } + m_headerModFlagsPlayers |= NeoUI::TableHeader(TABLE_HEADERS_PLAYER, GSPS__TOTAL, + m_iColsWideDetailedPlayerList, &m_serverPlayers.m_sortCtx.col, + &m_serverPlayers.m_sortCtx.bDescending, 1); } NeoUI::EndSection(); @@ -2111,38 +2341,41 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) g_uiCtx.dPanel.tall = iTallTotal - (2 * g_uiCtx.layout.iRowTall) - iInfoTall; NeoUI::BeginSection(); { - NeoUI::SetPerRowLayout(ARRAYSIZE(PLAYER_ROW_PROP), PLAYER_ROW_PROP); - // Players - rows - for (const auto &player : m_serverPlayers.m_sortedPlayers) + NeoUI::BeginTable(m_iColsWideDetailedPlayerList, GSPS__TOTAL); { - wchar_t wszText[32]; - - V_swprintf_safe(wszText, L"%d", player.iScore); - NeoUI::Label(wszText); - NeoUI::Label(player.wszName); + // Players - rows + for (const auto &player : m_serverPlayers.m_sortedPlayers) { - static constexpr float FL_SECSINMIN = 60.0f; - static constexpr float FL_SECSINHRS = 60.0f * FL_SECSINMIN; - if (player.flTimePlayed < FL_SECSINMIN) - { - V_swprintf_safe(wszText, L"%.0fs", player.flTimePlayed); - } - else if (player.flTimePlayed < FL_SECSINMIN * 60.0f) - { - const int iMin = player.flTimePlayed / FL_SECSINMIN; - const int iSec = player.flTimePlayed - (iMin * FL_SECSINMIN); - V_swprintf_safe(wszText, L"%dm %ds", iMin, iSec); - } - else + wchar_t wszText[32]; + + V_swprintf_safe(wszText, L"%d", player.iScore); + NeoUI::Label(wszText); + NeoUI::Label(player.wszName); { - const int iHrs = player.flTimePlayed / FL_SECSINHRS; - const int iMin = (player.flTimePlayed - (iHrs * FL_SECSINHRS)) / FL_SECSINMIN; - const int iSec = player.flTimePlayed - (iHrs * FL_SECSINHRS) - (iMin * FL_SECSINMIN); - V_swprintf_safe(wszText, L"%dh %dm %ds", iHrs, iMin, iSec); + static constexpr float FL_SECSINMIN = 60.0f; + static constexpr float FL_SECSINHRS = 60.0f * FL_SECSINMIN; + if (player.flTimePlayed < FL_SECSINMIN) + { + V_swprintf_safe(wszText, L"%.0fs", player.flTimePlayed); + } + else if (player.flTimePlayed < FL_SECSINMIN * 60.0f) + { + const int iMin = player.flTimePlayed / FL_SECSINMIN; + const int iSec = player.flTimePlayed - (iMin * FL_SECSINMIN); + V_swprintf_safe(wszText, L"%dm %ds", iMin, iSec); + } + else + { + const int iHrs = player.flTimePlayed / FL_SECSINHRS; + const int iMin = (player.flTimePlayed - (iHrs * FL_SECSINHRS)) / FL_SECSINMIN; + const int iSec = player.flTimePlayed - (iHrs * FL_SECSINHRS) - (iMin * FL_SECSINMIN); + V_swprintf_safe(wszText, L"%dh %dm %ds", iHrs, iMin, iSec); + } } + NeoUI::Label(wszText); } - NeoUI::Label(wszText); } + NeoUI::EndTable(); } NeoUI::EndSection(); } @@ -2211,20 +2444,19 @@ void CNeoRoot::MainLoopServerDetails(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); } void CNeoRoot::MainLoopPlayerList(const MainLoopParam param) { + const int iTallTotal = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 2); + g_uiCtx.dPanel.wide = g_iRootSubPanelWide; + g_uiCtx.dPanel.x = (param.wide / 2) - (g_iRootSubPanelWide / 2); + g_uiCtx.dPanel.y = (param.tall / 2) - (iTallTotal / 2); + g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 1); + NeoUI::BeginContext(&g_uiCtx, param.eMode, L"Player list", "CtxPlayerList"); if (IsInGame()) { - const int iTallTotal = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 2); - g_uiCtx.dPanel.wide = g_iRootSubPanelWide; - g_uiCtx.dPanel.x = (param.wide / 2) - (g_iRootSubPanelWide / 2); - g_uiCtx.dPanel.y = (param.tall / 2) - (iTallTotal / 2); - g_uiCtx.dPanel.tall = g_uiCtx.layout.iRowTall * (g_iRowsInScreen + 1); g_uiCtx.colors.sectionBg = COLOR_BLACK_TRANSPARENT; - NeoUI::BeginContext(&g_uiCtx, param.eMode, L"Player list", "CtxPlayerList"); { NeoUI::BeginSection(NeoUI::SECTIONFLAG_DEFAULTFOCUS); { @@ -2269,7 +2501,6 @@ void CNeoRoot::MainLoopPlayerList(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); } else { @@ -2391,20 +2622,11 @@ void CNeoRoot::MainLoopPopup(const MainLoopParam param) { if (NeoUI::Button(L"Enter (Enter)").bPressed || NeoUI::BindKeyEnter()) { - g_pNeoRoot->m_flTimeLoadingScreenTransition = gpGlobals->realtime; + const auto &gameServer = m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer]; char szServerPassword[ARRAYSIZE(m_wszServerPassword)]; g_pVGuiLocalize->ConvertUnicodeToANSI(m_wszServerPassword, szServerPassword, sizeof(szServerPassword)); - ConVarRef("password").SetValue(szServerPassword); - V_memset(m_wszServerPassword, 0, sizeof(m_wszServerPassword)); - - const auto gameServer = m_serverBrowser[m_iServerBrowserTab].m_filteredServers[m_iSelectedServer]; - char connectCmd[256]; - const char *szAddress = gameServer.m_NetAdr.GetConnectionAddressString(); - V_sprintf_safe(connectCmd, "progress_enable; wait; connect %s", szAddress); - engine->ClientCmd_Unrestricted(connectCmd); - - m_state = STATE_ROOT; + OnEnterServer(gameServer, szServerPassword); } NeoUI::Pad(); if (NeoUI::Button(NeoUI::HintAlt(L"Cancel (ESC)", L"Cancel (B)")).bPressed || NeoUI::BindKeyBack()) @@ -2580,7 +2802,6 @@ void CNeoRoot::MainLoopPopup(const MainLoopParam param) } NeoUI::EndSection(); } - NeoUI::EndContext(); } void CNeoRoot::HTTPCallbackRequest(HTTPRequestCompleted_t *request, bool bIOFailure) diff --git a/src/game/client/neo/ui/neo_root.h b/src/game/client/neo/ui/neo_root.h index 06b2e1a77..a7bbd2d48 100644 --- a/src/game/client/neo/ui/neo_root.h +++ b/src/game/client/neo/ui/neo_root.h @@ -3,8 +3,22 @@ #include #include "GameUI/IGameUI.h" #include + +// GCC shipped on SteamRT3 giving false positive +#ifdef ACTUALLY_COMPILER_GCC +#pragma GCC diagnostic push +#if ((__GNUC__ >= 10) && (__GNUC__ <= 13)) +#pragma GCC diagnostic ignored "-Wstringop-overflow" +#pragma GCC diagnostic ignored "-Wstringop-truncation" +#endif +#endif + #include +#ifdef ACTUALLY_COMPILER_GCC +#pragma GCC diagnostic pop +#endif + #include "neo_ui.h" #include "neo_root_serverbrowser.h" #include "neo_root_settings.h" @@ -139,6 +153,7 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener void OnTick() final; void FireGameEvent(IGameEvent *event) final; + void OnEnterServer(const gameserveritem_t gameServer, const char *pszServerPassword); void OnMainLoop(const NeoUI::Mode eMode); struct MainLoopParam @@ -172,10 +187,10 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener int m_iServerBrowserTab = 0; CNeoServerList m_serverBrowser[GS__TOTAL]; CNeoServerPlayers m_serverPlayers; - ServerBrowserFilters m_sbFilters; - bool m_bSBFiltModified = false; + ServerBrowserFilters m_sbFilters = {}; + NeoUI::TableHeaderModFlags m_headerModFlagsServerBrowser = 0; bool m_bShowFilterPanel = false; - bool m_bSPlayersSortModified = false; + NeoUI::TableHeaderModFlags m_headerModFlagsPlayers = 0; GameServerSortContext m_sortCtx = {}; wchar_t m_wszBindingText[128]; @@ -231,6 +246,21 @@ class CNeoRoot : public vgui::EditablePanel, public CGameEventListener servernetadr_t m_favCacheNetAdr = {}; bool m_bFavCacheIsFav = false; bool m_bAutoRefreshFav = false; + + int m_iColsWideServerBrowser[GSIW__TOTAL] = {}; + int m_iColsWideServerBlacklist[SBLIST_COL__TOTAL] = {}; + int m_iColsWideDetailedPlayerList[GSPS__TOTAL] = {}; + + int m_iUpDownInitialServer = -1; + int m_iUpDownDirection = 0; + + NeoUI::TabsState m_tabsStateSettings = {}; + NeoUI::TabsState m_tabsStateServerBrowser = {}; + NeoUI::TabsState m_tabsStateIFF = {}; + + float m_flAutoJoinLastAttempt = 0.0f; + CNeoServerPing m_serverPingAutoJoin = {}; + CNeoServerPing m_serverPingEnter = {}; }; extern CNeoRoot *g_pNeoRoot; diff --git a/src/game/client/neo/ui/neo_root_serverbrowser.cpp b/src/game/client/neo/ui/neo_root_serverbrowser.cpp index 05bd6063d..97f60ddd9 100644 --- a/src/game/client/neo/ui/neo_root_serverbrowser.cpp +++ b/src/game/client/neo/ui/neo_root_serverbrowser.cpp @@ -205,13 +205,14 @@ void CNeoServerList::UpdateFilteredList() } gameserveritem_t curGsi; - if (IN_BETWEEN_AR(0, g_pNeoRoot->m_iSelectedServer, m_filteredServers.Count())) + if (IN_BETWEEN_AR(0, g_pNeoRoot->m_iSelectedServer, m_filteredServers.size())) { V_memcpy(&curGsi, &m_filteredServers[g_pNeoRoot->m_iSelectedServer], sizeof(gameserveritem_t)); } m_filteredServers = m_servers; - if (m_filteredServers.IsEmpty()) + + if (m_filteredServers.empty()) { g_pNeoRoot->m_iSelectedServer = -1; return; @@ -258,6 +259,10 @@ void CNeoServerList::UpdateFilteredList() iLeft = gsiLeft.m_nPing; iRight = gsiRight.m_nPing; break; + case GSIW_IP_ADDRESS: + iLeft = gsiLeft.m_NetAdr.GetIP(); + iRight = gsiRight.m_NetAdr.GetIP(); + break; case GSIW_NAME: default: // no-op, already assigned (default) @@ -272,6 +277,7 @@ void CNeoServerList::UpdateFilteredList() break; case GSIW_PLAYERS: case GSIW_PING: + case GSIW_IP_ADDRESS: if (iLeft != iRight) return (m_pSortCtx->bDescending) ? iLeft < iRight : iLeft > iRight; break; default: @@ -280,10 +286,10 @@ void CNeoServerList::UpdateFilteredList() return (m_pSortCtx->bDescending) ? (V_strcmp(szRight, szLeft) > 0) : (V_strcmp(szLeft, szRight) > 0); }); - if (IN_BETWEEN_AR(0, g_pNeoRoot->m_iSelectedServer, m_filteredServers.Count())) + if (IN_BETWEEN_AR(0, g_pNeoRoot->m_iSelectedServer, m_filteredServers.size())) { g_pNeoRoot->m_iSelectedServer = -1; - for (int i = 0; i < m_filteredServers.Count(); ++i) + for (int i = 0; i < m_filteredServers.size(); ++i) { if (V_memcmp(&curGsi, &m_filteredServers[i], sizeof(gameserveritem_t)) == 0) { @@ -301,6 +307,8 @@ void CNeoServerList::RequestList() return; } + m_servers.clear(); + static MatchMakingKeyValuePair_t mmFilters[] = { {"gamedir", "neo"}, }; @@ -331,9 +339,26 @@ void CNeoServerList::ServerResponded(HServerListRequest hRequest, int iServer) ISteamMatchmakingServers *steamMM = steamapicontext->SteamMatchmakingServers(); gameserveritem_t *pServerDetails = steamMM->GetServerDetails(hRequest, iServer); - if (pServerDetails) + if (pServerDetails && m_servers.size() < 1024) { - m_servers.AddToTail(*pServerDetails); + wchar_t wszServerName[k_cbMaxGameServerName] = {}; + g_pVGuiLocalize->ConvertANSIToUnicode( + pServerDetails->GetName(), wszServerName, sizeof(wszServerName)); + const int iWszLen = V_wcslen(wszServerName); + // NEO TODO (nullsystem): Need to implement fallback fonts in NeoUI or otherwise + // without fallback unicode fonts can corrupt the memory on drawing the text somehow + for (int i = 0; i < iWszLen; ++i) + { + if (wszServerName[i] >= 256) + { + wszServerName[i] = L'?'; + } + } + char szServerName[k_cbMaxGameServerName] = {}; + g_pVGuiLocalize->ConvertUnicodeToANSI(wszServerName, szServerName, sizeof(szServerName)); + Q_UnicodeRepair(szServerName); + pServerDetails->SetName(szServerName); + m_servers.push_back(*pServerDetails); m_bModified = true; } } @@ -350,7 +375,7 @@ void CNeoServerList::RefreshComplete(HServerListRequest hRequest, EMatchMakingSe if (m_iType == GS_BLACKLIST || hRequest != m_hdlRequest) return; m_bSearching = false; - if (response == eNoServersListedOnMasterServer && m_servers.IsEmpty()) + if (response == eNoServersListedOnMasterServer && m_servers.empty()) { m_bModified = true; } @@ -431,3 +456,28 @@ void CNeoServerPlayers::PlayersRefreshComplete() { m_hdlQuery = HSERVERQUERY_INVALID; } + +void CNeoServerPing::RequestPing() +{ + auto *steamMM = steamapicontext->SteamMatchmakingServers(); + if (m_hdlPing != HSERVERQUERY_INVALID) + { + steamMM->CancelServerQuery(m_hdlPing); + } + const uint32 unIP = m_serverInfo.m_NetAdr.GetIP(); + const uint16 usPort = m_serverInfo.m_NetAdr.GetQueryPort(); + m_hdlPing = steamMM->PingServer(unIP, usPort, this); + m_ePingState = PINGSTATE_NIL; +} + +void CNeoServerPing::ServerResponded(gameserveritem_t &server) +{ + m_serverInfo = server; + m_ePingState = PINGSTATE_SUCCESS; +} + +void CNeoServerPing::ServerFailedToRespond() +{ + m_ePingState = PINGSTATE_FAILED; +} + diff --git a/src/game/client/neo/ui/neo_root_serverbrowser.h b/src/game/client/neo/ui/neo_root_serverbrowser.h index aa297d503..9b4b53990 100644 --- a/src/game/client/neo/ui/neo_root_serverbrowser.h +++ b/src/game/client/neo/ui/neo_root_serverbrowser.h @@ -1,8 +1,24 @@ #pragma once #include + +// GCC shipped on SteamRT3 giving false positive +#ifdef ACTUALLY_COMPILER_GCC +#pragma GCC diagnostic push +#if ((__GNUC__ >= 10) && (__GNUC__ <= 13)) +#pragma GCC diagnostic ignored "-Wstringop-overflow" +#pragma GCC diagnostic ignored "-Wstringop-truncation" +#endif +#endif + #include + +#ifdef ACTUALLY_COMPILER_GCC +#pragma GCC diagnostic pop +#endif + #include +#include enum EServerBlacklistType { @@ -64,9 +80,11 @@ enum GameServerInfoW GSIW_LOCKED = 0, GSIW_VAC, GSIW_NAME, + GSIW_IP_ADDRESS, GSIW_MAP, GSIW_PLAYERS, GSIW_PING, + GSIW_TAGS, GSIW__TOTAL, }; @@ -80,6 +98,14 @@ enum AntiCheatMode ANTICHEAT__TOTAL, }; +enum ETagsFilterMode +{ + TAGSFILTER_INCLUDE = 0, + TAGSFILTER_EXCLUDE, + + TAGSFILTER__TOTAL, +}; + enum GameServerPlayerSort { GSPS_SCORE = 0, @@ -101,7 +127,7 @@ struct GameServerSortContext struct GameServerPlayerSortContext { - GameServerPlayerSort col = GSPS_SCORE; + int col = GSPS_SCORE; bool bDescending = true; }; @@ -109,8 +135,11 @@ class CNeoServerList : public ISteamMatchmakingServerListResponse { public: GameServerType m_iType; - CUtlVector m_servers; - CUtlVector m_filteredServers; + + // NEO JANK (nullsystem): CUtlVector easily corrupts memory with + // gameserveritem_t results, so using std::vector instead + std::vector m_servers; + std::vector m_filteredServers; void UpdateFilteredList(); void RequestList(); @@ -148,6 +177,24 @@ class CNeoServerPlayers : public ISteamMatchmakingPlayersResponse GameServerPlayerSortContext m_sortCtx; }; +class CNeoServerPing : public ISteamMatchmakingPingResponse +{ +public: + void RequestPing(); + void ServerResponded(gameserveritem_t &server) final; + void ServerFailedToRespond() final; + HServerQuery m_hdlPing = HSERVERQUERY_INVALID; + gameserveritem_t m_serverInfo = {}; + + enum EPingState + { + PINGSTATE_NIL = 0, // Either pinging or inactive + PINGSTATE_SUCCESS, + PINGSTATE_FAILED, + }; + EPingState m_ePingState = PINGSTATE_NIL; +}; + struct ServerBrowserFilters { bool bServerNotFull = false; @@ -155,4 +202,9 @@ struct ServerBrowserFilters bool bIsNotPasswordProtected = false; int iAntiCheat = 0; int iMaxPing = 0; + wchar_t wszMapFilter[k_cbMaxGameServerMapName] = {}; + int iMaxPlayerCount = 0; + int iTagsFilterType = 0; + wchar_t wszTagsFilter[k_cbMaxGameServerTags] = {}; }; + diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 44a6eb96e..3191b5419 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1489,7 +1489,8 @@ void NeoSettings_HUD(NeoSettings *ns) NeoUI::SetPerRowLayout(1); NeoUI::Tabs(IFF_LABELS, iIFFLabelsSize, &optionChosen, - NeoUI::TABFLAG_NOSIDEKEYS | NeoUI::TABFLAG_NOSTATERESETS); + NeoUI::TABFLAG_NOSIDEKEYS | NeoUI::TABFLAG_NOSTATERESETS, + &g_pNeoRoot->m_tabsStateIFF); NeoUI::SetPerRowLayout(2, NeoUI::ROWLAYOUT_TWOSPLIT); // NEO TODO (Adam) Show what the marker looks like somewhere here diff --git a/src/game/client/neo/ui/neo_theme.cpp b/src/game/client/neo/ui/neo_theme.cpp index 7fa00bce5..b25463718 100644 --- a/src/game/client/neo/ui/neo_theme.cpp +++ b/src/game/client/neo/ui/neo_theme.cpp @@ -27,5 +27,8 @@ void SetupNTRETheme(NeoUI::Context *pNeoUICtx) pColors->sliderHotBg = COLOR_GREY; pColors->sliderActiveBg = COLOR_BLACK; pColors->tabHintsFg = COLOR_WHITE; + pColors->tableHeaderSortIndicatorBg = COLOR_WHITE; + pColors->headerDragNormalBg = COLOR_FADED_WHITE; + pColors->headerDragActiveBg = COLOR_DARK_RED; } diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index 28607a409..714c85e27 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -26,7 +26,15 @@ static Context *c = &g_emptyCtx; const int ROWLAYOUT_TWOSPLIT[] = { 40, -1 }; static constexpr int WDGINFO_ALLOC_STEPS = 64; static constexpr float FL_BORDER_RATIO = 0.2f; -#define NEOUI_SCROLL_THICKNESS() (c->iMarginX * 4) + +// NEO JANK (nullsystem): DrawUnicodeChar is buggy, so use DrawPrintText instead, hence +// single character but a (wide-)string instead of (wide-)char +static constexpr wchar_t WSZ_ARROW_UP[] = L"\u2191"; +static constexpr wchar_t WSZ_ARROW_DOWN[] = L"\u2193"; +static constexpr wchar_t WSZ_ARROW_LEFT[] = L"<"; +static constexpr wchar_t WSZ_ARROW_RIGHT[] = L">"; +static constexpr wchar_t WSZ_CHECK_MARK[] = L"\u2713"; +#define NEOUI_SCROLL_THICKNESS() (c->iMarginX * 3) #define DEBUG_NEOUI 0 // NEO NOTE (nullsystem): !!! Always flip to 0 on master + PR !!! #ifndef DEBUG @@ -160,8 +168,11 @@ void BeginMultiWidgetHighlighter(const int iTotalWidgets) if (bActive) c->uMultiHighlightFlags |= MULTIHIGHLIGHTFLAG_ACTIVE; } -static void DrawBorder(const vgui::IntRect &rect, const int iMargin) +static void DrawBorder(const vgui::IntRect &rect) { + const int iMargin = Max(static_cast(FL_BORDER_RATIO * c->iMarginY), 1); + vgui::surface()->DrawSetColor(c->colors.hotBorder); + vgui::surface()->DrawFilledRect(rect.x0, rect.y0, rect.x1, rect.y0 + iMargin); // top vgui::surface()->DrawFilledRect(rect.x0, rect.y1 - iMargin, rect.x1, rect.y1); // bottom vgui::surface()->DrawFilledRect(rect.x0, rect.y0, rect.x0 + iMargin, rect.y1); // left @@ -172,9 +183,7 @@ void EndMultiWidgetHighlighter() { if (c->eMode == MODE_PAINT && c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_HOT) { - const int iHotMargin = static_cast(FL_BORDER_RATIO * c->iMarginY); - vgui::surface()->DrawSetColor(c->colors.hotBorder); - DrawBorder(c->rMultiHighlightArea, iHotMargin); + DrawBorder(c->rMultiHighlightArea); } c->uMultiHighlightFlags = 0; } @@ -272,7 +281,9 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar c->eMode = eMode; c->iLayoutX = 0; c->iLayoutY = 0; + c->irWidgetLayoutX = 0; c->irWidgetLayoutY = 0; + c->irWidgetMaxX = 0; c->iWidget = 0; c->iSection = 0; c->iHasMouseInPanel = 0; @@ -282,12 +293,13 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar c->ibfSectionCanActive = 0; c->ibfSectionCanController = 0; // Different pointer, change context - const bool bFirstCtxUse = (c->pSzCurCtxName != pSzCtxName); - if (bFirstCtxUse) + c->bFirstCtxUse = (c->pSzCurCtxName != pSzCtxName); + if (c->bFirstCtxUse) { c->htSliders.RemoveAll(); c->pSzCurCtxName = pSzCtxName; - c->ibfSectionHasScroll = 0; + c->ibfSectionHasXScroll = 0; + c->ibfSectionHasYScroll = 0; c->iCurPopupId = 0; V_memset(&c->dimPopup, 0, sizeof(Dim)); c->colorEditInfo.r = nullptr; @@ -329,7 +341,7 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar { FontInfo *pFontI = &c->fonts[i]; const int iTall = vgui::surface()->GetFontTall(pFontI->hdl); - pFontI->iYOffset = (c->layout.iRowTall / 2) - (iTall / 2); + pFontI->iYFontOffset = (c->layout.iRowTall / 2) - (iTall / 2); { int iPrevNextWide, iPrevNextTall; vgui::surface()->GetTextSize(pFontI->hdl, L"<", iPrevNextWide, iPrevNextTall); @@ -343,7 +355,7 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar SwapFont(FONT_NTLARGE, true); vgui::surface()->DrawSetTextColor(c->colors.titleFg); vgui::surface()->DrawSetTextPos(c->dPanel.x + c->iMarginX, - c->dPanel.y + -c->layout.iRowTall + c->fonts[FONT_NTLARGE].iYOffset); + c->dPanel.y + -c->layout.iRowTall + c->fonts[FONT_NTLARGE].iYFontOffset); vgui::surface()->DrawPrintText(wszTitle, V_wcslen(wszTitle)); } break; @@ -460,23 +472,33 @@ void EndContext() void BeginSection(const ISectionFlags iSectionFlags) { // Previous frame(s) known this section does scroll - if (c->ibfSectionHasScroll & (1ULL << c->iSection)) + if (c->ibfSectionHasYScroll & (1ULL << c->iSection)) { // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" // without extra variable c->dPanel.wide -= NEOUI_SCROLL_THICKNESS(); } + if (c->ibfSectionHasXScroll & (1ULL << c->iSection)) + { + c->dPanel.tall -= NEOUI_SCROLL_THICKNESS(); + } - c->iLayoutX = 0; + c->iLayoutX = -c->iXOffset[c->iSection]; c->iLayoutY = -c->iYOffset[c->iSection]; + c->irWidgetLayoutX = c->iLayoutX; c->irWidgetLayoutY = c->iLayoutY; + c->irWidgetMaxX = 0; c->iWidget = 0; c->iIdxRowParts = -1; c->iIdxVertParts = -1; c->iVertLayoutY = 0; c->iSectionFlags = iSectionFlags; c->uMultiHighlightFlags = MULTIHIGHLIGHTFLAG_NONE; + c->layout.iTableLabelsSize = 0; + c->layout.piTableColsWide = nullptr; + c->curRowFlags = NEXTTABLEROWFLAG_NONE; + c->bBlockSectionMWheel = false; if (iSectionFlags & SECTIONFLAG_POPUP) { @@ -534,44 +556,93 @@ void EndSection() c->iActive = FOCUSOFF_NUM; c->iActiveSection = -1; } + const int iAbsLayoutX = c->irWidgetMaxX + c->iXOffset[c->iSection]; const int iAbsLayoutY = c->irWidgetLayoutY + c->irWidgetTall + c->iYOffset[c->iSection]; + c->wdgInfos[c->iWidget].iXOffsets = iAbsLayoutX; c->wdgInfos[c->iWidget].iYOffsets = iAbsLayoutY; // Scroll handling - const int iScrollThick = NEOUI_SCROLL_THICKNESS(); const int iMWheelJump = c->layout.iDefRowTall; - const bool bHasScroll = (iAbsLayoutY > c->dPanel.tall); - const bool bResetScrollPanelWide = (c->ibfSectionHasScroll & (1ULL << c->iSection)); + const bool bHasXScroll = (iAbsLayoutX > c->dPanel.wide); + const bool bHasYScroll = (iAbsLayoutY > c->dPanel.tall); + const int iScrollThick = NEOUI_SCROLL_THICKNESS(); + const bool bResetXScrollPanelWide = (c->ibfSectionHasXScroll & (1ULL << c->iSection)); + const bool bResetYScrollPanelTall = (c->ibfSectionHasYScroll & (1ULL << c->iSection)); - // Saved for next frame(s) to layout x-axis based on scroll - if (bHasScroll) + // Saved for next frame(s) to layout y-axis based on scroll + if (bHasYScroll) { - c->ibfSectionHasScroll |= (1ULL << c->iSection); + c->ibfSectionHasYScroll |= (1ULL << c->iSection); } else { - c->ibfSectionHasScroll &= ~(1ULL << c->iSection); + c->ibfSectionHasYScroll &= ~(1ULL << c->iSection); } - vgui::IntRect rectScrollArea = { + if (bHasXScroll) + { + c->ibfSectionHasXScroll |= (1ULL << c->iSection); + } + else + { + c->ibfSectionHasXScroll &= ~(1ULL << c->iSection); + } + + vgui::IntRect rectYScrollArea = { .x0 = c->dPanel.x + c->dPanel.wide, .y0 = c->dPanel.y, - .x1 = c->dPanel.x + c->dPanel.wide + iScrollThick, - .y1 = c->dPanel.y + c->dPanel.tall, + .x1 = c->dPanel.x + c->dPanel.wide + (bHasYScroll ? iScrollThick : 0), + .y1 = c->dPanel.y + c->dPanel.tall - (bHasXScroll ? iScrollThick : 0), + }; + // NEO TODO (nullsystem): X-axis scrollbar visibility is optional + vgui::IntRect rectXScrollArea = { + .x0 = c->dPanel.x, + .y0 = c->dPanel.y + c->dPanel.tall, + .x1 = c->dPanel.x + c->dPanel.wide - (bHasYScroll ? iScrollThick : 0), + .y1 = c->dPanel.y + c->dPanel.tall + (bHasXScroll ? iScrollThick : 0), }; - const bool bMouseInScrollbar = bHasScroll && InRect(rectScrollArea, c->iMouseAbsX, c->iMouseAbsY); - const bool bMouseInWheelable = c->bMouseInPanel || bMouseInScrollbar; + EXYMouseDragOffset eMouseInScrollbar = XYMOUSEDRAGOFFSET_NIL; + if (bHasYScroll && InRect(rectYScrollArea, c->iMouseAbsX, c->iMouseAbsY)) + { + eMouseInScrollbar = XYMOUSEDRAGOFFSET_YAXIS; + } + else if (bHasXScroll && InRect(rectXScrollArea, c->iMouseAbsX, c->iMouseAbsY)) + { + eMouseInScrollbar = XYMOUSEDRAGOFFSET_XAXIS; + } - if (c->eMode == MODE_MOUSEWHEELED && bMouseInWheelable) + const bool bMouseInYWheelable = c->bMouseInPanel || (XYMOUSEDRAGOFFSET_YAXIS == eMouseInScrollbar); + const bool bMouseInXWheelable = bHasXScroll && ((!bMouseInYWheelable && c->bMouseInPanel) || (XYMOUSEDRAGOFFSET_XAXIS == eMouseInScrollbar)); + + // y-axis always have precedence over x-axis on being wheelable + if (c->eMode == MODE_MOUSEWHEELED && bMouseInYWheelable) { - if (!bHasScroll) + if (false == c->bBlockSectionMWheel) { - c->iYOffset[c->iSection] = 0; + if (!bHasYScroll) + { + c->iYOffset[c->iSection] = 0; + } + else + { + c->iYOffset[c->iSection] += (c->eCode == MOUSE_WHEEL_UP) ? -iMWheelJump : +iMWheelJump; + c->iYOffset[c->iSection] = clamp(c->iYOffset[c->iSection], 0, iAbsLayoutY - c->dPanel.tall); + } } - else + } + else if (c->eMode == MODE_MOUSEWHEELED && bMouseInXWheelable) + { + if (false == c->bBlockSectionMWheel) { - c->iYOffset[c->iSection] += (c->eCode == MOUSE_WHEEL_UP) ? -iMWheelJump : +iMWheelJump; - c->iYOffset[c->iSection] = clamp(c->iYOffset[c->iSection], 0, iAbsLayoutY - c->dPanel.tall); + if (!bHasXScroll) + { + c->iXOffset[c->iSection] = 0; + } + else + { + c->iXOffset[c->iSection] += (c->eCode == MOUSE_WHEEL_UP) ? -iMWheelJump : +iMWheelJump; + c->iXOffset[c->iSection] = clamp(c->iXOffset[c->iSection], 0, iAbsLayoutX - c->dPanel.wide); + } } } else if (c->eMode == MODE_KEYPRESSED && IsKeyChangeWidgetFocus() && @@ -594,7 +665,7 @@ void EndSection() c->iHotSection = c->iActiveSection; } - if (!bHasScroll) + if (!bHasYScroll) { // Disable scroll if it doesn't need to c->iYOffset[c->iSection] = 0; @@ -604,7 +675,7 @@ void EndSection() // Scrolling up past visible, re-adjust c->iYOffset[c->iSection] = c->wdgInfos[c->iActive].iYOffsets; } - else if (c->wdgInfos[c->iActive].iYOffsets >= (c->iYOffset[c->iSection] + c->dPanel.tall)) + else if ((c->wdgInfos[c->iActive].iYOffsets + c->wdgInfos[c->iActive].iYTall) >= (c->iYOffset[c->iSection] + c->dPanel.tall)) { // Scrolling down post visible, re-adjust c->iYOffset[c->iSection] = c->wdgInfos[c->iActive].iYOffsets - c->dPanel.tall + c->wdgInfos[c->iActive].iYTall; @@ -612,63 +683,140 @@ void EndSection() } // Scroll bar area painting and mouse interaction (no keyboard as that's handled by active widgets) - if (bHasScroll) - { - const int iYStart = c->iYOffset[c->iSection]; - const int iYEnd = iYStart + c->dPanel.tall; - const float flYPercStart = iYStart / static_cast(iAbsLayoutY); - const float flYPercEnd = iYEnd / static_cast(iAbsLayoutY); - vgui::IntRect rectHandle{ - c->dPanel.x + c->dPanel.wide, - c->dPanel.y + static_cast(c->dPanel.tall * flYPercStart), - c->dPanel.x + c->dPanel.wide + iScrollThick, - c->dPanel.y + static_cast(c->dPanel.tall * flYPercEnd) - }; + if (bHasYScroll || bHasXScroll) + { + vgui::IntRect rectYHandle = {}; + vgui::IntRect rectXHandle = {}; - bool bAlterOffset = false; + if (bHasYScroll) + { + const int iYStart = c->iYOffset[c->iSection]; + const int iYEnd = iYStart + c->dPanel.tall; + const float flYPercStart = iYStart / static_cast(iAbsLayoutY); + const float flYPercEnd = iYEnd / static_cast(iAbsLayoutY); + rectYHandle.x0 = c->dPanel.x + c->dPanel.wide; + rectYHandle.y0 = c->dPanel.y + static_cast(c->dPanel.tall * flYPercStart); + rectYHandle.x1 = c->dPanel.x + c->dPanel.wide + iScrollThick; + rectYHandle.y1 = c->dPanel.y + static_cast(c->dPanel.tall * flYPercEnd); + } + if (bHasXScroll) + { + const int iXStart = c->iXOffset[c->iSection]; + const int iXEnd = iXStart + c->dPanel.wide; + const float flXPercStart = iXStart / static_cast(iAbsLayoutX); + const float flXPercEnd = iXEnd / static_cast(iAbsLayoutX); + rectXHandle.x0 = c->dPanel.x + static_cast(c->dPanel.wide * flXPercStart); + rectXHandle.y0 = c->dPanel.y + c->dPanel.tall; + rectXHandle.x1 = c->dPanel.x + static_cast(c->dPanel.wide * flXPercEnd); + rectXHandle.y1 = c->dPanel.y + c->dPanel.tall + iScrollThick; + } + + EXYMouseDragOffset eXYAlterOffset = XYMOUSEDRAGOFFSET_NIL; switch (c->eMode) { case MODE_PAINT: vgui::surface()->DrawSetColor(c->colors.scrollbarBg); - vgui::surface()->DrawFilledRectArray(&rectScrollArea, 1); - vgui::surface()->DrawSetColor(c->abYMouseDragOffset[c->iSection] ? - c->colors.scrollbarHandleActiveBg : c->colors.scrollbarHandleNormalBg); - vgui::surface()->DrawFilledRectArray(&rectHandle, 1); + if (bHasYScroll) vgui::surface()->DrawFilledRectArray(&rectYScrollArea, 1); + if (bHasXScroll) vgui::surface()->DrawFilledRectArray(&rectXScrollArea, 1); + + if (bHasYScroll) + { + vgui::surface()->DrawSetColor( + (c->aeXYMouseDragOffset[c->iSection] == XYMOUSEDRAGOFFSET_YAXIS) + ? c->colors.scrollbarHandleActiveBg + : c->colors.scrollbarHandleNormalBg); + vgui::surface()->DrawFilledRectArray(&rectYHandle, 1); + } + if (bHasXScroll) + { + vgui::surface()->DrawSetColor( + (c->aeXYMouseDragOffset[c->iSection] == XYMOUSEDRAGOFFSET_XAXIS) + ? c->colors.scrollbarHandleActiveBg + : c->colors.scrollbarHandleNormalBg); + vgui::surface()->DrawFilledRectArray(&rectXHandle, 1); + } + if (bHasYScroll && bHasXScroll) + { + vgui::surface()->DrawSetColor(c->colors.sectionBg); + vgui::surface()->DrawFilledRect(c->dPanel.x + c->dPanel.wide, + c->dPanel.y + c->dPanel.tall, + c->dPanel.x + c->dPanel.wide + iScrollThick, + c->dPanel.y + c->dPanel.tall + iScrollThick); + } break; case MODE_MOUSEPRESSED: - c->abYMouseDragOffset[c->iSection] = bMouseInScrollbar; - if (bMouseInScrollbar) + c->aeXYMouseDragOffset[c->iSection] = eMouseInScrollbar; + if (XYMOUSEDRAGOFFSET_YAXIS == eMouseInScrollbar) { + c->aeXYMouseDragOffset[c->iSection] = XYMOUSEDRAGOFFSET_YAXIS; // If not pressed on handle, set the drag at the middle - const bool bInHandle = InRect(rectHandle, c->iMouseAbsX, c->iMouseAbsY); + const bool bInHandle = InRect(rectYHandle, c->iMouseAbsX, c->iMouseAbsY); c->iStartMouseDragOffset[c->iSection] = (bInHandle) ? - (c->iMouseAbsY - rectHandle.y0) :((rectHandle.y1 - rectHandle.y0) / 2.0f); - bAlterOffset = true; + (c->iMouseAbsY - rectYHandle.y0) : ((rectYHandle.y1 - rectYHandle.y0) / 2.0f); + eXYAlterOffset = XYMOUSEDRAGOFFSET_YAXIS; + } + else if (XYMOUSEDRAGOFFSET_XAXIS == eMouseInScrollbar) + { + // If not pressed on handle, set the drag at the middle + const bool bInHandle = InRect(rectXHandle, c->iMouseAbsX, c->iMouseAbsY); + c->iStartMouseDragOffset[c->iSection] = (bInHandle) ? + (c->iMouseAbsX - rectXHandle.x0) : ((rectXHandle.x1 - rectXHandle.x0) / 2.0f); + eXYAlterOffset = XYMOUSEDRAGOFFSET_XAXIS; } break; case MODE_MOUSERELEASED: - c->abYMouseDragOffset[c->iSection] = false; + c->aeXYMouseDragOffset[c->iSection] = XYMOUSEDRAGOFFSET_NIL; break; case MODE_MOUSEMOVED: - bAlterOffset = c->abYMouseDragOffset[c->iSection]; + eXYAlterOffset = c->aeXYMouseDragOffset[c->iSection]; break; default: break; } - if (bAlterOffset) + // NEO TODO (nullsystem): Could we deal with overlap/partial if we restrict paint area for + // each section properly? So it actually paints partially if partial area painted. + if (eXYAlterOffset == XYMOUSEDRAGOFFSET_YAXIS) { - const float flXPercMouse = static_cast(c->iMouseRelY - c->iStartMouseDragOffset[c->iSection]) / static_cast(c->dPanel.tall); + const float flYPercMouse = static_cast(c->iMouseRelY - c->iStartMouseDragOffset[c->iSection]) / static_cast(c->dPanel.tall); // Do not allow smooth scrolling so we don't have to deal with overlap/partial in section widgets - const int iNextYOffset = clamp(flXPercMouse * iAbsLayoutY, 0, (iAbsLayoutY - c->dPanel.tall)); - c->iYOffset[c->iSection] = iNextYOffset - (iNextYOffset % c->layout.iDefRowTall); + const int iNextYOffset = clamp(flYPercMouse * iAbsLayoutY, 0, (iAbsLayoutY - c->dPanel.tall)); + if (iNextYOffset >= (iAbsLayoutY - c->dPanel.tall)) + { + c->iYOffset[c->iSection] = iNextYOffset; + } + else + { + c->iYOffset[c->iSection] = iNextYOffset - (iNextYOffset % c->layout.iDefRowTall); + } + } + else if (eXYAlterOffset == XYMOUSEDRAGOFFSET_XAXIS) + { + const float flXPercMouse = static_cast(c->iMouseRelX - c->iStartMouseDragOffset[c->iSection]) / static_cast(c->dPanel.wide); + // Allow smooth scrolling as we deal with overlap/partial for x-axis + // Unlike Y-axis we don't typically layout for X-axis scroll + const int iNextXOffset = clamp(flXPercMouse * iAbsLayoutX, 0, (iAbsLayoutX - c->dPanel.wide)); + c->iXOffset[c->iSection] = iNextXOffset; } - } + if (bHasYScroll) + { + c->iYOffset[c->iSection] = clamp(c->iYOffset[c->iSection], 0, iAbsLayoutY - c->dPanel.tall); + } + if (bHasXScroll) + { + c->iXOffset[c->iSection] = clamp(c->iXOffset[c->iSection], 0, iAbsLayoutX - c->dPanel.wide); + } + } + // NEO TODO (nullsystem): Change how dPanel works to enforce setting per BeginSection // so don't need to shift around wide on scrollbars and keep usage dPanel "immutable" // without extra variable - if (bResetScrollPanelWide) + if (bResetXScrollPanelWide) + { + c->dPanel.tall += iScrollThick; + } + if (bResetYScrollPanelTall) { c->dPanel.wide += iScrollThick; } @@ -752,7 +900,7 @@ void SetPerRowLayout(const int iColTotal, const int *iColProportions, const int // Bump y-axis with previous row layout before applying new row layout if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) { - c->iLayoutX = 0; + c->iLayoutX = -c->iXOffset[c->iSection]; c->iLayoutY += c->layout.iRowTall; c->irWidgetLayoutY = c->iLayoutY; } @@ -793,12 +941,15 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) c->wdgInfos = (DynWidgetInfos *)(realloc(c->wdgInfos, sizeof(DynWidgetInfos) * c->iWdgInfosMax)); } + const int iWdgXOffset = (c->bIgnoreXOffset) ? 0 : c->iXOffset[c->iSection]; + const bool bInTable = (c->layout.iTableLabelsSize > 0 && c->layout.piTableColsWide); + if (c->iIdxRowParts < 0) { // NEO NOTE (nullsystem): Don't need to bump c->iLayoutY // because any reset of iIdxRowParts either don't need to // or already done it - c->iLayoutX = 0; + c->iLayoutX = -iWdgXOffset; c->iIdxRowParts = 0; } @@ -823,7 +974,6 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) { iWdgTall = (1.0f / static_cast(c->layout.iVertPartsTotal)) * c->layout.iRowTall; } - c->iVertLayoutY += iWdgTall; iVertYOffset = c->iVertLayoutY - iWdgTall; @@ -840,19 +990,27 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) } Assert(c->layout.iRowPartsTotal > 0); - const int iRowPartsTotal = Max(1, c->layout.iRowPartsTotal); + const int iRowPartsTotal = (bInTable) + ? Max(1, c->layout.iTableLabelsSize) + : Max(1, c->layout.iRowPartsTotal); const bool bNextIsLast = (c->iIdxRowParts + 1) >= iRowPartsTotal; // NEO NOTE (nullsystem): If very last partition of the row, use what's left // instead so whatever pixels don't get left out int xWide; - if (bNextIsLast) + if (bNextIsLast && !bInTable) { xWide = c->dPanel.wide - c->iLayoutX; } else { - if (c->layout.iRowParts) + if (bInTable) + { + // If this is less than/equal 0, then the widgets should just check visibility + // and skip over + xWide = Max(0, c->layout.piTableColsWide[c->iIdxRowParts]); + } + else if (c->layout.iRowParts) { xWide = (c->layout.iRowParts[c->iIdxRowParts] / 100.0f) * c->dPanel.wide; } @@ -870,7 +1028,9 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) }; c->irWidgetWide = c->rWidgetArea.x1 - c->rWidgetArea.x0; c->irWidgetTall = c->rWidgetArea.y1 - c->rWidgetArea.y0; + c->irWidgetLayoutX = c->iLayoutX; c->irWidgetLayoutY = c->iLayoutY; + c->irWidgetMaxX = Max(c->irWidgetMaxX, c->irWidgetLayoutX + c->irWidgetWide); c->wdgInfos[c->iWidget].iYOffsets = c->iLayoutY + c->iYOffset[c->iSection]; c->wdgInfos[c->iWidget].iYTall = c->irWidgetTall; c->wdgInfos[c->iWidget].bCannotActive = (eWidgetFlag & WIDGETFLAG_SKIPACTIVE); @@ -879,20 +1039,13 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) { if (bNextIsLast) { - c->iLayoutX = 0; + c->iLayoutX = -iWdgXOffset; c->iLayoutY += c->layout.iRowTall; c->iIdxRowParts = 0; } else { - if (c->layout.iRowParts) - { - c->iLayoutX += (c->layout.iRowParts[c->iIdxRowParts] / 100.0f) * c->dPanel.wide; - } - else - { - c->iLayoutX += (1.0f / static_cast(iRowPartsTotal)) * c->dPanel.wide; - } + c->iLayoutX += xWide; ++c->iIdxRowParts; } } @@ -900,10 +1053,14 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) bool bHot = false; bool bActive = false; bool bNotCurHot = false; - const bool bInView = IN_BETWEEN_AR(0, c->irWidgetLayoutY, c->dPanel.tall); + // Only deal with Y-axis, X-axis only have smooth scrolling and isn't + // as reliable to check if in view or not + const bool bInView = (xWide > 0) && IN_BETWEEN_AR(0, c->irWidgetLayoutY, c->dPanel.tall); + const bool bInteract = bInView && IN_BETWEEN_AR(0, c->irWidgetLayoutY + c->layout.iRowTall - 1, + c->dPanel.tall); // Check mouse hot/active states if WIDGETFLAG_MOUSE flag set - if (eWidgetFlag & WIDGETFLAG_MOUSE && bInView) + if (eWidgetFlag & WIDGETFLAG_MOUSE && bInteract) { bNotCurHot = (c->iHotPersist != c->iWidget || c->iHotPersistSection != c->iSection); const bool bMouseIn = IN_BETWEEN_EQ(c->rWidgetArea.x0, c->iMouseAbsX, c->rWidgetArea.x1 - 1) @@ -962,12 +1119,12 @@ CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag) const bool bColorHot = bHot || (c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_HOT); const bool bColorActive = ((bActive || (c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_ACTIVE)) || (((c->popupFlags & POPUPFLAGS_COLORHOTASACTIVE) == POPUPFLAGS_COLORHOTASACTIVE) && bColorHot)); - if (bColorActive) + if (bColorActive || (bInTable && (c->curRowFlags & NEXTTABLEROWFLAG_SELECTED))) { vgui::surface()->DrawSetColor(c->colors.activeBg); vgui::surface()->DrawSetTextColor(c->colors.activeFg); } - else if (bColorHot) + else if (bColorHot || (bInTable && (c->curRowFlags & NEXTTABLEROWFLAG__HOT))) { vgui::surface()->DrawSetColor(c->colors.hotBg); vgui::surface()->DrawSetTextColor(c->colors.hotFg); @@ -998,9 +1155,7 @@ void EndWidget(const CurrentWidgetState &wdgState) !(c->uMultiHighlightFlags & MULTIHIGHLIGHTFLAG_IN_USE) && wdgState.bHot) { - const int iHotMargin = static_cast(FL_BORDER_RATIO * c->iMarginY); - vgui::surface()->DrawSetColor(c->colors.hotBorder); - DrawBorder(c->rWidgetArea, iHotMargin); + DrawBorder(c->rWidgetArea); } ++c->iWidget; @@ -1034,8 +1189,8 @@ void Pad() void Divider(const wchar_t *wszText) { - Context::Layout tmp; - V_memcpy(&tmp, &c->layout, sizeof(Context::Layout)); + Layout tmp; + V_memcpy(&tmp, &c->layout, sizeof(Layout)); SetPerRowLayout(1, nullptr, tmp.iRowTall); c->eLabelTextStyle = NeoUI::TEXTSTYLE_CENTER; @@ -1056,7 +1211,7 @@ void Divider(const wchar_t *wszText) // Text const auto *pFontI = &c->fonts[c->eFont]; const int x = XPosFromText(wszText, pFontI, c->eLabelTextStyle); - const int y = pFontI->iYOffset; + const int y = pFontI->iYFontOffset; vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + x, c->rWidgetArea.y0 + y); vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); @@ -1115,7 +1270,7 @@ void LabelWrap(const wchar_t *wszText) if (c->eMode == MODE_PAINT) { vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + c->iMarginX, - c->rWidgetArea.y0 + pFontI->iYOffset + iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset + iYOffset); vgui::surface()->DrawPrintText(wszText + iStart, iLastSpace - iStart); } ++iLines; @@ -1131,7 +1286,7 @@ void LabelWrap(const wchar_t *wszText) if (c->eMode == MODE_PAINT) { vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + c->iMarginX, - c->rWidgetArea.y0 + pFontI->iYOffset + iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset + iYOffset); vgui::surface()->DrawPrintText(wszText + iStart, iWszSize - iStart); } ++iLines; @@ -1139,15 +1294,17 @@ void LabelWrap(const wchar_t *wszText) } c->iLayoutY += (c->layout.iRowTall * iLines); + c->irWidgetLayoutX = c->iLayoutX; c->irWidgetLayoutY = c->iLayoutY; + c->irWidgetMaxX = Max(c->irWidgetMaxX, c->irWidgetLayoutX + c->irWidgetWide); EndWidget(wdgState); } void HeadingLabel(const wchar_t *wszText) { - Context::Layout tmp; - V_memcpy(&tmp, &c->layout, sizeof(Context::Layout)); + Layout tmp; + V_memcpy(&tmp, &c->layout, sizeof(Layout)); SetPerRowLayout(1, nullptr, tmp.iRowTall); c->eLabelTextStyle = NeoUI::TEXTSTYLE_CENTER; @@ -1171,11 +1328,51 @@ void Label(const wchar_t *wszText, const bool bNotWidget) } if (wdgState.bInView && c->eMode == MODE_PAINT) { - const auto *pFontI = &c->fonts[c->eFont]; - const int x = XPosFromText(wszText, pFontI, c->eLabelTextStyle); - const int y = pFontI->iYOffset; - vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + x, c->rWidgetArea.y0 + y); - vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + const int iLDiff = Max(c->dPanel.x - c->rWidgetArea.x0, 0); + const int iRDiff = Max(c->rWidgetArea.x1 - (c->dPanel.x + c->dPanel.wide), 0); + Dim viewportDim = {}; + if (bNotWidget) + { + viewportDim.x = c->dPanel.x; + viewportDim.y = c->dPanel.y; + viewportDim.wide = c->dPanel.wide; + viewportDim.tall = c->dPanel.tall; + } + else + { + viewportDim.x = Max(c->rWidgetArea.x0, c->dPanel.x); + viewportDim.y = c->rWidgetArea.y0; + viewportDim.wide = c->irWidgetWide - iLDiff - iRDiff; + viewportDim.tall = c->irWidgetTall; + } + // NEO NOTE (nullsystem): Sanity check the viewport makes sense + if (IN_BETWEEN_EQ(c->dPanel.x, viewportDim.x, c->dPanel.x + c->dPanel.wide) + && IN_BETWEEN_EQ(c->dPanel.y, viewportDim.y, c->dPanel.y + c->dPanel.tall) + && IN_BETWEEN_EQ(c->dPanel.y, viewportDim.y + viewportDim.tall, c->dPanel.y + c->dPanel.tall) + && IN_BETWEEN_EQ(1, viewportDim.wide, c->dPanel.wide) + && IN_BETWEEN_EQ(1, viewportDim.tall, c->dPanel.tall)) + { + vgui::surface()->SetFullscreenViewport( + viewportDim.x, viewportDim.y, + viewportDim.wide, viewportDim.tall); + vgui::surface()->PushFullscreenViewport(); + + const auto *pFontI = &c->fonts[c->eFont]; + int x = XPosFromText(wszText, pFontI, c->eLabelTextStyle); + int y = pFontI->iYFontOffset; + if (bNotWidget) + { + vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 - c->dPanel.x + x, c->rWidgetArea.y0 - c->dPanel.y + y); + } + else + { + vgui::surface()->DrawSetTextPos(x - iLDiff, y); + } + vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + + vgui::surface()->PopFullscreenViewport(); + vgui::surface()->SetFullscreenViewport(0, 0, 0, 0); + } } if (!bNotWidget) { @@ -1203,7 +1400,7 @@ void Label(const wchar_t *wszLabel, const wchar_t *wszText) Label(wszText); } -NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const EBaseButtonType eType) +NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const EBaseButtonType eType, const bool bVal) { const auto wdgState = BeginWidget(WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE); @@ -1224,7 +1421,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c const auto *pFontI = &c->fonts[c->eFont]; const int x = XPosFromText(wszText, pFontI, c->eButtonTextStyle); - const int y = pFontI->iYOffset; + const int y = pFontI->iYFontOffset; vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + x, c->rWidgetArea.y0 + y); vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); } break; @@ -1235,6 +1432,30 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, c->irWidgetWide, c->irWidgetTall); } break; + case BASEBUTTONTYPE_CHECKBOX: + { + vgui::surface()->DrawFilledRectArray(&c->rWidgetArea, 1); + + const auto *pFontI = &c->fonts[c->eFont]; + int iFontTextWidth = 0, iFontTextHeight = 0; + vgui::surface()->GetTextSize(pFontI->hdl, WSZ_CHECK_MARK, iFontTextWidth, iFontTextHeight); + + const int x = (c->layout.iRowTall / 2) - (iFontTextWidth / 2); + const int y = pFontI->iYFontOffset; + + if (bVal) + { + vgui::surface()->DrawSetTextPos( + c->rWidgetArea.x0 + x, + c->rWidgetArea.y0 + y); + vgui::surface()->DrawPrintText(WSZ_CHECK_MARK, 1); + } + + vgui::surface()->DrawSetTextPos( + c->rWidgetArea.x0 + c->layout.iRowTall + c->iMarginX, + c->rWidgetArea.y0 + y); + vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + } break; } } break; @@ -1251,7 +1472,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c break; case MODE_KEYPRESSED: { - ret.bKeyPressed = ret.bPressed = (wdgState.bActive && IsKeyEnter()); + ret.bKeyEnterPressed = ret.bPressed = (wdgState.bActive && IsKeyEnter()); } break; default: @@ -1456,6 +1677,11 @@ NeoUI::RetButton ButtonTexture(const char *szTexturePath) return BaseButton(L"", szTexturePath, BASEBUTTONTYPE_IMAGE); } +NeoUI::RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal) +{ + return BaseButton(wszText, "", BASEBUTTONTYPE_CHECKBOX, bVal); +} + void ResetTextures() { CUtlHashtable *pHtTexMap = &(c->htTexMap); @@ -1516,7 +1742,7 @@ void RingBox(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex) // Center-text label vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + ((c->irWidgetWide / 2) - (iFontWide / 2)), - c->rWidgetArea.y0 + pFontI->iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset); vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); // Left-side "<" prev button @@ -1524,14 +1750,14 @@ void RingBox(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex) c->rWidgetArea.x0 + c->layout.iRowTall, c->rWidgetArea.y1); vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + pFontI->iStartBtnXPos, c->rWidgetArea.y0 + pFontI->iStartBtnYPos); - vgui::surface()->DrawPrintText(L"<", 1); + vgui::surface()->DrawPrintText(WSZ_ARROW_LEFT, 1); // Right-side ">" next button vgui::surface()->DrawFilledRect(c->rWidgetArea.x1 - c->layout.iRowTall, c->rWidgetArea.y0, c->rWidgetArea.x1, c->rWidgetArea.y1); vgui::surface()->DrawSetTextPos(c->rWidgetArea.x1 - c->layout.iRowTall + pFontI->iStartBtnXPos, c->rWidgetArea.y0 + pFontI->iStartBtnYPos); - vgui::surface()->DrawPrintText(L">", 1); + vgui::surface()->DrawPrintText(WSZ_ARROW_RIGHT, 1); } break; case MODE_MOUSEPRESSED: @@ -1583,7 +1809,7 @@ void RingBox(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex) } void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, - const TabsFlags flags, int *piTabWide) + const TabsFlags flags, TabsState *pState) { // This is basically a ringbox but different UI const auto wdgState = BeginWidget(WIDGETFLAG_SKIPACTIVE | WIDGETFLAG_MOUSE | WIDGETFLAG_NOHOTBORDER); @@ -1594,10 +1820,15 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, SwapFont(FONT_NTNORMAL); int iTabWide = 0; - if (piTabWide) + int iTabOffset = 0; + if (pState) { - if (*piTabWide <= 0) + const bool bThisYScroll = (c->ibfSectionHasYScroll & (1ULL << c->iSection)); + if (c->bFirstCtxUse + || pState->iWide <= 0 + || pState->bInYScroll != bThisYScroll) { + const int iTabWideEqual = (c->dPanel.wide / iLabelsSize); for (int i = 0; i < iLabelsSize; ++i) { int iCurTabWide, iTabTall; @@ -1605,39 +1836,65 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, iTabWide = Max(iCurTabWide, iTabWide); } iTabWide += 2 * c->iMarginX; - *piTabWide = iTabWide; + // Just go use equal tab spacing if the text + X-margins fits + iTabWide = Max(iTabWide, iTabWideEqual); + pState->iWide = iTabWide; + pState->iOffset = 0; + pState->bInYScroll = bThisYScroll; Assert(iTabWide > 0); } - iTabWide = *piTabWide; + iTabWide = pState->iWide; + iTabOffset = pState->iOffset; } else { iTabWide = (c->dPanel.wide / iLabelsSize); } + + const int iOverflowBtnX = (iTabWide > (c->dPanel.wide / iLabelsSize)) + ? c->layout.iRowTall : 0; const int iTotalTabsWidth = (iTabWide * iLabelsSize); - const int iCenterTabsXOffset = iTotalTabsWidth < c->irWidgetWide ? (c->irWidgetWide - iTotalTabsWidth) * 0.5 : 0; + const int iTabOffsetMax = iTotalTabsWidth - c->dPanel.wide + (iOverflowBtnX * 2); bool bResetSectionStates = false; - + bool bShiftOffset = false; + + const bool bHotLeft = wdgState.bHot + && IN_BETWEEN_EQ(c->rWidgetArea.x0, + c->iMouseAbsX, + c->rWidgetArea.x0 + iOverflowBtnX); + const bool bHotRight = wdgState.bHot + && IN_BETWEEN_EQ(c->rWidgetArea.x1 - iOverflowBtnX, + c->iMouseAbsX, + c->rWidgetArea.x1); + const int iMWheelJump = iTabWide * 0.2; switch (c->eMode) { case MODE_PAINT: { - vgui::surface()->SetFullscreenViewport(c->rWidgetArea.x0, c->rWidgetArea.y0, c->irWidgetWide, c->irWidgetTall); + const int iXViewportStart = c->rWidgetArea.x0 + iOverflowBtnX; + const int iXViewportWide = c->irWidgetWide - (iOverflowBtnX * 2); + vgui::surface()->SetFullscreenViewport( + iXViewportStart, + c->rWidgetArea.y0, + iXViewportWide, + c->irWidgetTall); + const int iXViewportEnd = iXViewportStart + iXViewportWide; vgui::surface()->PushFullscreenViewport(); const auto *pFontI = &c->fonts[c->eFont]; for (int i = 0, iXPosTab = 0; i < iLabelsSize; ++i, iXPosTab += iTabWide) { vgui::IntRect tabRect = { - .x0 = c->iXOffset[c->iSection] + iCenterTabsXOffset + iXPosTab, + .x0 = iTabOffset + iXPosTab, .y0 = 0, - .x1 = c->iXOffset[c->iSection] + iCenterTabsXOffset + iXPosTab + iTabWide, + .x1 = iTabOffset + iXPosTab + iTabWide, .y1 = c->irWidgetTall, }; - const bool bHotTab = IN_BETWEEN_EQ(tabRect.x0, c->iMouseAbsX - c->rWidgetArea.x0, tabRect.x1) && - IN_BETWEEN_EQ(tabRect.y0, c->iMouseAbsY - c->rWidgetArea.y0, tabRect.y1); + const bool bHotTab = IN_BETWEEN_EQ(iXViewportStart, c->iMouseAbsX, iXViewportEnd) + && IN_BETWEEN_EQ(tabRect.x0 + iXViewportStart, c->iMouseAbsX, tabRect.x1 + iXViewportStart) + && IN_BETWEEN_EQ(tabRect.y0 + c->rWidgetArea.y0, c->iMouseAbsY, tabRect.y1 + c->rWidgetArea.y0); const bool bActiveTab = i == *iIndex; if (bHotTab || bActiveTab) { @@ -1647,40 +1904,56 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, const wchar_t *wszText = wszLabelsList[i]; int wide, tall; vgui::surface()->GetTextSize(c->fonts[c->eFont].hdl, wszText, wide, tall); - vgui::surface()->DrawSetTextPos(c->iXOffset[c->iSection] + iCenterTabsXOffset + iXPosTab + ((iTabWide - wide) * 0.5), pFontI->iYOffset); + vgui::surface()->DrawSetTextPos(iTabOffset + iXPosTab + ((iTabWide - wide) * 0.5), pFontI->iYFontOffset); vgui::surface()->DrawSetTextColor(bActiveTab ? c->colors.activeFg : bHotTab ? c->colors.hotFg : c->colors.normalFg); vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); if (bHotTab) { - const int iHotTabMargin = static_cast(FL_BORDER_RATIO * c->iMarginY); - vgui::surface()->DrawSetColor(c->colors.hotBorder); - DrawBorder(tabRect, iHotTabMargin); + DrawBorder(tabRect); } } - if (c->iXOffset[c->iSection] != 0) - { - // more tab content to the left - vgui::surface()->DrawSetColor(COLOR_WHITE); - vgui::surface()->DrawFilledRect(0, 0, 2, c->irWidgetTall); - } - const int iTabsWiderBy = c->dPanel.wide - iTotalTabsWidth; - if (iTabsWiderBy < 0 && c->iXOffset[c->iSection] != iTabsWiderBy) - { - // more tab content to the right - vgui::surface()->DrawSetColor(COLOR_WHITE); - vgui::surface()->DrawFilledRect(c->irWidgetWide, 0, c->irWidgetWide-2, c->irWidgetTall); - } - vgui::surface()->PopFullscreenViewport(); vgui::surface()->SetFullscreenViewport(0, 0, 0, 0); + if (iOverflowBtnX > 0) + { + const bool bIndicateLeft = iTabOffset < 0; + const bool bIndicateRight = iTabOffset > -iTabOffsetMax; + + // Show left button + vgui::surface()->DrawSetColor(bHotLeft ? c->colors.activeBg : c->colors.normalBg); + vgui::surface()->DrawSetTextColor(bHotLeft ? c->colors.activeFg : c->colors.normalFg); + vgui::surface()->DrawFilledRect( + c->rWidgetArea.x0, + c->rWidgetArea.y0, + c->rWidgetArea.x0 + iOverflowBtnX, + c->rWidgetArea.y1); + + vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + pFontI->iStartBtnXPos, + c->rWidgetArea.y0 + pFontI->iStartBtnYPos); + vgui::surface()->DrawPrintText(bIndicateLeft ? WSZ_ARROW_LEFT : L"|", 1); + + // Show right button + vgui::surface()->DrawSetColor(bHotRight ? c->colors.activeBg : c->colors.normalBg); + vgui::surface()->DrawSetTextColor(bHotRight ? c->colors.activeFg : c->colors.normalFg); + vgui::surface()->DrawFilledRect( + c->rWidgetArea.x1 - iOverflowBtnX, + c->rWidgetArea.y0, + c->rWidgetArea.x1, + c->rWidgetArea.y1); + + vgui::surface()->DrawSetTextPos(c->rWidgetArea.x1 - iOverflowBtnX + pFontI->iStartBtnXPos, + c->rWidgetArea.y0 + pFontI->iStartBtnYPos); + vgui::surface()->DrawPrintText(bIndicateRight ? WSZ_ARROW_RIGHT : L"|", 1); + } + if (!(flags & TABFLAG_NOSIDEKEYS)) { // Draw the side-hints text // NEO NOTE (nullsystem): F# as 1 is thinner than 3/not monospaced font int iFontWidth, iFontHeight; vgui::surface()->GetTextSize(c->fonts[c->eFont].hdl, L"F #", iFontWidth, iFontHeight); - const int iHintYPos = c->rWidgetArea.y0 + pFontI->iYOffset; + const int iHintYPos = c->rWidgetArea.y0 + pFontI->iYFontOffset; vgui::surface()->DrawSetTextColor(c->colors.tabHintsFg); vgui::surface()->DrawSetTextPos(c->dPanel.x - c->iMarginX - iFontWidth, iHintYPos); @@ -1691,14 +1964,25 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, } break; case MODE_MOUSEPRESSED: + case MODE_MOUSEDOUBLEPRESSED: { - if (wdgState.bHot && c->eCode == MOUSE_LEFT && c->iMouseAbsX >= (c->rWidgetArea.x0 + iCenterTabsXOffset) && c->iMouseAbsX <= c->rWidgetArea.x0 + iCenterTabsXOffset + (iTabWide * iLabelsSize)) + if (wdgState.bHot && c->eCode == MOUSE_LEFT) { - const int iNextIndex = ((-c->iXOffset[c->iSection] + c->iMouseAbsX - c->rWidgetArea.x0 - iCenterTabsXOffset) / iTabWide); - if (iNextIndex != *iIndex) + if (bHotLeft || bHotRight) { - *iIndex = clamp(iNextIndex, 0, iLabelsSize - 1); - bResetSectionStates = true; + *iIndex = clamp(*iIndex + ((bHotLeft) ? -1 : +1), 0, iLabelsSize - 1); + bShiftOffset = true; + } + else if (c->iMouseAbsX >= (c->rWidgetArea.x0 + iOverflowBtnX) + && c->iMouseAbsX <= c->rWidgetArea.x0 + iOverflowBtnX + (iTabWide * iLabelsSize)) + { + const int iNextIndex = ((-iTabOffset + c->iMouseAbsX - c->rWidgetArea.x0 - iOverflowBtnX) / iTabWide); + if (iNextIndex != *iIndex) + { + *iIndex = clamp(iNextIndex, 0, iLabelsSize - 1); + bResetSectionStates = true; + bShiftOffset = true; + } } } } @@ -1715,40 +1999,50 @@ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, { *iIndex += (bLeftKey) ? -1 : +1; *iIndex = LoopAroundInArray(*iIndex, iLabelsSize); - const int distanceToStartOfButton = *iIndex * iTabWide; - if (-c->iXOffset[c->iSection] > distanceToStartOfButton) - { - c->iXOffset[c->iSection] = -distanceToStartOfButton; - } - const int distanceToEndOfButton = (*iIndex + 1) * iTabWide; - if (-c->iXOffset[c->iSection] + c->irWidgetWide < distanceToEndOfButton) - { - c->iXOffset[c->iSection] = c->irWidgetWide - distanceToEndOfButton; - } + bShiftOffset = true; bResetSectionStates = true; } } } break; case MODE_MOUSEWHEELED: - if (!(c->bMouseInPanel)) + if (wdgState.bHot && iOverflowBtnX > 0) { - break; + iTabOffset += (c->eCode == MOUSE_WHEEL_UP) ? iMWheelJump : -iMWheelJump; + iTabOffset = clamp(iTabOffset, -iTabOffsetMax, 0); + c->bBlockSectionMWheel = true; } - c->iXOffset[c->iSection] += (c->eCode == MOUSE_WHEEL_UP) ? iMWheelJump : -iMWheelJump; // NEO TODO (Adam) customizable horizontal scroll direction? - c->iXOffset[c->iSection] = clamp(c->iXOffset[c->iSection], -(iTotalTabsWidth - c->dPanel.wide), 0); break; default: break; } + if (bShiftOffset) + { + const int distanceToStartOfButton = *iIndex * iTabWide; + if (-iTabOffset > distanceToStartOfButton) + { + iTabOffset = -distanceToStartOfButton; + } + const int distanceToEndOfButton = (*iIndex + 1) * iTabWide; + if (-iTabOffset + c->irWidgetWide < distanceToEndOfButton) + { + iTabOffset = c->irWidgetWide - distanceToEndOfButton - (iOverflowBtnX * 2); + } + } if (bResetSectionStates && !(flags & TABFLAG_NOSTATERESETS)) { V_memset(c->iYOffset, 0, sizeof(c->iYOffset)); + V_memset(c->iXOffset, 0, sizeof(c->iXOffset)); c->iActive = FOCUSOFF_NUM; c->iActiveSection = -1; c->iHot = FOCUSOFF_NUM; c->iHotSection = -1; + iTabOffset = 0; + } + if (pState) + { + pState->iOffset = iTabOffset; } EndWidget(wdgState); @@ -1826,15 +2120,15 @@ void Slider(const wchar_t *wszLeftLabel, float *flValue, const float flMin, cons vgui::surface()->DrawFilledRect(c->rWidgetArea.x0, c->rWidgetArea.y0, c->rWidgetArea.x0 + c->layout.iRowTall, c->rWidgetArea.y1); vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + pFontI->iStartBtnXPos, - c->rWidgetArea.y0 + pFontI->iStartBtnYPos); - vgui::surface()->DrawPrintText(L"<", 1); + c->rWidgetArea.y0 + pFontI->iStartBtnYPos); + vgui::surface()->DrawPrintText(WSZ_ARROW_LEFT, 1); // Right-side ">" next button vgui::surface()->DrawFilledRect(c->rWidgetArea.x1 - c->layout.iRowTall, c->rWidgetArea.y0, c->rWidgetArea.x1, c->rWidgetArea.y1); vgui::surface()->DrawSetTextPos(c->rWidgetArea.x1 - c->layout.iRowTall + pFontI->iStartBtnXPos, - c->rWidgetArea.y0 + pFontI->iStartBtnYPos); - vgui::surface()->DrawPrintText(L">", 1); + c->rWidgetArea.y0 + pFontI->iStartBtnYPos); + vgui::surface()->DrawPrintText(WSZ_ARROW_RIGHT, 1); // Center-text text dimensions const bool bSpecial = (wszSpecialText && !wdgState.bActive && *flValue == 0.0f); @@ -1868,7 +2162,7 @@ void Slider(const wchar_t *wszLeftLabel, float *flValue, const float flMin, cons // Center-text text render vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + iFontStartX, - c->rWidgetArea.y0 + pFontI->iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset); vgui::surface()->DrawPrintText(bSpecial ? wszSpecialText : pSInfo->wszText, bSpecial ? V_wcslen(wszSpecialText) : V_wcslen(pSInfo->wszText)); @@ -1880,9 +2174,9 @@ void Slider(const wchar_t *wszLeftLabel, float *flValue, const float flMin, cons const int iMarkX = iFontStartX + iFontWide; vgui::surface()->DrawSetColor(c->colors.cursor); vgui::surface()->DrawFilledRect(c->rWidgetArea.x0 + iMarkX, - c->rWidgetArea.y0 + pFontI->iYOffset, + c->rWidgetArea.y0 + pFontI->iYFontOffset, c->rWidgetArea.x0 + iMarkX + c->iMarginX, - c->rWidgetArea.y0 + pFontI->iYOffset + iFontTall); + c->rWidgetArea.y0 + pFontI->iYFontOffset + iFontTall); } } } @@ -2245,7 +2539,7 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f int iFontWide, iFontTall; vgui::surface()->GetTextSize(pFontI->hdl, wszDbgText, iFontWide, iFontTall); vgui::surface()->DrawSetTextPos(c->rWidgetArea.x1 - c->iMarginX - iFontWide, - c->rWidgetArea.y0 + pFontI->iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset); vgui::surface()->DrawPrintText(wszDbgText, V_wcslen(wszDbgText)); vgui::surface()->DrawSetTextColor(c->colors.normalFg); #endif // defined(DEBUG) && DEBUG_NEOUI @@ -2293,14 +2587,14 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f bIsCursor ? c->colors.cursor : c->colors.textSelectionBg); vgui::surface()->DrawFilledRect( c->rWidgetArea.x0 + c->iMarginX + iXStart, - c->rWidgetArea.y0 + ((bIsCursor) ? pFontI->iYOffset : 0), + c->rWidgetArea.y0 + ((bIsCursor) ? pFontI->iYFontOffset : 0), c->rWidgetArea.x0 + c->iMarginX + iXEnd, - (bIsCursor) ? c->rWidgetArea.y0 + pFontI->iYOffset + vgui::surface()->GetFontTall(pFontI->hdl) : c->rWidgetArea.y1); + (bIsCursor) ? c->rWidgetArea.y0 + pFontI->iYFontOffset + vgui::surface()->GetFontTall(pFontI->hdl) : c->rWidgetArea.y1); vgui::surface()->DrawSetColor(c->colors.normalBg); } } vgui::surface()->DrawSetTextPos(c->rWidgetArea.x0 + c->iMarginX, - c->rWidgetArea.y0 + pFontI->iYOffset); + c->rWidgetArea.y0 + pFontI->iYFontOffset); const wchar_t *pwszPrintText = (bIsPassword) ? staticWszPasswordChars : wszText; vgui::surface()->DrawPrintText(pwszPrintText, iWszTextSize); } @@ -2613,6 +2907,405 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f EndWidget(wdgState); } +TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iColsTotal, + int *piColsWide, int *piSortIndex, bool *pbSortDescending, + const int iRelSyncSectionXOffset) +{ + TableHeaderModFlags modFlags = 0; + + const auto wdgState = BeginWidget(WIDGETFLAG_SKIPACTIVE | WIDGETFLAG_MOUSE | WIDGETFLAG_NOHOTBORDER); + const int iDragWide = 5; // TODO size by resolution + + // Sanity check if iLabelsSize dynamically changes + *piSortIndex = clamp(*piSortIndex, 0, iColsTotal - 1); + + if (wdgState.bInView) + { + switch (c->eMode) + { + case MODE_PAINT: + { + bool bHotDrag = false; + int prevX1 = c->rWidgetArea.x0 - c->iXOffset[c->iSection + iRelSyncSectionXOffset]; + const auto *pFontI = &c->fonts[c->eFont]; + for (int i = 0; i < iColsTotal; ++i) + { + if (piColsWide[i] <= 0) + { + continue; + } + + vgui::IntRect headerCellRect = { + .x0 = prevX1, + .y0 = c->rWidgetArea.y0, + .x1 = prevX1 + piColsWide[i], + .y1 = c->rWidgetArea.y0 + c->irWidgetTall, + }; + prevX1 = headerCellRect.x1; + + // Header cell background section + const bool bHotCell = + IN_BETWEEN_EQ(headerCellRect.x0, c->iMouseAbsX, headerCellRect.x1 - iDragWide) + && IN_BETWEEN_EQ(headerCellRect.y0, c->iMouseAbsY, headerCellRect.y1); + const bool bSortCell = (i == *piSortIndex); + + // Drag area section + bHotDrag = bHotDrag || + ( IN_BETWEEN_EQ(headerCellRect.x1 - iDragWide, c->iMouseAbsX, headerCellRect.x1) + && IN_BETWEEN_EQ(headerCellRect.y0, c->iMouseAbsY, headerCellRect.y1)); + + const int iLDiff = Max(c->rWidgetArea.x0 - headerCellRect.x0, 0); + const int iRDiff = Max(headerCellRect.x1 - c->rWidgetArea.x1, 0); + const int iXTotalWide = headerCellRect.x1 - headerCellRect.x0; + const int iXWide = Max(iXTotalWide - iLDiff - iRDiff, 0); + if (iXWide > 0) + { + vgui::surface()->SetFullscreenViewport( + Max(headerCellRect.x0, c->rWidgetArea.x0), + headerCellRect.y0, + iXWide, + c->irWidgetTall); + vgui::surface()->PushFullscreenViewport(); + + // Rendering + if (bHotCell || bSortCell) + { + vgui::surface()->DrawSetColor(bSortCell ? c->colors.activeBg : c->colors.hotBg); + vgui::surface()->DrawFilledRect(0, 0, iXWide - ((iRDiff > 0) ? 0 : iDragWide), c->irWidgetTall); + } + + vgui::surface()->DrawSetColor((c->iInDrag == (i + 1)) + ? c->colors.headerDragActiveBg + : c->colors.headerDragNormalBg); + vgui::surface()->DrawFilledRect(iXTotalWide - iLDiff - iDragWide, 0, iXTotalWide - iLDiff, c->irWidgetTall); + + // Text section + const wchar_t *wszText = wszColNamesList[i]; + vgui::surface()->DrawSetTextPos(c->iMarginX - iLDiff, pFontI->iYFontOffset); + vgui::surface()->DrawSetTextColor(bSortCell ? c->colors.activeFg : bHotCell ? c->colors.hotFg : c->colors.normalFg); + vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + if (bSortCell) + { + const auto *pFontI = &c->fonts[c->eFont]; + vgui::surface()->DrawSetColor(c->colors.tableHeaderSortIndicatorBg); + + const int iSortIndX = iXWide - c->layout.iRowTall + pFontI->iStartBtnXPos; + const int iSortIndY = pFontI->iStartBtnYPos; + vgui::surface()->DrawSetTextPos(iSortIndX, iSortIndY); + + vgui::surface()->DrawPrintText((*pbSortDescending) ? WSZ_ARROW_UP : WSZ_ARROW_DOWN, 1); + } + if (bHotCell) + { + DrawBorder(vgui::IntRect{0, 0, iXWide - ((iRDiff > 0) ? 0 : iDragWide), c->irWidgetTall}); + } + + vgui::surface()->PopFullscreenViewport(); + vgui::surface()->SetFullscreenViewport(0, 0, 0, 0); + } + } + vgui::surface()->SetCursor((bHotDrag || c->iInDrag > 0) + ? vgui::dc_sizewe + : vgui::dc_arrow); + } + break; + case MODE_MOUSEPRESSED: + case MODE_MOUSEDOUBLEPRESSED: + { + if (wdgState.bHot && c->eCode == MOUSE_LEFT) + { + int cellX0 = 0; + int cellX1 = c->rWidgetArea.x0 - c->iXOffset[c->iSection + iRelSyncSectionXOffset]; + for (int i = 0; i < iColsTotal; ++i) + { + if (piColsWide[i] <= 0) + { + continue; + } + + // y-axis already covered by wdgState.bHot, just check x-axis + cellX0 = cellX1; + cellX1 = cellX0 + piColsWide[i]; + + const bool bMouseInCell = IN_BETWEEN_EQ(cellX0, c->iMouseAbsX, cellX1); + if (bMouseInCell) + { + const bool bMouseInDrag = IN_BETWEEN_EQ(cellX1 - iDragWide, c->iMouseAbsX, cellX1); + if (bMouseInDrag) + { + c->iInDrag = i + 1; + } + else + { + const bool bSortCell = (i == *piSortIndex); + if (bSortCell) + { + *pbSortDescending = !*pbSortDescending; + modFlags |= TABLEHEADERMODFLAG_DESCENDINGCHANGED; + } + else + { + *piSortIndex = i; + modFlags |= TABLEHEADERMODFLAG_INDEXCHANGED; + } + c->bValueEdited = true; + } + break; + } + } + } + } + break; + case MODE_MOUSERELEASED: + { + c->iInDrag = 0; + } + break; + case MODE_MOUSEMOVED: + { + if (c->iInDrag > 0) + { + const int iDragIdx = c->iInDrag - 1; + int iAccWide = 0; + for (int i = 0; i < iColsTotal; ++i) + { + if (piColsWide[i] <= 0) + { + continue; + } + + iAccWide += piColsWide[i]; + if (iDragIdx == i) + { + // Alter by the difference in drag + const int iAbsColX1 = c->rWidgetArea.x0 + iAccWide - + c->iXOffset[c->iSection + iRelSyncSectionXOffset]; + const int iDiff = c->iMouseAbsX - iAbsColX1; + piColsWide[i] = Max(piColsWide[i] + iDiff, 0); + iAccWide += iDiff; + + // If dragging the end, expand/retract offset + // This is assuming c->dPanel.wide on next section equals to this + if (iDragIdx == (iColsTotal - 1)) + { + c->iXOffset[c->iSection + iRelSyncSectionXOffset] = Max(iAccWide - c->dPanel.wide, 0); + } + break; + } + } + } + } + break; + default: + break; + } + } + + EndWidget(wdgState); + return modFlags; +} + +void BeginTable(const int *piColsWide, const int iLabelsSize) +{ + // Bump y-axis with previous row layout before applying table layout + if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) + { + c->iLayoutX = -c->iXOffset[c->iSection]; + c->iLayoutY += c->layout.iRowTall; + c->irWidgetLayoutY = c->iLayoutY; + } + + Assert(0 == c->layout.iTableLabelsSize); + Assert(nullptr == c->layout.piTableColsWide); + + Assert(iLabelsSize > 0); + Assert(piColsWide); + + c->layout.iTableLabelsSize = iLabelsSize; + c->layout.piTableColsWide = piColsWide; + c->curRowFlags = NEXTTABLEROWFLAG_NONE; + c->iLastTableRowWidget = 0; + + c->iIdxRowParts = -1; +} + +NeoUI::RetButton EndTable() +{ + RetButton ret = {}; + + if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) + { + c->iLayoutX = -c->iXOffset[c->iSection]; + c->iLayoutY += c->layout.iRowTall; + c->irWidgetLayoutY = c->iLayoutY; + } + + if ((c->curRowFlags & NEXTTABLEROWFLAG__HASROWSELECTABLES) + && (false == (c->curRowFlags & NEXTTABLEROWFLAG__HASROWSELECTED)) + && (c->eMode == MODE_KEYPRESSED)) + { + ret.bKeyUpPressed = IsKeyUpWidget(); + ret.bKeyDownPressed = IsKeyDownWidget(); + if (ret.bKeyUpPressed || ret.bKeyDownPressed) + { + c->iActiveSection = c->iSection; + c->iActive = ret.bKeyUpPressed ? c->iLastTableRowWidget : 0; + } + } + + c->layout.iTableLabelsSize = 0; + c->layout.piTableColsWide = nullptr; + c->curRowFlags = NEXTTABLEROWFLAG_NONE; + + c->iIdxRowParts = -1; + return ret; +} + +NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) +{ + Assert(c->layout.iTableLabelsSize > 0 && c->layout.piTableColsWide); + + if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) + { + c->iLayoutX = -c->iXOffset[c->iSection]; + c->iLayoutY += c->layout.iRowTall; + c->irWidgetLayoutY = c->iLayoutY; + } + + RetButton ret = {}; + c->curRowFlags = ((flags & NEXTTABLEROWFLAG__EXTERNAL) | (c->curRowFlags & NEXTTABLEROWFLAG__PERSISTS)); + + if (flags & NEXTTABLEROWFLAG_SELECTABLE) + { + vgui::IntRect rRowArea = { + .x0 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection], + .y0 = c->dPanel.y + c->iLayoutY, + .x1 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection] + c->dPanel.wide, + .y1 = c->dPanel.y + c->iLayoutY + c->layout.iRowTall, + }; + + // Not a full Begin/EndWidget, but just fills in wdgInfos mainly + // for key-pressed up/down Y-axis slider adjustments + c->irWidgetTall = c->layout.iRowTall; + c->wdgInfos[c->iWidget].iYOffsets = c->iLayoutY + c->iYOffset[c->iSection]; + c->wdgInfos[c->iWidget].iYTall = c->irWidgetTall; + c->wdgInfos[c->iWidget].bCannotActive = false; + + bool bMouseIn = false; + const bool bFullyVisible = (rRowArea.y0 >= c->dPanel.y) && (rRowArea.y1 <= (c->dPanel.y + c->dPanel.tall)); + if (bFullyVisible) + { + bMouseIn = IN_BETWEEN_EQ(rRowArea.x0, c->iMouseAbsX, rRowArea.x1 - 1) + && IN_BETWEEN_EQ(rRowArea.y0, c->iMouseAbsY, rRowArea.y1 - 1); + if (bMouseIn) + { + c->curRowFlags |= NEXTTABLEROWFLAG__HOT; + } + } + ret.bMouseHover = bMouseIn; + + switch (c->eMode) + { + case MODE_PAINT: + { + if (bFullyVisible) + { + Color color; + if (c->curRowFlags & NEXTTABLEROWFLAG_SELECTED) + { + color = c->colors.activeBg; + } + else if (c->curRowFlags & NEXTTABLEROWFLAG__HOT) + { + color = c->colors.hotBg; + } + + if (color.a() > 0) + { + vgui::surface()->DrawSetColor(color); + vgui::surface()->DrawFilledRectArray(&rRowArea, 1); + } + + if (c->curRowFlags & NEXTTABLEROWFLAG__HOT) + { + DrawBorder(rRowArea); + } + } + } + break; + case MODE_MOUSEPRESSED: + { + ret.bMousePressed = ret.bPressed = (ret.bMouseHover && c->eCode == MOUSE_LEFT); + ret.bMouseRightPressed = (ret.bMouseHover && c->eCode == MOUSE_RIGHT); + } + break; + case MODE_MOUSEDOUBLEPRESSED: + { + ret.bMouseDoublePressed = ret.bPressed = (ret.bMouseHover && c->eCode == MOUSE_LEFT); + } + break; + case MODE_KEYPRESSED: + { + if (c->curRowFlags & NEXTTABLEROWFLAG_SELECTED) + { + ret.bKeyEnterPressed = ret.bPressed = IsKeyEnter(); + if (false == (c->curRowFlags & NEXTTABLEROWFLAG__UPDOWNPROCESSED)) + { + ret.bKeyUpPressed = IsKeyUpWidget(); + ret.bKeyDownPressed = IsKeyDownWidget(); + } + if (ret.bKeyUpPressed || ret.bKeyDownPressed) + { + c->curRowFlags |= NEXTTABLEROWFLAG__UPDOWNPROCESSED; + c->iActiveSection = c->iSection; + c->iActive = c->iWidget; + } + } + } + break; + default: + break; + } + + if (ret.bPressed) + { + c->iActive = c->iWidget; + c->iActiveSection = c->iSection; + } + + if (c->curRowFlags & NEXTTABLEROWFLAG_SELECTED) + { + c->curRowFlags |= NEXTTABLEROWFLAG__HASROWSELECTED; + } + c->curRowFlags |= NEXTTABLEROWFLAG__HASROWSELECTABLES; + c->iLastTableRowWidget = c->iWidget; + + ++c->iWidget; + } + + return ret; +} + +void PadTableXScroll(const int *piColsWide, const int iLabelsSize) +{ + int iTotalX = 0; + for (int i = 0; i < iLabelsSize; ++i) + { + iTotalX += piColsWide[i]; + } + c->irWidgetMaxX = Max(c->irWidgetMaxX, iTotalX - c->iXOffset[c->iSection]); +} + +void BeginIgnoreXOffset() +{ + c->bIgnoreXOffset = true; +} + +void EndIgnoreXOffset() +{ + c->bIgnoreXOffset = false; +} + bool Bind(const ButtonCode_t eCode) { return c->eMode == MODE_KEYPRESSED && c->eCode == eCode; diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index b7f3d00d0..d6d5acb54 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -107,7 +107,7 @@ struct Dim struct FontInfo { vgui::HFont hdl; - int iYOffset; + int iYFontOffset; int iStartBtnXPos; int iStartBtnYPos; }; @@ -138,6 +138,7 @@ enum EBaseButtonType { BASEBUTTONTYPE_TEXT = 0, BASEBUTTONTYPE_IMAGE, + BASEBUTTONTYPE_CHECKBOX, }; struct SliderInfo @@ -150,6 +151,7 @@ struct SliderInfo struct DynWidgetInfos { + int iXOffsets; int iYOffsets; int iYTall; bool bCannotActive; @@ -218,6 +220,32 @@ enum PopupFlag_ }; typedef int PopupFlags; +enum NextTableRowFlag_ +{ + // Public flags, used as NextTableRow options + NEXTTABLEROWFLAG_NONE = 0, + NEXTTABLEROWFLAG_SELECTABLE = 1 << 0, // Mark this row as selectable + NEXTTABLEROWFLAG_SELECTED = 1 << 1, // Mark as selected (only if selectable) + + // Internal only flags + NEXTTABLEROWFLAG__EXTERNAL = ((1 << 8) - 1), // Mask of all external/options flags below start of internal + NEXTTABLEROWFLAG__HOT = 1 << 8, // Mouse hovering the row + NEXTTABLEROWFLAG__UPDOWNPROCESSED = 1 << 9, // Up/down button already been processed + NEXTTABLEROWFLAG__HASROWSELECTED = 1 << 10, // There's a row selection in this table + NEXTTABLEROWFLAG__HASROWSELECTABLES = 1 << 11, // Any row in this table can be selected + + // Flags that'll persists when going to the next row + NEXTTABLEROWFLAG__PERSISTS = NEXTTABLEROWFLAG__UPDOWNPROCESSED | NEXTTABLEROWFLAG__HASROWSELECTED | NEXTTABLEROWFLAG__HASROWSELECTABLES, +}; +typedef int NextTableRowFlags; + +enum EXYMouseDragOffset +{ + XYMOUSEDRAGOFFSET_NIL = 0, + XYMOUSEDRAGOFFSET_YAXIS, + XYMOUSEDRAGOFFSET_XAXIS, +}; + struct Colors { Color normalFg; @@ -241,8 +269,38 @@ struct Colors Color sliderHotBg; Color sliderActiveBg; Color tabHintsFg; + Color tableHeaderSortIndicatorBg; + Color headerDragNormalBg; + Color headerDragActiveBg; +}; + +// NEO NOTE (nullsystem): +// If iRowParts = nullptr, iRowPartsTotal will be used of an equal proportions split +struct Layout +{ + // iRowParts: int percentage of partitioning + // EX: { 60, 20, -1 } -> 60%, 20%, 20% | 3 widgets per row + + // Main layout + int iRowPartsTotal; + const int *iRowParts; + int iRowTall; + int iDefRowTall; + + // Vertical partitioning + int iVertPartsTotal; + const int *iVertParts; + + // Table-only layout + int iTableLabelsSize; + const int *piTableColsWide; // This is in pixels unlike iRowParts }; +// NEO TODO (nullsystem): Re-arrange variables by levels of variable +// lifecycles from "permanent" -> multi-pass context -> single context pass -> section pass +// And should make it easier to just zero them in one go for each level +// where appropriate. Also turn more variables to zero-initialization +// by default. struct Context { Mode eMode; @@ -262,18 +320,16 @@ struct Context bool bMouseInPanel; int iHasMouseInPanel; - // NEO NOTE (nullsystem): - // If iRowParts = nullptr, iRowPartsTotal will be used of an equal proportions split - struct Layout - { - int iRowPartsTotal; - const int *iRowParts; - int iRowTall; - int iDefRowTall; - - int iVertPartsTotal; - const int *iVertParts; - }; + // If a widget captures mouse wheel, don't let + // section deal with it afterward + bool bBlockSectionMWheel; + + // This run is a context reset + bool bFirstCtxUse; + + // Ignore X offset when putting in widget + bool bIgnoreXOffset; + Layout layout; // Themes @@ -289,7 +345,9 @@ struct Context vgui::IntRect rWidgetArea; int irWidgetWide; int irWidgetTall; + int irWidgetLayoutX; int irWidgetLayoutY; + int irWidgetMaxX; // Layout management // "Static" sizing @@ -305,7 +363,7 @@ struct Context int iVertLayoutY; int iYOffset[MAX_SECTIONS] = {}; int iXOffset[MAX_SECTIONS] = {}; - bool abYMouseDragOffset[MAX_SECTIONS] = {}; + EXYMouseDragOffset aeXYMouseDragOffset[MAX_SECTIONS] = {}; int iStartMouseDragOffset[MAX_SECTIONS] = {}; // Saved infos for EndSection managing scrolling @@ -323,7 +381,8 @@ struct Context // Caches/read-ahead this section has scroll is // known in previous frame(s) - uint64_t ibfSectionHasScroll = 0; + uint64_t ibfSectionHasXScroll = 0; + uint64_t ibfSectionHasYScroll = 0; int iHot; int iHotSection; @@ -376,16 +435,26 @@ struct Context // Sound paths const char *pszSoundBtnPressed = "ui/buttonclickrelease.wav"; const char *pszSoundBtnRollover = "ui/buttonrollover.wav"; + + // Table + int iInDrag = 0; + NextTableRowFlags curRowFlags; + int iLastTableRowWidget = 0; }; struct RetButton { + // Button + table row bool bPressed; - bool bKeyPressed; + bool bKeyEnterPressed; bool bMousePressed; bool bMouseHover; bool bMouseDoublePressed; bool bMouseRightPressed; + + // Table row only + bool bKeyUpPressed; // bPressed not set if this set + bool bKeyDownPressed; // bPressed not set if this set }; struct LabelExOpt @@ -454,7 +523,8 @@ void EndOverrideFgColor(); /*1W*/ void Label(const wchar_t *wszText, const LabelExOpt &opt); /*2W*/ void Label(const wchar_t *wszLabel, const wchar_t *wszText); -// If piTabWide == nullptr, fills the whole layout width, otherwise only length of the tabs. +// If pState == nullptr, fills the whole layout width, otherwise, +// will automatically choose either whole layout width or only length of the tabs. // Initialize the variable to -1 and NeoUI::Tabs will auto-figure it out. enum TabsFlag_ { @@ -463,12 +533,21 @@ enum TabsFlag_ TABFLAG_NOSTATERESETS = 1 << 1, // Don't reset scroll/hot/active states }; typedef int TabsFlags; +struct TabsState +{ + int iWide; + int iOffset; + bool bInYScroll; +}; /*1W*/ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, - const TabsFlags flags = TABFLAG_DEFAULT, int *piTabWide = nullptr); -/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const EBaseButtonType eType); + const TabsFlags flags = TABFLAG_DEFAULT, + TabsState *pState = nullptr); +/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, + const EBaseButtonType eType, const bool bVal = false); /*1W*/ RetButton Button(const wchar_t *wszText); /*2W*/ RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText); /*1W*/ RetButton ButtonTexture(const char *szTexturePath); +/*1W*/ RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal); /*1W*/ void RingBoxBool(bool *bChecked); /*2W*/ void RingBoxBool(const wchar_t *wszLeftLabel, bool *bChecked); /*1W*/ void RingBox(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex); @@ -495,6 +574,42 @@ typedef int TextEditFlags; /*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); /*SW*/ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg = L"", const char *szTextureGroup = ""); +// Table widgets + functionalities +// NEO TODO (nullsystem): iColProportions non-const, resizable within TableHeader +enum TableHeaderModFlag_ +{ + TABLEHEADERMODFLAG_NONE = 0, + TABLEHEADERMODFLAG_INDEXCHANGED = 1 << 0, + TABLEHEADERMODFLAG_DESCENDINGCHANGED = 1 << 1, +}; +typedef int TableHeaderModFlags; +// Use like: modFlags |= NeoUI::TableHeader(...) to detect sort modifications and deal with it +// at a later tick point. +// +// iRelSyncSectionXOffset = relative position to this header, sync to the +// X-offset of the next/prev section +/*SW*/ [[nodiscard]] TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iColsTotal, + int *piColsWide, int *piSortIndex, bool *pbSortDescending, + const int iRelSyncSectionXOffset = 0); +// NEO TODO (nullsystem): A mode for the table where it'll show y-axis scrollbar but only requires +// and returns filter view of loop only necessary that's in-view +// NEO TODO (nullsystem): Instead of iRelSyncSectionXOffset, have it as part of an option for each +// section to sync their scrollbar with each other? + +// piColsWide - X px size wide of each columns, if negative it's a hidden column +void BeginTable(const int *piColsWide, const int iLabelsSize); +// EndTable's RetButton mainly to catch Up/Down on a selectable table without anything selected +// From the given up/down action can initialize the row index there +RetButton EndTable(); +RetButton NextTableRow(const NextTableRowFlags flags = NEXTTABLEROWFLAG_NONE); +// If not going to really show a table and instead some message on why it's empty +// but still want the X-scroll to appear and usable +void PadTableXScroll(const int *piColsWide, const int iLabelsSize); + +// Ignore X-offset when putting in widget +void BeginIgnoreXOffset(); +void EndIgnoreXOffset(); + // NeoUI::Texture is non-widget, but utilizes NeoUI's image/texture handling enum TextureOptFlags { diff --git a/src/game/client/neo/ui/neo_utils.cpp b/src/game/client/neo/ui/neo_utils.cpp index afa860145..c3c31b35f 100644 --- a/src/game/client/neo/ui/neo_utils.cpp +++ b/src/game/client/neo/ui/neo_utils.cpp @@ -252,7 +252,7 @@ void NeoUtils::SerializeVTFDXTSprayToBuffer(CUtlBuffer *buffer, const uint8 *dat void bpr( int level, CUtlBuffer& buf, char const *fmt, ... ) { - char txt[ 4096 ]; + char txt[ 4096 ] = {}; va_list argptr; va_start( argptr, fmt ); _vsnprintf( txt, sizeof( txt ) - 1, fmt, argptr );