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
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        // These temporary variables exist so state is only borrowed by resize_ids.
128        let binding_mode = state.binding_mode;
129        let gamepad_binding_option = state.gamepad_binding_option;
130
131        // Button and Text resizing logic to be used by each binding type branch
132        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        // Used for sequential placement in a flow-down pattern
146        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                    // Loop all existing keybindings and the ids for text and button widgets
156                    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                            // TODO: handle rebind text
165                            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                        // Place top-left if it's the first text, else under the previous one
204                        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                            // TODO: handle change and remove binding
218                        }
219                        // Set the previous id to the current one for the next cycle
220                        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                    // Loop all existing keybindings and the ids for text and button widgets
229                    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                            // TODO: handle rebind text
238                            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                        // Place top-left if it's the first text, else under the previous one
272                        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                            // TODO: handle change and remove binding
286                        }
287                        // Set the previous id to the current one for the next cycle
288                        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                    // Loop all existing keybindings and the ids for text and button widgets
297                    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                            // TODO: handle rebind text
306                            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                        // Place top-left if it's the first text, else under the previous one
345                        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                            // TODO: handle change and remove binding
359                        }
360                        // Set the previous id to the current one for the next cycle
361                        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            // Loop all existing keybindings and the ids for text and button widgets
371            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                // Place top-left if it's the first text, else under the previous one
425                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                // Set the previous id to the current one for the next cycle
442                previous_element_id = Some(text_id);
443            }
444        }
445
446        // Reset the KeyBindings settings to the default settings
447        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                // TODO: handle reset button in gamepad mode
465                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        // Add an empty text widget to simulate some bottom margin, because conrod sucks
569        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}