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