veloren_voxygen/menu/char_selection/
ui.rs

1use crate::{
2    GlobalState,
3    hud::default_water_color,
4    render::UiDrawer,
5    ui::{
6        self, Graphic, GraphicId,
7        fonts::IcedFonts as Fonts,
8        ice::{
9            Element, IcedRenderer, IcedUi as Ui,
10            component::{
11                neat_button,
12                tooltip::{self, WithTooltip},
13            },
14            style,
15            widget::{
16                AspectRatioContainer, BackgroundContainer, Image, MouseDetector, Overlay, Padding,
17                TooltipManager, mouse_detector,
18            },
19        },
20        img_ids::ImageGraphic,
21    },
22    window,
23};
24use client::{Client, ServerInfo};
25use common::{
26    LoadoutBuilder,
27    character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH},
28    comp::{self, Inventory, Item, humanoid, inventory::slot::EquipSlot},
29    resources::Time,
30    terrain::TerrainChunkSize,
31    vol::RectVolSize,
32};
33use common_net::msg::world_msg::{SiteId, SiteInfo};
34use i18n::{Localization, LocalizationHandle};
35use rand::{Rng, thread_rng};
36//ImageFrame, Tooltip,
37use crate::settings::Settings;
38//use std::time::Duration;
39//use ui::ice::widget;
40use iced::{
41    Align, Button, Checkbox, Color, Column, Container, HorizontalAlignment, Length, Row,
42    Scrollable, Slider, Space, Text, TextInput, button, scrollable, slider, text_input,
43};
44use std::sync::Arc;
45use vek::{Rgba, Vec2};
46
47pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
48pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
49pub const TOOLTIP_BACK_COLOR: Rgba<u8> = Rgba::new(20, 18, 10, 255);
50const FILL_FRAC_ONE: f32 = 0.77;
51const FILL_FRAC_TWO: f32 = 0.53;
52const TOOLTIP_HOVER_DUR: std::time::Duration = std::time::Duration::from_millis(150);
53const TOOLTIP_FADE_DUR: std::time::Duration = std::time::Duration::from_millis(350);
54const BANNER_ALPHA: u8 = 210;
55// Buttons in the bottom corners
56const SMALL_BUTTON_HEIGHT: u16 = 31;
57
58const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer";
59const STARTER_BOW: &str = "common.items.weapons.bow.starter";
60const STARTER_AXE: &str = "common.items.weapons.axe.starter_axe";
61const STARTER_STAFF: &str = "common.items.weapons.staff.starter_staff";
62const STARTER_SWORD: &str = "common.items.weapons.sword.starter";
63const STARTER_SWORDS: &str = "common.items.weapons.sword_1h.starter";
64
65// TODO: what does this comment mean?
66// // Use in future MR to make this a starter weapon
67
68// TODO: use for info popup frame/background
69const UI_MAIN: Rgba<u8> = Rgba::new(156, 179, 179, 255); // Greenish Blue
70
71image_ids_ice! {
72    struct Imgs {
73        <ImageGraphic>
74        frame_bottom: "voxygen.element.ui.generic.frames.banner_bot",
75
76        slider_range: "voxygen.element.ui.generic.slider.track",
77        slider_indicator: "voxygen.element.ui.generic.slider.indicator",
78
79        char_selection: "voxygen.element.ui.generic.frames.selection",
80        char_selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
81        char_selection_press: "voxygen.element.ui.generic.frames.selection_press",
82
83        delete_button: "voxygen.element.ui.char_select.icons.bin",
84        delete_button_hover: "voxygen.element.ui.char_select.icons.bin_hover",
85        delete_button_press: "voxygen.element.ui.char_select.icons.bin_press",
86
87        edit_button: "voxygen.element.ui.char_select.icons.pen",
88        edit_button_hover: "voxygen.element.ui.char_select.icons.pen_hover",
89        edit_button_press: "voxygen.element.ui.char_select.icons.pen_press",
90
91        name_input: "voxygen.element.ui.generic.textbox",
92
93        // Tool Icons
94        swords: "voxygen.element.weapons.swords",
95        sword: "voxygen.element.weapons.sword",
96        axe: "voxygen.element.weapons.axe",
97        hammer: "voxygen.element.weapons.hammer",
98        bow: "voxygen.element.weapons.bow",
99        staff: "voxygen.element.weapons.staff",
100
101        // Hardcore icon
102        hardcore: "voxygen.element.ui.map.icons.dif_map_icon",
103
104        // Dice icons
105        dice: "voxygen.element.ui.char_select.icons.dice",
106        dice_hover: "voxygen.element.ui.char_select.icons.dice_hover",
107        dice_press: "voxygen.element.ui.char_select.icons.dice_press",
108
109        // Species Icons
110        human_m: "voxygen.element.ui.char_select.portraits.human_m",
111        human_f: "voxygen.element.ui.char_select.portraits.human_f",
112        orc_m: "voxygen.element.ui.char_select.portraits.orc_m",
113        orc_f: "voxygen.element.ui.char_select.portraits.orc_f",
114        dwarf_m: "voxygen.element.ui.char_select.portraits.dwarf_m",
115        dwarf_f: "voxygen.element.ui.char_select.portraits.dwarf_f",
116        draugr_m: "voxygen.element.ui.char_select.portraits.ud_m",
117        draugr_f: "voxygen.element.ui.char_select.portraits.ud_f",
118        elf_m: "voxygen.element.ui.char_select.portraits.elf_m",
119        elf_f: "voxygen.element.ui.char_select.portraits.elf_f",
120        danari_m: "voxygen.element.ui.char_select.portraits.danari_m",
121        danari_f: "voxygen.element.ui.char_select.portraits.danari_f",
122        // Icon Borders
123        icon_border: "voxygen.element.ui.generic.buttons.border",
124        icon_border_mo: "voxygen.element.ui.generic.buttons.border_mo",
125        icon_border_press: "voxygen.element.ui.generic.buttons.border_press",
126        icon_border_pressed: "voxygen.element.ui.generic.buttons.border_pressed",
127
128        button: "voxygen.element.ui.generic.buttons.button",
129        button_hover: "voxygen.element.ui.generic.buttons.button_hover",
130        button_press: "voxygen.element.ui.generic.buttons.button_press",
131
132        // Tooltips
133        tt_edge: "voxygen.element.ui.generic.frames.tooltip.edge",
134        tt_corner: "voxygen.element.ui.generic.frames.tooltip.corner",
135
136        // Startzone Selection
137        town_marker: "voxygen.element.ui.char_select.icons.town_marker",
138    }
139}
140
141pub enum Event {
142    Logout,
143    Play(CharacterId),
144    Spectate,
145    AddCharacter {
146        alias: String,
147        mainhand: Option<String>,
148        offhand: Option<String>,
149        body: comp::Body,
150        hardcore: bool,
151        start_site: Option<SiteId>,
152    },
153    EditCharacter {
154        alias: String,
155        character_id: CharacterId,
156        body: comp::Body,
157    },
158    DeleteCharacter(CharacterId),
159    ClearCharacterListError,
160    SelectCharacter(Option<CharacterId>),
161    ShowRules,
162}
163
164#[expect(clippy::large_enum_variant)]
165enum Mode {
166    Select {
167        info_content: Option<InfoContent>,
168
169        characters_scroll: scrollable::State,
170        character_buttons: Vec<button::State>,
171        new_character_button: button::State,
172        logout_button: button::State,
173        rule_button: button::State,
174        enter_world_button: button::State,
175        spectate_button: button::State,
176        yes_button: button::State,
177        no_button: button::State,
178    },
179    CreateOrEdit {
180        name: String,
181        body: humanoid::Body,
182        inventory: Box<Inventory>,
183        mainhand: Option<&'static str>,
184        offhand: Option<&'static str>,
185
186        body_type_buttons: [button::State; 2],
187        species_buttons: [button::State; 6],
188        tool_buttons: [button::State; 6],
189        sliders: Sliders,
190        hardcore_enabled: bool,
191        left_scroll: scrollable::State,
192        right_scroll: scrollable::State,
193        name_input: text_input::State,
194        back_button: button::State,
195        create_button: button::State,
196        rand_character_button: button::State,
197        rand_name_button: button::State,
198        prev_starting_site_button: button::State,
199        next_starting_site_button: button::State,
200        /// `character_id.is_some()` can be used to determine if we're in edit
201        /// mode as opposed to create mode.
202        // TODO: Something less janky? Express the problem domain better!
203        character_id: Option<CharacterId>,
204        start_site_idx: Option<usize>,
205    },
206}
207
208impl Mode {
209    pub fn select(info_content: Option<InfoContent>) -> Self {
210        Self::Select {
211            info_content,
212            characters_scroll: Default::default(),
213            character_buttons: Vec::new(),
214            new_character_button: Default::default(),
215            logout_button: Default::default(),
216            rule_button: Default::default(),
217            enter_world_button: Default::default(),
218            spectate_button: Default::default(),
219            yes_button: Default::default(),
220            no_button: Default::default(),
221        }
222    }
223
224    pub fn create(name: String) -> Self {
225        // TODO: Load these from the server (presumably from a .ron) to allow for easier
226        // modification of custom starting weapons
227        let mainhand = Some(STARTER_SWORD);
228        let offhand = None;
229
230        let loadout = LoadoutBuilder::empty()
231            .defaults()
232            .active_mainhand(mainhand.map(Item::new_from_asset_expect))
233            .active_offhand(offhand.map(Item::new_from_asset_expect))
234            .build();
235
236        let inventory = Box::new(Inventory::with_loadout_humanoid(loadout));
237
238        Self::CreateOrEdit {
239            name,
240            body: humanoid::Body::random(),
241            inventory,
242            mainhand,
243            offhand,
244            body_type_buttons: Default::default(),
245            species_buttons: Default::default(),
246            tool_buttons: Default::default(),
247            sliders: Default::default(),
248            hardcore_enabled: false,
249            left_scroll: Default::default(),
250            right_scroll: Default::default(),
251            name_input: Default::default(),
252            back_button: Default::default(),
253            create_button: Default::default(),
254            rand_character_button: Default::default(),
255            rand_name_button: Default::default(),
256            prev_starting_site_button: Default::default(),
257            next_starting_site_button: Default::default(),
258            character_id: None,
259            start_site_idx: None,
260        }
261    }
262
263    pub fn edit(
264        name: String,
265        character_id: CharacterId,
266        body: humanoid::Body,
267        inventory: &Inventory,
268    ) -> Self {
269        Self::CreateOrEdit {
270            name,
271            body,
272            inventory: Box::new(inventory.clone()),
273            mainhand: None,
274            offhand: None,
275            body_type_buttons: Default::default(),
276            species_buttons: Default::default(),
277            tool_buttons: Default::default(),
278            sliders: Default::default(),
279            hardcore_enabled: false,
280            left_scroll: Default::default(),
281            right_scroll: Default::default(),
282            name_input: Default::default(),
283            back_button: Default::default(),
284            create_button: Default::default(),
285            rand_character_button: Default::default(),
286            rand_name_button: Default::default(),
287            prev_starting_site_button: Default::default(),
288            next_starting_site_button: Default::default(),
289            character_id: Some(character_id),
290            start_site_idx: None,
291        }
292    }
293}
294
295#[derive(PartialEq)]
296enum InfoContent {
297    Deletion(usize),
298    LoadingCharacters,
299    CreatingCharacter,
300    EditingCharacter,
301    JoiningCharacter,
302    CharacterError(String),
303}
304
305struct Controls {
306    fonts: Fonts,
307    imgs: Imgs,
308    // Voxygen version
309    version: String,
310    // Alpha disclaimer
311    alpha: String,
312    server_mismatched_version: Option<String>,
313    tooltip_manager: TooltipManager,
314    // Zone for rotating the character with the mouse
315    mouse_detector: mouse_detector::State,
316    mode: Mode,
317    // Id of the selected character
318    selected: Option<CharacterId>,
319    default_name: String,
320    map_img: GraphicId,
321    possible_starting_sites: Vec<SiteInfo>,
322    world_sz: Vec2<u32>,
323    has_rules: bool,
324}
325
326#[derive(Clone)]
327enum Message {
328    Back,
329    Logout,
330    ShowRules,
331    EnterWorld,
332    Spectate,
333    Select(CharacterId),
334    Delete(usize),
335    Edit(usize),
336    ConfirmEdit(CharacterId),
337    NewCharacter,
338    CreateCharacter,
339    Name(String),
340    BodyType(humanoid::BodyType),
341    Species(humanoid::Species),
342    Tool((Option<&'static str>, Option<&'static str>)),
343    RandomizeCharacter,
344    HardcoreEnabled(bool),
345    RandomizeName,
346    CancelDeletion,
347    ConfirmDeletion,
348    ClearCharacterListError,
349    HairStyle(u8),
350    HairColor(u8),
351    Skin(u8),
352    Eyes(u8),
353    EyeColor(u8),
354    Accessory(u8),
355    Beard(u8),
356    StartingSite(usize),
357    PrevStartingSite,
358    NextStartingSite,
359    // Workaround for widgets that require a message but we don't want them to actually do
360    // anything
361    DoNothing,
362}
363
364impl Controls {
365    fn new(
366        fonts: Fonts,
367        imgs: Imgs,
368        selected: Option<CharacterId>,
369        default_name: String,
370        server_info: &ServerInfo,
371        map_img: GraphicId,
372        possible_starting_sites: Vec<SiteInfo>,
373        world_sz: Vec2<u32>,
374        has_rules: bool,
375    ) -> Self {
376        let version = common::util::DISPLAY_VERSION_LONG.clone();
377        let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str());
378        let server_mismatched_version = (common::util::GIT_HASH.to_string()
379            != server_info.git_hash)
380            .then(|| server_info.git_hash.clone());
381
382        Self {
383            fonts,
384            imgs,
385            version,
386            alpha,
387            server_mismatched_version,
388            tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR),
389            mouse_detector: Default::default(),
390            mode: Mode::select(Some(InfoContent::LoadingCharacters)),
391            selected,
392            default_name,
393            map_img,
394            possible_starting_sites,
395            world_sz,
396            has_rules,
397        }
398    }
399
400    fn view<'a>(
401        &'a mut self,
402        _settings: &Settings,
403        client: &Client,
404        error: &Option<String>,
405        i18n: &'a Localization,
406    ) -> Element<'a, Message> {
407        // TODO: use font scale thing for text size (use on button size for buttons with
408        // text)
409
410        // Maintain tooltip manager
411        self.tooltip_manager.maintain();
412
413        let imgs = &self.imgs;
414        let fonts = &self.fonts;
415        let tooltip_manager = &self.tooltip_manager;
416
417        let button_style = style::button::Style::new(imgs.button)
418            .hover_image(imgs.button_hover)
419            .press_image(imgs.button_press)
420            .text_color(TEXT_COLOR)
421            .disabled_text_color(DISABLED_TEXT_COLOR);
422
423        let tooltip_style = tooltip::Style {
424            container: style::container::Style::color_with_image_border(
425                TOOLTIP_BACK_COLOR,
426                imgs.tt_corner,
427                imgs.tt_edge,
428            ),
429            text_color: TEXT_COLOR,
430            text_size: self.fonts.cyri.scale(17),
431            padding: 10,
432        };
433
434        let version = Text::new(&self.version)
435            .size(self.fonts.cyri.scale(15))
436            .width(Length::Fill)
437            .horizontal_alignment(HorizontalAlignment::Right);
438
439        let alpha = Text::new(&self.alpha)
440            .size(self.fonts.cyri.scale(12))
441            .width(Length::Fill)
442            .horizontal_alignment(HorizontalAlignment::Center);
443
444        let top_text = Row::with_children(vec![
445            Space::new(Length::Fill, Length::Shrink).into(),
446            alpha.into(),
447            version.into(),
448        ])
449        .width(Length::Fill);
450
451        let mut warning_container = if let Some(mismatched_version) =
452            &self.server_mismatched_version
453        {
454            let warning = Text::<IcedRenderer>::new(format!(
455                "{}\n{}: {} {}: {}",
456                i18n.get_msg("char_selection-version_mismatch"),
457                i18n.get_msg("main-login-server_version"),
458                mismatched_version,
459                i18n.get_msg("main-login-client_version"),
460                *common::util::GIT_HASH
461            ))
462            .size(self.fonts.cyri.scale(18))
463            .color(iced::Color::from_rgb(1.0, 0.0, 0.0))
464            .width(Length::Fill)
465            .horizontal_alignment(HorizontalAlignment::Center);
466            Some(
467                Container::new(
468                    Container::new(Row::with_children(vec![warning.into()]).width(Length::Fill))
469                        .style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
470                        .padding(12)
471                        .width(Length::Fill)
472                        .center_x(),
473                )
474                .padding(16),
475            )
476        } else {
477            None
478        };
479
480        let content = match &mut self.mode {
481            Mode::Select {
482                ref mut info_content,
483                ref mut characters_scroll,
484                ref mut character_buttons,
485                ref mut new_character_button,
486                ref mut logout_button,
487                ref mut rule_button,
488                ref mut enter_world_button,
489                ref mut spectate_button,
490                ref mut yes_button,
491                ref mut no_button,
492            } => {
493                match self.selected {
494                    Some(character_id) => {
495                        // If the selected character no longer exists, deselect it.
496                        if !client
497                            .character_list()
498                            .characters
499                            .iter()
500                            .any(|char| char.character.id == Some(character_id))
501                        {
502                            self.selected = None;
503                        }
504                    },
505                    None => {
506                        // If no character is selected then select the first one
507                        // Note: we don't need to persist this because it is the default
508                        self.selected = client
509                            .character_list()
510                            .characters
511                            .first()
512                            .and_then(|i| i.character.id);
513                    },
514                }
515
516                // Get the index of the selected character
517                let selected = self.selected.and_then(|id| {
518                    client
519                        .character_list()
520                        .characters
521                        .iter()
522                        .position(|i| i.character.id == Some(id))
523                });
524
525                if let Some(error) = error {
526                    // TODO: use more user friendly errors with suggestions on potential solutions
527                    // instead of directly showing error message here
528                    *info_content = Some(InfoContent::CharacterError(format!(
529                        "{}: {}",
530                        i18n.get_msg("common-error"),
531                        error
532                    )))
533                } else if let Some(InfoContent::CharacterError(_)) = info_content {
534                    *info_content = None;
535                } else if matches!(
536                    info_content,
537                    Some(InfoContent::LoadingCharacters)
538                        | Some(InfoContent::CreatingCharacter)
539                        | Some(InfoContent::EditingCharacter)
540                ) && !client.character_list().loading
541                {
542                    *info_content = None;
543                }
544
545                #[cfg(feature = "singleplayer")]
546                let server_name =
547                    if client.server_info().name == server::settings::SINGLEPLAYER_SERVER_NAME {
548                        &i18n.get_msg("common-singleplayer").to_string()
549                    } else {
550                        &client.server_info().name
551                    };
552                #[cfg(not(feature = "singleplayer"))]
553                let server_name = &client.server_info().name;
554
555                let server = Container::new(
556                    Column::with_children(vec![
557                        Text::new(server_name).size(fonts.cyri.scale(25)).into(),
558                        // TODO: show additional server info here
559                        Space::new(Length::Fill, Length::Units(25)).into(),
560                    ])
561                    .spacing(5)
562                    .align_items(Align::Center),
563                )
564                .style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
565                .padding(12)
566                .center_x()
567                .center_y()
568                .width(Length::Fill);
569
570                let characters = {
571                    let characters = &client.character_list().characters;
572                    let num = characters.len();
573                    // Ensure we have enough button states
574                    const CHAR_BUTTONS: usize = 3;
575                    character_buttons.resize_with(num * CHAR_BUTTONS, Default::default);
576
577                    // Character Selection List
578                    let mut characters = characters
579                        .iter()
580                        .zip(character_buttons.chunks_exact_mut(CHAR_BUTTONS))
581                        .filter_map(|(character, buttons)| {
582                            let mut buttons = buttons.iter_mut();
583                            // TODO: eliminate option in character id?
584                            character.character.id.map(|id| {
585                                (
586                                    id,
587                                    character,
588                                    (
589                                        buttons.next().unwrap(),
590                                        buttons.next().unwrap(),
591                                        buttons.next().unwrap(),
592                                    ),
593                                )
594                            })
595                        })
596                        .enumerate()
597                        .map(
598                            |(
599                                i,
600                                (
601                                    character_id,
602                                    character,
603                                    (select_button, edit_button, delete_button),
604                                ),
605                            )| {
606                                let select_col = if Some(i) == selected {
607                                    (255, 208, 69)
608                                } else {
609                                    (255, 255, 255)
610                                };
611                                Overlay::new(
612                                    Container::new(Column::with_children({
613                                        let mut elements = Vec::new();
614                                        if character.hardcore {
615                                            elements.push(
616                                                Image::new(imgs.hardcore)
617                                                    .width(Length::Units(32))
618                                                    .height(Length::Units(32))
619                                                    .into(),
620                                            );
621                                        }
622                                        elements.push(
623                                            Row::with_children(vec![
624                                                // Edit button
625                                                Button::new(
626                                                    edit_button,
627                                                    Space::new(
628                                                        Length::Units(16),
629                                                        Length::Units(16),
630                                                    ),
631                                                )
632                                                .style(
633                                                    style::button::Style::new(imgs.edit_button)
634                                                        .hover_image(imgs.edit_button_hover)
635                                                        .press_image(imgs.edit_button_press),
636                                                )
637                                                .on_press(Message::Edit(i))
638                                                .into(),
639                                                // Delete button
640                                                Button::new(
641                                                    delete_button,
642                                                    Space::new(
643                                                        Length::Units(16),
644                                                        Length::Units(16),
645                                                    ),
646                                                )
647                                                .style(
648                                                    style::button::Style::new(imgs.delete_button)
649                                                        .hover_image(imgs.delete_button_hover)
650                                                        .press_image(imgs.delete_button_press),
651                                                )
652                                                .on_press(Message::Delete(i))
653                                                .into(),
654                                            ])
655                                            .spacing(5)
656                                            .into(),
657                                        );
658
659                                        elements
660                                    }))
661                                    .padding(4),
662                                    // Select Button
663                                    AspectRatioContainer::new(
664                                        Button::new(
665                                            select_button,
666                                            Column::with_children(vec![
667                                                Text::new(&character.character.alias)
668                                                    .size(fonts.cyri.scale(26))
669                                                    .into(),
670                                                Text::new(character.location.as_ref().map_or_else(
671                                                    || {
672                                                        i18n.get_msg(
673                                                            "char_selection-uncanny_valley",
674                                                        )
675                                                        .to_string()
676                                                    },
677                                                    |s| s.clone(),
678                                                ))
679                                                .into(),
680                                            ]),
681                                        )
682                                        .padding(10)
683                                        .style(
684                                            style::button::Style::new(if Some(i) == selected {
685                                                imgs.char_selection_hover
686                                            } else {
687                                                imgs.char_selection
688                                            })
689                                            .hover_image(imgs.char_selection_hover)
690                                            .press_image(imgs.char_selection_press)
691                                            .image_color(Rgba::new(
692                                                select_col.0,
693                                                select_col.1,
694                                                select_col.2,
695                                                255,
696                                            )),
697                                        )
698                                        .width(Length::Fill)
699                                        .height(Length::Fill)
700                                        .on_press(Message::Select(character_id)),
701                                    )
702                                    .ratio_of_image(imgs.char_selection),
703                                )
704                                .padding(0)
705                                .align_x(Align::End)
706                                .align_y(Align::End)
707                                .into()
708                            },
709                        )
710                        .collect::<Vec<_>>();
711
712                    // Add create new character button
713                    let color = if num >= MAX_CHARACTERS_PER_PLAYER {
714                        (97, 97, 25)
715                    } else {
716                        (97, 255, 18)
717                    };
718                    characters.push(
719                        AspectRatioContainer::new({
720                            let button = Button::new(
721                                new_character_button,
722                                Container::new(Text::new(
723                                    i18n.get_msg("char_selection-create_new_character"),
724                                ))
725                                .width(Length::Fill)
726                                .height(Length::Fill)
727                                .center_x()
728                                .center_y(),
729                            )
730                            .style(
731                                style::button::Style::new(imgs.char_selection)
732                                    .hover_image(imgs.char_selection_hover)
733                                    .press_image(imgs.char_selection_press)
734                                    .image_color(Rgba::new(color.0, color.1, color.2, 255))
735                                    .text_color(iced::Color::from_rgb8(color.0, color.1, color.2))
736                                    .disabled_text_color(iced::Color::from_rgb8(
737                                        color.0, color.1, color.2,
738                                    )),
739                            )
740                            .width(Length::Fill)
741                            .height(Length::Fill);
742                            if num < MAX_CHARACTERS_PER_PLAYER {
743                                button.on_press(Message::NewCharacter)
744                            } else {
745                                button
746                            }
747                        })
748                        .ratio_of_image(imgs.char_selection)
749                        .into(),
750                    );
751                    characters
752                };
753
754                // TODO: could replace column with scrollable completely if it had a with
755                // children method
756                let characters = Column::with_children(vec![
757                    Container::new(
758                        Scrollable::new(characters_scroll)
759                            .push(Column::with_children(characters).spacing(4))
760                            .padding(6)
761                            .scrollbar_width(5)
762                            .scroller_width(5)
763                            .width(Length::Fill)
764                            .style(style::scrollable::Style {
765                                track: None,
766                                scroller: style::scrollable::Scroller::Color(UI_MAIN),
767                            }),
768                    )
769                    .style(style::container::Style::color(Rgba::from_translucent(
770                        0,
771                        BANNER_ALPHA,
772                    )))
773                    .width(Length::Units(322))
774                    .height(Length::Fill)
775                    .center_x()
776                    .into(),
777                    Image::new(imgs.frame_bottom)
778                        .height(Length::Units(40))
779                        .width(Length::Units(322))
780                        .color(Rgba::from_translucent(0, BANNER_ALPHA))
781                        .into(),
782                ])
783                .height(Length::Fill);
784
785                let mut left_column_children = vec![server.into(), characters.into()];
786
787                if self.has_rules {
788                    left_column_children.push(
789                        Container::new(neat_button(
790                            rule_button,
791                            i18n.get_msg("char_selection-rules").into_owned(),
792                            FILL_FRAC_ONE,
793                            button_style,
794                            Some(Message::ShowRules),
795                        ))
796                        .align_y(Align::End)
797                        .width(Length::Fill)
798                        .center_x()
799                        .height(Length::Units(52))
800                        .into(),
801                    );
802                }
803                let left_column = Column::with_children(left_column_children)
804                    .spacing(10)
805                    .width(Length::Units(322)) // TODO: see if we can get iced to work with settings below
806                    // .max_width(360)
807                    // .width(Length::Fill)
808                    .height(Length::Fill);
809
810                let top = Row::with_children(vec![
811                    left_column.into(),
812                    MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill).into(),
813                ])
814                .padding(15)
815                .width(Length::Fill)
816                .height(Length::Fill);
817                let mut bottom_content = vec![
818                    Container::new(neat_button(
819                        logout_button,
820                        i18n.get_msg("char_selection-logout").into_owned(),
821                        FILL_FRAC_ONE,
822                        button_style,
823                        Some(Message::Logout),
824                    ))
825                    .width(Length::Fill)
826                    .height(Length::Units(SMALL_BUTTON_HEIGHT))
827                    .into(),
828                ];
829
830                if client.is_moderator() && client.client_type().can_spectate() {
831                    bottom_content.push(
832                        Container::new(neat_button(
833                            spectate_button,
834                            i18n.get_msg("char_selection-spectate").into_owned(),
835                            FILL_FRAC_TWO,
836                            button_style,
837                            Some(Message::Spectate),
838                        ))
839                        .width(Length::Fill)
840                        .height(Length::Units(52))
841                        .center_x()
842                        .into(),
843                    );
844                }
845
846                if client.client_type().can_enter_character() {
847                    bottom_content.push(
848                        Container::new(neat_button(
849                            enter_world_button,
850                            i18n.get_msg("char_selection-enter_world").into_owned(),
851                            FILL_FRAC_TWO,
852                            button_style,
853                            selected.map(|_| Message::EnterWorld),
854                        ))
855                        .width(Length::Fill)
856                        .height(Length::Units(52))
857                        .center_x()
858                        .into(),
859                    );
860                }
861
862                bottom_content.push(Space::new(Length::Fill, Length::Shrink).into());
863
864                let bottom = Row::with_children(bottom_content).align_items(Align::End);
865
866                let content = Column::with_children(vec![top.into(), bottom.into()])
867                    .width(Length::Fill)
868                    .padding(5)
869                    .height(Length::Fill);
870
871                // Overlay delete prompt
872                if let Some(info_content) = info_content {
873                    let over_content: Element<_> = match &info_content {
874                        InfoContent::Deletion(_) => Column::with_children(vec![
875                            Text::new(i18n.get_msg("char_selection-delete_permanently"))
876                                .size(fonts.cyri.scale(24))
877                                .into(),
878                            Row::with_children(vec![
879                                neat_button(
880                                    no_button,
881                                    i18n.get_msg("common-no").into_owned(),
882                                    FILL_FRAC_ONE,
883                                    button_style,
884                                    Some(Message::CancelDeletion),
885                                ),
886                                neat_button(
887                                    yes_button,
888                                    i18n.get_msg("common-yes").into_owned(),
889                                    FILL_FRAC_ONE,
890                                    button_style,
891                                    Some(Message::ConfirmDeletion),
892                                ),
893                            ])
894                            .height(Length::Units(28))
895                            .spacing(30)
896                            .into(),
897                        ])
898                        .align_items(Align::Center)
899                        .spacing(10)
900                        .into(),
901                        InfoContent::LoadingCharacters => {
902                            Text::new(i18n.get_msg("char_selection-loading_characters"))
903                                .size(fonts.cyri.scale(24))
904                                .into()
905                        },
906                        InfoContent::CreatingCharacter => {
907                            Text::new(i18n.get_msg("char_selection-creating_character"))
908                                .size(fonts.cyri.scale(24))
909                                .into()
910                        },
911                        InfoContent::EditingCharacter => {
912                            Text::new(i18n.get_msg("char_selection-editing_character"))
913                                .size(fonts.cyri.scale(24))
914                                .into()
915                        },
916                        InfoContent::JoiningCharacter => {
917                            Text::new(i18n.get_msg("char_selection-joining_character"))
918                                .size(fonts.cyri.scale(24))
919                                .into()
920                        },
921                        InfoContent::CharacterError(error) => Column::with_children(vec![
922                            Text::new(error).size(fonts.cyri.scale(24)).into(),
923                            Row::with_children(vec![neat_button(
924                                no_button,
925                                i18n.get_msg("common-close").into_owned(),
926                                FILL_FRAC_ONE,
927                                button_style,
928                                Some(Message::ClearCharacterListError),
929                            )])
930                            .height(Length::Units(28))
931                            .into(),
932                        ])
933                        .align_items(Align::Center)
934                        .spacing(10)
935                        .into(),
936                    };
937
938                    let over = Container::new(over_content)
939                        .style(
940                            style::container::Style::color_with_double_cornerless_border(
941                                (0, 0, 0, 200).into(),
942                                (3, 4, 4, 255).into(),
943                                (28, 28, 22, 255).into(),
944                            ),
945                        )
946                        .width(Length::Shrink)
947                        .height(Length::Shrink)
948                        .max_width(400)
949                        .max_height(500)
950                        .padding(24)
951                        .center_x()
952                        .center_y();
953
954                    Overlay::new(over, content)
955                        .width(Length::Fill)
956                        .height(Length::Fill)
957                        .center_x()
958                        .center_y()
959                        .into()
960                } else {
961                    content.into()
962                }
963            },
964            Mode::CreateOrEdit {
965                name,
966                body,
967                inventory: _,
968                mainhand,
969                offhand: _,
970                ref mut left_scroll,
971                ref mut right_scroll,
972                ref mut body_type_buttons,
973                ref mut species_buttons,
974                ref mut tool_buttons,
975                ref mut sliders,
976                ref mut hardcore_enabled,
977                ref mut name_input,
978                ref mut back_button,
979                ref mut create_button,
980                ref mut rand_character_button,
981                ref mut rand_name_button,
982                ref mut prev_starting_site_button,
983                ref mut next_starting_site_button,
984                character_id,
985                start_site_idx,
986            } => {
987                let unselected_style = style::button::Style::new(imgs.icon_border)
988                    .hover_image(imgs.icon_border_mo)
989                    .press_image(imgs.icon_border_press);
990
991                let selected_style = style::button::Style::new(imgs.icon_border_pressed)
992                    .hover_image(imgs.icon_border_mo)
993                    .press_image(imgs.icon_border_press);
994
995                let icon_button = |button, selected, msg, img| {
996                    Container::new(
997                        Button::<_, IcedRenderer>::new(
998                            button,
999                            Space::new(Length::Units(60), Length::Units(60)),
1000                        )
1001                        .style(if selected {
1002                            selected_style
1003                        } else {
1004                            unselected_style
1005                        })
1006                        .on_press(msg),
1007                    )
1008                    .style(style::container::Style::image(img))
1009                };
1010                let icon_button_tooltip = |button, selected, msg, img, tooltip_i18n_key| {
1011                    icon_button(button, selected, msg, img).with_tooltip(
1012                        tooltip_manager,
1013                        move || {
1014                            let tooltip_text = i18n.get_msg(tooltip_i18n_key);
1015                            tooltip::text(&tooltip_text, tooltip_style)
1016                        },
1017                    )
1018                };
1019
1020                // TODO: tooltips
1021                let (tool, species, body_type) = if character_id.is_some() {
1022                    (Column::new(), Column::new(), Row::new())
1023                } else {
1024                    let (body_m_ico, body_f_ico) = match body.species {
1025                        humanoid::Species::Human => (imgs.human_m, imgs.human_f),
1026                        humanoid::Species::Orc => (imgs.orc_m, imgs.orc_f),
1027                        humanoid::Species::Dwarf => (imgs.dwarf_m, imgs.dwarf_f),
1028                        humanoid::Species::Elf => (imgs.elf_m, imgs.elf_f),
1029                        humanoid::Species::Draugr => (imgs.draugr_m, imgs.draugr_f),
1030                        humanoid::Species::Danari => (imgs.danari_m, imgs.danari_f),
1031                    };
1032                    let [ref mut body_m_button, ref mut body_f_button] = body_type_buttons;
1033                    let body_type = Row::with_children(vec![
1034                        icon_button(
1035                            body_m_button,
1036                            matches!(body.body_type, humanoid::BodyType::Male),
1037                            Message::BodyType(humanoid::BodyType::Male),
1038                            body_m_ico,
1039                        )
1040                        .into(),
1041                        icon_button(
1042                            body_f_button,
1043                            matches!(body.body_type, humanoid::BodyType::Female),
1044                            Message::BodyType(humanoid::BodyType::Female),
1045                            body_f_ico,
1046                        )
1047                        .into(),
1048                    ])
1049                    .spacing(1);
1050                    let (human_icon, orc_icon, dwarf_icon, elf_icon, draugr_icon, danari_icon) =
1051                        match body.body_type {
1052                            humanoid::BodyType::Male => (
1053                                self.imgs.human_m,
1054                                self.imgs.orc_m,
1055                                self.imgs.dwarf_m,
1056                                self.imgs.elf_m,
1057                                self.imgs.draugr_m,
1058                                self.imgs.danari_m,
1059                            ),
1060                            humanoid::BodyType::Female => (
1061                                self.imgs.human_f,
1062                                self.imgs.orc_f,
1063                                self.imgs.dwarf_f,
1064                                self.imgs.elf_f,
1065                                self.imgs.draugr_f,
1066                                self.imgs.danari_f,
1067                            ),
1068                        };
1069                    let [
1070                        ref mut human_button,
1071                        ref mut orc_button,
1072                        ref mut dwarf_button,
1073                        ref mut elf_button,
1074                        ref mut draugr_button,
1075                        ref mut danari_button,
1076                    ] = species_buttons;
1077                    let species = Column::with_children(vec![
1078                        Row::with_children(vec![
1079                            icon_button_tooltip(
1080                                human_button,
1081                                matches!(body.species, humanoid::Species::Human),
1082                                Message::Species(humanoid::Species::Human),
1083                                human_icon,
1084                                "common-species-human",
1085                            )
1086                            .into(),
1087                            icon_button_tooltip(
1088                                orc_button,
1089                                matches!(body.species, humanoid::Species::Orc),
1090                                Message::Species(humanoid::Species::Orc),
1091                                orc_icon,
1092                                "common-species-orc",
1093                            )
1094                            .into(),
1095                            icon_button_tooltip(
1096                                dwarf_button,
1097                                matches!(body.species, humanoid::Species::Dwarf),
1098                                Message::Species(humanoid::Species::Dwarf),
1099                                dwarf_icon,
1100                                "common-species-dwarf",
1101                            )
1102                            .into(),
1103                        ])
1104                        .spacing(1)
1105                        .into(),
1106                        Row::with_children(vec![
1107                            icon_button_tooltip(
1108                                elf_button,
1109                                matches!(body.species, humanoid::Species::Elf),
1110                                Message::Species(humanoid::Species::Elf),
1111                                elf_icon,
1112                                "common-species-elf",
1113                            )
1114                            .into(),
1115                            icon_button_tooltip(
1116                                draugr_button,
1117                                matches!(body.species, humanoid::Species::Draugr),
1118                                Message::Species(humanoid::Species::Draugr),
1119                                draugr_icon,
1120                                "common-species-draugr",
1121                            )
1122                            .into(),
1123                            icon_button_tooltip(
1124                                danari_button,
1125                                matches!(body.species, humanoid::Species::Danari),
1126                                Message::Species(humanoid::Species::Danari),
1127                                danari_icon,
1128                                "common-species-danari",
1129                            )
1130                            .into(),
1131                        ])
1132                        .spacing(1)
1133                        .into(),
1134                    ])
1135                    .spacing(1);
1136                    let [
1137                        ref mut sword_button,
1138                        ref mut swords_button,
1139                        ref mut axe_button,
1140                        ref mut hammer_button,
1141                        ref mut bow_button,
1142                        ref mut staff_button,
1143                    ] = tool_buttons;
1144                    let tool = Column::with_children(vec![
1145                        Row::with_children(vec![
1146                            icon_button_tooltip(
1147                                sword_button,
1148                                *mainhand == Some(STARTER_SWORD),
1149                                Message::Tool((Some(STARTER_SWORD), None)),
1150                                imgs.sword,
1151                                "common-weapons-greatsword",
1152                            )
1153                            .into(),
1154                            icon_button_tooltip(
1155                                hammer_button,
1156                                *mainhand == Some(STARTER_HAMMER),
1157                                Message::Tool((Some(STARTER_HAMMER), None)),
1158                                imgs.hammer,
1159                                "common-weapons-hammer",
1160                            )
1161                            .into(),
1162                            icon_button_tooltip(
1163                                axe_button,
1164                                *mainhand == Some(STARTER_AXE),
1165                                Message::Tool((Some(STARTER_AXE), None)),
1166                                imgs.axe,
1167                                "common-weapons-axe",
1168                            )
1169                            .into(),
1170                        ])
1171                        .spacing(1)
1172                        .into(),
1173                        Row::with_children(vec![
1174                            icon_button_tooltip(
1175                                swords_button,
1176                                *mainhand == Some(STARTER_SWORDS),
1177                                Message::Tool((Some(STARTER_SWORDS), Some(STARTER_SWORDS))),
1178                                imgs.swords,
1179                                "common-weapons-shortswords",
1180                            )
1181                            .into(),
1182                            icon_button_tooltip(
1183                                bow_button,
1184                                *mainhand == Some(STARTER_BOW),
1185                                Message::Tool((Some(STARTER_BOW), None)),
1186                                imgs.bow,
1187                                "common-weapons-bow",
1188                            )
1189                            .into(),
1190                            icon_button_tooltip(
1191                                staff_button,
1192                                *mainhand == Some(STARTER_STAFF),
1193                                Message::Tool((Some(STARTER_STAFF), None)),
1194                                imgs.staff,
1195                                "common-weapons-staff",
1196                            )
1197                            .into(),
1198                        ])
1199                        .spacing(1)
1200                        .into(),
1201                    ])
1202                    .spacing(1);
1203
1204                    (tool, species, body_type)
1205                };
1206
1207                const SLIDER_TEXT_SIZE: u16 = 20;
1208                const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
1209                const SLIDER_BAR_HEIGHT: u16 = 9;
1210                const SLIDER_BAR_PAD: u16 = 5;
1211                // Height of interactable area
1212                const SLIDER_HEIGHT: u16 = 30;
1213
1214                fn starter_slider<'a>(
1215                    text: String,
1216                    size: u16,
1217                    state: &'a mut slider::State,
1218                    max: u32,
1219                    selected_val: u32,
1220                    on_change: impl 'static + Fn(u32) -> Message,
1221                    imgs: &Imgs,
1222                ) -> Element<'a, Message> {
1223                    Column::with_children(vec![
1224                        Text::new(text).size(size).into(),
1225                        Slider::new(state, 0..=max, selected_val, on_change)
1226                            .height(SLIDER_HEIGHT)
1227                            .style(style::slider::Style::images(
1228                                imgs.slider_indicator,
1229                                imgs.slider_range,
1230                                SLIDER_BAR_PAD,
1231                                SLIDER_CURSOR_SIZE,
1232                                SLIDER_BAR_HEIGHT,
1233                            ))
1234                            .into(),
1235                    ])
1236                    .align_items(Align::Center)
1237                    .into()
1238                }
1239                fn char_slider<'a>(
1240                    text: String,
1241                    state: &'a mut slider::State,
1242                    max: u8,
1243                    selected_val: u8,
1244                    on_change: impl 'static + Fn(u8) -> Message,
1245                    (fonts, imgs): (&Fonts, &Imgs),
1246                ) -> Element<'a, Message> {
1247                    Column::with_children(vec![
1248                        Text::new(text)
1249                            .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1250                            .into(),
1251                        Slider::new(state, 0..=max, selected_val, on_change)
1252                            .height(SLIDER_HEIGHT)
1253                            .style(style::slider::Style::images(
1254                                imgs.slider_indicator,
1255                                imgs.slider_range,
1256                                SLIDER_BAR_PAD,
1257                                SLIDER_CURSOR_SIZE,
1258                                SLIDER_BAR_HEIGHT,
1259                            ))
1260                            .into(),
1261                    ])
1262                    .align_items(Align::Center)
1263                    .into()
1264                }
1265                fn char_slider_greyable<'a>(
1266                    active: bool,
1267                    text: String,
1268                    state: &'a mut slider::State,
1269                    max: u8,
1270                    selected_val: u8,
1271                    on_change: impl 'static + Fn(u8) -> Message,
1272                    (fonts, imgs): (&Fonts, &Imgs),
1273                ) -> Element<'a, Message> {
1274                    if active {
1275                        char_slider(text, state, max, selected_val, on_change, (fonts, imgs))
1276                    } else {
1277                        Column::with_children(vec![
1278                            Text::new(text)
1279                                .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1280                                .color(DISABLED_TEXT_COLOR)
1281                                .into(),
1282                            // "Disabled" slider
1283                            // TODO: add iced support for disabled sliders (like buttons)
1284                            Slider::new(state, 0..=max.into(), selected_val.into(), |_| {
1285                                Message::DoNothing
1286                            })
1287                            .height(SLIDER_HEIGHT)
1288                            .style(style::slider::Style {
1289                                cursor: style::slider::Cursor::Color(Rgba::zero()),
1290                                bar: style::slider::Bar::Image(
1291                                    imgs.slider_range,
1292                                    Rgba::from_translucent(255, 51),
1293                                    SLIDER_BAR_PAD,
1294                                ),
1295                                labels: false,
1296                                ..Default::default()
1297                            })
1298                            .into(),
1299                        ])
1300                        .align_items(Align::Center)
1301                        .into()
1302                    }
1303                }
1304
1305                let slider_options = Column::with_children(vec![
1306                    char_slider(
1307                        i18n.get_msg("char_selection-hair_style").into_owned(),
1308                        &mut sliders.hair_style,
1309                        body.species.num_hair_styles(body.body_type) - 1,
1310                        body.hair_style,
1311                        Message::HairStyle,
1312                        (fonts, imgs),
1313                    ),
1314                    char_slider(
1315                        i18n.get_msg("char_selection-hair_color").into_owned(),
1316                        &mut sliders.hair_color,
1317                        body.species.num_hair_colors() - 1,
1318                        body.hair_color,
1319                        Message::HairColor,
1320                        (fonts, imgs),
1321                    ),
1322                    char_slider(
1323                        i18n.get_msg("char_selection-skin").into_owned(),
1324                        &mut sliders.skin,
1325                        body.species.num_skin_colors() - 1,
1326                        body.skin,
1327                        Message::Skin,
1328                        (fonts, imgs),
1329                    ),
1330                    char_slider(
1331                        i18n.get_msg("char_selection-eyeshape").into_owned(),
1332                        &mut sliders.eyes,
1333                        body.species.num_eyes(body.body_type) - 1,
1334                        body.eyes,
1335                        Message::Eyes,
1336                        (fonts, imgs),
1337                    ),
1338                    char_slider(
1339                        i18n.get_msg("char_selection-eye_color").into_owned(),
1340                        &mut sliders.eye_color,
1341                        body.species.num_eye_colors() - 1,
1342                        body.eye_color,
1343                        Message::EyeColor,
1344                        (fonts, imgs),
1345                    ),
1346                    char_slider_greyable(
1347                        body.species.num_accessories(body.body_type) > 1,
1348                        i18n.get_msg("char_selection-accessories").into_owned(),
1349                        &mut sliders.accessory,
1350                        body.species.num_accessories(body.body_type) - 1,
1351                        body.accessory,
1352                        Message::Accessory,
1353                        (fonts, imgs),
1354                    ),
1355                    char_slider_greyable(
1356                        body.species.num_beards(body.body_type) > 1,
1357                        i18n.get_msg("char_selection-beard").into_owned(),
1358                        &mut sliders.beard,
1359                        body.species.num_beards(body.body_type) - 1,
1360                        body.beard,
1361                        Message::Beard,
1362                        (fonts, imgs),
1363                    ),
1364                ])
1365                .max_width(200)
1366                .padding(5);
1367
1368                let hardcore_checkbox = if character_id.is_some() {
1369                    Row::new()
1370                } else {
1371                    Row::with_children(vec![
1372                        Checkbox::new(
1373                            *hardcore_enabled,
1374                            i18n.get_msg("char_selection-hardcore"),
1375                            Message::HardcoreEnabled,
1376                        )
1377                        .size(32)
1378                        .spacing(8)
1379                        .text_size(24)
1380                        .style(style::checkbox::Style::new(
1381                            imgs.icon_border,
1382                            self.imgs.hardcore,
1383                        ))
1384                        .with_tooltip(tooltip_manager, move || {
1385                            let tooltip_text = i18n.get_msg("char_selection-hardcore_tooltip");
1386                            tooltip::text(&tooltip_text, tooltip_style)
1387                        })
1388                        .into(),
1389                    ])
1390                };
1391
1392                const CHAR_DICE_SIZE: u16 = 50;
1393                let rand_character = Button::new(
1394                    rand_character_button,
1395                    Space::new(Length::Units(CHAR_DICE_SIZE), Length::Units(CHAR_DICE_SIZE)),
1396                )
1397                .style(
1398                    style::button::Style::new(imgs.dice)
1399                        .hover_image(imgs.dice_hover)
1400                        .press_image(imgs.dice_press),
1401                )
1402                .on_press(Message::RandomizeCharacter)
1403                .with_tooltip(tooltip_manager, move || {
1404                    let tooltip_text = i18n.get_msg("common-rand_appearance");
1405                    tooltip::text(&tooltip_text, tooltip_style)
1406                });
1407
1408                let left_column_content = vec![
1409                    body_type.into(),
1410                    tool.into(),
1411                    species.into(),
1412                    slider_options.into(),
1413                    hardcore_checkbox.into(),
1414                    rand_character.into(),
1415                ];
1416
1417                let right_column_content = if character_id.is_none() {
1418                    let map_sz = Vec2::new(500, 500);
1419                    let map_img = Image::new(self.map_img)
1420                        .height(Length::Units(map_sz.x))
1421                        .width(Length::Units(map_sz.y));
1422                    /* .stroke(Stroke {
1423                        color: Color::WHITE,
1424                        width: 1.0,
1425                    }) */
1426                    //TODO: Add text-outline here whenever we updated iced to a version supporting
1427                    // this
1428
1429                    let map = if let Some(info) = self
1430                        .possible_starting_sites
1431                        .get(start_site_idx.unwrap_or_default())
1432                    {
1433                        let site_name = Text::new(
1434                            self.possible_starting_sites[start_site_idx.unwrap_or_default()]
1435                                .name
1436                                .as_deref()
1437                                .unwrap_or("Unknown"),
1438                        )
1439                        .horizontal_alignment(HorizontalAlignment::Left)
1440                        .color(Color::from_rgb(131.0, 102.0, 0.0));
1441                        let pos_frac = info
1442                            .wpos
1443                            .map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
1444                                e as f32 / sz as f32
1445                            });
1446                        let point = Vec2::new(pos_frac.x, 1.0 - pos_frac.y)
1447                            .map2(map_sz, |e, sz| e * sz as f32 - 12.0);
1448                        let marker_img = Image::new(imgs.town_marker)
1449                            .height(Length::Units(27))
1450                            .width(Length::Units(16));
1451                        let marker_content: Column<Message, IcedRenderer> = Column::new()
1452                            .spacing(2)
1453                            .push(site_name)
1454                            .push(marker_img)
1455                            .align_items(Align::Center);
1456
1457                        Overlay::new(
1458                            Container::new(marker_content)
1459                                .width(Length::Fill)
1460                                .height(Length::Fill)
1461                                .center_x()
1462                                .center_y(),
1463                            map_img,
1464                        )
1465                        .over_position(iced::Point::new(point.x, point.y - 34.0))
1466                        .into()
1467                    } else {
1468                        map_img.into()
1469                    };
1470
1471                    if self.possible_starting_sites.is_empty() {
1472                        vec![map]
1473                    } else {
1474                        let selected = start_site_idx.get_or_insert_with(|| {
1475                            thread_rng().gen_range(0..self.possible_starting_sites.len())
1476                        });
1477
1478                        let site_slider = starter_slider(
1479                            i18n.get_msg("char_selection-starting_site").into_owned(),
1480                            30,
1481                            &mut sliders.starting_site,
1482                            self.possible_starting_sites.len() as u32 - 1,
1483                            *selected as u32,
1484                            |x| Message::StartingSite(x as usize),
1485                            imgs,
1486                        );
1487                        let site_buttons = Row::with_children(vec![
1488                            neat_button(
1489                                prev_starting_site_button,
1490                                i18n.get_msg("char_selection-starting_site_prev")
1491                                    .into_owned(),
1492                                FILL_FRAC_ONE,
1493                                button_style,
1494                                Some(Message::PrevStartingSite),
1495                            ),
1496                            neat_button(
1497                                next_starting_site_button,
1498                                i18n.get_msg("char_selection-starting_site_next")
1499                                    .into_owned(),
1500                                FILL_FRAC_ONE,
1501                                button_style,
1502                                Some(Message::NextStartingSite),
1503                            ),
1504                        ])
1505                        .max_height(60)
1506                        .padding(15)
1507                        .into();
1508                        // Todo: use this to change the site icon if we use different starting site
1509                        // types
1510                        /* let site_kind = Text::new(i18n
1511                            .get_msg_ctx("char_selection-starting_site_kind", &i18n::fluent_args! {
1512                                "kind" => match self.possible_starting_sites[*start_site_idx].kind {
1513                                    SiteKind::Town => i18n.get_msg("hud-map-town").into_owned(),
1514                                    SiteKind::Castle => i18n.get_msg("hud-map-castle").into_owned(),
1515                                    SiteKind::Bridge => i18n.get_msg("hud-map-bridge").into_owned(),
1516                                    _ => "Unknown".to_string(),
1517                                },
1518                            })
1519                            .into_owned())
1520                        .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1521                        .into(); */
1522
1523                        vec![site_slider, map, site_buttons]
1524                    }
1525                } else {
1526                    // If we're editing an existing character, don't display the world column
1527                    Vec::new()
1528                };
1529
1530                let column_left = |column_content, scroll| {
1531                    let column = Container::new(
1532                        Scrollable::new(scroll)
1533                            .push(
1534                                Column::with_children(column_content)
1535                                    .align_items(Align::Center)
1536                                    .width(Length::Fill)
1537                                    .spacing(5)
1538                                    .padding(5),
1539                            )
1540                            .padding(5)
1541                            .width(Length::Fill)
1542                            .align_items(Align::Center)
1543                            .style(style::scrollable::Style {
1544                                track: None,
1545                                scroller: style::scrollable::Scroller::Color(UI_MAIN),
1546                            }),
1547                    )
1548                    .width(Length::Units(320)) // TODO: see if we can get iced to work with settings below
1549                    // .max_width(360)
1550                    // .width(Length::Fill)
1551                    .height(Length::Fill);
1552
1553                    Column::with_children(vec![
1554                        Container::new(column)
1555                            .style(style::container::Style::color(Rgba::from_translucent(
1556                                0,
1557                                BANNER_ALPHA,
1558                            )))
1559                            .width(Length::Units(320))
1560                            .center_x()
1561                            .into(),
1562                        Image::new(imgs.frame_bottom)
1563                            .height(Length::Units(40))
1564                            .width(Length::Units(320))
1565                            .color(Rgba::from_translucent(0, BANNER_ALPHA))
1566                            .into(),
1567                    ])
1568                    .height(Length::Fill)
1569                };
1570                let column_right = |column_content, scroll| {
1571                    let column = Container::new(
1572                        Scrollable::new(scroll)
1573                            .push(
1574                                Column::with_children(column_content)
1575                                    .align_items(Align::Center)
1576                                    .width(Length::Fill)
1577                                    .spacing(5)
1578                                    .padding(5),
1579                            )
1580                            .padding(5)
1581                            .width(Length::Fill)
1582                            .align_items(Align::Center)
1583                            .style(style::scrollable::Style {
1584                                track: None,
1585                                scroller: style::scrollable::Scroller::Color(UI_MAIN),
1586                            }),
1587                    )
1588                    .width(Length::Units(520)) // TODO: see if we can get iced to work with settings below
1589                    // .max_width(360)
1590                    // .width(Length::Fill)
1591                    .height(Length::Fill);
1592                    if character_id.is_none() {
1593                        Column::with_children(vec![
1594                            Container::new(column)
1595                                .style(style::container::Style::color(Rgba::from_translucent(
1596                                    0,
1597                                    BANNER_ALPHA,
1598                                )))
1599                                .width(Length::Units(520))
1600                                .center_x()
1601                                .into(),
1602                            Image::new(imgs.frame_bottom)
1603                                .height(Length::Units(40))
1604                                .width(Length::Units(520))
1605                                .color(Rgba::from_translucent(0, BANNER_ALPHA))
1606                                .into(),
1607                        ])
1608                        .height(Length::Fill)
1609                    } else {
1610                        Column::with_children(vec![Container::new(column).into()])
1611                    }
1612                };
1613
1614                let mouse_area =
1615                    MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill);
1616
1617                let top = Row::with_children(vec![
1618                    column_left(left_column_content, left_scroll).into(),
1619                    Column::with_children(
1620                        if let Some(warning_container) = warning_container.take() {
1621                            vec![warning_container.into(), mouse_area.into()]
1622                        } else {
1623                            vec![mouse_area.into()]
1624                        },
1625                    )
1626                    .width(Length::Fill)
1627                    .height(Length::Fill)
1628                    .into(),
1629                    column_right(right_column_content, right_scroll)
1630                        .width(Length::Units(520))
1631                        .into(),
1632                ])
1633                .padding(10)
1634                .width(Length::Fill)
1635                .height(Length::Fill);
1636
1637                let back = neat_button(
1638                    back_button,
1639                    i18n.get_msg("common-back").into_owned(),
1640                    FILL_FRAC_ONE,
1641                    button_style,
1642                    Some(Message::Back),
1643                );
1644
1645                const NAME_DICE_SIZE: u16 = 35;
1646                let rand_name = Button::new(
1647                    rand_name_button,
1648                    Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE)),
1649                )
1650                .style(
1651                    style::button::Style::new(imgs.dice)
1652                        .hover_image(imgs.dice_hover)
1653                        .press_image(imgs.dice_press),
1654                )
1655                .on_press(Message::RandomizeName)
1656                .with_tooltip(tooltip_manager, move || {
1657                    let tooltip_text = i18n.get_msg("common-rand_name");
1658                    tooltip::text(&tooltip_text, tooltip_style)
1659                });
1660
1661                let confirm_msg = if let Some(character_id) = character_id {
1662                    Message::ConfirmEdit(*character_id)
1663                } else {
1664                    Message::CreateCharacter
1665                };
1666
1667                let name_input = BackgroundContainer::new(
1668                    Image::new(imgs.name_input)
1669                        .height(Length::Units(40))
1670                        .fix_aspect_ratio(),
1671                    TextInput::new(
1672                        name_input,
1673                        &i18n.get_msg("character_window-character_name"),
1674                        name,
1675                        Message::Name,
1676                    )
1677                    .size(25)
1678                    .on_submit(confirm_msg.clone()),
1679                )
1680                .padding(Padding::new().horizontal(7).top(5));
1681
1682                let bottom_center = Container::new(
1683                    Row::with_children(vec![
1684                        rand_name.into(),
1685                        name_input.into(),
1686                        Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE))
1687                            .into(),
1688                    ])
1689                    .align_items(Align::Center)
1690                    .spacing(5)
1691                    .padding(16),
1692                )
1693                .style(style::container::Style::color(Rgba::new(0, 0, 0, 100)));
1694
1695                let create = neat_button(
1696                    create_button,
1697                    i18n.get_msg(if character_id.is_some() {
1698                        "common-confirm"
1699                    } else {
1700                        "common-create"
1701                    }),
1702                    FILL_FRAC_ONE,
1703                    button_style,
1704                    (!name.is_empty()).then_some(confirm_msg),
1705                );
1706
1707                let create: Element<Message> = if name.is_empty() {
1708                    create
1709                        .with_tooltip(tooltip_manager, move || {
1710                            let tooltip_text = i18n.get_msg("char_selection-create_info_name");
1711                            tooltip::text(&tooltip_text, tooltip_style)
1712                        })
1713                        .into()
1714                } else {
1715                    create
1716                };
1717
1718                let bottom = Row::with_children(vec![
1719                    Container::new(back)
1720                        .width(Length::Fill)
1721                        .height(Length::Units(SMALL_BUTTON_HEIGHT))
1722                        .into(),
1723                    Container::new(bottom_center)
1724                        .width(Length::Fill)
1725                        .center_x()
1726                        .into(),
1727                    Container::new(create)
1728                        .width(Length::Fill)
1729                        .height(Length::Units(SMALL_BUTTON_HEIGHT))
1730                        .align_x(Align::End)
1731                        .into(),
1732                ])
1733                .align_items(Align::End);
1734
1735                Column::with_children(vec![top.into(), bottom.into()])
1736                    .width(Length::Fill)
1737                    .height(Length::Fill)
1738                    .padding(5)
1739                    .into()
1740            },
1741        };
1742
1743        let children = if let Some(warning_container) = warning_container {
1744            vec![top_text.into(), warning_container.into(), content]
1745        } else {
1746            vec![top_text.into(), content]
1747        };
1748
1749        Container::new(
1750            Column::with_children(children)
1751                .spacing(3)
1752                .width(Length::Fill)
1753                .height(Length::Fill),
1754        )
1755        .padding(3)
1756        .into()
1757    }
1758
1759    fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
1760        match message {
1761            Message::Back => {
1762                if matches!(&self.mode, Mode::CreateOrEdit { .. }) {
1763                    self.mode = Mode::select(None);
1764                }
1765            },
1766            Message::Logout => {
1767                events.push(Event::Logout);
1768            },
1769            Message::ShowRules => {
1770                events.push(Event::ShowRules);
1771            },
1772            Message::ConfirmDeletion => {
1773                if let Mode::Select { info_content, .. } = &mut self.mode {
1774                    if let Some(InfoContent::Deletion(idx)) = info_content {
1775                        if let Some(id) = characters.get(*idx).and_then(|i| i.character.id) {
1776                            events.push(Event::DeleteCharacter(id));
1777                            // Deselect if the selected character was deleted
1778                            if Some(id) == self.selected {
1779                                self.selected = None;
1780                                events.push(Event::SelectCharacter(None));
1781                            }
1782                        }
1783                        *info_content = None;
1784                    }
1785                }
1786            },
1787            Message::CancelDeletion => {
1788                if let Mode::Select { info_content, .. } = &mut self.mode {
1789                    if let Some(InfoContent::Deletion(_)) = info_content {
1790                        *info_content = None;
1791                    }
1792                }
1793            },
1794            Message::ClearCharacterListError => {
1795                events.push(Event::ClearCharacterListError);
1796            },
1797            Message::DoNothing => {},
1798            _ if matches!(self.mode, Mode::Select {
1799                info_content: Some(_),
1800                ..
1801            }) =>
1802            {
1803                // Don't allow use of the UI on the select screen to deal with
1804                // things other than the event currently being
1805                // procesed; all the select screen events after this
1806                // modify the info content or selection, except for Spectate
1807                // which currently causes us to exit the
1808                // character select state.
1809            },
1810            Message::EnterWorld => {
1811                if let (Mode::Select { info_content, .. }, Some(selected)) =
1812                    (&mut self.mode, self.selected)
1813                {
1814                    events.push(Event::Play(selected));
1815                    *info_content = Some(InfoContent::JoiningCharacter);
1816                }
1817            },
1818            Message::Spectate => {
1819                if matches!(self.mode, Mode::Select { .. }) {
1820                    events.push(Event::Spectate);
1821                    // FIXME: Enter JoiningCharacter when we have a proper error
1822                    // event for spectating.
1823                }
1824            },
1825            Message::Select(id) => {
1826                if let Mode::Select { .. } = &mut self.mode {
1827                    self.selected = Some(id);
1828                    events.push(Event::SelectCharacter(Some(id)))
1829                }
1830            },
1831            Message::Delete(idx) => {
1832                if let Mode::Select { info_content, .. } = &mut self.mode {
1833                    *info_content = Some(InfoContent::Deletion(idx));
1834                }
1835            },
1836            Message::Edit(idx) => {
1837                if matches!(&self.mode, Mode::Select { .. }) {
1838                    if let Some(character) = characters.get(idx) {
1839                        if let comp::Body::Humanoid(body) = character.body {
1840                            if let Some(id) = character.character.id {
1841                                self.mode = Mode::edit(
1842                                    character.character.alias.clone(),
1843                                    id,
1844                                    body,
1845                                    &character.inventory,
1846                                );
1847                            }
1848                        }
1849                    }
1850                }
1851            },
1852            Message::NewCharacter => {
1853                if matches!(&self.mode, Mode::Select { .. }) {
1854                    self.mode = Mode::create(self.default_name.clone());
1855                }
1856            },
1857            Message::CreateCharacter => {
1858                if let Mode::CreateOrEdit {
1859                    name,
1860                    body,
1861                    hardcore_enabled,
1862                    mainhand,
1863                    offhand,
1864                    start_site_idx,
1865                    ..
1866                } = &self.mode
1867                {
1868                    events.push(Event::AddCharacter {
1869                        alias: name.clone(),
1870                        mainhand: mainhand.map(String::from),
1871                        offhand: offhand.map(String::from),
1872                        body: comp::Body::Humanoid(*body),
1873                        hardcore: *hardcore_enabled,
1874                        start_site: self
1875                            .possible_starting_sites
1876                            .get(start_site_idx.unwrap_or_default())
1877                            .map(|info| info.id),
1878                    });
1879                    self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
1880                }
1881            },
1882            Message::ConfirmEdit(character_id) => {
1883                if let Mode::CreateOrEdit { name, body, .. } = &self.mode {
1884                    events.push(Event::EditCharacter {
1885                        alias: name.clone(),
1886                        character_id,
1887                        body: comp::Body::Humanoid(*body),
1888                    });
1889                    self.mode = Mode::select(Some(InfoContent::EditingCharacter));
1890                }
1891            },
1892            Message::Name(value) => {
1893                if let Mode::CreateOrEdit { name, .. } = &mut self.mode {
1894                    *name = value.chars().take(MAX_NAME_LENGTH).collect();
1895                }
1896            },
1897            Message::BodyType(value) => {
1898                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1899                    body.body_type = value;
1900                    body.validate();
1901                }
1902            },
1903            Message::Species(value) => {
1904                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1905                    body.species = value;
1906                    body.validate();
1907                }
1908            },
1909            Message::Tool(value) => {
1910                if let Mode::CreateOrEdit {
1911                    mainhand,
1912                    offhand,
1913                    inventory,
1914                    ..
1915                } = &mut self.mode
1916                {
1917                    *mainhand = value.0;
1918                    *offhand = value.1;
1919                    inventory.replace_loadout_item(
1920                        EquipSlot::ActiveMainhand,
1921                        mainhand.map(Item::new_from_asset_expect),
1922                        // Voxygen is not authoritative on inventory so we don't care if fake time
1923                        // is supplied
1924                        Time(0.0),
1925                    );
1926                    inventory.replace_loadout_item(
1927                        EquipSlot::ActiveOffhand,
1928                        offhand.map(Item::new_from_asset_expect),
1929                        // Voxygen is not authoritative on inventory so we don't care if fake time
1930                        // is supplied
1931                        Time(0.0),
1932                    );
1933                }
1934            },
1935            //Todo: Add species and body type to randomization.
1936            Message::RandomizeCharacter => {
1937                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1938                    let body_type = body.body_type;
1939                    let species = body.species;
1940                    let mut rng = rand::thread_rng();
1941                    body.hair_style = rng.gen_range(0..species.num_hair_styles(body_type));
1942                    body.beard = rng.gen_range(0..species.num_beards(body_type));
1943                    body.accessory = rng.gen_range(0..species.num_accessories(body_type));
1944                    body.hair_color = rng.gen_range(0..species.num_hair_colors());
1945                    body.skin = rng.gen_range(0..species.num_skin_colors());
1946                    body.eye_color = rng.gen_range(0..species.num_eye_colors());
1947                    body.eyes = rng.gen_range(0..species.num_eyes(body_type));
1948                }
1949            },
1950            Message::HardcoreEnabled(checked) => {
1951                if let Mode::CreateOrEdit {
1952                    hardcore_enabled: hardcore_checkbox,
1953                    ..
1954                } = &mut self.mode
1955                {
1956                    *hardcore_checkbox = checked;
1957                }
1958            },
1959            Message::RandomizeName => {
1960                if let Mode::CreateOrEdit { name, body, .. } = &mut self.mode {
1961                    use common::npc;
1962                    *name = npc::get_npc_name(
1963                        npc::NpcKind::Humanoid,
1964                        npc::BodyType::from_body(comp::Body::Humanoid(*body)),
1965                    );
1966                }
1967            },
1968            Message::HairStyle(value) => {
1969                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1970                    body.hair_style = value;
1971                    body.validate();
1972                }
1973            },
1974            Message::HairColor(value) => {
1975                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1976                    body.hair_color = value;
1977                    body.validate();
1978                }
1979            },
1980            Message::Skin(value) => {
1981                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1982                    body.skin = value;
1983                    body.validate();
1984                }
1985            },
1986            Message::Eyes(value) => {
1987                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1988                    body.eyes = value;
1989                    body.validate();
1990                }
1991            },
1992            Message::EyeColor(value) => {
1993                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1994                    body.eye_color = value;
1995                    body.validate();
1996                }
1997            },
1998            Message::Accessory(value) => {
1999                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2000                    body.accessory = value;
2001                    body.validate();
2002                }
2003            },
2004            Message::Beard(value) => {
2005                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2006                    body.beard = value;
2007                    body.validate();
2008                }
2009            },
2010            Message::StartingSite(idx) => {
2011                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2012                    *start_site_idx = Some(idx);
2013                }
2014            },
2015            Message::PrevStartingSite => {
2016                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2017                    if !self.possible_starting_sites.is_empty() {
2018                        *start_site_idx = Some(
2019                            (start_site_idx.unwrap_or_default()
2020                                + self.possible_starting_sites.len()
2021                                - 1)
2022                                % self.possible_starting_sites.len(),
2023                        );
2024                    }
2025                }
2026            },
2027            Message::NextStartingSite => {
2028                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2029                    if !self.possible_starting_sites.is_empty() {
2030                        *start_site_idx = Some(
2031                            (start_site_idx.unwrap_or_default()
2032                                + self.possible_starting_sites.len()
2033                                + 1)
2034                                % self.possible_starting_sites.len(),
2035                        );
2036                    }
2037                }
2038            },
2039        }
2040    }
2041
2042    /// Get the character to display
2043    pub fn display_body_inventory<'a>(
2044        &'a self,
2045        characters: &'a [CharacterItem],
2046    ) -> Option<(comp::Body, &'a Inventory)> {
2047        match &self.mode {
2048            Mode::Select { .. } => self
2049                .selected
2050                .and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
2051                .map(|i| (i.body, &i.inventory)),
2052            Mode::CreateOrEdit {
2053                inventory, body, ..
2054            } => Some((comp::Body::Humanoid(*body), inventory)),
2055        }
2056    }
2057}
2058
2059pub struct CharSelectionUi {
2060    ui: Ui,
2061    controls: Controls,
2062    enter_pressed: bool,
2063    select_character: Option<CharacterId>,
2064    pub error: Option<String>,
2065}
2066
2067impl CharSelectionUi {
2068    pub fn new(global_state: &mut GlobalState, client: &Client) -> Self {
2069        // Load up the last selected character for this server
2070        let server_name = &client.server_info().name;
2071        let selected_character = global_state.profile.get_selected_character(server_name);
2072
2073        // Load language
2074        let i18n = global_state.i18n.read();
2075
2076        // TODO: don't add default font twice
2077        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2078
2079        let mut ui = Ui::new(
2080            &mut global_state.window,
2081            font,
2082            global_state.settings.interface.ui_scale,
2083        )
2084        .unwrap();
2085
2086        let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
2087
2088        #[cfg(feature = "singleplayer")]
2089        let default_name = match global_state.singleplayer.is_running() {
2090            true => String::new(),
2091            false => global_state.settings.networking.username.clone(),
2092        };
2093
2094        #[cfg(not(feature = "singleplayer"))]
2095        let default_name = global_state.settings.networking.username.clone();
2096
2097        let controls = Controls::new(
2098            fonts,
2099            Imgs::load(&mut ui).expect("Failed to load images"),
2100            selected_character,
2101            default_name,
2102            client.server_info(),
2103            ui.add_graphic(Graphic::Image(
2104                Arc::clone(client.world_data().topo_map_image()),
2105                Some(default_water_color()),
2106            )),
2107            client
2108                .possible_starting_sites()
2109                .iter()
2110                .filter_map(|site_id| client.sites().get(site_id))
2111                .map(|info| info.site.clone())
2112                .collect(),
2113            client.world_data().chunk_size().as_(),
2114            client.server_description().rules.is_some(),
2115        );
2116
2117        Self {
2118            ui,
2119            controls,
2120            enter_pressed: false,
2121            select_character: None,
2122            error: None,
2123        }
2124    }
2125
2126    pub fn display_body_inventory<'a>(
2127        &'a self,
2128        characters: &'a [CharacterItem],
2129    ) -> Option<(comp::Body, &'a Inventory)> {
2130        self.controls.display_body_inventory(characters)
2131    }
2132
2133    pub fn handle_event(&mut self, event: window::Event) -> bool {
2134        match event {
2135            window::Event::IcedUi(event) => {
2136                // Enter Key pressed
2137                use iced::keyboard;
2138                if let iced::Event::Keyboard(keyboard::Event::KeyPressed {
2139                    key_code: keyboard::KeyCode::Enter,
2140                    ..
2141                }) = event
2142                {
2143                    self.enter_pressed = true;
2144                }
2145
2146                self.ui.handle_event(event);
2147                true
2148            },
2149            window::Event::MouseButton(_, window::PressState::Pressed) => {
2150                !self.controls.mouse_detector.mouse_over()
2151            },
2152            window::Event::ScaleFactorChanged(s) => {
2153                self.ui.scale_factor_changed(s);
2154                false
2155            },
2156            _ => false,
2157        }
2158    }
2159
2160    pub fn update_language(&mut self, i18n: LocalizationHandle) {
2161        let i18n = i18n.read();
2162        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2163
2164        self.ui.clear_fonts(font);
2165        self.controls.fonts =
2166            Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
2167    }
2168
2169    pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) {
2170        self.ui.set_scaling_mode(scale_mode);
2171    }
2172
2173    pub fn select_character(&mut self, id: CharacterId) { self.select_character = Some(id); }
2174
2175    pub fn display_error(&mut self, error: String) { self.error = Some(error); }
2176
2177    // TODO: do we need whole client here or just character list?
2178    pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
2179        let mut events = Vec::new();
2180        let i18n = global_state.i18n.read();
2181
2182        let (mut messages, _) = self.ui.maintain(
2183            self.controls
2184                .view(&global_state.settings, client, &self.error, &i18n),
2185            global_state.window.renderer_mut(),
2186            None,
2187            &mut global_state.clipboard,
2188        );
2189
2190        if self.enter_pressed {
2191            self.enter_pressed = false;
2192            messages.push(match self.controls.mode {
2193                Mode::Select { .. } => Message::EnterWorld,
2194                Mode::CreateOrEdit { .. } => Message::CreateCharacter,
2195            });
2196        }
2197
2198        if let Some(id) = self.select_character.take() {
2199            messages.push(Message::Select(id))
2200        }
2201
2202        messages.into_iter().for_each(|message| {
2203            self.controls
2204                .update(message, &mut events, &client.character_list().characters)
2205        });
2206
2207        events
2208    }
2209
2210    pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); }
2211}
2212
2213#[derive(Default)]
2214struct Sliders {
2215    hair_style: slider::State,
2216    hair_color: slider::State,
2217    skin: slider::State,
2218    eyes: slider::State,
2219    eye_color: slider::State,
2220    accessory: slider::State,
2221    beard: slider::State,
2222    starting_site: slider::State,
2223}