veloren_voxygen/hud/
popup.rs

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