1#![allow(dead_code)]
8
9use std::collections::HashMap;
10
11const FONT5X7: [[u8; 7]; 95] = [
19 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
21 [0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00],
23 [0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00],
25 [0x0A, 0x1F, 0x0A, 0x0A, 0x1F, 0x0A, 0x00],
27 [0x04, 0x0F, 0x14, 0x0E, 0x05, 0x1E, 0x04],
29 [0x18, 0x19, 0x02, 0x04, 0x13, 0x03, 0x00],
31 [0x08, 0x14, 0x08, 0x15, 0x12, 0x0D, 0x00],
33 [0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
35 [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
37 [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
39 [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00],
41 [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
43 [0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08],
45 [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
47 [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00],
49 [0x01, 0x02, 0x02, 0x04, 0x08, 0x10, 0x00],
51 [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
53 [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
55 [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
57 [0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E],
59 [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
61 [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
63 [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E],
65 [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
67 [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
69 [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
71 [0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00],
73 [0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08],
75 [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02],
77 [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00],
79 [0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10],
81 [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
83 [0x0E, 0x11, 0x01, 0x0D, 0x15, 0x15, 0x0E],
85 [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
87 [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
89 [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
91 [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C],
93 [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
95 [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
97 [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F],
99 [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
101 [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
103 [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C],
105 [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
107 [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
109 [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
111 [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
113 [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
115 [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
117 [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
119 [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
121 [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
123 [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
125 [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
127 [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
129 [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11],
131 [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
133 [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
135 [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
137 [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
139 [0x10, 0x08, 0x08, 0x04, 0x02, 0x01, 0x00],
141 [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
143 [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00],
145 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
147 [0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
149 [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F],
151 [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E],
153 [0x00, 0x00, 0x0E, 0x10, 0x10, 0x10, 0x0E],
155 [0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F],
157 [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E],
159 [0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08],
161 [0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x0E],
163 [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11],
165 [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E],
167 [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C],
169 [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12],
171 [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
173 [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11],
175 [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11],
177 [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E],
179 [0x00, 0x00, 0x1E, 0x11, 0x1E, 0x10, 0x10],
181 [0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x01],
183 [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10],
185 [0x00, 0x00, 0x0E, 0x10, 0x0E, 0x01, 0x1E],
187 [0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06],
189 [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D],
191 [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04],
193 [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A],
195 [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11],
197 [0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E],
199 [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F],
201 [0x03, 0x04, 0x04, 0x18, 0x04, 0x04, 0x03],
203 [0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04],
205 [0x18, 0x04, 0x04, 0x03, 0x04, 0x04, 0x18],
207 [0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00],
209];
210
211pub const GLYPH_W: u32 = 5;
213pub const GLYPH_H: u32 = 7;
215pub const GLYPH_GAP: u32 = 1;
217
218fn glyph(ch: char) -> [u8; 7] {
220 let code = ch as u32;
221 if code >= 0x20 && code <= 0x7E {
222 FONT5X7[(code - 0x20) as usize]
223 } else {
224 FONT5X7[('?' as u32 - 0x20) as usize]
225 }
226}
227
228fn glyph_pixel(bitmap: [u8; 7], col: u32, row: u32) -> bool {
231 if col >= GLYPH_W || row >= GLYPH_H {
232 return false;
233 }
234 (bitmap[row as usize] >> (4 - col)) & 1 == 1
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct Rgba {
245 pub r: u8,
247 pub g: u8,
249 pub b: u8,
251 pub a: u8,
253}
254
255impl Rgba {
256 #[must_use]
258 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
259 Self { r, g, b, a }
260 }
261
262 #[must_use]
264 pub const fn white() -> Self {
265 Self::new(255, 255, 255, 255)
266 }
267
268 #[must_use]
270 pub const fn black() -> Self {
271 Self::new(0, 0, 0, 255)
272 }
273
274 #[must_use]
276 pub const fn transparent() -> Self {
277 Self::new(0, 0, 0, 0)
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum HAlign {
284 Left,
286 Center,
288 Right,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum VAlign {
295 Top,
297 Middle,
299 Bottom,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum FontScale {
308 One,
310 Two,
312 Three,
314 Four,
316}
317
318impl FontScale {
319 #[must_use]
321 pub fn factor(self) -> u32 {
322 match self {
323 Self::One => 1,
324 Self::Two => 2,
325 Self::Three => 3,
326 Self::Four => 4,
327 }
328 }
329}
330
331#[derive(Debug, Clone)]
333pub struct TextStyle {
334 pub color: Rgba,
336 pub background: Option<Rgba>,
338 pub shadow: Option<Rgba>,
340 pub scale: FontScale,
342 pub halign: HAlign,
344 pub valign: VAlign,
346 pub padding: u32,
348}
349
350impl Default for TextStyle {
351 fn default() -> Self {
352 Self {
353 color: Rgba::white(),
354 background: None,
355 shadow: Some(Rgba::new(0, 0, 0, 180)),
356 scale: FontScale::Two,
357 halign: HAlign::Center,
358 valign: VAlign::Bottom,
359 padding: 8,
360 }
361 }
362}
363
364pub type OverlayId = u64;
370
371#[derive(Debug, Clone)]
376pub struct TitleOverlay {
377 pub id: OverlayId,
379 pub text: String,
381 pub x: i32,
383 pub y: i32,
385 pub width: u32,
387 pub height: u32,
389 pub style: TextStyle,
391 pub start: i64,
393 pub end: i64,
395 pub opacity: f32,
397 pub position_keyframes: Vec<(i64, i32, i32)>,
399}
400
401impl TitleOverlay {
402 #[must_use]
404 pub fn new(
405 id: OverlayId,
406 text: impl Into<String>,
407 x: i32,
408 y: i32,
409 start: i64,
410 end: i64,
411 ) -> Self {
412 Self {
413 id,
414 text: text.into(),
415 x,
416 y,
417 width: 0,
418 height: 0,
419 style: TextStyle::default(),
420 start,
421 end,
422 opacity: 1.0,
423 position_keyframes: Vec::new(),
424 }
425 }
426
427 #[must_use]
429 pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
430 self.width = width;
431 self.height = height;
432 self
433 }
434
435 #[must_use]
437 pub fn with_style(mut self, style: TextStyle) -> Self {
438 self.style = style;
439 self
440 }
441
442 #[must_use]
444 pub fn with_opacity(mut self, opacity: f32) -> Self {
445 self.opacity = opacity.clamp(0.0, 1.0);
446 self
447 }
448
449 pub fn add_position_keyframe(&mut self, time: i64, x: i32, y: i32) {
451 self.position_keyframes.push((time, x, y));
452 self.position_keyframes.sort_by_key(|&(t, _, _)| t);
453 }
454
455 #[allow(clippy::cast_precision_loss)]
461 #[must_use]
462 pub fn position_at(&self, time: i64) -> (i32, i32) {
463 if self.position_keyframes.is_empty() {
464 return (self.x, self.y);
465 }
466 let kf = &self.position_keyframes;
467 if time <= kf[0].0 {
468 return (kf[0].1, kf[0].2);
469 }
470 let last = kf[kf.len() - 1];
471 if time >= last.0 {
472 return (last.1, last.2);
473 }
474 let idx = kf.partition_point(|&(t, _, _)| t <= time) - 1;
476 let (t0, x0, y0) = kf[idx];
477 let (t1, x1, y1) = kf[idx + 1];
478 let span = (t1 - t0) as f64;
479 let alpha = if span > 0.0 {
480 (time - t0) as f64 / span
481 } else {
482 0.0
483 };
484 let ix = (x0 as f64 + alpha * (x1 - x0) as f64).round() as i32;
485 let iy = (y0 as f64 + alpha * (y1 - y0) as f64).round() as i32;
486 (ix, iy)
487 }
488
489 #[must_use]
491 pub fn is_active_at(&self, time: i64) -> bool {
492 time >= self.start && time < self.end
493 }
494
495 #[must_use]
497 pub fn text_width_px(&self) -> u32 {
498 let n = self.text.chars().count() as u32;
499 if n == 0 {
500 0
501 } else {
502 n * (GLYPH_W + GLYPH_GAP) - GLYPH_GAP
503 }
504 }
505
506 #[must_use]
508 pub fn text_height_px(&self) -> u32 {
509 GLYPH_H
510 }
511}
512
513pub struct OverlayRenderer {
522 pub frame_width: u32,
524 pub frame_height: u32,
526}
527
528impl OverlayRenderer {
529 #[must_use]
531 pub fn new(frame_width: u32, frame_height: u32) -> Self {
532 Self {
533 frame_width,
534 frame_height,
535 }
536 }
537
538 pub fn composite(&self, buffer: &mut [u8], overlay: &TitleOverlay, time: i64) {
543 if !overlay.is_active_at(time) {
544 return;
545 }
546 let scale = overlay.style.scale.factor();
547 let glyph_w_s = (GLYPH_W + GLYPH_GAP) * scale;
548 let glyph_h_s = GLYPH_H * scale;
549 let text_w = overlay.text_width_px() * scale;
550 let text_h = glyph_h_s;
551
552 let (ox, oy) = overlay.position_at(time);
553 let padding = overlay.style.padding;
554
555 let bbox_w = if overlay.width > 0 {
556 overlay.width
557 } else {
558 text_w + padding * 2
559 };
560 let bbox_h = if overlay.height > 0 {
561 overlay.height
562 } else {
563 text_h + padding * 2
564 };
565
566 if let Some(bg) = overlay.style.background {
568 self.fill_rect(buffer, ox, oy, bbox_w, bbox_h, bg, overlay.opacity);
569 }
570
571 let text_start_x = match overlay.style.halign {
573 HAlign::Left => ox + padding as i32,
574 HAlign::Center => ox + (bbox_w.saturating_sub(text_w) / 2) as i32,
575 HAlign::Right => ox + bbox_w as i32 - text_w as i32 - padding as i32,
576 };
577 let text_start_y = match overlay.style.valign {
578 VAlign::Top => oy + padding as i32,
579 VAlign::Middle => oy + (bbox_h.saturating_sub(text_h) / 2) as i32,
580 VAlign::Bottom => oy + bbox_h as i32 - text_h as i32 - padding as i32,
581 };
582
583 let mut cursor_x = text_start_x;
585 for ch in overlay.text.chars() {
586 let bitmap = glyph(ch);
587 if let Some(shadow) = overlay.style.shadow {
589 self.draw_glyph(
590 buffer,
591 bitmap,
592 cursor_x + 1,
593 text_start_y + 1,
594 scale,
595 shadow,
596 overlay.opacity,
597 );
598 }
599 self.draw_glyph(
601 buffer,
602 bitmap,
603 cursor_x,
604 text_start_y,
605 scale,
606 overlay.style.color,
607 overlay.opacity,
608 );
609 cursor_x += glyph_w_s as i32;
610 }
611 }
612
613 fn fill_rect(
615 &self,
616 buffer: &mut [u8],
617 x: i32,
618 y: i32,
619 w: u32,
620 h: u32,
621 color: Rgba,
622 opacity: f32,
623 ) {
624 for row in 0..h {
625 for col in 0..w {
626 let px = x + col as i32;
627 let py = y + row as i32;
628 self.blend_pixel(buffer, px, py, color, opacity);
629 }
630 }
631 }
632
633 fn draw_glyph(
635 &self,
636 buffer: &mut [u8],
637 bitmap: [u8; 7],
638 x: i32,
639 y: i32,
640 scale: u32,
641 color: Rgba,
642 opacity: f32,
643 ) {
644 for row in 0..GLYPH_H {
645 for col in 0..GLYPH_W {
646 if !glyph_pixel(bitmap, col, row) {
647 continue;
648 }
649 for sy in 0..scale {
650 for sx in 0..scale {
651 let px = x + (col * scale + sx) as i32;
652 let py = y + (row * scale + sy) as i32;
653 self.blend_pixel(buffer, px, py, color, opacity);
654 }
655 }
656 }
657 }
658 }
659
660 #[allow(
662 clippy::cast_possible_truncation,
663 clippy::cast_sign_loss,
664 clippy::cast_precision_loss
665 )]
666 fn blend_pixel(&self, buffer: &mut [u8], x: i32, y: i32, color: Rgba, opacity: f32) {
667 if x < 0 || y < 0 {
668 return;
669 }
670 let px = x as u32;
671 let py = y as u32;
672 if px >= self.frame_width || py >= self.frame_height {
673 return;
674 }
675 let idx = ((py * self.frame_width + px) * 4) as usize;
676 if idx + 3 >= buffer.len() {
677 return;
678 }
679 let src_a = (color.a as f32 / 255.0) * opacity;
680 let dst_a = 1.0 - src_a;
681 buffer[idx] = (color.r as f32 * src_a + buffer[idx] as f32 * dst_a).round() as u8;
682 buffer[idx + 1] = (color.g as f32 * src_a + buffer[idx + 1] as f32 * dst_a).round() as u8;
683 buffer[idx + 2] = (color.b as f32 * src_a + buffer[idx + 2] as f32 * dst_a).round() as u8;
684 buffer[idx + 3] = (src_a * 255.0 + buffer[idx + 3] as f32 * dst_a).round() as u8;
685 }
686}
687
688#[derive(Debug, Default)]
694pub struct OverlayManager {
695 overlays: HashMap<OverlayId, TitleOverlay>,
696 next_id: OverlayId,
697}
698
699impl OverlayManager {
700 #[must_use]
702 pub fn new() -> Self {
703 Self {
704 overlays: HashMap::new(),
705 next_id: 1,
706 }
707 }
708
709 pub fn add(&mut self, mut overlay: TitleOverlay) -> OverlayId {
711 let id = self.next_id;
712 self.next_id += 1;
713 overlay.id = id;
714 self.overlays.insert(id, overlay);
715 id
716 }
717
718 pub fn remove(&mut self, id: OverlayId) -> Option<TitleOverlay> {
720 self.overlays.remove(&id)
721 }
722
723 #[must_use]
725 pub fn get(&self, id: OverlayId) -> Option<&TitleOverlay> {
726 self.overlays.get(&id)
727 }
728
729 pub fn get_mut(&mut self, id: OverlayId) -> Option<&mut TitleOverlay> {
731 self.overlays.get_mut(&id)
732 }
733
734 #[must_use]
736 pub fn active_at(&self, time: i64) -> Vec<&TitleOverlay> {
737 let mut active: Vec<&TitleOverlay> = self
738 .overlays
739 .values()
740 .filter(|o| o.is_active_at(time))
741 .collect();
742 active.sort_by_key(|o| o.id);
743 active
744 }
745
746 #[must_use]
748 pub fn len(&self) -> usize {
749 self.overlays.len()
750 }
751
752 #[must_use]
754 pub fn is_empty(&self) -> bool {
755 self.overlays.is_empty()
756 }
757}
758
759#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
768 fn test_glyph_pixel_space_is_blank() {
769 let bm = glyph(' ');
770 for row in 0..GLYPH_H {
771 for col in 0..GLYPH_W {
772 assert!(!glyph_pixel(bm, col, row), "space should be blank");
773 }
774 }
775 }
776
777 #[test]
778 fn test_glyph_pixel_uppercase_a_has_pixels() {
779 let bm = glyph('A');
780 let set_count: u32 = (0..GLYPH_H)
781 .flat_map(|row| (0..GLYPH_W).map(move |col| (row, col)))
782 .filter(|&(r, c)| glyph_pixel(bm, c, r))
783 .count() as u32;
784 assert!(
785 set_count > 5,
786 "A should have many set pixels, got {set_count}"
787 );
788 }
789
790 #[test]
791 fn test_glyph_unknown_char_returns_question_mark() {
792 let bm_q = glyph('?');
793 let bm_unknown = glyph('\x01');
794 assert_eq!(bm_q, bm_unknown);
795 }
796
797 #[test]
798 fn test_title_overlay_is_active_at() {
799 let o = TitleOverlay::new(1, "Hello", 0, 0, 1000, 5000);
800 assert!(o.is_active_at(1000));
801 assert!(o.is_active_at(4999));
802 assert!(!o.is_active_at(5000));
803 assert!(!o.is_active_at(999));
804 }
805
806 #[test]
807 fn test_title_overlay_text_width() {
808 let o = TitleOverlay::new(1, "AB", 0, 0, 0, 1000);
809 assert_eq!(o.text_width_px(), 11);
811 }
812
813 #[test]
814 fn test_title_overlay_text_width_empty() {
815 let o = TitleOverlay::new(1, "", 0, 0, 0, 1000);
816 assert_eq!(o.text_width_px(), 0);
817 }
818
819 #[test]
820 fn test_position_keyframes_interpolation() {
821 let mut o = TitleOverlay::new(1, "Hi", 0, 0, 0, 10000);
822 o.add_position_keyframe(0, 0, 100);
823 o.add_position_keyframe(1000, 200, 100);
824 let (x, y) = o.position_at(500);
825 assert_eq!(x, 100);
826 assert_eq!(y, 100);
827 }
828
829 #[test]
830 fn test_position_keyframes_before_first() {
831 let mut o = TitleOverlay::new(1, "Hi", 0, 0, 0, 10000);
832 o.add_position_keyframe(500, 10, 20);
833 let pos = o.position_at(0);
834 assert_eq!(pos, (10, 20));
835 }
836
837 #[test]
838 fn test_position_no_keyframes_returns_static() {
839 let o = TitleOverlay::new(1, "Hi", 42, 99, 0, 1000);
840 assert_eq!(o.position_at(500), (42, 99));
841 }
842
843 #[test]
844 fn test_overlay_renderer_composites_white_pixel() {
845 let width = 32u32;
846 let height = 32u32;
847 let mut buf = vec![0u8; (width * height * 4) as usize];
848 let renderer = OverlayRenderer::new(width, height);
849 let overlay = TitleOverlay::new(1, "A", 0, 0, 0, 1000).with_style(TextStyle {
850 color: Rgba::white(),
851 background: None,
852 shadow: None,
853 scale: FontScale::One,
854 padding: 0,
855 ..TextStyle::default()
856 });
857 renderer.composite(&mut buf, &overlay, 0);
858 let any_set = buf.iter().any(|&b| b > 0);
860 assert!(any_set, "Expected rendered pixels after compositing 'A'");
861 }
862
863 #[test]
864 fn test_overlay_renderer_inactive_not_rendered() {
865 let width = 32u32;
866 let height = 32u32;
867 let mut buf = vec![0u8; (width * height * 4) as usize];
868 let renderer = OverlayRenderer::new(width, height);
869 let overlay = TitleOverlay::new(1, "A", 0, 0, 5000, 10000);
870 renderer.composite(&mut buf, &overlay, 0);
871 assert!(buf.iter().all(|&b| b == 0));
873 }
874
875 #[test]
876 fn test_overlay_manager_add_remove() {
877 let mut mgr = OverlayManager::new();
878 let o = TitleOverlay::new(0, "Test", 0, 0, 0, 1000);
879 let id = mgr.add(o);
880 assert_eq!(mgr.len(), 1);
881 assert!(mgr.get(id).is_some());
882 assert!(mgr.remove(id).is_some());
883 assert!(mgr.is_empty());
884 }
885
886 #[test]
887 fn test_overlay_manager_active_at() {
888 let mut mgr = OverlayManager::new();
889 let o1 = TitleOverlay::new(0, "A", 0, 0, 0, 1000);
890 let o2 = TitleOverlay::new(0, "B", 0, 50, 500, 2000);
891 mgr.add(o1);
892 mgr.add(o2);
893 let active = mgr.active_at(600);
894 assert_eq!(active.len(), 2);
895 let active_early = mgr.active_at(200);
896 assert_eq!(active_early.len(), 1);
897 }
898
899 #[test]
900 fn test_rgba_white_black() {
901 let w = Rgba::white();
902 assert_eq!((w.r, w.g, w.b, w.a), (255, 255, 255, 255));
903 let b = Rgba::black();
904 assert_eq!((b.r, b.g, b.b, b.a), (0, 0, 0, 255));
905 }
906
907 #[test]
908 fn test_font_scale_factors() {
909 assert_eq!(FontScale::One.factor(), 1);
910 assert_eq!(FontScale::Two.factor(), 2);
911 assert_eq!(FontScale::Three.factor(), 3);
912 assert_eq!(FontScale::Four.factor(), 4);
913 }
914
915 #[test]
916 fn test_background_fills_buffer() {
917 let w = 20u32;
918 let h = 20u32;
919 let mut buf = vec![0u8; (w * h * 4) as usize];
920 let renderer = OverlayRenderer::new(w, h);
921 let style = TextStyle {
922 background: Some(Rgba::new(255, 0, 0, 255)),
923 color: Rgba::transparent(),
924 shadow: None,
925 scale: FontScale::One,
926 halign: HAlign::Left,
927 valign: VAlign::Top,
928 padding: 0,
929 };
930 let overlay = TitleOverlay::new(1, " ", 0, 0, 0, 1000).with_style(style);
931 renderer.composite(&mut buf, &overlay, 500);
932 let has_red = buf.chunks(4).any(|p| p[0] > 0);
934 assert!(has_red);
935 }
936}