1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "menus.h"
4
5#include <base/str.h>
6
7#include <engine/font_icons.h>
8#include <engine/graphics.h>
9#include <engine/shared/config.h>
10#include <engine/storage.h>
11#include <engine/textrender.h>
12
13#include <game/client/animstate.h>
14#include <game/client/components/console.h>
15#include <game/client/components/emoticon.h>
16#include <game/client/components/skins.h>
17#include <game/client/components/tooltips.h>
18#include <game/client/gameclient.h>
19#include <game/client/skin.h>
20#include <game/client/ui.h>
21#include <game/client/ui_listbox.h>
22#include <game/localization.h>
23
24#include <algorithm>
25#include <vector>
26
27void CMenus::RenderSettingsTee(CUIRect MainView)
28{
29 CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo;
30 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
31 TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f);
32 TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab);
33 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
34
35 static CButtonContainer s_PlayerTabButton;
36 if(DoButton_MenuTab(pButtonContainer: &s_PlayerTabButton, pText: Localize(pStr: "Player"), Checked: !m_Dummy, pRect: &PlayerTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
37 {
38 m_Dummy = false;
39 m_SkinListScrollToSelected = true;
40 }
41
42 static CButtonContainer s_DummyTabButton;
43 if(DoButton_MenuTab(pButtonContainer: &s_DummyTabButton, pText: Localize(pStr: "Dummy"), Checked: m_Dummy, pRect: &DummyTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
44 {
45 m_Dummy = true;
46 m_SkinListScrollToSelected = true;
47 }
48
49 if(Client()->State() == IClient::STATE_ONLINE &&
50 GameClient()->m_aNextChangeInfo[m_Dummy] > Client()->GameTick(Conn: m_Dummy))
51 {
52 char aChangeInfo[128], aTimeLeft[32];
53 str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left"), (GameClient()->m_aNextChangeInfo[m_Dummy] - Client()->GameTick(Conn: m_Dummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed());
54 str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s", Localize(pStr: "Player info change cooldown"), aTimeLeft);
55 Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML);
56 }
57
58 if(g_Config.m_Debug)
59 {
60 const CSkins::CSkinLoadingStats Stats = GameClient()->m_Skins.LoadingStats();
61 char aStats[256];
62 str_format(buffer: aStats, buffer_size: sizeof(aStats), format: "unloaded: %" PRIzu ", pending: %" PRIzu ", loading: %" PRIzu ",\nloaded: %" PRIzu ", error: %" PRIzu ", notfound: %" PRIzu,
63 Stats.m_NumUnloaded, Stats.m_NumPending, Stats.m_NumLoading, Stats.m_NumLoaded, Stats.m_NumError, Stats.m_NumNotFound);
64 Ui()->DoLabel(pRect: &ChangeInfo, pText: aStats, Size: 9.0f, Align: TEXTALIGN_MR);
65 }
66
67 char *pSkinName;
68 size_t SkinNameSize;
69 int *pUseCustomColor;
70 unsigned *pColorBody;
71 unsigned *pColorFeet;
72 int *pEmote;
73 if(!m_Dummy)
74 {
75 pSkinName = g_Config.m_ClPlayerSkin;
76 SkinNameSize = sizeof(g_Config.m_ClPlayerSkin);
77 pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor;
78 pColorBody = &g_Config.m_ClPlayerColorBody;
79 pColorFeet = &g_Config.m_ClPlayerColorFeet;
80 pEmote = &g_Config.m_ClPlayerDefaultEyes;
81 }
82 else
83 {
84 pSkinName = g_Config.m_ClDummySkin;
85 SkinNameSize = sizeof(g_Config.m_ClDummySkin);
86 pUseCustomColor = &g_Config.m_ClDummyUseCustomColor;
87 pColorBody = &g_Config.m_ClDummyColorBody;
88 pColorFeet = &g_Config.m_ClDummyColorFeet;
89 pEmote = &g_Config.m_ClDummyDefaultEyes;
90 }
91
92 const float EyeButtonSize = 40.0f;
93 const bool RenderEyesBelow = MainView.w < 750.0f;
94 CUIRect YourSkin, Checkboxes, SkinPrefix, Eyes, Button, Label;
95 MainView.HSplitTop(Cut: 90.0f, pTop: &YourSkin, pBottom: &MainView);
96 if(RenderEyesBelow)
97 {
98 YourSkin.VSplitLeft(Cut: MainView.w * 0.45f, pLeft: &YourSkin, pRight: &Checkboxes);
99 Checkboxes.VSplitLeft(Cut: MainView.w * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
100 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
101 MainView.HSplitTop(Cut: EyeButtonSize, pTop: &Eyes, pBottom: &MainView);
102 Eyes.VSplitRight(Cut: EyeButtonSize * (float)NUM_EMOTES + 5.0f * (float)(NUM_EMOTES - 1), pLeft: nullptr, pRight: &Eyes);
103 }
104 else
105 {
106 YourSkin.VSplitRight(Cut: 3 * EyeButtonSize + 2 * 5.0f, pLeft: &YourSkin, pRight: &Eyes);
107 const float RemainderWidth = YourSkin.w;
108 YourSkin.VSplitLeft(Cut: RemainderWidth * 0.4f, pLeft: &YourSkin, pRight: &Checkboxes);
109 Checkboxes.VSplitLeft(Cut: RemainderWidth * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
110 SkinPrefix.VSplitRight(Cut: 20.0f, pLeft: &SkinPrefix, pRight: nullptr);
111 }
112 YourSkin.VSplitRight(Cut: 20.0f, pLeft: &YourSkin, pRight: nullptr);
113 Checkboxes.VSplitRight(Cut: 20.0f, pLeft: &Checkboxes, pRight: nullptr);
114
115 // Checkboxes
116 bool ShouldRefresh = false;
117 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
118 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadSkins, pText: Localize(pStr: "Download skins"), Checked: g_Config.m_ClDownloadSkins, pRect: &Button))
119 {
120 g_Config.m_ClDownloadSkins ^= 1;
121 ShouldRefresh = true;
122 }
123
124 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
125 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadCommunitySkins, pText: Localize(pStr: "Download community skins"), Checked: g_Config.m_ClDownloadCommunitySkins, pRect: &Button))
126 {
127 g_Config.m_ClDownloadCommunitySkins ^= 1;
128 ShouldRefresh = true;
129 }
130
131 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
132 if(DoButton_CheckBox(pId: &g_Config.m_ClVanillaSkinsOnly, pText: Localize(pStr: "Vanilla skins only"), Checked: g_Config.m_ClVanillaSkinsOnly, pRect: &Button))
133 {
134 g_Config.m_ClVanillaSkinsOnly ^= 1;
135 ShouldRefresh = true;
136 }
137
138 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
139 if(DoButton_CheckBox(pId: &g_Config.m_ClFatSkins, pText: Localize(pStr: "Fat skins (DDFat)"), Checked: g_Config.m_ClFatSkins, pRect: &Button))
140 {
141 g_Config.m_ClFatSkins ^= 1;
142 }
143
144 // Skin prefix
145 {
146 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &SkinPrefix);
147 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Skin prefix"), Size: 14.0f, Align: TEXTALIGN_ML);
148
149 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
150 static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix));
151 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinPrefixInput, pRect: &Button, FontSize: 14.0f))
152 {
153 ShouldRefresh = true;
154 }
155
156 SkinPrefix.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &SkinPrefix);
157
158 static const char *s_apSkinPrefixes[] = {"kitty", "santa"};
159 static CButtonContainer s_aPrefixButtons[std::size(s_apSkinPrefixes)];
160 for(size_t i = 0; i < std::size(s_apSkinPrefixes); i++)
161 {
162 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
163 Button.HMargin(Cut: 2.0f, pOtherRect: &Button);
164 if(DoButton_Menu(pButtonContainer: &s_aPrefixButtons[i], pText: s_apSkinPrefixes[i], Checked: 0, pRect: &Button))
165 {
166 str_copy(dst&: g_Config.m_ClSkinPrefix, src: s_apSkinPrefixes[i]);
167 ShouldRefresh = true;
168 }
169 }
170 }
171
172 // Player skin area
173 CUIRect CustomColorsButton, RandomSkinButton;
174 YourSkin.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &YourSkin);
175 YourSkin.HSplitBottom(Cut: 20.0f, pTop: &YourSkin, pBottom: &CustomColorsButton);
176 CustomColorsButton.VSplitRight(Cut: 30.0f, pLeft: &CustomColorsButton, pRight: &RandomSkinButton);
177 CustomColorsButton.VSplitRight(Cut: 20.0f, pLeft: &CustomColorsButton, pRight: nullptr);
178 YourSkin.VSplitLeft(Cut: 65.0f, pLeft: &YourSkin, pRight: &Button);
179 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
180 Button.HMargin(Cut: (Button.h - 20.0f) / 2.0f, pOtherRect: &Button);
181
182 char aBuf[128 + IO_MAX_PATH_LENGTH];
183 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Your skin"));
184 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
185
186 CSkins::CSkinList &SkinList = GameClient()->m_Skins.SkinList();
187 const CSkin *pDefaultSkin = GameClient()->m_Skins.Find(pName: "default");
188 const CSkins::CSkinContainer *pOwnSkinContainer = GameClient()->m_Skins.FindContainerOrNullptr(pName: pSkinName[0] == '\0' ? "default" : pSkinName);
189 if(pOwnSkinContainer != nullptr && pOwnSkinContainer->IsSpecial())
190 {
191 pOwnSkinContainer = nullptr; // Special skins cannot be selected, show as missing due to invalid name
192 }
193
194 CTeeRenderInfo OwnSkinInfo;
195 OwnSkinInfo.Apply(pSkin: pOwnSkinContainer == nullptr || pOwnSkinContainer->Skin() == nullptr ? pDefaultSkin : pOwnSkinContainer->Skin().get());
196 OwnSkinInfo.ApplyColors(CustomColoredSkin: *pUseCustomColor, ColorBody: *pColorBody, ColorFeet: *pColorFeet);
197 OwnSkinInfo.m_Size = 50.0f;
198
199 // Tee
200 {
201 vec2 OffsetToMid;
202 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, TeeOffsetToMid&: OffsetToMid);
203 const vec2 TeeRenderPos = vec2(YourSkin.x + YourSkin.w / 2.0f, YourSkin.y + YourSkin.h / 2.0f + OffsetToMid.y);
204 // tee looking towards cursor, and it is happy when you touch it
205 const vec2 DeltaPosition = Ui()->MousePos() - TeeRenderPos;
206 const float Distance = length(a: DeltaPosition);
207 const float InteractionDistance = 20.0f;
208 const vec2 TeeDirection = Distance < InteractionDistance ? normalize(v: vec2(DeltaPosition.x, std::max(a: DeltaPosition.y, b: 0.5f))) : normalize(v: DeltaPosition);
209 const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : *pEmote;
210 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: TeeEmote, Dir: TeeDirection, Pos: TeeRenderPos);
211 }
212
213 // Skin loading status
214 const auto &&RenderSkinStatus = [&](CUIRect Parent, const CSkins::CSkinContainer *pSkinContainer, const void *pStatusTooltipId) {
215 if(pSkinContainer != nullptr && pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED)
216 {
217 return;
218 }
219
220 CUIRect StatusIcon;
221 Parent.HSplitTop(Cut: 20.0f, pTop: &StatusIcon, pBottom: nullptr);
222 StatusIcon.VSplitLeft(Cut: 20.0f, pLeft: &StatusIcon, pRight: nullptr);
223
224 if(pSkinContainer != nullptr &&
225 (pSkinContainer->State() == CSkins::CSkinContainer::EState::UNLOADED ||
226 pSkinContainer->State() == CSkins::CSkinContainer::EState::PENDING ||
227 pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADING))
228 {
229 Ui()->RenderProgressSpinner(Center: StatusIcon.Center(), OuterRadius: 5.0f);
230 }
231 else
232 {
233 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
234 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
235 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
236 Ui()->DoLabel(pRect: &StatusIcon, pText: pSkinContainer == nullptr || pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR ? FontIcon::TRIANGLE_EXCLAMATION : FontIcon::QUESTION, Size: 12.0f, Align: TEXTALIGN_MC);
237 TextRender()->SetRenderFlags(0);
238 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
239 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
240 Ui()->DoButtonLogic(pId: pStatusTooltipId, Checked: 0, pRect: &StatusIcon, Flags: BUTTONFLAG_NONE);
241 const char *pErrorTooltip;
242 if(pSkinContainer == nullptr)
243 {
244 pErrorTooltip = Localize(pStr: "This skin name cannot be used.");
245 }
246 else if(pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR)
247 {
248 pErrorTooltip = Localize(pStr: "Skin could not be loaded due to an error. Check the local console for details.");
249 }
250 else
251 {
252 pErrorTooltip = Localize(pStr: "Skin could not be found.");
253 }
254 GameClient()->m_Tooltips.DoToolTip(pId: pStatusTooltipId, pNearRect: &StatusIcon, pText: pErrorTooltip);
255 }
256 };
257 static char s_StatusTooltipId;
258 RenderSkinStatus(YourSkin, pOwnSkinContainer, &s_StatusTooltipId);
259
260 // Skin name
261 static CLineInput s_SkinInput;
262 s_SkinInput.SetBuffer(pStr: pSkinName, MaxSize: SkinNameSize);
263 s_SkinInput.SetEmptyText("default");
264 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinInput, pRect: &Button, FontSize: 14.0f))
265 {
266 SetNeedSendInfo();
267 m_SkinListScrollToSelected = true;
268 SkinList.ForceRefresh();
269 }
270
271 // Random skin button
272 static CButtonContainer s_RandomSkinButton;
273 static const char *s_apDice[] = {FontIcon::DICE_ONE, FontIcon::DICE_TWO, FontIcon::DICE_THREE, FontIcon::DICE_FOUR, FontIcon::DICE_FIVE, FontIcon::DICE_SIX};
274 static int s_CurrentDie = rand() % std::size(s_apDice);
275 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
276 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
277 if(DoButton_Menu(pButtonContainer: &s_RandomSkinButton, pText: s_apDice[s_CurrentDie], Checked: 0, pRect: &RandomSkinButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: -0.2f))
278 {
279 GameClient()->m_Skins.RandomizeSkin(Dummy: m_Dummy);
280 SetNeedSendInfo();
281 m_SkinListScrollToSelected = true;
282 s_CurrentDie = rand() % std::size(s_apDice);
283 }
284 TextRender()->SetRenderFlags(0);
285 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
286 GameClient()->m_Tooltips.DoToolTip(pId: &s_RandomSkinButton, pNearRect: &RandomSkinButton, pText: Localize(pStr: "Create a random skin"));
287
288 // Custom colors button
289 if(DoButton_CheckBox(pId: pUseCustomColor, pText: Localize(pStr: "Custom colors"), Checked: *pUseCustomColor, pRect: &CustomColorsButton))
290 {
291 *pUseCustomColor = *pUseCustomColor ? 0 : 1;
292 SetNeedSendInfo();
293 }
294
295 // Default eyes
296 {
297 CTeeRenderInfo EyeSkinInfo = OwnSkinInfo;
298 EyeSkinInfo.m_Size = EyeButtonSize;
299 vec2 OffsetToMid;
300 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, TeeOffsetToMid&: OffsetToMid);
301
302 CUIRect EyesRow;
303 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
304 static CButtonContainer s_aEyeButtons[NUM_EMOTES];
305 for(int CurrentEyeEmote = 0; CurrentEyeEmote < NUM_EMOTES; CurrentEyeEmote++)
306 {
307 EyesRow.VSplitLeft(Cut: EyeButtonSize, pLeft: &Button, pRight: &EyesRow);
308 EyesRow.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &EyesRow);
309 if(!RenderEyesBelow && (CurrentEyeEmote + 1) % 3 == 0)
310 {
311 Eyes.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Eyes);
312 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
313 }
314
315 const ColorRGBA EyeButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + (*pEmote == CurrentEyeEmote ? 0.25f : 0.0f));
316 if(DoButton_Menu(pButtonContainer: &s_aEyeButtons[CurrentEyeEmote], pText: "", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: EyeButtonColor))
317 {
318 *pEmote = CurrentEyeEmote;
319 if((int)m_Dummy == g_Config.m_ClDummy)
320 GameClient()->m_Emoticon.EyeEmote(EyeEmote: CurrentEyeEmote);
321 }
322 GameClient()->m_Tooltips.DoToolTip(pId: &s_aEyeButtons[CurrentEyeEmote], pNearRect: &Button, pText: Localize(pStr: "Choose default eyes when joining a server"));
323 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, Emote: CurrentEyeEmote, Dir: vec2(1.0f, 0.0f), Pos: vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2.0f + OffsetToMid.y));
324 }
325 }
326
327 // Custom color pickers
328 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
329 if(*pUseCustomColor)
330 {
331 CUIRect CustomColors;
332 MainView.HSplitTop(Cut: 95.0f, pTop: &CustomColors, pBottom: &MainView);
333 CUIRect aRects[2];
334 CustomColors.VSplitMid(pLeft: &aRects[0], pRight: &aRects[1], Spacing: 20.0f);
335
336 unsigned *apColors[] = {pColorBody, pColorFeet};
337 const char *apParts[] = {Localize(pStr: "Body"), Localize(pStr: "Feet")};
338
339 for(int i = 0; i < 2; i++)
340 {
341 aRects[i].HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &aRects[i]);
342 Ui()->DoLabel(pRect: &Label, pText: apParts[i], Size: 14.0f, Align: TEXTALIGN_ML);
343 if(RenderHslaScrollbars(pRect: &aRects[i], pColor: apColors[i], Alpha: false, DarkestLight: ColorHSLA::DARKEST_LGT))
344 {
345 SetNeedSendInfo();
346 }
347 }
348 }
349 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
350
351 // Layout bottom controls and use remainder for skin selector
352 CUIRect QuickSearch, DatabaseButton, DirectoryButton, RefreshButton;
353 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &QuickSearch);
354 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
355 QuickSearch.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: &DatabaseButton);
356 DatabaseButton.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &DatabaseButton);
357 DatabaseButton.VSplitLeft(Cut: 150.0f, pLeft: &DatabaseButton, pRight: &DirectoryButton);
358 DirectoryButton.VSplitRight(Cut: 175.0f, pLeft: nullptr, pRight: &DirectoryButton);
359 DirectoryButton.VSplitRight(Cut: 25.0f, pLeft: &DirectoryButton, pRight: &RefreshButton);
360 DirectoryButton.VSplitRight(Cut: 10.0f, pLeft: &DirectoryButton, pRight: nullptr);
361
362 // Skin selector
363 static CListBox s_ListBox;
364 std::vector<CSkins::CSkinListEntry> &vSkinList = SkinList.Skins();
365 int OldSelected = -1;
366 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: vSkinList.size(), ItemsPerRow: 4, RowsPerScroll: 2, SelectedIndex: OldSelected, pRect: &MainView);
367 for(size_t i = 0; i < vSkinList.size(); ++i)
368 {
369 CSkins::CSkinListEntry &SkinListEntry = vSkinList[i];
370 const CSkins::CSkinContainer *pSkinContainer = vSkinList[i].SkinContainer();
371
372 if(!m_Dummy ? SkinListEntry.IsSelectedMain() : SkinListEntry.IsSelectedDummy())
373 {
374 OldSelected = i;
375 if(m_SkinListScrollToSelected)
376 {
377 s_ListBox.ScrollToSelected();
378 m_SkinListScrollToSelected = false;
379 }
380 }
381
382 const CListboxItem Item = s_ListBox.DoNextItem(pId: SkinListEntry.ListItemId(), Selected: OldSelected >= 0 && (size_t)OldSelected == i);
383 if(!Item.m_Visible)
384 {
385 continue;
386 }
387
388 SkinListEntry.RequestLoad();
389 const CSkin *pSkin = pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED ? pSkinContainer->Skin().get() : pDefaultSkin;
390
391 Item.m_Rect.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &Label);
392
393 {
394 CTeeRenderInfo Info = OwnSkinInfo;
395 Info.Apply(pSkin);
396 vec2 OffsetToMid;
397 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid);
398 const vec2 TeeRenderPos = vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2 + OffsetToMid.y);
399 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: *pEmote, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
400 }
401
402 {
403 SLabelProperties Props;
404 Props.m_MaxWidth = Label.w - 5.0f;
405 const auto &NameMatch = SkinListEntry.NameMatch();
406 if(NameMatch.has_value())
407 {
408 const auto [MatchStart, MatchLength] = NameMatch.value();
409 Props.m_vColorSplits.emplace_back(args: MatchStart, args: MatchLength, args: ColorRGBA(0.4f, 0.4f, 1.0f, 1.0f));
410 }
411 Ui()->DoLabel(pRect: &Label, pText: pSkinContainer->Name(), Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
412 }
413
414 if(g_Config.m_Debug)
415 {
416 Graphics()->TextureClear();
417 Graphics()->QuadsBegin();
418 Graphics()->SetColor(*pUseCustomColor ? color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorBody).UnclampLighting(Darkest: ColorHSLA::DARKEST_LGT)) : pSkin->m_BloodColor);
419 IGraphics::CQuadItem QuadItem(Label.x, Label.y, 12.0f, 12.0f);
420 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
421 Graphics()->QuadsEnd();
422 }
423
424 // render skin favorite icon
425 {
426 CUIRect FavIcon;
427 Item.m_Rect.HSplitTop(Cut: 20.0f, pTop: &FavIcon, pBottom: nullptr);
428 FavIcon.VSplitRight(Cut: 20.0f, pLeft: nullptr, pRight: &FavIcon);
429 if(DoButton_Favorite(pButtonId: SkinListEntry.FavoriteButtonId(), pParentId: SkinListEntry.ListItemId(), Checked: SkinListEntry.IsFavorite(), pRect: &FavIcon))
430 {
431 if(SkinListEntry.IsFavorite())
432 {
433 GameClient()->m_Skins.RemoveFavorite(pName: pSkinContainer->Name());
434 }
435 else
436 {
437 GameClient()->m_Skins.AddFavorite(pName: pSkinContainer->Name());
438 }
439 }
440 }
441
442 RenderSkinStatus(Item.m_Rect, pSkinContainer, SkinListEntry.ErrorTooltipId());
443 }
444
445 const int NewSelected = s_ListBox.DoEnd();
446 if(OldSelected != NewSelected)
447 {
448 str_copy(dst: pSkinName, src: vSkinList[NewSelected].SkinContainer()->Name(), dst_size: SkinNameSize);
449 SkinList.ForceRefresh();
450 SetNeedSendInfo();
451 }
452
453 static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString));
454 if(SkinList.UnfilteredCount() > 0 && vSkinList.empty())
455 {
456 CUIRect FilterLabel, ResetButton;
457 MainView.HMargin(Cut: (MainView.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &FilterLabel);
458 FilterLabel.HSplitTop(Cut: 16.0f, pTop: &FilterLabel, pBottom: &ResetButton);
459 ResetButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ResetButton);
460 ResetButton.VMargin(Cut: (ResetButton.w - 200.0f) / 2.0f, pOtherRect: &ResetButton);
461 Ui()->DoLabel(pRect: &FilterLabel, pText: Localize(pStr: "No skins match your filter criteria"), Size: 16.0f, Align: TEXTALIGN_MC);
462 static CButtonContainer s_ResetButton;
463 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
464 {
465 s_SkinFilterInput.Clear();
466 SkinList.ForceRefresh();
467 }
468 }
469
470 if(Ui()->DoEditBox_Search(pLineInput: &s_SkinFilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
471 {
472 SkinList.ForceRefresh();
473 }
474
475 static CButtonContainer s_SkinDatabaseButton;
476 if(DoButton_Menu(pButtonContainer: &s_SkinDatabaseButton, pText: Localize(pStr: "Skin Database"), Checked: 0, pRect: &DatabaseButton))
477 {
478 Client()->ViewLink(pLink: "https://ddnet.org/skins/");
479 }
480
481 static CButtonContainer s_DirectoryButton;
482 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Skins directory"), Checked: 0, pRect: &DirectoryButton))
483 {
484 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "skins", pBuffer: aBuf, BufferSize: sizeof(aBuf));
485 Storage()->CreateFolder(pFoldername: "skins", Type: IStorage::TYPE_SAVE);
486 Client()->ViewFile(pFilename: aBuf);
487 }
488 GameClient()->m_Tooltips.DoToolTip(pId: &s_DirectoryButton, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom skins"));
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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
492 static CButtonContainer s_SkinRefreshButton;
493 if(DoButton_Menu(pButtonContainer: &s_SkinRefreshButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
494 {
495 ShouldRefresh = true;
496 }
497 TextRender()->SetRenderFlags(0);
498 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
499
500 if(ShouldRefresh)
501 {
502 GameClient()->RefreshSkins(SkinDescriptorFlags: CSkinDescriptor::FLAG_SIX);
503 }
504}
505