use std::cell::{Cell, RefCell};
use std::rc::Rc;
use dinamika_cpu::{Color, Font, Path, PathBuilder, PathSegment};
use crate::easing::Easing;
use crate::signal::Signal;
use crate::timeline::{Action, TweenObj};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextPos {
Char(usize),
Line(usize),
End,
}
impl From<usize> for TextPos {
fn from(i: usize) -> Self {
TextPos::Char(i)
}
}
pub fn line(n: usize) -> TextPos {
TextPos::Line(n)
}
pub fn infinite() -> TextPos {
TextPos::End
}
impl TextPos {
fn resolve(self, text: &str) -> usize {
let count = text.chars().count();
match self {
TextPos::Char(i) => i.min(count),
TextPos::Line(n) => line_start_char(text, n).min(count),
TextPos::End => count,
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum TextStage {
Base,
Shown(String),
Typing { text: String, visible: f32 },
Crossfade { from: String, to: String, p: f32 },
}
#[derive(Clone, Debug)]
pub(crate) enum HighlightStage {
Base,
Morph {
from: Vec<(TextPos, TextPos)>,
to: Vec<(TextPos, TextPos)>,
p: f32,
},
}
#[derive(Copy, Clone)]
pub(super) enum TextMotion {
Spawn,
Typing,
Smooth,
}
pub(crate) struct TextTween {
stage: Rc<RefCell<TextStage>>,
motion: TextMotion,
old: RefCell<String>,
new: RefCell<String>,
start: Cell<f64>,
duration: f64,
easing: Easing,
}
impl TextTween {
pub(super) fn action(
stage: Rc<RefCell<TextStage>>,
motion: TextMotion,
old: String,
new: String,
duration: f64,
easing: Easing,
) -> Action {
let leaf = TextTween {
stage,
motion,
old: RefCell::new(old),
new: RefCell::new(new),
start: Cell::new(0.0),
duration: duration.max(0.0),
easing,
};
Action::from_tween(Rc::new(leaf))
}
fn stage_at(&self, local: f32) -> TextStage {
let old = self.old.borrow();
let new = self.new.borrow();
match self.motion {
TextMotion::Spawn => {
TextStage::Shown(if local <= 0.0 { old.clone() } else { new.clone() })
}
TextMotion::Typing => {
let from = common_prefix_chars(&old, &new) as f32;
let to = new.chars().count() as f32;
let eased = self.easing.apply(local);
TextStage::Typing { text: new.clone(), visible: from + (to - from) * eased }
}
TextMotion::Smooth => {
let p = self.easing.apply(local);
TextStage::Crossfade { from: old.clone(), to: new.clone(), p }
}
}
}
}
impl TweenObj for TextTween {
fn duration(&self) -> f64 {
self.duration
}
fn start(&self) -> f64 {
self.start.get()
}
fn set_start(&self, start: f64) {
self.start.set(start);
}
fn reset(&self) {
*self.stage.borrow_mut() = match self.motion {
TextMotion::Smooth => TextStage::Shown(self.old.borrow().clone()),
_ => self.stage_at(0.0),
};
}
fn capture_from(&self) {
}
fn apply(&self, t: f64) {
let local = if self.duration <= 0.0 {
1.0
} else {
(((t - self.start.get()) / self.duration).clamp(0.0, 1.0)) as f32
};
*self.stage.borrow_mut() = self.stage_at(local);
}
fn morph_group(&self) -> Option<*const ()> {
Some(Rc::as_ptr(&self.stage) as *const ())
}
fn morph_from(&self) -> Option<String> {
Some(self.old.borrow().clone())
}
fn morph_new(&self) -> Option<String> {
Some(self.new.borrow().clone())
}
fn rebase(&self, old: &str, new: &str) {
*self.old.borrow_mut() = old.to_owned();
*self.new.borrow_mut() = new.to_owned();
}
}
pub(crate) struct HighlightTween {
stage: Rc<RefCell<HighlightStage>>,
from: RefCell<Vec<(TextPos, TextPos)>>,
to: RefCell<Vec<(TextPos, TextPos)>>,
start: Cell<f64>,
duration: f64,
easing: Easing,
}
impl HighlightTween {
pub(super) fn action(
stage: Rc<RefCell<HighlightStage>>,
from: Vec<(TextPos, TextPos)>,
to: Vec<(TextPos, TextPos)>,
duration: f64,
easing: Easing,
) -> Action {
let leaf = HighlightTween {
stage,
from: RefCell::new(from),
to: RefCell::new(to),
start: Cell::new(0.0),
duration: duration.max(0.0),
easing,
};
Action::from_tween(Rc::new(leaf))
}
fn stage_at(&self, local: f32) -> HighlightStage {
HighlightStage::Morph {
from: self.from.borrow().clone(),
to: self.to.borrow().clone(),
p: self.easing.apply(local),
}
}
}
impl TweenObj for HighlightTween {
fn duration(&self) -> f64 {
self.duration
}
fn start(&self) -> f64 {
self.start.get()
}
fn set_start(&self, start: f64) {
self.start.set(start);
}
fn reset(&self) {
*self.stage.borrow_mut() = self.stage_at(0.0);
}
fn capture_from(&self) {
}
fn apply(&self, t: f64) {
let local = if self.duration <= 0.0 {
1.0
} else {
(((t - self.start.get()) / self.duration).clamp(0.0, 1.0)) as f32
};
*self.stage.borrow_mut() = self.stage_at(local);
}
fn highlight_group(&self) -> Option<*const ()> {
Some(Rc::as_ptr(&self.stage) as *const ())
}
fn highlight_from(&self) -> Option<Vec<(TextPos, TextPos)>> {
Some(self.from.borrow().clone())
}
fn highlight_to(&self) -> Option<Vec<(TextPos, TextPos)>> {
Some(self.to.borrow().clone())
}
fn highlight_rebase(&self, from: Vec<(TextPos, TextPos)>, to: Vec<(TextPos, TextPos)>) {
*self.from.borrow_mut() = from;
*self.to.borrow_mut() = to;
}
}
const DEFAULT_DIM: f32 = 0.3;
pub(crate) struct TextData {
font: RefCell<Option<Rc<Vec<u8>>>>,
face_index: Cell<u32>,
text: RefCell<String>,
stage: Rc<RefCell<TextStage>>,
pub size: Signal<f32>,
pub color: Signal<Color>,
pub letter_spacing: Signal<f32>,
pub line_height: Signal<f32>,
pub align: Cell<TextAlign>,
highlights: RefCell<Vec<(TextPos, TextPos)>>,
highlight_stage: Rc<RefCell<HighlightStage>>,
metrics_memo: RefCell<Option<(MetricsKey, (f32, f32))>>,
path_memo: RefCell<Option<(PathKey, Option<Rc<Path>>)>>,
}
#[derive(Clone, PartialEq)]
struct MetricsKey {
text: String,
size: f32,
letter_spacing: f32,
line_height: f32,
}
#[derive(Clone, PartialEq)]
struct PathKey {
text: String,
size: f32,
letter_spacing: f32,
line_height: f32,
align: TextAlign,
width: f32,
}
impl TextData {
pub(crate) fn new(text: String) -> Self {
TextData {
font: RefCell::new(None),
face_index: Cell::new(0),
text: RefCell::new(text),
stage: Rc::new(RefCell::new(TextStage::Base)),
size: Signal::new(32.0),
color: Signal::new(Color::BLACK),
letter_spacing: Signal::new(0.0),
line_height: Signal::new(1.0),
align: Cell::new(TextAlign::Left),
highlights: RefCell::new(Vec::new()),
highlight_stage: Rc::new(RefCell::new(HighlightStage::Base)),
metrics_memo: RefCell::new(None),
path_memo: RefCell::new(None),
}
}
pub(crate) fn set_font(&self, bytes: Rc<Vec<u8>>, index: u32) {
*self.font.borrow_mut() = Some(bytes);
self.face_index.set(index);
self.invalidate();
}
pub(crate) fn set_text(&self, text: String) {
*self.text.borrow_mut() = text;
}
pub(crate) fn get_text(&self) -> String {
self.text.borrow().clone()
}
pub(crate) fn stage_handle(&self) -> Rc<RefCell<TextStage>> {
Rc::clone(&self.stage)
}
fn invalidate(&self) {
*self.metrics_memo.borrow_mut() = None;
*self.path_memo.borrow_mut() = None;
}
pub(crate) fn color(&self) -> Color {
self.color.get()
}
pub(crate) fn add_highlight(&self, from: TextPos, to: TextPos) {
self.highlights.borrow_mut().push((from, to));
}
pub(crate) fn clear_highlights(&self) {
self.highlights.borrow_mut().clear();
}
pub(crate) fn get_highlights(&self) -> Vec<(TextPos, TextPos)> {
self.highlights.borrow().clone()
}
pub(crate) fn highlight_stage_handle(&self) -> Rc<RefCell<HighlightStage>> {
Rc::clone(&self.highlight_stage)
}
pub(crate) fn natural_size(&self) -> (f32, f32) {
match &*self.stage.borrow() {
TextStage::Base => self.measure_committed(),
TextStage::Shown(s) => self.measure_text(s),
TextStage::Typing { text, visible } => self.measure_text(&prefix_chars(text, *visible)),
TextStage::Crossfade { from, to, p } => {
let (fw, fh) = self.measure_text(from);
let (tw, th) = self.measure_text(to);
(lerp_f32(fw, tw, *p), lerp_f32(fh, th, *p))
}
}
}
pub(crate) fn layout_path(&self, content_w: f32) -> Option<Rc<Path>> {
let key = PathKey {
text: self.layout_text(),
size: self.size.get(),
letter_spacing: self.letter_spacing.get(),
line_height: self.line_height.get(),
align: self.align.get(),
width: content_w,
};
if let Some((k, v)) = self.path_memo.borrow().as_ref() {
if *k == key {
return v.clone();
}
}
let built = self
.with_font(|font| {
build_block_path(
font,
&key.text,
key.size,
key.letter_spacing,
key.line_height,
key.align,
content_w,
)
})
.flatten()
.map(Rc::new);
*self.path_memo.borrow_mut() = Some((key, built.clone()));
built
}
pub(crate) fn draw_layers(&self, content_w: f32) -> Vec<(Rc<Path>, f32)> {
let stage = self.stage.borrow().clone();
if let TextStage::Crossfade { from, to, p } = &stage {
return self.morph_layers(from, to, *p, content_w);
}
if self.highlight_idle() {
return self
.layout_path(content_w)
.map(|path| vec![(path, 1.0)])
.unwrap_or_default();
}
let text = self.layout_text();
match self.highlight_alphas(&text) {
None => self
.layout_path(content_w)
.map(|path| vec![(path, 1.0)])
.unwrap_or_default(),
Some(alphas) => self.alpha_layers(&text, content_w, &alphas),
}
}
fn alpha_layers(&self, text: &str, content_w: f32, alphas: &[f32]) -> Vec<(Rc<Path>, f32)> {
let size = self.size.get();
let ls = self.letter_spacing.get();
let lh = self.line_height.get();
let align = self.align.get();
self.with_font(|font| {
let positions = char_positions(font, text, size, ls, lh, align, content_w);
let mut groups: Vec<(f32, PathBuilder)> = Vec::new();
for (i, ch) in text.chars().enumerate() {
if let Some((x, y)) = positions[i] {
let alpha = alphas.get(i).copied().unwrap_or(1.0);
if alpha <= 0.0 {
continue;
}
place_glyph(alpha_group(&mut groups, alpha), font, ch, size, x, y);
}
}
groups
.into_iter()
.filter_map(|(alpha, b)| b.finish().map(|path| (Rc::new(path), alpha)))
.collect()
})
.unwrap_or_default()
}
fn morph_layers(&self, from: &str, to: &str, p: f32, content_w: f32) -> Vec<(Rc<Path>, f32)> {
let size = self.size.get();
let ls = self.letter_spacing.get();
let lh = self.line_height.get();
let align = self.align.get();
self.with_font(|font| {
let from_chars: Vec<char> = from.chars().collect();
let to_chars: Vec<char> = to.chars().collect();
let from_pos = char_positions(font, from, size, ls, lh, align, content_w);
let to_pos = char_positions(font, to, size, ls, lh, align, content_w);
let mut ops = Vec::new();
diff_runs(&from_chars, &to_chars, 0, 0, &mut ops);
let (old_a, new_a) = crossfade_mid_alpha(p);
let mut common = PathBuilder::new();
let mut deleted = PathBuilder::new();
let mut inserted = PathBuilder::new();
for op in ops {
match op {
DiffOp::Common { a, b, len } => {
for k in 0..len {
if let (Some((fx, fy)), Some((tx, ty))) = (from_pos[a + k], to_pos[b + k]) {
let x = lerp_f32(fx, tx, p);
let y = lerp_f32(fy, ty, p);
place_glyph(&mut common, font, to_chars[b + k], size, x, y);
}
}
}
DiffOp::Replace { a, alen, b, blen } => {
if old_a > 0.0 {
for k in 0..alen {
if let Some((fx, fy)) = from_pos[a + k] {
place_glyph(&mut deleted, font, from_chars[a + k], size, fx, fy);
}
}
}
if new_a > 0.0 {
for k in 0..blen {
if let Some((tx, ty)) = to_pos[b + k] {
place_glyph(&mut inserted, font, to_chars[b + k], size, tx, ty);
}
}
}
}
}
}
let mut layers: Vec<(Rc<Path>, f32)> = Vec::new();
if let Some(path) = common.finish() {
layers.push((Rc::new(path), 1.0));
}
if old_a > 0.0 {
if let Some(path) = deleted.finish() {
layers.push((Rc::new(path), old_a));
}
}
if new_a > 0.0 {
if let Some(path) = inserted.finish() {
layers.push((Rc::new(path), new_a));
}
}
layers
})
.unwrap_or_default()
}
pub(crate) fn draw_layers_colored(
&self,
content_w: f32,
default: Color,
colorize: &dyn Fn(&str) -> Rc<Vec<Color>>,
) -> Vec<(Rc<Path>, Color, f32)> {
let stage = self.stage.borrow().clone();
if let TextStage::Crossfade { from, to, p } = &stage {
return self.morph_layers_colored(from, to, *p, content_w, default, colorize);
}
self.colored_block_layers(&self.layout_text(), content_w, default, colorize)
}
fn colored_block_layers(
&self,
text: &str,
content_w: f32,
default: Color,
colorize: &dyn Fn(&str) -> Rc<Vec<Color>>,
) -> Vec<(Rc<Path>, Color, f32)> {
let size = self.size.get();
let ls = self.letter_spacing.get();
let lh = self.line_height.get();
let align = self.align.get();
let alphas = self.highlight_alphas(text);
self.with_font(|font| {
let positions = char_positions(font, text, size, ls, lh, align, content_w);
let colors = colorize(text);
let mut groups: Vec<(Color, f32, PathBuilder)> = Vec::new();
for (i, ch) in text.chars().enumerate() {
if let Some((x, y)) = positions[i] {
let color = colors.get(i).copied().unwrap_or(default);
let alpha = alphas.as_ref().and_then(|a| a.get(i).copied()).unwrap_or(1.0);
if alpha <= 0.0 {
continue;
}
place_glyph(color_alpha_group(&mut groups, color, alpha), font, ch, size, x, y);
}
}
groups
.into_iter()
.filter_map(|(color, alpha, b)| b.finish().map(|path| (Rc::new(path), color, alpha)))
.collect()
})
.unwrap_or_default()
}
fn highlight_idle(&self) -> bool {
matches!(&*self.highlight_stage.borrow(), HighlightStage::Base)
&& self.highlights.borrow().is_empty()
}
fn highlight_alphas(&self, text: &str) -> Option<Vec<f32>> {
let count = text.chars().count();
match &*self.highlight_stage.borrow() {
HighlightStage::Base => {
let committed = self.highlights.borrow();
if committed.is_empty() {
return None;
}
let ranges = resolve_ranges(&committed, text);
Some((0..count).map(|i| alpha_for(i, &ranges)).collect())
}
HighlightStage::Morph { from, to, p } => {
let rf = resolve_ranges(from, text);
let rt = resolve_ranges(to, text);
Some((0..count).map(|i| lerp_f32(alpha_for(i, &rf), alpha_for(i, &rt), *p)).collect())
}
}
}
fn morph_layers_colored(
&self,
from: &str,
to: &str,
p: f32,
content_w: f32,
default: Color,
colorize: &dyn Fn(&str) -> Rc<Vec<Color>>,
) -> Vec<(Rc<Path>, Color, f32)> {
let size = self.size.get();
let ls = self.letter_spacing.get();
let lh = self.line_height.get();
let align = self.align.get();
self.with_font(|font| {
let from_chars: Vec<char> = from.chars().collect();
let to_chars: Vec<char> = to.chars().collect();
let from_pos = char_positions(font, from, size, ls, lh, align, content_w);
let to_pos = char_positions(font, to, size, ls, lh, align, content_w);
let from_col = colorize(from);
let to_col = colorize(to);
let mut ops = Vec::new();
diff_runs(&from_chars, &to_chars, 0, 0, &mut ops);
let (old_a, new_a) = crossfade_mid_alpha(p);
let mut common: Vec<(Color, PathBuilder)> = Vec::new();
let mut deleted: Vec<(Color, PathBuilder)> = Vec::new();
let mut inserted: Vec<(Color, PathBuilder)> = Vec::new();
for op in ops {
match op {
DiffOp::Common { a, b, len } => {
for k in 0..len {
if let (Some((fx, fy)), Some((tx, ty))) = (from_pos[a + k], to_pos[b + k]) {
let x = lerp_f32(fx, tx, p);
let y = lerp_f32(fy, ty, p);
let color = to_col.get(b + k).copied().unwrap_or(default);
place_glyph(group_for(&mut common, color), font, to_chars[b + k], size, x, y);
}
}
}
DiffOp::Replace { a, alen, b, blen } => {
if old_a > 0.0 {
for k in 0..alen {
if let Some((fx, fy)) = from_pos[a + k] {
let color = from_col.get(a + k).copied().unwrap_or(default);
place_glyph(group_for(&mut deleted, color), font, from_chars[a + k], size, fx, fy);
}
}
}
if new_a > 0.0 {
for k in 0..blen {
if let Some((tx, ty)) = to_pos[b + k] {
let color = to_col.get(b + k).copied().unwrap_or(default);
place_glyph(group_for(&mut inserted, color), font, to_chars[b + k], size, tx, ty);
}
}
}
}
}
}
let mut layers = finish_groups(common, 1.0);
if old_a > 0.0 {
layers.extend(finish_groups(deleted, old_a));
}
if new_a > 0.0 {
layers.extend(finish_groups(inserted, new_a));
}
layers
})
.unwrap_or_default()
}
fn layout_text(&self) -> String {
match &*self.stage.borrow() {
TextStage::Base => self.text.borrow().clone(),
TextStage::Shown(s) => s.clone(),
TextStage::Typing { text, visible } => prefix_chars(text, *visible),
TextStage::Crossfade { from, to, p } => {
if *p < 0.5 {
from.clone()
} else {
to.clone()
}
}
}
}
fn measure_committed(&self) -> (f32, f32) {
let key = self.metrics_key();
if let Some((k, v)) = self.metrics_memo.borrow().as_ref() {
if *k == key {
return *v;
}
}
let result = self.measure_text(&key.text);
*self.metrics_memo.borrow_mut() = Some((key, result));
result
}
fn measure_text(&self, text: &str) -> (f32, f32) {
self.with_font(|font| {
measure_block(font, text, self.size.get(), self.letter_spacing.get(), self.line_height.get())
})
.unwrap_or((0.0, 0.0))
}
fn metrics_key(&self) -> MetricsKey {
MetricsKey {
text: self.text.borrow().clone(),
size: self.size.get(),
letter_spacing: self.letter_spacing.get(),
line_height: self.line_height.get(),
}
}
fn with_font<R>(&self, f: impl FnOnce(&Font) -> R) -> Option<R> {
let bytes = self.font.borrow().clone()?;
let font = Font::from_collection(bytes.as_slice(), self.face_index.get()).ok()?;
Some(f(&font))
}
}
pub(super) fn common_prefix_chars(a: &str, b: &str) -> usize {
a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count()
}
fn crossfade_mid_alpha(p: f32) -> (f32, f32) {
if p < 0.5 {
(1.0 - p * 2.0, 0.0)
} else {
(0.0, (p - 0.5) * 2.0)
}
}
fn char_positions(
font: &Font,
text: &str,
size: f32,
letter_spacing: f32,
line_height: f32,
align: TextAlign,
content_w: f32,
) -> Vec<Option<(f32, f32)>> {
let ascent = font.ascent(size);
let step = font.line_height(size) * line_height;
let lines: Vec<&str> = text.split('\n').collect();
let mut out = Vec::new();
for (i, line) in lines.iter().enumerate() {
let baseline = ascent + i as f32 * step;
let lw = line_width(font, line, size, letter_spacing);
let mut pen = match align {
TextAlign::Left => 0.0,
TextAlign::Center => (content_w - lw) * 0.5,
TextAlign::Right => content_w - lw,
};
for ch in line.chars() {
out.push(Some((pen, baseline)));
pen += font.advance_width(ch, size) + letter_spacing;
}
if i + 1 < lines.len() {
out.push(None);
}
}
out
}
fn place_glyph(builder: &mut PathBuilder, font: &Font, ch: char, size: f32, x: f32, baseline: f32) {
if let Some(glyph) = font.glyph_path(ch, size, x, baseline) {
append_segments(builder, glyph.segments());
}
}
fn group_for(groups: &mut Vec<(Color, PathBuilder)>, color: Color) -> &mut PathBuilder {
match groups.iter().position(|(c, _)| *c == color) {
Some(i) => &mut groups[i].1,
None => {
groups.push((color, PathBuilder::new()));
&mut groups.last_mut().expect("the just-added group").1
}
}
}
fn in_ranges(i: usize, ranges: &[(usize, usize)]) -> bool {
ranges.iter().any(|&(a, b)| i >= a && i < b)
}
fn resolve_ranges(ranges: &[(TextPos, TextPos)], text: &str) -> Vec<(usize, usize)> {
ranges
.iter()
.map(|(from, to)| {
let mut a = from.resolve(text);
let mut b = to.resolve(text);
if a > b {
std::mem::swap(&mut a, &mut b);
}
(a, b)
})
.collect()
}
fn alpha_for(i: usize, ranges: &[(usize, usize)]) -> f32 {
if ranges.is_empty() || in_ranges(i, ranges) {
1.0
} else {
DEFAULT_DIM
}
}
fn alpha_group(groups: &mut Vec<(f32, PathBuilder)>, alpha: f32) -> &mut PathBuilder {
match groups.iter().position(|(a, _)| (a - alpha).abs() < 1e-4) {
Some(i) => &mut groups[i].1,
None => {
groups.push((alpha, PathBuilder::new()));
&mut groups.last_mut().expect("the just-added group").1
}
}
}
fn color_alpha_group(
groups: &mut Vec<(Color, f32, PathBuilder)>,
color: Color,
alpha: f32,
) -> &mut PathBuilder {
match groups.iter().position(|(c, a, _)| *c == color && (a - alpha).abs() < 1e-4) {
Some(i) => &mut groups[i].2,
None => {
groups.push((color, alpha, PathBuilder::new()));
&mut groups.last_mut().expect("the just-added group").2
}
}
}
fn finish_groups(groups: Vec<(Color, PathBuilder)>, alpha: f32) -> Vec<(Rc<Path>, Color, f32)> {
groups
.into_iter()
.filter_map(|(color, builder)| builder.finish().map(|path| (Rc::new(path), color, alpha)))
.collect()
}
enum DiffOp {
Common { a: usize, b: usize, len: usize },
Replace { a: usize, alen: usize, b: usize, blen: usize },
}
const MIN_COMMON_RUN: usize = 3;
fn diff_runs(a: &[char], b: &[char], ai: usize, bi: usize, out: &mut Vec<DiffOp>) {
if a.is_empty() && b.is_empty() {
return;
}
let (la, lb, len) = longest_common_substring(a, b);
let whole = len == a.len() && len == b.len();
if len == 0 || (len < MIN_COMMON_RUN && !whole) {
out.push(DiffOp::Replace { a: ai, alen: a.len(), b: bi, blen: b.len() });
return;
}
diff_runs(&a[..la], &b[..lb], ai, bi, out);
out.push(DiffOp::Common { a: ai + la, b: bi + lb, len });
diff_runs(&a[la + len..], &b[lb + len..], ai + la + len, bi + lb + len, out);
}
fn longest_common_substring(a: &[char], b: &[char]) -> (usize, usize, usize) {
if a.is_empty() || b.is_empty() {
return (0, 0, 0);
}
let (mut best_a, mut best_b, mut best) = (0, 0, 0);
let mut prev = vec![0usize; b.len() + 1];
let mut curr = vec![0usize; b.len() + 1];
for i in 1..=a.len() {
for j in 1..=b.len() {
curr[j] = if a[i - 1] == b[j - 1] { prev[j - 1] + 1 } else { 0 };
if curr[j] > best {
best = curr[j];
best_a = i - curr[j];
best_b = j - curr[j];
}
}
std::mem::swap(&mut prev, &mut curr);
}
(best_a, best_b, best)
}
pub(super) fn insert_at(text: &str, char_index: usize, ins: &str) -> String {
let count = text.chars().count();
let at = char_to_byte(text, char_index.min(count));
let mut out = String::with_capacity(text.len() + ins.len());
out.push_str(&text[..at]);
out.push_str(ins);
out.push_str(&text[at..]);
out
}
pub(super) fn rewrite_range(text: &str, from: TextPos, to: TextPos, ins: &str) -> String {
let mut a = from.resolve(text);
let mut b = to.resolve(text);
if a > b {
std::mem::swap(&mut a, &mut b);
}
let ba = char_to_byte(text, a);
let bb = char_to_byte(text, b);
let mut out = String::with_capacity(text.len() + ins.len());
out.push_str(&text[..ba]);
out.push_str(ins);
out.push_str(&text[bb..]);
out
}
fn char_to_byte(text: &str, char_index: usize) -> usize {
text.char_indices().nth(char_index).map(|(b, _)| b).unwrap_or(text.len())
}
fn line_start_char(text: &str, line: usize) -> usize {
if line == 0 {
return 0;
}
let mut seen = 0;
for (i, ch) in text.chars().enumerate() {
if ch == '\n' {
seen += 1;
if seen == line {
return i + 1;
}
}
}
text.chars().count()
}
fn prefix_chars(text: &str, visible: f32) -> String {
let n = visible.max(0.0).floor() as usize;
text.chars().take(n).collect()
}
fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn line_width(font: &Font, line: &str, size: f32, letter_spacing: f32) -> f32 {
let mut width = 0.0;
let mut count = 0u32;
for ch in line.chars() {
width += font.advance_width(ch, size);
count += 1;
}
if count > 1 {
width += letter_spacing * (count - 1) as f32;
}
width
}
fn measure_block(font: &Font, text: &str, size: f32, letter_spacing: f32, line_height: f32) -> (f32, f32) {
let mut widest = 0.0f32;
let mut lines = 0u32;
for line in text.split('\n') {
lines += 1;
widest = widest.max(line_width(font, line, size, letter_spacing));
}
let lines = lines.max(1);
let step = font.line_height(size) * line_height;
let height = font.ascent(size) + font.descent(size) + (lines - 1) as f32 * step;
(widest, height)
}
fn build_block_path(
font: &Font,
text: &str,
size: f32,
letter_spacing: f32,
line_height: f32,
align: TextAlign,
content_w: f32,
) -> Option<Path> {
let ascent = font.ascent(size);
let step = font.line_height(size) * line_height;
let mut builder = PathBuilder::new();
for (i, line) in text.split('\n').enumerate() {
let baseline = ascent + i as f32 * step;
let lw = line_width(font, line, size, letter_spacing);
let mut pen = match align {
TextAlign::Left => 0.0,
TextAlign::Center => (content_w - lw) * 0.5,
TextAlign::Right => content_w - lw,
};
for ch in line.chars() {
if let Some(glyph) = font.glyph_path(ch, size, pen, baseline) {
append_segments(&mut builder, glyph.segments());
}
pen += font.advance_width(ch, size) + letter_spacing;
}
}
builder.finish()
}
fn append_segments(builder: &mut PathBuilder, segments: &[PathSegment]) {
for seg in segments {
match *seg {
PathSegment::MoveTo(p) => {
builder.move_to(p.x, p.y);
}
PathSegment::LineTo(p) => {
builder.line_to(p.x, p.y);
}
PathSegment::QuadTo(c, p) => {
builder.quad_to(c.x, c.y, p.x, p.y);
}
PathSegment::CubicTo(c1, c2, p) => {
builder.cubic_to(c1.x, c1.y, c2.x, c2.y, p.x, p.y);
}
PathSegment::Close => {
builder.close();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_start_char_indexes_lines() {
let t = "abc\nDEF\nxyz";
assert_eq!(line_start_char(t, 0), 0);
assert_eq!(line_start_char(t, 1), 4); assert_eq!(line_start_char(t, 2), 8);
assert_eq!(line_start_char(t, 3), t.chars().count());
assert_eq!(line_start_char(t, 99), t.chars().count());
}
#[test]
fn rewrite_range_replaces_half_open() {
let t = "abc\nDEF\nxyz";
assert_eq!(rewrite_range(t, TextPos::Char(0), TextPos::Line(1), "X"), "XDEF\nxyz");
assert_eq!(rewrite_range(t, TextPos::Line(1), TextPos::End, "Y"), "abc\nY");
assert_eq!(rewrite_range(t, TextPos::Char(1), TextPos::Char(2), "_"), "a_c\nDEF\nxyz");
}
#[test]
fn rewrite_range_swaps_reversed_bounds() {
let t = "abcdef";
assert_eq!(rewrite_range(t, TextPos::Char(4), TextPos::Char(2), "_"), "ab_ef");
}
#[test]
fn rewrite_range_clamps_out_of_range() {
let t = "abc";
assert_eq!(rewrite_range(t, TextPos::Char(10), TextPos::Char(20), "Z"), "abcZ");
}
#[test]
fn insert_at_inserts_before_index() {
assert_eq!(insert_at("héllo", 0, ">"), ">héllo");
assert_eq!(insert_at("héllo", 2, "-"), "hé-llo");
assert_eq!(insert_at("abc", 99, "!"), "abc!");
}
#[test]
fn common_prefix_counts_shared_chars() {
assert_eq!(common_prefix_chars("Hello", "Hello world"), 5);
assert_eq!(common_prefix_chars("foo", "bar"), 0);
assert_eq!(common_prefix_chars("", "abc"), 0);
assert_eq!(common_prefix_chars("abc", "abc"), 3);
}
fn diff_script(from: &str, to: &str) -> Vec<String> {
let a: Vec<char> = from.chars().collect();
let b: Vec<char> = to.chars().collect();
let mut ops = Vec::new();
diff_runs(&a, &b, 0, 0, &mut ops);
let mut out = Vec::new();
for op in ops {
match op {
DiffOp::Common { a: ai, len, .. } => {
out.push(format!("={}", a[ai..ai + len].iter().collect::<String>()));
}
DiffOp::Replace { a: ai, alen, b: bi, blen } => {
if alen > 0 {
out.push(format!("-{}", a[ai..ai + alen].iter().collect::<String>()));
}
if blen > 0 {
out.push(format!("+{}", b[bi..bi + blen].iter().collect::<String>()));
}
}
}
}
out
}
#[test]
fn diff_runs_keeps_unchanged_middle_when_wrapping() {
assert_eq!(
diff_script("println!();", "{\n println!();\n}"),
vec!["+{\n ", "=println!();", "+\n}"]
);
}
#[test]
fn diff_runs_handles_prepend_append_and_replace() {
assert_eq!(diff_script("bar", "foobar"), vec!["+foo", "=bar"]);
assert_eq!(diff_script("a\nb", "a\nb\nc"), vec!["=a\nb", "+\nc"]);
assert_eq!(diff_script("main.rs", "test.rs"), vec!["-main", "+test", "=.rs"]);
assert_eq!(diff_script("same", "same"), vec!["=same"]);
assert_eq!(diff_script("abc", "xyz"), vec!["-abc", "+xyz"]);
}
#[test]
fn diff_runs_ignores_coincidental_short_runs() {
assert_eq!(
diff_script("Привет мир!", "На дворе {age} год"),
vec!["-Привет мир!", "+На дворе {age} год"]
);
assert_eq!(diff_script("ab xy", "cd ab"), vec!["-ab xy", "+cd ab"]);
assert_eq!(diff_script("xxxcommonyyy", "zzcommonww"), vec!["-xxx", "+zz", "=common", "-yyy", "+ww"]);
}
#[test]
fn diff_runs_keeps_short_identical_text_as_anchor() {
assert_eq!(diff_script("ok", "ok"), vec!["=ok"]);
assert_eq!(diff_script(" ", " "), vec!["= "]);
}
#[test]
fn crossfade_mid_alpha_swaps_through_zero() {
assert_eq!(crossfade_mid_alpha(0.0), (1.0, 0.0));
assert_eq!(crossfade_mid_alpha(0.5), (0.0, 0.0));
assert_eq!(crossfade_mid_alpha(1.0), (0.0, 1.0));
}
#[test]
fn prefix_chars_takes_floor_visible() {
assert_eq!(prefix_chars("abcd", 0.0), "");
assert_eq!(prefix_chars("abcd", 2.9), "ab");
assert_eq!(prefix_chars("abcd", 4.0), "abcd");
assert_eq!(prefix_chars("abcd", 99.0), "abcd");
}
#[test]
fn resolve_ranges_orders_and_resolves_bounds() {
let text = "foo\nbar\nbaz";
assert_eq!(
resolve_ranges(&[(TextPos::Char(0), TextPos::Char(3)), (TextPos::Line(1), TextPos::End)], text),
vec![(0, 3), (4, 11)]
);
assert_eq!(resolve_ranges(&[(TextPos::Char(5), TextPos::Char(2))], text), vec![(2, 5)]);
}
#[test]
fn in_ranges_checks_half_open_membership() {
let r = [(0, 3), (5, 7)];
assert!(in_ranges(0, &r));
assert!(in_ranges(2, &r));
assert!(!in_ranges(3, &r)); assert!(!in_ranges(4, &r)); assert!(in_ranges(6, &r));
assert!(!in_ranges(7, &r));
}
#[test]
fn alpha_for_dims_outside_ranges() {
assert_eq!(alpha_for(0, &[]), 1.0);
let r = [(2usize, 5usize)];
assert_eq!(alpha_for(2, &r), 1.0);
assert_eq!(alpha_for(4, &r), 1.0);
assert_eq!(alpha_for(5, &r), DEFAULT_DIM); assert_eq!(alpha_for(0, &r), DEFAULT_DIM);
}
#[test]
fn committed_highlight_dims_and_clears() {
let t = TextData::new("abcdef".to_string());
assert!(t.highlight_idle());
assert!(t.highlight_alphas("abcdef").is_none());
t.add_highlight(TextPos::Char(1), TextPos::Char(3));
assert_eq!(t.get_highlights().len(), 1);
assert!(!t.highlight_idle());
assert_eq!(
t.highlight_alphas("abcdef").unwrap(),
vec![DEFAULT_DIM, 1.0, 1.0, DEFAULT_DIM, DEFAULT_DIM, DEFAULT_DIM]
);
t.clear_highlights();
assert!(t.highlight_alphas("abcdef").is_none());
}
#[test]
fn morph_stage_interpolates_alpha() {
let t = TextData::new("abc".to_string());
*t.highlight_stage.borrow_mut() = HighlightStage::Morph {
from: vec![],
to: vec![(TextPos::Char(0), TextPos::Char(1))],
p: 0.5,
};
let a = t.highlight_alphas("abc").unwrap();
assert!((a[0] - 1.0).abs() < 1e-6);
let mid = 1.0 + (DEFAULT_DIM - 1.0) * 0.5;
assert!((a[1] - mid).abs() < 1e-6, "a[1]={}", a[1]);
assert!((a[2] - mid).abs() < 1e-6);
}
}