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