diff --git a/src/game/client/neo/ui/neo_root.cpp b/src/game/client/neo/ui/neo_root.cpp index ff9a1a5c0..8f1715649 100644 --- a/src/game/client/neo/ui/neo_root.cpp +++ b/src/game/client/neo/ui/neo_root.cpp @@ -839,6 +839,13 @@ void CNeoRoot::OnMainLoop(const NeoUI::Mode eMode) NeoUI::EndContext(); + if (STATE_SETTINGS == m_state + && !m_ns.bModified + && g_uiCtx.bValueEdited) + { + m_ns.bModified = true; + } + // When the state changes, save some variables if (m_state != ePrevState) { @@ -1161,7 +1168,7 @@ void CNeoRoot::MainLoopRoot(const MainLoopParam param) g_uiCtx.dPanel.wide = flMP3Wide; g_uiCtx.dPanel.tall = param.tall; - NeoUI::BeginSection(); + NeoUI::BeginSection(NeoUI::SECTIONFLAG_DISABLEOFFSETS); NeoUI::SwapFont(NeoUI::FONT_NTNORMAL); g_uiCtx.eButtonTextStyle = NeoUI::TEXTSTYLE_LEFT; @@ -1413,11 +1420,6 @@ void CNeoRoot::MainLoopSettings(const MainLoopParam param) NeoUI::EndSection(); } - if (!m_ns.bModified && g_uiCtx.bValueEdited) - { - m_ns.bModified = true; - } - if (m_ns.bBack) { m_ns.bBack = false; @@ -1671,7 +1673,7 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::OpenPopup(NEOPOPUP_ACTIONBLACKLIST, NeoUI::Dim{ .x = g_uiCtx.iMouseAbsX, .y = g_uiCtx.iMouseAbsY, - .wide = NeoUI::PopupWideByStr("Remove from blacklist"), + .wide = NeoUI::SuitableWideByWStr(L"Remove from blacklist", NeoUI::SUITABLEWIDE_POPUP), .tall = g_uiCtx.layout.iDefRowTall, }); } @@ -1867,7 +1869,7 @@ void CNeoRoot::MainLoopServerBrowser(const MainLoopParam param) NeoUI::OpenPopup(NEOPOPUP_ACTIONSERVER, NeoUI::Dim{ .x = g_uiCtx.iMouseAbsX, .y = g_uiCtx.iMouseAbsY, - .wide = NeoUI::PopupWideByStr("Add to blacklist"), + .wide = NeoUI::SuitableWideByWStr(L"Add to blacklist", NeoUI::SUITABLEWIDE_POPUP), .tall = g_uiCtx.layout.iDefRowTall * 2, }); diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index a06b4baf0..d2a233f02 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -294,6 +294,7 @@ void BeginContext(NeoUI::Context *pNextCtx, const NeoUI::Mode eMode, const wchar c->eLabelTextStyle = TEXTSTYLE_LEFT; c->ibfSectionCanActive = 0; c->ibfSectionCanController = 0; + c->popupFlags &= ~(POPUPFLAG__CTXDONEPOPUP); // Different pointer, change context c->bFirstCtxUse = (c->pSzCurCtxName != pSzCtxName); if (c->bFirstCtxUse) @@ -498,17 +499,20 @@ void EndContext() void BeginSection(const ISectionFlags iSectionFlags) { - // Previous frame(s) known this section does scroll - if (c->ibfSectionHasYScroll & (1ULL << c->iSection)) + if (!(iSectionFlags & SECTIONFLAG_DISABLEOFFSETS)) { - // 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(); + // Previous frame(s) known this section does scroll + 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 = -c->iXOffset[c->iSection]; @@ -583,6 +587,13 @@ void EndSection() c->iActive = FOCUSOFF_NUM; c->iActiveSection = -1; } + const bool bOffsetDisabled = (c->iSectionFlags & SECTIONFLAG_DISABLEOFFSETS); + if (bOffsetDisabled) + { + c->iXOffset[c->iSection] = 0; + c->iYOffset[c->iSection] = 0; + } + 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; @@ -590,8 +601,8 @@ void EndSection() // Scroll handling const int iMWheelJump = c->layout.iDefRowTall; - const bool bHasXScroll = (iAbsLayoutX > c->dPanel.wide); - const bool bHasYScroll = (iAbsLayoutY > c->dPanel.tall); + const bool bHasXScroll = (false == bOffsetDisabled) && (iAbsLayoutX > c->dPanel.wide); + const bool bHasYScroll = (false == bOffsetDisabled) && (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)); @@ -836,16 +847,19 @@ void EndSection() } } - // 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 (bResetXScrollPanelWide) - { - c->dPanel.tall += iScrollThick; - } - if (bResetYScrollPanelTall) + if (false == bOffsetDisabled) { - c->dPanel.wide += iScrollThick; + // 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 (bResetXScrollPanelWide) + { + c->dPanel.tall += iScrollThick; + } + if (bResetYScrollPanelTall) + { + c->dPanel.wide += iScrollThick; + } } ++c->iSection; @@ -885,7 +899,7 @@ void ClosePopup() bool BeginPopup(const int iPopupId, const PopupFlags flags) { - if (iPopupId != c->iCurPopupId) + if (iPopupId != c->iCurPopupId || (c->popupFlags & POPUPFLAG__CTXDONEPOPUP)) { return false; } @@ -902,7 +916,7 @@ bool BeginPopup(const int iPopupId, const PopupFlags flags) } c->popupFlags &= ~(POPUPFLAG__EXTERNAL); - c->popupFlags |= (flags & POPUPFLAG__EXTERNAL); + c->popupFlags |= (flags & POPUPFLAG__EXTERNAL) | POPUPFLAG__CTXDONEPOPUP; V_memcpy(&c->dPanel, &dim, sizeof(Dim)); BeginSection(SECTIONFLAG_POPUP); @@ -920,21 +934,28 @@ int CurrentPopup() return c->iCurPopupId; } -static int BasePopupWideByStr(const int iSzSize) +int SuitableWideByWStr(const wchar_t *pwszStr, const ESuitableWide eWideType) { const auto *pFontI = &c->fonts[c->eFont]; - const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); - return (c->iMarginX * 2) + (iSzSize * iChWidth); -} - -int PopupWideByStr(const char *pszStr) -{ - return BasePopupWideByStr(V_strlen(pszStr)); -} - -int PopupWideByStr(const wchar_t *pwszStr) -{ - return BasePopupWideByStr(V_wcslen(pwszStr)); + switch (eWideType) + { + case SUITABLEWIDE_POPUP: + { + // Rough-estimate, suitable for popup menus + const int iWszSize = V_wcslen(pwszStr); + const int iChWidth = vgui::surface()->GetCharacterWidth(pFontI->hdl, 'A'); + return (c->iMarginX * 2) + (iWszSize * iChWidth); + } break; + case SUITABLEWIDE_TABLE: + { + // More precise wide, suitable for fixed-tables + [[maybe_unused]] int iWide = 0, iTall = 0; + vgui::surface()->GetTextSize(pFontI->hdl, pwszStr, iWide, iTall); + return (c->iMarginX * 2) + iWide; + } break; + } + Assert(false); // Should not be able to get here + return 0; } void SetPerRowLayout(const int iColTotal, const int *iColProportions, const int iRowTall) @@ -1445,7 +1466,9 @@ 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, const bool bVal, const ButtonFlags flags, const float flScrollStart) +NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, + const char *szTextureGroup, const EBaseButtonType eType, const bool bVal, + const ButtonFlags flags, const float flScrollStart) { const auto wdgState = BeginWidget(WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE); @@ -1532,8 +1555,28 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c { vgui::surface()->DrawFilledRect(c->rWidgetArea.x0, c->rWidgetArea.y0, c->rWidgetArea.x1, c->rWidgetArea.y1); - Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, - c->irWidgetWide, c->irWidgetTall); + if (wszText && wszText[0]) + { + // NEO TODO (nullsystem): Currently only top-center texture, + // bottom-center label style but if wanted can tweak texture-label styling + const int iTexYTall = c->irWidgetTall * 0.75f; + Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, + c->irWidgetWide, iTexYTall, + szTextureGroup); + + const auto *pFontI = &c->fonts[c->eFont]; + const int x = XPosFromText(wszText, pFontI, TEXTSTYLE_CENTER); + vgui::surface()->DrawSetTextPos( + c->rWidgetArea.x0 + x, + c->rWidgetArea.y0 + iTexYTall - pFontI->iYFontOffset); + vgui::surface()->DrawPrintText(wszText, V_wcslen(wszText)); + } + else + { + Texture(szTexturePath, c->rWidgetArea.x0, c->rWidgetArea.y0, + c->irWidgetWide, c->irWidgetTall, + szTextureGroup); + } } break; case BASEBUTTONTYPE_CHECKBOX: { @@ -1609,7 +1652,7 @@ NeoUI::RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, c NeoUI::RetButton Button(const wchar_t *wszText) { - return BaseButton(wszText, "", BASEBUTTONTYPE_TEXT); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_TEXT); } NeoUI::RetButton Button(const wchar_t *wszLeftLabel, const wchar_t *wszText) @@ -1651,7 +1694,22 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt { // General images decoded via stb_image int width, height, channels; - uint8 *data = stbi_load(szTexturePath, &width, &height, &channels, 0); + char szFullTexturePath[MAX_PATH] = {}; +#ifdef _WIN32 + if (V_isalpha(szTexturePath[0]) + && szTexturePath[1] == ':' + && (szTexturePath[2] == '\\' || szTexturePath[2] == '/')) +#else + if (szTexturePath[0] == '/') +#endif + { + V_strcpy_safe(szFullTexturePath, szTexturePath); + } + else + { + filesystem->RelativePathToFullPath_safe(szTexturePath, szTextureGroup, szFullTexturePath); + } + uint8 *data = stbi_load(szFullTexturePath, &width, &height, &channels, 0); if (data) { if (channels == 3) @@ -1775,19 +1833,20 @@ bool Texture(const char *szTexturePath, const int x, const int y, const int widt return false; } -NeoUI::RetButton ButtonTexture(const char *szTexturePath) +NeoUI::RetButton ButtonTexture(const char *szTexturePath, const char *szTextureGroup, + const wchar_t *wszText) { - return BaseButton(L"", szTexturePath, BASEBUTTONTYPE_IMAGE); + return BaseButton(wszText, szTexturePath, szTextureGroup, BASEBUTTONTYPE_IMAGE); } NeoUI::RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal) { - return BaseButton(wszText, "", BASEBUTTONTYPE_CHECKBOX, bVal); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_CHECKBOX, bVal); } NeoUI::RetButton ButtonToggle(const wchar_t *wszText, const bool bVal, const ButtonFlags flags, const float flScrollStart) { - return BaseButton(wszText, "", BASEBUTTONTYPE_TOGGLE, bVal, flags, flScrollStart); + return BaseButton(wszText, "", "", BASEBUTTONTYPE_TOGGLE, bVal, flags, flScrollStart); } void ResetTextures() @@ -2814,7 +2873,7 @@ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags f OpenPopup(INTERNALPOPUP_COPYMENU, Dim{ .x = c->iMouseAbsX, .y = c->iMouseAbsY, - .wide = PopupWideByStr("Paste"), + .wide = SuitableWideByWStr(L"Paste", SUITABLEWIDE_POPUP), .tall = c->layout.iDefRowTall * 3, }); c->eRightClickCopyMenuRet = COPYMENU_NIL; @@ -3268,8 +3327,8 @@ TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iCols OpenPopup(INTERNALPOPUP_TABLEHEADER, Dim{ .x = c->iMouseAbsX, .y = c->iMouseAbsY, - .wide = NeoUI::PopupWideByStr("__") // Offset by checkmark - + NeoUI::PopupWideByStr(wszColNamesList[iWidestIdx]), + .wide = NeoUI::SuitableWideByWStr(L"__", NeoUI::SUITABLEWIDE_POPUP) // Offset by checkmark + + NeoUI::SuitableWideByWStr(wszColNamesList[iWidestIdx], NeoUI::SUITABLEWIDE_POPUP), .tall = c->layout.iDefRowTall * iColsTotal, }); } @@ -3327,6 +3386,27 @@ TableHeaderModFlags TableHeader(const wchar_t **wszColNamesList, const int iCols return modFlags; } +// NEO NOTE (nullsystem): It's done like this so that the highlighter +// border goes over rather than under the content of the row +static void TableHighlightPrevRow() +{ + if (c->curRowFlags & NEXTTABLEROWFLAG__HOT && MODE_PAINT == c->eMode) + { + vgui::IntRect rRowArea = { + .x0 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection], + .y0 = c->dPanel.y + c->iLayoutY - c->layout.iRowTall, + .x1 = c->dPanel.x + c->iLayoutX + c->iXOffset[c->iSection] + c->dPanel.wide, + .y1 = c->dPanel.y + c->iLayoutY, + }; + const bool bFullyVisible = (rRowArea.y0 >= c->dPanel.y) + && (rRowArea.y1 <= (c->dPanel.y + c->dPanel.tall)); + if (bFullyVisible) + { + DrawBorder(rRowArea); + } + } +} + void BeginTable(const int *piColsWide, const int iLabelsSize) { // Bump y-axis with previous row layout before applying table layout @@ -3353,6 +3433,14 @@ void BeginTable(const int *piColsWide, const int iLabelsSize) NeoUI::RetButton EndTable() { + 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; + } + TableHighlightPrevRow(); + RetButton ret = {}; if (c->iWidget > 0 && c->iIdxRowParts > 0 && c->iIdxRowParts < c->layout.iRowPartsTotal) @@ -3393,6 +3481,7 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) c->iLayoutY += c->layout.iRowTall; c->irWidgetLayoutY = c->iLayoutY; } + TableHighlightPrevRow(); RetButton ret = {}; c->curRowFlags = ((flags & NEXTTABLEROWFLAG__EXTERNAL) | (c->curRowFlags & NEXTTABLEROWFLAG__PERSISTS)); @@ -3419,6 +3508,13 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) { bMouseIn = IN_BETWEEN_EQ(rRowArea.x0, c->iMouseAbsX, rRowArea.x1 - 1) && IN_BETWEEN_EQ(rRowArea.y0, c->iMouseAbsY, rRowArea.y1 - 1); + if (bMouseIn && (c->dimPopup.wide > 0 && c->dimPopup.tall > 0) && + !(c->popupFlags & POPUPFLAG__INPOPUPSECTION)) + { + const Dim &dim = c->dimPopup; + bMouseIn = !(IN_BETWEEN_EQ(dim.x, c->iMouseAbsX, dim.x + dim.wide) && + IN_BETWEEN_EQ(dim.y, c->iMouseAbsY, dim.y + dim.tall)); + } if (bMouseIn) { c->curRowFlags |= NEXTTABLEROWFLAG__HOT; @@ -3447,11 +3543,6 @@ NeoUI::RetButton NextTableRow(const NextTableRowFlags flags) vgui::surface()->DrawSetColor(color); vgui::surface()->DrawFilledRectArray(&rRowArea, 1); } - - if (c->curRowFlags & NEXTTABLEROWFLAG__HOT) - { - DrawBorder(rRowArea); - } } } break; diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index caf6188a6..2f25cf0eb 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -192,6 +192,8 @@ enum ESectionFlag SECTIONFLAG_POPUP = 1 << 4, // Don't restrict viewport to only label's area SECTIONFLAG_LABELPANELVIEWPORT = 1 << 5, + // Disable X and Y axis offsets + SECTIONFLAG_DISABLEOFFSETS = 1 << 6, }; typedef int ISectionFlags; @@ -221,6 +223,7 @@ enum PopupFlag_ POPUPFLAG__EXTERNAL = ((1 << 8) - 1), // Mask of all external/options flags below start of internal POPUPFLAG__INPOPUPSECTION = 1 << 8, // Inside a Begin/EndPopup section POPUPFLAG__NEWOPENPOPUP = 1 << 9, // The popup just initialized + POPUPFLAG__CTXDONEPOPUP = 1 << 10, // Popup have been shown in this context, it's so OpenPopup to another one won't just immediately close }; typedef int PopupFlags; @@ -517,8 +520,12 @@ void EndPopup(); [[nodiscard]] int CurrentPopup(); // Get a suitable wide size for a popup by the longest text in the popup -int PopupWideByStr(const char *pszStr); -int PopupWideByStr(const wchar_t *pwszStr); +enum ESuitableWide +{ + SUITABLEWIDE_POPUP = 0, + SUITABLEWIDE_TABLE, +}; +int SuitableWideByWStr(const wchar_t *pwszStr, const ESuitableWide eWideType); [[nodiscard]] CurrentWidgetState BeginWidget(const WidgetFlag eWidgetFlag = WIDGETFLAG_NONE); void EndWidget(const CurrentWidgetState &wdgState); @@ -562,11 +569,12 @@ struct TabsState /*1W*/ void Tabs(const wchar_t **wszLabelsList, const int iLabelsSize, int *iIndex, const TabsFlags flags = TABFLAG_DEFAULT, TabsState *pState = nullptr); -/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, +/*1W*/ RetButton BaseButton(const wchar_t *wszText, const char *szTexturePath, const char *szTextureGroup, const EBaseButtonType eType, const bool bVal = false, const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f); /*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 ButtonTexture(const char *szTexturePath, const char *szTextureGroup = "", + const wchar_t *wszText = L""); /*1W*/ RetButton ButtonCheckbox(const wchar_t *wszText, const bool bVal); /*1W*/ RetButton ButtonToggle(const wchar_t *wszText, const bool bVal, const ButtonFlags flags = BUTTONFLAG_NONE, const float flScrollStart = 0.0f); /*1W*/ void RingBoxFlag(const int iToggleFlag, int *iFlags, const wchar_t **wszLabelsCustomList = nullptr);