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