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