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