1use crate::{Animation, Easing, PresenceMode, StaggerFrom, Transition};
2use std::{ops::Range, sync::Arc, time::Duration};
3use unicode_segmentation::UnicodeSegmentation;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum TextUnit {
8 #[default]
10 Whole,
11 Words,
13 Graphemes,
15 Lines,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub enum TextMatch {
22 #[default]
24 Replace,
25 Content,
27 Position,
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum TextClip {
34 #[default]
36 None,
37 Unit,
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub enum TextPreset {
44 #[default]
46 Fade,
47 FadeUp,
49 SlideUp,
51 WipeRight,
53 WipeLeft,
55 Typewriter,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct TextTarget {
63 values: [f64; 4],
64 relative_y: bool,
65}
66
67impl Default for TextTarget {
68 fn default() -> Self {
69 Self {
70 values: [0., 0., 1., 1.],
71 relative_y: false,
72 }
73 }
74}
75
76impl TextTarget {
77 pub fn x_em(mut self, value: f32) -> Self {
79 assert!(value.is_finite(), "text offset must be finite");
80 self.values[0] = value.into();
81
82 self
83 }
84
85 pub fn y_em(mut self, value: f32) -> Self {
87 assert!(value.is_finite(), "text offset must be finite");
88 self.values[1] = value.into();
89 self.relative_y = false;
90
91 self
92 }
93
94 pub fn y_relative(mut self, value: f32) -> Self {
98 assert!(value.is_finite(), "text offset must be finite");
99 self.values[1] = value.into();
100 self.relative_y = true;
101
102 self
103 }
104
105 pub fn opacity(mut self, value: f32) -> Self {
107 assert!(
108 (0. ..=1.).contains(&value),
109 "text opacity must be in zero through one"
110 );
111 self.values[2] = value.into();
112
113 self
114 }
115
116 pub fn reveal(mut self, value: f32) -> Self {
118 assert!(
119 (0. ..=1.).contains(&value),
120 "text reveal must be in zero through one"
121 );
122 self.values[3] = value.into();
123
124 self
125 }
126
127 pub(crate) fn values(self) -> [f64; 4] {
128 self.values
129 }
130
131 pub(crate) fn resolved_values(self, em: f64, height: f64) -> [f64; 4] {
132 let mut values = self.values;
133
134 if self.relative_y {
135 values[1] *= height / em;
136 }
137
138 values
139 }
140}
141
142pub struct TextUnitInfo<'a> {
144 pub index: usize,
146 pub count: usize,
148 pub text: &'a str,
150}
151
152#[derive(Clone, Debug, PartialEq)]
154pub struct Scramble {
155 pub(crate) alphabet: Arc<[String]>,
156 pub(crate) duration: Duration,
157 pub(crate) seed: u64,
158}
159
160impl Scramble {
161 pub fn new(alphabet: &str) -> Self {
163 let alphabet: Vec<_> = alphabet.graphemes(true).map(str::to_owned).collect();
164
165 assert!(!alphabet.is_empty(), "scramble alphabet must not be empty");
166
167 Self {
168 alphabet: alphabet.into(),
169 duration: Duration::from_millis(500),
170 seed: 0,
171 }
172 }
173
174 pub fn duration(mut self, duration: Duration) -> Self {
176 self.duration = duration;
177
178 self
179 }
180
181 pub fn seed(mut self, seed: u64) -> Self {
183 self.seed = seed;
184
185 self
186 }
187
188 pub fn alphabet(&self) -> &[String] {
190 &self.alphabet
191 }
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
196pub enum NumberEffect {
197 #[default]
199 Roll,
200 Crossfade,
202 Slide,
204 Interpolate,
206}
207
208#[derive(Clone, Debug, PartialEq)]
210pub struct TextOptions {
211 pub unit: TextUnit,
213 pub matching: TextMatch,
215 pub enter: TextTarget,
217 pub exit: TextTarget,
219 pub transition: Transition,
221 pub opacity_transition: Option<Transition>,
223 pub reflow: Option<Transition>,
225 pub stagger: Duration,
227 pub stagger_from: StaggerFrom,
229 pub presence_mode: PresenceMode,
231 pub clip: TextClip,
233 pub wipe_left: bool,
235 pub animate_initial: bool,
237 pub scramble: Option<Scramble>,
239 pub animation: Option<Animation>,
241}
242
243impl Default for TextOptions {
244 fn default() -> Self {
245 Self {
246 unit: TextUnit::Whole,
247 matching: TextMatch::Replace,
248 enter: TextTarget::default().opacity(0.),
249 exit: TextTarget::default().opacity(0.),
250 transition: Transition::default(),
251 opacity_transition: None,
252 reflow: None,
253 stagger: Duration::ZERO,
254 stagger_from: StaggerFrom::Start,
255 presence_mode: PresenceMode::Sync,
256 clip: TextClip::None,
257 wipe_left: false,
258 animate_initial: true,
259 scramble: None,
260 animation: None,
261 }
262 }
263}
264
265impl TextOptions {
266 pub fn preset(&mut self, preset: TextPreset) {
268 self.enter = TextTarget::default();
269 self.exit = TextTarget::default();
270 self.clip = TextClip::None;
271 self.wipe_left = false;
272 self.opacity_transition = None;
273
274 match preset {
275 TextPreset::Fade => {
276 self.enter = self.enter.opacity(0.);
277 self.exit = self.exit.opacity(0.);
278 }
279 TextPreset::FadeUp => {
280 self.enter = self.enter.y_em(0.6).opacity(0.);
281 self.exit = self.exit.y_em(-0.6).opacity(0.);
282 }
283 TextPreset::SlideUp => {
284 self.enter = self.enter.y_relative(1.);
285 self.exit = self.exit.y_relative(-1.).opacity(0.);
286 self.clip = TextClip::Unit;
287 self.opacity_transition = Some(Transition::Tween(
288 Duration::from_millis(100),
289 Easing::EaseOut,
290 ));
291 }
292 TextPreset::WipeRight | TextPreset::WipeLeft => {
293 self.enter = self.enter.reveal(0.);
294 self.exit = self.exit.reveal(0.).opacity(0.);
295 self.opacity_transition = Some(Transition::Tween(
296 Duration::from_millis(100),
297 Easing::EaseOut,
298 ));
299 self.clip = TextClip::Unit;
300 self.wipe_left = preset == TextPreset::WipeLeft;
301 }
302 TextPreset::Typewriter => {
303 self.enter = self.enter.opacity(0.);
304 self.exit = self.exit.opacity(0.);
305 self.transition = Transition::Instant;
306 }
307 }
308 }
309
310 pub(crate) fn pose_transition(&self, index: usize) -> Transition {
311 if index == 2 {
312 self.opacity_transition.unwrap_or(self.transition)
313 } else {
314 self.transition
315 }
316 }
317
318 pub fn entrance_duration(&self, count: usize) -> Duration {
321 let base = self
322 .animation
323 .as_ref()
324 .and_then(Animation::duration)
325 .unwrap_or_else(|| transition_horizon(self.transition))
326 .max(
327 self.opacity_transition
328 .map_or(Duration::ZERO, transition_horizon),
329 )
330 .max(
331 self.scramble
332 .as_ref()
333 .map_or(Duration::ZERO, |value| value.duration),
334 );
335
336 base.saturating_add(
337 self.stagger
338 .saturating_mul(count.saturating_sub(1).min(u32::MAX as usize) as u32),
339 )
340 }
341}
342
343fn transition_horizon(transition: Transition) -> Duration {
344 match transition {
345 Transition::Instant => Duration::ZERO,
346 Transition::Tween(duration, _) => duration,
347 Transition::Spring(_) => Duration::from_secs(2),
348 }
349}
350
351pub fn text_ranges(text: &str, unit: TextUnit) -> Vec<Range<usize>> {
354 match unit {
355 TextUnit::Whole => {
356 if text.is_empty() {
357 Vec::new()
358 } else {
359 std::iter::once(0..text.len()).collect()
360 }
361 }
362 TextUnit::Words => text
363 .split_word_bound_indices()
364 .map(|(index, value)| index..index + value.len())
365 .collect(),
366 TextUnit::Graphemes => text
367 .grapheme_indices(true)
368 .map(|(index, value)| index..index + value.len())
369 .collect(),
370 TextUnit::Lines => {
371 let mut start = 0;
372
373 text.split_inclusive('\n')
374 .map(|value| {
375 let range = start..start + value.trim_end_matches('\n').len();
376
377 start += value.len();
378
379 range
380 })
381 .collect()
382 }
383 }
384}
385
386#[derive(Clone, Debug, PartialEq, Eq)]
388pub enum TextError {
389 Shaping(Arc<str>),
391 Painting(Arc<str>),
393}
394
395impl std::fmt::Display for TextError {
396 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 match self {
398 Self::Shaping(message) => write!(formatter, "text shaping failed: {message}"),
399 Self::Painting(message) => write!(formatter, "text painting failed: {message}"),
400 }
401 }
402}
403
404impl std::error::Error for TextError {}