Skip to main content

nightshade_text/
text.rs

1//! Shared text state and styling. The retained UI and the engine's 3D text both
2//! measure, wrap, and cache strings and describe their appearance with these
3//! types.
4
5use crate::text_data::{FontKind, TextAlignment, VerticalAlignment};
6
7/// The shaping context and the strings it is asked to shape. One instance is
8/// shared: the retained UI and the engine's 3D text both allocate slots in
9/// `cache`, and the renderer borrows `font_engine` to mesh glyphs.
10#[cfg(feature = "shaping")]
11#[derive(Default)]
12pub struct TextState {
13    pub cache: TextCache,
14    pub font_engine: crate::font_engine::FontEngine,
15}
16
17#[derive(Default)]
18pub struct TextCache {
19    pub text_strings: Vec<Option<String>>,
20    pub next_free_slot: usize,
21    pub slot_generations: Vec<u64>,
22    pub generation: u64,
23}
24
25impl TextCache {
26    pub fn new() -> Self {
27        Self {
28            text_strings: Vec::new(),
29            next_free_slot: 0,
30            slot_generations: Vec::new(),
31            generation: 0,
32        }
33    }
34
35    pub fn add_text(&mut self, content: impl Into<String>) -> usize {
36        let content = content.into();
37        self.generation += 1;
38
39        if self.next_free_slot < self.text_strings.len() {
40            let slot = self.next_free_slot;
41            self.text_strings[slot] = Some(content);
42            self.slot_generations[slot] += 1;
43            self.next_free_slot = self.find_next_free_slot();
44            slot
45        } else {
46            let slot = self.text_strings.len();
47            self.text_strings.push(Some(content));
48            self.slot_generations.push(1);
49            self.next_free_slot = slot + 1;
50            slot
51        }
52    }
53
54    pub fn get_text(&self, index: usize) -> Option<&str> {
55        self.text_strings.get(index).and_then(|opt| opt.as_deref())
56    }
57
58    /// Puts new content in a slot, answering whether the slot exists.
59    ///
60    /// Writing a slot the same thing it already holds is not a change. The
61    /// layout pass gates on this generation to decide whether any text moved
62    /// since the last frame, and bumping it regardless defeated that gate
63    /// outright: an interface that refreshes its labels every frame, which is
64    /// the ordinary way to write one, relaid the entire tree every frame
65    /// whether or not a single character differed.
66    pub fn set_text(&mut self, index: usize, content: impl Into<String>) -> bool {
67        let Some(text_opt) = self.text_strings.get_mut(index) else {
68            return false;
69        };
70        let content = content.into();
71        if text_opt.as_deref() == Some(content.as_str()) {
72            return true;
73        }
74        *text_opt = Some(content);
75        self.slot_generations[index] += 1;
76        self.generation += 1;
77        true
78    }
79
80    pub fn remove_text(&mut self, index: usize) -> bool {
81        if let Some(text_opt) = self.text_strings.get_mut(index) {
82            if text_opt.is_some() {
83                *text_opt = None;
84                self.slot_generations[index] += 1;
85                if index < self.next_free_slot {
86                    self.next_free_slot = index;
87                }
88                true
89            } else {
90                false
91            }
92        } else {
93            false
94        }
95    }
96
97    pub fn is_slot_valid(&self, index: usize) -> bool {
98        self.text_strings
99            .get(index)
100            .is_some_and(|opt| opt.is_some())
101    }
102
103    pub fn get_all_text_with_indices(&self) -> Vec<(usize, &str)> {
104        self.text_strings
105            .iter()
106            .enumerate()
107            .filter_map(|(i, opt)| opt.as_deref().map(|s| (i, s)))
108            .collect()
109    }
110
111    pub fn get_valid_slots(&self) -> Vec<usize> {
112        self.text_strings
113            .iter()
114            .enumerate()
115            .filter_map(|(i, opt)| if opt.is_some() { Some(i) } else { None })
116            .collect()
117    }
118
119    pub fn compact(&mut self) -> Vec<(usize, usize)> {
120        let mut new_strings = Vec::new();
121        let mut new_generations = Vec::new();
122        let mut index_mapping = Vec::new();
123
124        for (old_index, opt) in self.text_strings.iter().enumerate() {
125            if let Some(text) = opt {
126                index_mapping.push((old_index, new_strings.len()));
127                new_strings.push(Some(text.clone()));
128                new_generations.push(self.slot_generations.get(old_index).copied().unwrap_or(1));
129            }
130        }
131
132        self.text_strings = new_strings;
133        self.slot_generations = new_generations;
134        self.next_free_slot = self.text_strings.len();
135
136        index_mapping
137    }
138
139    fn find_next_free_slot(&self) -> usize {
140        for (i, opt) in self.text_strings.iter().enumerate() {
141            if opt.is_none() {
142                return i;
143            }
144        }
145        self.text_strings.len()
146    }
147}
148
149/// Text styling shared by the engine's 3D text and the retained UI: size,
150/// color, alignment, and outline.
151#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, enum2schema::Schema)]
152pub struct TextProperties {
153    pub font_size: f32,
154    pub color: nalgebra_glm::Vec4,
155    pub alignment: TextAlignment,
156    pub vertical_alignment: VerticalAlignment,
157    pub line_height: f32,
158    pub letter_spacing: f32,
159    pub outline_width: f32,
160    pub outline_color: nalgebra_glm::Vec4,
161    pub smoothing: f32,
162    pub monospace_width: Option<f32>,
163    pub anchor_character: Option<usize>,
164    pub font_kind: FontKind,
165}
166
167impl Default for TextProperties {
168    fn default() -> Self {
169        Self {
170            font_size: 16.0,
171            color: nalgebra_glm::Vec4::new(1.0, 1.0, 1.0, 1.0),
172            alignment: TextAlignment::Left,
173            vertical_alignment: VerticalAlignment::Baseline,
174            line_height: 1.2,
175            letter_spacing: 0.0,
176            outline_width: 0.0,
177            outline_color: nalgebra_glm::Vec4::new(0.0, 0.0, 0.0, 1.0),
178            smoothing: 0.003,
179            monospace_width: None,
180            anchor_character: None,
181            font_kind: FontKind::Default,
182        }
183    }
184}