use crate::{Animation, Easing, PresenceMode, StaggerFrom, Transition};
use std::{ops::Range, sync::Arc, time::Duration};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextUnit {
#[default]
Whole,
Words,
Graphemes,
Lines,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextMatch {
#[default]
Replace,
Content,
Position,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextClip {
#[default]
None,
Unit,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextPreset {
#[default]
Fade,
FadeUp,
SlideUp,
WipeRight,
WipeLeft,
WipeDown,
WipeUp,
Typewriter,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextTarget {
values: [f64; 4],
relative_y: bool,
}
impl Default for TextTarget {
fn default() -> Self {
Self {
values: [0., 0., 1., 1.],
relative_y: false,
}
}
}
impl TextTarget {
pub fn x_em(mut self, value: f32) -> Self {
assert!(value.is_finite(), "text offset must be finite");
self.values[0] = value.into();
self
}
pub fn y_em(mut self, value: f32) -> Self {
assert!(value.is_finite(), "text offset must be finite");
self.values[1] = value.into();
self.relative_y = false;
self
}
pub fn y_relative(mut self, value: f32) -> Self {
assert!(value.is_finite(), "text offset must be finite");
self.values[1] = value.into();
self.relative_y = true;
self
}
pub fn opacity(mut self, value: f32) -> Self {
assert!(
(0. ..=1.).contains(&value),
"text opacity must be in zero through one"
);
self.values[2] = value.into();
self
}
pub fn reveal(mut self, value: f32) -> Self {
assert!(
(0. ..=1.).contains(&value),
"text reveal must be in zero through one"
);
self.values[3] = value.into();
self
}
pub(crate) fn values(self) -> [f64; 4] {
self.values
}
pub(crate) fn resolved_values(self, em: f64, height: f64) -> [f64; 4] {
let mut values = self.values;
if self.relative_y {
values[1] *= height / em;
}
values
}
}
pub struct TextUnitInfo<'a> {
pub index: usize,
pub count: usize,
pub text: &'a str,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Scramble {
pub(crate) alphabet: Arc<[String]>,
pub(crate) duration: Duration,
pub(crate) seed: u64,
}
impl Scramble {
pub fn new(alphabet: &str) -> Self {
let alphabet: Vec<_> = alphabet.graphemes(true).map(str::to_owned).collect();
assert!(!alphabet.is_empty(), "scramble alphabet must not be empty");
Self {
alphabet: alphabet.into(),
duration: Duration::from_millis(500),
seed: 0,
}
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn alphabet(&self) -> &[String] {
&self.alphabet
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NumberEffect {
#[default]
Roll,
Crossfade,
Slide,
Interpolate,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextRevealAxis {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextOptions {
pub unit: TextUnit,
pub matching: TextMatch,
pub enter: TextTarget,
pub exit: TextTarget,
pub transition: Transition,
pub opacity_transition: Option<Transition>,
pub reflow: Option<Transition>,
pub stagger: Duration,
pub stagger_from: StaggerFrom,
pub presence_mode: PresenceMode,
pub clip: TextClip,
pub wipe_left: bool,
pub reveal_axis: TextRevealAxis,
pub animate_initial: bool,
pub scramble: Option<Scramble>,
pub animation: Option<Animation>,
}
impl Default for TextOptions {
fn default() -> Self {
Self {
unit: TextUnit::Whole,
matching: TextMatch::Replace,
enter: TextTarget::default().opacity(0.),
exit: TextTarget::default().opacity(0.),
transition: Transition::default(),
opacity_transition: None,
reflow: None,
stagger: Duration::ZERO,
stagger_from: StaggerFrom::Start,
presence_mode: PresenceMode::Sync,
clip: TextClip::None,
wipe_left: false,
reveal_axis: TextRevealAxis::Horizontal,
animate_initial: true,
scramble: None,
animation: None,
}
}
}
impl TextOptions {
pub fn preset(&mut self, preset: TextPreset) {
self.enter = TextTarget::default();
self.exit = TextTarget::default();
self.clip = TextClip::None;
self.wipe_left = false;
self.reveal_axis = TextRevealAxis::Horizontal;
self.opacity_transition = None;
match preset {
TextPreset::Fade => {
self.enter = self.enter.opacity(0.);
self.exit = self.exit.opacity(0.);
}
TextPreset::FadeUp => {
self.enter = self.enter.y_em(0.6).opacity(0.);
self.exit = self.exit.y_em(-0.6).opacity(0.);
}
TextPreset::SlideUp => {
self.enter = self.enter.y_relative(1.);
self.exit = self.exit.y_relative(-1.).opacity(0.);
self.clip = TextClip::Unit;
self.opacity_transition = Some(Transition::Tween(
Duration::from_millis(100),
Easing::EaseOut,
));
}
TextPreset::WipeRight
| TextPreset::WipeLeft
| TextPreset::WipeDown
| TextPreset::WipeUp => {
self.enter = self.enter.reveal(0.);
self.exit = self.exit.reveal(0.).opacity(0.);
self.opacity_transition = Some(Transition::Tween(
Duration::from_millis(100),
Easing::EaseOut,
));
self.clip = TextClip::Unit;
self.wipe_left = matches!(preset, TextPreset::WipeLeft | TextPreset::WipeUp);
if matches!(preset, TextPreset::WipeDown | TextPreset::WipeUp) {
self.reveal_axis = TextRevealAxis::Vertical;
}
}
TextPreset::Typewriter => {
self.enter = self.enter.opacity(0.);
self.exit = self.exit.opacity(0.);
self.transition = Transition::Instant;
}
}
}
pub(crate) fn pose_transition(&self, index: usize) -> Transition {
if index == 2 {
self.opacity_transition.unwrap_or(self.transition)
} else {
self.transition
}
}
pub fn entrance_duration(&self, count: usize) -> Duration {
let base = self
.animation
.as_ref()
.and_then(Animation::duration)
.unwrap_or_else(|| transition_horizon(self.transition))
.max(
self.opacity_transition
.map_or(Duration::ZERO, transition_horizon),
)
.max(
self.scramble
.as_ref()
.map_or(Duration::ZERO, |value| value.duration),
);
base.saturating_add(
self.stagger
.saturating_mul(count.saturating_sub(1).min(u32::MAX as usize) as u32),
)
}
}
fn transition_horizon(transition: Transition) -> Duration {
match transition {
Transition::Instant => Duration::ZERO,
Transition::Tween(duration, _) => duration,
Transition::Spring(_) => Duration::from_secs(2),
}
}
pub fn text_ranges(text: &str, unit: TextUnit) -> Vec<Range<usize>> {
match unit {
TextUnit::Whole => {
if text.is_empty() {
Vec::new()
} else {
std::iter::once(0..text.len()).collect()
}
}
TextUnit::Words => text
.split_word_bound_indices()
.map(|(index, value)| index..index + value.len())
.collect(),
TextUnit::Graphemes => text
.grapheme_indices(true)
.map(|(index, value)| index..index + value.len())
.collect(),
TextUnit::Lines => {
let mut start = 0;
text.split_inclusive('\n')
.map(|value| {
let range = start..start + value.trim_end_matches('\n').len();
start += value.len();
range
})
.collect()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TextError {
Shaping(Arc<str>),
Painting(Arc<str>),
}
impl std::fmt::Display for TextError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Shaping(message) => write!(formatter, "text shaping failed: {message}"),
Self::Painting(message) => write!(formatter, "text painting failed: {message}"),
}
}
}
impl std::error::Error for TextError {}
impl crate::MotionData<4> for TextTarget {
fn channels(self) -> [f64; 4] {
self.values
}
fn with_channels(self, values: [f64; 4]) -> Self {
Self {
values,
relative_y: self.relative_y,
}
}
fn compatible(self, other: Self) -> bool {
self.relative_y == other.relative_y
}
}