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