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