1use csscolorparser::Color;
4use widestring::{U16Str, U16String};
5
6use crate::legacy::{
7 bidi::{Char16, StyledText},
8 font_stack::{FontStack, FontStackHash, FontStackHasher},
9 util::{
10 i18n,
11 i18n::{BACKSLACK_F, BACKSLACK_V},
12 },
13};
14
15#[derive(Clone, Default)]
17pub struct SectionOptions {
18 pub scale: f64,
19 pub font_stack_hash: FontStackHash,
20 pub font_stack: FontStack,
21 pub text_color: Option<Color>,
22 pub image_id: Option<String>,
23}
24impl SectionOptions {
25 pub fn from_image_id(image_id: String) -> Self {
27 Self {
28 scale: 1.0,
29 image_id: Some(image_id),
30 ..SectionOptions::default()
31 }
32 }
33 pub fn new(scale: f64, font_stack: FontStack, text_color: Option<Color>) -> Self {
35 Self {
36 scale,
37 font_stack_hash: FontStackHasher::new(&font_stack),
38 font_stack,
39 text_color,
40 image_id: None,
41 }
42 }
43}
44
45const PUABEGIN: Char16 = '\u{E000}' as Char16;
46const PUAEND: Char16 = '\u{F8FF}' as Char16;
47
48#[derive(Clone)]
65pub struct TaggedString {
66 pub styled_text: StyledText,
67 pub sections: Vec<SectionOptions>,
68 pub supports_vertical_writing_mode: Option<bool>,
69 pub image_section_id: Char16,
72}
73
74impl Default for TaggedString {
75 fn default() -> Self {
78 Self {
79 styled_text: (U16String::new(), vec![]), sections: vec![],
81 supports_vertical_writing_mode: None,
82 image_section_id: 0 as Char16, }
84 }
85}
86
87impl TaggedString {
88 pub fn new_from_raw(text_: U16String, options: SectionOptions) -> Self {
90 let text_len = text_.len();
91 Self {
92 styled_text: (text_, vec![0; text_len]), sections: vec![options],
94 supports_vertical_writing_mode: None,
95 image_section_id: 0 as Char16, }
97 }
98
99 pub fn new(styled_text: StyledText, sections_: Vec<SectionOptions>) -> Self {
101 Self {
102 styled_text,
103 sections: sections_,
104 supports_vertical_writing_mode: None,
105 image_section_id: 0 as Char16, }
107 }
108
109 pub fn length(&self) -> usize {
111 self.styled_text.0.len()
112 }
113
114 pub fn section_count(&self) -> usize {
116 self.sections.len()
117 }
118
119 pub fn empty(&self) -> bool {
121 self.styled_text.0.is_empty()
122 }
123
124 pub fn get_section(&self, index: usize) -> &SectionOptions {
126 &self.sections[self.styled_text.1[index] as usize] }
128
129 pub fn get_char_code_at(&self, index: usize) -> u16 {
131 return self.styled_text.0.as_slice()[index];
132 }
133
134 pub fn raw_text(&self) -> &U16String {
136 &self.styled_text.0
137 }
138
139 pub fn get_styled_text(&self) -> &StyledText {
141 &self.styled_text
142 }
143
144 pub fn add_text_section(
146 &mut self,
147 section_text: &U16String,
148 scale: f64,
149 font_stack: FontStack,
150 text_color: Option<Color>,
151 ) {
152 self.styled_text.0.push(section_text);
153 self.sections
154 .push(SectionOptions::new(scale, font_stack, text_color));
155 self.styled_text
156 .1
157 .resize(self.styled_text.0.len(), (self.sections.len() - 1) as u8);
158 self.supports_vertical_writing_mode = None;
159 }
160
161 pub fn add_image_section(&mut self, image_id: String) {
163 let next_image_section_char_code = self.get_next_image_section_char_code();
164
165 if let Some(next_image_section_char_code) = next_image_section_char_code {
166 self.styled_text
167 .0
168 .push(U16Str::from_slice(&[next_image_section_char_code])); self.sections.push(SectionOptions::from_image_id(image_id));
170 self.styled_text
171 .1
172 .resize(self.styled_text.0.len(), (self.sections.len() - 1) as u8);
173 } else {
174 log::warn!("Exceeded maximum number of images in a label.");
175 }
176 }
177
178 pub fn section_at(&self, index: usize) -> &SectionOptions {
180 &self.sections[index]
181 }
182
183 pub fn get_sections(&self) -> &Vec<SectionOptions> {
185 &self.sections
186 }
187
188 pub fn get_section_index(&self, character_index: usize) -> u8 {
190 self.styled_text.1[character_index] }
192
193 pub fn get_max_scale(&self) -> f64 {
195 let mut max_scale: f64 = 0.0;
196 for i in 0..self.styled_text.0.len() {
197 max_scale = max_scale.max(self.get_section(i).scale)
198 }
199 max_scale
200 }
201
202 const WHITESPACE_CHARS: &'static [Char16] = &[
203 ' ' as Char16,
204 '\t' as Char16,
205 '\n' as Char16,
206 BACKSLACK_V as Char16,
207 BACKSLACK_F as Char16,
208 '\r' as Char16,
209 ];
210
211 pub fn trim(&mut self) {
213 let beginning_whitespace: Option<usize> = self
214 .styled_text
215 .0
216 .as_slice()
217 .iter()
218 .position(|c| !Self::WHITESPACE_CHARS.contains(c));
219
220 if let Some(beginning_whitespace) = beginning_whitespace {
221 let trailing_whitespace: usize = self
222 .styled_text
223 .0
224 .as_slice()
225 .iter()
226 .rposition(|c| !Self::WHITESPACE_CHARS.contains(c))
227 .expect("there is a whitespace char")
228 + 1;
229
230 self.styled_text.0 =
231 U16String::from(&self.styled_text.0[beginning_whitespace..trailing_whitespace]); self.styled_text.1 =
233 Vec::from(&self.styled_text.1[beginning_whitespace..trailing_whitespace]);
234 } else {
235 self.styled_text.0.clear();
237 self.styled_text.1.clear();
238 }
239 }
240
241 pub fn verticalize_punctuation(&mut self) {
243 self.styled_text.0 = i18n::verticalize_punctuation_str(&self.styled_text.0);
245 }
246 pub fn allows_vertical_writing_mode(&mut self) -> bool {
248 if self.supports_vertical_writing_mode.is_none() {
249 let new_value = i18n::allows_vertical_writing_mode(self.raw_text());
250 self.supports_vertical_writing_mode = Some(new_value);
251 return new_value;
252 }
253 self.supports_vertical_writing_mode
254 .expect("supportsVerticalWritingMode mut be set")
255 }
256}
257
258impl TaggedString {
259 fn get_next_image_section_char_code(&mut self) -> Option<Char16> {
261 if self.image_section_id == 0 {
262 self.image_section_id = PUABEGIN;
263 return Some(self.image_section_id);
264 }
265
266 self.image_section_id += 1;
267 if self.image_section_id > PUAEND {
268 return None;
269 }
270
271 Some(self.image_section_id)
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use widestring::U16String;
278
279 use crate::legacy::{
280 bidi::Char16,
281 tagged_string::{SectionOptions, TaggedString},
282 util::i18n::BACKSLACK_V,
283 };
284
285 #[test]
286 fn tagged_string_trim() {
288 let mut basic = TaggedString::new_from_raw(
289 " \t\ntrim that and not this \n\t".into(),
290 SectionOptions::new(1.0, vec![], None),
291 );
292 basic.trim();
293 assert_eq!(basic.raw_text(), &U16String::from("trim that and not this"));
294
295 let mut two_sections = TaggedString::default();
296 two_sections.add_text_section(&" \t\ntrim that".into(), 1.5, vec![], None);
297 two_sections.add_text_section(&" and not this \n\t".into(), 0.5, vec![], None);
298
299 two_sections.trim();
300 assert_eq!(
301 two_sections.raw_text(),
302 &U16String::from("trim that and not this")
303 );
304
305 let mut empty = TaggedString::new_from_raw(
306 format!(
307 "\n\t{} \r \t\n",
308 char::from_u32(BACKSLACK_V as u32).unwrap()
309 )
310 .into(),
311 SectionOptions::new(1.0, vec![], None),
312 );
313 empty.trim();
314 assert_eq!(empty.raw_text(), &U16String::from(""));
315
316 let mut no_trim =
317 TaggedString::new_from_raw("no trim!".into(), SectionOptions::new(1.0, vec![], None));
318 no_trim.trim();
319 assert_eq!(no_trim.raw_text(), &U16String::from("no trim!"));
320 }
321 #[test]
322 fn tagged_string_image_sections() {
324 let mut string = TaggedString::new_from_raw(U16String::new(), SectionOptions::default());
325 string.add_image_section("image_name".to_string());
326 assert_eq!(string.raw_text(), &U16String::from("\u{E000}"));
327 assert!(string.get_section(0).image_id.is_some());
328 assert_eq!(
329 string.get_section(0).image_id.as_ref().unwrap(),
330 &"image_name".to_string()
331 );
332
333 let mut max_sections = TaggedString::default();
334 for i in 0..6401 {
335 max_sections.add_image_section(i.to_string());
336 }
337
338 assert_eq!(max_sections.get_sections().len(), 6400);
339 assert_eq!(max_sections.get_char_code_at(0), '\u{E000}' as Char16);
340 assert_eq!(max_sections.get_char_code_at(6399), '\u{F8FF}' as Char16);
341 }
342}