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
112 Rectangle::fill_with(args.rect.dim(), color::TRANSPARENT)
113 .xy(args.rect.xy())
114 .graphics_for(args.id)
115 .scroll_kids()
116 .scroll_kids_vertically()
117 .set(state.ids.window, ui);
118 Rectangle::fill_with([args.rect.w() / 2.0, args.rect.h()], color::TRANSPARENT)
119 .top_right()
120 .parent(state.ids.window)
121 .set(state.ids.window_r, ui);
122 Scrollbar::y_axis(state.ids.window)
123 .thickness(5.0)
124 .rgba(0.33, 0.33, 0.33, 1.0)
125 .set(state.ids.window_scrollbar, ui);
126
127 let binding_mode = state.binding_mode;
129 let gamepad_binding_option = state.gamepad_binding_option;
130
131 let mut resize_ids = |len| {
133 if len > state.ids.controls_texts.len() || len > state.ids.controls_buttons.len() {
134 state.update(|s| {
135 s.ids
136 .controls_texts
137 .resize(len, &mut ui.widget_id_generator());
138 s.ids
139 .controls_buttons
140 .resize(len, &mut ui.widget_id_generator());
141 });
142 }
143 };
144
145 let mut previous_element_id = None;
147
148 if let BindingMode::Gamepad = binding_mode {
149 match gamepad_binding_option {
150 GamepadBindingOption::GameButtons => {
151 let gamepad_controls = &self.global_state.window.controller_settings;
152
153 resize_ids(SORTED_GAMEINPUTS.len());
154
155 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
157 state
158 .ids
159 .controls_texts
160 .iter()
161 .zip(state.ids.controls_buttons.iter()),
162 ) {
163 let (input_string, input_color) =
164 if let Some(button) = gamepad_controls.get_game_button_binding(*game_input) {
166 (
167 format!(
168 "{} {}",
169 button.display_string(self.localized_strings),
170 button.try_shortened()
171 .map_or("".to_owned(), |short| format!("({})", short))
172 ),
173 if gamepad_controls.game_button_has_conflicting_bindings(button) {
174 TEXT_BIND_CONFLICT_COLOR
175 } else {
176 TEXT_COLOR
177 },
178 )
179 } else {
180 (
181 self.localized_strings
182 .get_msg("hud-settings-unbound")
183 .into_owned(),
184 ERROR_COLOR,
185 )
186 };
187 let loc_key = self
188 .localized_strings
189 .get_msg(game_input.get_localization_key());
190 let text_widget = Text::new(&loc_key)
191 .color(TEXT_COLOR)
192 .font_id(self.fonts.cyri.conrod_id)
193 .font_size(self.fonts.cyri.scale(18));
194 let button_widget = Button::new()
195 .label(&input_string)
196 .label_color(input_color)
197 .label_font_id(self.fonts.cyri.conrod_id)
198 .label_font_size(self.fonts.cyri.scale(15))
199 .w(150.0)
200 .rgba(0.0, 0.0, 0.0, 0.0)
201 .border_rgba(0.0, 0.0, 0.0, 255.0)
202 .label_y(Relative::Scalar(3.0));
203 let text_widget = match previous_element_id {
205 None => {
206 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
207 },
208 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
209 };
210 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
211 text_widget.set(text_id, ui);
212 if button_widget
213 .right_from(text_id, 350.0 - text_width)
214 .set(button_id, ui)
215 .was_clicked()
216 {
217 }
219 previous_element_id = Some(text_id);
221 }
222 },
223 GamepadBindingOption::GameLayers => {
224 let gamepad_controls = &self.global_state.window.controller_settings;
225
226 resize_ids(SORTED_GAMEINPUTS.len());
227
228 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
230 state
231 .ids
232 .controls_texts
233 .iter()
234 .zip(state.ids.controls_buttons.iter()),
235 ) {
236 let (input_string, input_color) =
237 if let Some(entry) = gamepad_controls.get_layer_button_binding(*game_input) {
239 (
240 entry.display_string(self.localized_strings),
241 if gamepad_controls.layer_entry_has_conflicting_bindings(entry) {
242 TEXT_BIND_CONFLICT_COLOR
243 } else {
244 TEXT_COLOR
245 },
246 )
247 } else {
248 (
249 self.localized_strings
250 .get_msg("hud-settings-unbound")
251 .into_owned(),
252 ERROR_COLOR,
253 )
254 };
255 let loc_key = self
256 .localized_strings
257 .get_msg(game_input.get_localization_key());
258 let text_widget = Text::new(&loc_key)
259 .color(TEXT_COLOR)
260 .font_id(self.fonts.cyri.conrod_id)
261 .font_size(self.fonts.cyri.scale(18));
262 let button_widget = Button::new()
263 .label(&input_string)
264 .label_color(input_color)
265 .label_font_id(self.fonts.cyri.conrod_id)
266 .label_font_size(self.fonts.cyri.scale(15))
267 .w(150.0)
268 .rgba(0.0, 0.0, 0.0, 0.0)
269 .border_rgba(0.0, 0.0, 0.0, 255.0)
270 .label_y(Relative::Scalar(3.0));
271 let text_widget = match previous_element_id {
273 None => {
274 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
275 },
276 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
277 };
278 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
279 text_widget.set(text_id, ui);
280 if button_widget
281 .right_from(text_id, 350.0 - text_width)
282 .set(button_id, ui)
283 .was_clicked()
284 {
285 }
287 previous_element_id = Some(text_id);
289 }
290 },
291 GamepadBindingOption::MenuButtons => {
292 let gamepad_controls = &self.global_state.window.controller_settings;
293
294 resize_ids(SORTED_MENUINPUTS.len());
295
296 for (menu_input, (&text_id, &button_id)) in SORTED_MENUINPUTS.iter().zip(
298 state
299 .ids
300 .controls_texts
301 .iter()
302 .zip(state.ids.controls_buttons.iter()),
303 ) {
304 let (input_string, input_color) =
305 if let Some(button) = gamepad_controls.get_menu_button_binding(*menu_input) {
307 (
308 format!(
309 "{} {}",
310 button.display_string(self.localized_strings),
311 button.try_shortened()
312 .map_or("".to_owned(), |short| format!("({})", short))
313 ),
314 if gamepad_controls.menu_button_has_conflicting_bindings(button) {
315 TEXT_BIND_CONFLICT_COLOR
316 } else {
317 TEXT_COLOR
318 },
319 )
320 } else {
321 (
322 self.localized_strings
323 .get_msg("hud-settings-unbound")
324 .into_owned(),
325 ERROR_COLOR,
326 )
327 };
328 let loc_key = self
329 .localized_strings
330 .get_msg(menu_input.get_localization_key());
331 let text_widget = Text::new(&loc_key)
332 .color(TEXT_COLOR)
333 .font_id(self.fonts.cyri.conrod_id)
334 .font_size(self.fonts.cyri.scale(18));
335 let button_widget = Button::new()
336 .label(&input_string)
337 .label_color(input_color)
338 .label_font_id(self.fonts.cyri.conrod_id)
339 .label_font_size(self.fonts.cyri.scale(15))
340 .w(150.0)
341 .rgba(0.0, 0.0, 0.0, 0.0)
342 .border_rgba(0.0, 0.0, 0.0, 255.0)
343 .label_y(Relative::Scalar(3.0));
344 let text_widget = match previous_element_id {
346 None => {
347 text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0)
348 },
349 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
350 };
351 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
352 text_widget.set(text_id, ui);
353 if button_widget
354 .right_from(text_id, 350.0 - text_width)
355 .set(button_id, ui)
356 .was_clicked()
357 {
358 }
360 previous_element_id = Some(text_id);
362 }
363 },
364 }
365 } else {
366 let controls = &self.global_state.settings.controls;
367
368 resize_ids(SORTED_GAMEINPUTS.len());
369
370 for (game_input, (&text_id, &button_id)) in SORTED_GAMEINPUTS.iter().zip(
372 state
373 .ids
374 .controls_texts
375 .iter()
376 .zip(state.ids.controls_buttons.iter()),
377 ) {
378 let (key_string, key_color) =
379 if self.global_state.window.remapping_keybindings == Some(*game_input) {
380 (
381 self.localized_strings
382 .get_msg("hud-settings-awaitingkey")
383 .into_owned(),
384 TEXT_COLOR,
385 )
386 } else if let Some(key) = controls.get_binding(*game_input) {
387 (
388 format!(
389 "{} {}",
390 key.display_string(),
391 key.try_shortened()
392 .map_or("".to_owned(), |short| format!("({})", short))
393 ),
394 if controls.has_conflicting_bindings(key) {
395 TEXT_BIND_CONFLICT_COLOR
396 } else {
397 TEXT_COLOR
398 },
399 )
400 } else {
401 (
402 self.localized_strings
403 .get_msg("hud-settings-unbound")
404 .into_owned(),
405 ERROR_COLOR,
406 )
407 };
408 let loc_key = self
409 .localized_strings
410 .get_msg(game_input.get_localization_key());
411 let text_widget = Text::new(&loc_key)
412 .color(TEXT_COLOR)
413 .font_id(self.fonts.cyri.conrod_id)
414 .font_size(self.fonts.cyri.scale(18));
415 let button_widget = Button::new()
416 .label(&key_string)
417 .label_color(key_color)
418 .label_font_id(self.fonts.cyri.conrod_id)
419 .label_font_size(self.fonts.cyri.scale(15))
420 .w(150.0)
421 .rgba(0.0, 0.0, 0.0, 0.0)
422 .border_rgba(0.0, 0.0, 0.0, 255.0)
423 .label_y(Relative::Scalar(3.0));
424 let text_widget = match previous_element_id {
426 None => text_widget.top_left_with_margins_on(state.ids.window, 10.0, 5.0),
427 Some(prev_id) => text_widget.down_from(prev_id, 10.0),
428 };
429 let text_width = text_widget.get_w(ui).unwrap_or(0.0);
430 text_widget.set(text_id, ui);
431 button_widget
432 .right_from(text_id, 350.0 - text_width)
433 .set(button_id, ui);
434
435 for _ in ui.widget_input(button_id).clicks().left() {
436 events.push(ChangeBinding(*game_input));
437 }
438 for _ in ui.widget_input(button_id).clicks().right() {
439 events.push(RemoveBinding(*game_input));
440 }
441 previous_element_id = Some(text_id);
443 }
444 }
445
446 if let Some(prev_id) = previous_element_id {
448 if Button::image(self.imgs.button)
449 .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT)
450 .hover_image(self.imgs.button_hover)
451 .press_image(self.imgs.button_press)
452 .down_from(prev_id, 20.0)
453 .label(
454 &self
455 .localized_strings
456 .get_msg("hud-settings-reset_keybinds"),
457 )
458 .label_font_size(self.fonts.cyri.scale(14))
459 .label_color(TEXT_COLOR)
460 .label_font_id(self.fonts.cyri.conrod_id)
461 .label_y(Relative::Scalar(2.0))
462 .set(state.ids.reset_controls_button, ui)
463 .was_clicked() &&
464 state.binding_mode != BindingMode::Gamepad
466 {
467 events.push(ResetKeyBindings);
468 }
469 previous_element_id = Some(state.ids.reset_controls_button)
470 }
471
472 let offset = ui
473 .widget_graph()
474 .widget(state.ids.window)
475 .and_then(|widget| {
476 widget
477 .maybe_y_scroll_state
478 .as_ref()
479 .map(|scroll| scroll.offset)
480 })
481 .unwrap_or(0.0);
482
483 let keybind_helper_text = self
484 .localized_strings
485 .get_msg("hud-settings-keybind-helper");
486 let keybind_helper = Text::new(&keybind_helper_text)
487 .color(TEXT_COLOR)
488 .font_id(self.fonts.cyri.conrod_id)
489 .font_size(self.fonts.cyri.scale(18));
490 keybind_helper
491 .top_right_with_margins_on(state.ids.window, offset + 5.0, 10.0)
492 .set(state.ids.keybind_helper, ui);
493
494 if let BindingMode::Gamepad = state.binding_mode {
495 let game_buttons = &self.localized_strings.get_msg("hud-settings-game_buttons");
496 let game_layers = &self.localized_strings.get_msg("hud-settings-game_layers");
497 let menu_buttons = &self.localized_strings.get_msg("hud-settings-menu_buttons");
498
499 let binding_mode_list = [game_buttons, game_layers, menu_buttons];
500 if let Some(clicked) = DropDownList::new(
501 &binding_mode_list,
502 Some(state.gamepad_binding_option as usize),
503 )
504 .label_color(TEXT_COLOR)
505 .label_font_id(self.fonts.cyri.conrod_id)
506 .label_font_size(self.fonts.cyri.scale(15))
507 .w(125.0)
508 .rgba(0.0, 0.0, 0.0, 0.0)
509 .border_rgba(0.0, 0.0, 0.0, 255.0)
510 .label_y(Relative::Scalar(1.0))
511 .down_from(state.ids.gamepad_mode_button, 10.0)
512 .set(state.ids.gamepad_option_dropdown, ui)
513 {
514 match clicked {
515 0 => {
516 state.update(|s| {
517 s.gamepad_binding_option = GamepadBindingOption::GameButtons
518 });
519 },
520 1 => {
521 state.update(|s| {
522 s.gamepad_binding_option = GamepadBindingOption::GameLayers
523 });
524 },
525 2 => {
526 state.update(|s| {
527 s.gamepad_binding_option = GamepadBindingOption::MenuButtons
528 });
529 },
530 _ => {
531 state.update(|s| {
532 s.gamepad_binding_option = GamepadBindingOption::GameButtons
533 });
534 },
535 }
536 }
537 }
538
539 let gamepad = &self.localized_strings.get_msg("hud-settings-gamepad");
540 let keyboard = &self.localized_strings.get_msg("hud-settings-keyboard");
541
542 let binding_mode_toggle_widget = Button::new()
543 .label(if let BindingMode::Gamepad = state.binding_mode {
544 gamepad
545 } else {
546 keyboard
547 })
548 .label_color(TEXT_COLOR)
549 .label_font_id(self.fonts.cyri.conrod_id)
550 .label_font_size(self.fonts.cyri.scale(15))
551 .w(125.0)
552 .rgba(0.0, 0.0, 0.0, 0.0)
553 .border_rgba(0.0, 0.0, 0.0, 255.0)
554 .label_y(Relative::Scalar(1.0));
555 if binding_mode_toggle_widget
556 .down_from(state.ids.keybind_helper, 10.0)
557 .align_right_of(state.ids.keybind_helper)
558 .set(state.ids.gamepad_mode_button, ui)
559 .was_clicked()
560 {
561 if let BindingMode::Keyboard = state.binding_mode {
562 state.update(|s| s.binding_mode = BindingMode::Gamepad);
563 } else {
564 state.update(|s| s.binding_mode = BindingMode::Keyboard);
565 }
566 }
567
568 if let Some(prev_id) = previous_element_id {
570 Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT)
571 .down_from(prev_id, 10.0)
572 .set(state.ids.controls_alignment_rectangle, ui);
573 }
574
575 events
576 }
577}