1mod connecting;
2mod credits;
5mod login;
6mod servers;
7#[cfg(feature = "singleplayer")]
8mod world_selector;
9
10use crate::{
11 GlobalState,
12 credits::Credits,
13 render::UiDrawer,
14 ui::{
15 self, Graphic,
16 fonts::IcedFonts as Fonts,
17 ice::{Element, IcedUi as Ui, load_font, style, widget},
18 img_ids::ImageGraphic,
19 },
20 window,
21};
22use i18n::{LanguageMetadata, LocalizationHandle};
23use iced::{Column, Container, HorizontalAlignment, Length, Row, Space, text_input};
24use keyboard_keynames::key_layout::KeyLayout;
25use crate::settings::Settings;
27use common::assets::{self, AssetExt};
28use rand::{seq::SliceRandom, thread_rng};
29use std::time::Duration;
30use tracing::warn;
31
32use super::DetailedInitializationStage;
33
34pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
38pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
39
40pub const FILL_FRAC_ONE: f32 = 0.67;
41pub const FILL_FRAC_TWO: f32 = 0.53;
42
43image_ids_ice! {
44 struct Imgs {
45 <ImageGraphic>
46 v_logo: "voxygen.element.v_logo",
47 bg: "voxygen.background.bg_main",
48 banner_top: "voxygen.element.ui.generic.frames.banner_top",
49 banner_gradient_bottom: "voxygen.element.ui.generic.frames.banner_gradient_bottom",
50 button: "voxygen.element.ui.generic.buttons.button",
51 button_hover: "voxygen.element.ui.generic.buttons.button_hover",
52 button_press: "voxygen.element.ui.generic.buttons.button_press",
53 input_bg: "voxygen.element.ui.generic.textbox",
54 loading_art: "voxygen.element.ui.generic.frames.loading_screen.loading_bg",
55 loading_art_l: "voxygen.element.ui.generic.frames.loading_screen.loading_bg_l",
56 loading_art_r: "voxygen.element.ui.generic.frames.loading_screen.loading_bg_r",
57 selection: "voxygen.element.ui.generic.frames.selection",
58 selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
59 selection_press: "voxygen.element.ui.generic.frames.selection_press",
60
61 #[cfg(feature = "singleplayer")]
62 slider_range: "voxygen.element.ui.generic.slider.track",
63 #[cfg(feature = "singleplayer")]
64 slider_indicator: "voxygen.element.ui.generic.slider.indicator",
65
66 unlock: "voxygen.element.ui.generic.buttons.unlock",
67 unlock_hover: "voxygen.element.ui.generic.buttons.unlock_hover",
68 unlock_press: "voxygen.element.ui.generic.buttons.unlock_press",
69 }
70}
71
72const BG_IMGS: [&str; 41] = [
74 "voxygen.background.bg_1",
75 "voxygen.background.bg_2",
76 "voxygen.background.bg_3",
77 "voxygen.background.bg_4",
78 "voxygen.background.bg_5",
79 "voxygen.background.bg_6",
80 "voxygen.background.bg_7",
81 "voxygen.background.bg_8",
82 "voxygen.background.bg_9",
83 "voxygen.background.bg_10",
84 "voxygen.background.bg_11",
85 "voxygen.background.bg_12",
86 "voxygen.background.bg_13",
87 "voxygen.background.bg_14",
88 "voxygen.background.bg_15",
89 "voxygen.background.bg_16",
90 "voxygen.background.bg_17",
91 "voxygen.background.bg_18",
92 "voxygen.background.bg_19",
93 "voxygen.background.bg_20",
94 "voxygen.background.bg_21",
95 "voxygen.background.bg_22",
96 "voxygen.background.bg_23",
97 "voxygen.background.bg_24",
98 "voxygen.background.bg_25",
99 "voxygen.background.bg_26",
100 "voxygen.background.bg_27",
101 "voxygen.background.bg_28",
102 "voxygen.background.bg_29",
103 "voxygen.background.bg_30",
104 "voxygen.background.bg_31",
105 "voxygen.background.bg_32",
106 "voxygen.background.bg_33",
107 "voxygen.background.bg_34",
108 "voxygen.background.bg_35",
109 "voxygen.background.bg_36",
110 "voxygen.background.bg_37",
111 "voxygen.background.bg_38",
112 "voxygen.background.bg_39",
113 "voxygen.background.bg_40",
114 "voxygen.background.bg_41",
115];
116
117#[cfg(feature = "singleplayer")]
118#[derive(Clone)]
119pub enum WorldChange {
120 Name(String),
121 Seed(u32),
122 DayLength(f64),
123 SizeX(u32),
124 SizeY(u32),
125 Scale(f64),
126 MapKind(common::resources::MapKind),
127 ErosionQuality(f32),
128 DefaultGenOps,
129}
130
131#[cfg(feature = "singleplayer")]
132impl WorldChange {
133 pub fn apply(self, world: &mut crate::singleplayer::SingleplayerWorld) {
134 let mut def = Default::default();
135 let gen_opts = world.gen_opts.as_mut().unwrap_or(&mut def);
136 match self {
137 WorldChange::Name(name) => world.name = name,
138 WorldChange::Seed(seed) => world.seed = seed,
139 WorldChange::DayLength(d) => world.day_length = d,
140 WorldChange::SizeX(s) => gen_opts.x_lg = s,
141 WorldChange::SizeY(s) => gen_opts.y_lg = s,
142 WorldChange::Scale(scale) => gen_opts.scale = scale,
143 WorldChange::MapKind(kind) => gen_opts.map_kind = kind,
144 WorldChange::ErosionQuality(q) => gen_opts.erosion_quality = q,
145 WorldChange::DefaultGenOps => world.gen_opts = Some(Default::default()),
146 }
147 }
148}
149
150#[cfg(feature = "singleplayer")]
151#[derive(Clone)]
152pub enum WorldsChange {
153 SetActive(Option<usize>),
154 Delete(usize),
155 Regenerate(usize),
156 AddNew,
157 CurrentWorldChange(WorldChange),
158}
159
160pub enum Event {
161 LoginAttempt {
162 username: String,
163 password: String,
164 server_address: String,
165 },
166 CancelLoginAttempt,
167 ChangeLanguage(LanguageMetadata),
168 #[cfg(feature = "singleplayer")]
169 StartSingleplayer,
170 #[cfg(feature = "singleplayer")]
171 InitSingleplayer,
172 #[cfg(feature = "singleplayer")]
173 SinglePlayerChange(WorldsChange),
174 Quit,
175 AuthServerTrust(String, bool),
178 DeleteServer {
179 server_index: usize,
180 },
181}
182
183pub struct LoginInfo {
184 pub username: String,
185 pub password: String,
186 pub server: String,
187}
188
189enum ConnectionState {
190 InProgress,
191 AuthTrustPrompt { auth_server: String, msg: String },
192}
193
194enum Screen {
195 Credits {
200 screen: credits::Screen,
201 },
202 Login {
203 screen: Box<login::Screen>, error: Option<String>,
206 },
207 Servers {
208 screen: servers::Screen,
209 },
210 Connecting {
211 screen: connecting::Screen,
212 connection_state: ConnectionState,
213 init_stage: DetailedInitializationStage,
214 },
215 #[cfg(feature = "singleplayer")]
216 WorldSelector {
217 screen: world_selector::Screen,
218 },
219}
220
221#[derive(PartialEq, Eq)]
222enum Showing {
223 Login,
224 Languages,
225}
226
227impl Showing {
228 fn toggle(&mut self, other: Showing) {
229 if *self == other {
230 *self = Showing::Login;
231 } else {
232 *self = other;
233 }
234 }
235}
236
237pub struct Controls {
238 fonts: Fonts,
239 imgs: Imgs,
240 bg_img: widget::image::Handle,
241 i18n: LocalizationHandle,
242 version: String,
244 alpha: String,
246 credits: Credits,
247
248 server_field_locked: bool,
252 selected_server_index: Option<usize>,
253 login_info: LoginInfo,
254
255 show: Showing,
256 selected_language_index: Option<usize>,
257
258 time: f64,
259
260 screen: Screen,
261}
262
263#[derive(Clone)]
264enum Message {
265 Quit,
266 Back,
267 ShowServers,
268 ShowCredits,
269 #[cfg(feature = "singleplayer")]
270 Singleplayer,
271 #[cfg(feature = "singleplayer")]
272 SingleplayerPlay,
273 #[cfg(feature = "singleplayer")]
274 WorldChanged(WorldsChange),
275 #[cfg(feature = "singleplayer")]
276 WorldCancelConfirmation,
277 #[cfg(feature = "singleplayer")]
278 WorldConfirmation(world_selector::Confirmation),
279 Multiplayer,
280 UnlockServerField,
281 LanguageChanged(usize),
282 OpenLanguageMenu,
283 Username(String),
284 Password(String),
285 Server(String),
286 ServerChanged(usize),
287 FocusPassword,
288 CancelConnect,
289 TrustPromptAdd,
290 TrustPromptCancel,
291 CloseError,
292 DeleteServer,
293 }
296
297impl Controls {
298 fn new(
299 fonts: Fonts,
300 imgs: Imgs,
301 bg_img: widget::image::Handle,
302 i18n: LocalizationHandle,
303 settings: &Settings,
304 server: Option<String>,
305 ) -> Self {
306 let version = common::util::DISPLAY_VERSION_LONG.clone();
307 let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str());
308
309 let credits = Credits::load_expect_cloned("credits");
310
311 let screen = Screen::Login {
318 screen: Box::default(),
319 error: None,
320 };
321 let server_field_locked = server.is_some();
324 let login_info = LoginInfo {
325 username: settings.networking.username.clone(),
326 password: String::new(),
327 server: server.unwrap_or_else(|| settings.networking.default_server.clone()),
328 };
329 let selected_server_index = settings
330 .networking
331 .servers
332 .iter()
333 .position(|f| f == &login_info.server);
334
335 let language_metadatas = i18n::list_localizations();
336 let selected_language_index = language_metadatas
337 .iter()
338 .position(|f| f.language_identifier == settings.language.selected_language);
339
340 Self {
341 fonts,
342 imgs,
343 bg_img,
344 i18n,
345 version,
346 alpha,
347 credits,
348
349 server_field_locked,
350 selected_server_index,
351 login_info,
352
353 show: Showing::Login,
354 selected_language_index,
355
356 time: 0.0,
357
358 screen,
359 }
360 }
361
362 fn view(
363 &mut self,
364 settings: &Settings,
365 key_layout: &Option<KeyLayout>,
366 dt: f32,
367 #[cfg(feature = "singleplayer")] worlds: &crate::singleplayer::SingleplayerWorlds,
368 ) -> Element<Message> {
369 self.time += dt as f64;
370
371 let button_style = style::button::Style::new(self.imgs.button)
373 .hover_image(self.imgs.button_hover)
374 .press_image(self.imgs.button_press)
375 .text_color(TEXT_COLOR)
376 .disabled_text_color(DISABLED_TEXT_COLOR);
377
378 let alpha = iced::Text::new(&self.alpha)
379 .size(self.fonts.cyri.scale(12))
380 .width(Length::Fill)
381 .horizontal_alignment(HorizontalAlignment::Center);
382
383 let top_text = Row::with_children(vec![
384 Space::new(Length::Fill, Length::Shrink).into(),
385 alpha.into(),
386 if matches!(&self.screen, Screen::Login { .. }) {
387 Space::new(Length::Fill, Length::Shrink).into()
389 } else {
390 iced::Text::new(&self.version)
391 .size(self.fonts.cyri.scale(15))
392 .width(Length::Fill)
393 .horizontal_alignment(HorizontalAlignment::Right)
394 .into()
395 },
396 ])
397 .padding(3)
398 .width(Length::Fill);
399
400 let bg_img = if matches!(&self.screen, Screen::Connecting { .. }) {
401 self.bg_img
402 } else {
403 self.imgs.bg
404 };
405
406 let language_metadatas = i18n::list_localizations();
407
408 let content = match &mut self.screen {
411 Screen::Credits { screen } => {
414 screen.view(&self.fonts, &self.i18n.read(), &self.credits, button_style)
415 },
416 Screen::Login { screen, error } => screen.view(
417 &self.fonts,
418 &self.imgs,
419 self.server_field_locked,
420 &self.login_info,
421 error.as_deref(),
422 &self.i18n.read(),
423 &self.show,
424 self.selected_language_index,
425 &language_metadatas,
426 button_style,
427 &self.version,
428 ),
429 Screen::Servers { screen } => screen.view(
430 &self.fonts,
431 &self.imgs,
432 &settings.networking.servers,
433 self.selected_server_index,
434 &self.i18n.read(),
435 button_style,
436 ),
437 Screen::Connecting {
438 screen,
439 connection_state,
440 init_stage,
441 } => screen.view(
442 &self.fonts,
443 &self.imgs,
444 connection_state,
445 init_stage,
446 self.time,
447 &self.i18n.read(),
448 button_style,
449 settings.interface.loading_tips,
450 &settings.controls,
451 key_layout,
452 ),
453 #[cfg(feature = "singleplayer")]
454 Screen::WorldSelector { screen } => screen.view(
455 &self.fonts,
456 &self.imgs,
457 worlds,
458 &self.i18n.read(),
459 button_style,
460 ),
461 };
462
463 Container::new(
464 Column::with_children(vec![top_text.into(), content])
465 .spacing(3)
466 .width(Length::Fill)
467 .height(Length::Fill),
468 )
469 .style(style::container::Style::image(bg_img))
470 .into()
471 }
472
473 fn update(
474 &mut self,
475 message: Message,
476 events: &mut Vec<Event>,
477 settings: &Settings,
478 ui: &mut Ui,
479 ) {
480 let servers = &settings.networking.servers;
481 let mut language_metadatas = i18n::list_localizations();
482
483 match message {
484 Message::Quit => events.push(Event::Quit),
485 Message::Back => {
486 self.screen = Screen::Login {
487 screen: Box::default(),
488 error: None,
489 };
490 },
491 Message::ShowServers => {
492 if matches!(&self.screen, Screen::Login { .. }) {
493 self.selected_server_index =
494 servers.iter().position(|f| f == &self.login_info.server);
495 self.screen = Screen::Servers {
496 screen: servers::Screen::new(),
497 };
498 }
499 },
500 Message::ShowCredits => {
501 self.screen = Screen::Credits {
502 screen: credits::Screen::new(),
503 };
504 },
505 #[cfg(feature = "singleplayer")]
506 Message::Singleplayer => {
507 self.screen = Screen::WorldSelector {
508 screen: world_selector::Screen::default(),
509 };
510 events.push(Event::InitSingleplayer);
511 },
512 #[cfg(feature = "singleplayer")]
513 Message::SingleplayerPlay => {
514 self.screen = Screen::Connecting {
515 screen: connecting::Screen::new(ui),
516 connection_state: ConnectionState::InProgress,
517 init_stage: DetailedInitializationStage::Singleplayer,
518 };
519 events.push(Event::StartSingleplayer);
520 },
521 #[cfg(feature = "singleplayer")]
522 Message::WorldChanged(change) => {
523 match change {
524 WorldsChange::Delete(_) | WorldsChange::Regenerate(_) => {
525 if let Screen::WorldSelector {
526 screen: world_selector::Screen { confirmation, .. },
527 } = &mut self.screen
528 {
529 *confirmation = None;
530 }
531 },
532 _ => {},
533 }
534 events.push(Event::SinglePlayerChange(change))
535 },
536 #[cfg(feature = "singleplayer")]
537 Message::WorldCancelConfirmation => {
538 if let Screen::WorldSelector {
539 screen: world_selector::Screen { confirmation, .. },
540 } = &mut self.screen
541 {
542 *confirmation = None;
543 }
544 },
545 #[cfg(feature = "singleplayer")]
546 Message::WorldConfirmation(new_confirmation) => {
547 if let Screen::WorldSelector {
548 screen: world_selector::Screen { confirmation, .. },
549 } = &mut self.screen
550 {
551 *confirmation = Some(new_confirmation);
552 }
553 },
554 Message::Multiplayer => {
555 self.screen = Screen::Connecting {
556 screen: connecting::Screen::new(ui),
557 connection_state: ConnectionState::InProgress,
558 init_stage: DetailedInitializationStage::StartingMultiplayer,
559 };
560
561 events.push(Event::LoginAttempt {
562 username: self.login_info.username.trim().to_string(),
563 password: self.login_info.password.clone(),
564 server_address: self.login_info.server.trim().to_string(),
565 });
566 },
567 Message::UnlockServerField => self.server_field_locked = false,
568 Message::Username(new_value) => self.login_info.username = new_value,
569 Message::LanguageChanged(new_value) => {
570 events.push(Event::ChangeLanguage(language_metadatas.remove(new_value)));
571 },
572 Message::OpenLanguageMenu => self.show.toggle(Showing::Languages),
573 Message::Password(new_value) => self.login_info.password = new_value,
574 Message::Server(new_value) => {
575 self.login_info.server = new_value;
576 },
577 Message::ServerChanged(new_value) => {
578 self.selected_server_index = Some(new_value);
579 self.login_info.server.clone_from(&servers[new_value]);
580 },
581 Message::FocusPassword => {
582 if let Screen::Login { screen, .. } = &mut self.screen {
583 screen.banner.password = text_input::State::focused();
584 screen.banner.username = text_input::State::new();
585 }
586 },
587 Message::CancelConnect => {
588 self.exit_connect_screen();
589 events.push(Event::CancelLoginAttempt);
590 },
591 msg @ Message::TrustPromptAdd | msg @ Message::TrustPromptCancel => {
592 if let Screen::Connecting {
593 connection_state, ..
594 } = &mut self.screen
595 {
596 if let ConnectionState::AuthTrustPrompt { auth_server, .. } = connection_state {
597 let auth_server = std::mem::take(auth_server);
598 let added = matches!(msg, Message::TrustPromptAdd);
599
600 *connection_state = ConnectionState::InProgress;
601 events.push(Event::AuthServerTrust(auth_server, added));
602 }
603 }
604 },
605 Message::CloseError => {
606 if let Screen::Login { error, .. } = &mut self.screen {
607 *error = None;
608 }
609 },
610 Message::DeleteServer => {
611 if let Some(server_index) = self.selected_server_index {
612 events.push(Event::DeleteServer { server_index });
613 self.selected_server_index = None;
614 }
615 },
616 }
627 }
628
629 fn exit_connect_screen(&mut self) {
631 if matches!(&self.screen, Screen::Connecting { .. }) {
632 self.screen = Screen::Login {
633 screen: Box::default(),
634 error: None,
635 }
636 }
637 }
638
639 fn auth_trust_prompt(&mut self, auth_server: String) {
640 if let Screen::Connecting {
641 connection_state, ..
642 } = &mut self.screen
643 {
644 let msg = format!(
645 "Warning: The server you are trying to connect to has provided this \
646 authentication server address:\n\n{}\n\nbut it is not in your list of trusted \
647 authentication servers.\n\nMake sure that you trust this site and owner to not \
648 try and bruteforce your password!",
649 &auth_server
650 );
651
652 *connection_state = ConnectionState::AuthTrustPrompt { auth_server, msg };
653 }
654 }
655
656 fn connection_error(&mut self, error: String) {
657 if matches!(&self.screen, Screen::Connecting { .. })
658 || matches!(&self.screen, Screen::Login { .. })
659 {
660 self.screen = Screen::Login {
661 screen: Box::default(),
662 error: Some(error),
663 }
664 } else {
665 warn!("connection_error invoked on unhandled screen!");
666 }
667 }
668
669 fn update_init_stage(&mut self, stage: DetailedInitializationStage) {
670 if let Screen::Connecting { init_stage, .. } = &mut self.screen {
671 *init_stage = stage
672 }
673 }
674
675 fn tab(&mut self) {
676 if let Screen::Login { screen, .. } = &mut self.screen {
677 if screen.banner.username.is_focused() {
679 screen.banner.username = text_input::State::new();
680 screen.banner.password = text_input::State::focused();
681 screen.banner.password.move_cursor_to_end();
682 } else if screen.banner.password.is_focused() {
683 screen.banner.password = text_input::State::new();
684 if self.server_field_locked {
686 screen.banner.username = text_input::State::focused();
687 } else {
688 screen.banner.server = text_input::State::focused();
689 }
690 screen.banner.server.move_cursor_to_end();
691 } else if screen.banner.server.is_focused() {
692 screen.banner.server = text_input::State::new();
693 screen.banner.username = text_input::State::focused();
694 screen.banner.username.move_cursor_to_end();
695 } else {
696 screen.banner.username = text_input::State::focused();
697 screen.banner.username.move_cursor_to_end();
698 }
699 }
700 }
701}
702
703pub struct MainMenuUi {
704 ui: Ui,
705 controls: Controls,
708 bg_img_spec: &'static str,
709}
710
711impl MainMenuUi {
712 pub fn new(global_state: &mut GlobalState) -> Self {
713 let i18n = &global_state.i18n.read();
715 let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
717
718 let mut ui = Ui::new(
719 &mut global_state.window,
720 font,
721 global_state.settings.interface.ui_scale,
722 )
723 .unwrap();
724
725 let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
726
727 let bg_img_spec = rand_bg_image_spec();
728
729 let bg_img = assets::Image::load_expect(bg_img_spec).read().to_image();
730 let controls = Controls::new(
731 fonts,
732 Imgs::load(&mut ui).expect("Failed to load images"),
733 ui.add_graphic(Graphic::Image(bg_img, None)),
734 global_state.i18n,
735 &global_state.settings,
736 global_state.args.server.clone(),
737 );
738
739 Self {
740 ui,
741 controls,
742 bg_img_spec,
743 }
744 }
745
746 pub fn bg_img_spec(&self) -> &'static str { self.bg_img_spec }
747
748 pub fn update_language(&mut self, i18n: LocalizationHandle, settings: &Settings) {
749 self.controls.i18n = i18n;
750 let i18n = &i18n.read();
751 let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
752 self.ui.clear_fonts(font);
753 self.controls.fonts =
754 Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
755 let language_metadatas = i18n::list_localizations();
756 self.controls.selected_language_index = language_metadatas
757 .iter()
758 .position(|f| f.language_identifier == settings.language.selected_language);
759 }
760
761 pub fn auth_trust_prompt(&mut self, auth_server: String) {
762 self.controls.auth_trust_prompt(auth_server);
763 }
764
765 pub fn show_info(&mut self, msg: String) { self.controls.connection_error(msg); }
766
767 pub fn update_stage(&mut self, stage: DetailedInitializationStage) {
768 tracing::trace!(?stage, "Updating stage");
769 self.controls.update_init_stage(stage);
770 }
771
772 pub fn connected(&mut self) { self.controls.exit_connect_screen(); }
773
774 pub fn cancel_connection(&mut self) { self.controls.exit_connect_screen(); }
775
776 pub fn handle_event(&mut self, event: window::Event) -> bool {
777 match event {
778 window::Event::IcedUi(event) => {
780 self.handle_ui_event(event);
781 true
782 },
783 window::Event::ScaleFactorChanged(s) => {
784 self.ui.scale_factor_changed(s);
785 false
786 },
787 _ => false,
788 }
789 }
790
791 pub fn handle_ui_event(&mut self, event: ui::ice::Event) {
792 use iced::keyboard;
794 if matches!(
795 &event,
796 iced::Event::Keyboard(keyboard::Event::KeyPressed {
797 key_code: keyboard::KeyCode::Tab,
798 ..
799 })
800 ) {
801 self.controls.tab();
802 }
803
804 self.ui.handle_event(event);
805 }
806
807 pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) {
808 self.ui.set_scaling_mode(scale_mode);
809 }
810
811 pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec<Event> {
812 let mut events = Vec::new();
813
814 #[cfg(feature = "singleplayer")]
815 let worlds_default = crate::singleplayer::SingleplayerWorlds::default();
816 #[cfg(feature = "singleplayer")]
817 let worlds = global_state
818 .singleplayer
819 .as_init()
820 .unwrap_or(&worlds_default);
821
822 let (messages, _) = self.ui.maintain(
823 self.controls.view(
824 &global_state.settings,
825 &global_state.window.key_layout,
826 dt.as_secs_f32(),
827 #[cfg(feature = "singleplayer")]
828 worlds,
829 ),
830 global_state.window.renderer_mut(),
831 None,
832 &mut global_state.clipboard,
833 );
834
835 messages.into_iter().for_each(|message| {
836 self.controls
837 .update(message, &mut events, &global_state.settings, &mut self.ui)
838 });
839
840 events
841 }
842
843 pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); }
844}
845
846pub fn rand_bg_image_spec() -> &'static str { BG_IMGS.choose(&mut thread_rng()).unwrap() }