Skip to main content

cotis_utils/
font_manager.rs

1//! Thread-safe font registry abstractions.
2//!
3//! This module underpins renderer-side font ownership. The API is functional but
4//! expected to evolve with the planned text/font refactor.
5use std::sync::{Arc, RwLock, RwLockReadGuard};
6
7/// Shared, thread-safe handle to a font storage implementation.
8pub struct FontManager<Font> {
9    font_data: Arc<RwLock<Box<dyn FontsHolder<Font>>>>,
10}
11
12/// Read guard over the underlying font holder.
13pub type FontsGuard<'a, Font> = RwLockReadGuard<'a, Box<dyn FontsHolder<Font>>>;
14
15impl<Font> FontManager<Font> {
16    /// Creates a new manager from a concrete font holder.
17    pub fn new<Holder: FontsHolder<Font> + 'static>(font_data: Holder) -> Self {
18        Self {
19            font_data: Arc::new(RwLock::new(Box::new(font_data))),
20        }
21    }
22
23    /// Leaks the manager and returns a `'static` reference to it.
24    ///
25    /// This is an advanced escape hatch for global callback-driven integrations.
26    /// Most applications should prefer regular owned values.
27    pub fn leak(self) -> &'static Self {
28        Box::leak(Box::new(self))
29    }
30    /// Returns read-only access to the underlying holder.
31    ///
32    /// # Panics
33    ///
34    /// Panics if the internal `RwLock` is poisoned.
35    pub fn get_fonts(&self) -> FontsGuard<'_, Font> {
36        self.font_data.read().unwrap()
37    }
38    /// Inserts a font and returns its slot index.
39    ///
40    /// # Panics
41    ///
42    /// Panics if the internal `RwLock` is poisoned.
43    pub fn push_font(&self, font: Font) -> usize {
44        self.font_data.write().unwrap().push_font(font)
45    }
46    /// Marks a font slot as free.
47    ///
48    /// # Panics
49    ///
50    /// Panics if the internal `RwLock` is poisoned.
51    pub fn delete_font(&self, index: usize) {
52        self.font_data.write().unwrap().delete_font(index);
53    }
54}
55
56/// Storage contract used by [`FontManager`].
57pub trait FontsHolder<Font> {
58    /// Returns all stored fonts.
59    fn get_fonts(&self) -> &[Font];
60    /// Stores a font and returns the slot index.
61    fn push_font(&mut self, font: Font) -> usize;
62    /// Marks the slot as removed or reusable.
63    fn delete_font(&mut self, index: usize);
64}
65
66/// Default in-memory font holder used by most integrations.
67pub struct GenericFontsHolder<Font> {
68    fonts: Vec<Font>,
69    free_fonts: Vec<usize>,
70}
71
72impl<Font> Default for GenericFontsHolder<Font> {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl<Font> GenericFontsHolder<Font> {
79    /// Creates an empty holder.
80    pub fn new() -> Self {
81        Self {
82            fonts: vec![],
83            free_fonts: vec![],
84        }
85    }
86}
87
88impl<Font> FontsHolder<Font> for GenericFontsHolder<Font> {
89    fn get_fonts(&self) -> &[Font] {
90        self.fonts.as_slice()
91    }
92
93    /// Inserts a font.
94    ///
95    /// If a free index exists, this implementation uses `Vec::insert` at that
96    /// index, otherwise it appends.
97    fn push_font(&mut self, font: Font) -> usize {
98        if let Some(index) = self.free_fonts.pop() {
99            self.fonts.insert(index, font);
100            index
101        } else {
102            self.fonts.push(font);
103            self.fonts.len() - 1
104        }
105    }
106
107    fn delete_font(&mut self, index: usize) {
108        self.free_fonts.push(index);
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn generic_fonts_holder_reuses_deleted_slots() {
118        let mut holder = GenericFontsHolder::new();
119        assert_eq!(holder.push_font("a"), 0);
120        assert_eq!(holder.push_font("b"), 1);
121        holder.delete_font(0);
122        assert_eq!(holder.push_font("c"), 0);
123        assert_eq!(holder.get_fonts()[0], "c");
124        assert_eq!(holder.get_fonts()[1], "a");
125        assert_eq!(holder.get_fonts()[2], "b");
126    }
127
128    #[test]
129    fn font_manager_round_trips_through_rwlock() {
130        let manager = FontManager::new(GenericFontsHolder::new());
131        assert_eq!(manager.push_font("font-a"), 0);
132        assert_eq!(manager.get_fonts().get_fonts()[0], "font-a");
133        manager.delete_font(0);
134        assert_eq!(manager.push_font("font-b"), 0);
135        assert_eq!(manager.get_fonts().get_fonts()[0], "font-b");
136    }
137}