veloren_voxygen/hud/settings_window/
controls.rs

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        // These temporary variables exist so state is only borrowed by resize_ids.
129        let binding_mode = state.binding_mode;
130        let gamepad_binding_option = state.gamepad_binding_option;
131
132        // Button and Text resizing logic to be used by each binding type branch
133        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        // Used for sequential placement in a flow-down pattern
147        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                    // Loop all existing keybindings and the ids for text and button widgets
157                    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                            // TODO: handle rebind text
166                            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                        // Place top-left if it's the first text, else under the previous one
205                        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                            // TODO: handle change and remove binding
219                        }
220                        // Set the previous id to the current one for the next cycle
221                        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                    // Loop all existing keybindings and the ids for text and button widgets
230                    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                            // TODO: handle rebind text
239                            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                        // Place top-left if it's the first text, else under the previous one
273                        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                            // TODO: handle change and remove binding
287                        }
288                        // Set the previous id to the current one for the next cycle
289                        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                    // Loop all existing keybindings and the ids for text and button widgets
298                    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                            // TODO: handle rebind text
307                            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                        // Place top-left if it's the first text, else under the previous one
346                        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                            // TODO: handle change and remove binding
360                        }
361                        // Set the previous id to the current one for the next cycle
362                        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            // Loop all existing keybindings and the ids for text and button widgets
372            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                // Place top-left if it's the first text, else under the previous one
426                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                // Set the previous id to the current one for the next cycle
443                previous_element_id = Some(text_id);
444            }
445        }
446
447        // Reset the KeyBindings settings to the default settings
448        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                // TODO: handle reset button in gamepad mode
466                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        // Add an empty text widget to simulate some bottom margin, because conrod sucks
570        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}