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 pub position: Option<Vec3<f32>>,
70 pub show_for: f64,
72}
73
74#[derive(Clone, PartialEq)]
75struct SubtitleData {
76 position: Option<Vec3<f32>>,
77 show_until: f64,
79}
80
81impl SubtitleData {
82 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 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 if let Some((i, s)) = data
147 .iter()
148 .enumerate()
149 .max_by(|(_, a), (_, b)| a.compare_priority(b, listener_pos))
150 {
151 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 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 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 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}