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::{Marker, SiteId};
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<Marker>,
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<Marker>,
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                info_content,
483                characters_scroll,
484                character_buttons,
485                new_character_button,
486                logout_button,
487                rule_button,
488                enter_world_button,
489                spectate_button,
490                yes_button,
491                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                left_scroll,
971                right_scroll,
972                body_type_buttons,
973                species_buttons,
974                tool_buttons,
975                sliders,
976                hardcore_enabled,
977                name_input,
978                back_button,
979                create_button,
980                rand_character_button,
981                rand_name_button,
982                prev_starting_site_button,
983                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 [body_m_button, 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                        human_button,
1071                        orc_button,
1072                        dwarf_button,
1073                        elf_button,
1074                        draugr_button,
1075                        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                        sword_button,
1138                        swords_button,
1139                        axe_button,
1140                        hammer_button,
1141                        bow_button,
1142                        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_ref()
1437                                .map(|name| i18n.get_content(name))
1438                                .unwrap_or_else(|| "Unknown".to_string()),
1439                        )
1440                        .horizontal_alignment(HorizontalAlignment::Left)
1441                        .color(Color::from_rgb(131.0, 102.0, 0.0));
1442                        let pos_frac = info
1443                            .wpos
1444                            .map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
1445                                e as f32 / sz as f32
1446                            });
1447                        let point = Vec2::new(pos_frac.x, 1.0 - pos_frac.y)
1448                            .map2(map_sz, |e, sz| e * sz as f32 - 12.0);
1449                        let marker_img = Image::new(imgs.town_marker)
1450                            .height(Length::Units(27))
1451                            .width(Length::Units(16));
1452                        let marker_content: Column<Message, IcedRenderer> = Column::new()
1453                            .spacing(2)
1454                            .push(site_name)
1455                            .push(marker_img)
1456                            .align_items(Align::Center);
1457
1458                        Overlay::new(
1459                            Container::new(marker_content)
1460                                .width(Length::Fill)
1461                                .height(Length::Fill)
1462                                .center_x()
1463                                .center_y(),
1464                            map_img,
1465                        )
1466                        .over_position(iced::Point::new(point.x, point.y - 34.0))
1467                        .into()
1468                    } else {
1469                        map_img.into()
1470                    };
1471
1472                    if self.possible_starting_sites.is_empty() {
1473                        vec![map]
1474                    } else {
1475                        let selected = start_site_idx.get_or_insert_with(|| {
1476                            thread_rng().gen_range(0..self.possible_starting_sites.len())
1477                        });
1478
1479                        let site_slider = starter_slider(
1480                            i18n.get_msg("char_selection-starting_site").into_owned(),
1481                            30,
1482                            &mut sliders.starting_site,
1483                            self.possible_starting_sites.len() as u32 - 1,
1484                            *selected as u32,
1485                            |x| Message::StartingSite(x as usize),
1486                            imgs,
1487                        );
1488                        let site_buttons = Row::with_children(vec![
1489                            neat_button(
1490                                prev_starting_site_button,
1491                                i18n.get_msg("char_selection-starting_site_prev")
1492                                    .into_owned(),
1493                                FILL_FRAC_ONE,
1494                                button_style,
1495                                Some(Message::PrevStartingSite),
1496                            ),
1497                            neat_button(
1498                                next_starting_site_button,
1499                                i18n.get_msg("char_selection-starting_site_next")
1500                                    .into_owned(),
1501                                FILL_FRAC_ONE,
1502                                button_style,
1503                                Some(Message::NextStartingSite),
1504                            ),
1505                        ])
1506                        .max_height(60)
1507                        .padding(15)
1508                        .into();
1509                        // Todo: use this to change the site icon if we use different starting site
1510                        // types
1511                        /* let site_kind = Text::new(i18n
1512                            .get_msg_ctx("char_selection-starting_site_kind", &i18n::fluent_args! {
1513                                "kind" => match self.possible_starting_sites[*start_site_idx].kind {
1514                                    SiteKind::Town => i18n.get_msg("hud-map-town").into_owned(),
1515                                    SiteKind::Castle => i18n.get_msg("hud-map-castle").into_owned(),
1516                                    SiteKind::Bridge => i18n.get_msg("hud-map-bridge").into_owned(),
1517                                    _ => "Unknown".to_string(),
1518                                },
1519                            })
1520                            .into_owned())
1521                        .size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
1522                        .into(); */
1523
1524                        vec![site_slider, map, site_buttons]
1525                    }
1526                } else {
1527                    // If we're editing an existing character, don't display the world column
1528                    Vec::new()
1529                };
1530
1531                let column_left = |column_content, scroll| {
1532                    let column = Container::new(
1533                        Scrollable::new(scroll)
1534                            .push(
1535                                Column::with_children(column_content)
1536                                    .align_items(Align::Center)
1537                                    .width(Length::Fill)
1538                                    .spacing(5)
1539                                    .padding(5),
1540                            )
1541                            .padding(5)
1542                            .width(Length::Fill)
1543                            .align_items(Align::Center)
1544                            .style(style::scrollable::Style {
1545                                track: None,
1546                                scroller: style::scrollable::Scroller::Color(UI_MAIN),
1547                            }),
1548                    )
1549                    .width(Length::Units(320)) // TODO: see if we can get iced to work with settings below
1550                    // .max_width(360)
1551                    // .width(Length::Fill)
1552                    .height(Length::Fill);
1553
1554                    Column::with_children(vec![
1555                        Container::new(column)
1556                            .style(style::container::Style::color(Rgba::from_translucent(
1557                                0,
1558                                BANNER_ALPHA,
1559                            )))
1560                            .width(Length::Units(320))
1561                            .center_x()
1562                            .into(),
1563                        Image::new(imgs.frame_bottom)
1564                            .height(Length::Units(40))
1565                            .width(Length::Units(320))
1566                            .color(Rgba::from_translucent(0, BANNER_ALPHA))
1567                            .into(),
1568                    ])
1569                    .height(Length::Fill)
1570                };
1571                let column_right = |column_content, scroll| {
1572                    let column = Container::new(
1573                        Scrollable::new(scroll)
1574                            .push(
1575                                Column::with_children(column_content)
1576                                    .align_items(Align::Center)
1577                                    .width(Length::Fill)
1578                                    .spacing(5)
1579                                    .padding(5),
1580                            )
1581                            .padding(5)
1582                            .width(Length::Fill)
1583                            .align_items(Align::Center)
1584                            .style(style::scrollable::Style {
1585                                track: None,
1586                                scroller: style::scrollable::Scroller::Color(UI_MAIN),
1587                            }),
1588                    )
1589                    .width(Length::Units(520)) // TODO: see if we can get iced to work with settings below
1590                    // .max_width(360)
1591                    // .width(Length::Fill)
1592                    .height(Length::Fill);
1593                    if character_id.is_none() {
1594                        Column::with_children(vec![
1595                            Container::new(column)
1596                                .style(style::container::Style::color(Rgba::from_translucent(
1597                                    0,
1598                                    BANNER_ALPHA,
1599                                )))
1600                                .width(Length::Units(520))
1601                                .center_x()
1602                                .into(),
1603                            Image::new(imgs.frame_bottom)
1604                                .height(Length::Units(40))
1605                                .width(Length::Units(520))
1606                                .color(Rgba::from_translucent(0, BANNER_ALPHA))
1607                                .into(),
1608                        ])
1609                        .height(Length::Fill)
1610                    } else {
1611                        Column::with_children(vec![Container::new(column).into()])
1612                    }
1613                };
1614
1615                let mouse_area =
1616                    MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill);
1617
1618                let top = Row::with_children(vec![
1619                    column_left(left_column_content, left_scroll).into(),
1620                    Column::with_children(
1621                        if let Some(warning_container) = warning_container.take() {
1622                            vec![warning_container.into(), mouse_area.into()]
1623                        } else {
1624                            vec![mouse_area.into()]
1625                        },
1626                    )
1627                    .width(Length::Fill)
1628                    .height(Length::Fill)
1629                    .into(),
1630                    column_right(right_column_content, right_scroll)
1631                        .width(Length::Units(520))
1632                        .into(),
1633                ])
1634                .padding(10)
1635                .width(Length::Fill)
1636                .height(Length::Fill);
1637
1638                let back = neat_button(
1639                    back_button,
1640                    i18n.get_msg("common-back").into_owned(),
1641                    FILL_FRAC_ONE,
1642                    button_style,
1643                    Some(Message::Back),
1644                );
1645
1646                const NAME_DICE_SIZE: u16 = 35;
1647                let rand_name = Button::new(
1648                    rand_name_button,
1649                    Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE)),
1650                )
1651                .style(
1652                    style::button::Style::new(imgs.dice)
1653                        .hover_image(imgs.dice_hover)
1654                        .press_image(imgs.dice_press),
1655                )
1656                .on_press(Message::RandomizeName)
1657                .with_tooltip(tooltip_manager, move || {
1658                    let tooltip_text = i18n.get_msg("common-rand_name");
1659                    tooltip::text(&tooltip_text, tooltip_style)
1660                });
1661
1662                let confirm_msg = if let Some(character_id) = character_id {
1663                    Message::ConfirmEdit(*character_id)
1664                } else {
1665                    Message::CreateCharacter
1666                };
1667
1668                let name_input = BackgroundContainer::new(
1669                    Image::new(imgs.name_input)
1670                        .height(Length::Units(40))
1671                        .fix_aspect_ratio(),
1672                    TextInput::new(
1673                        name_input,
1674                        &i18n.get_msg("character_window-character_name"),
1675                        name,
1676                        Message::Name,
1677                    )
1678                    .size(25)
1679                    .on_submit(confirm_msg.clone()),
1680                )
1681                .padding(Padding::new().horizontal(7).top(5));
1682
1683                let bottom_center = Container::new(
1684                    Row::with_children(vec![
1685                        rand_name.into(),
1686                        name_input.into(),
1687                        Space::new(Length::Units(NAME_DICE_SIZE), Length::Units(NAME_DICE_SIZE))
1688                            .into(),
1689                    ])
1690                    .align_items(Align::Center)
1691                    .spacing(5)
1692                    .padding(16),
1693                )
1694                .style(style::container::Style::color(Rgba::new(0, 0, 0, 100)));
1695
1696                let create = neat_button(
1697                    create_button,
1698                    i18n.get_msg(if character_id.is_some() {
1699                        "common-confirm"
1700                    } else {
1701                        "common-create"
1702                    }),
1703                    FILL_FRAC_ONE,
1704                    button_style,
1705                    (!name.is_empty()).then_some(confirm_msg),
1706                );
1707
1708                let create: Element<Message> = if name.is_empty() {
1709                    create
1710                        .with_tooltip(tooltip_manager, move || {
1711                            let tooltip_text = i18n.get_msg("char_selection-create_info_name");
1712                            tooltip::text(&tooltip_text, tooltip_style)
1713                        })
1714                        .into()
1715                } else {
1716                    create
1717                };
1718
1719                let bottom = Row::with_children(vec![
1720                    Container::new(back)
1721                        .width(Length::Fill)
1722                        .height(Length::Units(SMALL_BUTTON_HEIGHT))
1723                        .into(),
1724                    Container::new(bottom_center)
1725                        .width(Length::Fill)
1726                        .center_x()
1727                        .into(),
1728                    Container::new(create)
1729                        .width(Length::Fill)
1730                        .height(Length::Units(SMALL_BUTTON_HEIGHT))
1731                        .align_x(Align::End)
1732                        .into(),
1733                ])
1734                .align_items(Align::End);
1735
1736                Column::with_children(vec![top.into(), bottom.into()])
1737                    .width(Length::Fill)
1738                    .height(Length::Fill)
1739                    .padding(5)
1740                    .into()
1741            },
1742        };
1743
1744        let children = if let Some(warning_container) = warning_container {
1745            vec![top_text.into(), warning_container.into(), content]
1746        } else {
1747            vec![top_text.into(), content]
1748        };
1749
1750        Container::new(
1751            Column::with_children(children)
1752                .spacing(3)
1753                .width(Length::Fill)
1754                .height(Length::Fill),
1755        )
1756        .padding(3)
1757        .into()
1758    }
1759
1760    fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
1761        match message {
1762            Message::Back => {
1763                if matches!(&self.mode, Mode::CreateOrEdit { .. }) {
1764                    self.mode = Mode::select(None);
1765                }
1766            },
1767            Message::Logout => {
1768                events.push(Event::Logout);
1769            },
1770            Message::ShowRules => {
1771                events.push(Event::ShowRules);
1772            },
1773            Message::ConfirmDeletion => {
1774                if let Mode::Select { info_content, .. } = &mut self.mode {
1775                    if let Some(InfoContent::Deletion(idx)) = info_content {
1776                        if let Some(id) = characters.get(*idx).and_then(|i| i.character.id) {
1777                            events.push(Event::DeleteCharacter(id));
1778                            // Deselect if the selected character was deleted
1779                            if Some(id) == self.selected {
1780                                self.selected = None;
1781                                events.push(Event::SelectCharacter(None));
1782                            }
1783                        }
1784                        *info_content = None;
1785                    }
1786                }
1787            },
1788            Message::CancelDeletion => {
1789                if let Mode::Select { info_content, .. } = &mut self.mode {
1790                    if let Some(InfoContent::Deletion(_)) = info_content {
1791                        *info_content = None;
1792                    }
1793                }
1794            },
1795            Message::ClearCharacterListError => {
1796                events.push(Event::ClearCharacterListError);
1797            },
1798            Message::DoNothing => {},
1799            _ if matches!(self.mode, Mode::Select {
1800                info_content: Some(_),
1801                ..
1802            }) =>
1803            {
1804                // Don't allow use of the UI on the select screen to deal with
1805                // things other than the event currently being
1806                // procesed; all the select screen events after this
1807                // modify the info content or selection, except for Spectate
1808                // which currently causes us to exit the
1809                // character select state.
1810            },
1811            Message::EnterWorld => {
1812                if let (Mode::Select { info_content, .. }, Some(selected)) =
1813                    (&mut self.mode, self.selected)
1814                {
1815                    events.push(Event::Play(selected));
1816                    *info_content = Some(InfoContent::JoiningCharacter);
1817                }
1818            },
1819            Message::Spectate => {
1820                if matches!(self.mode, Mode::Select { .. }) {
1821                    events.push(Event::Spectate);
1822                    // FIXME: Enter JoiningCharacter when we have a proper error
1823                    // event for spectating.
1824                }
1825            },
1826            Message::Select(id) => {
1827                if let Mode::Select { .. } = &mut self.mode {
1828                    self.selected = Some(id);
1829                    events.push(Event::SelectCharacter(Some(id)))
1830                }
1831            },
1832            Message::Delete(idx) => {
1833                if let Mode::Select { info_content, .. } = &mut self.mode {
1834                    *info_content = Some(InfoContent::Deletion(idx));
1835                }
1836            },
1837            Message::Edit(idx) => {
1838                if matches!(&self.mode, Mode::Select { .. }) {
1839                    if let Some(character) = characters.get(idx) {
1840                        if let comp::Body::Humanoid(body) = character.body {
1841                            if let Some(id) = character.character.id {
1842                                self.mode = Mode::edit(
1843                                    character.character.alias.clone(),
1844                                    id,
1845                                    body,
1846                                    &character.inventory,
1847                                );
1848                            }
1849                        }
1850                    }
1851                }
1852            },
1853            Message::NewCharacter => {
1854                if matches!(&self.mode, Mode::Select { .. }) {
1855                    self.mode = Mode::create(self.default_name.clone());
1856                }
1857            },
1858            Message::CreateCharacter => {
1859                if let Mode::CreateOrEdit {
1860                    name,
1861                    body,
1862                    hardcore_enabled,
1863                    mainhand,
1864                    offhand,
1865                    start_site_idx,
1866                    ..
1867                } = &self.mode
1868                {
1869                    events.push(Event::AddCharacter {
1870                        alias: name.clone(),
1871                        mainhand: mainhand.map(String::from),
1872                        offhand: offhand.map(String::from),
1873                        body: comp::Body::Humanoid(*body),
1874                        hardcore: *hardcore_enabled,
1875                        start_site: self
1876                            .possible_starting_sites
1877                            .get(start_site_idx.unwrap_or_default())
1878                            .and_then(|info| info.id),
1879                    });
1880                    self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
1881                }
1882            },
1883            Message::ConfirmEdit(character_id) => {
1884                if let Mode::CreateOrEdit { name, body, .. } = &self.mode {
1885                    events.push(Event::EditCharacter {
1886                        alias: name.clone(),
1887                        character_id,
1888                        body: comp::Body::Humanoid(*body),
1889                    });
1890                    self.mode = Mode::select(Some(InfoContent::EditingCharacter));
1891                }
1892            },
1893            Message::Name(value) => {
1894                if let Mode::CreateOrEdit { name, .. } = &mut self.mode {
1895                    *name = value.chars().take(MAX_NAME_LENGTH).collect();
1896                }
1897            },
1898            Message::BodyType(value) => {
1899                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1900                    body.body_type = value;
1901                    body.validate();
1902                }
1903            },
1904            Message::Species(value) => {
1905                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1906                    body.species = value;
1907                    body.validate();
1908                }
1909            },
1910            Message::Tool(value) => {
1911                if let Mode::CreateOrEdit {
1912                    mainhand,
1913                    offhand,
1914                    inventory,
1915                    ..
1916                } = &mut self.mode
1917                {
1918                    *mainhand = value.0;
1919                    *offhand = value.1;
1920                    inventory.replace_loadout_item(
1921                        EquipSlot::ActiveMainhand,
1922                        mainhand.map(Item::new_from_asset_expect),
1923                        // Voxygen is not authoritative on inventory so we don't care if fake time
1924                        // is supplied
1925                        Time(0.0),
1926                    );
1927                    inventory.replace_loadout_item(
1928                        EquipSlot::ActiveOffhand,
1929                        offhand.map(Item::new_from_asset_expect),
1930                        // Voxygen is not authoritative on inventory so we don't care if fake time
1931                        // is supplied
1932                        Time(0.0),
1933                    );
1934                }
1935            },
1936            //Todo: Add species and body type to randomization.
1937            Message::RandomizeCharacter => {
1938                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1939                    let body_type = body.body_type;
1940                    let species = body.species;
1941                    let mut rng = rand::thread_rng();
1942                    body.hair_style = rng.gen_range(0..species.num_hair_styles(body_type));
1943                    body.beard = rng.gen_range(0..species.num_beards(body_type));
1944                    body.accessory = rng.gen_range(0..species.num_accessories(body_type));
1945                    body.hair_color = rng.gen_range(0..species.num_hair_colors());
1946                    body.skin = rng.gen_range(0..species.num_skin_colors());
1947                    body.eye_color = rng.gen_range(0..species.num_eye_colors());
1948                    body.eyes = rng.gen_range(0..species.num_eyes(body_type));
1949                }
1950            },
1951            Message::HardcoreEnabled(checked) => {
1952                if let Mode::CreateOrEdit {
1953                    hardcore_enabled: hardcore_checkbox,
1954                    ..
1955                } = &mut self.mode
1956                {
1957                    *hardcore_checkbox = checked;
1958                }
1959            },
1960            Message::RandomizeName => {
1961                if let Mode::CreateOrEdit { name, body, .. } = &mut self.mode {
1962                    use common::npc;
1963                    *name = npc::get_npc_name(
1964                        npc::NpcKind::Humanoid,
1965                        npc::BodyType::from_body(comp::Body::Humanoid(*body)),
1966                    );
1967                }
1968            },
1969            Message::HairStyle(value) => {
1970                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1971                    body.hair_style = value;
1972                    body.validate();
1973                }
1974            },
1975            Message::HairColor(value) => {
1976                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1977                    body.hair_color = value;
1978                    body.validate();
1979                }
1980            },
1981            Message::Skin(value) => {
1982                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1983                    body.skin = value;
1984                    body.validate();
1985                }
1986            },
1987            Message::Eyes(value) => {
1988                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1989                    body.eyes = value;
1990                    body.validate();
1991                }
1992            },
1993            Message::EyeColor(value) => {
1994                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
1995                    body.eye_color = value;
1996                    body.validate();
1997                }
1998            },
1999            Message::Accessory(value) => {
2000                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2001                    body.accessory = value;
2002                    body.validate();
2003                }
2004            },
2005            Message::Beard(value) => {
2006                if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
2007                    body.beard = value;
2008                    body.validate();
2009                }
2010            },
2011            Message::StartingSite(idx) => {
2012                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2013                    *start_site_idx = Some(idx);
2014                }
2015            },
2016            Message::PrevStartingSite => {
2017                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2018                    if !self.possible_starting_sites.is_empty() {
2019                        *start_site_idx = Some(
2020                            (start_site_idx.unwrap_or_default()
2021                                + self.possible_starting_sites.len()
2022                                - 1)
2023                                % self.possible_starting_sites.len(),
2024                        );
2025                    }
2026                }
2027            },
2028            Message::NextStartingSite => {
2029                if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
2030                    if !self.possible_starting_sites.is_empty() {
2031                        *start_site_idx = Some(
2032                            (start_site_idx.unwrap_or_default()
2033                                + self.possible_starting_sites.len()
2034                                + 1)
2035                                % self.possible_starting_sites.len(),
2036                        );
2037                    }
2038                }
2039            },
2040        }
2041    }
2042
2043    /// Get the character to display
2044    pub fn display_body_inventory<'a>(
2045        &'a self,
2046        characters: &'a [CharacterItem],
2047    ) -> Option<(comp::Body, &'a Inventory)> {
2048        match &self.mode {
2049            Mode::Select { .. } => self
2050                .selected
2051                .and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
2052                .map(|i| (i.body, &i.inventory)),
2053            Mode::CreateOrEdit {
2054                inventory, body, ..
2055            } => Some((comp::Body::Humanoid(*body), inventory)),
2056        }
2057    }
2058}
2059
2060pub struct CharSelectionUi {
2061    ui: Ui,
2062    controls: Controls,
2063    enter_pressed: bool,
2064    select_character: Option<CharacterId>,
2065    pub error: Option<String>,
2066}
2067
2068impl CharSelectionUi {
2069    pub fn new(global_state: &mut GlobalState, client: &Client) -> Self {
2070        // Load up the last selected character for this server
2071        let server_name = &client.server_info().name;
2072        let selected_character = global_state.profile.get_selected_character(server_name);
2073
2074        // Load language
2075        let i18n = global_state.i18n.read();
2076
2077        // TODO: don't add default font twice
2078        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2079
2080        let mut ui = Ui::new(
2081            &mut global_state.window,
2082            font,
2083            global_state.settings.interface.ui_scale,
2084        )
2085        .unwrap();
2086
2087        let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
2088
2089        #[cfg(feature = "singleplayer")]
2090        let default_name = match global_state.singleplayer.is_running() {
2091            true => String::new(),
2092            false => global_state.settings.networking.username.clone(),
2093        };
2094
2095        #[cfg(not(feature = "singleplayer"))]
2096        let default_name = global_state.settings.networking.username.clone();
2097
2098        let controls = Controls::new(
2099            fonts,
2100            Imgs::load(&mut ui).expect("Failed to load images"),
2101            selected_character,
2102            default_name,
2103            client.server_info(),
2104            ui.add_graphic(Graphic::Image(
2105                Arc::clone(client.world_data().topo_map_image()),
2106                Some(default_water_color()),
2107            )),
2108            client
2109                .possible_starting_sites()
2110                .iter()
2111                .filter_map(|site_id| client.sites().get(site_id))
2112                .map(|info| info.marker.clone())
2113                .collect(),
2114            client.world_data().chunk_size().as_(),
2115            client.server_description().rules.is_some(),
2116        );
2117
2118        Self {
2119            ui,
2120            controls,
2121            enter_pressed: false,
2122            select_character: None,
2123            error: None,
2124        }
2125    }
2126
2127    pub fn display_body_inventory<'a>(
2128        &'a self,
2129        characters: &'a [CharacterItem],
2130    ) -> Option<(comp::Body, &'a Inventory)> {
2131        self.controls.display_body_inventory(characters)
2132    }
2133
2134    pub fn handle_event(&mut self, event: window::Event) -> bool {
2135        match event {
2136            window::Event::IcedUi(event) => {
2137                // Enter Key pressed
2138                use iced::keyboard;
2139                if let iced::Event::Keyboard(keyboard::Event::KeyPressed {
2140                    key_code: keyboard::KeyCode::Enter,
2141                    ..
2142                }) = event
2143                {
2144                    self.enter_pressed = true;
2145                }
2146
2147                self.ui.handle_event(event);
2148                true
2149            },
2150            window::Event::MouseButton(_, window::PressState::Pressed) => {
2151                !self.controls.mouse_detector.mouse_over()
2152            },
2153            window::Event::ScaleFactorChanged(s) => {
2154                self.ui.scale_factor_changed(s);
2155                false
2156            },
2157            _ => false,
2158        }
2159    }
2160
2161    pub fn update_language(&mut self, i18n: LocalizationHandle) {
2162        let i18n = i18n.read();
2163        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
2164
2165        self.ui.clear_fonts(font);
2166        self.controls.fonts =
2167            Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
2168    }
2169
2170    pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) {
2171        self.ui.set_scaling_mode(scale_mode);
2172    }
2173
2174    pub fn select_character(&mut self, id: CharacterId) { self.select_character = Some(id); }
2175
2176    pub fn display_error(&mut self, error: String) { self.error = Some(error); }
2177
2178    // TODO: do we need whole client here or just character list?
2179    pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
2180        let mut events = Vec::new();
2181        let i18n = global_state.i18n.read();
2182
2183        let (mut messages, _) = self.ui.maintain(
2184            self.controls
2185                .view(&global_state.settings, client, &self.error, &i18n),
2186            global_state.window.renderer_mut(),
2187            None,
2188            &mut global_state.clipboard,
2189        );
2190
2191        if self.enter_pressed {
2192            self.enter_pressed = false;
2193            messages.push(match self.controls.mode {
2194                Mode::Select { .. } => Message::EnterWorld,
2195                Mode::CreateOrEdit { .. } => Message::CreateCharacter,
2196            });
2197        }
2198
2199        if let Some(id) = self.select_character.take() {
2200            messages.push(Message::Select(id))
2201        }
2202
2203        messages.into_iter().for_each(|message| {
2204            self.controls
2205                .update(message, &mut events, &client.character_list().characters)
2206        });
2207
2208        events
2209    }
2210
2211    pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); }
2212}
2213
2214#[derive(Default)]
2215struct Sliders {
2216    hair_style: slider::State,
2217    hair_color: slider::State,
2218    skin: slider::State,
2219    eyes: slider::State,
2220    eye_color: slider::State,
2221    accessory: slider::State,
2222    beard: slider::State,
2223    starting_site: slider::State,
2224}