1use onedrop_parser::MessageDef;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone)]
25pub struct MessageRenderInstance {
26 pub text: String,
27 pub font: u32,
28 pub size_px: f32,
29 pub x: f32,
31 pub y: f32,
32 pub rgba: [f32; 4],
35 pub burn: bool,
41}
42
43#[derive(Debug, Clone)]
45struct ActiveMessage {
46 def_index: usize,
51 spawned_at: f32,
54 text_override: Option<String>,
58}
59
60pub struct MessageManager {
62 defs: Vec<MessageDef>,
63 slot_to_index: HashMap<u32, usize>,
64 active: Vec<ActiveMessage>,
65 cycle_cursor: usize,
67 transient_default: MessageDef,
71}
72
73impl Default for MessageManager {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl MessageManager {
80 pub fn new() -> Self {
81 Self {
82 defs: Vec::new(),
83 slot_to_index: HashMap::new(),
84 active: Vec::new(),
85 cycle_cursor: 0,
86 transient_default: MessageDef {
87 slot: 0,
88 text: String::new(),
89 font: 0,
90 size: 40.0,
91 x: 0.5,
92 y: 0.92,
93 r: 255,
94 g: 255,
95 b: 255,
96 bold: false,
97 italic: false,
98 fade_in_s: 0.4,
99 fade_out_s: 0.6,
100 duration_s: 3.5,
101 burn: false,
102 },
103 }
104 }
105
106 pub fn load_defs(&mut self, defs: Vec<MessageDef>) {
109 self.slot_to_index.clear();
110 for (i, d) in defs.iter().enumerate() {
111 self.slot_to_index.insert(d.slot, i);
112 }
113 self.defs = defs;
114 self.cycle_cursor = 0;
115 }
116
117 pub fn def_count(&self) -> usize {
118 self.defs.len()
119 }
120
121 pub fn active_count(&self) -> usize {
122 self.active.len()
123 }
124
125 pub fn spawn_slot(&mut self, slot: u32, time: f32) -> bool {
128 let Some(&idx) = self.slot_to_index.get(&slot) else {
129 return false;
130 };
131 self.active.push(ActiveMessage {
132 def_index: idx,
133 spawned_at: time,
134 text_override: None,
135 });
136 true
137 }
138
139 pub fn cycle_next(&mut self, time: f32) -> Option<u32> {
141 if self.defs.is_empty() {
142 return None;
143 }
144 let idx = self.cycle_cursor % self.defs.len();
145 let slot = self.defs[idx].slot;
146 self.cycle_cursor = (idx + 1) % self.defs.len();
147 self.active.push(ActiveMessage {
148 def_index: idx,
149 spawned_at: time,
150 text_override: None,
151 });
152 Some(slot)
153 }
154
155 pub fn clear(&mut self) {
157 self.active.clear();
158 }
159
160 pub fn show_transient(&mut self, text: impl Into<String>, time: f32) {
166 let text = text.into();
167 if text.is_empty() {
168 return;
169 }
170 let (def_index, _styling) = if let Some(def) = self.defs.first() {
175 (Some(0_usize), def)
176 } else {
177 (None, &self.transient_default)
178 };
179 match def_index {
180 Some(idx) => self.active.push(ActiveMessage {
181 def_index: idx,
182 spawned_at: time,
183 text_override: Some(text),
184 }),
185 None => {
186 let mut def = self.transient_default.clone();
191 def.text = text;
192 self.defs.insert(0, def);
193 self.slot_to_index.clear();
194 for (i, d) in self.defs.iter().enumerate() {
195 self.slot_to_index.insert(d.slot, i);
196 }
197 self.active.push(ActiveMessage {
198 def_index: 0,
199 spawned_at: time,
200 text_override: None,
201 });
202 }
203 }
204 }
205
206 pub fn tick(&mut self, time: f32) -> Vec<MessageRenderInstance> {
211 let mut out = Vec::with_capacity(self.active.len());
212 let mut keep = Vec::with_capacity(self.active.len());
213 for m in self.active.drain(..) {
214 let Some(def) = self.defs.get(m.def_index) else {
215 continue;
216 };
217 let age = (time - m.spawned_at).max(0.0);
218 let fade_in = def.fade_in_s.max(0.0);
219 let sustain = def.duration_s.max(0.0);
220 let fade_out = def.fade_out_s.max(0.0);
221 let total = fade_in + sustain + fade_out;
222 if age >= total && !def.burn {
223 continue;
224 }
225 let mut burn_emit = false;
226 let alpha = if age < fade_in {
227 if fade_in > 0.0 { age / fade_in } else { 1.0 }
228 } else if age < fade_in + sustain {
229 1.0
230 } else if age < total {
231 let t = (age - fade_in - sustain) / fade_out.max(1e-6);
232 (1.0 - t).clamp(0.0, 1.0)
233 } else {
234 burn_emit = true;
237 0.0
238 };
239 let text = m.text_override.clone().unwrap_or_else(|| def.text.clone());
240 if text.is_empty() {
241 continue;
242 }
243 let rgba = [
244 def.r as f32 / 255.0,
245 def.g as f32 / 255.0,
246 def.b as f32 / 255.0,
247 alpha,
248 ];
249 out.push(MessageRenderInstance {
250 text,
251 font: def.font,
252 size_px: def.size,
253 x: def.x,
254 y: def.y,
255 rgba,
256 burn: burn_emit,
257 });
258 if !burn_emit {
259 keep.push(m);
260 }
261 }
262 self.active = keep;
263 out
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn def(slot: u32, text: &str, fade_in: f32, dur: f32, fade_out: f32) -> MessageDef {
272 MessageDef {
273 slot,
274 text: text.into(),
275 fade_in_s: fade_in,
276 duration_s: dur,
277 fade_out_s: fade_out,
278 ..Default::default()
279 }
280 }
281
282 #[test]
283 fn spawn_then_fade_in_full_alpha_at_end() {
284 let mut mgr = MessageManager::new();
285 mgr.load_defs(vec![def(1, "hi", 1.0, 2.0, 1.0)]);
286 mgr.spawn_slot(1, 0.0);
287 let r = mgr.tick(0.5);
289 assert_eq!(r.len(), 1);
290 assert!((r[0].rgba[3] - 0.5).abs() < 1e-3);
291 let r = mgr.tick(2.0);
293 assert!((r[0].rgba[3] - 1.0).abs() < 1e-3);
294 let r = mgr.tick(3.5);
296 assert!((r[0].rgba[3] - 0.5).abs() < 1e-3);
297 let r = mgr.tick(5.0);
299 assert!(r.is_empty());
300 assert_eq!(mgr.active_count(), 0);
301 }
302
303 #[test]
304 fn cycle_walks_in_order() {
305 let mut mgr = MessageManager::new();
306 mgr.load_defs(vec![def(1, "a", 0.0, 1.0, 0.0), def(2, "b", 0.0, 1.0, 0.0)]);
307 assert_eq!(mgr.cycle_next(0.0), Some(1));
308 assert_eq!(mgr.cycle_next(0.0), Some(2));
309 assert_eq!(mgr.cycle_next(0.0), Some(1));
310 }
311
312 #[test]
313 fn clear_drops_everything() {
314 let mut mgr = MessageManager::new();
315 mgr.load_defs(vec![def(1, "a", 0.0, 1.0, 0.0)]);
316 mgr.spawn_slot(1, 0.0);
317 mgr.spawn_slot(1, 0.0);
318 assert_eq!(mgr.active_count(), 2);
319 mgr.clear();
320 assert_eq!(mgr.active_count(), 0);
321 }
322
323 #[test]
324 fn transient_works_without_any_defs() {
325 let mut mgr = MessageManager::new();
326 mgr.show_transient("Now playing: foo", 0.0);
327 assert_eq!(mgr.active_count(), 1);
328 let r = mgr.tick(0.5);
329 assert_eq!(r.len(), 1);
330 assert_eq!(r[0].text, "Now playing: foo");
331 }
332
333 #[test]
334 fn transient_overrides_first_def_text_only() {
335 let mut mgr = MessageManager::new();
336 mgr.load_defs(vec![def(1, "authored", 0.0, 10.0, 0.0)]);
337 mgr.show_transient("dynamic", 0.0);
338 let r = mgr.tick(0.0);
339 assert_eq!(r[0].text, "dynamic");
340 mgr.spawn_slot(1, 1.0);
342 let r = mgr.tick(1.5);
343 assert!(r.iter().any(|i| i.text == "authored"));
345 assert!(r.iter().any(|i| i.text == "dynamic"));
346 }
347}