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