1use super::{char_selection::CharSelectionState, dummy_scene::Scene};
2use crate::{
3 Direction, GlobalState, PlayState, PlayStateResult,
4 menu::main::get_client_msg_error,
5 render::{Drawer, GlobalsBindGroup},
6 settings::Settings,
7 ui::{
8 Graphic,
9 fonts::IcedFonts as Fonts,
10 ice::{Element, IcedUi as Ui, component::neat_button, load_font, style, widget},
11 img_ids::ImageGraphic,
12 },
13 window::{self, Event},
14};
15use client::ServerInfo;
16use common::{
17 assets::{self, AssetExt},
18 comp,
19};
20use common_base::span;
21use common_net::msg::server::ServerDescription;
22use i18n::LocalizationHandle;
23use iced::{
24 Align, Column, Container, HorizontalAlignment, Length, Row, Scrollable, VerticalAlignment,
25 button, scrollable,
26};
27use std::{
28 collections::hash_map::DefaultHasher,
29 hash::{Hash, Hasher},
30};
31use tracing::error;
32
33image_ids_ice! {
34 struct Imgs {
35 <ImageGraphic>
36 button: "voxygen.element.ui.generic.buttons.button",
37 button_hover: "voxygen.element.ui.generic.buttons.button_hover",
38 button_press: "voxygen.element.ui.generic.buttons.button_press",
39 }
40}
41
42pub struct Controls {
43 fonts: Fonts,
44 imgs: Imgs,
45 i18n: LocalizationHandle,
46 bg_img: widget::image::Handle,
47
48 accept_button: button::State,
49 decline_button: button::State,
50 scrollable: scrollable::State,
51 server_info: ServerInfo,
52 server_description: ServerDescription,
53 changed: bool,
54}
55
56pub struct ServerInfoState {
57 ui: Ui,
58 scene: Scene,
59 controls: Controls,
60 char_select: Option<CharSelectionState>,
61}
62
63#[derive(Clone)]
64pub enum Message {
65 Accept,
66 Decline,
67}
68
69fn rules_hash(rules: &Option<String>) -> u64 {
70 let mut hasher = DefaultHasher::default();
71 rules.hash(&mut hasher);
72 hasher.finish()
73}
74
75impl ServerInfoState {
76 pub fn try_from_server_info(
78 global_state: &mut GlobalState,
79 bg_img_spec: &'static str,
80 char_select: CharSelectionState,
81 server_info: ServerInfo,
82 server_description: ServerDescription,
83 force_show: bool,
84 ) -> Result<Self, CharSelectionState> {
85 let server = global_state.profile.servers.get(&server_info.name);
86
87 if (server_description.rules.is_none()
90 || server
91 .is_some_and(|s| s.accepted_rules == Some(rules_hash(&server_description.rules))))
92 && !force_show
93 {
94 return Err(char_select);
95 }
96
97 let i18n = &global_state.i18n.read();
99 let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
101
102 let mut ui = Ui::new(
103 &mut global_state.window,
104 font,
105 global_state.settings.interface.ui_scale,
106 )
107 .unwrap();
108
109 let changed = server.is_some_and(|s| {
110 s.accepted_rules
111 .is_some_and(|accepted| accepted != rules_hash(&server_description.rules))
112 });
113
114 Ok(Self {
115 scene: Scene::new(global_state.window.renderer_mut()),
116 controls: Controls {
117 bg_img: ui.add_graphic(Graphic::Image(
118 assets::Image::load_expect(bg_img_spec).read().to_image(),
119 None,
120 )),
121 imgs: Imgs::load(&mut ui).expect("Failed to load images"),
122 fonts: Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts"),
123 i18n: global_state.i18n,
124 accept_button: Default::default(),
125 decline_button: Default::default(),
126 scrollable: Default::default(),
127 server_info,
128 server_description,
129 changed,
130 },
131 ui,
132 char_select: Some(char_select),
133 })
134 }
135
136 fn handle_event(&mut self, event: window::Event) -> bool {
137 match event {
138 window::Event::IcedUi(event) => {
140 self.ui.handle_event(event);
141 true
142 },
143 window::Event::ScaleFactorChanged(s) => {
144 self.ui.scale_factor_changed(s);
145 false
146 },
147 _ => false,
148 }
149 }
150}
151
152impl PlayState for ServerInfoState {
153 fn enter(&mut self, _global_state: &mut GlobalState, _: Direction) {
154 }
163
164 fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
165 span!(_guard, "tick", "<ServerInfoState as PlayState>::tick");
166
167 for event in events {
169 if self.handle_event(event.clone()) {
171 continue;
172 }
173
174 match event {
175 Event::Close => return PlayStateResult::Shutdown,
176 _ => {},
178 }
179 }
180
181 if let Some(char_select) = &mut self.char_select {
182 if let Err(err) = char_select
183 .client()
184 .borrow_mut()
185 .tick(comp::ControllerInputs::default(), global_state.clock.dt())
186 {
187 error!(?err, "[server_info] Failed to tick the client");
188 global_state.info_message =
189 Some(get_client_msg_error(err, None, &global_state.i18n.read()));
190 return PlayStateResult::Pop;
191 }
192 }
193
194 let view = self.controls.view();
196 let (messages, _) = self.ui.maintain(
197 view,
198 global_state.window.renderer_mut(),
199 None,
200 &mut global_state.clipboard,
201 );
202
203 #[expect(clippy::never_loop)] for message in messages {
205 match message {
206 Message::Accept => {
207 if let Some(server) = global_state
209 .profile
210 .servers
211 .get_mut(&self.controls.server_info.name)
212 {
213 server.accepted_rules =
214 Some(rules_hash(&self.controls.server_description.rules));
215 }
216
217 return PlayStateResult::Switch(Box::new(self.char_select.take().unwrap()));
218 },
219 Message::Decline => return PlayStateResult::Pop,
220 }
221 }
222
223 PlayStateResult::Continue
224 }
225
226 fn name(&self) -> &'static str { "Server Info" }
227
228 fn capped_fps(&self) -> bool { true }
229
230 fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
231
232 fn render(&self, drawer: &mut Drawer<'_>, _: &Settings) {
233 let mut third_pass = drawer.third_pass();
235 if let Some(mut ui_drawer) = third_pass.draw_ui() {
236 self.ui.render(&mut ui_drawer);
237 };
238 }
239
240 fn egui_enabled(&self) -> bool { false }
241}
242
243impl Controls {
244 fn view(&mut self) -> Element<Message> {
245 pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
246 pub const IMPORTANT_TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 0.85, 0.5);
247 pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
248
249 pub const FILL_FRAC_ONE: f32 = 0.67;
250
251 let i18n = self.i18n.read();
252
253 let button_style = style::button::Style::new(self.imgs.button)
255 .hover_image(self.imgs.button_hover)
256 .press_image(self.imgs.button_press)
257 .text_color(TEXT_COLOR)
258 .disabled_text_color(DISABLED_TEXT_COLOR);
259
260 let accept_button = Container::new(
261 Container::new(neat_button(
262 &mut self.accept_button,
263 i18n.get_msg("common-accept"),
264 FILL_FRAC_ONE,
265 button_style,
266 Some(Message::Accept),
267 ))
268 .max_width(200),
269 )
270 .width(Length::Fill)
271 .align_x(Align::Center);
272
273 let decline_button = Container::new(
274 Container::new(neat_button(
275 &mut self.decline_button,
276 i18n.get_msg("common-decline"),
277 FILL_FRAC_ONE,
278 button_style,
279 Some(Message::Decline),
280 ))
281 .max_width(200),
282 )
283 .width(Length::Fill)
284 .align_x(Align::Center);
285
286 let mut elements = Vec::new();
287
288 elements.push(
289 Container::new(
290 iced::Text::new(i18n.get_msg("main-server-rules"))
291 .size(self.fonts.cyri.scale(36))
292 .horizontal_alignment(HorizontalAlignment::Center),
293 )
294 .width(Length::Fill)
295 .into(),
296 );
297
298 if self.changed {
299 elements.push(
300 Container::new(
301 iced::Text::new(i18n.get_msg("main-server-rules-seen-before"))
302 .size(self.fonts.cyri.scale(30))
303 .color(IMPORTANT_TEXT_COLOR)
304 .horizontal_alignment(HorizontalAlignment::Center),
305 )
306 .width(Length::Fill)
307 .into(),
308 );
309 }
310
311 elements.push(
318 Scrollable::new(&mut self.scrollable)
319 .push(
320 iced::Text::new(
321 self.server_description
322 .rules
323 .as_deref()
324 .unwrap_or("<rules>"),
325 )
326 .size(self.fonts.cyri.scale(26))
327 .width(Length::Shrink)
328 .horizontal_alignment(HorizontalAlignment::Left)
329 .vertical_alignment(VerticalAlignment::Top),
330 )
331 .height(Length::Fill)
332 .width(Length::Fill)
333 .into(),
334 );
335
336 elements.push(
337 Row::with_children(vec![decline_button.into(), accept_button.into()])
338 .width(Length::Shrink)
339 .height(Length::Shrink)
340 .padding(25)
341 .into(),
342 );
343
344 Container::new(
345 Container::new(
346 Column::with_children(elements)
347 .spacing(10)
348 .padding(20),
349 )
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 .max_width(1000)
358 .align_x(Align::Center)
359 .padding(15),
362 )
363 .style(style::container::Style::image(self.bg_img))
364 .width(Length::Fill)
365 .height(Length::Fill)
366 .align_x(Align::Center)
367 .padding(50)
368 .into()
369 }
370}