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 WipeDown,
57 WipeUp,
59 Typewriter,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct TextTarget {
67 values: [f64; 4],
68 relative_y: bool,
69}
70
71impl Default for TextTarget {
72 fn default() -> Self {
73 Self {
74 values: [0., 0., 1., 1.],
75 relative_y: false,
76 }
77 }
78}
79
80impl TextTarget {
81 pub fn x_em(mut self, value: f32) -> Self {
83 assert!(value.is_finite(), "text offset must be finite");
84 self.values[0] = value.into();
85
86 self
87 }
88
89 pub fn y_em(mut self, value: f32) -> Self {
91 assert!(value.is_finite(), "text offset must be finite");
92 self.values[1] = value.into();
93 self.relative_y = false;
94
95 self
96 }
97
98 pub fn y_relative(mut self, value: f32) -> Self {
102 assert!(value.is_finite(), "text offset must be finite");
103 self.values[1] = value.into();
104 self.relative_y = true;
105
106 self
107 }
108
109 pub fn opacity(mut self, value: f32) -> Self {
111 assert!(
112 (0. ..=1.).contains(&value),
113 "text opacity must be in zero through one"
114 );
115 self.values[2] = value.into();
116
117 self
118 }
119
120 pub fn reveal(mut self, value: f32) -> Self {
122 assert!(
123 (0. ..=1.).contains(&value),
124 "text reveal must be in zero through one"
125 );
126 self.values[3] = value.into();
127
128 self
129 }
130
131 pub(crate) fn values(self) -> [f64; 4] {
132 self.values
133 }
134
135 pub(crate) fn resolved_values(self, em: f64, height: f64) -> [f64; 4] {
136 let mut values = self.values;
137
138 if self.relative_y {
139 values[1] *= height / em;
140 }
141
142 values
143 }
144}
145
146pub struct TextUnitInfo<'a> {
148 pub index: usize,
150 pub count: usize,
152 pub text: &'a str,
154}
155
156#[derive(Clone, Debug, PartialEq)]
158pub struct Scramble {
159 pub(crate) alphabet: Arc<[String]>,
160 pub(crate) duration: Duration,
161 pub(crate) seed: u64,
162}
163
164impl Scramble {
165 pub fn new(alphabet: &str) -> Self {
167 let alphabet: Vec<_> = alphabet.graphemes(true).map(str::to_owned).collect();
168
169 assert!(!alphabet.is_empty(), "scramble alphabet must not be empty");
170
171 Self {
172 alphabet: alphabet.into(),
173 duration: Duration::from_millis(500),
174 seed: 0,
175 }
176 }
177
178 pub fn duration(mut self, duration: Duration) -> Self {
180 self.duration = duration;
181
182 self
183 }
184
185 pub fn seed(mut self, seed: u64) -> Self {
187 self.seed = seed;
188
189 self
190 }
191
192 pub fn alphabet(&self) -> &[String] {
194 &self.alphabet
195 }
196}
197
198#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
200pub enum NumberEffect {
201 #[default]
203 Roll,
204 Crossfade,
206 Slide,
208 Interpolate,
210}
211
212#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub enum TextRevealAxis {
215 #[default]
217 Horizontal,
218 Vertical,
220}
221
222#[derive(Clone, Debug, PartialEq)]
224pub struct TextOptions {
225 pub unit: TextUnit,
227 pub matching: TextMatch,
229 pub enter: TextTarget,
231 pub exit: TextTarget,
233 pub transition: Transition,
235 pub opacity_transition: Option<Transition>,
237 pub reflow: Option<Transition>,
239 pub stagger: Duration,
241 pub stagger_from: StaggerFrom,
243 pub presence_mode: PresenceMode,
245 pub clip: TextClip,
247 pub wipe_left: bool,
249 pub reveal_axis: TextRevealAxis,
251 pub animate_initial: bool,
253 pub scramble: Option<Scramble>,
255 pub animation: Option<Animation>,
257}
258
259impl Default for TextOptions {
260 fn default() -> Self {
261 Self {
262 unit: TextUnit::Whole,
263 matching: TextMatch::Replace,
264 enter: TextTarget::default().opacity(0.),
265 exit: TextTarget::default().opacity(0.),
266 transition: Transition::default(),
267 opacity_transition: None,
268 reflow: None,
269 stagger: Duration::ZERO,
270 stagger_from: StaggerFrom::Start,
271 presence_mode: PresenceMode::Sync,
272 clip: TextClip::None,
273 wipe_left: false,
274 reveal_axis: TextRevealAxis::Horizontal,
275 animate_initial: true,
276 scramble: None,
277 animation: None,
278 }
279 }
280}
281
282impl TextOptions {
283 pub fn preset(&mut self, preset: TextPreset) {
285 self.enter = TextTarget::default();
286 self.exit = TextTarget::default();
287 self.clip = TextClip::None;
288 self.wipe_left = false;
289 self.reveal_axis = TextRevealAxis::Horizontal;
290 self.opacity_transition = None;
291
292 match preset {
293 TextPreset::Fade => {
294 self.enter = self.enter.opacity(0.);
295 self.exit = self.exit.opacity(0.);
296 }
297 TextPreset::FadeUp => {
298 self.enter = self.enter.y_em(0.6).opacity(0.);
299 self.exit = self.exit.y_em(-0.6).opacity(0.);
300 }
301 TextPreset::SlideUp => {
302 self.enter = self.enter.y_relative(1.);
303 self.exit = self.exit.y_relative(-1.).opacity(0.);
304 self.clip = TextClip::Unit;
305 self.opacity_transition = Some(Transition::Tween(
306 Duration::from_millis(100),
307 Easing::EaseOut,
308 ));
309 }
310 TextPreset::WipeRight
311 | TextPreset::WipeLeft
312 | TextPreset::WipeDown
313 | TextPreset::WipeUp => {
314 self.enter = self.enter.reveal(0.);
315 self.exit = self.exit.reveal(0.).opacity(0.);
316 self.opacity_transition = Some(Transition::Tween(
317 Duration::from_millis(100),
318 Easing::EaseOut,
319 ));
320 self.clip = TextClip::Unit;
321 self.wipe_left = matches!(preset, TextPreset::WipeLeft | TextPreset::WipeUp);
322
323 if matches!(preset, TextPreset::WipeDown | TextPreset::WipeUp) {
324 self.reveal_axis = TextRevealAxis::Vertical;
325 }
326 }
327 TextPreset::Typewriter => {
328 self.enter = self.enter.opacity(0.);
329 self.exit = self.exit.opacity(0.);
330 self.transition = Transition::Instant;
331 }
332 }
333 }
334
335 pub(crate) fn pose_transition(&self, index: usize) -> Transition {
336 if index == 2 {
337 self.opacity_transition.unwrap_or(self.transition)
338 } else {
339 self.transition
340 }
341 }
342
343 pub fn entrance_duration(&self, count: usize) -> Duration {
346 let base = self
347 .animation
348 .as_ref()
349 .and_then(Animation::duration)
350 .unwrap_or_else(|| transition_horizon(self.transition))
351 .max(
352 self.opacity_transition
353 .map_or(Duration::ZERO, transition_horizon),
354 )
355 .max(
356 self.scramble
357 .as_ref()
358 .map_or(Duration::ZERO, |value| value.duration),
359 );
360
361 base.saturating_add(
362 self.stagger
363 .saturating_mul(count.saturating_sub(1).min(u32::MAX as usize) as u32),
364 )
365 }
366}
367
368fn transition_horizon(transition: Transition) -> Duration {
369 match transition {
370 Transition::Instant => Duration::ZERO,
371 Transition::Tween(duration, _) => duration,
372 Transition::Spring(_) => Duration::from_secs(2),
373 }
374}
375
376pub fn text_ranges(text: &str, unit: TextUnit) -> Vec<Range<usize>> {
379 match unit {
380 TextUnit::Whole => {
381 if text.is_empty() {
382 Vec::new()
383 } else {
384 std::iter::once(0..text.len()).collect()
385 }
386 }
387 TextUnit::Words => text
388 .split_word_bound_indices()
389 .map(|(index, value)| index..index + value.len())
390 .collect(),
391 TextUnit::Graphemes => text
392 .grapheme_indices(true)
393 .map(|(index, value)| index..index + value.len())
394 .collect(),
395 TextUnit::Lines => {
396 let mut start = 0;
397
398 text.split_inclusive('\n')
399 .map(|value| {
400 let range = start..start + value.trim_end_matches('\n').len();
401
402 start += value.len();
403
404 range
405 })
406 .collect()
407 }
408 }
409}
410
411#[derive(Clone, Debug, PartialEq, Eq)]
413pub enum TextError {
414 Shaping(Arc<str>),
416 Painting(Arc<str>),
418}
419
420impl std::fmt::Display for TextError {
421 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422 match self {
423 Self::Shaping(message) => write!(formatter, "text shaping failed: {message}"),
424 Self::Painting(message) => write!(formatter, "text painting failed: {message}"),
425 }
426 }
427}
428
429impl std::error::Error for TextError {}
430
431impl crate::MotionData<4> for TextTarget {
432 fn channels(self) -> [f64; 4] {
433 self.values
434 }
435
436 fn with_channels(self, values: [f64; 4]) -> Self {
437 Self {
438 values,
439 relative_y: self.relative_y,
440 }
441 }
442
443 fn compatible(self, other: Self) -> bool {
444 self.relative_y == other.relative_y
445 }
446}