1use super::{ConnectionState, Imgs, LoginInfo, Message, credentials};
2
3use crate::{
4 game_input::GameInput,
5 menu::main::DetailedInitializationStage,
6 settings::ControlSettings,
7 ui::{
8 Graphic,
9 fonts::IcedFonts as Fonts,
10 ice::{Element, IcedUi as Ui, Id, component::neat_button, style, widget::Image},
11 },
12};
13use client::ClientInitStage;
14use common::assets::{self, AssetExt, Ron};
15use i18n::Localization;
16use iced::{Align, Column, Container, Length, Row, Space, Text, button};
17#[cfg(feature = "singleplayer")]
18use server::{ServerInitStage, WorldCivStage, WorldGenerateStage, WorldSimStage};
19
20struct LoadingAnimation {
21 speed_factor: f32,
22 frames: Vec<Id>,
23}
24impl LoadingAnimation {
25 fn new(raw: &(f32, Vec<String>), ui: &mut Ui) -> Self {
26 let mut frames = vec![];
27 for frame_path in raw.1.iter() {
28 if let Ok(image) = assets::Image::load(frame_path) {
29 frames.push(ui.add_graphic(Graphic::Image(image.read().to_image(), None)));
30 } else {
31 frames.push(
32 ui.add_graphic(Graphic::Image(
33 assets::Image::load("voxygen.element.not_found")
34 .unwrap_or_else(|_| panic!("Missing asset '{}'", frame_path))
35 .read()
36 .to_image(),
37 None,
38 )),
39 )
40 }
41 }
42 Self {
43 speed_factor: raw.0,
44 frames,
45 }
46 }
47}
48
49pub struct Screen {
51 cancel_button: button::State,
52 trust_cancel_button: button::State,
53 add_button: button::State,
54 pub credentials: credentials::Prompt,
55 tip_number: u16,
56 loading_animation: LoadingAnimation,
57}
58
59impl Screen {
60 pub fn new(ui: &mut Ui) -> Self {
61 let animations = Ron::<Vec<(f32, Vec<String>)>>::load_cloned(
62 "voxygen.element.animation.loaders.manifest",
63 )
64 .expect("Missing loader manifest file 'voxygen/element/animation/loaders/manifest.ron'")
65 .into_inner();
66 Self {
67 cancel_button: Default::default(),
68 trust_cancel_button: Default::default(),
69 add_button: Default::default(),
70 credentials: Default::default(),
71 tip_number: rand::random(),
72 loading_animation: LoadingAnimation::new(
73 &animations[(rand::random::<u64>() as usize) % animations.len()],
74 ui,
75 ),
76 }
77 }
78
79 pub(super) fn view(
80 &mut self,
81 fonts: &Fonts,
82 imgs: &Imgs,
83 connection_state: &ConnectionState,
84 login_info: &LoginInfo,
85 init_stage: &DetailedInitializationStage,
86 time: f64,
87 i18n: &Localization,
88 button_style: style::button::Style,
89 show_tip: bool,
90 controls: &ControlSettings,
91 ) -> Element<'_, Message> {
92 let frame_index = (time * self.loading_animation.speed_factor as f64)
94 % self.loading_animation.frames.len() as f64;
95 let frame_id = self.loading_animation.frames[frame_index as usize];
96
97 let bottom_bar = {
98 let tip = if show_tip {
99 let key = |code| match controls.keybindings.get(&code) {
100 Some(Some(key_mouse)) => key_mouse.display_string(),
101 Some(None) => i18n.get_msg("main-unbound_key_tip").into_owned(),
102 None => match ControlSettings::default_binding(code) {
103 Some(key_mouse) => key_mouse.display_string(),
104 None => i18n.get_msg("main-unbound_key_tip").into_owned(),
105 },
106 };
107 let keys = i18n::fluent_args! {
108 "gameinput-togglelantern" => key(GameInput::ToggleLantern),
109 "gameinput-controls" => key(GameInput::Controls),
110 "gameinput-settings" => key(GameInput::Settings),
111 "gameinput-social" => key(GameInput::Social),
112 "gameinput-dance" => key(GameInput::Dance),
113 "gameinput-glide" => key(GameInput::Glide),
114 "gameinput-sit" => key(GameInput::Sit),
115 "gameinput-crafting" => key(GameInput::Crafting),
116 "gameinput-roll" => key(GameInput::Roll),
117 "gameinput-screenshot" => key(GameInput::Screenshot),
118 };
119 let tip = &i18n.get_variation_ctx("loading-tips", self.tip_number, &keys);
120 let tip = format!("{} {}", i18n.get_msg("main-tip"), tip);
121
122 Container::new(
123 Text::new(tip)
124 .horizontal_alignment(iced::HorizontalAlignment::Center)
125 .size(fonts.cyri.scale(25)),
126 )
127 .width(Length::Fill)
128 .height(Length::Fill)
129 .center_x()
130 .align_y(Align::Center)
131 .into()
132 } else {
133 Space::new(Length::Fill, Length::Fill).into()
134 };
135
136 let stage = {
137 let stage_message = match init_stage {
138 #[cfg(feature = "singleplayer")]
139 DetailedInitializationStage::Singleplayer => {
140 i18n.get_msg("hud-init-stage-singleplayer")
141 },
142 #[cfg(feature = "singleplayer")]
143 DetailedInitializationStage::SingleplayerServer(server_stage) => {
144 match server_stage {
145 ServerInitStage::DbMigrations => {
146 i18n.get_msg("hud-init-stage-server-db-migrations")
147 },
148 ServerInitStage::DbVacuum => {
149 i18n.get_msg("hud-init-stage-server-db-vacuum")
150 },
151 ServerInitStage::WorldGen(worldgen_stage) => match worldgen_stage {
152 WorldGenerateStage::WorldSimGenerate(worldsim_stage) => {
153 match worldsim_stage {
154 WorldSimStage::Erosion { progress, estimate } => {
155 let mut msg = i18n
156 .get_msg_ctx(
157 "hud-init-stage-server-worldsim-erosion",
158 &i18n::fluent_args! { "percentage" => format!("{progress:.0}") }
159 ).into_owned();
160 if let Some(estimate) = estimate {
161 let (attr, duration) =
162 chrono::Duration::from_std(*estimate)
163 .map(|dur| {
164 let days = dur.num_days();
165 if days > 0 {
166 return ("days", days);
167 }
168 let hours = dur.num_hours();
169 if hours > 0 {
170 return ("hours", hours);
171 }
172 let minutes = dur.num_minutes();
173 if minutes > 0 {
174 return ("minutes", minutes);
175 }
176
177 ("seconds", dur.num_seconds())
178 })
179 .unwrap_or(("days", i64::MAX));
180 msg.push(' ');
181 msg.push('(');
182 msg.push_str(&i18n.get_attr_ctx(
183 "hud-init-stage-server-worldsim-erosion_time_left",
184 attr,
185 &i18n::fluent_args! { "n" => duration }
186 ));
187 msg.push(')');
188 }
189
190 std::borrow::Cow::Owned(msg)
191 },
192 }
193 },
194 WorldGenerateStage::WorldCivGenerate(worldciv_stage) => {
195 match worldciv_stage {
196 WorldCivStage::CivCreation(generated, total) => i18n
197 .get_msg_ctx(
198 "hud-init-stage-server-worldciv-civcreate",
199 &i18n::fluent_args! {
200 "generated" => generated.to_string(),
201 "total" => total.to_string(),
202 },
203 ),
204 WorldCivStage::SiteGeneration => {
205 i18n.get_msg("hud-init-stage-server-worldciv-site")
206 },
207 }
208 },
209 WorldGenerateStage::EconomySimulation => {
210 i18n.get_msg("hud-init-stage-server-economysim")
211 },
212 WorldGenerateStage::SpotGeneration => {
213 i18n.get_msg("hud-init-stage-server-spotgen")
214 },
215 },
216 ServerInitStage::StartingSystems => {
217 i18n.get_msg("hud-init-stage-server-starting")
218 },
219 }
220 },
221 DetailedInitializationStage::StartingMultiplayer => {
222 i18n.get_msg("hud-init-stage-multiplayer")
223 },
224 DetailedInitializationStage::Client(client_stage) => match client_stage {
225 ClientInitStage::ConnectionEstablish => {
226 i18n.get_msg("hud-init-stage-client-connection-establish")
227 },
228 ClientInitStage::WatingForServerVersion => {
229 i18n.get_msg("hud-init-stage-client-request-server-version")
230 },
231 ClientInitStage::ServerVersionArrived(_)
232 | ClientInitStage::Authentication => {
233 i18n.get_msg("hud-init-stage-client-authentication")
234 },
235 ClientInitStage::LoadingInitData => {
236 i18n.get_msg("hud-init-stage-client-load-init-data")
237 },
238 ClientInitStage::StartingClient => {
239 i18n.get_msg("hud-init-stage-client-starting-client")
240 },
241 ClientInitStage::RequestingPlugins => {
242 i18n.get_msg("hud-init-stage-client-requesting-plugins")
243 },
244 },
245 DetailedInitializationStage::CreatingRenderPipeline(done, total) => i18n
246 .get_msg_ctx(
247 "hud-init-stage-render-pipeline",
248 &i18n::fluent_args! { "done" => done, "total" => total },
249 ),
250 };
251
252 Container::new(Text::new(stage_message).size(fonts.cyri.scale(20)))
253 .width(Length::Fill)
254 .height(Length::Fill)
255 .padding(10)
256 .align_x(Align::Start)
257 .into()
258 };
259
260 let cancel = Container::new(neat_button(
261 &mut self.cancel_button,
262 i18n.get_msg("common-cancel"),
263 0.7,
264 button_style,
265 Some(Message::CancelConnect),
266 ))
267 .width(Length::Fill)
268 .height(Length::Units(fonts.cyri.scale(30)))
269 .center_x()
270 .padding(3);
271
272 let tip_cancel = Column::with_children(vec![tip, cancel.into()])
273 .width(Length::FillPortion(2))
274 .align_items(Align::Center)
275 .spacing(5)
276 .padding(5);
277
278 let gear = Container::new(
279 Image::new(frame_id)
280 .width(Length::Units(64))
281 .height(Length::Units(64)),
282 )
283 .width(Length::Fill)
284 .padding(10)
285 .align_x(Align::End);
286
287 let bottom_content = Row::with_children(vec![stage, tip_cancel.into(), gear.into()])
288 .align_items(Align::Center)
289 .width(Length::Fill);
290
291 let left_art = Image::new(imgs.loading_art_l)
292 .width(Length::Units(12))
293 .height(Length::Units(12));
294 let right_art = Image::new(imgs.loading_art_r)
295 .width(Length::Units(12))
296 .height(Length::Units(12));
297
298 Container::new(Row::with_children(vec![
299 left_art.into(),
300 bottom_content.into(),
301 right_art.into(),
302 ]))
303 .height(Length::Units(90))
304 .style(style::container::Style::image(imgs.loading_art))
305 };
306
307 let content = match connection_state {
308 ConnectionState::InProgress => Space::new(Length::Fill, Length::Fill).into(),
309 ConnectionState::AuthTrustPrompt { auth_server } => {
310 let msg = i18n.get_msg_ctx(
311 "main-login-trust-auth-server-warning",
312 &i18n::fluent_args! {
313 "server" => auth_server
314 },
315 );
316 let text = Text::new(msg).size(fonts.cyri.scale(25));
317
318 let cancel = neat_button(
319 &mut self.trust_cancel_button,
320 i18n.get_msg("common-cancel"),
321 0.7,
322 button_style,
323 Some(Message::TrustPromptCancel),
324 );
325 let add = neat_button(
326 &mut self.add_button,
327 i18n.get_msg("common-add"),
328 0.7,
329 button_style,
330 Some(Message::TrustPromptAdd),
331 );
332
333 let content = Column::with_children(vec![
334 text.into(),
335 Container::new(
336 Row::with_children(vec![cancel, add])
337 .spacing(20)
338 .height(Length::Units(25)),
339 )
340 .align_x(Align::End)
341 .width(Length::Fill)
342 .into(),
343 ])
344 .spacing(4)
345 .max_width(520)
346 .width(Length::Fill)
347 .height(Length::Fill);
348
349 let prompt_window = Container::new(content)
350 .style(
351 style::container::Style::color_with_double_cornerless_border(
352 (22, 18, 16, 255).into(),
353 (11, 11, 11, 255).into(),
354 (54, 46, 38, 255).into(),
355 ),
356 )
357 .padding(20);
358
359 let container = Container::new(prompt_window)
360 .width(Length::Fill)
361 .height(Length::Fill)
362 .center_x()
363 .center_y();
364
365 container.into()
366 },
367 ConnectionState::CredentialsPrompt { auth_server } => {
368 self.credentials
369 .view(fonts, imgs, login_info, auth_server, i18n, button_style)
370 },
371 };
372
373 Column::with_children(vec![content, bottom_bar.into()])
374 .width(Length::Fill)
375 .height(Length::Fill)
376 .into()
377 }
378}