Skip to main content

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