Skip to main content

veloren_voxygen/hud/
popup.rs

1use super::Show;
2use crate::{GlobalState, ui::fonts::Fonts};
3use client::{self, Client, UserNotification};
4use conrod_core::{
5    Color, Colorable, Positionable, Widget, WidgetCommon,
6    widget::{self, Text},
7    widget_ids,
8};
9use i18n::Localization;
10use std::{collections::VecDeque, time::Instant};
11
12widget_ids! {
13    struct Ids {
14        error_bg,
15        error_text,
16        info_bg,
17        info_text,
18        message_bg,
19        message_text,
20    }
21}
22
23#[derive(WidgetCommon)]
24pub struct Popup<'a> {
25    i18n: &'a Localization,
26    client: &'a Client,
27    new_notifications: &'a VecDeque<UserNotification>,
28    fonts: &'a Fonts,
29    #[conrod(common_builder)]
30    common: widget::CommonBuilder,
31    show: &'a Show,
32    global_state: &'a GlobalState,
33}
34
35/// Popup notifications for messages such as <Chunk Name>, Waypoint Saved,
36/// Dungeon Cleared (TODO), and Quest Completed (TODO)
37impl<'a> Popup<'a> {
38    pub fn new(
39        i18n: &'a Localization,
40        client: &'a Client,
41        new_notifications: &'a VecDeque<UserNotification>,
42        fonts: &'a Fonts,
43        show: &'a Show,
44        global_state: &'a GlobalState,
45    ) -> Self {
46        Self {
47            i18n,
48            client,
49            new_notifications,
50            fonts,
51            common: widget::CommonBuilder::default(),
52            show,
53            global_state,
54        }
55    }
56}
57
58pub struct State {
59    ids: Ids,
60    errors: VecDeque<String>,
61    infos: VecDeque<String>,
62    messages: VecDeque<String>,
63    last_error_update: Instant,
64    last_info_update: Instant,
65    last_message_update: Instant,
66    last_region_name: Option<String>,
67}
68
69impl Widget for Popup<'_> {
70    type Event = ();
71    type State = State;
72    type Style = ();
73
74    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
75        State {
76            ids: Ids::new(id_gen),
77            errors: VecDeque::new(),
78            infos: VecDeque::new(),
79            messages: VecDeque::new(),
80            last_error_update: Instant::now(),
81            last_info_update: Instant::now(),
82            last_message_update: Instant::now(),
83            last_region_name: None,
84        }
85    }
86
87    fn style(&self) -> Self::Style {}
88
89    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
90        common_base::prof_span!("Popup::update");
91        let widget::UpdateArgs { state, ui, .. } = args;
92
93        const FADE_IN: f32 = 0.5;
94        const FADE_HOLD: f32 = 1.0;
95        const FADE_OUT: f32 = 3.0;
96
97        let bg_color = |fade| Color::Rgba(0.0, 0.0, 0.0, fade);
98        let error_color = |fade| Color::Rgba(1.0, 0.0, 0.0, fade);
99        let info_color = |fade| Color::Rgba(1.0, 1.0, 0.0, fade);
100        let message_color = |fade| Color::Rgba(1.0, 1.0, 1.0, fade);
101
102        // Push chunk name to message queue
103        if let Some(chunk) = self.client.current_chunk()
104            && let Some(current) = chunk.meta().name()
105        {
106            // Check if no other popup is displayed and a new one is needed
107            if state.messages.is_empty()
108                && state
109                    .last_region_name
110                    .as_ref()
111                    .map(|l| l != current)
112                    .unwrap_or(true)
113            {
114                // Update last_region
115                state.update(|s| {
116                    s.last_region_name = Some(current.to_owned());
117                    if self
118                        .global_state
119                        .settings
120                        .interface
121                        .toggle_biome_change_popups
122                    {
123                        if s.messages.is_empty() {
124                            s.last_message_update = Instant::now();
125                        }
126                        s.messages.push_back(current.to_owned());
127                    }
128                });
129            }
130        }
131
132        // Push waypoint to message queue
133        for notification in self.new_notifications {
134            match notification {
135                UserNotification::WaypointUpdated => {
136                    state.update(|s| {
137                        if s.infos.is_empty() {
138                            s.last_info_update = Instant::now();
139                        }
140                        let text = self.i18n.get_msg("hud-waypoint_saved");
141                        s.infos.push_back(text.to_string());
142                    });
143                },
144            }
145        }
146
147        // Get next error from queue
148        if !state.errors.is_empty()
149            && state.last_error_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT
150        {
151            state.update(|s| {
152                s.errors.pop_front();
153                s.last_error_update = Instant::now();
154            });
155        }
156
157        // Display error as popup
158        if let Some(error) = state.errors.front() {
159            let seconds = state.last_error_update.elapsed().as_secs_f32();
160            let fade = if seconds < FADE_IN {
161                seconds / FADE_IN
162            } else if seconds < FADE_IN + FADE_HOLD {
163                1.0
164            } else {
165                (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0)
166            };
167            Text::new(error)
168                .mid_top_with_margin_on(ui.window, 50.0)
169                .font_size(self.fonts.cyri.scale(20))
170                .font_id(self.fonts.cyri.conrod_id)
171                .color(bg_color(fade))
172                .set(state.ids.error_bg, ui);
173            Text::new(error)
174                .top_left_with_margins_on(state.ids.error_bg, -1.0, -1.0)
175                .font_size(self.fonts.cyri.scale(20))
176                .font_id(self.fonts.cyri.conrod_id)
177                .color(error_color(fade))
178                .set(state.ids.error_text, ui);
179        }
180
181        // Get next info from queue
182        if !state.infos.is_empty()
183            && state.last_info_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT
184        {
185            state.update(|s| {
186                s.infos.pop_front();
187                s.last_info_update = Instant::now();
188            });
189        }
190
191        // Display info as popup
192        if !self.show.intro
193            && let Some(info) = state.infos.front()
194        {
195            let seconds = state.last_info_update.elapsed().as_secs_f32();
196            let fade = if seconds < FADE_IN {
197                seconds / FADE_IN
198            } else if seconds < FADE_IN + FADE_HOLD {
199                1.0
200            } else {
201                (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0)
202            };
203
204            Text::new(info)
205                .mid_top_with_margin_on(ui.window, 100.0)
206                .font_size(self.fonts.cyri.scale(20))
207                .font_id(self.fonts.cyri.conrod_id)
208                .color(bg_color(fade))
209                .set(state.ids.info_bg, ui);
210            Text::new(info)
211                .top_left_with_margins_on(state.ids.info_bg, -1.0, -1.0)
212                .font_size(self.fonts.cyri.scale(20))
213                .font_id(self.fonts.cyri.conrod_id)
214                .color(info_color(fade))
215                .set(state.ids.info_text, ui);
216        }
217
218        // Get next message from queue
219        if !state.messages.is_empty()
220            && state.last_message_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT
221        {
222            state.update(|s| {
223                s.messages.pop_front();
224                s.last_message_update = Instant::now();
225            });
226        }
227
228        // Display message as popup
229        if !self.show.intro
230            && let Some(message) = state.messages.front()
231        {
232            let seconds = state.last_message_update.elapsed().as_secs_f32();
233            let fade = if seconds < FADE_IN {
234                seconds / FADE_IN
235            } else if seconds < FADE_IN + FADE_HOLD {
236                1.0
237            } else {
238                (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0)
239            };
240            Text::new(message)
241                .mid_top_with_margin_on(ui.window, 200.0)
242                .font_size(self.fonts.alkhemi.scale(70))
243                .font_id(self.fonts.alkhemi.conrod_id)
244                .color(bg_color(fade))
245                .set(state.ids.message_bg, ui);
246            Text::new(message)
247                .top_left_with_margins_on(state.ids.message_bg, -2.5, -2.5)
248                .font_size(self.fonts.alkhemi.scale(70))
249                .font_id(self.fonts.alkhemi.conrod_id)
250                .color(message_color(fade))
251                .set(state.ids.message_text, ui);
252        }
253    }
254}