1#![warn(missing_docs)]
16
17mod code;
18
19pub use code::{Lang, Token, highlight};
20
21use std::cell::RefCell;
22use std::hash::{Hash, Hasher};
23use std::rc::Rc;
24
25use kurbo::{Affine, BezPath, Rect};
26use parley::fontique::{Blob, FontInfoOverride};
27use parley::{
28 Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, Layout, LayoutContext,
29 LineHeight, PositionedLayoutItem, StyleProperty,
30};
31use skrifa::instance::{LocationRef, Size};
32use skrifa::outline::{DrawSettings, OutlinePen};
33use skrifa::{FontRef, GlyphId, MetadataProvider};
34
35const INTER: &[u8] = include_bytes!("../fonts/Inter.ttf");
36const JETBRAINS_MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono.ttf");
37
38pub const SANS: &str = "Inter";
40pub const MONO: &str = "JetBrains Mono";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub enum Align {
46 #[default]
47 Start,
49 Center,
51 End,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct TextStyle {
58 pub family: String,
60 pub size: f64,
62 pub weight: f64,
64 pub italic: bool,
66 pub line_height: f64,
68 pub letter_spacing: f64,
70 pub align: Align,
72 pub max_width: Option<f64>,
74}
75
76impl Default for TextStyle {
77 fn default() -> Self {
78 Self {
79 family: SANS.to_owned(),
80 size: 48.0,
81 weight: 400.0,
82 italic: false,
83 line_height: 1.2,
84 letter_spacing: 0.0,
85 align: Align::Start,
86 max_width: None,
87 }
88 }
89}
90
91impl Hash for TextStyle {
92 fn hash<H: Hasher>(&self, h: &mut H) {
93 self.family.hash(h);
94 for v in [
95 self.size,
96 self.weight,
97 self.line_height,
98 self.letter_spacing,
99 ] {
100 v.to_bits().hash(h);
101 }
102 self.italic.hash(h);
103 self.align.hash(h);
104 self.max_width.map(f64::to_bits).hash(h);
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Default)]
110pub struct TextSpan {
111 pub text: String,
113 pub family: Option<String>,
115 pub size: Option<f64>,
117 pub weight: Option<f64>,
119 pub italic: Option<bool>,
121}
122
123impl Hash for TextSpan {
124 fn hash<H: Hasher>(&self, h: &mut H) {
125 self.text.hash(h);
126 self.family.hash(h);
127 self.size.map(f64::to_bits).hash(h);
128 self.weight.map(f64::to_bits).hash(h);
129 self.italic.hash(h);
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct GlyphInfo {
136 pub span: u16,
138 pub ch: char,
140 pub origin: kurbo::Point,
142}
143
144#[derive(Debug, Clone)]
147pub struct TextLayout {
148 pub path: BezPath,
150 pub glyphs: Vec<BezPath>,
152 pub info: Vec<GlyphInfo>,
154 pub width: f64,
156 pub height: f64,
158}
159
160impl TextLayout {
161 pub fn bounds(&self) -> Rect {
163 Rect::new(0.0, 0.0, self.width, self.height)
164 }
165}
166
167struct Fonts {
168 font_cx: FontContext,
169 layout_cx: LayoutContext<u16>,
170 cache: Vec<(u64, Rc<TextLayout>)>,
171}
172
173const CACHE_SIZE: usize = 256;
174
175thread_local! {
176 static FONTS: RefCell<Fonts> = RefCell::new(Fonts::new());
177}
178
179impl Fonts {
180 fn new() -> Self {
181 let mut font_cx = FontContext::new();
182 for data in [INTER, JETBRAINS_MONO] {
183 font_cx
184 .collection
185 .register_fonts(Blob::new(std::sync::Arc::new(data)), None);
186 }
187 Self {
188 font_cx,
189 layout_cx: LayoutContext::new(),
190 cache: Vec::new(),
191 }
192 }
193}
194
195pub fn register_font(data: &'static [u8]) -> Vec<String> {
198 FONTS.with(|f| {
199 let mut f = f.borrow_mut();
200 f.cache.clear();
201 let collection = &mut f.font_cx.collection;
202 let families = collection.register_fonts(
203 Blob::new(std::sync::Arc::new(data)),
204 None::<FontInfoOverride<'_>>,
205 );
206 families
207 .into_iter()
208 .filter_map(|(id, _)| collection.family_name(id).map(str::to_owned))
209 .collect()
210 })
211}
212
213pub fn layout(text: &str, style: &TextStyle) -> Rc<TextLayout> {
215 layout_rich(
216 &[TextSpan {
217 text: text.to_owned(),
218 ..Default::default()
219 }],
220 style,
221 )
222}
223
224pub fn layout_rich(spans: &[TextSpan], style: &TextStyle) -> Rc<TextLayout> {
226 let mut h = std::hash::DefaultHasher::new();
227 spans.hash(&mut h);
228 style.hash(&mut h);
229 let key = h.finish();
230 FONTS.with(|f| {
231 let mut f = f.borrow_mut();
232 if let Some(pos) = f.cache.iter().position(|(k, _)| *k == key) {
233 let entry = f.cache.remove(pos);
234 let value = entry.1.clone();
235 f.cache.push(entry);
236 return value;
237 }
238 let value = Rc::new(build(&mut f, spans, style));
239 if f.cache.len() >= CACHE_SIZE {
240 f.cache.remove(0);
241 }
242 f.cache.push((key, value.clone()));
243 value
244 })
245}
246
247fn build(f: &mut Fonts, spans: &[TextSpan], style: &TextStyle) -> TextLayout {
248 let Fonts {
249 font_cx, layout_cx, ..
250 } = f;
251 let text: String = spans.iter().map(|s| s.text.as_str()).collect();
252 let text = text.as_str();
253 let mut b = layout_cx.ranged_builder(font_cx, text, 1.0, true);
254 b.push_default(StyleProperty::FontFamily(FontFamily::named(&style.family)));
255 b.push_default(StyleProperty::FontSize(style.size as f32));
256 b.push_default(StyleProperty::FontWeight(FontWeight::new(
257 style.weight as f32,
258 )));
259 if style.italic {
260 b.push_default(StyleProperty::FontStyle(parley::FontStyle::Italic));
261 }
262 b.push_default(StyleProperty::LineHeight(LineHeight::FontSizeRelative(
263 style.line_height as f32,
264 )));
265 b.push_default(StyleProperty::LetterSpacing(style.letter_spacing as f32));
266 b.push_default(StyleProperty::Brush(0u16));
267 let mut at = 0;
268 for (i, span) in spans.iter().enumerate() {
269 let r = at..at + span.text.len();
270 at = r.end;
271 if r.is_empty() {
272 continue;
273 }
274 b.push(StyleProperty::Brush(i as u16), r.clone());
275 if let Some(f) = &span.family {
276 b.push(StyleProperty::FontFamily(FontFamily::named(f)), r.clone());
277 }
278 if let Some(sz) = span.size {
279 b.push(StyleProperty::FontSize(sz as f32), r.clone());
280 }
281 if let Some(w) = span.weight {
282 b.push(
283 StyleProperty::FontWeight(FontWeight::new(w as f32)),
284 r.clone(),
285 );
286 }
287 if let Some(it) = span.italic {
288 let st = if it {
289 parley::FontStyle::Italic
290 } else {
291 parley::FontStyle::Normal
292 };
293 b.push(StyleProperty::FontStyle(st), r.clone());
294 }
295 }
296 let mut layout: Layout<u16> = b.build(text);
297 layout.break_all_lines(style.max_width.map(|w| w as f32));
298 let align = match style.align {
299 Align::Start => Alignment::Start,
300 Align::Center => Alignment::Center,
301 Align::End => Alignment::End,
302 };
303 layout.align(align, AlignmentOptions::default());
304
305 let mut path = BezPath::new();
306 let mut glyphs = Vec::new();
307 let mut info = Vec::new();
308 for line in layout.lines() {
309 for item in line.items() {
310 let PositionedLayoutItem::GlyphRun(run) = item else {
311 continue;
312 };
313 let r = run.run();
314 let span = run.style().brush;
315 let chars: Vec<char> = r
317 .visual_clusters()
318 .flat_map(|c| {
319 let ch = text[c.text_range()].chars().next().unwrap_or('\0');
320 std::iter::repeat_n(ch, c.glyphs().count())
321 })
322 .collect();
323 let font = r.font();
324 let Ok(font_ref) = FontRef::from_index(font.data.as_ref(), font.index) else {
325 continue;
326 };
327 let outlines = font_ref.outline_glyphs();
328 let coords = r.normalized_coords();
329 let size = Size::new(r.font_size());
330 let coords: Vec<skrifa::instance::NormalizedCoord> = coords
331 .iter()
332 .map(|c| skrifa::raw::types::F2Dot14::from_bits(c.to_bits()))
333 .collect();
334 let location = LocationRef::new(&coords);
335 let skew = r
337 .synthesis()
338 .skew()
339 .map_or(0.0, |deg| f64::from(deg).to_radians().tan());
340 for (gi, g) in run.positioned_glyphs().enumerate() {
341 let Some(outline) = outlines.get(GlyphId::new(g.id)) else {
342 continue;
343 };
344 let mut pen = Pen(BezPath::new());
345 if outline
346 .draw(DrawSettings::unhinted(size, location), &mut pen)
347 .is_err()
348 {
349 continue;
350 }
351 let tf = Affine::translate((f64::from(g.x), f64::from(g.y)))
353 * Affine::scale_non_uniform(1.0, -1.0)
354 * Affine::skew(skew, 0.0);
355 let glyph = tf * pen.0;
356 if !glyph.elements().is_empty() {
357 path.extend(glyph.elements().iter().copied());
358 glyphs.push(glyph);
359 info.push(GlyphInfo {
360 span,
361 ch: chars.get(gi).copied().unwrap_or('\0'),
362 origin: kurbo::Point::new(f64::from(g.x), f64::from(g.y)),
363 });
364 }
365 }
366 }
367 }
368 TextLayout {
369 path,
370 glyphs,
371 info,
372 width: f64::from(layout.width()),
373 height: f64::from(layout.height()),
374 }
375}
376
377struct Pen(BezPath);
378
379impl OutlinePen for Pen {
380 fn move_to(&mut self, x: f32, y: f32) {
381 self.0.move_to((f64::from(x), f64::from(y)));
382 }
383 fn line_to(&mut self, x: f32, y: f32) {
384 self.0.line_to((f64::from(x), f64::from(y)));
385 }
386 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
387 self.0.quad_to(
388 (f64::from(cx0), f64::from(cy0)),
389 (f64::from(x), f64::from(y)),
390 );
391 }
392 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
393 self.0.curve_to(
394 (f64::from(cx0), f64::from(cy0)),
395 (f64::from(cx1), f64::from(cy1)),
396 (f64::from(x), f64::from(y)),
397 );
398 }
399 fn close(&mut self) {
400 self.0.close_path();
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use kurbo::Shape;
407
408 use super::*;
409
410 #[test]
411 fn lays_out_text_with_embedded_font() {
412 let l = layout("Hello", &TextStyle::default());
413 assert_eq!(l.glyphs.len(), 5);
414 assert!(l.width > 100.0 && l.width < 160.0, "width {}", l.width);
415 let bb = l.path.bounding_box();
416 assert!(bb.y0 >= 0.0 && bb.y1 <= l.height, "{bb:?} h={}", l.height);
417 }
418
419 #[test]
420 fn mono_and_weight_change_shapes() {
421 let s = TextStyle::default();
422 let regular = layout("iii", &s);
423 let mono = layout(
424 "iii",
425 &TextStyle {
426 family: MONO.into(),
427 ..s.clone()
428 },
429 );
430 let bold = layout("iii", &TextStyle { weight: 800.0, ..s });
431 assert!(mono.width > regular.width);
432 assert_ne!(bold.path, regular.path);
433 }
434
435 #[test]
436 fn wraps_and_centers() {
437 let s = TextStyle {
438 max_width: Some(120.0),
439 align: Align::Center,
440 ..Default::default()
441 };
442 let l = layout("one two three four", &s);
443 assert!(l.height > 2.0 * 48.0);
444 }
445
446 #[test]
447 fn rich_spans_carry_index_and_chars() {
448 let spans = [
449 TextSpan {
450 text: "ab ".into(),
451 ..Default::default()
452 },
453 TextSpan {
454 text: "cd".into(),
455 weight: Some(800.0),
456 ..Default::default()
457 },
458 ];
459 let l = layout_rich(&spans, &TextStyle::default());
460 let got: Vec<(u16, char)> = l.info.iter().map(|i| (i.span, i.ch)).collect();
461 assert_eq!(got, vec![(0, 'a'), (0, 'b'), (1, 'c'), (1, 'd')]);
462 assert!(l.info[1].origin.x > l.info[0].origin.x);
463 }
464
465 #[test]
466 fn cyrillic_is_supported() {
467 let l = layout("Привет", &TextStyle::default());
468 assert_eq!(l.glyphs.len(), 6);
469 }
470}