1use 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#[derive(Debug, Clone, PartialEq, Default)]
12pub struct RichSpan {
13 pub text: String,
15 pub color: Option<Color>,
17 pub weight: Option<f64>,
19 pub italic: Option<bool>,
21 pub size: Option<f64>,
23 pub family: Option<String>,
25}
26
27impl RichSpan {
28 pub fn new(text: impl Into<String>) -> Self {
30 Self {
31 text: text.into(),
32 ..Default::default()
33 }
34 }
35 pub fn color(mut self, c: Color) -> Self {
37 self.color = Some(c);
38 self
39 }
40 pub fn weight(mut self, w: f64) -> Self {
42 self.weight = Some(w);
43 self
44 }
45 pub fn bold(self) -> Self {
47 self.weight(700.0)
48 }
49 pub fn italic(mut self) -> Self {
51 self.italic = Some(true);
52 self
53 }
54 pub fn size(mut self, s: f64) -> Self {
56 self.size = Some(s);
57 self
58 }
59 pub fn family(mut self, f: impl Into<String>) -> Self {
61 self.family = Some(f.into());
62 self
63 }
64}
65
66pub fn span(text: impl Into<String>) -> RichSpan {
68 RichSpan::new(text)
69}
70
71impl 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#[derive(Debug, Clone, PartialEq)]
81pub struct CodeTheme {
82 pub plain: Color,
84 pub keyword: Color,
86 pub types: Color,
88 pub function: Color,
90 pub macros: Color,
92 pub string: Color,
94 pub number: Color,
96 pub comment: Color,
98 pub attribute: Color,
100 pub punct: Color,
102}
103
104impl CodeTheme {
105 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 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 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
162pub(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
186pub(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 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
228pub(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 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}