mod connecting; // Note: Keeping in case we re-add the disclaimer //mod disclaimer; mod credits; mod login; mod servers; use crate::{ credits::Credits, render::UiDrawer, ui::{ self, fonts::IcedFonts as Fonts, ice::{load_font, style, widget, Element, IcedUi as Ui}, img_ids::ImageGraphic, Graphic, }, window, GlobalState, }; use i18n::{LanguageMetadata, LocalizationHandle}; use iced::{text_input, Column, Container, HorizontalAlignment, Length, Row, Space}; //ImageFrame, Tooltip, use crate::settings::Settings; use common::assets::{self, AssetExt}; use rand::{seq::SliceRandom, thread_rng}; use std::time::Duration; use tracing::warn; // TODO: what is this? (showed up in rebase) //const COL1: Color = Color::Rgba(0.07, 0.1, 0.1, 0.9); pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0); pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2); pub const FILL_FRAC_ONE: f32 = 0.77; pub const FILL_FRAC_TWO: f32 = 0.53; image_ids_ice! { struct Imgs { v_logo: "voxygen.element.v_logo", bg: "voxygen.background.bg_main", banner_top: "voxygen.element.ui.generic.frames.banner_top", banner_gradient_bottom: "voxygen.element.ui.generic.frames.banner_gradient_bottom", button: "voxygen.element.ui.generic.buttons.button", button_hover: "voxygen.element.ui.generic.buttons.button_hover", button_press: "voxygen.element.ui.generic.buttons.button_press", input_bg: "voxygen.element.ui.generic.textbox", loading_art: "voxygen.element.ui.generic.frames.loading_screen.loading_bg", loading_art_l: "voxygen.element.ui.generic.frames.loading_screen.loading_bg_l", loading_art_r: "voxygen.element.ui.generic.frames.loading_screen.loading_bg_r", selection: "voxygen.element.ui.generic.frames.selection", selection_hover: "voxygen.element.ui.generic.frames.selection_hover", selection_press: "voxygen.element.ui.generic.frames.selection_press", } } // Randomly loaded background images const BG_IMGS: [&str; 13] = [ "voxygen.background.bg_1", "voxygen.background.bg_2", "voxygen.background.bg_3", "voxygen.background.bg_4", "voxygen.background.bg_5", "voxygen.background.bg_6", "voxygen.background.bg_7", "voxygen.background.bg_8", "voxygen.background.bg_9", "voxygen.background.bg_10", "voxygen.background.bg_11", "voxygen.background.bg_12", "voxygen.background.bg_13", ]; pub enum Event { LoginAttempt { username: String, password: String, server_address: String, }, CancelLoginAttempt, ChangeLanguage(LanguageMetadata), #[cfg(feature = "singleplayer")] StartSingleplayer, Quit, // Note: Keeping in case we re-add the disclaimer //DisclaimerAccepted, AuthServerTrust(String, bool), } pub struct LoginInfo { pub username: String, pub password: String, pub server: String, } enum ConnectionState { InProgress, AuthTrustPrompt { auth_server: String, msg: String }, } enum Screen { // Note: Keeping in case we re-add the disclaimer /*Disclaimer { screen: disclaimer::Screen, },*/ Credits { screen: credits::Screen, }, Login { screen: Box, // boxed to avoid large variant // Error to display in a box error: Option, }, Servers { screen: servers::Screen, }, Connecting { screen: connecting::Screen, connection_state: ConnectionState, }, } struct Controls { fonts: Fonts, imgs: Imgs, bg_img: widget::image::Handle, i18n: LocalizationHandle, // Voxygen version version: String, // Alpha disclaimer alpha: String, credits: Credits, selected_server_index: Option, login_info: LoginInfo, is_selecting_language: bool, selected_language_index: Option, time: f64, screen: Screen, } #[derive(Clone)] enum Message { Quit, Back, ShowServers, ShowCredits, #[cfg(feature = "singleplayer")] Singleplayer, Multiplayer, LanguageChanged(usize), OpenLanguageMenu, Username(String), Password(String), Server(String), ServerChanged(usize), FocusPassword, CancelConnect, TrustPromptAdd, TrustPromptCancel, CloseError, /* Note: Keeping in case we re-add the disclaimer *AcceptDisclaimer, */ } impl Controls { fn new( fonts: Fonts, imgs: Imgs, bg_img: widget::image::Handle, i18n: LocalizationHandle, settings: &Settings, ) -> Self { let version = common::util::DISPLAY_VERSION_LONG.clone(); let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); let credits = Credits::load_expect_cloned("common.credits"); // Note: Keeping in case we re-add the disclaimer let screen = /* if settings.show_disclaimer { Screen::Disclaimer { screen: disclaimer::Screen::new(), } } else { */ Screen::Login { screen: Box::new(login::Screen::new()), error: None, }; //}; let login_info = LoginInfo { username: settings.networking.username.clone(), password: String::new(), server: settings.networking.default_server.clone(), }; let selected_server_index = settings .networking .servers .iter() .position(|f| f == &login_info.server); let language_metadatas = i18n::list_localizations(); let selected_language_index = language_metadatas .iter() .position(|f| f.language_identifier == settings.language.selected_language); Self { fonts, imgs, bg_img, i18n, version, alpha, credits, selected_server_index, login_info, is_selecting_language: false, selected_language_index, time: 0.0, screen, } } fn view(&mut self, settings: &Settings, dt: f32) -> Element { self.time += dt as f64; // TODO: consider setting this as the default in the renderer let button_style = style::button::Style::new(self.imgs.button) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) .text_color(TEXT_COLOR) .disabled_text_color(DISABLED_TEXT_COLOR); let alpha = iced::Text::new(&self.alpha) .size(self.fonts.cyri.scale(12)) .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Center); let top_text = Row::with_children(vec![ Space::new(Length::Fill, Length::Shrink).into(), alpha.into(), if matches!(&self.screen, Screen::Login { .. }) { // Login screen shows the Velroen logo over the version Space::new(Length::Fill, Length::Shrink).into() } else { iced::Text::new(&self.version) .size(self.fonts.cyri.scale(15)) .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Right) .into() }, ]) .padding(3) .width(Length::Fill); let bg_img = if matches!(&self.screen, Screen::Connecting { .. }) { self.bg_img } else { self.imgs.bg }; let language_metadatas = i18n::list_localizations(); // TODO: make any large text blocks scrollable so that if the area is to // small they can still be read let content = match &mut self.screen { // Note: Keeping in case we re-add the disclaimer //Screen::Disclaimer { screen } => screen.view(&self.fonts, &self.i18n, button_style), Screen::Credits { screen } => { screen.view(&self.fonts, &self.i18n.read(), &self.credits, button_style) }, Screen::Login { screen, error } => screen.view( &self.fonts, &self.imgs, &self.login_info, error.as_deref(), &self.i18n.read(), self.is_selecting_language, self.selected_language_index, &language_metadatas, button_style, &self.version, ), Screen::Servers { screen } => screen.view( &self.fonts, &self.imgs, &settings.networking.servers, self.selected_server_index, &self.i18n.read(), button_style, ), Screen::Connecting { screen, connection_state, } => screen.view( &self.fonts, &self.imgs, connection_state, self.time, &self.i18n.read(), button_style, settings.interface.loading_tips, &settings.controls, ), }; Container::new( Column::with_children(vec![top_text.into(), content]) .spacing(3) .width(Length::Fill) .height(Length::Fill), ) .style(style::container::Style::image(bg_img)) .into() } fn update( &mut self, message: Message, events: &mut Vec, settings: &Settings, ui: &mut Ui, ) { let servers = &settings.networking.servers; let mut language_metadatas = i18n::list_localizations(); match message { Message::Quit => events.push(Event::Quit), Message::Back => { self.screen = Screen::Login { screen: Box::new(login::Screen::new()), error: None, }; }, Message::ShowServers => { if matches!(&self.screen, Screen::Login { .. }) { self.selected_server_index = servers.iter().position(|f| f == &self.login_info.server); self.screen = Screen::Servers { screen: servers::Screen::new(), }; } }, Message::ShowCredits => { self.screen = Screen::Credits { screen: credits::Screen::new(), }; }, #[cfg(feature = "singleplayer")] Message::Singleplayer => { self.screen = Screen::Connecting { screen: connecting::Screen::new(ui), connection_state: ConnectionState::InProgress, }; events.push(Event::StartSingleplayer); }, Message::Multiplayer => { self.screen = Screen::Connecting { screen: connecting::Screen::new(ui), connection_state: ConnectionState::InProgress, }; events.push(Event::LoginAttempt { username: self.login_info.username.trim().to_string(), password: self.login_info.password.clone(), server_address: self.login_info.server.clone(), }); }, Message::Username(new_value) => self.login_info.username = new_value, Message::LanguageChanged(new_value) => { events.push(Event::ChangeLanguage(language_metadatas.remove(new_value))); }, Message::OpenLanguageMenu => self.is_selecting_language = !self.is_selecting_language, Message::Password(new_value) => self.login_info.password = new_value, Message::Server(new_value) => { self.login_info.server = new_value; }, Message::ServerChanged(new_value) => { self.selected_server_index = Some(new_value); self.login_info.server = servers[new_value].clone(); }, Message::FocusPassword => { if let Screen::Login { screen, .. } = &mut self.screen { screen.banner.password = text_input::State::focused(); screen.banner.username = text_input::State::new(); } }, Message::CancelConnect => { self.exit_connect_screen(); events.push(Event::CancelLoginAttempt); }, msg @ Message::TrustPromptAdd | msg @ Message::TrustPromptCancel => { if let Screen::Connecting { connection_state, .. } = &mut self.screen { if let ConnectionState::AuthTrustPrompt { auth_server, .. } = connection_state { let auth_server = std::mem::take(auth_server); let added = matches!(msg, Message::TrustPromptAdd); *connection_state = ConnectionState::InProgress; events.push(Event::AuthServerTrust(auth_server, added)); } } }, Message::CloseError => { if let Screen::Login { error, .. } = &mut self.screen { *error = None; } }, /* Note: Keeping in case we re-add the disclaimer */ /*Message::AcceptDisclaimer => { if let Screen::Disclaimer { .. } = &self.screen { events.push(Event::DisclaimerAccepted); self.screen = Screen::Login { screen: login::Screen::new(), error: None, }; } },*/ } } // Connection successful of failed fn exit_connect_screen(&mut self) { if matches!(&self.screen, Screen::Connecting { .. }) { self.screen = Screen::Login { screen: Box::new(login::Screen::new()), error: None, } } } fn auth_trust_prompt(&mut self, auth_server: String) { if let Screen::Connecting { connection_state, .. } = &mut self.screen { let msg = format!( "Warning: The server you are trying to connect to has provided this \ authentication server address:\n\n{}\n\nbut it is not in your list of trusted \ authentication servers.\n\nMake sure that you trust this site and owner to not \ try and bruteforce your password!", &auth_server ); *connection_state = ConnectionState::AuthTrustPrompt { auth_server, msg }; } } fn connection_error(&mut self, error: String) { if matches!(&self.screen, Screen::Connecting { .. }) || matches!(&self.screen, Screen::Login { .. }) { self.screen = Screen::Login { screen: Box::new(login::Screen::new()), error: Some(error), } } else { warn!("connection_error invoked on unhandled screen!"); } } fn tab(&mut self) { if let Screen::Login { screen, .. } = &mut self.screen { // TODO: add select all function in iced if screen.banner.username.is_focused() { screen.banner.username = iced::text_input::State::new(); screen.banner.password = iced::text_input::State::focused(); screen.banner.password.move_cursor_to_end(); } else if screen.banner.password.is_focused() { screen.banner.password = iced::text_input::State::new(); screen.banner.server = iced::text_input::State::focused(); screen.banner.server.move_cursor_to_end(); } else if screen.banner.server.is_focused() { screen.banner.server = iced::text_input::State::new(); screen.banner.username = iced::text_input::State::focused(); screen.banner.username.move_cursor_to_end(); } } } } pub struct MainMenuUi { ui: Ui, // TODO: re add this // tip_no: u16, controls: Controls, } impl MainMenuUi { pub fn new(global_state: &mut GlobalState) -> Self { // Load language let i18n = &global_state.i18n.read(); // TODO: don't add default font twice let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key); let mut ui = Ui::new( &mut global_state.window, font, global_state.settings.interface.ui_scale, ) .unwrap(); let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts"); let bg_img_spec = BG_IMGS.choose(&mut thread_rng()).unwrap(); let bg_img = assets::Image::load_expect(bg_img_spec).read().to_image(); let controls = Controls::new( fonts, Imgs::load(&mut ui).expect("Failed to load images"), ui.add_graphic(Graphic::Image(bg_img, None)), global_state.i18n, &global_state.settings, ); Self { ui, controls } } pub fn update_language(&mut self, i18n: LocalizationHandle, settings: &Settings) { self.controls.i18n = i18n; let i18n = &i18n.read(); let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key); self.ui.clear_fonts(font); self.controls.fonts = Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!"); let language_metadatas = i18n::list_localizations(); self.controls.selected_language_index = language_metadatas .iter() .position(|f| f.language_identifier == settings.language.selected_language); } pub fn auth_trust_prompt(&mut self, auth_server: String) { self.controls.auth_trust_prompt(auth_server); } pub fn show_info(&mut self, msg: String) { self.controls.connection_error(msg); } pub fn connected(&mut self) { self.controls.exit_connect_screen(); } pub fn cancel_connection(&mut self) { self.controls.exit_connect_screen(); } pub fn handle_event(&mut self, event: window::Event) -> bool { match event { // Pass events to ui. window::Event::IcedUi(event) => { self.handle_ui_event(event); true }, window::Event::ScaleFactorChanged(s) => { self.ui.scale_factor_changed(s); false }, _ => false, } } pub fn handle_ui_event(&mut self, event: ui::ice::Event) { // Tab for input fields use iced::keyboard; if matches!( &event, iced::Event::Keyboard(keyboard::Event::KeyPressed { key_code: keyboard::KeyCode::Tab, .. }) ) { self.controls.tab(); } self.ui.handle_event(event); } pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) { self.ui.set_scaling_mode(scale_mode); } pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec { let mut events = Vec::new(); let (messages, _) = self.ui.maintain( self.controls.view(&global_state.settings, dt.as_secs_f32()), global_state.window.renderer_mut(), None, &mut global_state.clipboard, ); messages.into_iter().for_each(|message| { self.controls .update(message, &mut events, &global_state.settings, &mut self.ui) }); events } pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); } }