Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 18 kB View raw
1use core::ops::Range; 2use std::sync::Arc; 3 4use parley::fontique::{Blob, Collection, CollectionOptions, SourceCache}; 5use parley::{ 6 Alignment, AlignmentOptions, FontContext, FontFamily, GlyphRun, Layout, LayoutContext, 7 LineHeight as ParleyLineHeight, PositionedLayoutItem, StyleProperty, 8}; 9 10use crate::fonts::{MONO_DATA, MONO_FAMILY, SANS_DATA, SANS_FAMILY, family_for, parley_weight}; 11use crate::{FontFace, FontWeight}; 12 13#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 14pub struct GlyphId(u32); 15 16impl GlyphId { 17 #[must_use] 18 pub const fn new(value: u32) -> Self { 19 Self(value) 20 } 21 22 #[must_use] 23 pub const fn raw(self) -> u32 { 24 self.0 25 } 26} 27 28#[derive( 29 Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, 30)] 31#[serde(transparent)] 32pub struct SourceByteIndex(usize); 33 34impl SourceByteIndex { 35 #[must_use] 36 pub const fn new(value: usize) -> Self { 37 Self(value) 38 } 39 40 #[must_use] 41 pub const fn value(self) -> usize { 42 self.0 43 } 44} 45 46#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] 47pub struct MaxWidth(f32); 48 49impl MaxWidth { 50 #[must_use] 51 pub fn new(width_px: f32) -> Option<Self> { 52 if width_px > 0.0 && width_px.is_finite() { 53 Some(Self(width_px)) 54 } else { 55 None 56 } 57 } 58 59 #[must_use] 60 pub const fn px(self) -> f32 { 61 self.0 62 } 63} 64 65#[derive(Copy, Clone, Debug, PartialEq)] 66pub struct ShapeRequest { 67 pub face: FontFace, 68 pub size_px: f32, 69 pub weight: FontWeight, 70 pub line_height_px: f32, 71 pub letter_spacing_px: f32, 72 pub max_width: Option<MaxWidth>, 73} 74 75#[derive(Clone, Debug, PartialEq)] 76pub struct ShapedText { 77 pub face: FontFace, 78 pub font_size_px: f32, 79 pub lines: Vec<ShapedLine>, 80 pub width_px: f32, 81 pub height_px: f32, 82} 83 84#[derive(Clone, Debug, PartialEq)] 85pub struct ShapedLine { 86 pub runs: Vec<ShapedRun>, 87 pub baseline_px: f32, 88 pub ascent_px: f32, 89 pub descent_px: f32, 90 pub advance_px: f32, 91 pub trailing_whitespace_px: f32, 92 pub source_range: Range<SourceByteIndex>, 93} 94 95impl ShapedLine { 96 #[must_use] 97 pub fn visible_advance_px(&self) -> f32 { 98 (self.advance_px - self.trailing_whitespace_px).max(0.0) 99 } 100} 101 102#[derive(Clone, Debug, PartialEq)] 103pub struct ShapedRun { 104 pub glyphs: Vec<ShapedGlyph>, 105 pub origin_x_px: f32, 106 pub advance_px: f32, 107 pub is_rtl: bool, 108 pub source_range: Range<SourceByteIndex>, 109} 110 111#[derive(Copy, Clone, Debug, PartialEq)] 112pub struct ShapedGlyph { 113 pub id: GlyphId, 114 pub x_px: f32, 115 pub y_px: f32, 116 pub advance_px: f32, 117 pub cluster: SourceByteIndex, 118} 119 120pub struct Shaper { 121 fonts: FontContext, 122 layout: LayoutContext<()>, 123} 124 125impl Shaper { 126 #[must_use] 127 pub fn new() -> Self { 128 let mut collection = Collection::new(CollectionOptions { 129 shared: false, 130 system_fonts: false, 131 }); 132 register(&mut collection, SANS_DATA, SANS_FAMILY); 133 register(&mut collection, MONO_DATA, MONO_FAMILY); 134 Self { 135 fonts: FontContext { 136 collection, 137 source_cache: SourceCache::default(), 138 }, 139 layout: LayoutContext::new(), 140 } 141 } 142 143 pub fn shape(&mut self, text: &str, request: ShapeRequest) -> ShapedText { 144 let family = family_for(request.face); 145 let max_advance = request.max_width.map(MaxWidth::px); 146 let mut builder = self.layout.ranged_builder(&mut self.fonts, text, 1.0, true); 147 builder.push_default(StyleProperty::FontFamily(FontFamily::named(family))); 148 builder.push_default(StyleProperty::FontSize(request.size_px)); 149 builder.push_default(StyleProperty::FontWeight(parley_weight(request.weight))); 150 builder.push_default(StyleProperty::Brush(())); 151 if request.line_height_px > 0.0 { 152 builder.push_default(StyleProperty::LineHeight(ParleyLineHeight::Absolute( 153 request.line_height_px, 154 ))); 155 } 156 if request.letter_spacing_px != 0.0 { 157 builder.push_default(StyleProperty::LetterSpacing(request.letter_spacing_px)); 158 } 159 let mut layout: Layout<()> = builder.build(text); 160 layout.break_all_lines(max_advance); 161 layout.align(Alignment::Start, AlignmentOptions::default()); 162 ShapedText::from_layout(request.face, request.size_px, &layout) 163 } 164} 165 166impl Default for Shaper { 167 fn default() -> Self { 168 Self::new() 169 } 170} 171 172impl core::fmt::Debug for Shaper { 173 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 174 f.debug_struct("Shaper").finish_non_exhaustive() 175 } 176} 177 178impl ShapedText { 179 fn from_layout(face: FontFace, font_size_px: f32, layout: &Layout<()>) -> Self { 180 let lines = layout.lines().map(ShapedLine::from_line).collect(); 181 Self { 182 face, 183 font_size_px, 184 lines, 185 width_px: layout.width().max(0.0), 186 height_px: layout.height().max(0.0), 187 } 188 } 189 190 #[must_use] 191 pub fn line_count(&self) -> usize { 192 self.lines.len() 193 } 194 195 #[must_use] 196 pub fn glyph_count(&self) -> usize { 197 self.lines 198 .iter() 199 .flat_map(|line| line.runs.iter()) 200 .map(|run| run.glyphs.len()) 201 .sum() 202 } 203} 204 205impl ShapedLine { 206 fn from_line(line: parley::Line<'_, ()>) -> Self { 207 let metrics = line.metrics(); 208 let runs = line 209 .items() 210 .filter_map(|item| match item { 211 PositionedLayoutItem::GlyphRun(run) => Some(ShapedRun::from_glyph_run(&run)), 212 PositionedLayoutItem::InlineBox(_) => None, 213 }) 214 .collect(); 215 Self { 216 runs, 217 baseline_px: metrics.baseline.max(0.0), 218 ascent_px: metrics.ascent.max(0.0), 219 descent_px: metrics.descent.max(0.0), 220 advance_px: metrics.advance.max(0.0), 221 trailing_whitespace_px: metrics.trailing_whitespace.max(0.0), 222 source_range: byte_range(line.text_range()), 223 } 224 } 225} 226 227impl ShapedRun { 228 fn from_glyph_run(run: &GlyphRun<'_, ()>) -> Self { 229 let parley_run = run.run(); 230 let glyphs = parley_run 231 .visual_clusters() 232 .flat_map(|cluster| { 233 let byte = SourceByteIndex::new(cluster.text_range().start); 234 cluster.glyphs().map(move |g| (byte, g)) 235 }) 236 .scan(0.0_f32, |cursor, (byte, g)| { 237 let item = ShapedGlyph { 238 id: GlyphId::new(g.id), 239 x_px: *cursor + g.x, 240 y_px: g.y, 241 advance_px: g.advance.max(0.0), 242 cluster: byte, 243 }; 244 *cursor += g.advance; 245 Some(item) 246 }) 247 .collect(); 248 Self { 249 glyphs, 250 origin_x_px: run.offset(), 251 advance_px: parley_run.advance().max(0.0), 252 is_rtl: parley_run.is_rtl(), 253 source_range: byte_range(parley_run.text_range()), 254 } 255 } 256} 257 258fn byte_range(r: Range<usize>) -> Range<SourceByteIndex> { 259 SourceByteIndex::new(r.start)..SourceByteIndex::new(r.end) 260} 261 262fn register(collection: &mut Collection, data: &'static [u8], family: &'static str) { 263 let blob = Blob::new(Arc::new(data)); 264 let registered = collection.register_fonts(blob, None); 265 assert!( 266 !registered.is_empty(), 267 "bundled font `{family}` failed to register; binary asset is broken", 268 ); 269} 270 271#[cfg(test)] 272mod tests { 273 use super::{ 274 FontFace, FontWeight, MaxWidth, ShapeRequest, ShapedText, Shaper, SourceByteIndex, 275 }; 276 277 fn req(face: FontFace, size_px: f32, max_width: Option<MaxWidth>) -> ShapeRequest { 278 ShapeRequest { 279 face, 280 size_px, 281 weight: FontWeight::Regular, 282 line_height_px: 0.0, 283 letter_spacing_px: 0.0, 284 max_width, 285 } 286 } 287 288 fn shape(text: &str, request: ShapeRequest) -> ShapedText { 289 Shaper::new().shape(text, request) 290 } 291 292 #[test] 293 fn max_width_rejects_zero_and_negative_and_non_finite() { 294 assert!(MaxWidth::new(0.0).is_none()); 295 assert!(MaxWidth::new(-1.0).is_none()); 296 assert!(MaxWidth::new(f32::NAN).is_none()); 297 assert!(MaxWidth::new(f32::INFINITY).is_none()); 298 assert!(MaxWidth::new(0.5).is_some()); 299 } 300 301 #[test] 302 fn empty_text_yields_one_empty_line_with_positive_height() { 303 let out = shape("", req(FontFace::Sans, 13.0, None)); 304 assert_eq!(out.line_count(), 1); 305 assert_eq!(out.glyph_count(), 0); 306 assert!(out.lines[0].runs.is_empty()); 307 assert!(out.height_px > 0.0); 308 assert!(out.width_px.abs() < f32::EPSILON); 309 } 310 311 #[test] 312 fn shaped_text_carries_request_face_and_size() { 313 let out = shape("hi", req(FontFace::Mono, 15.0, None)); 314 assert_eq!(out.face, FontFace::Mono); 315 assert!((out.font_size_px - 15.0).abs() < 0.01); 316 } 317 318 #[test] 319 fn ascii_run_emits_one_glyph_per_char() { 320 let text = "Sketch"; 321 let out = shape(text, req(FontFace::Sans, 13.0, None)); 322 assert_eq!(out.line_count(), 1); 323 assert_eq!(out.glyph_count(), text.chars().count()); 324 let line = &out.lines[0]; 325 assert_eq!( 326 line.source_range, 327 SourceByteIndex::new(0)..SourceByteIndex::new(text.len()), 328 ); 329 assert_eq!(line.runs.len(), 1); 330 assert!(!line.runs[0].is_rtl); 331 assert!(out.width_px > 0.0); 332 } 333 334 #[test] 335 fn ascii_clusters_track_byte_offsets_left_to_right() { 336 let out = shape("AB", req(FontFace::Sans, 13.0, None)); 337 let glyphs = &out.lines[0].runs[0].glyphs; 338 assert_eq!(glyphs.len(), 2); 339 assert_eq!(glyphs[0].cluster, SourceByteIndex::new(0)); 340 assert_eq!(glyphs[1].cluster, SourceByteIndex::new(1)); 341 assert!(glyphs[1].x_px > glyphs[0].x_px); 342 } 343 344 #[test] 345 fn line_metrics_expose_positive_ascent_and_descent() { 346 let out = shape("Ag", req(FontFace::Sans, 13.0, None)); 347 let line = &out.lines[0]; 348 assert!(line.ascent_px > 0.0); 349 assert!(line.descent_px > 0.0); 350 assert!(line.baseline_px > 0.0); 351 } 352 353 #[test] 354 fn glyph_y_offsets_lie_within_line_metrics() { 355 let out = shape("Ag", req(FontFace::Sans, 13.0, None)); 356 let line = &out.lines[0]; 357 let max_dy = line.descent_px + 0.5; 358 let min_dy = -line.ascent_px - 0.5; 359 line.runs 360 .iter() 361 .flat_map(|r| r.glyphs.iter()) 362 .for_each(|glyph| { 363 let dy = glyph.y_px; 364 assert!( 365 dy >= min_dy && dy <= max_dy, 366 "glyph dy {dy} outside [{min_dy}, {max_dy}]", 367 ); 368 }); 369 } 370 371 #[test] 372 fn arabic_run_marks_rtl() { 373 let out = shape("مرحبا", req(FontFace::Sans, 13.0, None)); 374 assert_eq!(out.line_count(), 1); 375 let runs = &out.lines[0].runs; 376 assert!(!runs.is_empty()); 377 assert!(runs.iter().all(|r| r.is_rtl)); 378 assert!(runs.iter().any(|r| !r.glyphs.is_empty())); 379 } 380 381 #[test] 382 fn mixed_direction_runs_partition_source_bytes_without_overlap() { 383 let text = "abc مرحبا def"; 384 let out = shape(text, req(FontFace::Sans, 13.0, None)); 385 assert_eq!(out.line_count(), 1); 386 let runs = &out.lines[0].runs; 387 assert!(runs.iter().any(|r| !r.is_rtl), "expected an LTR run"); 388 assert!(runs.iter().any(|r| r.is_rtl), "expected an RTL run"); 389 let mut spans: Vec<_> = runs 390 .iter() 391 .map(|r| (r.source_range.start.value(), r.source_range.end.value())) 392 .collect(); 393 spans.sort_unstable(); 394 assert_eq!(spans.first().map(|s| s.0), Some(0)); 395 assert_eq!(spans.last().map(|s| s.1), Some(text.len())); 396 spans.windows(2).for_each(|pair| { 397 let (_, prev_end) = pair[0]; 398 let (next_start, _) = pair[1]; 399 assert!( 400 next_start >= prev_end, 401 "runs overlap: prev end {prev_end} > next start {next_start}", 402 ); 403 }); 404 } 405 406 #[test] 407 fn multi_glyph_cluster_keeps_shared_source_byte() { 408 let text = "i\u{0307}\u{0301}"; 409 let out = shape(text, req(FontFace::Sans, 13.0, None)); 410 let glyphs = &out.lines[0].runs[0].glyphs; 411 assert!( 412 glyphs.len() >= 2, 413 "expected multi-glyph cluster, got {}", 414 glyphs.len(), 415 ); 416 assert!(glyphs.iter().all(|g| g.cluster == SourceByteIndex::new(0))); 417 } 418 419 #[test] 420 fn precomposed_combining_mark_collapses_to_single_glyph() { 421 let out = shape("e\u{0301}", req(FontFace::Sans, 13.0, None)); 422 let glyphs = &out.lines[0].runs[0].glyphs; 423 assert_eq!(glyphs.len(), 1); 424 assert_eq!(glyphs[0].cluster, SourceByteIndex::new(0)); 425 } 426 427 #[test] 428 fn long_text_wraps_when_max_width_set() { 429 let text = "the quick brown fox jumps over the lazy dog"; 430 let single = shape(text, req(FontFace::Sans, 13.0, None)); 431 assert_eq!(single.line_count(), 1); 432 let cap = MaxWidth::new(60.0); 433 let wrapped = shape(text, req(FontFace::Sans, 13.0, cap)); 434 assert!( 435 wrapped.line_count() >= 2, 436 "expected wrap into multiple lines, got {}", 437 wrapped.line_count(), 438 ); 439 let total: usize = wrapped 440 .lines 441 .iter() 442 .flat_map(|l| l.runs.iter()) 443 .map(|r| r.glyphs.len()) 444 .sum(); 445 assert_eq!(total, single.glyph_count()); 446 } 447 448 #[test] 449 fn trailing_whitespace_surfaces_on_line() { 450 let bare = shape("abc", req(FontFace::Mono, 13.0, None)); 451 let trailed = shape("abc ", req(FontFace::Mono, 13.0, None)); 452 let bare_line = &bare.lines[0]; 453 let trailed_line = &trailed.lines[0]; 454 assert!(bare_line.trailing_whitespace_px < 0.5); 455 assert!(trailed_line.trailing_whitespace_px > bare_line.trailing_whitespace_px); 456 assert!(trailed_line.advance_px > trailed_line.visible_advance_px()); 457 assert!( 458 (trailed_line.visible_advance_px() - bare_line.advance_px).abs() < 0.5, 459 "visible advance with trailing spaces should match bare advance", 460 ); 461 } 462 463 #[test] 464 fn explicit_newline_breaks_into_separate_lines() { 465 let out = shape("a\nb", req(FontFace::Sans, 13.0, None)); 466 assert_eq!(out.line_count(), 2); 467 assert!( 468 out.lines 469 .iter() 470 .all(|l| l.runs.iter().any(|r| !r.glyphs.is_empty())) 471 ); 472 } 473 474 #[test] 475 fn tab_character_emits_one_glyph_per_codepoint() { 476 let out = shape("a\tb", req(FontFace::Mono, 13.0, None)); 477 assert_eq!(out.line_count(), 1); 478 assert_eq!(out.glyph_count(), 3); 479 } 480 481 #[test] 482 fn mono_face_advances_uniformly() { 483 let out = shape("iWiW", req(FontFace::Mono, 12.0, None)); 484 let glyphs = &out.lines[0].runs[0].glyphs; 485 assert_eq!(glyphs.len(), 4); 486 let advances: Vec<f32> = glyphs.iter().map(|g| g.advance_px).collect(); 487 let first = advances[0]; 488 assert!( 489 advances.iter().all(|a| (a - first).abs() < 1e-3), 490 "monospace advances should match: {advances:?}", 491 ); 492 } 493 494 #[test] 495 fn sans_collapses_fi_ligature_mono_does_not() { 496 let sans = shape("fi", req(FontFace::Sans, 13.0, None)); 497 let mono = shape("fi", req(FontFace::Mono, 13.0, None)); 498 assert_eq!(sans.glyph_count(), 1, "DejaVu Sans should ligate fi"); 499 assert_eq!(mono.glyph_count(), 2, "DejaVu Sans Mono should not ligate"); 500 } 501 502 #[test] 503 fn trailing_newline_emits_empty_terminal_line() { 504 let out = shape("a\n", req(FontFace::Sans, 13.0, None)); 505 assert_eq!(out.line_count(), 2); 506 assert_eq!(out.glyph_count(), 1); 507 assert!(out.lines[1].runs.is_empty()); 508 } 509 510 #[test] 511 fn sub_glyph_max_width_does_not_split_a_glyph() { 512 let cap = MaxWidth::new(0.5); 513 assert!(cap.is_some()); 514 let out = shape("abc", req(FontFace::Sans, 13.0, cap)); 515 assert_eq!(out.line_count(), 1); 516 assert_eq!(out.glyph_count(), 3); 517 } 518 519 #[test] 520 fn explicit_line_height_drives_line_advance() { 521 let mut request = req(FontFace::Sans, 13.0, None); 522 let baseline = shape("a", request); 523 request.line_height_px = 48.0; 524 let tall = shape("a", request); 525 assert!( 526 tall.height_px > baseline.height_px + 20.0, 527 "tall line height must stretch bounds: baseline {} vs tall {}", 528 baseline.height_px, 529 tall.height_px, 530 ); 531 } 532 533 #[test] 534 fn positive_letter_spacing_widens_advance() { 535 let bare = req(FontFace::Sans, 13.0, None); 536 let mut spaced = bare; 537 spaced.letter_spacing_px = 4.0; 538 let bare_out = shape("abc", bare); 539 let spaced_out = shape("abc", spaced); 540 assert!( 541 spaced_out.width_px > bare_out.width_px + 6.0, 542 "letter spacing must widen advance: bare {} vs spaced {}", 543 bare_out.width_px, 544 spaced_out.width_px, 545 ); 546 } 547 548 #[test] 549 fn rtl_run_emits_glyphs_visual_left_to_right_with_descending_source_bytes() { 550 let out = shape("مرحبا", req(FontFace::Sans, 13.0, None)); 551 let line = &out.lines[0]; 552 assert_eq!(line.runs.len(), 1); 553 let run = &line.runs[0]; 554 assert!(run.is_rtl); 555 assert!(run.glyphs.len() >= 2); 556 let dxs: Vec<f32> = run.glyphs.iter().map(|g| g.x_px).collect(); 557 dxs.windows(2) 558 .for_each(|p| assert!(p[1] >= p[0], "non-monotonic visual dx: {dxs:?}")); 559 let bytes: Vec<usize> = run.glyphs.iter().map(|g| g.cluster.value()).collect(); 560 let last_idx = bytes.len() - 1; 561 assert!( 562 bytes[0] > bytes[last_idx], 563 "expected RTL visual-leftmost glyph to map to later source byte: {bytes:?}", 564 ); 565 } 566}