diff --git a/CMakeLists.txt b/CMakeLists.txt index b48b54a5e..003555ded 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1958,6 +1958,8 @@ if(CLIENT) components/spectator.h components/statboard.cpp components/statboard.h + components/tooltips.cpp + components/tooltips.h components/voting.cpp components/voting.h gameclient.cpp diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index bfad618b6..c75ac8b16 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -2832,64 +2832,4 @@ bool CMenus::HandleListInputs(const CUIRect &View, float &ScrollValue, const flo } return NewIndex != -1; -} - -void CMenus::DoToolTip(const void *pID, const CUIRect *pNearRect, const char *pText, float WidthHint) -{ - static int64_t HoverTime = -1; - - if(!UI()->MouseInside(pNearRect)) - { - if(pID == UI()->ActiveTooltipItem()) - HoverTime = -1; - return; - } - - UI()->SetActiveTooltipItem(pID); - - if(HoverTime == -1) - HoverTime = time_get(); - - // Delay tooltip until 1 second passed. - if(HoverTime > time_get() - time_freq()) - return; - - const float MARGIN = 5.0f; - - CUIRect Rect; - Rect.w = WidthHint; - if(WidthHint < 0.0f) - Rect.w = TextRender()->TextWidth(0, 14.0f, pText, -1, -1.0f) + 4.0f; - Rect.h = 30.0f; - - CUIRect *pScreen = UI()->Screen(); - - // Try the top side. - if(pNearRect->y - Rect.h - MARGIN > pScreen->y) - { - Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, MARGIN, pScreen->w - Rect.w - MARGIN); - Rect.y = pNearRect->y - Rect.h - MARGIN; - } - // Try the bottom side. - else if(pNearRect->y + pNearRect->h + MARGIN < pScreen->h) - { - Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, MARGIN, pScreen->w - Rect.w - MARGIN); - Rect.y = pNearRect->y + pNearRect->h + MARGIN; - } - // Try the right side. - else if(pNearRect->x + pNearRect->w + MARGIN + Rect.w < pScreen->w) - { - Rect.x = pNearRect->x + pNearRect->w + MARGIN; - Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, MARGIN, pScreen->h - Rect.h - MARGIN); - } - // Try the left side. - else if(pNearRect->x - Rect.w - MARGIN > pScreen->x) - { - Rect.x = pNearRect->x - Rect.w - MARGIN; - Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, MARGIN, pScreen->h - Rect.h - MARGIN); - } - - RenderTools()->DrawUIRect(&Rect, ColorRGBA(0.2, 0.2, 0.2, 0.80f), CUI::CORNER_ALL, 5.0f); - Rect.Margin(2.0f, &Rect); - UI()->DoLabel(&Rect, pText, 14.0f, TEXTALIGN_LEFT); -} +} \ No newline at end of file diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 20adbf3da..dc8bc048c 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -530,7 +530,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) { m_Dummy ^= 1; } - DoToolTip(&m_Dummy, &DummyLabel, Localize("Toggle to edit your dummy settings.")); + GameClient()->m_Tooltips.DoToolTip(&m_Dummy, &DummyLabel, Localize("Toggle to edit your dummy settings.")); Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); @@ -1265,7 +1265,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) MainView.HSplitTop(20.0f, &Button, &MainView); if(DoButton_CheckBox(&g_Config.m_GfxHighDetail, Localize("High Detail"), g_Config.m_GfxHighDetail, &Button)) g_Config.m_GfxHighDetail ^= 1; - DoToolTip(&g_Config.m_GfxHighDetail, &Button, Localize("Allows maps to render with more detail.")); + GameClient()->m_Tooltips.DoToolTip(&g_Config.m_GfxHighDetail, &Button, Localize("Allows maps to render with more detail.")); MainView.HSplitTop(20.0f, &Button, &MainView); if(DoButton_CheckBox(&g_Config.m_GfxHighdpi, Localize("Use high DPI"), g_Config.m_GfxHighdpi, &Button)) @@ -2635,6 +2635,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) { g_Config.m_ClRaceGhost ^= 1; } + GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClRaceGhost, &Button, Localize("When you cross the start line, show a ghost tee replicating the movements of your best time.")); if(g_Config.m_ClRaceGhost) { @@ -2686,13 +2687,15 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) Button.VSplitMid(&LeftLeft, &Button); Button.VSplitLeft(50.0f, &Label, &Button); - UI()->DoLabelScaled(&Label, Localize("Alpha"), 14.0f, TEXTALIGN_LEFT); + UI()->DoLabelScaled(&Label, Localize("Opacity"), 14.0f, TEXTALIGN_LEFT); g_Config.m_ClShowOthersAlpha = (int)(UIEx()->DoScrollbarH(&g_Config.m_ClShowOthersAlpha, &Button, g_Config.m_ClShowOthersAlpha / 100.0f) * 100.0f); if(DoButton_CheckBox(&g_Config.m_ClShowOthers, Localize("Show others"), g_Config.m_ClShowOthers == SHOW_OTHERS_ON, &LeftLeft)) { g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF; } + + GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClShowOthersAlpha, &Button, "Adjust the opacity of entities belonging to other teams, such as tees and nameplates"); } Left.HSplitTop(20.0f, &Button, &Left); @@ -2707,6 +2710,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) { g_Config.m_ClShowQuads ^= 1; } + GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClShowQuads, &Button, Localize("Quads are used for background decoration")); Right.HSplitTop(20.0f, &Label, &Right); Label.VSplitLeft(130.0f, &Label, &Button); @@ -2720,6 +2724,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) { g_Config.m_ClAntiPing ^= 1; } + GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClAntiPing, &Button, Localize("Tries to predict other entities to give a feel of low latency.")); if(g_Config.m_ClAntiPing) { diff --git a/src/game/client/components/tooltips.cpp b/src/game/client/components/tooltips.cpp new file mode 100644 index 000000000..430078248 --- /dev/null +++ b/src/game/client/components/tooltips.cpp @@ -0,0 +1,117 @@ +#include "tooltips.h" + +#include + +CTooltips::CTooltips() +{ + OnReset(); +} + +void CTooltips::OnReset() +{ + HoverTime = -1; + m_Tooltips.clear(); +} + +void CTooltips::SetActiveTooltip(CTooltip &Tooltip) +{ + if(m_ActiveTooltip.has_value()) + return; + + m_ActiveTooltip.emplace(Tooltip); + HoverTime = time_get(); +} + +inline void CTooltips::ClearActiveTooltip() +{ + m_ActiveTooltip.reset(); +} + +void CTooltips::DoToolTip(const void *pID, const CUIRect *pNearRect, const char *pText, float WidthHint) +{ + uintptr_t ID = reinterpret_cast(pID); + + const auto &it = m_Tooltips.find(ID); + + if(it == m_Tooltips.end()) + { + CTooltip NewTooltip = { + .m_Rect = *pNearRect, + .m_pText = pText, + .m_WidthHint = WidthHint, + }; + + m_Tooltips[ID] = NewTooltip; + + CTooltip &Tooltip = m_Tooltips[ID]; + + if(UI()->MouseInside(&Tooltip.m_Rect)) + { + SetActiveTooltip(Tooltip); + } + } + else + { + if(UI()->MouseInside(&it->second.m_Rect)) + { + SetActiveTooltip(it->second); + } + } +} + +void CTooltips::OnRender() +{ + if(m_ActiveTooltip.has_value()) + { + CTooltip &Tooltip = m_ActiveTooltip.value(); + + if(!UI()->MouseInside(&Tooltip.m_Rect)) + { + ClearActiveTooltip(); + return; + } + + // Delay tooltip until 1 second passed. + if(HoverTime > time_get() - time_freq()) + return; + + const float MARGIN = 5.0f; + + CUIRect Rect; + Rect.w = Tooltip.m_WidthHint; + if(Tooltip.m_WidthHint < 0.0f) + Rect.w = TextRender()->TextWidth(0, 14.0f, Tooltip.m_pText, -1, -1.0f) + 4.0f; + Rect.h = 30.0f; + + CUIRect *pScreen = UI()->Screen(); + + // Try the top side. + if(Tooltip.m_Rect.y - Rect.h - MARGIN > pScreen->y) + { + Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, MARGIN, pScreen->w - Rect.w - MARGIN); + Rect.y = Tooltip.m_Rect.y - Rect.h - MARGIN; + } + // Try the bottom side. + else if(Tooltip.m_Rect.y + Tooltip.m_Rect.h + MARGIN < pScreen->h) + { + Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, MARGIN, pScreen->w - Rect.w - MARGIN); + Rect.y = Tooltip.m_Rect.y + Tooltip.m_Rect.h + MARGIN; + } + // Try the right side. + else if(Tooltip.m_Rect.x + Tooltip.m_Rect.w + MARGIN + Rect.w < pScreen->w) + { + Rect.x = Tooltip.m_Rect.x + Tooltip.m_Rect.w + MARGIN; + Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, MARGIN, pScreen->h - Rect.h - MARGIN); + } + // Try the left side. + else if(Tooltip.m_Rect.x - Rect.w - MARGIN > pScreen->x) + { + Rect.x = Tooltip.m_Rect.x - Rect.w - MARGIN; + Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, MARGIN, pScreen->h - Rect.h - MARGIN); + } + + RenderTools()->DrawUIRect(&Rect, ColorRGBA(0.2, 0.2, 0.2, 0.80f), CUI::CORNER_ALL, 5.0f); + Rect.Margin(2.0f, &Rect); + UI()->DoLabel(&Rect, Tooltip.m_pText, 14.0f, TEXTALIGN_LEFT); + } +} diff --git a/src/game/client/components/tooltips.h b/src/game/client/components/tooltips.h new file mode 100644 index 000000000..806268860 --- /dev/null +++ b/src/game/client/components/tooltips.h @@ -0,0 +1,59 @@ +#ifndef GAME_CLIENT_COMPONENTS_TOOLTIPS_H +#define GAME_CLIENT_COMPONENTS_TOOLTIPS_H + +#include +#include +#include + +#include +#include + +struct CTooltip +{ + CUIRect m_Rect; + const char *m_pText; + float m_WidthHint; +}; + +/** + * A component that manages and renders UI tooltips. + * + * Should be among the last components to render. + */ +class CTooltips : public CComponent +{ + std::unordered_map m_Tooltips; + std::optional> m_ActiveTooltip; + int64_t HoverTime; + + /** + * The passed tooltip is only actually set if there is no currently active tooltip. + * + * @param Tooltip A reference to the tooltip that should be active. + */ + void SetActiveTooltip(CTooltip &Tooltip); + + inline void ClearActiveTooltip(); + +public: + CTooltips(); + virtual int Sizeof() const override { return sizeof(*this); } + + /** + * Adds the tooltip to a cache and renders it when active. + * + * On the first call to this function, the data passed is cached, afterwards the calls are used to detect if the tooltip should be activated. + * + * For now only works correctly with single line tooltips, since Text width calculation gets broken when there are multiple lines. + * + * @param pID The ID of the tooltip. Usually a reference to some g_Config value. + * @param pNearTo Place the tooltip near this rect. + * @param pText The text to display in the tooltip + */ + void DoToolTip(const void *pID, const CUIRect *pNearRect, const char *pText, float WidthHint = -1.0f); + + virtual void OnReset() override; + virtual void OnRender() override; +}; + +#endif \ No newline at end of file diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 2fac3ddca..29924fa1f 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -137,6 +137,7 @@ void CGameClient::OnConsoleInit() m_All.Add(&m_Statboard); m_All.Add(&m_Motd); m_All.Add(&m_Menus); + m_All.Add(&m_Tooltips); m_All.Add(&CMenus::m_Binder); m_All.Add(&m_GameConsole); diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index a76d49274..758b5c20f 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -54,6 +54,7 @@ #include "components/spectator.h" #include "components/statboard.h" #include "components/voting.h" +#include "components/tooltips.h" class CGameInfo { @@ -144,6 +145,8 @@ public: CRaceDemo m_RaceDemo; CGhost m_Ghost; + CTooltips m_Tooltips; + private: class CStack {