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