1use super::{RESET_BUTTONS_HEIGHT, RESET_BUTTONS_WIDTH};
2
3use crate::{
4 GlobalState,
5 game_input::GameInput,
6 hud::{ERROR_COLOR, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR, img_ids::Imgs},
7 session::settings_change::Control::{self as ControlChange, *},
8 ui::{ToggleButton, fonts::Fonts},
9 window::{MenuInput, RemappingMode},
10};
11use conrod_core::{
12 Borderable, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, color,
13 position::Relative,
14 widget::{self, Button, DropDownList, Rectangle, Scrollbar, Text},
15 widget_ids,
16};
17use i18n::Localization;
18use std::sync::LazyLock;
19use strum::IntoEnumIterator;
20
21widget_ids! {
22 struct Ids {
23 window,
24 window_r,
25 window_scrollbar,
26 reset_controls_button,
27 keybind_helper,
28 gamepad_mode_button,
29 gamepad_option_dropdown,
30 controls_alignment_rectangle,
31 controls_texts[],
32 controls_buttons[],
33 gamelayer_mod1_checkbox,
34 gamelayer_mod2_checkbox,
35 gamelayer_mod1_text,
36 gamelayer_mod2_text,
37 }
38}
39
40#[derive(WidgetCommon)]
41pub struct Controls<'a> {
42 global_state: &'a GlobalState,
43 imgs: &'a Imgs,
44 fonts: &'a Fonts,
45 localized_strings: &'a Localization,
46 #[conrod(common_builder)]
47 common: widget::CommonBuilder,
48}
49impl<'a> Controls<'a> {
50 pub fn new(
51 global_state: &'a GlobalState,
52 imgs: &'a Imgs,
53 fonts: &'a Fonts,
54 localized_strings: &'a Localization,
55 ) -> Self {
56 Self {
57 global_state,
58 imgs,
59 fonts,
60 localized_strings,
61 common: widget::CommonBuilder::default(),
62 }
63 }
64}
65
66#[derive(PartialEq, Clone, Copy)]
67pub enum BindingMode {
68 Keyboard,
69 Gamepad,
70}
71#[derive(Clone, Copy)]
72pub enum GamepadBindingOption {
73 GameButtons,
74 GameLayers,
75 MenuButtons,
76}
77
78pub struct State {
79 ids: Ids,
80 pub binding_mode: BindingMode,
81 pub gamepad_binding_option: GamepadBindingOption,
82}
83
84static SORTED_GAMEINPUTS: LazyLock<Vec<GameInput>> = LazyLock::new(|| {
85 let mut bindings_vec: Vec<GameInput> = GameInput::iter().collect();
86 bindings_vec.sort();
87 bindings_vec
88});
89static SORTED_MENUINPUTS: LazyLock<Vec<MenuInput>> = LazyLock::new(|| {
90 let mut bindings_vec: Vec<MenuInput> = MenuInput::iter().collect();
91 bindings_vec.sort();
92 bindings_vec
93});
94
95impl Widget for Controls<'_> {
96 type Event = Vec<ControlChange>;
97 type State = State;
98 type Style = ();
99
100 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
101 State {
102 ids: Ids::new(id_gen),
103 binding_mode: BindingMode::Keyboard,
104 gamepad_binding_option: GamepadBindingOption::GameButtons,
105 }
106 }
107
108 fn style(&self) -> Self::Style {}
109
110 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
111 common_base::prof_span!("Controls::update");
112 let widget::UpdateArgs { state, ui, .. } = args;
113
114 let mut events = Vec::new();
115
116 Rectangle::fill_with(args.rect.dim(), color::TRANSPARENT)
117 .xy(args.rect.xy())
118 .graphics_for(args.id)
119 .scroll_kids()
120 .scroll_kids_vertically()
121 .set(state.ids.window, ui);
122 Rectangle::fill_with([args.rect.w() / 2.0, args.rect.h()], color::TRANSPARENT)
123 .top_right()
124 .parent(state.ids.window)
125 .set(state.ids.window_r, ui);
126 Scrollbar::y_axis(state.ids.window)
127 .thickness(5.0)
128 .rgba(0.33, 0.33, 0.33, 1.0)
129 .set(state.ids.window_scrollbar, ui);
130
131 let binding_mode = state.binding_mode;
133 let gamepad_binding_option = state.gamepad_binding_option;
134
135 let mut resize_ids = |len| {
137 if len > state.ids.controls_texts.len() || len > state.ids.controls_buttons.len() {
138 state.update(|s| {
139 s.ids
140 .controls_texts
141 .resize(len, &mut ui.widget_id_generator());
142 s.ids
143 .controls_buttons
144 .resize(len, &mut ui.widget_id_generator());
145 });
146 }
147 };
148
149 let mut previous_element_id = None;
151
152 if let BindingMode::Gamepad = binding_mode {
153 match gamepad_binding_option {
154 GamepadBindingOption::GameButtons => {
155 let gamepad_controls = &self.global_state.settings.controller;
156
157 resize_ids(SORTED_GAMEINPUTS.len());
158
159 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
161 state
162 .ids
163 .controls_texts
164 .iter()
165 .zip(state.ids.controls_buttons.iter()),
166 ) {
167 let (input_string, input_color) =
168 if let RemappingMode::RemapGamepadButtons(r_input) =
169 self.global_state.window.remapping_mode
170 && r_input == *game_input
171 {
172 (
173 self.localized_strings
174 .get_msg("hud-settings-awaitingkey")
175 .into_owned(),
176 TEXT_COLOR,
177 )
178 } else if let Some(button) =
179 gamepad_controls.get_game_button_binding(*game_input)
180 {
181 (
182 format!(
183 "{} {}",
184 button.display_string(self.localized_strings),
185 button
186 .try_shortened()
187 .map_or("".to_owned(), |short| format!("({})", short))
188 ),
189 if gamepad_controls.game_button_has_conflicting_bindings(button)
190 {
191 TEXT_BIND_CONFLICT_COLOR
192 } else {
193 TEXT_COLOR
194 },
195 )
196 } else {
197 (
198 self.localized_strings
199 .get_msg("hud-settings-unbound")
200 .into_owned(),
201 ERROR_COLOR,
202 )
203 };
204 let loc_key = self
205 .localized_strings
206 .get_msg(game_input.get_localization_key());
207 let text_widget = Text::new(&loc_key)
208 .color(TEXT_COLOR)
209 .font_id(self.fonts.cyri.conrod_id)
210 .font_size(self.fonts.cyri.scale(18));
211 let button_widget = Button::new()
212 .label(&input_string)
213 .label_color(input_color)
214 .label_font_id(self.fonts.cyri.conrod_id)
215 .label_font_size(self.fonts.cyri.scale(15))
216 .w(150.0)
217 .rgba(0.0, 0.0, 0.0, 0.0)
218 .border_rgba(0.0, 0.0, 0.0, 255.0)
219 .label_y(Relative::Scalar(3.0));
220 let text_widget = match previous_element_id {
222 None => {
223 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
224 },
225 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
226 };
227 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
228 text_widget.set(text_id, ui);
229 button_widget
230 .right_from(text_id, 350.0 - text_width)
231 .set(button_id, ui);
232
233 for _ in ui.widget_input(button_id).clicks().left() {
234 events.push(ChangeBindingGamepadButton(*game_input));
235 }
236 for _ in ui.widget_input(button_id).clicks().right() {
237 events.push(RemoveBindingGamepadButton(*game_input));
238 }
239 previous_element_id = Some(text_id);
241 }
242 },
243 GamepadBindingOption::GameLayers => {
244 let gamepad_controls = &self.global_state.settings.controller;
245
246 resize_ids(SORTED_GAMEINPUTS.len());
247
248 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
250 state
251 .ids
252 .controls_texts
253 .iter()
254 .zip(state.ids.controls_buttons.iter()),
255 ) {
256 let (input_string, input_color) =
257 if let RemappingMode::RemapGamepadLayers(r_input) =
258 self.global_state.window.remapping_mode
259 && r_input == *game_input
260 {
261 (
262 self.localized_strings
263 .get_msg("hud-settings-awaitingkey")
264 .into_owned(),
265 TEXT_COLOR,
266 )
267 } else if let Some(entry) =
268 gamepad_controls.get_layer_button_binding(*game_input)
269 {
270 (
271 entry.display_string(self.localized_strings),
272 if gamepad_controls.layer_entry_has_conflicting_bindings(entry)
273 {
274 TEXT_BIND_CONFLICT_COLOR
275 } else {
276 TEXT_COLOR
277 },
278 )
279 } else {
280 (
281 self.localized_strings
282 .get_msg("hud-settings-unbound")
283 .into_owned(),
284 ERROR_COLOR,
285 )
286 };
287 let loc_key = self
288 .localized_strings
289 .get_msg(game_input.get_localization_key());
290 let text_widget = Text::new(&loc_key)
291 .color(TEXT_COLOR)
292 .font_id(self.fonts.cyri.conrod_id)
293 .font_size(self.fonts.cyri.scale(18));
294 let button_widget = Button::new()
295 .label(&input_string)
296 .label_color(input_color)
297 .label_font_id(self.fonts.cyri.conrod_id)
298 .label_font_size(self.fonts.cyri.scale(15))
299 .w(150.0)
300 .rgba(0.0, 0.0, 0.0, 0.0)
301 .border_rgba(0.0, 0.0, 0.0, 255.0)
302 .label_y(Relative::Scalar(3.0));
303 let text_widget = match previous_element_id {
305 None => {
306 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
307 },
308 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
309 };
310 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
311 text_widget.set(text_id, ui);
312 button_widget
313 .right_from(text_id, 350.0 - text_width)
314 .set(button_id, ui);
315
316 for _ in ui.widget_input(button_id).clicks().left() {
317 events.push(ChangeBindingGamepadLayer(*game_input));
318 }
319 for _ in ui.widget_input(button_id).clicks().right() {
320 events.push(RemoveBindingGamepadLayer(*game_input));
321 }
322 previous_element_id = Some(text_id);
324 }
325 },
326 GamepadBindingOption::MenuButtons => {
327 let gamepad_controls = &self.global_state.settings.controller;
328
329 resize_ids(SORTED_MENUINPUTS.len());
330
331 for (menu_input, (&text_id, &button_id)) in SORTED_MENUINPUTS.iter().zip(
333 state
334 .ids
335 .controls_texts
336 .iter()
337 .zip(state.ids.controls_buttons.iter()),
338 ) {
339 let (input_string, input_color) =
340 if let RemappingMode::RemapGamepadMenu(r_input) =
341 self.global_state.window.remapping_mode
342 && r_input == *menu_input
343 {
344 (
345 self.localized_strings
346 .get_msg("hud-settings-awaitingkey")
347 .into_owned(),
348 TEXT_COLOR,
349 )
350 } else if let Some(button) =
351 gamepad_controls.get_menu_button_binding(*menu_input)
352 {
353 (
354 format!(
355 "{} {}",
356 button.display_string(self.localized_strings),
357 button
358 .try_shortened()
359 .map_or("".to_owned(), |short| format!("({})", short))
360 ),
361 if gamepad_controls.menu_button_has_conflicting_bindings(button)
362 {
363 TEXT_BIND_CONFLICT_COLOR
364 } else {
365 TEXT_COLOR
366 },
367 )
368 } else {
369 (
370 self.localized_strings
371 .get_msg("hud-settings-unbound")
372 .into_owned(),
373 ERROR_COLOR,
374 )
375 };
376 let loc_key = self
377 .localized_strings
378 .get_msg(menu_input.get_localization_key());
379 let text_widget = Text::new(&loc_key)
380 .color(TEXT_COLOR)
381 .font_id(self.fonts.cyri.conrod_id)
382 .font_size(self.fonts.cyri.scale(18));
383 let button_widget = Button::new()
384 .label(&input_string)
385 .label_color(input_color)
386 .label_font_id(self.fonts.cyri.conrod_id)
387 .label_font_size(self.fonts.cyri.scale(15))
388 .w(150.0)
389 .rgba(0.0, 0.0, 0.0, 0.0)
390 .border_rgba(0.0, 0.0, 0.0, 255.0)
391 .label_y(Relative::Scalar(3.0));
392 let text_widget = match previous_element_id {
394 None => {
395 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
396 },
397 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
398 };
399 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
400 text_widget.set(text_id, ui);
401 button_widget
402 .right_from(text_id, 350.0 - text_width)
403 .set(button_id, ui);
404
405 for _ in ui.widget_input(button_id).clicks().left() {
406 events.push(ChangeBindingGamepadMenu(*menu_input));
407 }
408 for _ in ui.widget_input(button_id).clicks().right() {
409 events.push(RemoveBindingGamepadMenu(*menu_input));
410 }
411 previous_element_id = Some(text_id);
413 }
414 },
415 }
416 } else {
417 let controls = &self.global_state.settings.controls;
418
419 resize_ids(SORTED_GAMEINPUTS.len());
420
421 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
423 state
424 .ids
425 .controls_texts
426 .iter()
427 .zip(state.ids.controls_buttons.iter()),
428 ) {
429 let (key_string, key_color) = if let RemappingMode::RemapKeyboard(r_input) =
430 self.global_state.window.remapping_mode
431 && r_input == *game_input
432 {
433 (
434 self.localized_strings
435 .get_msg("hud-settings-awaitingkey")
436 .into_owned(),
437 TEXT_COLOR,
438 )
439 } else if let Some(key) = controls.get_binding(*game_input) {
440 (
441 format!(
442 "{} {}",
443 key.display_string(),
444 key.try_shortened()
445 .map_or("".to_owned(), |short| format!("({})", short))
446 ),
447 if controls.has_conflicting_bindings(key) {
448 TEXT_BIND_CONFLICT_COLOR
449 } else {
450 TEXT_COLOR
451 },
452 )
453 } else {
454 (
455 self.localized_strings
456 .get_msg("hud-settings-unbound")
457 .into_owned(),
458 ERROR_COLOR,
459 )
460 };
461 let loc_key = self
462 .localized_strings
463 .get_msg(game_input.get_localization_key());
464 let text_widget = Text::new(&loc_key)
465 .color(TEXT_COLOR)
466 .font_id(self.fonts.cyri.conrod_id)
467 .font_size(self.fonts.cyri.scale(18));
468 let button_widget = Button::new()
469 .label(&key_string)
470 .label_color(key_color)
471 .label_font_id(self.fonts.cyri.conrod_id)
472 .label_font_size(self.fonts.cyri.scale(15))
473 .w(150.0)
474 .rgba(0.0, 0.0, 0.0, 0.0)
475 .border_rgba(0.0, 0.0, 0.0, 255.0)
476 .label_y(Relative::Scalar(3.0));
477 let text_widget = match previous_element_id {
479 None => text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0),
480 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
481 };
482 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
483 text_widget.set(text_id, ui);
484 button_widget
485 .right_from(text_id, 350.0 - text_width)
486 .set(button_id, ui);
487
488 for _ in ui.widget_input(button_id).clicks().left() {
489 events.push(ChangeBindingKeyboard(*game_input));
490 }
491 for _ in ui.widget_input(button_id).clicks().right() {
492 events.push(RemoveBindingKeyboard(*game_input));
493 }
494 previous_element_id = Some(text_id);
496 }
497 }
498
499 if let Some(prev_id) = previous_element_id {
501 if Button::image(self.imgs.button)
502 .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT)
503 .hover_image(self.imgs.button_hover)
504 .press_image(self.imgs.button_press)
505 .down_from(prev_id, 20.0)
506 .label(
507 &self
508 .localized_strings
509 .get_msg("hud-settings-reset_keybinds"),
510 )
511 .label_font_size(self.fonts.cyri.scale(14))
512 .label_color(TEXT_COLOR)
513 .label_font_id(self.fonts.cyri.conrod_id)
514 .label_y(Relative::Scalar(2.0))
515 .set(state.ids.reset_controls_button, ui)
516 .was_clicked()
517 {
518 if state.binding_mode != BindingMode::Gamepad {
519 events.push(ResetKeyBindingsKeyboard);
520 } else {
521 events.push(ResetKeyBindingsGamepad);
524 }
525 }
526 previous_element_id = Some(state.ids.reset_controls_button)
527 }
528
529 let offset = ui
530 .widget_graph()
531 .widget(state.ids.window)
532 .and_then(|widget| {
533 widget
534 .maybe_y_scroll_state
535 .as_ref()
536 .map(|scroll| scroll.offset)
537 })
538 .unwrap_or(0.0);
539
540 let keybind_helper_text = self
541 .localized_strings
542 .get_msg("hud-settings-keybind-helper");
543 let keybind_helper = Text::new(&keybind_helper_text)
544 .color(TEXT_COLOR)
545 .font_id(self.fonts.cyri.conrod_id)
546 .font_size(self.fonts.cyri.scale(18));
547 keybind_helper
548 .top_right_with_margins_on(state.ids.window, offset + 5.0, 10.0)
549 .set(state.ids.keybind_helper, ui);
550
551 if let BindingMode::Gamepad = state.binding_mode {
552 let game_buttons = &self.localized_strings.get_msg("hud-settings-game_buttons");
553 let game_layers = &self.localized_strings.get_msg("hud-settings-game_layers");
554 let menu_buttons = &self.localized_strings.get_msg("hud-settings-menu_buttons");
555
556 let binding_mode_list = [game_buttons, game_layers, menu_buttons];
557 if let Some(clicked) = DropDownList::new(
558 &binding_mode_list,
559 Some(state.gamepad_binding_option as usize),
560 )
561 .label_color(TEXT_COLOR)
562 .label_font_id(self.fonts.cyri.conrod_id)
563 .label_font_size(self.fonts.cyri.scale(15))
564 .w_h(125.0, 35.0)
565 .rgba(0.0, 0.0, 0.0, 0.0)
566 .border_rgba(0.0, 0.0, 0.0, 255.0)
567 .label_y(Relative::Scalar(1.0))
568 .down_from(state.ids.gamepad_mode_button, 10.0)
569 .set(state.ids.gamepad_option_dropdown, ui)
570 {
571 match clicked {
572 0 => {
573 state.update(|s| {
574 s.gamepad_binding_option = GamepadBindingOption::GameButtons
575 });
576 events.push(ResetBindingMode);
577 },
578 1 => {
579 state.update(|s| {
580 s.gamepad_binding_option = GamepadBindingOption::GameLayers
581 });
582 events.push(ResetBindingMode);
583 },
584 2 => {
585 state.update(|s| {
586 s.gamepad_binding_option = GamepadBindingOption::MenuButtons
587 });
588 events.push(ResetBindingMode);
589 },
590 _ => {
591 state.update(|s| {
592 s.gamepad_binding_option = GamepadBindingOption::GameButtons
593 });
594 events.push(ResetBindingMode);
595 },
596 }
597 }
598
599 if let GamepadBindingOption::GameLayers = state.gamepad_binding_option {
601 let mod1_text = "RB";
603 let gamelayer_mod1 = ToggleButton::new(
604 self.global_state.window.gamelayer_mod1,
605 self.imgs.checkbox,
606 self.imgs.checkbox_checked,
607 )
608 .down_from(state.ids.gamepad_option_dropdown, 10.0)
609 .w_h(18.0, 18.0)
610 .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
611 .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
612 .set(state.ids.gamelayer_mod1_checkbox, ui);
613 if self.global_state.window.gamelayer_mod1 != gamelayer_mod1 {
614 events.push(GameLayerMod1(gamelayer_mod1));
615 }
616 Text::new(mod1_text)
617 .right_from(state.ids.gamelayer_mod1_checkbox, 10.0)
618 .font_size(self.fonts.cyri.scale(15))
619 .font_id(self.fonts.cyri.conrod_id)
620 .color(TEXT_COLOR)
621 .set(state.ids.gamelayer_mod1_text, ui);
622
623 let mod2_text = "LB";
625 let gamelayer_mod2 = ToggleButton::new(
626 self.global_state.window.gamelayer_mod2,
627 self.imgs.checkbox,
628 self.imgs.checkbox_checked,
629 )
630 .down_from(state.ids.gamelayer_mod1_checkbox, 10.0)
631 .w_h(18.0, 18.0)
632 .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
633 .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
634 .set(state.ids.gamelayer_mod2_checkbox, ui);
635 if self.global_state.window.gamelayer_mod2 != gamelayer_mod2 {
636 events.push(GameLayerMod2(gamelayer_mod2));
637 }
638 Text::new(mod2_text)
639 .right_from(state.ids.gamelayer_mod2_checkbox, 10.0)
640 .font_size(self.fonts.cyri.scale(15))
641 .font_id(self.fonts.cyri.conrod_id)
642 .color(TEXT_COLOR)
643 .set(state.ids.gamelayer_mod2_text, ui);
644 }
645 }
646
647 let gamepad = &self.localized_strings.get_msg("hud-settings-gamepad");
648 let keyboard = &self.localized_strings.get_msg("hud-settings-keyboard");
649
650 let binding_mode_toggle_widget = Button::new()
651 .label(if let BindingMode::Gamepad = state.binding_mode {
652 gamepad
653 } else {
654 keyboard
655 })
656 .label_color(TEXT_COLOR)
657 .label_font_id(self.fonts.cyri.conrod_id)
658 .label_font_size(self.fonts.cyri.scale(15))
659 .w_h(125.0, 35.0)
660 .rgba(0.0, 0.0, 0.0, 0.0)
661 .border_rgba(0.0, 0.0, 0.0, 255.0)
662 .label_y(Relative::Scalar(1.0));
663 if binding_mode_toggle_widget
664 .down_from(state.ids.keybind_helper, 10.0)
665 .align_right_of(state.ids.keybind_helper)
666 .set(state.ids.gamepad_mode_button, ui)
667 .was_clicked()
668 {
669 if let BindingMode::Keyboard = state.binding_mode {
670 state.update(|s| s.binding_mode = BindingMode::Gamepad);
671 events.push(ResetBindingMode);
672 } else {
673 state.update(|s| s.binding_mode = BindingMode::Keyboard);
674 events.push(ResetBindingMode);
675 }
676 }
677
678 if let Some(prev_id) = previous_element_id {
680 Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT)
681 .down_from(prev_id, 10.0)
682 .set(state.ids.controls_alignment_rectangle, ui);
683 }
684
685 events
686 }
687}