veloren_voxygen/hud/
subtitles.rs

1use std::{cmp::Ordering, collections::VecDeque};
2
3use crate::{settings::Settings, ui::fonts::Fonts};
4use client::Client;
5use conrod_core::{
6    Colorable, Positionable, UiCell, Widget, WidgetCommon,
7    widget::{self, Id, Rectangle, Text},
8    widget_ids,
9};
10use i18n::Localization;
11
12use vek::{Vec2, Vec3};
13
14widget_ids! {
15    struct Ids {
16        subtitle_box_bg,
17        subtitle_message[],
18        subtitle_dir[],
19    }
20}
21
22#[derive(WidgetCommon)]
23pub struct Subtitles<'a> {
24    client: &'a Client,
25    settings: &'a Settings,
26    listener_pos: Vec3<f32>,
27    listener_ori: Vec3<f32>,
28
29    fonts: &'a Fonts,
30
31    new_subtitles: &'a mut VecDeque<Subtitle>,
32
33    #[conrod(common_builder)]
34    common: widget::CommonBuilder,
35
36    localized_strings: &'a Localization,
37}
38
39impl<'a> Subtitles<'a> {
40    pub fn new(
41        client: &'a Client,
42        settings: &'a Settings,
43        listener_pos: Vec3<f32>,
44        listener_ori: Vec3<f32>,
45        new_subtitles: &'a mut VecDeque<Subtitle>,
46        fonts: &'a Fonts,
47        localized_strings: &'a Localization,
48    ) -> Self {
49        Self {
50            client,
51            settings,
52            listener_pos,
53            listener_ori,
54            fonts,
55            new_subtitles,
56            common: widget::CommonBuilder::default(),
57            localized_strings,
58        }
59    }
60}
61
62const MIN_SUBTITLE_DURATION: f64 = 1.5;
63const MAX_SUBTITLE_DIST: f32 = 80.0;
64
65#[derive(Debug)]
66pub struct Subtitle {
67    pub localization: String,
68    /// Position the sound is played at, if any.
69    pub position: Option<Vec3<f32>>,
70    /// Amount of seconds to show the subtitle for.
71    pub show_for: f64,
72}
73
74#[derive(Clone, PartialEq)]
75struct SubtitleData {
76    position: Option<Vec3<f32>>,
77    /// `Time` to show until.
78    show_until: f64,
79}
80
81impl SubtitleData {
82    /// Prioritize showing nearby sounds, and secondarily prioritize longer
83    /// living sounds.
84    fn compare_priority(&self, other: &Self, listener_pos: Vec3<f32>) -> Ordering {
85        let life_cmp = self
86            .show_until
87            .partial_cmp(&other.show_until)
88            .unwrap_or(Ordering::Equal);
89        match (self.position, other.position) {
90            (Some(a), Some(b)) => match a
91                .distance_squared(listener_pos)
92                .partial_cmp(&b.distance_squared(listener_pos))
93                .unwrap_or(Ordering::Equal)
94            {
95                Ordering::Equal => life_cmp,
96                Ordering::Less => Ordering::Greater,
97                Ordering::Greater => Ordering::Less,
98            },
99            (Some(_), None) => Ordering::Less,
100            (None, Some(_)) => Ordering::Greater,
101            (None, None) => life_cmp,
102        }
103    }
104}
105
106#[derive(Clone)]
107struct SubtitleList {
108    subtitles: Vec<(String, Vec<SubtitleData>)>,
109}
110
111impl SubtitleList {
112    fn new() -> Self {
113        Self {
114            subtitles: Vec::new(),
115        }
116    }
117
118    /// Updates the subtitle state, returns the amount of subtitles that should
119    /// be displayed.
120    fn update(
121        &mut self,
122        new_subtitles: impl Iterator<Item = Subtitle>,
123        time: f64,
124        listener_pos: Vec3<f32>,
125    ) -> usize {
126        for subtitle in new_subtitles {
127            let show_until = time + subtitle.show_for.max(MIN_SUBTITLE_DURATION);
128            let data = SubtitleData {
129                position: subtitle.position,
130                show_until,
131            };
132            if let Some((_, datas)) = self
133                .subtitles
134                .iter_mut()
135                .find(|(key, _)| key == &subtitle.localization)
136            {
137                datas.push(data);
138            } else {
139                self.subtitles.push((subtitle.localization, vec![data]))
140            }
141        }
142        let mut to_display = 0;
143        self.subtitles.retain_mut(|(_, data)| {
144            data.retain(|subtitle| subtitle.show_until > time);
145            // Place the most prioritized subtitle in the back.
146            if let Some((i, s)) = data
147                .iter()
148                .enumerate()
149                .max_by(|(_, a), (_, b)| a.compare_priority(b, listener_pos))
150            {
151                // We only display subtitles that are in range.
152                if s.position.is_none_or(|pos| {
153                    pos.distance_squared(listener_pos) < MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
154                }) {
155                    to_display += 1;
156                }
157                let last = data.len() - 1;
158                data.swap(i, last);
159                true
160            } else {
161                // If data is empty we have no sounds with this key.
162                false
163            }
164        });
165        to_display
166    }
167}
168
169pub struct State {
170    subtitles: SubtitleList,
171    ids: Ids,
172}
173
174impl Widget for Subtitles<'_> {
175    type Event = ();
176    type State = State;
177    type Style = ();
178
179    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
180        State {
181            subtitles: SubtitleList::new(),
182            ids: Ids::new(id_gen),
183        }
184    }
185
186    fn style(&self) -> Self::Style {}
187
188    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
189        common_base::prof_span!("Chat::update");
190
191        let widget::UpdateArgs { state, ui, .. } = args;
192        let time = self.client.state().get_time();
193        let listener_pos = self.listener_pos;
194        let listener_forward = self.listener_ori;
195
196        // Update subtitles and look for changes
197        let mut subtitles = state.subtitles.clone();
198
199        let has_new = !self.new_subtitles.is_empty();
200
201        let show_count = subtitles.update(self.new_subtitles.drain(..), time, listener_pos);
202
203        let subtitles = if has_new || show_count != state.ids.subtitle_message.len() {
204            state.update(|s| {
205                s.subtitles = subtitles;
206                s.ids
207                    .subtitle_message
208                    .resize(show_count, &mut ui.widget_id_generator());
209                s.ids
210                    .subtitle_dir
211                    .resize(show_count, &mut ui.widget_id_generator());
212            });
213            &state.subtitles
214        } else {
215            &subtitles
216        };
217        let color = |t: &SubtitleData| -> conrod_core::Color {
218            conrod_core::Color::Rgba(
219                0.9,
220                1.0,
221                1.0,
222                ((t.show_until - time) * 2.0).clamp(0.0, 1.0) as f32,
223            )
224        };
225
226        let listener_forward = listener_forward
227            .xy()
228            .try_normalized()
229            .unwrap_or(Vec2::unit_y());
230        let listener_right = Vec2::new(listener_forward.y, -listener_forward.x);
231
232        let dir = |subtitle: &SubtitleData, id: &Id, dir_id: &Id, ui: &mut UiCell| {
233            enum Side {
234                /// Also used for sounds without direction.
235                Forward,
236                Right,
237                Left,
238            }
239            let is_right = subtitle
240                .position
241                .map(|pos| {
242                    let dist = pos.distance(listener_pos);
243                    let dir = (pos - listener_pos) / dist;
244
245                    let dot = dir.xy().dot(listener_forward);
246                    if dist < 2.0 || dot > 0.85 {
247                        Side::Forward
248                    } else if dir.xy().dot(listener_right) >= 0.0 {
249                        Side::Right
250                    } else {
251                        Side::Left
252                    }
253                })
254                .unwrap_or(Side::Forward);
255
256            match is_right {
257                Side::Right => Text::new(">  ")
258                    .font_size(self.fonts.cyri.scale(14))
259                    .font_id(self.fonts.cyri.conrod_id)
260                    .parent(state.ids.subtitle_box_bg)
261                    .align_right_of(state.ids.subtitle_box_bg)
262                    .align_middle_y_of(*id)
263                    .color(color(subtitle))
264                    .set(*dir_id, ui),
265                Side::Left => Text::new("  <")
266                    .font_size(self.fonts.cyri.scale(14))
267                    .font_id(self.fonts.cyri.conrod_id)
268                    .parent(state.ids.subtitle_box_bg)
269                    .align_left_of(state.ids.subtitle_box_bg)
270                    .align_middle_y_of(*id)
271                    .color(color(subtitle))
272                    .set(*dir_id, ui),
273                Side::Forward => Text::new("")
274                    .font_size(self.fonts.cyri.scale(14))
275                    .font_id(self.fonts.cyri.conrod_id)
276                    .parent(state.ids.subtitle_box_bg)
277                    .color(color(subtitle))
278                    .set(*dir_id, ui),
279            }
280        };
281
282        Rectangle::fill([200.0, 22.0 * show_count as f64])
283            .rgba(0.0, 0.0, 0.0, self.settings.chat.chat_opacity)
284            .bottom_right_with_margins_on(ui.window, 40.0, 30.0)
285            .set(state.ids.subtitle_box_bg, ui);
286
287        let mut subtitles = state
288            .ids
289            .subtitle_message
290            .iter()
291            .zip(state.ids.subtitle_dir.iter())
292            .zip(
293                subtitles
294                    .subtitles
295                    .iter()
296                    .filter_map(|(localization, data)| {
297                        let data = data.last()?;
298                        data.position
299                            .is_none_or(|pos| {
300                                pos.distance_squared(listener_pos)
301                                    < MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
302                            })
303                            .then(|| (self.localized_strings.get_msg(localization), data))
304                    }),
305            );
306
307        if let Some(((id, dir_id), (message, data))) = subtitles.next() {
308            Text::new(&message)
309                .font_size(self.fonts.cyri.scale(14))
310                .font_id(self.fonts.cyri.conrod_id)
311                .parent(state.ids.subtitle_box_bg)
312                .center_justify()
313                .mid_bottom_with_margin_on(state.ids.subtitle_box_bg, 6.0)
314                .color(color(data))
315                .set(*id, ui);
316
317            dir(data, id, dir_id, ui);
318
319            let mut last_id = *id;
320            for ((id, dir_id), (message, data)) in subtitles {
321                Text::new(&message)
322                    .font_size(self.fonts.cyri.scale(14))
323                    .font_id(self.fonts.cyri.conrod_id)
324                    .parent(state.ids.subtitle_box_bg)
325                    .up_from(last_id, 8.0)
326                    .align_middle_x_of(last_id)
327                    .color(color(data))
328                    .set(*id, ui);
329
330                dir(data, id, dir_id, ui);
331
332                last_id = *id;
333            }
334        }
335    }
336}