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