veloren_voxygen/menu/main/ui/
world_selector.rs

1use common::resources::MapKind;
2use i18n::Localization;
3use iced::{
4    Align, Button, Column, Container, Length, Row, Scrollable, Slider, Space, Text, TextInput,
5    button, scrollable, slider, text_input,
6};
7use rand::Rng;
8use vek::Rgba;
9
10use crate::{
11    menu::main::ui::{FILL_FRAC_TWO, WorldsChange},
12    ui::{
13        fonts::IcedFonts,
14        ice::{
15            Element,
16            component::neat_button,
17            style,
18            widget::{
19                BackgroundContainer, Image, Overlay, Padding,
20                compound_graphic::{CompoundGraphic, Graphic},
21            },
22        },
23    },
24};
25
26use super::{Imgs, Message};
27
28const INPUT_TEXT_SIZE: u16 = 20;
29
30#[derive(Clone)]
31pub enum Confirmation {
32    Regenerate(usize),
33    Delete(usize),
34}
35
36#[derive(Default)]
37pub struct Screen {
38    back_button: button::State,
39    play_button: button::State,
40    new_button: button::State,
41    yes_button: button::State,
42    no_button: button::State,
43
44    worlds_buttons: Vec<button::State>,
45
46    selection_list: scrollable::State,
47
48    world_name: text_input::State,
49    map_seed: text_input::State,
50    day_length: slider::State,
51    random_seed_button: button::State,
52    world_size_x: slider::State,
53    world_size_y: slider::State,
54
55    map_vertical_scale: slider::State,
56    shape_buttons: enum_map::EnumMap<MapKind, button::State>,
57    map_erosion_quality: slider::State,
58
59    delete_world: button::State,
60    regenerate_map: button::State,
61    generate_map: button::State,
62
63    pub confirmation: Option<Confirmation>,
64}
65
66impl Screen {
67    pub(super) fn view(
68        &mut self,
69        fonts: &IcedFonts,
70        imgs: &Imgs,
71        worlds: &crate::singleplayer::SingleplayerWorlds,
72        i18n: &Localization,
73        button_style: style::button::Style,
74    ) -> Element<Message> {
75        let input_text_size = fonts.cyri.scale(INPUT_TEXT_SIZE);
76
77        let worlds_count = worlds.worlds.len();
78        if self.worlds_buttons.len() != worlds_count {
79            self.worlds_buttons = vec![Default::default(); worlds_count];
80        }
81
82        let title = Text::new(i18n.get_msg("gameinput-map"))
83            .size(fonts.cyri.scale(35))
84            .horizontal_alignment(iced::HorizontalAlignment::Center);
85
86        let mut list = Scrollable::new(&mut self.selection_list)
87            .spacing(8)
88            .height(Length::Fill)
89            .align_items(Align::Start);
90
91        let list_items = self
92            .worlds_buttons
93            .iter_mut()
94            .zip(
95                worlds
96                    .worlds
97                    .iter()
98                    .enumerate()
99                    .map(|(i, w)| (Some(i), &w.name)),
100            )
101            .map(|(state, (i, map))| {
102                let color = if i == worlds.current {
103                    (97, 255, 18)
104                } else {
105                    (97, 97, 25)
106                };
107                let button = Button::new(
108                    state,
109                    Row::with_children(vec![
110                        Space::new(Length::FillPortion(5), Length::Units(0)).into(),
111                        Text::new(map)
112                            .width(Length::FillPortion(95))
113                            .size(fonts.cyri.scale(25))
114                            .vertical_alignment(iced::VerticalAlignment::Center)
115                            .into(),
116                    ]),
117                )
118                .style(
119                    style::button::Style::new(imgs.selection)
120                        .hover_image(imgs.selection_hover)
121                        .press_image(imgs.selection_press)
122                        .image_color(Rgba::new(color.0, color.1, color.2, 192)),
123                )
124                .min_height(56)
125                .on_press(Message::WorldChanged(super::WorldsChange::SetActive(i)));
126                Row::with_children(vec![
127                    Space::new(Length::FillPortion(3), Length::Units(0)).into(),
128                    button.width(Length::FillPortion(92)).into(),
129                    Space::new(Length::FillPortion(5), Length::Units(0)).into(),
130                ])
131            });
132
133        for item in list_items {
134            list = list.push(item);
135        }
136
137        let new_button = Container::new(neat_button(
138            &mut self.new_button,
139            i18n.get_msg("main-singleplayer-new"),
140            FILL_FRAC_TWO,
141            button_style,
142            Some(Message::WorldChanged(super::WorldsChange::AddNew)),
143        ))
144        .center_x()
145        .max_width(200);
146
147        let back_button = Container::new(neat_button(
148            &mut self.back_button,
149            i18n.get_msg("common-back"),
150            FILL_FRAC_TWO,
151            button_style,
152            Some(Message::Back),
153        ))
154        .center_x()
155        .max_width(200);
156
157        let content = Column::with_children(vec![
158            title.into(),
159            list.into(),
160            new_button.into(),
161            back_button.into(),
162        ])
163        .spacing(8)
164        .width(Length::Fill)
165        .height(Length::FillPortion(38))
166        .align_items(Align::Center)
167        .padding(iced::Padding {
168            bottom: 25,
169            ..iced::Padding::new(0)
170        });
171
172        let selection_menu = BackgroundContainer::new(
173            CompoundGraphic::from_graphics(vec![
174                Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
175                Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
176                // TODO: use non image gradient
177                Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
178            ])
179            .fix_aspect_ratio()
180            .height(Length::Fill)
181            .width(Length::Fill),
182            content,
183        )
184        .padding(Padding::new().horizontal(5).top(15));
185        let mut items = vec![selection_menu.into()];
186
187        if let Some(i) = worlds.current {
188            let world = &worlds.worlds[i];
189            let can_edit = !world.is_generated;
190            let message = |m| Message::WorldChanged(super::WorldsChange::CurrentWorldChange(m));
191
192            use super::WorldChange;
193
194            const SLIDER_TEXT_SIZE: u16 = 20;
195            const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
196            const SLIDER_BAR_HEIGHT: u16 = 9;
197            const SLIDER_BAR_PAD: u16 = 0;
198            // Height of interactable area
199            const SLIDER_HEIGHT: u16 = 30;
200            // Day length slider values
201            pub const DAY_LENGTH_MIN: f64 = 10.0;
202            pub const DAY_LENGTH_MAX: f64 = 60.0;
203
204            let mut gen_content = vec![
205                BackgroundContainer::new(
206                    Image::new(imgs.input_bg)
207                        .width(Length::Units(230))
208                        .fix_aspect_ratio(),
209                    if can_edit {
210                        Element::from(
211                            TextInput::new(
212                                &mut self.world_name,
213                                &i18n.get_msg("main-singleplayer-world_name"),
214                                &world.name,
215                                move |s| message(WorldChange::Name(s)),
216                            )
217                            .size(input_text_size),
218                        )
219                    } else {
220                        Text::new(&world.name)
221                            .size(input_text_size)
222                            .width(Length::Fill)
223                            .height(Length::Shrink)
224                            .into()
225                    },
226                )
227                .padding(Padding::new().horizontal(7).top(5))
228                .into(),
229            ];
230
231            let seed = world.seed;
232            let seed_str = i18n.get_msg("main-singleplayer-seed");
233            let mut seed_content = vec![
234                Column::with_children(vec![
235                    Text::new(seed_str.to_string())
236                        .size(SLIDER_TEXT_SIZE)
237                        .horizontal_alignment(iced::HorizontalAlignment::Center)
238                        .into(),
239                ])
240                .padding(iced::Padding::new(5))
241                .into(),
242                BackgroundContainer::new(
243                    Image::new(imgs.input_bg)
244                        .width(Length::Units(190))
245                        .fix_aspect_ratio(),
246                    if can_edit {
247                        Element::from(
248                            TextInput::new(
249                                &mut self.map_seed,
250                                &seed_str,
251                                &seed.to_string(),
252                                move |s| {
253                                    if let Ok(seed) = if s.is_empty() {
254                                        Ok(0)
255                                    } else {
256                                        s.parse::<u32>()
257                                    } {
258                                        message(WorldChange::Seed(seed))
259                                    } else {
260                                        message(WorldChange::Seed(seed))
261                                    }
262                                },
263                            )
264                            .size(input_text_size),
265                        )
266                    } else {
267                        Text::new(world.seed.to_string())
268                            .size(input_text_size)
269                            .width(Length::Fill)
270                            .height(Length::Shrink)
271                            .into()
272                    },
273                )
274                .padding(Padding::new().horizontal(7).top(5))
275                .into(),
276            ];
277
278            if can_edit {
279                seed_content.push(
280                    Container::new(neat_button(
281                        &mut self.random_seed_button,
282                        i18n.get_msg("main-singleplayer-random_seed"),
283                        FILL_FRAC_TWO,
284                        button_style,
285                        Some(message(WorldChange::Seed(rand::thread_rng().gen()))),
286                    ))
287                    .max_width(200)
288                    .into(),
289                )
290            }
291
292            gen_content.push(Row::with_children(seed_content).into());
293
294            if let Some(gen_opts) = world.gen_opts.as_ref() {
295                // Day length setting label
296                gen_content.push(
297                    Text::new(format!(
298                        "{}: {}",
299                        i18n.get_msg("main-singleplayer-day_length"),
300                        world.day_length
301                    ))
302                    .size(SLIDER_TEXT_SIZE)
303                    .horizontal_alignment(iced::HorizontalAlignment::Center)
304                    .into(),
305                );
306
307                // Day length setting slider
308                if can_edit {
309                    gen_content.push(
310                        Row::with_children(vec![
311                            Slider::new(
312                                &mut self.day_length,
313                                DAY_LENGTH_MIN..=DAY_LENGTH_MAX,
314                                world.day_length,
315                                move |d| message(WorldChange::DayLength(d)),
316                            )
317                            .height(SLIDER_HEIGHT)
318                            .style(style::slider::Style::images(
319                                imgs.slider_indicator,
320                                imgs.slider_range,
321                                SLIDER_BAR_PAD,
322                                SLIDER_CURSOR_SIZE,
323                                SLIDER_BAR_HEIGHT,
324                            ))
325                            .into(),
326                        ])
327                        .into(),
328                    )
329                }
330
331                gen_content.push(
332                    Text::new(format!(
333                        "{}: x: {}, y: {}",
334                        i18n.get_msg("main-singleplayer-size_lg"),
335                        gen_opts.x_lg,
336                        gen_opts.y_lg
337                    ))
338                    .size(SLIDER_TEXT_SIZE)
339                    .horizontal_alignment(iced::HorizontalAlignment::Center)
340                    .into(),
341                );
342
343                if can_edit {
344                    gen_content.push(
345                        Row::with_children(vec![
346                            Slider::new(&mut self.world_size_x, 4..=13, gen_opts.x_lg, move |s| {
347                                message(WorldChange::SizeX(s))
348                            })
349                            .height(SLIDER_HEIGHT)
350                            .style(style::slider::Style::images(
351                                imgs.slider_indicator,
352                                imgs.slider_range,
353                                SLIDER_BAR_PAD,
354                                SLIDER_CURSOR_SIZE,
355                                SLIDER_BAR_HEIGHT,
356                            ))
357                            .into(),
358                            Slider::new(&mut self.world_size_y, 4..=13, gen_opts.y_lg, move |s| {
359                                message(WorldChange::SizeY(s))
360                            })
361                            .height(SLIDER_HEIGHT)
362                            .style(style::slider::Style::images(
363                                imgs.slider_indicator,
364                                imgs.slider_range,
365                                SLIDER_BAR_PAD,
366                                SLIDER_CURSOR_SIZE,
367                                SLIDER_BAR_HEIGHT,
368                            ))
369                            .into(),
370                        ])
371                        .into(),
372                    );
373                    let height = Length::Units(56);
374                    if gen_opts.x_lg + gen_opts.y_lg >= 19 {
375                        gen_content.push(
376                            Text::new(i18n.get_msg("main-singleplayer-map_large_warning"))
377                                .size(SLIDER_TEXT_SIZE)
378                                .height(height)
379                                .color([0.914, 0.835, 0.008])
380                                .horizontal_alignment(iced::HorizontalAlignment::Center)
381                                .into(),
382                        );
383                    } else {
384                        gen_content.push(Space::new(Length::Units(0), height).into());
385                    }
386                }
387
388                gen_content.push(
389                    Text::new(format!(
390                        "{}: {}",
391                        i18n.get_msg("main-singleplayer-map_scale"),
392                        gen_opts.scale
393                    ))
394                    .size(SLIDER_TEXT_SIZE)
395                    .horizontal_alignment(iced::HorizontalAlignment::Center)
396                    .into(),
397                );
398
399                if can_edit {
400                    gen_content.push(
401                        Slider::new(
402                            &mut self.map_vertical_scale,
403                            0.1..=160.0,
404                            gen_opts.scale * 10.0,
405                            move |s| message(WorldChange::Scale(s / 10.0)),
406                        )
407                        .height(SLIDER_HEIGHT)
408                        .style(style::slider::Style::images(
409                            imgs.slider_indicator,
410                            imgs.slider_range,
411                            SLIDER_BAR_PAD,
412                            SLIDER_CURSOR_SIZE,
413                            SLIDER_BAR_HEIGHT,
414                        ))
415                        .into(),
416                    );
417                }
418
419                if can_edit {
420                    gen_content.extend([
421                        Text::new(i18n.get_msg("main-singleplayer-map_shape"))
422                            .size(SLIDER_TEXT_SIZE)
423                            .horizontal_alignment(iced::HorizontalAlignment::Center)
424                            .into(),
425                        Row::with_children(
426                            self.shape_buttons
427                                .iter_mut()
428                                .map(|(shape, state)| {
429                                    let color = if gen_opts.map_kind == shape {
430                                        (97, 255, 18)
431                                    } else {
432                                        (97, 97, 25)
433                                    };
434                                    Button::new(
435                                        state,
436                                        Row::with_children(vec![
437                                            Space::new(Length::FillPortion(5), Length::Units(0))
438                                                .into(),
439                                            Text::new(i18n.get_msg(Self::map_kind_key(shape)))
440                                                .width(Length::FillPortion(95))
441                                                .size(fonts.cyri.scale(14))
442                                                .vertical_alignment(iced::VerticalAlignment::Center)
443                                                .into(),
444                                        ])
445                                        .align_items(Align::Center),
446                                    )
447                                    .style(
448                                        style::button::Style::new(imgs.selection)
449                                            .hover_image(imgs.selection_hover)
450                                            .press_image(imgs.selection_press)
451                                            .image_color(Rgba::new(color.0, color.1, color.2, 192)),
452                                    )
453                                    .width(Length::FillPortion(1))
454                                    .min_height(18)
455                                    .on_press(Message::WorldChanged(
456                                        super::WorldsChange::CurrentWorldChange(
457                                            WorldChange::MapKind(shape),
458                                        ),
459                                    ))
460                                    .into()
461                                })
462                                .collect(),
463                        )
464                        .into(),
465                    ]);
466                } else {
467                    gen_content.push(
468                        Text::new(format!(
469                            "{}: {}",
470                            i18n.get_msg("main-singleplayer-map_shape"),
471                            gen_opts.map_kind,
472                        ))
473                        .size(SLIDER_TEXT_SIZE)
474                        .horizontal_alignment(iced::HorizontalAlignment::Center)
475                        .into(),
476                    );
477                }
478
479                gen_content.push(
480                    Text::new(format!(
481                        "{}: {}",
482                        i18n.get_msg("main-singleplayer-map_erosion_quality"),
483                        gen_opts.erosion_quality
484                    ))
485                    .size(SLIDER_TEXT_SIZE)
486                    .horizontal_alignment(iced::HorizontalAlignment::Center)
487                    .into(),
488                );
489
490                if can_edit {
491                    gen_content.push(
492                        Slider::new(
493                            &mut self.map_erosion_quality,
494                            0.0..=20.0,
495                            gen_opts.erosion_quality * 10.0,
496                            move |s| message(WorldChange::ErosionQuality(s / 10.0)),
497                        )
498                        .height(SLIDER_HEIGHT)
499                        .style(style::slider::Style::images(
500                            imgs.slider_indicator,
501                            imgs.slider_range,
502                            SLIDER_BAR_PAD,
503                            SLIDER_CURSOR_SIZE,
504                            SLIDER_BAR_HEIGHT,
505                        ))
506                        .into(),
507                    );
508                }
509            }
510
511            let mut world_buttons = vec![];
512
513            if world.gen_opts.is_none() && can_edit {
514                let create_custom = Container::new(neat_button(
515                    &mut self.regenerate_map,
516                    i18n.get_msg("main-singleplayer-create_custom"),
517                    FILL_FRAC_TWO,
518                    button_style,
519                    Some(Message::WorldChanged(
520                        super::WorldsChange::CurrentWorldChange(WorldChange::DefaultGenOps),
521                    )),
522                ))
523                .center_x()
524                .width(Length::FillPortion(1))
525                .max_width(200);
526                world_buttons.push(create_custom.into());
527            }
528
529            if world.is_generated {
530                let regenerate = Container::new(neat_button(
531                    &mut self.generate_map,
532                    i18n.get_msg("main-singleplayer-regenerate"),
533                    FILL_FRAC_TWO,
534                    button_style,
535                    Some(Message::WorldConfirmation(Confirmation::Regenerate(i))),
536                ))
537                .center_x()
538                .width(Length::FillPortion(1))
539                .max_width(200);
540                world_buttons.push(regenerate.into())
541            }
542            let delete = Container::new(neat_button(
543                &mut self.delete_world,
544                i18n.get_msg("main-singleplayer-delete"),
545                FILL_FRAC_TWO,
546                button_style,
547                Some(Message::WorldConfirmation(Confirmation::Delete(i))),
548            ))
549            .center_x()
550            .width(Length::FillPortion(1))
551            .max_width(200);
552
553            world_buttons.push(delete.into());
554
555            gen_content.push(Row::with_children(world_buttons).into());
556
557            let play_button = Container::new(neat_button(
558                &mut self.play_button,
559                i18n.get_msg(if world.is_generated || world.gen_opts.is_none() {
560                    "main-singleplayer-play"
561                } else {
562                    "main-singleplayer-generate_and_play"
563                }),
564                FILL_FRAC_TWO,
565                button_style,
566                Some(Message::SingleplayerPlay),
567            ))
568            .center_x()
569            .max_width(200);
570
571            gen_content.push(play_button.into());
572
573            let gen_opts = Column::with_children(gen_content).align_items(Align::Center);
574
575            let opts_menu = BackgroundContainer::new(
576                CompoundGraphic::from_graphics(vec![
577                    Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
578                    Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
579                    // TODO: use non image gradient
580                    Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
581                ])
582                .fix_aspect_ratio()
583                .height(Length::Fill)
584                .width(Length::Fill),
585                gen_opts,
586            )
587            .padding(Padding::new().horizontal(5).top(15));
588
589            items.push(opts_menu.into());
590        }
591
592        let all = Row::with_children(items)
593            .height(Length::Fill)
594            .width(Length::Fill);
595
596        if let Some(confirmation) = self.confirmation.as_ref() {
597            const FILL_FRAC_ONE: f32 = 0.77;
598
599            let (text, yes_msg, index) = match confirmation {
600                Confirmation::Regenerate(i) => (
601                    "menu-singleplayer-confirm_regenerate",
602                    Message::WorldChanged(WorldsChange::Regenerate(*i)),
603                    i,
604                ),
605                Confirmation::Delete(i) => (
606                    "menu-singleplayer-confirm_delete",
607                    Message::WorldChanged(WorldsChange::Delete(*i)),
608                    i,
609                ),
610            };
611
612            if let Some(name) = worlds.worlds.get(*index).map(|world| &world.name) {
613                let over_content = Column::with_children(vec![
614                    Text::new(i18n.get_msg_ctx(text, &i18n::fluent_args! { "world_name" => name }))
615                        .size(fonts.cyri.scale(24))
616                        .into(),
617                    Row::with_children(vec![
618                        neat_button(
619                            &mut self.no_button,
620                            i18n.get_msg("common-no").into_owned(),
621                            FILL_FRAC_ONE,
622                            button_style,
623                            Some(Message::WorldCancelConfirmation),
624                        ),
625                        neat_button(
626                            &mut self.yes_button,
627                            i18n.get_msg("common-yes").into_owned(),
628                            FILL_FRAC_ONE,
629                            button_style,
630                            Some(yes_msg),
631                        ),
632                    ])
633                    .height(Length::Units(28))
634                    .spacing(30)
635                    .into(),
636                ])
637                .align_items(Align::Center)
638                .spacing(10);
639
640                let over = Container::new(over_content)
641                    .style(
642                        style::container::Style::color_with_double_cornerless_border(
643                            (0, 0, 0, 200).into(),
644                            (3, 4, 4, 255).into(),
645                            (28, 28, 22, 255).into(),
646                        ),
647                    )
648                    .width(Length::Shrink)
649                    .height(Length::Shrink)
650                    .max_width(400)
651                    .max_height(500)
652                    .padding(24)
653                    .center_x()
654                    .center_y();
655
656                Overlay::new(over, all)
657                    .width(Length::Fill)
658                    .height(Length::Fill)
659                    .center_x()
660                    .center_y()
661                    .into()
662            } else {
663                self.confirmation = None;
664                all.into()
665            }
666        } else {
667            all.into()
668        }
669    }
670
671    fn map_kind_key(map_kind: MapKind) -> &'static str {
672        match map_kind {
673            MapKind::Circle => "main-singleplayer-map_shape-circle",
674            MapKind::Square => "main-singleplayer-map_shape-square",
675        }
676    }
677}