Skip to main content

oxframe_2d/
text.rs

1//! Styled text for `Txt` and `Code`: rich spans, syntax themes, focus
2//! dimming and glyph-level morphing between two strings.
3
4use std::rc::Rc;
5
6use kurbo::{Affine, BezPath, Point, Vec2};
7use oxframe_core::{Color, Lerp};
8use oxframe_text::{Lang, TextLayout, TextSpan, TextStyle, Token};
9
10/// A run of text with its own look; unset fields use the node's style.
11#[derive(Debug, Clone, PartialEq, Default)]
12pub struct RichSpan {
13    /// The text of this run.
14    pub text: String,
15    /// Color; default: the node's fill.
16    pub color: Option<Color>,
17    /// Font weight 100..=900; default: the node's.
18    pub weight: Option<f64>,
19    /// Italic; default: the node's.
20    pub italic: Option<bool>,
21    /// Font size; default: the node's.
22    pub size: Option<f64>,
23    /// Font family; default: the node's.
24    pub family: Option<String>,
25}
26
27impl RichSpan {
28    /// A run with the node's style; same as [`span`].
29    pub fn new(text: impl Into<String>) -> Self {
30        Self {
31            text: text.into(),
32            ..Default::default()
33        }
34    }
35    /// Text color.
36    pub fn color(mut self, c: Color) -> Self {
37        self.color = Some(c);
38        self
39    }
40    /// Font weight, 100..=900.
41    pub fn weight(mut self, w: f64) -> Self {
42        self.weight = Some(w);
43        self
44    }
45    /// Weight 700.
46    pub fn bold(self) -> Self {
47        self.weight(700.0)
48    }
49    /// Italic style.
50    pub fn italic(mut self) -> Self {
51        self.italic = Some(true);
52        self
53    }
54    /// Font size in pixels.
55    pub fn size(mut self, s: f64) -> Self {
56        self.size = Some(s);
57        self
58    }
59    /// Font family, e.g. [`MONO`](crate::MONO).
60    pub fn family(mut self, f: impl Into<String>) -> Self {
61        self.family = Some(f.into());
62        self
63    }
64}
65
66/// Shorthand for [`RichSpan::new`].
67pub fn span(text: impl Into<String>) -> RichSpan {
68    RichSpan::new(text)
69}
70
71/// Strings tween as a typewriter over the concatenated text, keeping the
72/// styles of the target spans.
73impl Lerp for RichSpan {
74    fn lerp(&self, to: &Self, t: f64) -> Self {
75        if t < 0.5 { self.clone() } else { to.clone() }
76    }
77}
78
79/// Colors of a code theme.
80#[derive(Debug, Clone, PartialEq)]
81pub struct CodeTheme {
82    /// Text that is not a token below.
83    pub plain: Color,
84    /// `fn`, `let`, `if`…
85    pub keyword: Color,
86    /// Type names.
87    pub types: Color,
88    /// Function names.
89    pub function: Color,
90    /// Macro names.
91    pub macros: Color,
92    /// String literals.
93    pub string: Color,
94    /// Number literals.
95    pub number: Color,
96    /// Comments.
97    pub comment: Color,
98    /// Attributes and decorators.
99    pub attribute: Color,
100    /// Punctuation and operators.
101    pub punct: Color,
102}
103
104impl CodeTheme {
105    /// One Dark.
106    pub fn dark() -> Self {
107        let h = |s| Color::from_hex(s).expect("valid color");
108        Self {
109            plain: h("#abb2bf"),
110            keyword: h("#c678dd"),
111            types: h("#e5c07b"),
112            function: h("#61afef"),
113            macros: h("#56b6c2"),
114            string: h("#98c379"),
115            number: h("#d19a66"),
116            comment: h("#7f848e"),
117            attribute: h("#e06c75"),
118            punct: h("#abb2bf"),
119        }
120    }
121
122    /// One Light.
123    pub fn light() -> Self {
124        let h = |s| Color::from_hex(s).expect("valid color");
125        Self {
126            plain: h("#383a42"),
127            keyword: h("#a626a4"),
128            types: h("#c18401"),
129            function: h("#4078f2"),
130            macros: h("#0184bc"),
131            string: h("#50a14f"),
132            number: h("#986801"),
133            comment: h("#6b6f78"),
134            attribute: h("#e45649"),
135            punct: h("#383a42"),
136        }
137    }
138
139    /// Color for a token kind.
140    pub fn color(&self, t: Token) -> Color {
141        match t {
142            Token::Plain => self.plain,
143            Token::Keyword => self.keyword,
144            Token::Type => self.types,
145            Token::Function => self.function,
146            Token::Macro => self.macros,
147            Token::String => self.string,
148            Token::Number => self.number,
149            Token::Comment => self.comment,
150            Token::Attribute => self.attribute,
151            Token::Punct => self.punct,
152        }
153    }
154}
155
156impl Default for CodeTheme {
157    fn default() -> Self {
158        Self::dark()
159    }
160}
161
162/// Text ready to draw: layout plus the color of each span (`None` = node
163/// fill).
164pub(crate) struct Styled {
165    pub layout: Rc<TextLayout>,
166    pub colors: Vec<Option<Color>>,
167}
168
169pub(crate) fn rich(spans: &[RichSpan], style: &TextStyle) -> Styled {
170    let ts: Vec<TextSpan> = spans
171        .iter()
172        .map(|s| TextSpan {
173            text: s.text.clone(),
174            family: s.family.clone(),
175            size: s.size,
176            weight: s.weight,
177            italic: s.italic,
178        })
179        .collect();
180    Styled {
181        layout: oxframe_text::layout_rich(&ts, style),
182        colors: spans.iter().map(|s| s.color).collect(),
183    }
184}
185
186/// Highlighted code; lines outside `focus` (1-based, inclusive; (0, 0) =
187/// all) get their alpha multiplied by `dim`.
188pub(crate) fn code(
189    src: &str,
190    lang: Lang,
191    theme: &CodeTheme,
192    style: &TextStyle,
193    focus: Vec2,
194    dim: f64,
195) -> Styled {
196    let (a, b) = (focus.x.round(), focus.y.round());
197    let focused = |line: usize| a <= 0.0 && b <= 0.0 || (line as f64 >= a && line as f64 <= b);
198    let mut spans = Vec::new();
199    for (range, tok) in oxframe_text::highlight(src, lang) {
200        // Split tokens at newlines so each piece knows its line.
201        let mut start = range.start;
202        for (i, ch) in src[range.clone()].char_indices() {
203            if ch == '\n' {
204                spans.push((start..range.start + i + 1, tok));
205                start = range.start + i + 1;
206            }
207        }
208        if start < range.end {
209            spans.push((start..range.end, tok));
210        }
211    }
212    let mut out = Vec::with_capacity(spans.len());
213    for (r, tok) in spans {
214        let line = src[..r.start].matches('\n').count() + 1;
215        let mut c = theme.color(tok);
216        if !focused(line) {
217            c = c.with_alpha((f64::from(c.a) * dim.clamp(0.0, 1.0)) as f32);
218        }
219        out.push(RichSpan {
220            text: src[r].to_owned(),
221            color: Some(c),
222            ..Default::default()
223        });
224    }
225    rich(&out, style)
226}
227
228/// Glyphs of a morph frame: (path, alpha) in the text box centered at 0.
229pub(crate) fn morph(
230    from: &TextLayout,
231    to: &TextLayout,
232    t: f64,
233) -> (Vec<(BezPath, f64)>, (f64, f64)) {
234    let a: Vec<char> = from.info.iter().map(|g| g.ch).collect();
235    let b: Vec<char> = to.info.iter().map(|g| g.ch).collect();
236    // Longest common subsequence of characters: matched glyphs travel.
237    let (n, m) = (a.len(), b.len());
238    let mut dp = vec![vec![0u16; m + 1]; n + 1];
239    for i in (0..n).rev() {
240        for j in (0..m).rev() {
241            dp[i][j] = if a[i] == b[j] {
242                dp[i + 1][j + 1] + 1
243            } else {
244                dp[i + 1][j].max(dp[i][j + 1])
245            };
246        }
247    }
248    let mut pairs = Vec::new();
249    let (mut i, mut j) = (0, 0);
250    while i < n && j < m {
251        if a[i] == b[j] {
252            pairs.push((i, j));
253            i += 1;
254            j += 1;
255        } else if dp[i + 1][j] >= dp[i][j + 1] {
256            i += 1;
257        } else {
258            j += 1;
259        }
260    }
261    let off_from = Vec2::new(-from.width / 2.0, -from.height / 2.0);
262    let off_to = Vec2::new(-to.width / 2.0, -to.height / 2.0);
263    let mut out = Vec::new();
264    let mut matched_a = vec![false; n];
265    let mut matched_b = vec![false; m];
266    for &(i, j) in &pairs {
267        matched_a[i] = true;
268        matched_b[j] = true;
269        let pa = from.info[i].origin + off_from;
270        let pb = to.info[j].origin + off_to;
271        let shift = Lerp::lerp(&Point::ZERO, &(pb - pa).to_point(), t).to_vec2() + off_from;
272        out.push((Affine::translate(shift) * from.glyphs[i].clone(), 1.0));
273    }
274    for (i, g) in from.glyphs.iter().enumerate() {
275        if !matched_a[i] {
276            out.push((Affine::translate(off_from) * g.clone(), 1.0 - t));
277        }
278    }
279    for (j, g) in to.glyphs.iter().enumerate() {
280        if !matched_b[j] {
281            out.push((Affine::translate(off_to) * g.clone(), t));
282        }
283    }
284    let size = (
285        from.width.lerp(&to.width, t),
286        from.height.lerp(&to.height, t),
287    );
288    (out, size)
289}