1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "menus.h"
4
5#include <base/log.h>
6#include <base/time.h>
7
8#include <engine/engine.h>
9#include <engine/favorites.h>
10#include <engine/font_icons.h>
11#include <engine/friends.h>
12#include <engine/gfx/image_manipulation.h>
13#include <engine/keys.h>
14#include <engine/serverbrowser.h>
15#include <engine/shared/config.h>
16#include <engine/shared/localization.h>
17#include <engine/textrender.h>
18
19#include <game/client/animstate.h>
20#include <game/client/components/countryflags.h>
21#include <game/client/gameclient.h>
22#include <game/client/ui.h>
23#include <game/client/ui_listbox.h>
24#include <game/localization.h>
25
26static constexpr ColorRGBA HIGHLIGHTED_TEXT_COLOR = ColorRGBA(0.4f, 0.4f, 1.0f, 1.0f);
27
28static ColorRGBA PlayerBackgroundColor(bool Friend, bool Clan, bool Afk, bool InSelectedServer, bool Inside)
29{
30 static const ColorRGBA COLORS[] = {ColorRGBA(0.5f, 1.0f, 0.5f), ColorRGBA(0.4f, 0.4f, 1.0f), ColorRGBA(0.75f, 0.75f, 0.75f)};
31 static const ColorRGBA COLORS_AFK[] = {ColorRGBA(1.0f, 1.0f, 0.5f), ColorRGBA(0.4f, 0.75f, 1.0f), ColorRGBA(0.6f, 0.6f, 0.6f)};
32 int i;
33 if(Friend)
34 i = 0;
35 else if(Clan)
36 i = 1;
37 else
38 i = 2;
39 return (Afk ? COLORS_AFK[i] : COLORS[i]).WithAlpha(alpha: 0.3f + (Inside ? 0.15f : 0.0f) + (InSelectedServer ? 0.12f : 0.0f));
40}
41
42template<size_t N>
43static void FormatServerbrowserPing(char (&aBuffer)[N], const CServerInfo *pInfo)
44{
45 if(!pInfo->m_LatencyIsEstimated)
46 {
47 str_format(aBuffer, sizeof(aBuffer), "%d", pInfo->m_Latency);
48 return;
49 }
50 static const char *const LOCATION_NAMES[CServerInfo::NUM_LOCS] = {
51 "", // LOC_UNKNOWN
52 Localizable(pStr: "AFR"), // LOC_AFRICA
53 Localizable(pStr: "ASI"), // LOC_ASIA
54 Localizable(pStr: "AUS"), // LOC_AUSTRALIA
55 Localizable(pStr: "EUR"), // LOC_EUROPE
56 Localizable(pStr: "NA"), // LOC_NORTH_AMERICA
57 Localizable(pStr: "SA"), // LOC_SOUTH_AMERICA
58 Localizable(pStr: "CHN"), // LOC_CHINA
59 };
60 dbg_assert(0 <= pInfo->m_Location && pInfo->m_Location < CServerInfo::NUM_LOCS, "location out of range");
61 str_copy(aBuffer, Localize(pStr: LOCATION_NAMES[pInfo->m_Location]));
62}
63
64static ColorRGBA GetPingTextColor(int Latency)
65{
66 return color_cast<ColorRGBA>(hsl: ColorHSLA((300.0f - std::clamp(val: Latency, lo: 0, hi: 300)) / 1000.0f, 1.0f, 0.5f));
67}
68
69static ColorRGBA GetGametypeTextColor(const char *pGametype)
70{
71 ColorHSLA HslaColor;
72 if(str_comp(a: pGametype, b: "DM") == 0 || str_comp(a: pGametype, b: "TDM") == 0 || str_comp(a: pGametype, b: "CTF") == 0 || str_comp(a: pGametype, b: "LMS") == 0 || str_comp(a: pGametype, b: "LTS") == 0)
73 HslaColor = ColorHSLA(0.33f, 1.0f, 0.75f);
74 else if(str_find_nocase(haystack: pGametype, needle: "catch"))
75 HslaColor = ColorHSLA(0.17f, 1.0f, 0.75f);
76 else if(str_find_nocase(haystack: pGametype, needle: "dm") || str_find_nocase(haystack: pGametype, needle: "tdm") || str_find_nocase(haystack: pGametype, needle: "ctf") || str_find_nocase(haystack: pGametype, needle: "lms") || str_find_nocase(haystack: pGametype, needle: "lts"))
77 {
78 if(pGametype[0] == 'i' || pGametype[0] == 'g')
79 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
80 else
81 HslaColor = ColorHSLA(0.40f, 1.0f, 0.75f);
82 }
83 else if(str_find_nocase(haystack: pGametype, needle: "f-ddrace") || str_find_nocase(haystack: pGametype, needle: "freeze"))
84 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
85 else if(str_find_nocase(haystack: pGametype, needle: "fng"))
86 HslaColor = ColorHSLA(0.83f, 1.0f, 0.75f);
87 else if(str_find_nocase(haystack: pGametype, needle: "gores"))
88 HslaColor = ColorHSLA(0.525f, 1.0f, 0.75f);
89 else if(str_find_nocase(haystack: pGametype, needle: "BW"))
90 HslaColor = ColorHSLA(0.05f, 1.0f, 0.75f);
91 else if(str_find_nocase(haystack: pGametype, needle: "ddracenet") || str_find_nocase(haystack: pGametype, needle: "ddnet") || str_find_nocase(haystack: pGametype, needle: "0xf"))
92 HslaColor = ColorHSLA(0.58f, 1.0f, 0.75f);
93 else if(str_find_nocase(haystack: pGametype, needle: "ddrace") || str_find_nocase(haystack: pGametype, needle: "mkrace"))
94 HslaColor = ColorHSLA(0.75f, 1.0f, 0.75f);
95 else if(str_find_nocase(haystack: pGametype, needle: "race") || str_find_nocase(haystack: pGametype, needle: "fastcap"))
96 HslaColor = ColorHSLA(0.46f, 1.0f, 0.75f);
97 else if(str_find_nocase(haystack: pGametype, needle: "s-ddr"))
98 HslaColor = ColorHSLA(1.0f, 1.0f, 0.7f);
99 else
100 HslaColor = ColorHSLA(1.0f, 1.0f, 1.0f);
101 return color_cast<ColorRGBA>(hsl: HslaColor);
102}
103
104void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemActivated)
105{
106 static CListBox s_ListBox;
107
108 CUIRect Headers;
109 View.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &View);
110 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
111 Headers.VSplitRight(Cut: s_ListBox.ScrollbarWidthMax(), pLeft: &Headers, pRight: nullptr);
112 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
113
114 struct SColumn
115 {
116 int m_Id;
117 int m_Sort;
118 const char *m_pCaption;
119 int m_Direction;
120 float m_Width;
121 CUIRect m_Rect;
122 };
123
124 enum
125 {
126 COL_FLAG_LOCK = 0,
127 COL_FLAG_FAV,
128 COL_COMMUNITY,
129 COL_NAME,
130 COL_GAMETYPE,
131 COL_MAP,
132 COL_FRIENDS,
133 COL_PLAYERS,
134 COL_PING,
135 };
136
137 enum
138 {
139 UI_ELEM_LOCK_ICON = 0,
140 UI_ELEM_FAVORITE_ICON,
141 UI_ELEM_NAME_1,
142 UI_ELEM_NAME_2,
143 UI_ELEM_NAME_3,
144 UI_ELEM_GAMETYPE,
145 UI_ELEM_MAP_1,
146 UI_ELEM_MAP_2,
147 UI_ELEM_MAP_3,
148 UI_ELEM_FINISH_ICON,
149 UI_ELEM_PLAYERS,
150 UI_ELEM_FRIEND_ICON,
151 UI_ELEM_PING,
152 UI_ELEM_KEY_ICON,
153 NUM_UI_ELEMS,
154 };
155
156 static SColumn s_aCols[] = {
157 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
158 {.m_Id: COL_FLAG_LOCK, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 14.0f, .m_Rect: {.x: 0}},
159 {.m_Id: COL_FLAG_FAV, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 14.0f, .m_Rect: {.x: 0}},
160 {.m_Id: COL_COMMUNITY, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 28.0f, .m_Rect: {.x: 0}},
161 {.m_Id: COL_NAME, .m_Sort: IServerBrowser::SORT_NAME, .m_pCaption: Localizable(pStr: "Name"), .m_Direction: 0, .m_Width: 50.0f, .m_Rect: {.x: 0}},
162 {.m_Id: COL_GAMETYPE, .m_Sort: IServerBrowser::SORT_GAMETYPE, .m_pCaption: Localizable(pStr: "Type"), .m_Direction: 1, .m_Width: 50.0f, .m_Rect: {.x: 0}},
163 {.m_Id: COL_MAP, .m_Sort: IServerBrowser::SORT_MAP, .m_pCaption: Localizable(pStr: "Map"), .m_Direction: 1, .m_Width: 120.0f + (Headers.w - 480) / 8, .m_Rect: {.x: 0}},
164 {.m_Id: COL_FRIENDS, .m_Sort: IServerBrowser::SORT_NUMFRIENDS, .m_pCaption: "", .m_Direction: 1, .m_Width: 20.0f, .m_Rect: {.x: 0}},
165 {.m_Id: COL_PLAYERS, .m_Sort: IServerBrowser::SORT_NUMPLAYERS, .m_pCaption: Localizable(pStr: "Players"), .m_Direction: 1, .m_Width: 60.0f, .m_Rect: {.x: 0}},
166 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_Width: 4.0f, .m_Rect: {.x: 0}},
167 {.m_Id: COL_PING, .m_Sort: IServerBrowser::SORT_PING, .m_pCaption: Localizable(pStr: "Ping"), .m_Direction: 1, .m_Width: 40.0f, .m_Rect: {.x: 0}},
168 };
169
170 const int NumCols = std::size(s_aCols);
171
172 // do layout
173 for(int i = 0; i < NumCols; i++)
174 {
175 if(s_aCols[i].m_Direction == -1)
176 {
177 Headers.VSplitLeft(Cut: s_aCols[i].m_Width, pLeft: &s_aCols[i].m_Rect, pRight: &Headers);
178
179 if(i + 1 < NumCols)
180 {
181 Headers.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Headers);
182 }
183 }
184 }
185
186 for(int i = NumCols - 1; i >= 0; i--)
187 {
188 if(s_aCols[i].m_Direction == 1)
189 {
190 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
191 Headers.VSplitRight(Cut: 2.0f, pLeft: &Headers, pRight: nullptr);
192 }
193 }
194
195 for(auto &Col : s_aCols)
196 {
197 if(Col.m_Direction == 0)
198 Col.m_Rect = Headers;
199 }
200
201 const bool PlayersOrPing = (g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS || g_Config.m_BrSort == IServerBrowser::SORT_PING);
202
203 // do headers
204 for(const auto &Col : s_aCols)
205 {
206 int Checked = g_Config.m_BrSort == Col.m_Sort;
207 if(PlayersOrPing && g_Config.m_BrSortOrder == 2 && (Col.m_Sort == IServerBrowser::SORT_NUMPLAYERS || Col.m_Sort == IServerBrowser::SORT_PING))
208 Checked = 2;
209
210 if(DoButton_GridHeader(pId: &Col.m_Id, pText: Localize(pStr: Col.m_pCaption), Checked, pRect: &Col.m_Rect))
211 {
212 if(Col.m_Sort != -1)
213 {
214 if(g_Config.m_BrSort == Col.m_Sort)
215 g_Config.m_BrSortOrder = (g_Config.m_BrSortOrder + 1) % (PlayersOrPing ? 3 : 2);
216 else
217 g_Config.m_BrSortOrder = 0;
218 g_Config.m_BrSort = Col.m_Sort;
219 }
220 }
221
222 if(Col.m_Id == COL_FRIENDS)
223 {
224 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
225 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
226 Ui()->DoLabel(pRect: &Col.m_Rect, pText: FontIcon::HEART, Size: 14.0f, Align: TEXTALIGN_MC);
227 TextRender()->SetRenderFlags(0);
228 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
229 }
230 }
231
232 const int NumServers = ServerBrowser()->NumSortedServers();
233
234 // display important messages in the middle of the screen so no
235 // users misses it
236 {
237 if(!ServerBrowser()->NumServers() && ServerBrowser()->IsGettingServerlist())
238 {
239 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "Getting server list from master server"), Size: 16.0f, Align: TEXTALIGN_MC);
240 }
241 else if(!ServerBrowser()->NumServers())
242 {
243 if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
244 {
245 CUIRect Label, Button;
246 View.HMargin(Cut: (View.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &Label);
247 Label.HSplitTop(Cut: 16.0f, pTop: &Label, pBottom: &Button);
248 Button.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &Button);
249 Button.VMargin(Cut: (Button.w - 320.0f) / 2.0f, pOtherRect: &Button);
250 char aBuf[128];
251 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "No local servers found (ports %d-%d)"), IServerBrowser::LAN_PORT_BEGIN, IServerBrowser::LAN_PORT_END);
252 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 16.0f, Align: TEXTALIGN_MC);
253 static CButtonContainer s_StartLocalServerButton;
254 if(DoButton_Menu(pButtonContainer: &s_StartLocalServerButton, pText: Localize(pStr: "Start and connect to local server"), Checked: 0, pRect: &Button))
255 {
256 if(GameClient()->m_LocalServer.IsServerRunning())
257 {
258 RefreshBrowserTab(Force: true);
259 Connect(pAddress: "localhost");
260 }
261 else if(GameClient()->m_LocalServer.RunServer(vpArguments: {}))
262 {
263 Connect(pAddress: "localhost");
264 }
265 }
266 }
267 else if(ServerBrowser()->IsServerlistError())
268 {
269 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "Could not get server list from master server"), Size: 16.0f, Align: TEXTALIGN_MC);
270 }
271 else
272 {
273 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "No servers found"), Size: 16.0f, Align: TEXTALIGN_MC);
274 }
275 }
276 else if(ServerBrowser()->NumServers() && !NumServers)
277 {
278 CUIRect Label, ResetButton;
279 View.HMargin(Cut: (View.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &Label);
280 Label.HSplitTop(Cut: 16.0f, pTop: &Label, pBottom: &ResetButton);
281 ResetButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ResetButton);
282 ResetButton.VMargin(Cut: (ResetButton.w - 200.0f) / 2.0f, pOtherRect: &ResetButton);
283 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "No servers match your filter criteria"), Size: 16.0f, Align: TEXTALIGN_MC);
284 static CButtonContainer s_ResetButton;
285 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
286 {
287 ResetServerbrowserFilters();
288 }
289 }
290 }
291
292 s_ListBox.SetActive(!Ui()->IsPopupOpen());
293 s_ListBox.DoStart(RowHeight: ms_ListheaderHeight, NumItems: NumServers, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false);
294
295 if(m_ServerBrowserShouldRevealSelection)
296 {
297 s_ListBox.ScrollToSelected();
298 m_ServerBrowserShouldRevealSelection = false;
299 }
300 m_SelectedIndex = -1;
301
302 const auto &&RenderBrowserIcons = [this](CUIElement::SUIElementRect &UIRect, CUIRect *pRect, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor, const char *pText, int TextAlign, bool SmallFont = false) {
303 const float FontSize = SmallFont ? 6.0f : 14.0f;
304 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
305 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
306 TextRender()->TextColor(Color: TextColor);
307 TextRender()->TextOutlineColor(Color: TextOutlineColor);
308 Ui()->DoLabelStreamed(RectEl&: UIRect, pRect, pText, Size: FontSize, Align: TextAlign);
309 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
310 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
311 TextRender()->SetRenderFlags(0);
312 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
313 };
314
315 std::vector<CUIElement *> &vpServerBrowserUiElements = m_avpServerBrowserUiElements[ServerBrowser()->GetCurrentType()];
316 if(vpServerBrowserUiElements.size() < (size_t)NumServers)
317 vpServerBrowserUiElements.resize(new_size: NumServers, x: nullptr);
318
319 for(int i = 0; i < NumServers; i++)
320 {
321 const CServerInfo *pItem = ServerBrowser()->SortedGet(Index: i);
322 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: pItem->m_aCommunityId);
323
324 if(vpServerBrowserUiElements[i] == nullptr)
325 {
326 vpServerBrowserUiElements[i] = Ui()->GetNewUIElement(RequestedRectCount: NUM_UI_ELEMS);
327 }
328 CUIElement *pUiElement = vpServerBrowserUiElements[i];
329
330 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: str_comp(a: pItem->m_aAddress, b: g_Config.m_UiServerAddress) == 0);
331 if(ListItem.m_Selected)
332 m_SelectedIndex = i;
333
334 if(!ListItem.m_Visible)
335 {
336 // reset active item, if not visible
337 if(Ui()->CheckActiveItem(pId: pItem))
338 Ui()->SetActiveItem(nullptr);
339
340 // don't render invisible items
341 continue;
342 }
343
344 const float FontSize = 12.0f;
345 char aTemp[64];
346 for(const auto &Col : s_aCols)
347 {
348 CUIRect Button;
349 Button.x = Col.m_Rect.x;
350 Button.y = ListItem.m_Rect.y;
351 Button.h = ListItem.m_Rect.h;
352 Button.w = Col.m_Rect.w;
353
354 const int Id = Col.m_Id;
355 if(Id == COL_FLAG_LOCK)
356 {
357 if(pItem->m_Flags & SERVER_FLAG_PASSWORD)
358 {
359 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_LOCK_ICON), &Button, ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f), TextRender()->DefaultTextOutlineColor(), FontIcon::LOCK, TEXTALIGN_MC);
360 }
361 else if(pItem->m_RequiresLogin)
362 {
363 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_KEY_ICON), &Button, ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f), TextRender()->DefaultTextOutlineColor(), FontIcon::KEY, TEXTALIGN_MC);
364 }
365 }
366 else if(Id == COL_FLAG_FAV)
367 {
368 if(pItem->m_Favorite != TRISTATE::NONE)
369 {
370 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FAVORITE_ICON), &Button, ColorRGBA(1.0f, 0.85f, 0.3f, 1.0f), TextRender()->DefaultTextOutlineColor(), FontIcon::STAR, TEXTALIGN_MC);
371 }
372 }
373 else if(Id == COL_COMMUNITY)
374 {
375 if(pCommunity != nullptr)
376 {
377 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
378 if(pIcon != nullptr)
379 {
380 CUIRect CommunityIcon;
381 Button.Margin(Cut: 2.0f, pOtherRect: &CommunityIcon);
382 m_CommunityIcons.Render(pIcon, Rect: CommunityIcon, Active: true);
383 Ui()->DoButtonLogic(pId: &pItem->m_aCommunityId, Checked: 0, pRect: &CommunityIcon, Flags: BUTTONFLAG_NONE);
384 GameClient()->m_Tooltips.DoToolTip(pId: &pItem->m_aCommunityId, pNearRect: &CommunityIcon, pText: pCommunity->Name());
385 }
386 }
387 }
388 else if(Id == COL_NAME)
389 {
390 SLabelProperties Props;
391 Props.m_MaxWidth = Button.w;
392 Props.m_StopAtEnd = true;
393 Props.m_EnableWidthCheck = false;
394 bool Printed = false;
395 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_SERVERNAME))
396 Printed = PrintHighlighted(pName: pItem->m_aName, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
397 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_1), pRect: &Button, pText: pItem->m_aName, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: (int)(pFilteredStr - pItem->m_aName));
398 TextRender()->TextColor(Color: HIGHLIGHTED_TEXT_COLOR);
399 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_2), pRect: &Button, pText: pFilteredStr, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: FilterLen, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_NAME_1)->m_Cursor);
400 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
401 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_3), pRect: &Button, pText: pFilteredStr + FilterLen, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: -1, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_NAME_2)->m_Cursor);
402 });
403 if(!Printed)
404 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_1), pRect: &Button, pText: pItem->m_aName, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
405 }
406 else if(Id == COL_GAMETYPE)
407 {
408 SLabelProperties Props;
409 Props.m_MaxWidth = Button.w;
410 Props.m_StopAtEnd = true;
411 Props.m_EnableWidthCheck = false;
412 if(g_Config.m_UiColorizeGametype)
413 {
414 TextRender()->TextColor(Color: GetGametypeTextColor(pGametype: pItem->m_aGameType));
415 }
416 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_GAMETYPE), pRect: &Button, pText: pItem->m_aGameType, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
417 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
418 }
419 else if(Id == COL_MAP)
420 {
421 {
422 CUIRect Icon;
423 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
424 Button.VSplitLeft(Cut: Button.h, pLeft: &Icon, pRight: &Button);
425 if(g_Config.m_BrIndicateFinished && pItem->m_HasRank == CServerInfo::RANK_RANKED)
426 {
427 Icon.Margin(Cut: 2.0f, pOtherRect: &Icon);
428 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FINISH_ICON), &Icon, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), FontIcon::FLAG_CHECKERED, TEXTALIGN_MC);
429 }
430 }
431
432 SLabelProperties Props;
433 Props.m_MaxWidth = Button.w;
434 Props.m_StopAtEnd = true;
435 Props.m_EnableWidthCheck = false;
436 bool Printed = false;
437 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_MAPNAME))
438 Printed = PrintHighlighted(pName: pItem->m_aMap, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
439 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_1), pRect: &Button, pText: pItem->m_aMap, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: (int)(pFilteredStr - pItem->m_aMap));
440 TextRender()->TextColor(Color: HIGHLIGHTED_TEXT_COLOR);
441 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_2), pRect: &Button, pText: pFilteredStr, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: FilterLen, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_MAP_1)->m_Cursor);
442 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
443 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_3), pRect: &Button, pText: pFilteredStr + FilterLen, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: -1, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_MAP_2)->m_Cursor);
444 });
445 if(!Printed)
446 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_1), pRect: &Button, pText: pItem->m_aMap, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
447 }
448 else if(Id == COL_FRIENDS)
449 {
450 if(pItem->m_FriendState != IFriends::FRIEND_NO)
451 {
452 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FRIEND_ICON), &Button, ColorRGBA(0.94f, 0.4f, 0.4f, 1.0f), TextRender()->DefaultTextOutlineColor(), FontIcon::HEART, TEXTALIGN_MC);
453
454 if(pItem->m_FriendNum > 1)
455 {
456 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%d", pItem->m_FriendNum);
457 TextRender()->TextColor(r: 0.94f, g: 0.8f, b: 0.8f, a: 1.0f);
458 Ui()->DoLabel(pRect: &Button, pText: aTemp, Size: 9.0f, Align: TEXTALIGN_MC);
459 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
460 }
461 }
462 }
463 else if(Id == COL_PLAYERS)
464 {
465 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%i/%i", pItem->m_NumFilteredPlayers, ServerBrowser()->Max(Item: *pItem));
466 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_PLAYER))
467 {
468 TextRender()->TextColor(Color: HIGHLIGHTED_TEXT_COLOR);
469 }
470 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_PLAYERS), pRect: &Button, pText: aTemp, Size: FontSize, Align: TEXTALIGN_MR);
471 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
472 }
473 else if(Id == COL_PING)
474 {
475 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
476 FormatServerbrowserPing(aBuffer&: aTemp, pInfo: pItem);
477 if(g_Config.m_UiColorizePing)
478 {
479 TextRender()->TextColor(Color: GetPingTextColor(Latency: pItem->m_Latency));
480 }
481 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_PING), pRect: &Button, pText: aTemp, Size: FontSize, Align: TEXTALIGN_MR);
482 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
483 }
484 }
485 }
486
487 const int NewSelected = s_ListBox.DoEnd();
488 if(NewSelected != m_SelectedIndex)
489 {
490 m_SelectedIndex = NewSelected;
491 if(m_SelectedIndex >= 0)
492 {
493 // select the new server
494 const CServerInfo *pItem = ServerBrowser()->SortedGet(Index: NewSelected);
495 if(pItem)
496 {
497 str_copy(dst&: g_Config.m_UiServerAddress, src: pItem->m_aAddress);
498 m_ServerBrowserShouldRevealSelection = true;
499 }
500 }
501 }
502
503 WasListboxItemActivated = s_ListBox.WasItemActivated();
504}
505
506void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItemActivated)
507{
508 // Render bar that shows the loading progression.
509 // The bar is only shown while loading and fades out when it's done.
510 CUIRect RefreshBar;
511 StatusBox.HSplitTop(Cut: 5.0f, pTop: &RefreshBar, pBottom: &StatusBox);
512 static float s_LoadingProgressionFadeEnd = 0.0f;
513 if(ServerBrowser()->IsRefreshing() && ServerBrowser()->LoadingProgression() < 100)
514 {
515 s_LoadingProgressionFadeEnd = Client()->GlobalTime() + 2.0f;
516 }
517 const float LoadingProgressionTimeDiff = s_LoadingProgressionFadeEnd - Client()->GlobalTime();
518 if(LoadingProgressionTimeDiff > 0.0f)
519 {
520 const float RefreshBarAlpha = minimum(a: LoadingProgressionTimeDiff, b: 0.8f);
521 RefreshBar.h = 2.0f;
522 RefreshBar.w *= ServerBrowser()->LoadingProgression() / 100.0f;
523 RefreshBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, RefreshBarAlpha), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
524 }
525
526 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
527 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
528 const float SearchExcludeAddrStrMax = 130.0f;
529 const float SearchIconWidth = TextRender()->TextWidth(Size: 16.0f, pText: FontIcon::MAGNIFYING_GLASS);
530 const float ExcludeIconWidth = TextRender()->TextWidth(Size: 16.0f, pText: FontIcon::BAN);
531 const float ExcludeSearchIconMax = maximum(a: SearchIconWidth, b: ExcludeIconWidth);
532 TextRender()->SetRenderFlags(0);
533 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
534
535 CUIRect SearchInfoAndAddr, ServersAndConnect, ServersPlayersOnline, SearchAndInfo, ServerAddr, ConnectButtons;
536 StatusBox.VSplitRight(Cut: 135.0f, pLeft: &SearchInfoAndAddr, pRight: &ServersAndConnect);
537 if(SearchInfoAndAddr.w > 350.0f)
538 SearchInfoAndAddr.VSplitLeft(Cut: 350.0f, pLeft: &SearchInfoAndAddr, pRight: nullptr);
539 SearchInfoAndAddr.HSplitTop(Cut: 40.0f, pTop: &SearchAndInfo, pBottom: &ServerAddr);
540 ServersAndConnect.HSplitTop(Cut: 35.0f, pTop: &ServersPlayersOnline, pBottom: &ConnectButtons);
541 ConnectButtons.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ConnectButtons);
542
543 CUIRect QuickSearch, QuickExclude;
544 SearchAndInfo.HSplitTop(Cut: 20.0f, pTop: &QuickSearch, pBottom: &QuickExclude);
545 QuickSearch.Margin(Cut: 2.0f, pOtherRect: &QuickSearch);
546 QuickExclude.Margin(Cut: 2.0f, pOtherRect: &QuickExclude);
547
548 // render quick search
549 {
550 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
551 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
552 Ui()->DoLabel(pRect: &QuickSearch, pText: FontIcon::MAGNIFYING_GLASS, Size: 16.0f, Align: TEXTALIGN_ML);
553 TextRender()->SetRenderFlags(0);
554 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
555 QuickSearch.VSplitLeft(Cut: ExcludeSearchIconMax, pLeft: nullptr, pRight: &QuickSearch);
556 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickSearch);
557
558 char aBufSearch[64];
559 str_format(buffer: aBufSearch, buffer_size: sizeof(aBufSearch), format: "%s:", Localize(pStr: "Search"));
560 Ui()->DoLabel(pRect: &QuickSearch, pText: aBufSearch, Size: 14.0f, Align: TEXTALIGN_ML);
561 QuickSearch.VSplitLeft(Cut: SearchExcludeAddrStrMax, pLeft: nullptr, pRight: &QuickSearch);
562 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickSearch);
563
564 static CLineInput s_FilterInput(g_Config.m_BrFilterString, sizeof(g_Config.m_BrFilterString));
565 static char s_aTooltipText[64];
566 str_format(buffer: s_aTooltipText, buffer_size: sizeof(s_aTooltipText), format: "%s: \"solo; nameless tee; kobra 2\"", Localize(pStr: "Example of usage"));
567 GameClient()->m_Tooltips.DoToolTip(pId: &s_FilterInput, pNearRect: &QuickSearch, pText: s_aTooltipText);
568 if(!Ui()->IsPopupOpen() && Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
569 {
570 Ui()->SetActiveItem(&s_FilterInput);
571 s_FilterInput.SelectAll();
572 }
573 if(Ui()->DoClearableEditBox(pLineInput: &s_FilterInput, pRect: &QuickSearch, FontSize: 12.0f))
574 Client()->ServerBrowserUpdate();
575 }
576
577 // render quick exclude
578 {
579 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
580 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
581 Ui()->DoLabel(pRect: &QuickExclude, pText: FontIcon::BAN, Size: 16.0f, Align: TEXTALIGN_ML);
582 TextRender()->SetRenderFlags(0);
583 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
584 QuickExclude.VSplitLeft(Cut: ExcludeSearchIconMax, pLeft: nullptr, pRight: &QuickExclude);
585 QuickExclude.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickExclude);
586
587 char aBufExclude[64];
588 str_format(buffer: aBufExclude, buffer_size: sizeof(aBufExclude), format: "%s:", Localize(pStr: "Exclude"));
589 Ui()->DoLabel(pRect: &QuickExclude, pText: aBufExclude, Size: 14.0f, Align: TEXTALIGN_ML);
590 QuickExclude.VSplitLeft(Cut: SearchExcludeAddrStrMax, pLeft: nullptr, pRight: &QuickExclude);
591 QuickExclude.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickExclude);
592
593 static CLineInput s_ExcludeInput(g_Config.m_BrExcludeString, sizeof(g_Config.m_BrExcludeString));
594 static char s_aTooltipText[64];
595 str_format(buffer: s_aTooltipText, buffer_size: sizeof(s_aTooltipText), format: "%s: \"CHN; [A]\"", Localize(pStr: "Example of usage"));
596 GameClient()->m_Tooltips.DoToolTip(pId: &s_ExcludeInput, pNearRect: &QuickSearch, pText: s_aTooltipText);
597 if(!Ui()->IsPopupOpen() && Input()->KeyPress(Key: KEY_X) && Input()->ShiftIsPressed() && Input()->ModifierIsPressed())
598 {
599 Ui()->SetActiveItem(&s_ExcludeInput);
600 s_ExcludeInput.SelectAll();
601 }
602 if(Ui()->DoClearableEditBox(pLineInput: &s_ExcludeInput, pRect: &QuickExclude, FontSize: 12.0f))
603 Client()->ServerBrowserUpdate();
604 }
605
606 // render status
607 {
608 CUIRect ServersOnline, PlayersOnline;
609 ServersPlayersOnline.HSplitMid(pTop: &PlayersOnline, pBottom: &ServersOnline);
610
611 char aBuf[128];
612 if(ServerBrowser()->NumServers() != 1)
613 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d of %d servers"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers());
614 else
615 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d of %d server"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers());
616 Ui()->DoLabel(pRect: &ServersOnline, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
617
618 if(ServerBrowser()->NumSortedPlayers() != 1)
619 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d players"), ServerBrowser()->NumSortedPlayers());
620 else
621 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d player"), ServerBrowser()->NumSortedPlayers());
622 Ui()->DoLabel(pRect: &PlayersOnline, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
623 }
624
625 // address info
626 {
627 CUIRect ServerAddrLabel, ServerAddrEditBox;
628 ServerAddr.Margin(Cut: 2.0f, pOtherRect: &ServerAddr);
629 ServerAddr.VSplitLeft(Cut: SearchExcludeAddrStrMax + 5.0f + ExcludeSearchIconMax + 5.0f, pLeft: &ServerAddrLabel, pRight: &ServerAddrEditBox);
630
631 Ui()->DoLabel(pRect: &ServerAddrLabel, pText: Localize(pStr: "Server address:"), Size: 14.0f, Align: TEXTALIGN_ML);
632 static CLineInput s_ServerAddressInput(g_Config.m_UiServerAddress, sizeof(g_Config.m_UiServerAddress));
633 if(Ui()->DoClearableEditBox(pLineInput: &s_ServerAddressInput, pRect: &ServerAddrEditBox, FontSize: 12.0f))
634 m_ServerBrowserShouldRevealSelection = true;
635 }
636
637 // buttons
638 {
639 CUIRect ButtonRefresh, ButtonConnect;
640 ConnectButtons.VSplitMid(pLeft: &ButtonRefresh, pRight: &ButtonConnect, Spacing: 5.0f);
641
642 // refresh button
643 {
644 char aLabelBuf[32] = {0};
645 const auto &&RefreshLabelFunc = [this, aLabelBuf]() mutable {
646 if(ServerBrowser()->IsRefreshing() || ServerBrowser()->IsGettingServerlist())
647 str_format(buffer: aLabelBuf, buffer_size: sizeof(aLabelBuf), format: "%s%s", FontIcon::ARROW_ROTATE_RIGHT, FontIcon::ELLIPSIS);
648 else
649 str_copy(dst&: aLabelBuf, src: FontIcon::ARROW_ROTATE_RIGHT);
650 return aLabelBuf;
651 };
652
653 SMenuButtonProperties Props;
654 Props.m_HintRequiresStringCheck = true;
655 Props.m_UseIconFont = true;
656
657 static CButtonContainer s_RefreshButton;
658 if(Ui()->DoButton_Menu(UIElement&: m_RefreshButton, pId: &s_RefreshButton, GetTextLambda: RefreshLabelFunc, pRect: &ButtonRefresh, Props) || (!Ui()->IsPopupOpen() && (Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))))
659 {
660 RefreshBrowserTab(Force: true);
661 }
662 }
663
664 // connect button
665 {
666 const auto &&ConnectLabelFunc = []() { return FontIcon::RIGHT_TO_BRACKET; };
667
668 SMenuButtonProperties Props;
669 Props.m_UseIconFont = true;
670 Props.m_Color = ColorRGBA(0.5f, 1.0f, 0.5f, 0.5f);
671
672 static CButtonContainer s_ConnectButton;
673 if(Ui()->DoButton_Menu(UIElement&: m_ConnectButton, pId: &s_ConnectButton, GetTextLambda: ConnectLabelFunc, pRect: &ButtonConnect, Props) || WasListboxItemActivated || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
674 {
675 Connect(pAddress: g_Config.m_UiServerAddress);
676 }
677 }
678 }
679}
680
681void CMenus::Connect(const char *pAddress)
682{
683 if(Client()->State() == IClient::STATE_ONLINE && GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
684 {
685 str_copy(dst&: m_aNextServer, src: pAddress);
686 PopupConfirm(pTitle: Localize(pStr: "Disconnect"), pMessage: Localize(pStr: "Are you sure that you want to disconnect and switch to a different server?"), pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmSwitchServer);
687 }
688 else
689 Client()->Connect(pAddress);
690}
691
692void CMenus::PopupConfirmSwitchServer()
693{
694 Client()->Connect(pAddress: m_aNextServer);
695}
696
697void CMenus::RenderServerbrowserFilters(CUIRect View)
698{
699 const float RowHeight = 18.0f;
700 const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox
701
702 View.Margin(Cut: 5.0f, pOtherRect: &View);
703
704 CUIRect Button, ResetButton;
705 View.HSplitBottom(Cut: RowHeight, pTop: &View, pBottom: &ResetButton);
706 View.HSplitBottom(Cut: 3.0f, pTop: &View, pBottom: nullptr);
707
708 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
709 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterEmpty, pText: Localize(pStr: "Has people playing"), Checked: g_Config.m_BrFilterEmpty, pRect: &Button))
710 g_Config.m_BrFilterEmpty ^= 1;
711
712 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
713 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterSpectators, pText: Localize(pStr: "Count players only"), Checked: g_Config.m_BrFilterSpectators, pRect: &Button))
714 g_Config.m_BrFilterSpectators ^= 1;
715
716 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
717 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterFull, pText: Localize(pStr: "Server not full"), Checked: g_Config.m_BrFilterFull, pRect: &Button))
718 g_Config.m_BrFilterFull ^= 1;
719
720 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
721 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterFriends, pText: Localize(pStr: "Show friends only"), Checked: g_Config.m_BrFilterFriends, pRect: &Button))
722 g_Config.m_BrFilterFriends ^= 1;
723
724 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
725 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterPw, pText: Localize(pStr: "No password"), Checked: g_Config.m_BrFilterPw, pRect: &Button))
726 g_Config.m_BrFilterPw ^= 1;
727
728 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
729 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterLogin, pText: Localize(pStr: "No login required"), Checked: g_Config.m_BrFilterLogin, pRect: &Button))
730 g_Config.m_BrFilterLogin ^= 1;
731
732 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
733 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterGametypeStrict, pText: Localize(pStr: "Strict gametype filter"), Checked: g_Config.m_BrFilterGametypeStrict, pRect: &Button))
734 g_Config.m_BrFilterGametypeStrict ^= 1;
735
736 View.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &View);
737 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
738 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Game types:"), Size: FontSize, Align: TEXTALIGN_ML);
739 Button.VSplitRight(Cut: 60.0f, pLeft: nullptr, pRight: &Button);
740 static CLineInput s_GametypeInput(g_Config.m_BrFilterGametype, sizeof(g_Config.m_BrFilterGametype));
741 if(Ui()->DoEditBox(pLineInput: &s_GametypeInput, pRect: &Button, FontSize))
742 Client()->ServerBrowserUpdate();
743
744 // server address
745 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
746 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
747 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
748 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Server address:"), Size: FontSize, Align: TEXTALIGN_ML);
749 Button.VSplitRight(Cut: 60.0f, pLeft: nullptr, pRight: &Button);
750 static CLineInput s_FilterServerAddressInput(g_Config.m_BrFilterServerAddress, sizeof(g_Config.m_BrFilterServerAddress));
751 if(Ui()->DoEditBox(pLineInput: &s_FilterServerAddressInput, pRect: &Button, FontSize))
752 Client()->ServerBrowserUpdate();
753
754 // player country
755 {
756 CUIRect Flag;
757 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
758 Button.VSplitRight(Cut: 60.0f, pLeft: &Button, pRight: &Flag);
759 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterCountry, pText: Localize(pStr: "Player country:"), Checked: g_Config.m_BrFilterCountry, pRect: &Button))
760 g_Config.m_BrFilterCountry ^= 1;
761
762 const float OldWidth = Flag.w;
763 Flag.w = Flag.h * 2.0f;
764 Flag.x += (OldWidth - Flag.w) / 2.0f;
765 GameClient()->m_CountryFlags.Render(CountryCode: g_Config.m_BrFilterCountryIndex, Color: ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &g_Config.m_BrFilterCountryIndex ? 1.0f : (g_Config.m_BrFilterCountry ? 0.9f : 0.5f)), x: Flag.x, y: Flag.y, w: Flag.w, h: Flag.h);
766
767 if(Ui()->DoButtonLogic(pId: &g_Config.m_BrFilterCountryIndex, Checked: 0, pRect: &Flag, Flags: BUTTONFLAG_LEFT))
768 {
769 static SPopupMenuId s_PopupCountryId;
770 static SPopupCountrySelectionContext s_PopupCountryContext;
771 s_PopupCountryContext.m_pMenus = this;
772 s_PopupCountryContext.m_Selection = g_Config.m_BrFilterCountryIndex;
773 s_PopupCountryContext.m_New = true;
774 Ui()->DoPopupMenu(pId: &s_PopupCountryId, X: Flag.x, Y: Flag.y + Flag.h, Width: 490, Height: 210, pContext: &s_PopupCountryContext, pfnFunc: PopupCountrySelection);
775 }
776 }
777
778 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
779 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterConnectingPlayers, pText: Localize(pStr: "Filter connecting players"), Checked: g_Config.m_BrFilterConnectingPlayers, pRect: &Button))
780 g_Config.m_BrFilterConnectingPlayers ^= 1;
781
782 // map finish filters
783 if(ServerBrowser()->CommunityCache().AnyRanksAvailable())
784 {
785 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
786 if(DoButton_CheckBox(pId: &g_Config.m_BrIndicateFinished, pText: Localize(pStr: "Indicate map finish"), Checked: g_Config.m_BrIndicateFinished, pRect: &Button))
787 {
788 g_Config.m_BrIndicateFinished ^= 1;
789 if(g_Config.m_BrIndicateFinished)
790 ServerBrowser()->Refresh(Type: ServerBrowser()->GetCurrentType());
791 }
792
793 if(g_Config.m_BrIndicateFinished)
794 {
795 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
796 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterUnfinishedMap, pText: Localize(pStr: "Unfinished map"), Checked: g_Config.m_BrFilterUnfinishedMap, pRect: &Button))
797 g_Config.m_BrFilterUnfinishedMap ^= 1;
798 }
799 else
800 {
801 g_Config.m_BrFilterUnfinishedMap = 0;
802 }
803 }
804
805 // countries and types filters
806 if(ServerBrowser()->CommunityCache().CountriesTypesFilterAvailable())
807 {
808 const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f);
809 const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f);
810
811 CUIRect TabContents, CountriesTab, TypesTab;
812 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
813 View.HSplitTop(Cut: 19.0f, pTop: &Button, pBottom: &View);
814 View.HSplitTop(Cut: minimum(a: 4.0f * 22.0f + CScrollRegion::HEIGHT_MAGIC_FIX, b: View.h), pTop: &TabContents, pBottom: &View);
815 Button.VSplitMid(pLeft: &CountriesTab, pRight: &TypesTab);
816 TabContents.Draw(Color: ColorActive, Corners: IGraphics::CORNER_B, Rounding: 4.0f);
817
818 enum EFilterTab
819 {
820 FILTERTAB_COUNTRIES = 0,
821 FILTERTAB_TYPES,
822 };
823 static EFilterTab s_ActiveTab = FILTERTAB_COUNTRIES;
824
825 static CButtonContainer s_CountriesButton;
826 if(DoButton_MenuTab(pButtonContainer: &s_CountriesButton, pText: Localize(pStr: "Countries"), Checked: s_ActiveTab == FILTERTAB_COUNTRIES, pRect: &CountriesTab, Corners: IGraphics::CORNER_TL, pAnimator: nullptr, pDefaultColor: &ColorInactive, pActiveColor: &ColorActive, pHoverColor: nullptr, EdgeRounding: 4.0f))
827 {
828 s_ActiveTab = FILTERTAB_COUNTRIES;
829 }
830
831 static CButtonContainer s_TypesButton;
832 if(DoButton_MenuTab(pButtonContainer: &s_TypesButton, pText: Localize(pStr: "Types"), Checked: s_ActiveTab == FILTERTAB_TYPES, pRect: &TypesTab, Corners: IGraphics::CORNER_TR, pAnimator: nullptr, pDefaultColor: &ColorInactive, pActiveColor: &ColorActive, pHoverColor: nullptr, EdgeRounding: 4.0f))
833 {
834 s_ActiveTab = FILTERTAB_TYPES;
835 }
836
837 if(s_ActiveTab == FILTERTAB_COUNTRIES)
838 {
839 RenderServerbrowserCountriesFilter(View: TabContents);
840 }
841 else if(s_ActiveTab == FILTERTAB_TYPES)
842 {
843 RenderServerbrowserTypesFilter(View: TabContents);
844 }
845 }
846
847 static CButtonContainer s_ResetButton;
848 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
849 {
850 ResetServerbrowserFilters();
851 }
852}
853
854void CMenus::ResetServerbrowserFilters()
855{
856 g_Config.m_BrFilterString[0] = '\0';
857 g_Config.m_BrExcludeString[0] = '\0';
858 g_Config.m_BrFilterFull = 0;
859 g_Config.m_BrFilterEmpty = 0;
860 g_Config.m_BrFilterSpectators = 0;
861 g_Config.m_BrFilterFriends = 0;
862 g_Config.m_BrFilterCountry = 0;
863 g_Config.m_BrFilterCountryIndex = -1;
864 g_Config.m_BrFilterPw = 0;
865 g_Config.m_BrFilterGametype[0] = '\0';
866 g_Config.m_BrFilterGametypeStrict = 0;
867 g_Config.m_BrFilterConnectingPlayers = 1;
868 g_Config.m_BrFilterServerAddress[0] = '\0';
869 g_Config.m_BrFilterLogin = true;
870
871 if(g_Config.m_UiPage != PAGE_LAN)
872 {
873 if(ServerBrowser()->CommunityCache().AnyRanksAvailable())
874 {
875 g_Config.m_BrFilterUnfinishedMap = 0;
876 }
877 if(g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES)
878 {
879 ServerBrowser()->CommunitiesFilter().Clear();
880 }
881 ServerBrowser()->CountriesFilter().Clear();
882 ServerBrowser()->TypesFilter().Clear();
883 UpdateCommunityCache(Force: true);
884 }
885
886 Client()->ServerBrowserUpdate();
887}
888
889void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
890 IFilterList &Filter,
891 float ItemHeight, int MaxItems, int ItemsPerRow,
892 CScrollRegion &ScrollRegion, std::vector<unsigned char> &vItemIds,
893 bool UpdateCommunityCacheOnChange,
894 const std::function<const char *(int ItemIndex)> &GetItemName,
895 const std::function<void(int ItemIndex, CUIRect Item, const void *pItemId, bool Active)> &RenderItem)
896{
897 vItemIds.resize(new_size: MaxItems);
898
899 vec2 ScrollOffset(0.0f, 0.0f);
900 CScrollRegionParams ScrollParams;
901 ScrollParams.m_ScrollbarWidth = 10.0f;
902 ScrollParams.m_ScrollbarMargin = 3.0f;
903 ScrollParams.m_ScrollUnit = 2.0f * ItemHeight;
904 ScrollRegion.Begin(pClipRect: &View, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
905 View.y += ScrollOffset.y;
906
907 CUIRect Row;
908 int ColumnIndex = 0;
909 for(int ItemIndex = 0; ItemIndex < MaxItems; ++ItemIndex)
910 {
911 CUIRect Item;
912 if(ColumnIndex == 0)
913 View.HSplitTop(Cut: ItemHeight, pTop: &Row, pBottom: &View);
914 Row.VSplitLeft(Cut: View.w / ItemsPerRow, pLeft: &Item, pRight: &Row);
915 ColumnIndex = (ColumnIndex + 1) % ItemsPerRow;
916 if(!ScrollRegion.AddRect(Rect: Item))
917 continue;
918
919 const void *pItemId = &vItemIds[ItemIndex];
920 const char *pName = GetItemName(ItemIndex);
921 const bool Active = !Filter.Filtered(pElement: pName);
922
923 const int Click = Ui()->DoButtonLogic(pId: pItemId, Checked: 0, pRect: &Item, Flags: BUTTONFLAG_ALL);
924 if(Click == 1 || Click == 2)
925 {
926 // left/right click to toggle filter
927 if(Filter.Empty())
928 {
929 if(Click == 1)
930 {
931 // Left click: when all are active, only activate one and none
932 for(int j = 0; j < MaxItems; ++j)
933 {
934 if(const char *pItemName = GetItemName(j);
935 j != ItemIndex &&
936 !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
937 (&Filter == &ServerBrowser()->TypesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
938 Filter.Add(pElement: pItemName);
939 }
940 }
941 else if(Click == 2)
942 {
943 // Right click: when all are active, only deactivate one
944 if(MaxItems >= 2)
945 {
946 Filter.Add(pElement: GetItemName(ItemIndex));
947 }
948 }
949 }
950 else
951 {
952 bool AllFilteredExceptUs = true;
953 for(int j = 0; j < MaxItems; ++j)
954 {
955 if(const char *pItemName = GetItemName(j);
956 j != ItemIndex && !Filter.Filtered(pElement: pItemName) &&
957 !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
958 (&Filter == &ServerBrowser()->TypesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
959 {
960 AllFilteredExceptUs = false;
961 break;
962 }
963 }
964 // When last one is removed, re-enable all currently selectable items.
965 // Don't use Clear, to avoid enabling also currently unselectable items.
966 if(AllFilteredExceptUs && Active)
967 {
968 for(int j = 0; j < MaxItems; ++j)
969 {
970 Filter.Remove(pElement: GetItemName(j));
971 }
972 }
973 else if(Active)
974 {
975 Filter.Add(pElement: pName);
976 }
977 else
978 {
979 Filter.Remove(pElement: pName);
980 }
981 }
982
983 Client()->ServerBrowserUpdate();
984 if(UpdateCommunityCacheOnChange)
985 UpdateCommunityCache(Force: true);
986 }
987 else if(Click == 3)
988 {
989 // middle click to reset (re-enable all currently selectable items)
990 for(int j = 0; j < MaxItems; ++j)
991 {
992 Filter.Remove(pElement: GetItemName(j));
993 }
994 Client()->ServerBrowserUpdate();
995 if(UpdateCommunityCacheOnChange)
996 UpdateCommunityCache(Force: true);
997 }
998
999 if(Ui()->HotItem() == pItemId && !ScrollRegion.Animating())
1000 Item.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f), Corners: IGraphics::CORNER_ALL, Rounding: 2.0f);
1001 RenderItem(ItemIndex, Item, pItemId, Active);
1002 }
1003
1004 ScrollRegion.End();
1005}
1006
1007void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View)
1008{
1009 CUIRect Tab;
1010 View.HSplitTop(Cut: 19.0f, pTop: &Tab, pBottom: &View);
1011 Tab.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_T, Rounding: 4.0f);
1012 Ui()->DoLabel(pRect: &Tab, pText: Localize(pStr: "Communities"), Size: 12.0f, Align: TEXTALIGN_MC);
1013 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 4.0f);
1014
1015 const int MaxEntries = ServerBrowser()->Communities().size();
1016 if(MaxEntries == 0)
1017 {
1018 CUIRect ErrorLabel;
1019 View.Margin(Cut: 5.0f, pOtherRect: &ErrorLabel);
1020 SLabelProperties ErrorLabelProps;
1021 ErrorLabelProps.m_MaxWidth = ErrorLabel.w;
1022 ErrorLabelProps.SetColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
1023 Ui()->DoLabel(pRect: &ErrorLabel, pText: Localize(pStr: "Error loading communities"), Size: 10.0f, Align: TEXTALIGN_MC, LabelProps: ErrorLabelProps);
1024 return;
1025 }
1026
1027 const int EntriesPerRow = 1;
1028
1029 static CScrollRegion s_ScrollRegion;
1030 static std::vector<unsigned char> s_vItemIds;
1031 static std::vector<unsigned char> s_vFavoriteButtonIds;
1032
1033 const float ItemHeight = 13.0f;
1034 const float Spacing = 2.0f;
1035
1036 const auto &&GetItemName = [&](int ItemIndex) {
1037 return ServerBrowser()->Communities()[ItemIndex].Id();
1038 };
1039 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1040 const auto &Community = ServerBrowser()->Communities()[ItemIndex];
1041 const float Alpha = (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f);
1042
1043 CUIRect Icon, NameLabel, PlayerCountIcon, PlayerCountLabel, FavoriteButton;
1044 Item.VSplitRight(Cut: Item.h, pLeft: &Item, pRight: &FavoriteButton);
1045 Item.HMargin(Cut: Spacing, pOtherRect: &Item);
1046 Item.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &Item);
1047 Item.VSplitRight(Cut: 1.0f, pLeft: &Item, pRight: nullptr);
1048 Item.VSplitLeft(Cut: Item.h * 2.0f, pLeft: &Icon, pRight: &NameLabel);
1049 NameLabel.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &NameLabel);
1050 NameLabel.VSplitRight(Cut: 8.0f, pLeft: &NameLabel, pRight: &PlayerCountIcon);
1051 NameLabel.VSplitRight(Cut: 25.0f, pLeft: &NameLabel, pRight: &PlayerCountLabel);
1052
1053 const char *pItemName = Community.Id();
1054 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pItemName);
1055 if(pIcon != nullptr)
1056 {
1057 m_CommunityIcons.Render(pIcon, Rect: Icon, Active);
1058 }
1059
1060 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
1061 Ui()->DoLabel(pRect: &NameLabel, pText: Community.Name(), Size: NameLabel.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_ML);
1062 char aNumPlayersLabel[8];
1063 str_format(buffer: aNumPlayersLabel, buffer_size: sizeof(aNumPlayersLabel), format: "%d", Community.NumPlayers());
1064 Ui()->DoLabel(pRect: &PlayerCountLabel, pText: aNumPlayersLabel, Size: 7.0f, Align: TEXTALIGN_MR);
1065 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1066 Ui()->DoLabel(pRect: &PlayerCountIcon, pText: FontIcon::USER, Size: 7.0f, Align: TEXTALIGN_MC);
1067 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1068 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1069
1070 const bool Favorite = ServerBrowser()->FavoriteCommunitiesFilter().Filtered(pElement: pItemName);
1071 if(DoButton_Favorite(pButtonId: &s_vFavoriteButtonIds[ItemIndex], pParentId: pItemId, Checked: Favorite, pRect: &FavoriteButton))
1072 {
1073 if(Favorite)
1074 {
1075 ServerBrowser()->FavoriteCommunitiesFilter().Remove(pElement: pItemName);
1076 }
1077 else
1078 {
1079 ServerBrowser()->FavoriteCommunitiesFilter().Add(pElement: pItemName);
1080 }
1081 }
1082 GameClient()->m_Tooltips.DoToolTip(pId: &s_vFavoriteButtonIds[ItemIndex], pNearRect: &FavoriteButton,
1083 pText: Favorite ? Localize(pStr: "Click to remove this community from your favorites.") : Localize(pStr: "Click to add this community to your favorites."));
1084 };
1085
1086 s_vFavoriteButtonIds.resize(new_size: MaxEntries);
1087 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->CommunitiesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: true, GetItemName, RenderItem);
1088}
1089
1090void CMenus::RenderServerbrowserCountriesFilter(CUIRect View)
1091{
1092 const int MaxEntries = ServerBrowser()->CommunityCache().SelectableCountries().size();
1093 const int EntriesPerRow = MaxEntries > 8 ? 5 : 4;
1094
1095 static CScrollRegion s_ScrollRegion;
1096 static std::vector<unsigned char> s_vItemIds;
1097
1098 const float ItemHeight = 18.0f;
1099 const float Spacing = 2.0f;
1100
1101 const auto &&GetItemName = [&](int ItemIndex) {
1102 return ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->Name();
1103 };
1104 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1105 Item.Margin(Cut: Spacing, pOtherRect: &Item);
1106 const float OldWidth = Item.w;
1107 Item.w = Item.h * 2.0f;
1108 Item.x += (OldWidth - Item.w) / 2.0f;
1109 GameClient()->m_CountryFlags.Render(CountryCode: ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->FlagId(), Color: ColorRGBA(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f)), x: Item.x, y: Item.y, w: Item.w, h: Item.h);
1110 };
1111
1112 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->CountriesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: false, GetItemName, RenderItem);
1113}
1114
1115void CMenus::RenderServerbrowserTypesFilter(CUIRect View)
1116{
1117 const int MaxEntries = ServerBrowser()->CommunityCache().SelectableTypes().size();
1118 const int EntriesPerRow = 3;
1119
1120 static CScrollRegion s_ScrollRegion;
1121 static std::vector<unsigned char> s_vItemIds;
1122
1123 const float ItemHeight = 13.0f;
1124 const float Spacing = 2.0f;
1125
1126 const auto &&GetItemName = [&](int ItemIndex) {
1127 return ServerBrowser()->CommunityCache().SelectableTypes()[ItemIndex]->Name();
1128 };
1129 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1130 Item.Margin(Cut: Spacing, pOtherRect: &Item);
1131 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f));
1132 Ui()->DoLabel(pRect: &Item, pText: GetItemName(ItemIndex), Size: Item.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1133 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1134 };
1135
1136 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->TypesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: false, GetItemName, RenderItem);
1137}
1138
1139CUi::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIRect View, bool Active)
1140{
1141 SPopupCountrySelectionContext *pPopupContext = static_cast<SPopupCountrySelectionContext *>(pContext);
1142 CMenus *pMenus = pPopupContext->m_pMenus;
1143
1144 static CListBox s_ListBox;
1145 s_ListBox.SetActive(Active);
1146 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: pMenus->GameClient()->m_CountryFlags.Num(), ItemsPerRow: 8, RowsPerScroll: 1, SelectedIndex: -1, pRect: &View, Background: false);
1147
1148 if(pPopupContext->m_New)
1149 {
1150 pPopupContext->m_New = false;
1151 s_ListBox.ScrollToSelected();
1152 }
1153
1154 for(size_t i = 0; i < pMenus->GameClient()->m_CountryFlags.Num(); ++i)
1155 {
1156 const CCountryFlags::CCountryFlag &Entry = pMenus->GameClient()->m_CountryFlags.GetByIndex(Index: i);
1157
1158 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Entry, Selected: Entry.m_CountryCode == pPopupContext->m_Selection);
1159 if(!Item.m_Visible)
1160 continue;
1161
1162 CUIRect FlagRect, Label;
1163 Item.m_Rect.Margin(Cut: 5.0f, pOtherRect: &FlagRect);
1164 FlagRect.HSplitBottom(Cut: 12.0f, pTop: &FlagRect, pBottom: &Label);
1165 Label.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Label);
1166 const float OldWidth = FlagRect.w;
1167 FlagRect.w = FlagRect.h * 2.0f;
1168 FlagRect.x += (OldWidth - FlagRect.w) / 2.0f;
1169 pMenus->GameClient()->m_CountryFlags.Render(CountryCode: Entry.m_CountryCode, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), x: FlagRect.x, y: FlagRect.y, w: FlagRect.w, h: FlagRect.h);
1170
1171 pMenus->Ui()->DoLabel(pRect: &Label, pText: Entry.m_aCountryCodeString, Size: 10.0f, Align: TEXTALIGN_MC);
1172 }
1173
1174 const int NewSelected = s_ListBox.DoEnd();
1175 pPopupContext->m_Selection = NewSelected >= 0 ? pMenus->GameClient()->m_CountryFlags.GetByIndex(Index: NewSelected).m_CountryCode : -1;
1176 if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated())
1177 {
1178 g_Config.m_BrFilterCountry = 1;
1179 g_Config.m_BrFilterCountryIndex = pPopupContext->m_Selection;
1180 pMenus->Client()->ServerBrowserUpdate();
1181 return CUi::POPUP_CLOSE_CURRENT;
1182 }
1183
1184 return CUi::POPUP_KEEP_OPEN;
1185}
1186
1187void CMenus::RenderServerbrowserInfo(CUIRect View)
1188{
1189 const CServerInfo *pSelectedServer = ServerBrowser()->SortedGet(Index: m_SelectedIndex);
1190
1191 const float RowHeight = 18.0f;
1192 const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox
1193
1194 CUIRect ServerDetails, Scoreboard;
1195 View.HSplitTop(Cut: 4.0f * 15.0f + RowHeight + 2.0f * 5.0f + 2.0f * 2.0f, pTop: &ServerDetails, pBottom: &Scoreboard);
1196
1197 if(pSelectedServer)
1198 {
1199 ServerDetails.Margin(Cut: 5.0f, pOtherRect: &ServerDetails);
1200
1201 // copy info button
1202 {
1203 CUIRect Button;
1204 ServerDetails.HSplitBottom(Cut: 15.0f, pTop: &ServerDetails, pBottom: &Button);
1205 static CButtonContainer s_CopyButton;
1206 if(DoButton_Menu(pButtonContainer: &s_CopyButton, pText: Localize(pStr: "Copy info"), Checked: 0, pRect: &Button))
1207 {
1208 char aInfo[256];
1209 str_format(
1210 buffer: aInfo,
1211 buffer_size: sizeof(aInfo),
1212 format: "%s\n"
1213 "Address: ddnet://%s\n",
1214 pSelectedServer->m_aName,
1215 pSelectedServer->m_aAddress);
1216 Input()->SetClipboardText(aInfo);
1217 }
1218 }
1219
1220 // favorite checkbox
1221 {
1222 CUIRect ButtonAddFav, ButtonLeakIp;
1223 ServerDetails.HSplitBottom(Cut: 2.0f, pTop: &ServerDetails, pBottom: nullptr);
1224 ServerDetails.HSplitBottom(Cut: RowHeight, pTop: &ServerDetails, pBottom: &ButtonAddFav);
1225 ServerDetails.HSplitBottom(Cut: 2.0f, pTop: &ServerDetails, pBottom: nullptr);
1226 ButtonAddFav.VSplitMid(pLeft: &ButtonAddFav, pRight: &ButtonLeakIp);
1227 static int s_AddFavButton = 0;
1228 if(DoButton_CheckBox_Tristate(pId: &s_AddFavButton, pText: Localize(pStr: "Favorite"), Checked: pSelectedServer->m_Favorite, pRect: &ButtonAddFav))
1229 {
1230 if(pSelectedServer->m_Favorite != TRISTATE::NONE)
1231 {
1232 Favorites()->Remove(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses);
1233 }
1234 else
1235 {
1236 Favorites()->Add(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses);
1237 if(g_Config.m_UiPage == PAGE_LAN)
1238 {
1239 Favorites()->AllowPing(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses, AllowPing: true);
1240 }
1241 }
1242 Client()->ServerBrowserUpdate();
1243 }
1244 if(pSelectedServer->m_Favorite != TRISTATE::NONE)
1245 {
1246 static int s_LeakIpButton = 0;
1247 if(DoButton_CheckBox_Tristate(pId: &s_LeakIpButton, pText: Localize(pStr: "Leak IP"), Checked: pSelectedServer->m_FavoriteAllowPing, pRect: &ButtonLeakIp))
1248 {
1249 Favorites()->AllowPing(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses, AllowPing: pSelectedServer->m_FavoriteAllowPing == TRISTATE::NONE);
1250 Client()->ServerBrowserUpdate();
1251 }
1252 }
1253 }
1254
1255 CUIRect LeftColumn, RightColumn, Row;
1256 ServerDetails.VSplitLeft(Cut: 80.0f, pLeft: &LeftColumn, pRight: &RightColumn);
1257
1258 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1259 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1260
1261 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1262 Ui()->DoLabel(pRect: &Row, pText: pSelectedServer->m_aVersion, Size: FontSize, Align: TEXTALIGN_ML);
1263
1264 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1265 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Game type"), Size: FontSize, Align: TEXTALIGN_ML);
1266
1267 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1268 Ui()->DoLabel(pRect: &Row, pText: pSelectedServer->m_aGameType, Size: FontSize, Align: TEXTALIGN_ML);
1269
1270 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1271 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Ping"), Size: FontSize, Align: TEXTALIGN_ML);
1272
1273 if(g_Config.m_UiColorizePing)
1274 TextRender()->TextColor(Color: GetPingTextColor(Latency: pSelectedServer->m_Latency));
1275 char aTemp[16];
1276 FormatServerbrowserPing(aBuffer&: aTemp, pInfo: pSelectedServer);
1277 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1278 Ui()->DoLabel(pRect: &Row, pText: aTemp, Size: FontSize, Align: TEXTALIGN_ML);
1279 if(g_Config.m_UiColorizePing)
1280 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1281
1282 RenderServerbrowserInfoScoreboard(View: Scoreboard, pSelectedServer);
1283 }
1284 else
1285 {
1286 Ui()->DoLabel(pRect: &ServerDetails, pText: Localize(pStr: "No server selected"), Size: FontSize, Align: TEXTALIGN_MC);
1287 }
1288}
1289
1290void CMenus::RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo *pSelectedServer)
1291{
1292 const float FontSize = 10.0f;
1293
1294 static CListBox s_ListBox;
1295 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
1296 s_ListBox.DoAutoSpacing(Spacing: 2.0f);
1297 s_ListBox.SetScrollbarWidth(16.0f);
1298 s_ListBox.SetScrollbarMargin(5.0f);
1299 s_ListBox.DoStart(RowHeight: 25.0f, NumItems: pSelectedServer->m_NumReceivedClients, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false, BackgroundCorners: IGraphics::CORNER_NONE, ForceShowScrollbar: true);
1300
1301 for(int i = 0; i < pSelectedServer->m_NumReceivedClients; i++)
1302 {
1303 const CServerInfo::CClient &CurrentClient = pSelectedServer->m_aClients[i];
1304 const CListboxItem Item = s_ListBox.DoNextItem(pId: &CurrentClient);
1305 if(!Item.m_Visible)
1306 continue;
1307
1308 CUIRect Skin, Name, Clan, Score, Flag;
1309 Name = Item.m_Rect;
1310
1311 const ColorRGBA Color = PlayerBackgroundColor(Friend: CurrentClient.m_FriendState == IFriends::FRIEND_PLAYER, Clan: CurrentClient.m_FriendState == IFriends::FRIEND_CLAN, Afk: CurrentClient.m_Afk, InSelectedServer: false, Inside: false);
1312 Name.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
1313 Name.VSplitLeft(Cut: 1.0f, pLeft: nullptr, pRight: &Name);
1314 Name.VSplitLeft(Cut: 34.0f, pLeft: &Score, pRight: &Name);
1315 Name.VSplitLeft(Cut: 18.0f, pLeft: &Skin, pRight: &Name);
1316 Name.VSplitRight(Cut: 26.0f, pLeft: &Name, pRight: &Flag);
1317 Flag.HMargin(Cut: 6.0f, pOtherRect: &Flag);
1318 Name.HSplitTop(Cut: 12.0f, pTop: &Name, pBottom: &Clan);
1319
1320 // score
1321 char aTemp[16];
1322 if(!CurrentClient.m_Player)
1323 {
1324 str_copy(dst&: aTemp, src: "SPEC");
1325 }
1326 else if(pSelectedServer->m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_POINTS)
1327 {
1328 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%d", CurrentClient.m_Score);
1329 }
1330 else
1331 {
1332 std::optional<int> Time = {};
1333
1334 if(pSelectedServer->m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT)
1335 {
1336 const int TempTime = absolute(a: CurrentClient.m_Score);
1337 if(TempTime != 0 && TempTime != 9999)
1338 Time = TempTime;
1339 }
1340 else
1341 {
1342 // CServerInfo::CLIENT_SCORE_KIND_POINTS
1343 if(CurrentClient.m_Score >= 0)
1344 Time = CurrentClient.m_Score;
1345 }
1346
1347 if(Time.has_value())
1348 {
1349 str_time(centisecs: (int64_t)Time.value() * 100, format: ETimeFormat::HOURS, buffer: aTemp, buffer_size: sizeof(aTemp));
1350 }
1351 else
1352 {
1353 aTemp[0] = '\0';
1354 }
1355 }
1356
1357 Ui()->DoLabel(pRect: &Score, pText: aTemp, Size: FontSize, Align: TEXTALIGN_ML);
1358
1359 // render tee if available
1360 if(CurrentClient.m_aSkin[0] != '\0')
1361 {
1362 const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(Size: vec2(Skin.w, Skin.h), pSkinName: CurrentClient.m_aSkin, CustomSkinColors: CurrentClient.m_CustomSkinColors, CustomSkinColorBody: CurrentClient.m_CustomSkinColorBody, CustomSkinColorFeet: CurrentClient.m_CustomSkinColorFeet);
1363 const CAnimState *pIdleState = CAnimState::GetIdle();
1364 vec2 OffsetToMid;
1365 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1366 const vec2 TeeRenderPos = vec2(Skin.x + TeeInfo.m_Size / 2.0f, Skin.y + Skin.h / 2.0f + OffsetToMid.y);
1367 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: CurrentClient.m_Afk ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1368 Ui()->DoButtonLogic(pId: &CurrentClient.m_aSkin, Checked: 0, pRect: &Skin, Flags: BUTTONFLAG_NONE);
1369 GameClient()->m_Tooltips.DoToolTip(pId: &CurrentClient.m_aSkin, pNearRect: &Skin, pText: CurrentClient.m_aSkin);
1370 }
1371 else if(CurrentClient.m_aaSkin7[protocol7::SKINPART_BODY][0] != '\0')
1372 {
1373 CTeeRenderInfo TeeInfo;
1374 TeeInfo.m_Size = minimum(a: Skin.w, b: Skin.h);
1375 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
1376 {
1377 GameClient()->m_Skins7.FindSkinPart(Part, pName: CurrentClient.m_aaSkin7[Part], AllowSpecialPart: true)->ApplyTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy]);
1378 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy], UseCustomColors: CurrentClient.m_aUseCustomSkinColor7[Part], Value: CurrentClient.m_aCustomSkinColor7[Part], Part);
1379 }
1380 const CAnimState *pIdleState = CAnimState::GetIdle();
1381 vec2 OffsetToMid;
1382 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1383 const vec2 TeeRenderPos = vec2(Skin.x + TeeInfo.m_Size / 2.0f, Skin.y + Skin.h / 2.0f + OffsetToMid.y);
1384 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: CurrentClient.m_Afk ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1385 }
1386
1387 // name
1388 CTextCursor NameCursor;
1389 NameCursor.SetPosition(vec2(Name.x, Name.y + (Name.h - (FontSize - 1.0f)) / 2.0f));
1390 NameCursor.m_FontSize = FontSize - 1.0f;
1391 NameCursor.m_Flags |= TEXTFLAG_STOP_AT_END;
1392 NameCursor.m_LineWidth = Name.w;
1393 const char *pName = CurrentClient.m_aName;
1394 bool Printed = false;
1395 if(g_Config.m_BrFilterString[0])
1396 Printed = PrintHighlighted(pName, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
1397 TextRender()->TextEx(pCursor: &NameCursor, pText: pName, Length: (int)(pFilteredStr - pName));
1398 TextRender()->TextColor(Color: HIGHLIGHTED_TEXT_COLOR);
1399 TextRender()->TextEx(pCursor: &NameCursor, pText: pFilteredStr, Length: FilterLen);
1400 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1401 TextRender()->TextEx(pCursor: &NameCursor, pText: pFilteredStr + FilterLen, Length: -1);
1402 });
1403 if(!Printed)
1404 TextRender()->TextEx(pCursor: &NameCursor, pText: pName, Length: -1);
1405
1406 // clan
1407 CTextCursor ClanCursor;
1408 ClanCursor.SetPosition(vec2(Clan.x, Clan.y + (Clan.h - (FontSize - 2.0f)) / 2.0f));
1409 ClanCursor.m_FontSize = FontSize - 2.0f;
1410 ClanCursor.m_Flags |= TEXTFLAG_STOP_AT_END;
1411 ClanCursor.m_LineWidth = Clan.w;
1412 const char *pClan = CurrentClient.m_aClan;
1413 Printed = false;
1414 if(g_Config.m_BrFilterString[0])
1415 Printed = PrintHighlighted(pName: pClan, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
1416 TextRender()->TextEx(pCursor: &ClanCursor, pText: pClan, Length: (int)(pFilteredStr - pClan));
1417 TextRender()->TextColor(r: 0.4f, g: 0.4f, b: 1.0f, a: 1.0f);
1418 TextRender()->TextEx(pCursor: &ClanCursor, pText: pFilteredStr, Length: FilterLen);
1419 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1420 TextRender()->TextEx(pCursor: &ClanCursor, pText: pFilteredStr + FilterLen, Length: -1);
1421 });
1422 if(!Printed)
1423 TextRender()->TextEx(pCursor: &ClanCursor, pText: pClan, Length: -1);
1424
1425 // flag
1426 GameClient()->m_CountryFlags.Render(CountryCode: CurrentClient.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), x: Flag.x, y: Flag.y, w: Flag.w, h: Flag.h);
1427 }
1428
1429 const int NewSelected = s_ListBox.DoEnd();
1430 if(s_ListBox.WasItemSelected())
1431 {
1432 const CServerInfo::CClient &SelectedClient = pSelectedServer->m_aClients[NewSelected];
1433 if(SelectedClient.m_FriendState == IFriends::FRIEND_PLAYER)
1434 GameClient()->Friends()->RemoveFriend(pName: SelectedClient.m_aName, pClan: SelectedClient.m_aClan);
1435 else
1436 GameClient()->Friends()->AddFriend(pName: SelectedClient.m_aName, pClan: SelectedClient.m_aClan);
1437 FriendlistOnUpdate();
1438 Client()->ServerBrowserUpdate();
1439 }
1440}
1441
1442void CMenus::RenderServerbrowserFriends(CUIRect View)
1443{
1444 const float FontSize = 10.0f;
1445 static bool s_aListExtended[NUM_FRIEND_TYPES] = {true, true, false};
1446 const float SpacingH = 2.0f;
1447
1448 CUIRect List, ServerFriends;
1449 View.HSplitBottom(Cut: 70.0f, pTop: &List, pBottom: &ServerFriends);
1450 List.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &List);
1451 List.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &List);
1452
1453 // calculate friends
1454 // TODO: optimize this
1455 m_pRemoveFriend = nullptr;
1456 for(auto &vFriends : m_avFriends)
1457 vFriends.clear();
1458 m_avFriends[FRIEND_OFF].reserve(n: GameClient()->Friends()->NumFriends());
1459 for(int FriendIndex = 0; FriendIndex < GameClient()->Friends()->NumFriends(); ++FriendIndex)
1460 {
1461 m_avFriends[FRIEND_OFF].emplace_back(args: GameClient()->Friends()->GetFriend(Index: FriendIndex));
1462 }
1463 bool HasFriend = std::any_of(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: [&](const auto &Friend) {
1464 return Friend.Name()[0] != '\0';
1465 }),
1466 HasClan = std::any_of(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: [&](const auto &Friend) {
1467 return Friend.Name()[0] == '\0';
1468 });
1469
1470 for(int ServerIndex = 0; ServerIndex < ServerBrowser()->NumServers(); ++ServerIndex)
1471 {
1472 const CServerInfo *pEntry = ServerBrowser()->Get(Index: ServerIndex);
1473 if(pEntry->m_FriendState == IFriends::FRIEND_NO)
1474 continue;
1475
1476 for(int ClientIndex = 0; ClientIndex < pEntry->m_NumClients; ++ClientIndex)
1477 {
1478 const CServerInfo::CClient &CurrentClient = pEntry->m_aClients[ClientIndex];
1479 if(CurrentClient.m_FriendState == IFriends::FRIEND_NO)
1480 continue;
1481
1482 const int FriendIndex = CurrentClient.m_FriendState == IFriends::FRIEND_PLAYER ? FRIEND_PLAYER_ON : FRIEND_CLAN_ON;
1483 m_avFriends[FriendIndex].emplace_back(args: CurrentClient, args&: pEntry);
1484 const auto &&RemovalPredicate = [CurrentClient](const CFriendItem &Friend) {
1485 return (Friend.Name()[0] == '\0' || str_comp(a: Friend.Name(), b: CurrentClient.m_aName) == 0) && ((Friend.Name()[0] != '\0' && g_Config.m_ClFriendsIgnoreClan) || str_comp(a: Friend.Clan(), b: CurrentClient.m_aClan) == 0);
1486 };
1487 m_avFriends[FRIEND_OFF].erase(first: std::remove_if(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: RemovalPredicate), last: m_avFriends[FRIEND_OFF].end());
1488 }
1489 }
1490 for(auto &vFriends : m_avFriends)
1491 std::sort(first: vFriends.begin(), last: vFriends.end());
1492
1493 // friends list
1494 static CScrollRegion s_ScrollRegion;
1495 vec2 ScrollOffset(0.0f, 0.0f);
1496 CScrollRegionParams ScrollParams;
1497 ScrollParams.m_ScrollbarWidth = 16.0f;
1498 ScrollParams.m_ScrollbarMargin = 5.0f;
1499 ScrollParams.m_ScrollUnit = 80.0f;
1500 ScrollParams.m_Flags = CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
1501 s_ScrollRegion.Begin(pClipRect: &List, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
1502 List.y += ScrollOffset.y;
1503
1504 char aBuf[256];
1505 for(size_t FriendType = 0; FriendType < NUM_FRIEND_TYPES; ++FriendType)
1506 {
1507 // header
1508 CUIRect Header, GroupIcon, GroupLabel;
1509 List.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &List);
1510 s_ScrollRegion.AddRect(Rect: Header);
1511 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &s_aListExtended[FriendType] ? 0.4f : 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1512 Header.VSplitLeft(Cut: Header.h, pLeft: &GroupIcon, pRight: &GroupLabel);
1513 GroupIcon.Margin(Cut: 2.0f, pOtherRect: &GroupIcon);
1514 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1515 TextRender()->TextColor(Color: Ui()->HotItem() == &s_aListExtended[FriendType] ? TextRender()->DefaultTextColor() : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f));
1516 Ui()->DoLabel(pRect: &GroupIcon, pText: s_aListExtended[FriendType] ? FontIcon::SQUARE_MINUS : FontIcon::SQUARE_PLUS, Size: GroupIcon.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1517 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1518 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1519 switch(FriendType)
1520 {
1521 case FRIEND_PLAYER_ON:
1522 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Online friends (%d)"), (int)m_avFriends[FriendType].size());
1523 break;
1524 case FRIEND_CLAN_ON:
1525 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Online clanmates (%d)"), (int)m_avFriends[FriendType].size());
1526 break;
1527 case FRIEND_OFF:
1528 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Offline (%d)", pContext: "friends (server browser)"), (int)m_avFriends[FriendType].size());
1529 break;
1530 default:
1531 dbg_assert_failed("FriendType invalid");
1532 }
1533 Ui()->DoLabel(pRect: &GroupLabel, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
1534 if(Ui()->DoButtonLogic(pId: &s_aListExtended[FriendType], Checked: 0, pRect: &Header, Flags: BUTTONFLAG_LEFT))
1535 {
1536 s_aListExtended[FriendType] = !s_aListExtended[FriendType];
1537 }
1538
1539 // entries
1540 if(s_aListExtended[FriendType])
1541 {
1542 for(size_t FriendIndex = 0; FriendIndex < m_avFriends[FriendType].size(); ++FriendIndex)
1543 {
1544 // space
1545 {
1546 CUIRect Space;
1547 List.HSplitTop(Cut: SpacingH, pTop: &Space, pBottom: &List);
1548 s_ScrollRegion.AddRect(Rect: Space);
1549 }
1550
1551 CUIRect Rect;
1552 const auto &Friend = m_avFriends[FriendType][FriendIndex];
1553 List.HSplitTop(Cut: 11.0f + 10.0f + 2 * 2.0f + 1.0f + (Friend.ServerInfo() == nullptr ? 0.0f : 10.0f), pTop: &Rect, pBottom: &List);
1554 s_ScrollRegion.AddRect(Rect);
1555 if(s_ScrollRegion.RectClipped(Rect))
1556 continue;
1557
1558 const bool Inside = Ui()->HotItem() == Friend.ListItemId() || Ui()->HotItem() == Friend.RemoveButtonId() || Ui()->HotItem() == Friend.CommunityTooltipId() || Ui()->HotItem() == Friend.SkinTooltipId();
1559 int ButtonResult = Ui()->DoButtonLogic(pId: Friend.ListItemId(), Checked: 0, pRect: &Rect, Flags: BUTTONFLAG_LEFT);
1560
1561 if(Friend.ServerInfo())
1562 {
1563 GameClient()->m_Tooltips.DoToolTip(pId: Friend.ListItemId(), pNearRect: &Rect, pText: Localize(pStr: "Click to select server. Double click to join your friend."));
1564 }
1565
1566 // Compare unsorted server id of the friend with the unsorted id of the currently selected server
1567 bool InSelectedServer = m_SelectedIndex >= 0 && Friend.ServerInfo() && Friend.ServerInfo()->m_ServerIndex == ServerBrowser()->SortedGet(Index: m_SelectedIndex)->m_ServerIndex;
1568
1569 const ColorRGBA Color = PlayerBackgroundColor(Friend: FriendType == FRIEND_PLAYER_ON, Clan: FriendType == FRIEND_CLAN_ON, Afk: FriendType == FRIEND_OFF ? true : Friend.IsAfk(), InSelectedServer, Inside);
1570 Rect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1571 Rect.Margin(Cut: 2.0f, pOtherRect: &Rect);
1572
1573 CUIRect RemoveButton, NameLabel, ClanLabel, InfoLabel;
1574 Rect.HSplitTop(Cut: 16.0f, pTop: &RemoveButton, pBottom: nullptr);
1575 RemoveButton.VSplitRight(Cut: 13.0f, pLeft: nullptr, pRight: &RemoveButton);
1576 RemoveButton.HMargin(Cut: (RemoveButton.h - RemoveButton.w) / 2.0f, pOtherRect: &RemoveButton);
1577 Rect.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Rect);
1578
1579 if(Friend.ServerInfo())
1580 Rect.HSplitBottom(Cut: 10.0f, pTop: &Rect, pBottom: &InfoLabel);
1581 Rect.HSplitTop(Cut: 11.0f + 10.0f, pTop: &Rect, pBottom: nullptr);
1582
1583 // tee
1584 CUIRect Skin;
1585 Rect.VSplitLeft(Cut: Rect.h, pLeft: &Skin, pRight: &Rect);
1586 Rect.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Rect);
1587 if(Friend.Skin()[0] != '\0')
1588 {
1589 const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(Size: vec2(Skin.w, Skin.h), pSkinName: Friend.Skin(), CustomSkinColors: Friend.CustomSkinColors(), CustomSkinColorBody: Friend.CustomSkinColorBody(), CustomSkinColorFeet: Friend.CustomSkinColorFeet());
1590 const CAnimState *pIdleState = CAnimState::GetIdle();
1591 vec2 OffsetToMid;
1592 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1593 const vec2 TeeRenderPos = vec2(Skin.x + Skin.w / 2.0f, Skin.y + Skin.h * 0.55f + OffsetToMid.y);
1594 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: Friend.IsAfk() ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1595 Ui()->DoButtonLogic(pId: Friend.SkinTooltipId(), Checked: 0, pRect: &Skin, Flags: BUTTONFLAG_NONE);
1596 GameClient()->m_Tooltips.DoToolTip(pId: Friend.SkinTooltipId(), pNearRect: &Skin, pText: Friend.Skin());
1597 }
1598 else if(Friend.Skin7(Part: protocol7::SKINPART_BODY)[0] != '\0')
1599 {
1600 CTeeRenderInfo TeeInfo;
1601 TeeInfo.m_Size = minimum(a: Skin.w, b: Skin.h);
1602 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
1603 {
1604 GameClient()->m_Skins7.FindSkinPart(Part, pName: Friend.Skin7(Part), AllowSpecialPart: true)->ApplyTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy]);
1605 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy], UseCustomColors: Friend.UseCustomSkinColor7(Part), Value: Friend.CustomSkinColor7(Part), Part);
1606 }
1607 const CAnimState *pIdleState = CAnimState::GetIdle();
1608 vec2 OffsetToMid;
1609 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1610 const vec2 TeeRenderPos = vec2(Skin.x + Skin.w / 2.0f, Skin.y + Skin.h * 0.55f + OffsetToMid.y);
1611 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: Friend.IsAfk() ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1612 }
1613 Rect.HSplitTop(Cut: 11.0f, pTop: &NameLabel, pBottom: &ClanLabel);
1614
1615 // name
1616 Ui()->DoLabel(pRect: &NameLabel, pText: Friend.Name(), Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1617
1618 // clan
1619 Ui()->DoLabel(pRect: &ClanLabel, pText: Friend.Clan(), Size: FontSize - 2.0f, Align: TEXTALIGN_ML);
1620
1621 // server info
1622 if(Friend.ServerInfo())
1623 {
1624 // community icon
1625 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: Friend.ServerInfo()->m_aCommunityId);
1626 if(pCommunity != nullptr)
1627 {
1628 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
1629 if(pIcon != nullptr)
1630 {
1631 CUIRect CommunityIcon;
1632 InfoLabel.VSplitLeft(Cut: 21.0f, pLeft: &CommunityIcon, pRight: &InfoLabel);
1633 InfoLabel.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &InfoLabel);
1634 m_CommunityIcons.Render(pIcon, Rect: CommunityIcon, Active: true);
1635 Ui()->DoButtonLogic(pId: Friend.CommunityTooltipId(), Checked: 0, pRect: &CommunityIcon, Flags: BUTTONFLAG_NONE);
1636 GameClient()->m_Tooltips.DoToolTip(pId: Friend.CommunityTooltipId(), pNearRect: &CommunityIcon, pText: pCommunity->Name());
1637 }
1638 }
1639
1640 // server info text
1641 char aLatency[16];
1642 FormatServerbrowserPing(aBuffer&: aLatency, pInfo: Friend.ServerInfo());
1643 if(aLatency[0] != '\0')
1644 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s | %s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType, aLatency);
1645 else
1646 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType);
1647 Ui()->DoLabel(pRect: &InfoLabel, pText: aBuf, Size: FontSize - 2.0f, Align: TEXTALIGN_ML);
1648 }
1649
1650 // remove button
1651 if(Inside)
1652 {
1653 TextRender()->TextColor(Color: Ui()->HotItem() == Friend.RemoveButtonId() ? TextRender()->DefaultTextColor() : ColorRGBA(0.4f, 0.4f, 0.4f, 1.0f));
1654 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1655 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1656 Ui()->DoLabel(pRect: &RemoveButton, pText: FontIcon::TRASH, Size: RemoveButton.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1657 TextRender()->SetRenderFlags(0);
1658 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1659 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1660 if(Ui()->DoButtonLogic(pId: Friend.RemoveButtonId(), Checked: 0, pRect: &RemoveButton, Flags: BUTTONFLAG_LEFT))
1661 {
1662 m_pRemoveFriend = &Friend;
1663 ButtonResult = 0;
1664 }
1665 GameClient()->m_Tooltips.DoToolTip(pId: Friend.RemoveButtonId(), pNearRect: &RemoveButton, pText: Friend.FriendState() == IFriends::FRIEND_PLAYER ? Localize(pStr: "Click to remove this player from your friends list.") : Localize(pStr: "Click to remove this clan from your friends list."));
1666 }
1667
1668 // handle click and double click on item
1669 if(ButtonResult && Friend.ServerInfo())
1670 {
1671 str_copy(dst&: g_Config.m_UiServerAddress, src: Friend.ServerInfo()->m_aAddress);
1672 m_ServerBrowserShouldRevealSelection = true;
1673 if(ButtonResult == 1 && Ui()->DoDoubleClickLogic(pId: Friend.ListItemId()))
1674 {
1675 Connect(pAddress: g_Config.m_UiServerAddress);
1676 }
1677 }
1678 }
1679
1680 // Render empty description
1681 const char *pText = nullptr;
1682 if(FriendType == FRIEND_PLAYER_ON && !HasFriend)
1683 pText = Localize(pStr: "Add friends by entering their name below or by clicking their name in the player list.");
1684 else if(FriendType == FRIEND_CLAN_ON && !HasClan)
1685 pText = Localize(pStr: "Add clanmates by entering their clan below and leaving the name blank.");
1686 if(pText != nullptr)
1687 {
1688 const float DescriptionMargin = 2.0f;
1689 const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(Size: FontSize, pText, StrLength: -1, LineWidth: List.w - 2 * DescriptionMargin);
1690 CUIRect EmptyDescription;
1691 List.HSplitTop(Cut: BoundingBox.m_H + 2 * DescriptionMargin, pTop: &EmptyDescription, pBottom: &List);
1692 s_ScrollRegion.AddRect(Rect: EmptyDescription);
1693 EmptyDescription.Margin(Cut: DescriptionMargin, pOtherRect: &EmptyDescription);
1694 SLabelProperties DescriptionProps;
1695 DescriptionProps.m_MaxWidth = EmptyDescription.w;
1696 Ui()->DoLabel(pRect: &EmptyDescription, pText, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: DescriptionProps);
1697 }
1698 }
1699
1700 // space
1701 {
1702 CUIRect Space;
1703 List.HSplitTop(Cut: SpacingH, pTop: &Space, pBottom: &List);
1704 s_ScrollRegion.AddRect(Rect: Space);
1705 }
1706 }
1707 s_ScrollRegion.End();
1708
1709 if(m_pRemoveFriend != nullptr)
1710 {
1711 char aMessage[256];
1712 str_format(buffer: aMessage, buffer_size: sizeof(aMessage),
1713 format: m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? Localize(pStr: "Are you sure that you want to remove the player '%s' from your friends list?") : Localize(pStr: "Are you sure that you want to remove the clan '%s' from your friends list?"),
1714 m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? m_pRemoveFriend->Name() : m_pRemoveFriend->Clan());
1715 PopupConfirm(pTitle: Localize(pStr: "Remove friend"), pMessage: aMessage, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmRemoveFriend);
1716 }
1717
1718 // add friend
1719 if(GameClient()->Friends()->NumFriends() < IFriends::MAX_FRIENDS)
1720 {
1721 CUIRect Button;
1722 ServerFriends.Margin(Cut: 5.0f, pOtherRect: &ServerFriends);
1723
1724 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1725 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Name"));
1726 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: FontSize + 2.0f, Align: TEXTALIGN_ML);
1727 Button.VSplitLeft(Cut: 80.0f, pLeft: nullptr, pRight: &Button);
1728 static CLineInputBuffered<MAX_NAME_LENGTH> s_NameInput;
1729 Ui()->DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: FontSize + 2.0f);
1730
1731 ServerFriends.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &ServerFriends);
1732 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1733 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Clan"));
1734 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: FontSize + 2.0f, Align: TEXTALIGN_ML);
1735 Button.VSplitLeft(Cut: 80.0f, pLeft: nullptr, pRight: &Button);
1736 static CLineInputBuffered<MAX_CLAN_LENGTH> s_ClanInput;
1737 Ui()->DoEditBox(pLineInput: &s_ClanInput, pRect: &Button, FontSize: FontSize + 2.0f);
1738
1739 ServerFriends.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &ServerFriends);
1740 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1741 static CButtonContainer s_AddButton;
1742 if(DoButton_Menu(pButtonContainer: &s_AddButton, pText: s_NameInput.IsEmpty() && !s_ClanInput.IsEmpty() ? Localize(pStr: "Add clan") : Localize(pStr: "Add friend"), Checked: 0, pRect: &Button))
1743 {
1744 GameClient()->Friends()->AddFriend(pName: s_NameInput.GetString(), pClan: s_ClanInput.GetString());
1745 s_NameInput.Clear();
1746 s_ClanInput.Clear();
1747 FriendlistOnUpdate();
1748 Client()->ServerBrowserUpdate();
1749 }
1750 }
1751}
1752
1753void CMenus::FriendlistOnUpdate()
1754{
1755 // TODO: friends are currently updated every frame; optimize and only update friends when necessary
1756}
1757
1758void CMenus::PopupConfirmRemoveFriend()
1759{
1760 GameClient()->Friends()->RemoveFriend(pName: m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? m_pRemoveFriend->Name() : "", pClan: m_pRemoveFriend->Clan());
1761 FriendlistOnUpdate();
1762 Client()->ServerBrowserUpdate();
1763 m_pRemoveFriend = nullptr;
1764}
1765
1766enum
1767{
1768 UI_TOOLBOX_PAGE_FILTERS = 0,
1769 UI_TOOLBOX_PAGE_INFO,
1770 UI_TOOLBOX_PAGE_FRIENDS,
1771 NUM_UI_TOOLBOX_PAGES,
1772};
1773
1774void CMenus::RenderServerbrowserTabBar(CUIRect TabBar)
1775{
1776 CUIRect FilterTabButton, InfoTabButton, FriendsTabButton;
1777 TabBar.VSplitLeft(Cut: TabBar.w / 3.0f, pLeft: &FilterTabButton, pRight: &TabBar);
1778 TabBar.VSplitMid(pLeft: &InfoTabButton, pRight: &FriendsTabButton);
1779
1780 const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f);
1781 const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f);
1782
1783 if(!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_TAB))
1784 {
1785 const int Direction = Input()->ShiftIsPressed() ? -1 : 1;
1786 g_Config.m_UiToolboxPage = (g_Config.m_UiToolboxPage + NUM_UI_TOOLBOX_PAGES + Direction) % NUM_UI_TOOLBOX_PAGES;
1787 }
1788
1789 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1790 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1791
1792 static CButtonContainer s_FilterTabButton;
1793 if(DoButton_MenuTab(pButtonContainer: &s_FilterTabButton, pText: FontIcon::LIST_UL, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FILTERS, pRect: &FilterTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FILTER], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1794 {
1795 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FILTERS;
1796 }
1797 GameClient()->m_Tooltips.DoToolTip(pId: &s_FilterTabButton, pNearRect: &FilterTabButton, pText: Localize(pStr: "Server filter"));
1798
1799 static CButtonContainer s_InfoTabButton;
1800 if(DoButton_MenuTab(pButtonContainer: &s_InfoTabButton, pText: FontIcon::INFO, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_INFO, pRect: &InfoTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_INFO], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1801 {
1802 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_INFO;
1803 }
1804 GameClient()->m_Tooltips.DoToolTip(pId: &s_InfoTabButton, pNearRect: &InfoTabButton, pText: Localize(pStr: "Server info"));
1805
1806 static CButtonContainer s_FriendsTabButton;
1807 if(DoButton_MenuTab(pButtonContainer: &s_FriendsTabButton, pText: FontIcon::HEART, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FRIENDS, pRect: &FriendsTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FRIENDS], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1808 {
1809 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FRIENDS;
1810 }
1811 GameClient()->m_Tooltips.DoToolTip(pId: &s_FriendsTabButton, pNearRect: &FriendsTabButton, pText: Localize(pStr: "Friends"));
1812
1813 TextRender()->SetRenderFlags(0);
1814 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1815}
1816
1817void CMenus::RenderServerbrowserToolBox(CUIRect ToolBox)
1818{
1819 ToolBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_B, Rounding: 4.0f);
1820
1821 switch(g_Config.m_UiToolboxPage)
1822 {
1823 case UI_TOOLBOX_PAGE_FILTERS:
1824 RenderServerbrowserFilters(View: ToolBox);
1825 return;
1826 case UI_TOOLBOX_PAGE_INFO:
1827 RenderServerbrowserInfo(View: ToolBox);
1828 return;
1829 case UI_TOOLBOX_PAGE_FRIENDS:
1830 RenderServerbrowserFriends(View: ToolBox);
1831 return;
1832 default:
1833 dbg_assert_failed("ui_toolbox_page invalid");
1834 }
1835}
1836
1837void CMenus::RenderServerbrowser(CUIRect MainView)
1838{
1839 UpdateCommunityCache(Force: false);
1840
1841 switch(g_Config.m_UiPage)
1842 {
1843 case PAGE_INTERNET:
1844 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_INTERNET);
1845 break;
1846 case PAGE_LAN:
1847 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_LAN);
1848 if(m_ForceRefreshLanPage)
1849 {
1850 RefreshBrowserTab(Force: true);
1851 m_ForceRefreshLanPage = false;
1852 }
1853 break;
1854 case PAGE_FAVORITES:
1855 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_FAVORITES);
1856 break;
1857 case PAGE_FAVORITE_COMMUNITY_1:
1858 case PAGE_FAVORITE_COMMUNITY_2:
1859 case PAGE_FAVORITE_COMMUNITY_3:
1860 case PAGE_FAVORITE_COMMUNITY_4:
1861 case PAGE_FAVORITE_COMMUNITY_5:
1862 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + CMenuBackground::POS_BROWSER_CUSTOM0);
1863 break;
1864 default:
1865 dbg_assert_failed("ui_page invalid for RenderServerbrowser: %d", g_Config.m_UiPage);
1866 }
1867
1868 // clang-format off
1869 /*
1870 +---------------------------+ +---communities---+
1871 | | | |
1872 | | +------tabs-------+
1873 | server list | | |
1874 | | | tool |
1875 | | | box |
1876 +---------------------------+ | |
1877 status box +-----------------+
1878 */
1879 // clang-format on
1880
1881 CUIRect ServerList, StatusBox, ToolBox, TabBar;
1882 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1883 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1884 MainView.VSplitRight(Cut: 205.0f, pLeft: &ServerList, pRight: &ToolBox);
1885 ServerList.VSplitRight(Cut: 5.0f, pLeft: &ServerList, pRight: nullptr);
1886
1887 if(g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES)
1888 {
1889 CUIRect CommunityFilter;
1890 ToolBox.HSplitTop(Cut: 19.0f + 4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, pTop: &CommunityFilter, pBottom: &ToolBox);
1891 ToolBox.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ToolBox);
1892 RenderServerbrowserCommunitiesFilter(View: CommunityFilter);
1893 }
1894
1895 ToolBox.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &ToolBox);
1896 ServerList.HSplitBottom(Cut: 65.0f, pTop: &ServerList, pBottom: &StatusBox);
1897
1898 bool WasListboxItemActivated;
1899 RenderServerbrowserServerList(View: ServerList, WasListboxItemActivated);
1900 RenderServerbrowserStatusBox(StatusBox, WasListboxItemActivated);
1901
1902 RenderServerbrowserTabBar(TabBar);
1903 RenderServerbrowserToolBox(ToolBox);
1904}
1905
1906template<typename F>
1907bool CMenus::PrintHighlighted(const char *pName, F &&PrintFn)
1908{
1909 const char *pStr = g_Config.m_BrFilterString;
1910 char aFilterStr[sizeof(g_Config.m_BrFilterString)];
1911 char aFilterStrTrimmed[sizeof(g_Config.m_BrFilterString)];
1912 while((pStr = str_next_token(str: pStr, delim: IServerBrowser::SEARCH_EXCLUDE_TOKEN, buffer: aFilterStr, buffer_size: sizeof(aFilterStr))))
1913 {
1914 str_copy(dst&: aFilterStrTrimmed, src: str_utf8_skip_whitespaces(str: aFilterStr));
1915 str_utf8_trim_right(param: aFilterStrTrimmed);
1916 // highlight the parts that matches
1917 const char *pFilteredStr;
1918 int FilterLen = str_length(str: aFilterStrTrimmed);
1919 if(aFilterStrTrimmed[0] == '"' && aFilterStrTrimmed[FilterLen - 1] == '"')
1920 {
1921 aFilterStrTrimmed[FilterLen - 1] = '\0';
1922 pFilteredStr = str_comp(a: pName, b: &aFilterStrTrimmed[1]) == 0 ? pName : nullptr;
1923 FilterLen -= 2;
1924 }
1925 else
1926 {
1927 const char *pFilteredStrEnd;
1928 pFilteredStr = str_utf8_find_nocase(haystack: pName, needle: aFilterStrTrimmed, end: &pFilteredStrEnd);
1929 if(pFilteredStr != nullptr && pFilteredStrEnd != nullptr)
1930 FilterLen = pFilteredStrEnd - pFilteredStr;
1931 }
1932 if(pFilteredStr)
1933 {
1934 PrintFn(pFilteredStr, FilterLen);
1935 return true;
1936 }
1937 }
1938 return false;
1939}
1940
1941CTeeRenderInfo CMenus::GetTeeRenderInfo(vec2 Size, const char *pSkinName, bool CustomSkinColors, int CustomSkinColorBody, int CustomSkinColorFeet) const
1942{
1943 CTeeRenderInfo TeeInfo;
1944 TeeInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: pSkinName));
1945 TeeInfo.ApplyColors(CustomColoredSkin: CustomSkinColors, ColorBody: CustomSkinColorBody, ColorFeet: CustomSkinColorFeet);
1946 TeeInfo.m_Size = minimum(a: Size.x, b: Size.y);
1947 return TeeInfo;
1948}
1949
1950void CMenus::ConchainFriendlistUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1951{
1952 pfnCallback(pResult, pCallbackUserData);
1953 CMenus *pThis = ((CMenus *)pUserData);
1954 if(pResult->NumArguments() >= 1 && (pThis->Client()->State() == IClient::STATE_OFFLINE || pThis->Client()->State() == IClient::STATE_ONLINE))
1955 {
1956 pThis->FriendlistOnUpdate();
1957 pThis->Client()->ServerBrowserUpdate();
1958 }
1959}
1960
1961void CMenus::ConchainFavoritesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1962{
1963 pfnCallback(pResult, pCallbackUserData);
1964 if(pResult->NumArguments() >= 1 && g_Config.m_UiPage == PAGE_FAVORITES)
1965 ((CMenus *)pUserData)->ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITES);
1966}
1967
1968void CMenus::ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1969{
1970 pfnCallback(pResult, pCallbackUserData);
1971 CMenus *pThis = static_cast<CMenus *>(pUserData);
1972 if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES || (g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5)))
1973 {
1974 pThis->UpdateCommunityCache(Force: true);
1975 pThis->Client()->ServerBrowserUpdate();
1976 }
1977}
1978
1979void CMenus::ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1980{
1981 pfnCallback(pResult, pCallbackUserData);
1982 CMenus *pThis = static_cast<CMenus *>(pUserData);
1983 if(pResult->NumArguments() >= 1)
1984 {
1985 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
1986 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= pThis->ServerBrowser()->FavoriteCommunities().size())
1987 {
1988 // Reset page to internet when there is no favorite community for this page.
1989 g_Config.m_UiPage = PAGE_INTERNET;
1990 }
1991
1992 pThis->SetMenuPage(g_Config.m_UiPage);
1993 }
1994}
1995
1996void CMenus::UpdateCommunityCache(bool Force)
1997{
1998 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
1999 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size())
2000 {
2001 // Reset page to internet when there is no favorite community for this page,
2002 // i.e. when favorite community is removed via console while the page is open.
2003 // This also updates the community cache because the page is changed.
2004 SetMenuPage(PAGE_INTERNET);
2005 }
2006 else
2007 {
2008 ServerBrowser()->CommunityCache().Update(Force);
2009 }
2010}
2011