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 as ControlChange, Control::*},
8 ui::fonts::Fonts,
9 window::MenuInput,
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 }
34}
35
36#[derive(WidgetCommon)]
37pub struct Controls<'a> {
38 global_state: &'a GlobalState,
39 imgs: &'a Imgs,
40 fonts: &'a Fonts,
41 localized_strings: &'a Localization,
42 #[conrod(common_builder)]
43 common: widget::CommonBuilder,
44}
45impl<'a> Controls<'a> {
46 pub fn new(
47 global_state: &'a GlobalState,
48 imgs: &'a Imgs,
49 fonts: &'a Fonts,
50 localized_strings: &'a Localization,
51 ) -> Self {
52 Self {
53 global_state,
54 imgs,
55 fonts,
56 localized_strings,
57 common: widget::CommonBuilder::default(),
58 }
59 }
60}
61
62#[derive(PartialEq, Clone, Copy)]
63pub enum BindingMode {
64 Keyboard,
65 Gamepad,
66}
67#[derive(Clone, Copy)]
68pub enum GamepadBindingOption {
69 GameButtons,
70 GameLayers,
71 MenuButtons,
72}
73
74pub struct State {
75 ids: Ids,
76 pub binding_mode: BindingMode,
77 pub gamepad_binding_option: GamepadBindingOption,
78}
79
80static SORTED_GAMEINPUTS: LazyLock<Vec<GameInput>> = LazyLock::new(|| {
81 let mut bindings_vec: Vec<GameInput> = GameInput::iter().collect();
82 bindings_vec.sort();
83 bindings_vec
84});
85static SORTED_MENUINPUTS: LazyLock<Vec<MenuInput>> = LazyLock::new(|| {
86 let mut bindings_vec: Vec<MenuInput> = MenuInput::iter().collect();
87 bindings_vec.sort();
88 bindings_vec
89});
90
91impl Widget for Controls<'_> {
92 type Event = Vec<ControlChange>;
93 type State = State;
94 type Style = ();
95
96 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
97 State {
98 ids: Ids::new(id_gen),
99 binding_mode: BindingMode::Keyboard,
100 gamepad_binding_option: GamepadBindingOption::GameButtons,
101 }
102 }
103
104 fn style(&self) -> Self::Style {}
105
106 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
107 common_base::prof_span!("Controls::update");
108 let widget::UpdateArgs { state, ui, .. } = args;
109
110 let mut events = Vec::new();
111 let key_layout = &self.global_state.window.key_layout;
112
113 Rectangle::fill_with(args.rect.dim(), color::TRANSPARENT)
114 .xy(args.rect.xy())
115 .graphics_for(args.id)
116 .scroll_kids()
117 .scroll_kids_vertically()
118 .set(state.ids.window, ui);
119 Rectangle::fill_with([args.rect.w() / 2.0, args.rect.h()], color::TRANSPARENT)
120 .top_right()
121 .parent(state.ids.window)
122 .set(state.ids.window_r, ui);
123 Scrollbar::y_axis(state.ids.window)
124 .thickness(5.0)
125 .rgba(0.33, 0.33, 0.33, 1.0)
126 .set(state.ids.window_scrollbar, ui);
127
128 let binding_mode = state.binding_mode;
130 let gamepad_binding_option = state.gamepad_binding_option;
131
132 let mut resize_ids = |len| {
134 if len > state.ids.controls_texts.len() || len > state.ids.controls_buttons.len() {
135 state.update(|s| {
136 s.ids
137 .controls_texts
138 .resize(len, &mut ui.widget_id_generator());
139 s.ids
140 .controls_buttons
141 .resize(len, &mut ui.widget_id_generator());
142 });
143 }
144 };
145
146 let mut previous_element_id = None;
148
149 if let BindingMode::Gamepad = binding_mode {
150 match gamepad_binding_option {
151 GamepadBindingOption::GameButtons => {
152 let gamepad_controls = &self.global_state.window.controller_settings;
153
154 resize_ids(SORTED_GAMEINPUTS.len());
155
156 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
158 state
159 .ids
160 .controls_texts
161 .iter()
162 .zip(state.ids.controls_buttons.iter()),
163 ) {
164 let (input_string, input_color) =
165 if let Some(button) = gamepad_controls.get_game_button_binding(*game_input) {
167 (
168 format!(
169 "{} {}",
170 button.display_string(self.localized_strings),
171 button.try_shortened()
172 .map_or("".to_owned(), |short| format!("({})", short))
173 ),
174 if gamepad_controls.game_button_has_conflicting_bindings(button) {
175 TEXT_BIND_CONFLICT_COLOR
176 } else {
177 TEXT_COLOR
178 },
179 )
180 } else {
181 (
182 self.localized_strings
183 .get_msg("hud-settings-unbound")
184 .into_owned(),
185 ERROR_COLOR,
186 )
187 };
188 let loc_key = self
189 .localized_strings
190 .get_msg(game_input.get_localization_key());
191 let text_widget = Text::new(&loc_key)
192 .color(TEXT_COLOR)
193 .font_id(self.fonts.cyri.conrod_id)
194 .font_size(self.fonts.cyri.scale(18));
195 let button_widget = Button::new()
196 .label(&input_string)
197 .label_color(input_color)
198 .label_font_id(self.fonts.cyri.conrod_id)
199 .label_font_size(self.fonts.cyri.scale(15))
200 .w(150.0)
201 .rgba(0.0, 0.0, 0.0, 0.0)
202 .border_rgba(0.0, 0.0, 0.0, 255.0)
203 .label_y(Relative::Scalar(3.0));
204 let text_widget = match previous_element_id {
206 None => {
207 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
208 },
209 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
210 };
211 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
212 text_widget.set(text_id, ui);
213 if button_widget
214 .right_from(text_id, 350.0 - text_width)
215 .set(button_id, ui)
216 .was_clicked()
217 {
218 }
220 previous_element_id = Some(text_id);
222 }
223 },
224 GamepadBindingOption::GameLayers => {
225 let gamepad_controls = &self.global_state.window.controller_settings;
226
227 resize_ids(SORTED_GAMEINPUTS.len());
228
229 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
231 state
232 .ids
233 .controls_texts
234 .iter()
235 .zip(state.ids.controls_buttons.iter()),
236 ) {
237 let (input_string, input_color) =
238 if let Some(entry) = gamepad_controls.get_layer_button_binding(*game_input) {
240 (
241 entry.display_string(self.localized_strings),
242 if gamepad_controls.layer_entry_has_conflicting_bindings(entry) {
243 TEXT_BIND_CONFLICT_COLOR
244 } else {
245 TEXT_COLOR
246 },
247 )
248 } else {
249 (
250 self.localized_strings
251 .get_msg("hud-settings-unbound")
252 .into_owned(),
253 ERROR_COLOR,
254 )
255 };
256 let loc_key = self
257 .localized_strings
258 .get_msg(game_input.get_localization_key());
259 let text_widget = Text::new(&loc_key)
260 .color(TEXT_COLOR)
261 .font_id(self.fonts.cyri.conrod_id)
262 .font_size(self.fonts.cyri.scale(18));
263 let button_widget = Button::new()
264 .label(&input_string)
265 .label_color(input_color)
266 .label_font_id(self.fonts.cyri.conrod_id)
267 .label_font_size(self.fonts.cyri.scale(15))
268 .w(150.0)
269 .rgba(0.0, 0.0, 0.0, 0.0)
270 .border_rgba(0.0, 0.0, 0.0, 255.0)
271 .label_y(Relative::Scalar(3.0));
272 let text_widget = match previous_element_id {
274 None => {
275 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
276 },
277 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
278 };
279 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
280 text_widget.set(text_id, ui);
281 if button_widget
282 .right_from(text_id, 350.0 - text_width)
283 .set(button_id, ui)
284 .was_clicked()
285 {
286 }
288 previous_element_id = Some(text_id);
290 }
291 },
292 GamepadBindingOption::MenuButtons => {
293 let gamepad_controls = &self.global_state.window.controller_settings;
294
295 resize_ids(SORTED_MENUINPUTS.len());
296
297 for (menu_input, (&text_id, &button_id)) in SORTED_MENUINPUTS.iter().zip(
299 state
300 .ids
301 .controls_texts
302 .iter()
303 .zip(state.ids.controls_buttons.iter()),
304 ) {
305 let (input_string, input_color) =
306 if let Some(button) = gamepad_controls.get_menu_button_binding(*menu_input) {
308 (
309 format!(
310 "{} {}",
311 button.display_string(self.localized_strings),
312 button.try_shortened()
313 .map_or("".to_owned(), |short| format!("({})", short))
314 ),
315 if gamepad_controls.menu_button_has_conflicting_bindings(button) {
316 TEXT_BIND_CONFLICT_COLOR
317 } else {
318 TEXT_COLOR
319 },
320 )
321 } else {
322 (
323 self.localized_strings
324 .get_msg("hud-settings-unbound")
325 .into_owned(),
326 ERROR_COLOR,
327 )
328 };
329 let loc_key = self
330 .localized_strings
331 .get_msg(menu_input.get_localization_key());
332 let text_widget = Text::new(&loc_key)
333 .color(TEXT_COLOR)
334 .font_id(self.fonts.cyri.conrod_id)
335 .font_size(self.fonts.cyri.scale(18));
336 let button_widget = Button::new()
337 .label(&input_string)
338 .label_color(input_color)
339 .label_font_id(self.fonts.cyri.conrod_id)
340 .label_font_size(self.fonts.cyri.scale(15))
341 .w(150.0)
342 .rgba(0.0, 0.0, 0.0, 0.0)
343 .border_rgba(0.0, 0.0, 0.0, 255.0)
344 .label_y(Relative::Scalar(3.0));
345 let text_widget = match previous_element_id {
347 None => {
348 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
349 },
350 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
351 };
352 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
353 text_widget.set(text_id, ui);
354 if button_widget
355 .right_from(text_id, 350.0 - text_width)
356 .set(button_id, ui)
357 .was_clicked()
358 {
359 }
361 previous_element_id = Some(text_id);
363 }
364 },
365 }
366 } else {
367 let controls = &self.global_state.settings.controls;
368
369 resize_ids(SORTED_GAMEINPUTS.len());
370
371 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
373 state
374 .ids
375 .controls_texts
376 .iter()
377 .zip(state.ids.controls_buttons.iter()),
378 ) {
379 let (key_string, key_color) =
380 if self.global_state.window.remapping_keybindings == Some(*game_input) {
381 (
382 self.localized_strings
383 .get_msg("hud-settings-awaitingkey")
384 .into_owned(),
385 TEXT_COLOR,
386 )
387 } else if let Some(key) = controls.get_binding(*game_input) {
388 (
389 format!(
390 "{} {}",
391 key.display_string(key_layout),
392 key.try_shortened(key_layout)
393 .map_or("".to_owned(), |short| format!("({})", short))
394 ),
395 if controls.has_conflicting_bindings(key) {
396 TEXT_BIND_CONFLICT_COLOR
397 } else {
398 TEXT_COLOR
399 },
400 )
401 } else {
402 (
403 self.localized_strings
404 .get_msg("hud-settings-unbound")
405 .into_owned(),
406 ERROR_COLOR,
407 )
408 };
409 let loc_key = self
410 .localized_strings
411 .get_msg(game_input.get_localization_key());
412 let text_widget = Text::new(&loc_key)
413 .color(TEXT_COLOR)
414 .font_id(self.fonts.cyri.conrod_id)
415 .font_size(self.fonts.cyri.scale(18));
416 let button_widget = Button::new()
417 .label(&key_string)
418 .label_color(key_color)
419 .label_font_id(self.fonts.cyri.conrod_id)
420 .label_font_size(self.fonts.cyri.scale(15))
421 .w(150.0)
422 .rgba(0.0, 0.0, 0.0, 0.0)
423 .border_rgba(0.0, 0.0, 0.0, 255.0)
424 .label_y(Relative::Scalar(3.0));
425 let text_widget = match previous_element_id {
427 None => text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0),
428 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
429 };
430 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
431 text_widget.set(text_id, ui);
432 button_widget
433 .right_from(text_id, 350.0 - text_width)
434 .set(button_id, ui);
435
436 for _ in ui.widget_input(button_id).clicks().left() {
437 events.push(ChangeBinding(*game_input));
438 }
439 for _ in ui.widget_input(button_id).clicks().right() {
440 events.push(RemoveBinding(*game_input));
441 }
442 previous_element_id = Some(text_id);
444 }
445 }
446
447 if let Some(prev_id) = previous_element_id {
449 if Button::image(self.imgs.button)
450 .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT)
451 .hover_image(self.imgs.button_hover)
452 .press_image(self.imgs.button_press)
453 .down_from(prev_id, 20.0)
454 .label(
455 &self
456 .localized_strings
457 .get_msg("hud-settings-reset_keybinds"),
458 )
459 .label_font_size(self.fonts.cyri.scale(14))
460 .label_color(TEXT_COLOR)
461 .label_font_id(self.fonts.cyri.conrod_id)
462 .label_y(Relative::Scalar(2.0))
463 .set(state.ids.reset_controls_button, ui)
464 .was_clicked() &&
465 state.binding_mode != BindingMode::Gamepad
467 {
468 events.push(ResetKeyBindings);
469 }
470 previous_element_id = Some(state.ids.reset_controls_button)
471 }
472
473 let offset = ui
474 .widget_graph()
475 .widget(state.ids.window)
476 .and_then(|widget| {
477 widget
478 .maybe_y_scroll_state
479 .as_ref()
480 .map(|scroll| scroll.offset)
481 })
482 .unwrap_or(0.0);
483
484 let keybind_helper_text = self
485 .localized_strings
486 .get_msg("hud-settings-keybind-helper");
487 let keybind_helper = Text::new(&keybind_helper_text)
488 .color(TEXT_COLOR)
489 .font_id(self.fonts.cyri.conrod_id)
490 .font_size(self.fonts.cyri.scale(18));
491 keybind_helper
492 .top_right_with_margins_on(state.ids.window, offset + 5.0, 10.0)
493 .set(state.ids.keybind_helper, ui);
494
495 if let BindingMode::Gamepad = state.binding_mode {
496 let game_buttons = &self.localized_strings.get_msg("hud-settings-game_buttons");
497 let game_layers = &self.localized_strings.get_msg("hud-settings-game_layers");
498 let menu_buttons = &self.localized_strings.get_msg("hud-settings-menu_buttons");
499
500 let binding_mode_list = [game_buttons, game_layers, menu_buttons];
501 if let Some(clicked) = DropDownList::new(
502 &binding_mode_list,
503 Some(state.gamepad_binding_option as usize),
504 )
505 .label_color(TEXT_COLOR)
506 .label_font_id(self.fonts.cyri.conrod_id)
507 .label_font_size(self.fonts.cyri.scale(15))
508 .w(125.0)
509 .rgba(0.0, 0.0, 0.0, 0.0)
510 .border_rgba(0.0, 0.0, 0.0, 255.0)
511 .label_y(Relative::Scalar(1.0))
512 .down_from(state.ids.gamepad_mode_button, 10.0)
513 .set(state.ids.gamepad_option_dropdown, ui)
514 {
515 match clicked {
516 0 => {
517 state.update(|s| {
518 s.gamepad_binding_option = GamepadBindingOption::GameButtons
519 });
520 },
521 1 => {
522 state.update(|s| {
523 s.gamepad_binding_option = GamepadBindingOption::GameLayers
524 });
525 },
526 2 => {
527 state.update(|s| {
528 s.gamepad_binding_option = GamepadBindingOption::MenuButtons
529 });
530 },
531 _ => {
532 state.update(|s| {
533 s.gamepad_binding_option = GamepadBindingOption::GameButtons
534 });
535 },
536 }
537 }
538 }
539
540 let gamepad = &self.localized_strings.get_msg("hud-settings-gamepad");
541 let keyboard = &self.localized_strings.get_msg("hud-settings-keyboard");
542
543 let binding_mode_toggle_widget = Button::new()
544 .label(if let BindingMode::Gamepad = state.binding_mode {
545 gamepad
546 } else {
547 keyboard
548 })
549 .label_color(TEXT_COLOR)
550 .label_font_id(self.fonts.cyri.conrod_id)
551 .label_font_size(self.fonts.cyri.scale(15))
552 .w(125.0)
553 .rgba(0.0, 0.0, 0.0, 0.0)
554 .border_rgba(0.0, 0.0, 0.0, 255.0)
555 .label_y(Relative::Scalar(1.0));
556 if binding_mode_toggle_widget
557 .down_from(state.ids.keybind_helper, 10.0)
558 .align_right_of(state.ids.keybind_helper)
559 .set(state.ids.gamepad_mode_button, ui)
560 .was_clicked()
561 {
562 if let BindingMode::Keyboard = state.binding_mode {
563 state.update(|s| s.binding_mode = BindingMode::Gamepad);
564 } else {
565 state.update(|s| s.binding_mode = BindingMode::Keyboard);
566 }
567 }
568
569 if let Some(prev_id) = previous_element_id {
571 Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT)
572 .down_from(prev_id, 10.0)
573 .set(state.ids.controls_alignment_rectangle, ui);
574 }
575
576 events
577 }
578}