veloren_voxygen/hud/
popup.rs

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