use std::rc::Rc;
use kurbo::{Affine, BezPath, Point, Vec2};
use oxframe_core::{Color, Lerp};
use oxframe_text::{Lang, TextLayout, TextSpan, TextStyle, Token};
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RichSpan {
pub text: String,
pub color: Option<Color>,
pub weight: Option<f64>,
pub italic: Option<bool>,
pub size: Option<f64>,
pub family: Option<String>,
}
impl RichSpan {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
pub fn weight(mut self, w: f64) -> Self {
self.weight = Some(w);
self
}
pub fn bold(self) -> Self {
self.weight(700.0)
}
pub fn italic(mut self) -> Self {
self.italic = Some(true);
self
}
pub fn size(mut self, s: f64) -> Self {
self.size = Some(s);
self
}
pub fn family(mut self, f: impl Into<String>) -> Self {
self.family = Some(f.into());
self
}
}
pub fn span(text: impl Into<String>) -> RichSpan {
RichSpan::new(text)
}
impl Lerp for RichSpan {
fn lerp(&self, to: &Self, t: f64) -> Self {
if t < 0.5 { self.clone() } else { to.clone() }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CodeTheme {
pub plain: Color,
pub keyword: Color,
pub types: Color,
pub function: Color,
pub macros: Color,
pub string: Color,
pub number: Color,
pub comment: Color,
pub attribute: Color,
pub punct: Color,
}
impl CodeTheme {
pub fn dark() -> Self {
let h = |s| Color::from_hex(s).expect("valid color");
Self {
plain: h("#abb2bf"),
keyword: h("#c678dd"),
types: h("#e5c07b"),
function: h("#61afef"),
macros: h("#56b6c2"),
string: h("#98c379"),
number: h("#d19a66"),
comment: h("#7f848e"),
attribute: h("#e06c75"),
punct: h("#abb2bf"),
}
}
pub fn light() -> Self {
let h = |s| Color::from_hex(s).expect("valid color");
Self {
plain: h("#383a42"),
keyword: h("#a626a4"),
types: h("#c18401"),
function: h("#4078f2"),
macros: h("#0184bc"),
string: h("#50a14f"),
number: h("#986801"),
comment: h("#6b6f78"),
attribute: h("#e45649"),
punct: h("#383a42"),
}
}
pub fn color(&self, t: Token) -> Color {
match t {
Token::Plain => self.plain,
Token::Keyword => self.keyword,
Token::Type => self.types,
Token::Function => self.function,
Token::Macro => self.macros,
Token::String => self.string,
Token::Number => self.number,
Token::Comment => self.comment,
Token::Attribute => self.attribute,
Token::Punct => self.punct,
}
}
}
impl Default for CodeTheme {
fn default() -> Self {
Self::dark()
}
}
pub(crate) struct Styled {
pub layout: Rc<TextLayout>,
pub colors: Vec<Option<Color>>,
}
pub(crate) fn rich(spans: &[RichSpan], style: &TextStyle) -> Styled {
let ts: Vec<TextSpan> = spans
.iter()
.map(|s| TextSpan {
text: s.text.clone(),
family: s.family.clone(),
size: s.size,
weight: s.weight,
italic: s.italic,
})
.collect();
Styled {
layout: oxframe_text::layout_rich(&ts, style),
colors: spans.iter().map(|s| s.color).collect(),
}
}
pub(crate) fn code(
src: &str,
lang: Lang,
theme: &CodeTheme,
style: &TextStyle,
focus: Vec2,
dim: f64,
) -> Styled {
let (a, b) = (focus.x.round(), focus.y.round());
let focused = |line: usize| a <= 0.0 && b <= 0.0 || (line as f64 >= a && line as f64 <= b);
let mut spans = Vec::new();
for (range, tok) in oxframe_text::highlight(src, lang) {
let mut start = range.start;
for (i, ch) in src[range.clone()].char_indices() {
if ch == '\n' {
spans.push((start..range.start + i + 1, tok));
start = range.start + i + 1;
}
}
if start < range.end {
spans.push((start..range.end, tok));
}
}
let mut out = Vec::with_capacity(spans.len());
for (r, tok) in spans {
let line = src[..r.start].matches('\n').count() + 1;
let mut c = theme.color(tok);
if !focused(line) {
c = c.with_alpha((f64::from(c.a) * dim.clamp(0.0, 1.0)) as f32);
}
out.push(RichSpan {
text: src[r].to_owned(),
color: Some(c),
..Default::default()
});
}
rich(&out, style)
}
pub(crate) fn morph(
from: &TextLayout,
to: &TextLayout,
t: f64,
) -> (Vec<(BezPath, f64)>, (f64, f64)) {
let a: Vec<char> = from.info.iter().map(|g| g.ch).collect();
let b: Vec<char> = to.info.iter().map(|g| g.ch).collect();
let (n, m) = (a.len(), b.len());
let mut dp = vec![vec![0u16; m + 1]; n + 1];
for i in (0..n).rev() {
for j in (0..m).rev() {
dp[i][j] = if a[i] == b[j] {
dp[i + 1][j + 1] + 1
} else {
dp[i + 1][j].max(dp[i][j + 1])
};
}
}
let mut pairs = Vec::new();
let (mut i, mut j) = (0, 0);
while i < n && j < m {
if a[i] == b[j] {
pairs.push((i, j));
i += 1;
j += 1;
} else if dp[i + 1][j] >= dp[i][j + 1] {
i += 1;
} else {
j += 1;
}
}
let off_from = Vec2::new(-from.width / 2.0, -from.height / 2.0);
let off_to = Vec2::new(-to.width / 2.0, -to.height / 2.0);
let mut out = Vec::new();
let mut matched_a = vec![false; n];
let mut matched_b = vec![false; m];
for &(i, j) in &pairs {
matched_a[i] = true;
matched_b[j] = true;
let pa = from.info[i].origin + off_from;
let pb = to.info[j].origin + off_to;
let shift = Lerp::lerp(&Point::ZERO, &(pb - pa).to_point(), t).to_vec2() + off_from;
out.push((Affine::translate(shift) * from.glyphs[i].clone(), 1.0));
}
for (i, g) in from.glyphs.iter().enumerate() {
if !matched_a[i] {
out.push((Affine::translate(off_from) * g.clone(), 1.0 - t));
}
}
for (j, g) in to.glyphs.iter().enumerate() {
if !matched_b[j] {
out.push((Affine::translate(off_to) * g.clone(), t));
}
}
let size = (
from.width.lerp(&to.width, t),
from.height.lerp(&to.height, t),
);
(out, size)
}