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_settings_controls.h"
4
5#include <base/math.h>
6#include <base/str.h>
7
8#include <engine/font_icons.h>
9#include <engine/graphics.h>
10#include <engine/shared/config.h>
11#include <engine/shared/localization.h>
12#include <engine/textrender.h>
13
14#include <game/client/components/binds.h>
15#include <game/client/components/key_binder.h>
16#include <game/client/components/menus.h>
17#include <game/client/gameclient.h>
18#include <game/client/ui.h>
19#include <game/client/ui_scrollregion.h>
20#include <game/localization.h>
21
22#include <functional>
23#include <string>
24#include <vector>
25
26inline constexpr float HEADER_FONT_SIZE = 16.0f;
27inline constexpr float FONT_SIZE = 13.0f;
28inline constexpr float MARGIN = 10.0f;
29inline constexpr float BUTTON_HEIGHT = 20.0f;
30inline constexpr float BUTTON_SPACING = 2.0f;
31inline constexpr float BIND_OPTION_SPACING = 4.0f;
32
33bool CBindSlotUiElement::operator<(const CBindSlotUiElement &Other) const
34{
35 if(m_Bind == EMPTY_BIND_SLOT)
36 {
37 return false;
38 }
39 if(Other.m_Bind == EMPTY_BIND_SLOT)
40 {
41 return true;
42 }
43 return m_Bind.m_ModifierMask < Other.m_Bind.m_ModifierMask ||
44 m_Bind.m_Key < Other.m_Bind.m_Key;
45}
46
47std::vector<CBindSlotUiElement>::iterator CBindOption::GetBindSlotElement(const CBindSlot &BindSlot)
48{
49 return std::find_if(first: m_vCurrentBinds.begin(), last: m_vCurrentBinds.end(), pred: [&](const CBindSlotUiElement &BindSlotUiElement) {
50 return BindSlotUiElement.m_Bind == BindSlot;
51 });
52}
53
54bool CBindOption::MatchesSearch(const char *pSearch) const
55{
56 return (m_Group != EBindOptionGroup::CUSTOM && str_utf8_find_nocase(haystack: Localize(pStr: m_pLabel), needle: pSearch) != nullptr) ||
57 str_utf8_find_nocase(haystack: m_Command.c_str(), needle: pSearch) != nullptr;
58}
59
60void CMenusSettingsControls::OnInterfacesInit(CGameClient *pClient)
61{
62 CComponentInterfaces::OnInterfacesInit(pClient);
63
64 m_vBindOptions = {
65 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Move left"), .m_Command: "+left"},
66 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Move right"), .m_Command: "+right"},
67 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Jump"), .m_Command: "+jump"},
68 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Fire"), .m_Command: "+fire"},
69 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Hook"), .m_Command: "+hook"},
70 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Hook collisions"), .m_Command: "+showhookcoll"},
71 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Pause"), .m_Command: "say /pause"},
72 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Kill"), .m_Command: "kill"},
73 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Zoom in"), .m_Command: "zoom+"},
74 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Zoom out"), .m_Command: "zoom-"},
75 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Default zoom"), .m_Command: "zoom"},
76 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Show others"), .m_Command: "say /showothers"},
77 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Show all"), .m_Command: "say /showall"},
78 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Toggle dyncam"), .m_Command: "toggle cl_dyncam 0 1"},
79 {.m_Group: EBindOptionGroup::MOVEMENT, .m_pLabel: Localizable(pStr: "Toggle ghost"), .m_Command: "toggle cl_race_show_ghost 0 1"},
80 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Hammer"), .m_Command: "+weapon1"},
81 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Pistol"), .m_Command: "+weapon2"},
82 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Shotgun"), .m_Command: "+weapon3"},
83 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Grenade"), .m_Command: "+weapon4"},
84 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Laser"), .m_Command: "+weapon5"},
85 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Next weapon"), .m_Command: "+nextweapon"},
86 {.m_Group: EBindOptionGroup::WEAPON, .m_pLabel: Localizable(pStr: "Prev. weapon"), .m_Command: "+prevweapon"},
87 {.m_Group: EBindOptionGroup::VOTING, .m_pLabel: Localizable(pStr: "Vote yes"), .m_Command: "vote yes"},
88 {.m_Group: EBindOptionGroup::VOTING, .m_pLabel: Localizable(pStr: "Vote no"), .m_Command: "vote no"},
89 {.m_Group: EBindOptionGroup::CHAT, .m_pLabel: Localizable(pStr: "Chat"), .m_Command: "+show_chat; chat all"},
90 {.m_Group: EBindOptionGroup::CHAT, .m_pLabel: Localizable(pStr: "Team chat"), .m_Command: "+show_chat; chat team"},
91 {.m_Group: EBindOptionGroup::CHAT, .m_pLabel: Localizable(pStr: "Converse"), .m_Command: "+show_chat; chat all /c "},
92 {.m_Group: EBindOptionGroup::CHAT, .m_pLabel: Localizable(pStr: "Chat command"), .m_Command: "+show_chat; chat all /"},
93 {.m_Group: EBindOptionGroup::CHAT, .m_pLabel: Localizable(pStr: "Show chat"), .m_Command: "+show_chat"},
94 {.m_Group: EBindOptionGroup::DUMMY, .m_pLabel: Localizable(pStr: "Toggle dummy"), .m_Command: "toggle cl_dummy 0 1"},
95 {.m_Group: EBindOptionGroup::DUMMY, .m_pLabel: Localizable(pStr: "Dummy copy"), .m_Command: "toggle cl_dummy_copy_moves 0 1"},
96 {.m_Group: EBindOptionGroup::DUMMY, .m_pLabel: Localizable(pStr: "Hammerfly dummy"), .m_Command: "toggle cl_dummy_hammer 0 1"},
97 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Emoticon"), .m_Command: "+emote"},
98 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Spectator mode"), .m_Command: "+spectate"},
99 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Spectate next"), .m_Command: "spectate_next"},
100 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Spectate previous"), .m_Command: "spectate_previous"},
101 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Console"), .m_Command: "toggle_local_console"},
102 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Remote console"), .m_Command: "toggle_remote_console"},
103 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Screenshot"), .m_Command: "screenshot"},
104 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Scoreboard"), .m_Command: "+scoreboard"},
105 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Scoreboard cursor"), .m_Command: "toggle_scoreboard_cursor"},
106 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Statboard"), .m_Command: "+statboard"},
107 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Lock team"), .m_Command: "say /lock"},
108 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Show entities"), .m_Command: "toggle cl_overlay_entities 0 100"},
109 {.m_Group: EBindOptionGroup::MISCELLANEOUS, .m_pLabel: Localizable(pStr: "Show HUD"), .m_Command: "toggle cl_showhud 0 1"},
110 };
111 m_NumPredefinedBindOptions = m_vBindOptions.size();
112
113 std::fill(first: std::begin(arr&: m_aBindGroupExpanded), last: std::end(arr&: m_aBindGroupExpanded), value: true);
114 m_aBindGroupExpanded[(int)EBindOptionGroup::CUSTOM] = false;
115
116 m_JoystickDropDownState.m_SelectionPopupContext.m_pScrollRegion = &m_JoystickDropDownScrollRegion;
117}
118
119void CMenusSettingsControls::Render(CUIRect MainView)
120{
121 UpdateBindOptions();
122
123 CUIRect QuickSearch, SearchMatches, ResetToDefault;
124 MainView.HSplitBottom(Cut: BUTTON_HEIGHT, pTop: &MainView, pBottom: &QuickSearch);
125 QuickSearch.VSplitRight(Cut: 200.0f, pLeft: &QuickSearch, pRight: &ResetToDefault);
126 QuickSearch.VSplitRight(Cut: MARGIN, pLeft: &QuickSearch, pRight: nullptr);
127 QuickSearch.VSplitRight(Cut: 150.0f, pLeft: &QuickSearch, pRight: &SearchMatches);
128 QuickSearch.VSplitRight(Cut: MARGIN, pLeft: &QuickSearch, pRight: nullptr);
129 MainView.HSplitBottom(Cut: MARGIN, pTop: &MainView, pBottom: nullptr);
130
131 // Quick search
132 if(Ui()->DoEditBox_Search(pLineInput: &m_FilterInput, pRect: &QuickSearch, FontSize: FONT_SIZE, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive() && !GameClient()->m_KeyBinder.IsActive()))
133 {
134 m_CurrentSearchMatch = 0;
135 UpdateSearchMatches();
136 m_SearchMatchReveal = true;
137 }
138 else if(!m_vSearchMatches.empty() && (Ui()->ConsumeHotkey(Hotkey: CUi::EHotkey::HOTKEY_ENTER) || Ui()->ConsumeHotkey(Hotkey: CUi::EHotkey::HOTKEY_TAB)))
139 {
140 UpdateSearchMatches();
141 m_CurrentSearchMatch += Input()->ShiftIsPressed() ? -1 : 1;
142 if(m_CurrentSearchMatch >= (int)m_vSearchMatches.size())
143 {
144 m_CurrentSearchMatch = 0;
145 }
146 if(m_CurrentSearchMatch < 0)
147 {
148 m_CurrentSearchMatch = m_vSearchMatches.size() - 1;
149 }
150 m_SearchMatchReveal = true;
151 }
152
153 if(!m_FilterInput.IsEmpty())
154 {
155 if(!m_vSearchMatches.empty())
156 {
157 char aSearchMatchLabel[64];
158 str_format(buffer: aSearchMatchLabel, buffer_size: sizeof(aSearchMatchLabel), format: Localize(pStr: "Match %d of %d"), m_CurrentSearchMatch + 1, (int)m_vSearchMatches.size());
159 Ui()->DoLabel(pRect: &SearchMatches, pText: aSearchMatchLabel, Size: FONT_SIZE, Align: TEXTALIGN_MC);
160 }
161 else
162 {
163 Ui()->DoLabel(pRect: &SearchMatches, pText: Localize(pStr: "No results"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
164 }
165 }
166
167 // Reset to default button
168 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_ResetToDefaultButton, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &ResetToDefault))
169 {
170 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Reset controls"), pMessage: Localize(pStr: "Are you sure that you want to reset the controls to their defaults?"),
171 pConfirmButtonLabel: Localize(pStr: "Reset"), pCancelButtonLabel: Localize(pStr: "Cancel"), pfnConfirmButtonCallback: &CMenus::ResetSettingsControls);
172 }
173
174 CScrollRegionParams ScrollParams;
175 ScrollParams.m_ScrollUnit = 6.0f * BUTTON_HEIGHT;
176 ScrollParams.m_Flags = CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
177 m_SettingsScrollRegion.Begin(pClipRect: &MainView, pParams: &ScrollParams);
178
179 CUIRect LeftColumn, RightColumn;
180 MainView.VSplitMid(pLeft: &LeftColumn, pRight: &RightColumn, Spacing: MARGIN);
181
182 // Left column
183 RenderSettingsBlock(Height: MeasureSettingsMouseHeight(), pParentRect: &LeftColumn,
184 pTitle: Localize(pStr: "Mouse"), pExpanded: nullptr, pExpandButton: nullptr, RenderContentFunction: std::bind_front(fn: &CMenusSettingsControls::RenderSettingsMouse, args: this));
185 RenderSettingsBlock(Height: MeasureSettingsJoystickHeight(), pParentRect: &LeftColumn,
186 pTitle: Localize(pStr: "Controller"), pExpanded: nullptr, pExpandButton: nullptr, RenderContentFunction: std::bind_front(fn: &CMenusSettingsControls::RenderSettingsJoystick, args: this));
187 RenderSettingsBindsBlock(Group: EBindOptionGroup::MOVEMENT, pParentRect: &LeftColumn, pTitle: Localize(pStr: "Movement"));
188 RenderSettingsBindsBlock(Group: EBindOptionGroup::WEAPON, pParentRect: &LeftColumn, pTitle: Localize(pStr: "Weapon"));
189
190 // Right column
191 RenderSettingsBindsBlock(Group: EBindOptionGroup::VOTING, pParentRect: &RightColumn, pTitle: Localize(pStr: "Voting"));
192 RenderSettingsBindsBlock(Group: EBindOptionGroup::CHAT, pParentRect: &RightColumn, pTitle: Localize(pStr: "Chat"));
193 RenderSettingsBindsBlock(Group: EBindOptionGroup::DUMMY, pParentRect: &RightColumn, pTitle: Localize(pStr: "Dummy"));
194 RenderSettingsBindsBlock(Group: EBindOptionGroup::MISCELLANEOUS, pParentRect: &RightColumn, pTitle: Localize(pStr: "Miscellaneous"));
195 if(std::any_of(first: m_vBindOptions.begin(), last: m_vBindOptions.end(), pred: [](const CBindOption &Option) { return Option.m_Group == EBindOptionGroup::CUSTOM; }))
196 {
197 RenderSettingsBindsBlock(Group: EBindOptionGroup::CUSTOM, pParentRect: &RightColumn, pTitle: Localize(pStr: "Custom"));
198 }
199
200 m_SettingsScrollRegion.End();
201}
202
203void CMenusSettingsControls::UpdateBindOptions()
204{
205 for(CBindOption &Option : m_vBindOptions)
206 {
207 for(CBindSlotUiElement &BindSlot : Option.m_vCurrentBinds)
208 {
209 if(BindSlot.m_Bind != EMPTY_BIND_SLOT)
210 {
211 BindSlot.m_ToBeDeleted = true;
212 }
213 }
214 }
215
216 for(int Mod = KeyModifier::NONE; Mod < KeyModifier::COMBINATION_COUNT; Mod++)
217 {
218 for(int KeyId = KEY_FIRST; KeyId < KEY_LAST; KeyId++)
219 {
220 const CBindSlot BindSlot = CBindSlot(KeyId, Mod);
221 const char *pBind = GameClient()->m_Binds.Get(BindSlot);
222 if(!pBind[0])
223 {
224 continue;
225 }
226
227 auto ExistingOption = std::find_if(first: m_vBindOptions.begin(), last: m_vBindOptions.end(), pred: [pBind](const CBindOption &Option) {
228 return str_comp(a: pBind, b: Option.m_Command.c_str()) == 0;
229 });
230 if(ExistingOption == m_vBindOptions.end())
231 {
232 // Bind option not found for command, add custom bind option.
233 CBindOption NewOption = {.m_Group: EBindOptionGroup::CUSTOM, .m_pLabel: nullptr, .m_Command: pBind};
234 ExistingOption = m_vBindOptions.insert(
235 position: std::upper_bound(first: m_vBindOptions.begin() + m_NumPredefinedBindOptions, last: m_vBindOptions.end(), val: NewOption, comp: [&](const CBindOption &Option1, const CBindOption &Option2) {
236 return str_utf8_comp_nocase(a: Option1.m_Command.c_str(), b: Option2.m_Command.c_str()) < 0;
237 }),
238 x: NewOption);
239
240 // Update search matches due to new option being added.
241 if(!m_FilterInput.IsEmpty())
242 {
243 const int OptionIndex = ExistingOption - m_vBindOptions.begin();
244 for(int &SearchMatch : m_vSearchMatches)
245 {
246 if(OptionIndex <= SearchMatch)
247 {
248 ++SearchMatch;
249 }
250 }
251 if(ExistingOption->MatchesSearch(pSearch: m_FilterInput.GetString()))
252 {
253 const int MatchIndex = m_vSearchMatches.insert(position: std::upper_bound(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), val: OptionIndex), x: OptionIndex) - m_vSearchMatches.begin();
254 if(MatchIndex <= m_CurrentSearchMatch)
255 {
256 ++m_CurrentSearchMatch;
257 }
258 }
259 }
260 }
261 auto ExistingBindSlot = ExistingOption->GetBindSlotElement(BindSlot);
262 if(ExistingBindSlot == ExistingOption->m_vCurrentBinds.end())
263 {
264 // Remove empty bind slot if one is present because it will be replaced with a bind slot for the new bind.
265 auto ExistingEmptyBindSlot = ExistingOption->GetBindSlotElement(BindSlot: EMPTY_BIND_SLOT);
266 if(ExistingEmptyBindSlot != ExistingOption->m_vCurrentBinds.end())
267 {
268 ExistingOption->m_vCurrentBinds.erase(position: ExistingEmptyBindSlot);
269 }
270
271 CBindSlotUiElement BindSlotUiElement = {.m_Bind: BindSlot};
272 ExistingOption->m_vCurrentBinds.insert(
273 position: std::upper_bound(first: ExistingOption->m_vCurrentBinds.begin(), last: ExistingOption->m_vCurrentBinds.end(), val: BindSlotUiElement),
274 x: BindSlotUiElement);
275 }
276 else
277 {
278 ExistingBindSlot->m_ToBeDeleted = false;
279 }
280 }
281 }
282
283 // Remove bind slots that are not bound anymore,
284 // mark unused custom bind options for removal.
285 for(CBindOption &Option : m_vBindOptions)
286 {
287 Option.m_vCurrentBinds.erase(first: std::remove_if(first: Option.m_vCurrentBinds.begin(), last: Option.m_vCurrentBinds.end(),
288 pred: [&](const CBindSlotUiElement &BindSlotUiElement) { return BindSlotUiElement.m_ToBeDeleted; }),
289 last: Option.m_vCurrentBinds.end());
290
291 Option.m_ToBeDeleted = Option.m_vCurrentBinds.empty() && Option.m_Group == EBindOptionGroup::CUSTOM;
292 if(Option.m_ToBeDeleted)
293 {
294 continue;
295 }
296
297 if(Option.m_vCurrentBinds.empty() ||
298 (Option.m_AddNewBind && Option.GetBindSlotElement(BindSlot: EMPTY_BIND_SLOT) == Option.m_vCurrentBinds.end()))
299 {
300 Option.m_vCurrentBinds.emplace_back(args: EMPTY_BIND_SLOT);
301 }
302 }
303
304 // Update search matches when removing bind options.
305 for(const CBindOption &Option : m_vBindOptions)
306 {
307 if(!Option.m_ToBeDeleted)
308 {
309 continue;
310 }
311 const int OptionIndex = &Option - m_vBindOptions.data();
312 auto ExactSearchMatch = std::find(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), val: OptionIndex);
313 if(ExactSearchMatch != m_vSearchMatches.end())
314 {
315 m_vSearchMatches.erase(position: ExactSearchMatch);
316 if((int)(ExactSearchMatch - m_vSearchMatches.begin()) < m_CurrentSearchMatch)
317 {
318 --m_CurrentSearchMatch;
319 }
320 }
321 for(int &SearchMatch : m_vSearchMatches)
322 {
323 if(OptionIndex < SearchMatch)
324 {
325 --SearchMatch;
326 }
327 }
328 }
329 if(m_vSearchMatches.empty())
330 {
331 m_CurrentSearchMatch = 0;
332 }
333 else if(m_CurrentSearchMatch >= (int)m_vSearchMatches.size())
334 {
335 m_CurrentSearchMatch = m_vSearchMatches.size() - 1;
336 }
337
338 // Remove unused bind options.
339 m_vBindOptions.erase(first: std::remove_if(first: m_vBindOptions.begin() + m_NumPredefinedBindOptions, last: m_vBindOptions.end(),
340 pred: [&](const CBindOption &Option) { return Option.m_ToBeDeleted; }),
341 last: m_vBindOptions.end());
342}
343
344void CMenusSettingsControls::UpdateSearchMatches()
345{
346 m_vSearchMatches.clear();
347
348 if(!m_FilterInput.IsEmpty())
349 {
350 for(CBindOption &Option : m_vBindOptions)
351 {
352 if(!Option.MatchesSearch(pSearch: m_FilterInput.GetString()))
353 {
354 continue;
355 }
356
357 m_aBindGroupExpanded[(int)Option.m_Group] = true;
358 m_vSearchMatches.emplace_back(args: &Option - m_vBindOptions.data());
359 }
360 }
361
362 if(m_vSearchMatches.empty())
363 {
364 m_CurrentSearchMatch = 0;
365 }
366 else if(m_CurrentSearchMatch >= (int)m_vSearchMatches.size())
367 {
368 m_CurrentSearchMatch = m_vSearchMatches.size() - 1;
369 }
370}
371
372void CMenusSettingsControls::RenderSettingsBlock(float Height, CUIRect *pParentRect, const char *pTitle,
373 bool *pExpanded, CButtonContainer *pExpandButton, const std::function<void(CUIRect Rect)> &RenderContentFunction)
374{
375 const bool WasExpanded = pExpanded == nullptr || *pExpanded;
376 float FullHeight = WasExpanded ? Height : 0.0f; // Content
377 FullHeight += pTitle == nullptr ? 0.0f : HEADER_FONT_SIZE + (WasExpanded ? MARGIN : 0.0f); // Title and spacing
378 FullHeight += 2.0f * MARGIN; // Margin
379
380 CUIRect SettingsBlock;
381 pParentRect->HSplitTop(Cut: FullHeight, pTop: &SettingsBlock, pBottom: pParentRect);
382 pParentRect->HSplitTop(Cut: MARGIN, pTop: nullptr, pBottom: pParentRect);
383 if(m_SettingsScrollRegion.AddRect(Rect: SettingsBlock) || m_SearchMatchReveal)
384 {
385 SettingsBlock.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, pExpandButton == nullptr || Ui()->HotItem() != pExpandButton ? 0.25f : 0.3f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
386 SettingsBlock.Margin(Cut: MARGIN, pOtherRect: &SettingsBlock);
387
388 if(pTitle != nullptr)
389 {
390 CUIRect Label;
391 SettingsBlock.HSplitTop(Cut: HEADER_FONT_SIZE, pTop: &Label, pBottom: &SettingsBlock);
392 if(WasExpanded)
393 {
394 SettingsBlock.HSplitTop(Cut: MARGIN, pTop: nullptr, pBottom: &SettingsBlock);
395 }
396
397 if(pExpanded != nullptr)
398 {
399 CUIRect ButtonArea;
400 Label.Margin(Cut: -MARGIN, pOtherRect: &ButtonArea);
401 if(Ui()->DoButtonLogic(pId: pExpandButton, Checked: 0, pRect: &ButtonArea, Flags: BUTTONFLAG_LEFT))
402 {
403 *pExpanded = !*pExpanded;
404 }
405
406 CUIRect ExpandButton;
407 Label.VSplitRight(Cut: 20.0f, pLeft: &Label, pRight: &ExpandButton);
408 Label.VSplitRight(Cut: BUTTON_SPACING, pLeft: &Label, pRight: nullptr);
409 if(m_SettingsScrollRegion.AddRect(Rect: ExpandButton))
410 {
411 SLabelProperties Props;
412 Props.SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 0.65f * Ui()->ButtonColorMul(pId: pExpandButton)));
413 Props.m_EnableWidthCheck = false;
414 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
415 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);
416 Ui()->DoLabel(pRect: &ExpandButton, pText: *pExpanded ? FontIcon::CHEVRON_UP : FontIcon::CHEVRON_DOWN, Size: HEADER_FONT_SIZE, Align: TEXTALIGN_MR, LabelProps: Props);
417 TextRender()->SetRenderFlags(0);
418 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
419 }
420 }
421
422 if(m_SettingsScrollRegion.AddRect(Rect: Label))
423 {
424 Ui()->DoLabel(pRect: &Label, pText: pTitle, Size: HEADER_FONT_SIZE, Align: TEXTALIGN_ML);
425 }
426 }
427
428 if(WasExpanded)
429 {
430 RenderContentFunction(SettingsBlock);
431 }
432 }
433}
434
435void CMenusSettingsControls::RenderSettingsBindsBlock(EBindOptionGroup Group, CUIRect *pParentRect, const char *pTitle)
436{
437 RenderSettingsBlock(Height: MeasureSettingsBindsHeight(Group), pParentRect, pTitle,
438 pExpanded: &m_aBindGroupExpanded[(int)Group], pExpandButton: &m_aBindGroupExpandButtons[(int)Group],
439 RenderContentFunction: [&](CUIRect Rect) { RenderSettingsBinds(Group, View: Rect); });
440}
441
442float CMenusSettingsControls::MeasureSettingsBindsHeight(EBindOptionGroup Group) const
443{
444 float Height = 0.0f;
445 for(const CBindOption &BindOption : m_vBindOptions)
446 {
447 if(BindOption.m_Group != Group)
448 {
449 continue;
450 }
451 if(Height > 0.0f)
452 {
453 Height += BIND_OPTION_SPACING;
454 }
455 Height += BUTTON_HEIGHT * BindOption.m_vCurrentBinds.size() + BUTTON_SPACING * (BindOption.m_vCurrentBinds.size() - 1) + BIND_OPTION_SPACING;
456 }
457 return Height;
458}
459
460void CMenusSettingsControls::RenderSettingsBinds(EBindOptionGroup Group, CUIRect View)
461{
462 for(CBindOption &BindOption : m_vBindOptions)
463 {
464 if(BindOption.m_Group != Group)
465 {
466 continue;
467 }
468
469 CUIRect KeyReaders;
470 View.HSplitTop(Cut: BUTTON_HEIGHT * BindOption.m_vCurrentBinds.size() + BUTTON_SPACING * (BindOption.m_vCurrentBinds.size() - 1) + 4.0f, pTop: &KeyReaders, pBottom: &View);
471 View.HSplitTop(Cut: BIND_OPTION_SPACING, pTop: nullptr, pBottom: &View);
472 if(!m_SettingsScrollRegion.AddRect(Rect: KeyReaders) && !m_SearchMatchReveal)
473 {
474 continue;
475 }
476 KeyReaders.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
477 KeyReaders.Margin(Cut: 2.0f, pOtherRect: &KeyReaders);
478
479 CUIRect Label, AddButton;
480 KeyReaders.VSplitLeft(Cut: KeyReaders.w / 3.0f, pLeft: &Label, pRight: &KeyReaders);
481 KeyReaders.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &KeyReaders);
482 KeyReaders.VSplitLeft(Cut: BUTTON_HEIGHT, pLeft: &AddButton, pRight: &KeyReaders);
483 AddButton.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &AddButton, pBottom: nullptr);
484 KeyReaders.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &KeyReaders);
485 Label.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Label, pBottom: nullptr);
486
487 const auto SearchMatch = std::find(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), val: &BindOption - m_vBindOptions.data());
488 const bool SearchMatchSelected = SearchMatch != m_vSearchMatches.end() && m_CurrentSearchMatch == (int)(SearchMatch - m_vSearchMatches.begin());
489 if(SearchMatchSelected && m_SearchMatchReveal)
490 {
491 m_SearchMatchReveal = false;
492 // Scroll to reveal search match
493 CUIRect ScrollTarget;
494 Label.HMargin(Cut: -MARGIN, pOtherRect: &ScrollTarget);
495 m_SettingsScrollRegion.AddRect(Rect: ScrollTarget, ShouldScrollHere: true);
496 }
497 SLabelProperties LabelProps = {.m_MaxWidth = Label.w, .m_EllipsisAtEnd = BindOption.m_Group == EBindOptionGroup::CUSTOM, .m_MinimumFontSize = 9.0f};
498 if(SearchMatchSelected)
499 {
500 LabelProps.SetColor(ColorRGBA(0.1f, 0.1f, 1.0f, 1.0f));
501 }
502 else if(SearchMatch != m_vSearchMatches.end())
503 {
504 LabelProps.SetColor(ColorRGBA(0.4f, 0.4f, 0.9f, 1.0f));
505 }
506 const CLabelResult LabelResult = Ui()->DoLabel(pRect: &Label, pText: BindOption.m_Group == EBindOptionGroup::CUSTOM ? BindOption.m_Command.c_str() : Localize(pStr: BindOption.m_pLabel),
507 Size: FONT_SIZE, Align: TEXTALIGN_ML, LabelProps);
508 if(BindOption.m_Group != EBindOptionGroup::CUSTOM || LabelResult.m_Truncated)
509 {
510 Ui()->DoButtonLogic(pId: &BindOption.m_TooltipButtonId, Checked: 0, pRect: &Label, Flags: BUTTONFLAG_NONE);
511 GameClient()->m_Tooltips.DoToolTip(pId: &BindOption.m_TooltipButtonId, pNearRect: &Label, pText: BindOption.m_Command.c_str());
512 }
513
514 for(CBindSlotUiElement &CurrentBind : BindOption.m_vCurrentBinds)
515 {
516 CUIRect KeyReader;
517 KeyReaders.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &KeyReader, pBottom: &KeyReaders);
518 KeyReaders.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &KeyReaders);
519 const bool ActivateKeyReader = BindOption.m_AddNewBindActivate && CurrentBind.m_Bind == EMPTY_BIND_SLOT;
520 const CKeyBinder::CKeyReaderResult KeyReaderResult = GameClient()->m_KeyBinder.DoKeyReader(
521 pReaderButton: &CurrentBind.m_KeyReaderButton, pClearButton: &CurrentBind.m_KeyResetButton,
522 pRect: &KeyReader, CurrentBind: CurrentBind.m_Bind, Activate: ActivateKeyReader);
523 if(ActivateKeyReader)
524 {
525 BindOption.m_AddNewBindActivate = false;
526 // Scroll to reveal activated key reader
527 CUIRect ScrollTarget;
528 KeyReader.HMargin(Cut: -MARGIN, pOtherRect: &ScrollTarget);
529 m_SettingsScrollRegion.AddRect(Rect: ScrollTarget, ShouldScrollHere: true);
530 }
531 if(KeyReaderResult.m_Aborted)
532 {
533 BindOption.m_AddNewBind = false;
534 if(CurrentBind.m_Bind == EMPTY_BIND_SLOT && (&CurrentBind - BindOption.m_vCurrentBinds.data()) > 0)
535 {
536 CurrentBind.m_ToBeDeleted = true;
537 }
538 }
539 else if(KeyReaderResult.m_Bind != CurrentBind.m_Bind)
540 {
541 BindOption.m_AddNewBind = false;
542 if(CurrentBind.m_Bind.m_Key != KEY_UNKNOWN || KeyReaderResult.m_Bind.m_Key == KEY_UNKNOWN)
543 {
544 GameClient()->m_Binds.Bind(KeyId: CurrentBind.m_Bind.m_Key, pStr: "", FreeOnly: false, ModifierCombination: CurrentBind.m_Bind.m_ModifierMask);
545 }
546 if(KeyReaderResult.m_Bind.m_Key != KEY_UNKNOWN)
547 {
548 GameClient()->m_Binds.Bind(KeyId: KeyReaderResult.m_Bind.m_Key, pStr: BindOption.m_Command.c_str(), FreeOnly: false, ModifierCombination: KeyReaderResult.m_Bind.m_ModifierMask);
549 }
550 }
551 }
552
553 if(Ui()->DoButton_FontIcon(pButtonContainer: &BindOption.m_AddBindButtonContainer, pText: FontIcon::PLUS, Checked: BindOption.m_AddNewBind ? 1 : 0, pRect: &AddButton, Flags: BUTTONFLAG_LEFT))
554 {
555 BindOption.m_AddNewBind = true;
556 BindOption.m_AddNewBindActivate = true;
557 }
558 }
559}
560
561float CMenusSettingsControls::MeasureSettingsMouseHeight() const
562{
563 return 2.0f * BUTTON_HEIGHT + BUTTON_SPACING;
564}
565
566void CMenusSettingsControls::RenderSettingsMouse(CUIRect View)
567{
568 CUIRect Button;
569 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
570 Ui()->DoScrollbarOption(pId: &g_Config.m_InpMousesens, pOption: &g_Config.m_InpMousesens, pRect: &Button, pStr: Localize(pStr: "Ingame mouse sens."), Min: 1, Max: 500,
571 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
572
573 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
574
575 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
576 Ui()->DoScrollbarOption(pId: &g_Config.m_UiMousesens, pOption: &g_Config.m_UiMousesens, pRect: &Button, pStr: Localize(pStr: "UI mouse sens."), Min: 1, Max: 500,
577 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE | CUi::SCROLLBAR_OPTION_DELAYUPDATE);
578}
579
580float CMenusSettingsControls::MeasureSettingsJoystickHeight() const
581{
582 int NumOptions = 1; // expandable header
583 if(g_Config.m_InpControllerEnable)
584 {
585 NumOptions++; // message or joystick name/selection
586 if(Input()->NumJoysticks() > 0)
587 {
588 NumOptions += 3; // mode, ui sens, tolerance
589 if(!g_Config.m_InpControllerAbsolute)
590 NumOptions++; // ingame sens
591 NumOptions += Input()->GetActiveJoystick()->GetNumAxes() + 1; // axis selection + header
592 }
593 }
594 return NumOptions * (BUTTON_HEIGHT + BUTTON_SPACING) + (NumOptions == 1 ? 0.0f : BUTTON_SPACING);
595}
596
597void CMenusSettingsControls::RenderSettingsJoystick(CUIRect View)
598{
599 CUIRect Button;
600 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
601 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
602 const bool WasJoystickEnabled = g_Config.m_InpControllerEnable;
603 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &g_Config.m_InpControllerEnable, pText: Localize(pStr: "Enable controller"), Checked: g_Config.m_InpControllerEnable, pRect: &Button))
604 {
605 g_Config.m_InpControllerEnable ^= 1;
606 }
607 if(!WasJoystickEnabled) // Use old value because this was used to allocate the available height
608 {
609 return;
610 }
611
612 const int NumJoysticks = Input()->NumJoysticks();
613 if(NumJoysticks > 0)
614 {
615 // show joystick device selection if more than one available or just the joystick name if there is only one
616 {
617 CUIRect JoystickDropDown;
618 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
619 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &JoystickDropDown, pBottom: &View);
620 if(NumJoysticks > 1)
621 {
622 std::vector<std::string> vJoystickNames;
623 std::vector<const char *> vpJoystickNames;
624 vJoystickNames.resize(sz: NumJoysticks);
625 vpJoystickNames.resize(sz: NumJoysticks);
626
627 for(int i = 0; i < NumJoysticks; ++i)
628 {
629 char aJoystickName[256];
630 str_format(buffer: aJoystickName, buffer_size: sizeof(aJoystickName), format: "%s %d: %s", Localize(pStr: "Controller"), i, Input()->GetJoystick(Index: i)->GetName());
631 vJoystickNames[i] = aJoystickName;
632 vpJoystickNames[i] = vJoystickNames[i].c_str();
633 }
634
635 const int CurrentJoystick = Input()->GetActiveJoystick()->GetIndex();
636 const int NewJoystick = Ui()->DoDropDown(pRect: &JoystickDropDown, CurSelection: CurrentJoystick, pStrs: vpJoystickNames.data(), Num: vpJoystickNames.size(), State&: m_JoystickDropDownState);
637 if(NewJoystick != CurrentJoystick)
638 {
639 Input()->SetActiveJoystick(NewJoystick);
640 }
641 }
642 else
643 {
644 char aBuf[256];
645 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s 0: %s", Localize(pStr: "Controller"), Input()->GetJoystick(Index: 0)->GetName());
646 Ui()->DoLabel(pRect: &JoystickDropDown, pText: aBuf, Size: FONT_SIZE, Align: TEXTALIGN_ML);
647 }
648 }
649
650 const bool WasAbsolute = g_Config.m_InpControllerAbsolute;
651 GameClient()->m_Menus.DoLine_RadioMenu(View, pLabel: Localize(pStr: "Ingame controller mode"),
652 vButtonContainers&: m_vJoystickIngameModeButtonContainers,
653 vLabels: {Localize(pStr: "Relative", pContext: "Ingame controller mode"), Localize(pStr: "Absolute", pContext: "Ingame controller mode")},
654 vValues: {0, 1},
655 Value&: g_Config.m_InpControllerAbsolute);
656
657 if(!WasAbsolute) // Use old value because this was used to allocate the available height
658 {
659 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
660 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
661 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerSens, pOption: &g_Config.m_InpControllerSens, pRect: &Button, pStr: Localize(pStr: "Ingame controller sens."), Min: 1, Max: 500,
662 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
663 }
664
665 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
666 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
667 Ui()->DoScrollbarOption(pId: &g_Config.m_UiControllerSens, pOption: &g_Config.m_UiControllerSens, pRect: &Button, pStr: Localize(pStr: "UI controller sens."), Min: 1, Max: 500,
668 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
669
670 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
671 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
672 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerTolerance, pOption: &g_Config.m_InpControllerTolerance, pRect: &Button, pStr: Localize(pStr: "Controller jitter tolerance"), Min: 0, Max: 50);
673
674 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
675 if(m_SettingsScrollRegion.AddRect(Rect: View))
676 {
677 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
678 RenderJoystickAxisPicker(View);
679 }
680 }
681 else
682 {
683 View.HSplitTop(Cut: View.h - BUTTON_HEIGHT, pTop: nullptr, pBottom: &View);
684 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
685 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "No controller found. Plug in a controller."), Size: FONT_SIZE, Align: TEXTALIGN_ML);
686 }
687}
688
689void CMenusSettingsControls::RenderJoystickAxisPicker(CUIRect View)
690{
691 const float AxisWidth = 0.2f * View.w;
692 const float StatusWidth = 0.4f * View.w;
693 const float AimBindWidth = 90.0f;
694 const float SpacingV = (View.w - AxisWidth - StatusWidth - AimBindWidth) / 2.0f;
695
696 CUIRect Row, Axis, Status, AimBind;
697 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
698 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Row, pBottom: &View);
699 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
700 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
701 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
702 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
703 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
704
705 Ui()->DoLabel(pRect: &Axis, pText: Localize(pStr: "Axis"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
706 Ui()->DoLabel(pRect: &Status, pText: Localize(pStr: "Status"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
707 Ui()->DoLabel(pRect: &AimBind, pText: Localize(pStr: "Aim bind"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
708
709 IInput::IJoystick *pJoystick = Input()->GetActiveJoystick();
710 for(int i = 0; i < std::min<int>(a: pJoystick->GetNumAxes(), b: NUM_JOYSTICK_AXES); i++)
711 {
712 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
713 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Row, pBottom: &View);
714 if(!m_SettingsScrollRegion.AddRect(Rect: Row))
715 {
716 continue;
717 }
718 Row.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
719 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
720 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
721 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
722 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
723 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
724
725 const bool Active = g_Config.m_InpControllerX == i || g_Config.m_InpControllerY == i;
726
727 // Axis label
728 char aLabel[16];
729 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "%d", i + 1);
730 SLabelProperties LabelProps;
731 if(!Active)
732 {
733 LabelProps.SetColor(ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f));
734 }
735 Ui()->DoLabel(pRect: &Axis, pText: aLabel, Size: FONT_SIZE, Align: TEXTALIGN_MC, LabelProps);
736
737 // Axis status
738 Status.HMargin(Cut: 7.0f, pOtherRect: &Status);
739 RenderJoystickBar(pRect: &Status, Current: (pJoystick->GetAxisValue(Axis: i) + 1.0f) / 2.0f, Tolerance: g_Config.m_InpControllerTolerance / 50.0f, Active);
740
741 // Bind to X/Y
742 CUIRect AimBindX, AimBindY;
743 AimBind.VSplitMid(pLeft: &AimBindX, pRight: &AimBindY);
744 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aaJoystickAxisCheckboxIds[i][0], pText: "X", Checked: g_Config.m_InpControllerX == i, pRect: &AimBindX))
745 {
746 if(g_Config.m_InpControllerY == i)
747 g_Config.m_InpControllerY = g_Config.m_InpControllerX;
748 g_Config.m_InpControllerX = i;
749 }
750 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aaJoystickAxisCheckboxIds[i][1], pText: "Y", Checked: g_Config.m_InpControllerY == i, pRect: &AimBindY))
751 {
752 if(g_Config.m_InpControllerX == i)
753 g_Config.m_InpControllerX = g_Config.m_InpControllerY;
754 g_Config.m_InpControllerY = i;
755 }
756 }
757}
758
759void CMenusSettingsControls::RenderJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active)
760{
761 CUIRect Handle;
762 pRect->VSplitLeft(Cut: pRect->h, pLeft: &Handle, pRight: nullptr); // Slider size
763 Handle.x += (pRect->w - Handle.w) * Current;
764
765 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, Active ? 0.25f : 0.125f), Corners: IGraphics::CORNER_ALL, Rounding: pRect->h / 2.0f);
766
767 CUIRect ToleranceArea = *pRect;
768 ToleranceArea.w *= Tolerance;
769 ToleranceArea.x += (pRect->w - ToleranceArea.w) / 2.0f;
770 const ColorRGBA ToleranceColor = Active ? ColorRGBA(0.8f, 0.35f, 0.35f, 1.0f) : ColorRGBA(0.7f, 0.5f, 0.5f, 1.0f);
771 ToleranceArea.Draw(Color: ToleranceColor, Corners: IGraphics::CORNER_ALL, Rounding: ToleranceArea.h / 2.0f);
772
773 const ColorRGBA SliderColor = Active ? ColorRGBA(0.95f, 0.95f, 0.95f, 1.0f) : ColorRGBA(0.8f, 0.8f, 0.8f, 1.0f);
774 Handle.Draw(Color: SliderColor, Corners: IGraphics::CORNER_ALL, Rounding: Handle.h / 2.0f);
775}
776
777void CMenus::ResetSettingsControls()
778{
779 GameClient()->m_Binds.SetDefaults();
780
781 g_Config.m_InpMousesens = 200;
782 g_Config.m_UiMousesens = 200;
783
784 g_Config.m_InpControllerEnable = 0;
785 g_Config.m_InpControllerGUID[0] = '\0';
786 g_Config.m_InpControllerAbsolute = 0;
787 g_Config.m_InpControllerSens = 100;
788 g_Config.m_InpControllerX = 0;
789 g_Config.m_InpControllerY = 1;
790 g_Config.m_InpControllerTolerance = 5;
791 g_Config.m_UiControllerSens = 100;
792}
793