veloren_voxygen/menu/
server_info.rs

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    /// Create a new `MainMenuState`.
77    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 there are no rules, or we've already accepted these rules, we don't need
88        // this state
89        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        // Load language
98        let i18n = &global_state.i18n.read();
99        // TODO: don't add default font twice
100        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            // Pass events to ui.
139            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        /*
155        // Updated localization in case the selected language was changed
156        self.main_menu_ui
157            .update_language(global_state.i18n, &global_state.settings);
158        // Set scale mode in case it was change
159        self.main_menu_ui
160            .set_scale_mode(global_state.settings.interface.ui_scale);
161        */
162    }
163
164    fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
165        span!(_guard, "tick", "<ServerInfoState as PlayState>::tick");
166
167        // Handle window events.
168        for event in events {
169            // Pass all events to the ui first.
170            if self.handle_event(event.clone()) {
171                continue;
172            }
173
174            match event {
175                Event::Close => return PlayStateResult::Shutdown,
176                // Ignore all other events.
177                _ => {},
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        // Maintain the UI.
195        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)] // TODO: Remove when more message types are added
204        for message in messages {
205            match message {
206                Message::Accept => {
207                    // Update last-accepted rules hash so we don't see the message again
208                    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        // Draw the UI to the screen.
234        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        // TODO: consider setting this as the default in the renderer
254        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(iced::Text::new(format!("{}: {}", self.server_info.name,
312        // self.server_info.description))     .size(self.fonts.cyri.scale(20))
313        //     .width(Length::Shrink)
314        //     .horizontal_alignment(HorizontalAlignment::Center)
315        //     .into());
316
317        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                // .width(Length::Shrink)
360                // .height(Length::Shrink)
361                .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}