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/system.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 vec2 ScrollOffset(0.0f, 0.0f);
175 CScrollRegionParams ScrollParams;
176 ScrollParams.m_ScrollUnit = 6.0f * BUTTON_HEIGHT;
177 ScrollParams.m_Flags = CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
178 m_SettingsScrollRegion.Begin(pClipRect: &MainView, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
179 MainView.y += ScrollOffset.y;
180
181 CUIRect LeftColumn, RightColumn;
182 MainView.VSplitMid(pLeft: &LeftColumn, pRight: &RightColumn, Spacing: MARGIN);
183
184 // Left column
185 RenderSettingsBlock(Height: MeasureSettingsMouseHeight(), pParentRect: &LeftColumn,
186 pTitle: Localize(pStr: "Mouse"), pExpanded: nullptr, pExpandButton: nullptr, RenderContentFunction: std::bind_front(fn: &CMenusSettingsControls::RenderSettingsMouse, args: this));
187 RenderSettingsBlock(Height: MeasureSettingsJoystickHeight(), pParentRect: &LeftColumn,
188 pTitle: Localize(pStr: "Controller"), pExpanded: nullptr, pExpandButton: nullptr, RenderContentFunction: std::bind_front(fn: &CMenusSettingsControls::RenderSettingsJoystick, args: this));
189 RenderSettingsBindsBlock(Group: EBindOptionGroup::MOVEMENT, pParentRect: &LeftColumn, pTitle: Localize(pStr: "Movement"));
190 RenderSettingsBindsBlock(Group: EBindOptionGroup::WEAPON, pParentRect: &LeftColumn, pTitle: Localize(pStr: "Weapon"));
191
192 // Right column
193 RenderSettingsBindsBlock(Group: EBindOptionGroup::VOTING, pParentRect: &RightColumn, pTitle: Localize(pStr: "Voting"));
194 RenderSettingsBindsBlock(Group: EBindOptionGroup::CHAT, pParentRect: &RightColumn, pTitle: Localize(pStr: "Chat"));
195 RenderSettingsBindsBlock(Group: EBindOptionGroup::DUMMY, pParentRect: &RightColumn, pTitle: Localize(pStr: "Dummy"));
196 RenderSettingsBindsBlock(Group: EBindOptionGroup::MISCELLANEOUS, pParentRect: &RightColumn, pTitle: Localize(pStr: "Miscellaneous"));
197 if(std::any_of(first: m_vBindOptions.begin(), last: m_vBindOptions.end(), pred: [](const CBindOption &Option) { return Option.m_Group == EBindOptionGroup::CUSTOM; }))
198 {
199 RenderSettingsBindsBlock(Group: EBindOptionGroup::CUSTOM, pParentRect: &RightColumn, pTitle: Localize(pStr: "Custom"));
200 }
201
202 m_SettingsScrollRegion.End();
203}
204
205void CMenusSettingsControls::UpdateBindOptions()
206{
207 for(CBindOption &Option : m_vBindOptions)
208 {
209 for(CBindSlotUiElement &BindSlot : Option.m_vCurrentBinds)
210 {
211 if(BindSlot.m_Bind != EMPTY_BIND_SLOT)
212 {
213 BindSlot.m_ToBeDeleted = true;
214 }
215 }
216 }
217
218 for(int Mod = KeyModifier::NONE; Mod < KeyModifier::COMBINATION_COUNT; Mod++)
219 {
220 for(int KeyId = KEY_FIRST; KeyId < KEY_LAST; KeyId++)
221 {
222 const CBindSlot BindSlot = CBindSlot(KeyId, Mod);
223 const char *pBind = GameClient()->m_Binds.Get(BindSlot);
224 if(!pBind[0])
225 {
226 continue;
227 }
228
229 auto ExistingOption = std::find_if(first: m_vBindOptions.begin(), last: m_vBindOptions.end(), pred: [pBind](const CBindOption &Option) {
230 return str_comp(a: pBind, b: Option.m_Command.c_str()) == 0;
231 });
232 if(ExistingOption == m_vBindOptions.end())
233 {
234 // Bind option not found for command, add custom bind option.
235 CBindOption NewOption = {.m_Group: EBindOptionGroup::CUSTOM, .m_pLabel: nullptr, .m_Command: pBind};
236 ExistingOption = m_vBindOptions.insert(
237 position: std::upper_bound(first: m_vBindOptions.begin() + m_NumPredefinedBindOptions, last: m_vBindOptions.end(), val: NewOption, comp: [&](const CBindOption &Option1, const CBindOption &Option2) {
238 return str_utf8_comp_nocase(a: Option1.m_Command.c_str(), b: Option2.m_Command.c_str()) < 0;
239 }),
240 x: NewOption);
241
242 // Update search matches due to new option being added.
243 if(!m_FilterInput.IsEmpty())
244 {
245 const int OptionIndex = ExistingOption - m_vBindOptions.begin();
246 for(int &SearchMatch : m_vSearchMatches)
247 {
248 if(OptionIndex <= SearchMatch)
249 {
250 ++SearchMatch;
251 }
252 }
253 if(ExistingOption->MatchesSearch(pSearch: m_FilterInput.GetString()))
254 {
255 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();
256 if(MatchIndex <= m_CurrentSearchMatch)
257 {
258 ++m_CurrentSearchMatch;
259 }
260 }
261 }
262 }
263 auto ExistingBindSlot = ExistingOption->GetBindSlotElement(BindSlot);
264 if(ExistingBindSlot == ExistingOption->m_vCurrentBinds.end())
265 {
266 // Remove empty bind slot if one is present because it will be replaced with a bind slot for the new bind.
267 auto ExistingEmptyBindSlot = ExistingOption->GetBindSlotElement(BindSlot: EMPTY_BIND_SLOT);
268 if(ExistingEmptyBindSlot != ExistingOption->m_vCurrentBinds.end())
269 {
270 ExistingOption->m_vCurrentBinds.erase(position: ExistingEmptyBindSlot);
271 }
272
273 CBindSlotUiElement BindSlotUiElement = {.m_Bind: BindSlot};
274 ExistingOption->m_vCurrentBinds.insert(
275 position: std::upper_bound(first: ExistingOption->m_vCurrentBinds.begin(), last: ExistingOption->m_vCurrentBinds.end(), val: BindSlotUiElement),
276 x: BindSlotUiElement);
277 }
278 else
279 {
280 ExistingBindSlot->m_ToBeDeleted = false;
281 }
282 }
283 }
284
285 // Remove bind slots that are not bound anymore,
286 // mark unused custom bind options for removal.
287 for(CBindOption &Option : m_vBindOptions)
288 {
289 Option.m_vCurrentBinds.erase(first: std::remove_if(first: Option.m_vCurrentBinds.begin(), last: Option.m_vCurrentBinds.end(),
290 pred: [&](const CBindSlotUiElement &BindSlotUiElement) { return BindSlotUiElement.m_ToBeDeleted; }),
291 last: Option.m_vCurrentBinds.end());
292
293 Option.m_ToBeDeleted = Option.m_vCurrentBinds.empty() && Option.m_Group == EBindOptionGroup::CUSTOM;
294 if(Option.m_ToBeDeleted)
295 {
296 continue;
297 }
298
299 if(Option.m_vCurrentBinds.empty() ||
300 (Option.m_AddNewBind && Option.GetBindSlotElement(BindSlot: EMPTY_BIND_SLOT) == Option.m_vCurrentBinds.end()))
301 {
302 Option.m_vCurrentBinds.emplace_back(args: EMPTY_BIND_SLOT);
303 }
304 }
305
306 // Update search matches when removing bind options.
307 for(const CBindOption &Option : m_vBindOptions)
308 {
309 if(!Option.m_ToBeDeleted)
310 {
311 continue;
312 }
313 const int OptionIndex = &Option - m_vBindOptions.data();
314 auto ExactSearchMatch = std::find(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), val: OptionIndex);
315 if(ExactSearchMatch != m_vSearchMatches.end())
316 {
317 m_vSearchMatches.erase(position: ExactSearchMatch);
318 if((int)(ExactSearchMatch - m_vSearchMatches.begin()) < m_CurrentSearchMatch)
319 {
320 --m_CurrentSearchMatch;
321 }
322 }
323 for(int &SearchMatch : m_vSearchMatches)
324 {
325 if(OptionIndex < SearchMatch)
326 {
327 --SearchMatch;
328 }
329 }
330 }
331 if(m_vSearchMatches.empty())
332 {
333 m_CurrentSearchMatch = 0;
334 }
335 else if(m_CurrentSearchMatch >= (int)m_vSearchMatches.size())
336 {
337 m_CurrentSearchMatch = m_vSearchMatches.size() - 1;
338 }
339
340 // Remove unused bind options.
341 m_vBindOptions.erase(first: std::remove_if(first: m_vBindOptions.begin() + m_NumPredefinedBindOptions, last: m_vBindOptions.end(),
342 pred: [&](const CBindOption &Option) { return Option.m_ToBeDeleted; }),
343 last: m_vBindOptions.end());
344}
345
346void CMenusSettingsControls::UpdateSearchMatches()
347{
348 m_vSearchMatches.clear();
349
350 if(!m_FilterInput.IsEmpty())
351 {
352 for(CBindOption &Option : m_vBindOptions)
353 {
354 if(!Option.MatchesSearch(pSearch: m_FilterInput.GetString()))
355 {
356 continue;
357 }
358
359 m_aBindGroupExpanded[(int)Option.m_Group] = true;
360 m_vSearchMatches.emplace_back(args: &Option - m_vBindOptions.data());
361 }
362 }
363
364 if(m_vSearchMatches.empty())
365 {
366 m_CurrentSearchMatch = 0;
367 }
368 else if(m_CurrentSearchMatch >= (int)m_vSearchMatches.size())
369 {
370 m_CurrentSearchMatch = m_vSearchMatches.size() - 1;
371 }
372}
373
374void CMenusSettingsControls::RenderSettingsBlock(float Height, CUIRect *pParentRect, const char *pTitle,
375 bool *pExpanded, CButtonContainer *pExpandButton, const std::function<void(CUIRect Rect)> &RenderContentFunction)
376{
377 const bool WasExpanded = pExpanded == nullptr || *pExpanded;
378 float FullHeight = WasExpanded ? Height : 0.0f; // Content
379 FullHeight += pTitle == nullptr ? 0.0f : HEADER_FONT_SIZE + (WasExpanded ? MARGIN : 0.0f); // Title and spacing
380 FullHeight += 2.0f * MARGIN; // Margin
381
382 CUIRect SettingsBlock;
383 pParentRect->HSplitTop(Cut: FullHeight, pTop: &SettingsBlock, pBottom: pParentRect);
384 pParentRect->HSplitTop(Cut: MARGIN, pTop: nullptr, pBottom: pParentRect);
385 if(m_SettingsScrollRegion.AddRect(Rect: SettingsBlock) || m_SearchMatchReveal)
386 {
387 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);
388 SettingsBlock.Margin(Cut: MARGIN, pOtherRect: &SettingsBlock);
389
390 if(pTitle != nullptr)
391 {
392 CUIRect Label;
393 SettingsBlock.HSplitTop(Cut: HEADER_FONT_SIZE, pTop: &Label, pBottom: &SettingsBlock);
394 if(WasExpanded)
395 {
396 SettingsBlock.HSplitTop(Cut: MARGIN, pTop: nullptr, pBottom: &SettingsBlock);
397 }
398
399 if(pExpanded != nullptr)
400 {
401 CUIRect ButtonArea;
402 Label.Margin(Cut: -MARGIN, pOtherRect: &ButtonArea);
403 if(Ui()->DoButtonLogic(pId: pExpandButton, Checked: 0, pRect: &ButtonArea, Flags: BUTTONFLAG_LEFT))
404 {
405 *pExpanded = !*pExpanded;
406 }
407
408 CUIRect ExpandButton;
409 Label.VSplitRight(Cut: 20.0f, pLeft: &Label, pRight: &ExpandButton);
410 Label.VSplitRight(Cut: BUTTON_SPACING, pLeft: &Label, pRight: nullptr);
411 if(m_SettingsScrollRegion.AddRect(Rect: ExpandButton))
412 {
413 SLabelProperties Props;
414 Props.SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 0.65f * Ui()->ButtonColorMul(pId: pExpandButton)));
415 Props.m_EnableWidthCheck = false;
416 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
417 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);
418 Ui()->DoLabel(pRect: &ExpandButton, pText: *pExpanded ? FontIcon::CHEVRON_UP : FontIcon::CHEVRON_DOWN, Size: HEADER_FONT_SIZE, Align: TEXTALIGN_MR, LabelProps: Props);
419 TextRender()->SetRenderFlags(0);
420 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
421 }
422 }
423
424 if(m_SettingsScrollRegion.AddRect(Rect: Label))
425 {
426 Ui()->DoLabel(pRect: &Label, pText: pTitle, Size: HEADER_FONT_SIZE, Align: TEXTALIGN_ML);
427 }
428 }
429
430 if(WasExpanded)
431 {
432 RenderContentFunction(SettingsBlock);
433 }
434 }
435}
436
437void CMenusSettingsControls::RenderSettingsBindsBlock(EBindOptionGroup Group, CUIRect *pParentRect, const char *pTitle)
438{
439 RenderSettingsBlock(Height: MeasureSettingsBindsHeight(Group), pParentRect, pTitle,
440 pExpanded: &m_aBindGroupExpanded[(int)Group], pExpandButton: &m_aBindGroupExpandButtons[(int)Group],
441 RenderContentFunction: [&](CUIRect Rect) { RenderSettingsBinds(Group, View: Rect); });
442}
443
444float CMenusSettingsControls::MeasureSettingsBindsHeight(EBindOptionGroup Group) const
445{
446 float Height = 0.0f;
447 for(const CBindOption &BindOption : m_vBindOptions)
448 {
449 if(BindOption.m_Group != Group)
450 {
451 continue;
452 }
453 if(Height > 0.0f)
454 {
455 Height += BIND_OPTION_SPACING;
456 }
457 Height += BUTTON_HEIGHT * BindOption.m_vCurrentBinds.size() + BUTTON_SPACING * (BindOption.m_vCurrentBinds.size() - 1) + BIND_OPTION_SPACING;
458 }
459 return Height;
460}
461
462void CMenusSettingsControls::RenderSettingsBinds(EBindOptionGroup Group, CUIRect View)
463{
464 for(CBindOption &BindOption : m_vBindOptions)
465 {
466 if(BindOption.m_Group != Group)
467 {
468 continue;
469 }
470
471 CUIRect KeyReaders;
472 View.HSplitTop(Cut: BUTTON_HEIGHT * BindOption.m_vCurrentBinds.size() + BUTTON_SPACING * (BindOption.m_vCurrentBinds.size() - 1) + 4.0f, pTop: &KeyReaders, pBottom: &View);
473 View.HSplitTop(Cut: BIND_OPTION_SPACING, pTop: nullptr, pBottom: &View);
474 if(!m_SettingsScrollRegion.AddRect(Rect: KeyReaders) && !m_SearchMatchReveal)
475 {
476 continue;
477 }
478 KeyReaders.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
479 KeyReaders.Margin(Cut: 2.0f, pOtherRect: &KeyReaders);
480
481 CUIRect Label, AddButton;
482 KeyReaders.VSplitLeft(Cut: KeyReaders.w / 3.0f, pLeft: &Label, pRight: &KeyReaders);
483 KeyReaders.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &KeyReaders);
484 KeyReaders.VSplitLeft(Cut: BUTTON_HEIGHT, pLeft: &AddButton, pRight: &KeyReaders);
485 AddButton.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &AddButton, pBottom: nullptr);
486 KeyReaders.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &KeyReaders);
487 Label.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Label, pBottom: nullptr);
488
489 const auto SearchMatch = std::find(first: m_vSearchMatches.begin(), last: m_vSearchMatches.end(), val: &BindOption - m_vBindOptions.data());
490 const bool SearchMatchSelected = SearchMatch != m_vSearchMatches.end() && m_CurrentSearchMatch == (int)(SearchMatch - m_vSearchMatches.begin());
491 if(SearchMatchSelected && m_SearchMatchReveal)
492 {
493 m_SearchMatchReveal = false;
494 // Scroll to reveal search match
495 CUIRect ScrollTarget;
496 Label.HMargin(Cut: -MARGIN, pOtherRect: &ScrollTarget);
497 m_SettingsScrollRegion.AddRect(Rect: ScrollTarget, ShouldScrollHere: true);
498 }
499 SLabelProperties LabelProps = {.m_MaxWidth = Label.w, .m_EllipsisAtEnd = BindOption.m_Group == EBindOptionGroup::CUSTOM, .m_MinimumFontSize = 9.0f};
500 if(SearchMatchSelected)
501 {
502 LabelProps.SetColor(ColorRGBA(0.1f, 0.1f, 1.0f, 1.0f));
503 }
504 else if(SearchMatch != m_vSearchMatches.end())
505 {
506 LabelProps.SetColor(ColorRGBA(0.4f, 0.4f, 0.9f, 1.0f));
507 }
508 const CLabelResult LabelResult = Ui()->DoLabel(pRect: &Label, pText: BindOption.m_Group == EBindOptionGroup::CUSTOM ? BindOption.m_Command.c_str() : Localize(pStr: BindOption.m_pLabel),
509 Size: FONT_SIZE, Align: TEXTALIGN_ML, LabelProps);
510 if(BindOption.m_Group != EBindOptionGroup::CUSTOM || LabelResult.m_Truncated)
511 {
512 Ui()->DoButtonLogic(pId: &BindOption.m_TooltipButtonId, Checked: 0, pRect: &Label, Flags: BUTTONFLAG_NONE);
513 GameClient()->m_Tooltips.DoToolTip(pId: &BindOption.m_TooltipButtonId, pNearRect: &Label, pText: BindOption.m_Command.c_str());
514 }
515
516 for(CBindSlotUiElement &CurrentBind : BindOption.m_vCurrentBinds)
517 {
518 CUIRect KeyReader;
519 KeyReaders.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &KeyReader, pBottom: &KeyReaders);
520 KeyReaders.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &KeyReaders);
521 const bool ActivateKeyReader = BindOption.m_AddNewBindActivate && CurrentBind.m_Bind == EMPTY_BIND_SLOT;
522 const CKeyBinder::CKeyReaderResult KeyReaderResult = GameClient()->m_KeyBinder.DoKeyReader(
523 pReaderButton: &CurrentBind.m_KeyReaderButton, pClearButton: &CurrentBind.m_KeyResetButton,
524 pRect: &KeyReader, CurrentBind: CurrentBind.m_Bind, Activate: ActivateKeyReader);
525 if(ActivateKeyReader)
526 {
527 BindOption.m_AddNewBindActivate = false;
528 // Scroll to reveal activated key reader
529 CUIRect ScrollTarget;
530 KeyReader.HMargin(Cut: -MARGIN, pOtherRect: &ScrollTarget);
531 m_SettingsScrollRegion.AddRect(Rect: ScrollTarget, ShouldScrollHere: true);
532 }
533 if(KeyReaderResult.m_Aborted)
534 {
535 BindOption.m_AddNewBind = false;
536 if(CurrentBind.m_Bind == EMPTY_BIND_SLOT && (&CurrentBind - BindOption.m_vCurrentBinds.data()) > 0)
537 {
538 CurrentBind.m_ToBeDeleted = true;
539 }
540 }
541 else if(KeyReaderResult.m_Bind != CurrentBind.m_Bind)
542 {
543 BindOption.m_AddNewBind = false;
544 if(CurrentBind.m_Bind.m_Key != KEY_UNKNOWN || KeyReaderResult.m_Bind.m_Key == KEY_UNKNOWN)
545 {
546 GameClient()->m_Binds.Bind(KeyId: CurrentBind.m_Bind.m_Key, pStr: "", FreeOnly: false, ModifierCombination: CurrentBind.m_Bind.m_ModifierMask);
547 }
548 if(KeyReaderResult.m_Bind.m_Key != KEY_UNKNOWN)
549 {
550 GameClient()->m_Binds.Bind(KeyId: KeyReaderResult.m_Bind.m_Key, pStr: BindOption.m_Command.c_str(), FreeOnly: false, ModifierCombination: KeyReaderResult.m_Bind.m_ModifierMask);
551 }
552 }
553 }
554
555 if(Ui()->DoButton_FontIcon(pButtonContainer: &BindOption.m_AddBindButtonContainer, pText: FontIcon::PLUS, Checked: BindOption.m_AddNewBind ? 1 : 0, pRect: &AddButton, Flags: BUTTONFLAG_LEFT))
556 {
557 BindOption.m_AddNewBind = true;
558 BindOption.m_AddNewBindActivate = true;
559 }
560 }
561}
562
563float CMenusSettingsControls::MeasureSettingsMouseHeight() const
564{
565 return 2.0f * BUTTON_HEIGHT + BUTTON_SPACING;
566}
567
568void CMenusSettingsControls::RenderSettingsMouse(CUIRect View)
569{
570 CUIRect Button;
571 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
572 Ui()->DoScrollbarOption(pId: &g_Config.m_InpMousesens, pOption: &g_Config.m_InpMousesens, pRect: &Button, pStr: Localize(pStr: "Ingame mouse sens."), Min: 1, Max: 500,
573 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
574
575 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
576
577 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
578 Ui()->DoScrollbarOption(pId: &g_Config.m_UiMousesens, pOption: &g_Config.m_UiMousesens, pRect: &Button, pStr: Localize(pStr: "UI mouse sens."), Min: 1, Max: 500,
579 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE | CUi::SCROLLBAR_OPTION_DELAYUPDATE);
580}
581
582float CMenusSettingsControls::MeasureSettingsJoystickHeight() const
583{
584 int NumOptions = 1; // expandable header
585 if(g_Config.m_InpControllerEnable)
586 {
587 NumOptions++; // message or joystick name/selection
588 if(Input()->NumJoysticks() > 0)
589 {
590 NumOptions += 3; // mode, ui sens, tolerance
591 if(!g_Config.m_InpControllerAbsolute)
592 NumOptions++; // ingame sens
593 NumOptions += Input()->GetActiveJoystick()->GetNumAxes() + 1; // axis selection + header
594 }
595 }
596 return NumOptions * (BUTTON_HEIGHT + BUTTON_SPACING) + (NumOptions == 1 ? 0.0f : BUTTON_SPACING);
597}
598
599void CMenusSettingsControls::RenderSettingsJoystick(CUIRect View)
600{
601 CUIRect Button;
602 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
603 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
604 const bool WasJoystickEnabled = g_Config.m_InpControllerEnable;
605 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &g_Config.m_InpControllerEnable, pText: Localize(pStr: "Enable controller"), Checked: g_Config.m_InpControllerEnable, pRect: &Button))
606 {
607 g_Config.m_InpControllerEnable ^= 1;
608 }
609 if(!WasJoystickEnabled) // Use old value because this was used to allocate the available height
610 {
611 return;
612 }
613
614 const int NumJoysticks = Input()->NumJoysticks();
615 if(NumJoysticks > 0)
616 {
617 // show joystick device selection if more than one available or just the joystick name if there is only one
618 {
619 CUIRect JoystickDropDown;
620 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
621 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &JoystickDropDown, pBottom: &View);
622 if(NumJoysticks > 1)
623 {
624 std::vector<std::string> vJoystickNames;
625 std::vector<const char *> vpJoystickNames;
626 vJoystickNames.resize(new_size: NumJoysticks);
627 vpJoystickNames.resize(new_size: NumJoysticks);
628
629 for(int i = 0; i < NumJoysticks; ++i)
630 {
631 char aJoystickName[256];
632 str_format(buffer: aJoystickName, buffer_size: sizeof(aJoystickName), format: "%s %d: %s", Localize(pStr: "Controller"), i, Input()->GetJoystick(Index: i)->GetName());
633 vJoystickNames[i] = aJoystickName;
634 vpJoystickNames[i] = vJoystickNames[i].c_str();
635 }
636
637 const int CurrentJoystick = Input()->GetActiveJoystick()->GetIndex();
638 const int NewJoystick = Ui()->DoDropDown(pRect: &JoystickDropDown, CurSelection: CurrentJoystick, pStrs: vpJoystickNames.data(), Num: vpJoystickNames.size(), State&: m_JoystickDropDownState);
639 if(NewJoystick != CurrentJoystick)
640 {
641 Input()->SetActiveJoystick(NewJoystick);
642 }
643 }
644 else
645 {
646 char aBuf[256];
647 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s 0: %s", Localize(pStr: "Controller"), Input()->GetJoystick(Index: 0)->GetName());
648 Ui()->DoLabel(pRect: &JoystickDropDown, pText: aBuf, Size: FONT_SIZE, Align: TEXTALIGN_ML);
649 }
650 }
651
652 const bool WasAbsolute = g_Config.m_InpControllerAbsolute;
653 GameClient()->m_Menus.DoLine_RadioMenu(View, pLabel: Localize(pStr: "Ingame controller mode"),
654 vButtonContainers&: m_vJoystickIngameModeButtonContainers,
655 vLabels: {Localize(pStr: "Relative", pContext: "Ingame controller mode"), Localize(pStr: "Absolute", pContext: "Ingame controller mode")},
656 vValues: {0, 1},
657 Value&: g_Config.m_InpControllerAbsolute);
658
659 if(!WasAbsolute) // Use old value because this was used to allocate the available height
660 {
661 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
662 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
663 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerSens, pOption: &g_Config.m_InpControllerSens, pRect: &Button, pStr: Localize(pStr: "Ingame controller sens."), Min: 1, Max: 500,
664 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
665 }
666
667 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
668 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
669 Ui()->DoScrollbarOption(pId: &g_Config.m_UiControllerSens, pOption: &g_Config.m_UiControllerSens, pRect: &Button, pStr: Localize(pStr: "UI controller sens."), Min: 1, Max: 500,
670 pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
671
672 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
673 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
674 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerTolerance, pOption: &g_Config.m_InpControllerTolerance, pRect: &Button, pStr: Localize(pStr: "Controller jitter tolerance"), Min: 0, Max: 50);
675
676 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
677 if(m_SettingsScrollRegion.AddRect(Rect: View))
678 {
679 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
680 RenderJoystickAxisPicker(View);
681 }
682 }
683 else
684 {
685 View.HSplitTop(Cut: View.h - BUTTON_HEIGHT, pTop: nullptr, pBottom: &View);
686 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Button, pBottom: &View);
687 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "No controller found. Plug in a controller."), Size: FONT_SIZE, Align: TEXTALIGN_ML);
688 }
689}
690
691void CMenusSettingsControls::RenderJoystickAxisPicker(CUIRect View)
692{
693 const float AxisWidth = 0.2f * View.w;
694 const float StatusWidth = 0.4f * View.w;
695 const float AimBindWidth = 90.0f;
696 const float SpacingV = (View.w - AxisWidth - StatusWidth - AimBindWidth) / 2.0f;
697
698 CUIRect Row, Axis, Status, AimBind;
699 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
700 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Row, pBottom: &View);
701 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
702 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
703 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
704 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
705 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
706
707 Ui()->DoLabel(pRect: &Axis, pText: Localize(pStr: "Axis"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
708 Ui()->DoLabel(pRect: &Status, pText: Localize(pStr: "Status"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
709 Ui()->DoLabel(pRect: &AimBind, pText: Localize(pStr: "Aim bind"), Size: FONT_SIZE, Align: TEXTALIGN_MC);
710
711 IInput::IJoystick *pJoystick = Input()->GetActiveJoystick();
712 for(int i = 0; i < std::min<int>(a: pJoystick->GetNumAxes(), b: NUM_JOYSTICK_AXES); i++)
713 {
714 View.HSplitTop(Cut: BUTTON_SPACING, pTop: nullptr, pBottom: &View);
715 View.HSplitTop(Cut: BUTTON_HEIGHT, pTop: &Row, pBottom: &View);
716 if(!m_SettingsScrollRegion.AddRect(Rect: Row))
717 {
718 continue;
719 }
720 Row.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
721 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
722 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
723 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
724 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
725 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
726
727 const bool Active = g_Config.m_InpControllerX == i || g_Config.m_InpControllerY == i;
728
729 // Axis label
730 char aLabel[16];
731 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "%d", i + 1);
732 SLabelProperties LabelProps;
733 if(!Active)
734 {
735 LabelProps.SetColor(ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f));
736 }
737 Ui()->DoLabel(pRect: &Axis, pText: aLabel, Size: FONT_SIZE, Align: TEXTALIGN_MC, LabelProps);
738
739 // Axis status
740 Status.HMargin(Cut: 7.0f, pOtherRect: &Status);
741 RenderJoystickBar(pRect: &Status, Current: (pJoystick->GetAxisValue(Axis: i) + 1.0f) / 2.0f, Tolerance: g_Config.m_InpControllerTolerance / 50.0f, Active);
742
743 // Bind to X/Y
744 CUIRect AimBindX, AimBindY;
745 AimBind.VSplitMid(pLeft: &AimBindX, pRight: &AimBindY);
746 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aaJoystickAxisCheckboxIds[i][0], pText: "X", Checked: g_Config.m_InpControllerX == i, pRect: &AimBindX))
747 {
748 if(g_Config.m_InpControllerY == i)
749 g_Config.m_InpControllerY = g_Config.m_InpControllerX;
750 g_Config.m_InpControllerX = i;
751 }
752 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aaJoystickAxisCheckboxIds[i][1], pText: "Y", Checked: g_Config.m_InpControllerY == i, pRect: &AimBindY))
753 {
754 if(g_Config.m_InpControllerX == i)
755 g_Config.m_InpControllerX = g_Config.m_InpControllerY;
756 g_Config.m_InpControllerY = i;
757 }
758 }
759}
760
761void CMenusSettingsControls::RenderJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active)
762{
763 CUIRect Handle;
764 pRect->VSplitLeft(Cut: pRect->h, pLeft: &Handle, pRight: nullptr); // Slider size
765 Handle.x += (pRect->w - Handle.w) * Current;
766
767 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, Active ? 0.25f : 0.125f), Corners: IGraphics::CORNER_ALL, Rounding: pRect->h / 2.0f);
768
769 CUIRect ToleranceArea = *pRect;
770 ToleranceArea.w *= Tolerance;
771 ToleranceArea.x += (pRect->w - ToleranceArea.w) / 2.0f;
772 const ColorRGBA ToleranceColor = Active ? ColorRGBA(0.8f, 0.35f, 0.35f, 1.0f) : ColorRGBA(0.7f, 0.5f, 0.5f, 1.0f);
773 ToleranceArea.Draw(Color: ToleranceColor, Corners: IGraphics::CORNER_ALL, Rounding: ToleranceArea.h / 2.0f);
774
775 const ColorRGBA SliderColor = Active ? ColorRGBA(0.95f, 0.95f, 0.95f, 1.0f) : ColorRGBA(0.8f, 0.8f, 0.8f, 1.0f);
776 Handle.Draw(Color: SliderColor, Corners: IGraphics::CORNER_ALL, Rounding: Handle.h / 2.0f);
777}
778
779void CMenus::ResetSettingsControls()
780{
781 GameClient()->m_Binds.SetDefaults();
782
783 g_Config.m_InpMousesens = 200;
784 g_Config.m_UiMousesens = 200;
785
786 g_Config.m_InpControllerEnable = 0;
787 g_Config.m_InpControllerGUID[0] = '\0';
788 g_Config.m_InpControllerAbsolute = 0;
789 g_Config.m_InpControllerSens = 100;
790 g_Config.m_InpControllerX = 0;
791 g_Config.m_InpControllerY = 1;
792 g_Config.m_InpControllerTolerance = 5;
793 g_Config.m_UiControllerSens = 100;
794}
795