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