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 #[expect(clippy::result_large_err)]
78 pub fn try_from_server_info(
79 global_state: &mut GlobalState,
80 bg_img_spec: &'static str,
81 char_select: CharSelectionState,
82 server_info: ServerInfo,
83 server_description: ServerDescription,
84 force_show: bool,
85 ) -> Result<Self, CharSelectionState> {
86 let server = global_state.profile.servers.get(&server_info.name);
87
88 if (server_description.rules.is_none()
91 || server
92 .is_some_and(|s| s.accepted_rules == Some(rules_hash(&server_description.rules))))
93 && !force_show
94 {
95 return Err(char_select);
96 }
97
98 let i18n = &global_state.i18n.read();
100 let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
102
103 let mut ui = Ui::new(
104 &mut global_state.window,
105 font,
106 global_state.settings.interface.ui_scale,
107 )
108 .unwrap();
109
110 let changed = server.is_some_and(|s| {
111 s.accepted_rules
112 .is_some_and(|accepted| accepted != rules_hash(&server_description.rules))
113 });
114
115 Ok(Self {
116 scene: Scene::new(global_state.window.renderer_mut()),
117 controls: Controls {
118 bg_img: ui.add_graphic(Graphic::Image(
119 assets::Image::load_expect(bg_img_spec).read().to_image(),
120 None,
121 )),
122 imgs: Imgs::load(&mut ui).expect("Failed to load images"),
123 fonts: Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts"),
124 i18n: global_state.i18n,
125 accept_button: Default::default(),
126 decline_button: Default::default(),
127 scrollable: Default::default(),
128 server_info,
129 server_description,
130 changed,
131 },
132 ui,
133 char_select: Some(char_select),
134 })
135 }
136
137 fn handle_event(&mut self, event: window::Event) -> bool {
138 match event {
139 window::Event::IcedUi(event) => {
141 self.ui.handle_event(event);
142 true
143 },
144 window::Event::ScaleFactorChanged(s) => {
145 self.ui.scale_factor_changed(s);
146 false
147 },
148 _ => false,
149 }
150 }
151}
152
153impl PlayState for ServerInfoState {
154 fn enter(&mut self, _global_state: &mut GlobalState, _: Direction) {
155 }
164
165 fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
166 span!(_guard, "tick", "<ServerInfoState as PlayState>::tick");
167
168 for event in events {
170 if self.handle_event(event.clone()) {
172 continue;
173 }
174
175 if matches!(event, Event::Close) {
177 return PlayStateResult::Shutdown;
178 }
179 }
180
181 if let Some(char_select) = &mut self.char_select
182 && 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 let view = self.controls.view();
195 let (messages, _) = self.ui.maintain(
196 view,
197 global_state.window.renderer_mut(),
198 None,
199 &mut global_state.clipboard,
200 );
201
202 #[expect(clippy::never_loop)] for message in messages {
204 match message {
205 Message::Accept => {
206 if let Some(server) = global_state
208 .profile
209 .servers
210 .get_mut(&self.controls.server_info.name)
211 {
212 server.accepted_rules =
213 Some(rules_hash(&self.controls.server_description.rules));
214 }
215
216 return PlayStateResult::Switch(Box::new(self.char_select.take().unwrap()));
217 },
218 Message::Decline => return PlayStateResult::Pop,
219 }
220 }
221
222 PlayStateResult::Continue
223 }
224
225 fn name(&self) -> &'static str { "Server Info" }
226
227 fn capped_fps(&self) -> bool { true }
228
229 fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
230
231 fn render(&self, drawer: &mut Drawer<'_>, _: &Settings) {
232 let mut third_pass = drawer.third_pass();
234 if let Some(mut ui_drawer) = third_pass.draw_ui() {
235 self.ui.render(&mut ui_drawer);
236 };
237 }
238
239 fn egui_enabled(&self) -> bool { false }
240}
241
242impl Controls {
243 fn view(&mut self) -> Element<'_, Message> {
244 pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
245 pub const IMPORTANT_TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 0.85, 0.5);
246 pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
247
248 pub const FILL_FRAC_ONE: f32 = 0.67;
249
250 let i18n = self.i18n.read();
251
252 let button_style = style::button::Style::new(self.imgs.button)
254 .hover_image(self.imgs.button_hover)
255 .press_image(self.imgs.button_press)
256 .text_color(TEXT_COLOR)
257 .disabled_text_color(DISABLED_TEXT_COLOR);
258
259 let accept_button = Container::new(
260 Container::new(neat_button(
261 &mut self.accept_button,
262 i18n.get_msg("common-accept"),
263 FILL_FRAC_ONE,
264 button_style,
265 Some(Message::Accept),
266 ))
267 .max_width(200),
268 )
269 .width(Length::Fill)
270 .align_x(Align::Center);
271
272 let decline_button = Container::new(
273 Container::new(neat_button(
274 &mut self.decline_button,
275 i18n.get_msg("common-decline"),
276 FILL_FRAC_ONE,
277 button_style,
278 Some(Message::Decline),
279 ))
280 .max_width(200),
281 )
282 .width(Length::Fill)
283 .align_x(Align::Center);
284
285 let mut elements = Vec::new();
286
287 elements.push(
288 Container::new(
289 iced::Text::new(i18n.get_msg("main-server-rules"))
290 .size(self.fonts.cyri.scale(36))
291 .horizontal_alignment(HorizontalAlignment::Center),
292 )
293 .width(Length::Fill)
294 .into(),
295 );
296
297 if self.changed {
298 elements.push(
299 Container::new(
300 iced::Text::new(i18n.get_msg("main-server-rules-seen-before"))
301 .size(self.fonts.cyri.scale(30))
302 .color(IMPORTANT_TEXT_COLOR)
303 .horizontal_alignment(HorizontalAlignment::Center),
304 )
305 .width(Length::Fill)
306 .into(),
307 );
308 }
309
310 elements.push(
317 Scrollable::new(&mut self.scrollable)
318 .push(
319 iced::Text::new(
320 self.server_description
321 .rules
322 .as_deref()
323 .unwrap_or("<rules>"),
324 )
325 .size(self.fonts.cyri.scale(26))
326 .width(Length::Shrink)
327 .horizontal_alignment(HorizontalAlignment::Left)
328 .vertical_alignment(VerticalAlignment::Top),
329 )
330 .height(Length::Fill)
331 .width(Length::Fill)
332 .into(),
333 );
334
335 elements.push(
336 Row::with_children(vec![decline_button.into(), accept_button.into()])
337 .width(Length::Shrink)
338 .height(Length::Shrink)
339 .padding(25)
340 .into(),
341 );
342
343 Container::new(
344 Container::new(
345 Column::with_children(elements)
346 .spacing(10)
347 .padding(20),
348 )
349 .style(
350 style::container::Style::color_with_double_cornerless_border(
351 (22, 18, 16, 255).into(),
352 (11, 11, 11, 255).into(),
353 (54, 46, 38, 255).into(),
354 ),
355 )
356 .max_width(1000)
357 .align_x(Align::Center)
358 .padding(15),
361 )
362 .style(style::container::Style::image(self.bg_img))
363 .width(Length::Fill)
364 .height(Length::Fill)
365 .align_x(Align::Center)
366 .padding(50)
367 .into()
368 }
369}