1use std::cell::RefCell;
7use std::fmt;
8use std::hash::{Hash, Hasher};
9use std::ops::{DerefMut, Range, RangeBounds};
10use std::rc::Rc;
11
12use associative_cache::{AssociativeCache, Capacity64, HashFourWay, RoundRobinReplacement};
13use core_foundation::base::TCFType;
14use core_foundation::dictionary::{CFDictionary, CFMutableDictionary};
15use core_foundation::number::CFNumber;
16use core_foundation::string::CFString;
17use core_foundation_sys::base::CFRange;
18use core_graphics::base::CGFloat;
19use core_graphics::context::CGContextRef;
20use core_graphics::geometry::{CGPoint, CGRect, CGSize};
21use core_graphics::path::CGPath;
22use core_text::{
23 font,
24 font::CTFont,
25 font_descriptor::{self, SymbolicTraitAccessors},
26 string_attributes,
27};
28
29use piet::kurbo::{Affine, Point, Rect, Size};
30use piet::{
31 util, Error, FontFamily, FontStyle, FontWeight, HitTestPoint, HitTestPosition, LineMetric,
32 Text, TextAlignment, TextAttribute, TextLayout, TextLayoutBuilder, TextStorage,
33};
34
35use crate::ct_helpers::{self, AttributedString, FontCollection, Frame, Framesetter, Line};
36
37const MAX_LAYOUT_CONSTRAINT: f64 = 1e9;
39
40#[derive(Clone)]
41pub struct CoreGraphicsText {
42 shared: SharedTextState,
43}
44
45#[derive(Clone)]
50struct SharedTextState {
51 inner: Rc<RefCell<TextState>>,
52}
53
54type Cache<K, V> = AssociativeCache<K, V, Capacity64, HashFourWay, RoundRobinReplacement>;
55
56struct TextState {
57 collection: FontCollection,
58 family_cache: Cache<String, Option<FontFamily>>,
59 font_cache: Cache<CoreTextFontKey, CTFont>,
60}
61
62#[derive(Clone)]
63pub struct CoreGraphicsTextLayout {
64 text: Rc<dyn TextStorage>,
65 attr_string: AttributedString,
66 framesetter: Framesetter,
67 pub(crate) frame: Option<Frame>,
68 pub(crate) frame_size: Size,
70 bonus_height: f64,
74 image_bounds: Rect,
75 width_constraint: f64,
76 default_baseline: f64,
78 default_line_height: f64,
79 line_metrics: Rc<[LineMetric]>,
80 x_offsets: Rc<[f64]>,
81 trailing_ws_width: f64,
82}
83
84pub struct CoreGraphicsTextLayoutBuilder {
86 width: f64,
87 alignment: TextAlignment,
88 text: Rc<dyn TextStorage>,
89 last_resolved_pos: usize,
91 last_resolved_utf16: usize,
92 attr_string: AttributedString,
93 has_set_default_attrs: bool,
97 default_baseline: f64,
98 default_line_height: f64,
99 attrs: Attributes,
100 shared: SharedTextState,
101}
102
103#[derive(Default)]
105struct Attributes {
106 defaults: util::LayoutDefaults,
107 font: Option<Span<FontFamily>>,
108 size: Option<Span<f64>>,
109 weight: Option<Span<FontWeight>>,
110 style: Option<Span<FontStyle>>,
111}
112
113#[derive(Clone)]
114struct CoreTextFontKey {
115 font: FontFamily,
116 weight: FontWeight,
117 italic: bool,
118 size: f64,
119}
120
121impl PartialEq for CoreTextFontKey {
122 fn eq(&self, other: &CoreTextFontKey) -> bool {
123 self.font == other.font
124 && self.weight == other.weight
125 && self.italic == other.italic
126 && self.size.to_bits() == other.size.to_bits()
127 }
128}
129
130impl Eq for CoreTextFontKey {}
131
132impl Hash for CoreTextFontKey {
133 fn hash<H: Hasher>(&self, state: &mut H) {
134 self.font.hash(state);
135 self.weight.hash(state);
136 self.italic.hash(state);
137 self.size.to_bits().hash(state);
138 }
139}
140
141impl CoreTextFontKey {
142 fn create_ct_font(&self) -> CTFont {
143 const WEIGHT_AXIS_TAG: i32 = make_opentype_tag("wght") as i32;
145 const SLANT_TANGENT: f64 = 0.25;
148
149 unsafe {
150 let family_key =
151 CFString::wrap_under_create_rule(font_descriptor::kCTFontFamilyNameAttribute);
152 let family_name = ct_helpers::ct_family_name(&self.font, self.size);
153 let weight_key = CFString::wrap_under_create_rule(font_descriptor::kCTFontWeightTrait);
154 let weight = convert_to_coretext(self.weight);
155
156 let traits_key =
157 CFString::wrap_under_create_rule(font_descriptor::kCTFontTraitsAttribute);
158 let mut traits = CFMutableDictionary::new();
159 traits.set(weight_key, weight.as_CFType());
160 if self.italic {
161 let symbolic_traits_key =
162 CFString::wrap_under_create_rule(font_descriptor::kCTFontSymbolicTrait);
163 let symbolic_traits = CFNumber::from(font_descriptor::kCTFontItalicTrait as i32);
164 traits.set(symbolic_traits_key, symbolic_traits.as_CFType());
165 }
166
167 let attributes = CFDictionary::from_CFType_pairs(&[
168 (family_key, family_name.as_CFType()),
169 (traits_key, traits.as_CFType()),
170 ]);
171 let descriptor = font_descriptor::new_from_attributes(&attributes);
172 let font = font::new_from_descriptor(&descriptor, self.size);
173
174 let needs_synthetic_ital = self.italic && !font.symbolic_traits().is_italic();
175 let has_var_axes = font.get_variation_axes().is_some();
176
177 if !(needs_synthetic_ital | has_var_axes) {
178 return font;
179 }
180
181 let affine = if needs_synthetic_ital {
182 Affine::new([1.0, 0.0, SLANT_TANGENT, 1.0, 0., 0.])
183 } else {
184 Affine::default()
185 };
186
187 let variation_axes = font
188 .get_variation_axes()
189 .map(|axes| {
190 axes.iter()
191 .flat_map(|dict| {
192 dict.find(ct_helpers::kCTFontVariationAxisIdentifierKey)
195 .and_then(|v| v.downcast::<CFNumber>().and_then(|num| num.to_i32()))
196 })
197 .collect::<Vec<_>>()
198 })
199 .unwrap_or_default();
200
201 let descriptor = if variation_axes.contains(&WEIGHT_AXIS_TAG) && !self.font.is_generic()
203 {
204 let weight_axis_id: CFNumber = WEIGHT_AXIS_TAG.into();
205 let descriptor = font_descriptor::CTFontDescriptorCreateCopyWithVariation(
206 descriptor.as_concrete_TypeRef(),
207 weight_axis_id.as_concrete_TypeRef(),
208 self.weight.to_raw() as _,
209 );
210 font_descriptor::CTFontDescriptor::wrap_under_create_rule(descriptor)
211 } else {
212 descriptor
213 };
214
215 ct_helpers::make_font(&descriptor, self.size, affine)
216 }
217 }
218}
219
220struct Span<T> {
224 payload: T,
225 range: Range<usize>,
226}
227
228impl<T> Span<T> {
229 fn new(payload: T, range: Range<usize>) -> Self {
230 Span { payload, range }
231 }
232
233 fn range_end(&self) -> usize {
234 self.range.end
235 }
236}
237
238impl CoreGraphicsTextLayoutBuilder {
239 fn add(&mut self, attr: TextAttribute, range: Range<usize>) {
257 if !self.has_set_default_attrs {
258 self.set_default_attrs();
259 }
260 if matches!(
263 &attr,
264 TextAttribute::TextColor(_) | TextAttribute::Underline(_)
265 ) {
266 return self.add_immediately(attr, range);
267 }
268
269 debug_assert!(
270 range.start >= self.last_resolved_pos,
271 "attributes must be added with non-decreasing start positions"
272 );
273
274 self.resolve_up_to(range.start);
275 self.attrs.add(range, attr);
278 }
279
280 fn set_default_attrs(&mut self) {
281 self.has_set_default_attrs = true;
282 let whole_range = self.attr_string.range();
283 let font = self.current_font();
284 let height = compute_line_height(font.ascent(), font.descent(), font.leading());
285 self.default_line_height = height;
286 self.default_baseline = (font.ascent() + 0.5).floor();
287 self.attr_string.set_font(whole_range, &font);
288 self.attr_string
289 .set_fg_color(whole_range, self.attrs.defaults.fg_color);
290 self.attr_string
291 .set_underline(whole_range, self.attrs.defaults.underline);
292 }
293
294 fn add_immediately(&mut self, attr: TextAttribute, range: Range<usize>) {
295 let utf16_start = util::count_utf16(&self.text[..range.start]);
296 let utf16_len = util::count_utf16(&self.text[range]);
297 let range = CFRange::init(utf16_start as isize, utf16_len as isize);
298 match attr {
299 TextAttribute::TextColor(color) => {
300 self.attr_string.set_fg_color(range, color);
301 }
302 TextAttribute::Underline(flag) => self.attr_string.set_underline(range, flag),
303 _ => unreachable!(),
304 }
305 }
306
307 fn finalize(&mut self) {
308 if !self.has_set_default_attrs {
309 self.set_default_attrs();
310 }
311 self.resolve_up_to(self.text.len());
312 }
313
314 fn resolve_up_to(&mut self, resolve_end: usize) {
316 let mut next_span_end = self.last_resolved_pos;
317 while next_span_end < resolve_end {
318 next_span_end = self.next_span_end(resolve_end);
319 if next_span_end > self.last_resolved_pos {
320 let range_end_utf16 =
321 util::count_utf16(&self.text[self.last_resolved_pos..next_span_end]);
322 let range =
323 CFRange::init(self.last_resolved_utf16 as isize, range_end_utf16 as isize);
324 let font = self.current_font();
325 unsafe {
326 self.attr_string.inner.set_attribute(
327 range,
328 string_attributes::kCTFontAttributeName,
329 &font,
330 );
331 }
332 self.last_resolved_pos = next_span_end;
333 self.last_resolved_utf16 += range_end_utf16;
334 self.update_after_adding_span();
335 }
336 }
337 }
338
339 fn next_span_end(&self, max: usize) -> usize {
347 self.attrs.next_span_end(max)
348 }
349
350 fn current_font(&self) -> CTFont {
355 self.shared.get_ct_font(&CoreTextFontKey {
356 font: self.attrs.font().to_owned(),
357 weight: self.attrs.weight(),
358 italic: self.attrs.italic(),
359 size: self.attrs.size(),
360 })
361 }
362
363 fn update_after_adding_span(&mut self) {
369 self.attrs.clear_up_to(self.last_resolved_pos)
370 }
371}
372
373impl Attributes {
374 fn add(&mut self, range: Range<usize>, attr: TextAttribute) {
375 match attr {
376 TextAttribute::FontFamily(font) => self.font = Some(Span::new(font, range)),
377 TextAttribute::Weight(w) => self.weight = Some(Span::new(w, range)),
378 TextAttribute::FontSize(s) => self.size = Some(Span::new(s, range)),
379 TextAttribute::Style(s) => self.style = Some(Span::new(s, range)),
380 TextAttribute::Strikethrough(_) => { }
382 _ => unreachable!(),
383 }
384 }
385
386 fn size(&self) -> f64 {
387 self.size
388 .as_ref()
389 .map(|s| s.payload)
390 .unwrap_or(self.defaults.font_size)
391 }
392
393 fn weight(&self) -> FontWeight {
394 self.weight
395 .as_ref()
396 .map(|w| w.payload)
397 .unwrap_or(self.defaults.weight)
398 }
399
400 fn italic(&self) -> bool {
401 matches!(
402 self.style
403 .as_ref()
404 .map(|t| t.payload)
405 .unwrap_or(self.defaults.style),
406 FontStyle::Italic
407 )
408 }
409
410 fn font(&self) -> &FontFamily {
411 self.font
412 .as_ref()
413 .map(|t| &t.payload)
414 .unwrap_or_else(|| &self.defaults.font)
415 }
416
417 fn next_span_end(&self, max: usize) -> usize {
418 self.font
419 .as_ref()
420 .map(Span::range_end)
421 .unwrap_or(max)
422 .min(self.size.as_ref().map(Span::range_end).unwrap_or(max))
423 .min(self.weight.as_ref().map(Span::range_end).unwrap_or(max))
424 .min(self.style.as_ref().map(Span::range_end).unwrap_or(max))
425 .min(max)
426 }
427
428 fn clear_up_to(&mut self, last_pos: usize) {
430 if self.font.as_ref().map(Span::range_end) == Some(last_pos) {
431 self.font = None;
432 }
433 if self.weight.as_ref().map(Span::range_end) == Some(last_pos) {
434 self.weight = None;
435 }
436 if self.style.as_ref().map(Span::range_end) == Some(last_pos) {
437 self.style = None;
438 }
439 if self.size.as_ref().map(Span::range_end) == Some(last_pos) {
440 self.size = None;
441 }
442 }
443}
444
445fn convert_to_coretext(weight: FontWeight) -> CFNumber {
451 match weight.to_raw() {
452 0..=199 => -0.8,
453 200..=299 => -0.6,
454 300..=399 => -0.4,
455 400..=499 => 0.0,
456 500..=599 => 0.23,
457 600..=699 => 0.3,
458 700..=799 => 0.4,
459 800..=899 => 0.56,
460 _ => 0.62,
461 }
462 .into()
463}
464
465impl CoreGraphicsText {
466 pub fn new_with_unique_state() -> CoreGraphicsText {
473 let collection = FontCollection::new_with_all_fonts();
474 let inner = Rc::new(RefCell::new(TextState {
475 collection,
476 family_cache: Default::default(),
477 font_cache: Default::default(),
478 }));
479 CoreGraphicsText {
480 shared: SharedTextState { inner },
481 }
482 }
483}
484
485impl fmt::Debug for CoreGraphicsText {
486 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
487 f.debug_struct("CoreGraphicsText").finish()
488 }
489}
490
491impl Text for CoreGraphicsText {
492 type TextLayout = CoreGraphicsTextLayout;
493 type TextLayoutBuilder = CoreGraphicsTextLayoutBuilder;
494
495 fn font_family(&mut self, family_name: &str) -> Option<FontFamily> {
496 self.shared.get_font_family(family_name)
497 }
498
499 fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
500 CoreGraphicsTextLayoutBuilder::new(text, self.shared.clone())
501 }
502
503 fn load_font(&mut self, data: &[u8]) -> Result<FontFamily, Error> {
504 ct_helpers::add_font(data)
505 .map(FontFamily::new_unchecked)
506 .map_err(|_| Error::MissingFont)
507 }
508}
509
510impl SharedTextState {
511 fn get_font_family(&self, family_name: &str) -> Option<FontFamily> {
515 let mut inner = self.inner.borrow_mut();
516 let obj = inner.deref_mut();
517 let family_cache = &mut obj.family_cache;
518 let collection = &mut obj.collection;
519 family_cache
520 .entry(family_name)
521 .or_insert_with(
522 || family_name.to_owned(),
523 || collection.font_for_family_name(family_name),
524 )
525 .clone()
526 }
527
528 fn get_ct_font(&self, key: &CoreTextFontKey) -> CTFont {
532 let mut inner = self.inner.borrow_mut();
533 inner
534 .font_cache
535 .entry(key)
536 .or_insert_with(|| key.to_owned(), || key.create_ct_font())
537 .clone()
538 }
539}
540
541impl CoreGraphicsTextLayoutBuilder {
542 fn new(text: impl TextStorage, shared: SharedTextState) -> Self {
543 let text = Rc::new(text);
544 let attr_string = AttributedString::new(text.as_str());
545 CoreGraphicsTextLayoutBuilder {
546 shared,
547 width: MAX_LAYOUT_CONSTRAINT,
548 alignment: TextAlignment::default(),
549 attrs: Default::default(),
550 text,
551 last_resolved_pos: 0,
552 last_resolved_utf16: 0,
553 attr_string,
554 has_set_default_attrs: false,
555 default_baseline: 0.0,
556 default_line_height: 0.0,
557 }
558 }
559}
560
561impl fmt::Debug for CoreGraphicsTextLayoutBuilder {
562 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
563 f.debug_struct("CoreGraphicsTextLayoutBuilder").finish()
564 }
565}
566
567impl TextLayoutBuilder for CoreGraphicsTextLayoutBuilder {
568 type Out = CoreGraphicsTextLayout;
569
570 fn max_width(mut self, width: f64) -> Self {
571 self.width = width;
572 self
573 }
574
575 fn alignment(mut self, alignment: piet::TextAlignment) -> Self {
576 self.alignment = alignment;
577 self
578 }
579
580 fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
581 debug_assert!(
582 !self.has_set_default_attrs,
583 "default attributes mut be added before range attributes"
584 );
585 let attribute = attribute.into();
586 self.attrs.defaults.set(attribute);
587 self
588 }
589
590 fn range_attribute(
591 mut self,
592 range: impl RangeBounds<usize>,
593 attribute: impl Into<TextAttribute>,
594 ) -> Self {
595 let range = util::resolve_range(range, self.text.len());
596 let attribute = attribute.into();
597 self.add(attribute, range);
598 self
599 }
600
601 fn build(mut self) -> Result<Self::Out, Error> {
602 self.finalize();
603 self.attr_string.set_alignment(self.alignment);
604 Ok(CoreGraphicsTextLayout::new(
605 self.text,
606 self.attr_string,
607 self.width,
608 self.default_baseline,
609 self.default_line_height,
610 ))
611 }
612}
613
614impl fmt::Debug for CoreGraphicsTextLayout {
615 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
616 f.debug_struct("CoreGraphicsTextLayout").finish()
617 }
618}
619
620impl TextLayout for CoreGraphicsTextLayout {
621 fn size(&self) -> Size {
622 Size::new(
623 self.frame_size.width,
624 self.frame_size.height + self.bonus_height,
625 )
626 }
627
628 fn trailing_whitespace_width(&self) -> f64 {
629 self.trailing_ws_width
630 }
631
632 fn image_bounds(&self) -> Rect {
633 self.image_bounds
634 }
635
636 fn text(&self) -> &str {
637 &self.text
638 }
639
640 fn line_text(&self, line_number: usize) -> Option<&str> {
641 self.line_range(line_number)
642 .map(|(start, end)| unsafe { self.text.get_unchecked(start..end) })
643 }
644
645 fn line_metric(&self, line_number: usize) -> Option<LineMetric> {
646 self.line_metrics.get(line_number).cloned()
647 }
648
649 fn line_count(&self) -> usize {
650 self.line_metrics.len()
651 }
652
653 fn hit_test_point(&self, point: Point) -> HitTestPoint {
655 let line_num = self
656 .line_metrics
657 .iter()
658 .position(|lm| lm.y_offset + lm.height >= point.y)
659 .unwrap_or_else(|| self.line_metrics.len().saturating_sub(1));
661
662 let line = match self.unwrap_frame().get_line(line_num) {
663 Some(line) => line,
664 None => {
665 assert!(self.text.is_empty() || util::trailing_nlf(&self.text).is_some());
668 return HitTestPoint::new(self.text.len(), false);
669 }
670 };
671 let line_text = self.line_text(line_num).unwrap();
672 let metric = &self.line_metrics[line_num];
673 let x_offset = self.x_offsets[line_num];
674 let fake_y = metric.y_offset + metric.baseline;
676 let fake_y = -(self.frame_size.height - fake_y);
678 let point_in_string_space = CGPoint::new(point.x - x_offset, fake_y);
679 let offset_utf16 = line.get_string_index_for_position(point_in_string_space);
680 let mut offset = match offset_utf16 {
681 -1 => self.text.len(),
683 n if n >= 0 => {
684 let utf16_range = line.get_string_range();
685 let rel_offset = (n - utf16_range.location) as usize;
686 metric.start_offset
687 + util::count_until_utf16(line_text, rel_offset).unwrap_or(line_text.len())
688 }
689 _ => panic!("gross violation of api contract"),
691 };
692
693 if offset == metric.end_offset {
695 offset -= util::trailing_nlf(line_text).unwrap_or(0);
696 };
697
698 let typo_bounds = line.get_typographic_bounds();
699 let is_inside_y = point.y >= 0. && point.y <= self.frame_size.height;
700 let is_inside_x =
701 point_in_string_space.x >= 0. && point_in_string_space.x <= typo_bounds.width;
702 let is_inside = is_inside_x && is_inside_y;
703
704 HitTestPoint::new(offset, is_inside)
705 }
706
707 fn hit_test_text_position(&self, idx: usize) -> HitTestPosition {
708 let idx = idx.min(self.text.len());
709 assert!(self.text.is_char_boundary(idx));
710
711 let line_num = self.line_number_for_utf8_offset(idx);
712 let line = match self.unwrap_frame().get_line(line_num) {
713 Some(line) => line,
714 None => {
715 assert!(self.text.is_empty() || util::trailing_nlf(&self.text).is_some());
716 let lm = &self.line_metrics[line_num];
717 let y_pos = lm.y_offset + lm.baseline;
718 return HitTestPosition::new(Point::new(0., y_pos), line_num);
719 }
720 };
721
722 let text = self.line_text(line_num).unwrap();
723 let metric = &self.line_metrics[line_num];
724 let x_offset = self.x_offsets[line_num];
725
726 let offset_remainder = idx - metric.start_offset;
727 let off16: usize = util::count_utf16(&text[..offset_remainder]);
728 let line_range = line.get_string_range();
729 let char_idx = line_range.location + off16 as isize;
730 let x_pos = line.get_offset_for_string_index(char_idx) + x_offset;
731 let y_pos = metric.y_offset + metric.baseline;
732 HitTestPosition::new(Point::new(x_pos, y_pos), line_num)
733 }
734}
735
736impl CoreGraphicsTextLayout {
737 fn new(
738 text: Rc<dyn TextStorage>,
739 attr_string: AttributedString,
740 width_constraint: f64,
741 default_baseline: f64,
742 default_line_height: f64,
743 ) -> Self {
744 let framesetter = Framesetter::new(&attr_string);
745
746 let mut layout = CoreGraphicsTextLayout {
747 text,
748 attr_string,
749 framesetter,
750 frame: None,
752 frame_size: Size::ZERO,
753 bonus_height: 0.0,
754 image_bounds: Rect::ZERO,
755 width_constraint: f64::NAN,
757 default_baseline,
758 default_line_height,
759 line_metrics: Rc::new([]),
760 x_offsets: Rc::new([]),
761 trailing_ws_width: 0.0,
762 };
763 layout.update_width(width_constraint);
764 layout
765 }
766
767 #[allow(clippy::float_cmp)]
769 fn update_width(&mut self, new_width: impl Into<Option<f64>>) {
770 let width = new_width.into().unwrap_or(MAX_LAYOUT_CONSTRAINT);
771 let width = if width.is_normal() {
772 width
773 } else {
774 MAX_LAYOUT_CONSTRAINT
775 };
776
777 if width.ceil() == self.width_constraint.ceil() {
778 return;
779 }
780
781 let constraints = CGSize::new(width as CGFloat, MAX_LAYOUT_CONSTRAINT);
782 let char_range = self.attr_string.range();
783 let rect = CGRect::new(&CGPoint::new(0.0, 0.0), &constraints);
784 let path = CGPath::from_rect(rect, None);
785 self.width_constraint = width;
786
787 let frame = self.framesetter.create_frame(char_range, &path);
788 let layout_metrics = build_line_metrics(
789 &frame,
790 &self.text,
791 self.default_line_height,
792 self.default_baseline,
793 );
794 self.line_metrics = layout_metrics.line_metrics.into();
795 self.x_offsets = layout_metrics.x_offsets.into();
796 self.trailing_ws_width = layout_metrics.trailing_whitespace;
797 self.frame_size = layout_metrics.layout_size;
798 assert!(self.line_metrics.len() > 0);
799
800 self.bonus_height = if self.text.is_empty() || util::trailing_nlf(&self.text).is_some() {
801 self.line_metrics.last().unwrap().height
802 } else {
803 0.0
804 };
805
806 let mut line_bounds = frame
807 .lines()
808 .iter()
809 .map(Line::get_image_bounds)
810 .zip(self.line_metrics.iter().map(|l| l.y_offset + l.baseline))
811 .map(|(rect, y_pos)| Rect::new(rect.x0, y_pos - rect.y1, rect.x1, y_pos - rect.y0));
813
814 let first_line_bounds = line_bounds.next().unwrap_or_default();
815 self.image_bounds = line_bounds.fold(first_line_bounds, |acc, el| acc.union(el));
816 self.frame = Some(frame);
817 }
818
819 pub(crate) fn draw(&self, ctx: &mut CGContextRef) {
820 let lines = self.unwrap_frame().lines();
821 let lines_len = lines.len();
822 assert!(self.x_offsets.len() >= lines_len);
823 assert!(self.line_metrics.len() >= lines_len);
824
825 for (i, line) in lines.iter().enumerate() {
826 let x = self.x_offsets.get(i).copied().unwrap_or_default();
827 let y_off = self
829 .line_metrics
830 .get(i)
831 .map(|lm| lm.y_offset + lm.baseline)
832 .unwrap_or_default();
833 let y = self.frame_size.height - y_off;
834 ctx.set_text_position(x, y);
835 line.draw(ctx)
836 }
837 }
838
839 #[inline]
840 fn unwrap_frame(&self) -> &Frame {
841 self.frame.as_ref().expect("always inited in ::new")
842 }
843
844 fn line_number_for_utf8_offset(&self, offset: usize) -> usize {
845 match self
846 .line_metrics
847 .binary_search_by_key(&offset, |lm| lm.start_offset)
848 {
849 Ok(line) => line,
850 Err(line) => line.saturating_sub(1),
851 }
852 }
853
854 fn line_range(&self, line: usize) -> Option<(usize, usize)> {
855 self.line_metrics
856 .get(line)
857 .map(|lm| (lm.start_offset, lm.end_offset))
858 }
859
860 #[allow(dead_code)]
861 fn debug_print_lines(&self) {
862 for (i, lm) in self.line_metrics.iter().enumerate() {
863 let range = lm.range();
864 println!(
865 "L{} ({}..{}): '{}'",
866 i,
867 range.start,
868 range.end,
869 &self.text[lm.range()].escape_debug()
870 );
871 }
872 }
873}
874
875struct LayoutMetrics {
876 line_metrics: Vec<LineMetric>,
877 trailing_whitespace: f64,
878 x_offsets: Vec<f64>,
879 layout_size: Size,
880}
881
882#[allow(clippy::while_let_on_iterator)]
884fn build_line_metrics(
885 frame: &Frame,
886 text: &str,
887 default_line_height: f64,
888 default_baseline: f64,
889) -> LayoutMetrics {
890 let line_origins = frame.get_line_origins(CFRange::init(0, 0));
891 assert_eq!(frame.lines().len(), line_origins.len());
892
893 let mut metrics = Vec::with_capacity(frame.lines().len() + 1);
894 let mut x_offsets = Vec::with_capacity(frame.lines().len() + 1);
895 let mut cumulative_height = 0.0;
896 let mut max_width = 0f64;
897 let mut max_width_with_ws = 0f64;
898
899 let mut chars = text.chars();
900 let mut cur_16 = 0;
901 let mut cur_8 = 0;
902
903 let mut utf16_to_utf8 = |off_16| {
905 if off_16 == 0 {
906 0
907 } else {
908 while let Some(c) = chars.next() {
909 cur_16 += c.len_utf16();
910 cur_8 += c.len_utf8();
911 if cur_16 == off_16 {
912 return cur_8;
913 }
914 }
915 panic!("error calculating utf8 offsets");
916 }
917 };
918
919 let mut last_line_end = 0;
920 for (i, line) in frame.lines().iter().enumerate() {
921 let range = line.get_string_range();
922
923 let start_offset = last_line_end;
924 let end_offset = utf16_to_utf8((range.location + range.length) as usize);
925 last_line_end = end_offset;
926
927 let trailing_whitespace = count_trailing_ws(&text[start_offset..end_offset]);
928
929 let ws_width = line.get_trailing_whitespace_width();
930 let typo_bounds = line.get_typographic_bounds();
931 max_width_with_ws = max_width_with_ws.max(typo_bounds.width);
932 max_width = max_width.max(typo_bounds.width - ws_width);
933
934 let baseline = (typo_bounds.ascent + 0.5).floor();
935 let height =
936 compute_line_height(typo_bounds.ascent, typo_bounds.descent, typo_bounds.leading);
937 let y_offset = cumulative_height;
938 cumulative_height += height;
939
940 metrics.push(LineMetric {
941 start_offset,
942 end_offset,
943 trailing_whitespace,
944 baseline,
945 height,
946 y_offset,
947 });
948 x_offsets.push(line_origins[i].x);
949 }
950
951 let min_x_offset = if x_offsets.is_empty() {
953 0.0
954 } else {
955 x_offsets
956 .iter()
957 .fold(f64::MAX, |mx, this| if *this < mx { *this } else { mx })
958 };
959 x_offsets.iter_mut().for_each(|off| *off -= min_x_offset);
960
961 if text.is_empty() {
963 metrics.push(LineMetric {
964 height: default_line_height,
965 baseline: default_baseline,
966 ..Default::default()
967 });
968 } else if util::trailing_nlf(text).is_some() {
970 let newline_eof = metrics
971 .last()
972 .map(|lm| {
973 LineMetric {
974 start_offset: text.len(),
975 end_offset: text.len(),
976 height: lm.height,
981 baseline: lm.baseline,
982 y_offset: lm.y_offset + lm.height,
983 trailing_whitespace: 0,
984 }
985 })
986 .unwrap();
987 let x_offset = x_offsets.last().copied().unwrap();
988 metrics.push(newline_eof);
989 x_offsets.push(x_offset);
990 }
991
992 let layout_size = Size::new(max_width, cumulative_height);
993
994 LayoutMetrics {
995 line_metrics: metrics,
996 x_offsets,
997 layout_size,
998 trailing_whitespace: max_width_with_ws,
999 }
1000}
1001
1002fn compute_line_height(ascent: f64, descent: f64, leading: f64) -> f64 {
1005 let leading = leading.max(0.0);
1006 let leading = (leading + 0.5).floor();
1007 leading + (descent + 0.5).floor() + (ascent + 0.5).floor()
1008 }
1011
1012fn count_trailing_ws(s: &str) -> usize {
1013 s.as_bytes()
1015 .iter()
1016 .rev()
1017 .take_while(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
1018 .count()
1019}
1020
1021const fn make_opentype_tag(raw: &str) -> u32 {
1027 let b = raw.as_bytes();
1028 ((b[0] as u32) << 24) | ((b[1] as u32) << 16) | ((b[2] as u32) << 8) | (b[3] as u32)
1029}
1030
1031#[cfg(test)]
1032#[allow(clippy::float_cmp)]
1033mod tests {
1034 use super::*;
1035
1036 macro_rules! assert_close {
1037 ($val:expr, $target:expr, $tolerance:expr) => {{
1038 let min = $target - $tolerance;
1039 let max = $target + $tolerance;
1040 if $val < min || $val > max {
1041 panic!(
1042 "value {} outside target {} with tolerance {}",
1043 $val, $target, $tolerance
1044 );
1045 }
1046 }};
1047
1048 ($val:expr, $target:expr, $tolerance:expr,) => {{
1049 assert_close!($val, $target, $tolerance)
1050 }};
1051 }
1052
1053 #[test]
1054 fn line_offsets() {
1055 let text = "hi\ni'm\nπ four\nlines";
1056 let a_font = FontFamily::new_unchecked("Helvetica");
1057 let layout = CoreGraphicsText::new_with_unique_state()
1058 .new_text_layout(text)
1059 .font(a_font, 16.0)
1060 .build()
1061 .unwrap();
1062 assert_eq!(layout.line_text(0), Some("hi\n"));
1063 assert_eq!(layout.line_text(1), Some("i'm\n"));
1064 assert_eq!(layout.line_text(2), Some("π four\n"));
1065 assert_eq!(layout.line_text(3), Some("lines"));
1066 }
1067
1068 #[test]
1069 fn metrics() {
1070 let text = "π€‘:\na string\nwith a number \n of lines";
1071 let a_font = FontFamily::new_unchecked("Helvetica");
1072 let layout = CoreGraphicsText::new_with_unique_state()
1073 .new_text_layout(text)
1074 .font(a_font, 16.0)
1075 .build()
1076 .unwrap();
1077
1078 let line1 = layout.line_metric(0).unwrap();
1079 assert_eq!(line1.range(), 0..6);
1080 assert_eq!(line1.trailing_whitespace, 1);
1081 layout.line_metric(1);
1082
1083 let line3 = layout.line_metric(2).unwrap();
1084 assert_eq!(line3.range(), 15..30);
1085 assert_eq!(line3.trailing_whitespace, 2);
1086
1087 let line4 = layout.line_metric(3).unwrap();
1088 assert_eq!(layout.line_text(3), Some(" of lines"));
1089 assert_eq!(line4.trailing_whitespace, 0);
1090
1091 let total_height = layout.frame_size.height;
1092 assert_eq!(line4.y_offset + line4.height, total_height);
1093
1094 assert!(layout.line_metric(4).is_none());
1095 }
1096
1097 #[test]
1099 fn basic_hit_testing() {
1100 let text = "1\nπ\n8\nA";
1101 let a_font = FontFamily::new_unchecked("Helvetica");
1102 let layout = CoreGraphicsText::new_with_unique_state()
1103 .new_text_layout(text)
1104 .font(a_font, 16.0)
1105 .build()
1106 .unwrap();
1107
1108 assert_eq!(layout.line_count(), 4);
1109
1110 let p1 = layout.hit_test_point(Point::ZERO);
1111 assert_eq!(p1.idx, 0);
1112 assert!(p1.is_inside);
1113 let p2 = layout.hit_test_point(Point::new(2.0, 15.9));
1114 assert_eq!(p2.idx, 0);
1115 assert!(p2.is_inside);
1116
1117 let p3 = layout.hit_test_point(Point::new(50.0, 10.0));
1118 assert_eq!(p3.idx, 1);
1119 assert!(!p3.is_inside);
1120
1121 let p4 = layout.hit_test_point(Point::new(4.0, 25.0));
1122 assert_eq!(p4.idx, 2);
1123 assert!(p4.is_inside);
1124
1125 let p5 = layout.hit_test_point(Point::new(2.0, 64.0));
1126 assert_eq!(p5.idx, 9);
1127 assert!(p5.is_inside);
1128
1129 let p6 = layout.hit_test_point(Point::new(10.0, 64.0));
1130 assert_eq!(p6.idx, 10);
1131 assert!(p6.is_inside);
1132 }
1133
1134 #[test]
1135 fn hit_test_end_of_single_line() {
1136 let text = "hello";
1137 let a_font = FontFamily::new_unchecked("Helvetica");
1138 let layout = CoreGraphicsText::new_with_unique_state()
1139 .new_text_layout(text)
1140 .font(a_font, 16.0)
1141 .build()
1142 .unwrap();
1143 let pt = layout.hit_test_point(Point::new(0.0, 5.0));
1144 assert_eq!(pt.idx, 0);
1145 assert!(pt.is_inside);
1146 let next_to_last = layout.frame_size.width - 10.0;
1147 let pt = layout.hit_test_point(Point::new(next_to_last, 0.0));
1148 assert_eq!(pt.idx, 4);
1149 assert!(pt.is_inside);
1150 let pt = layout.hit_test_point(Point::new(100.0, 5.0));
1151 assert_eq!(pt.idx, 5);
1152 assert!(!pt.is_inside);
1153 }
1154
1155 #[test]
1156 fn hit_test_empty_string() {
1157 let a_font = FontFamily::new_unchecked("Helvetica");
1158 let layout = CoreGraphicsText::new_with_unique_state()
1159 .new_text_layout("")
1160 .font(a_font, 12.0)
1161 .build()
1162 .unwrap();
1163 let pt = layout.hit_test_point(Point::new(0.0, 0.0));
1164 assert_eq!(pt.idx, 0);
1165 let pos = layout.hit_test_text_position(0);
1166 assert_eq!(pos.point.x, 0.0);
1167 assert_close!(pos.point.y, 10.0, 3.0);
1168 let line = layout.line_metric(0).unwrap();
1169 assert_close!(line.height, 12.0, 3.0);
1170 }
1171
1172 #[test]
1173 fn hit_test_text_position() {
1174 let text = "aaaaa\nbbbbb";
1175 let a_font = FontFamily::new_unchecked("Helvetica");
1176 let layout = CoreGraphicsText::new_with_unique_state()
1177 .new_text_layout(text)
1178 .font(a_font, 16.0)
1179 .build()
1180 .unwrap();
1181 let p1 = layout.hit_test_text_position(0);
1182 assert_close!(p1.point.y, 12.0, 0.5);
1183
1184 let p1 = layout.hit_test_text_position(7);
1185 assert_close!(p1.point.y, 28.0, 0.5);
1186 assert_close!(p1.point.x, 10.0, 5.0);
1188 }
1189
1190 #[test]
1191 fn hit_test_text_position_astral_plane() {
1192 let text = "πΎπ€ \nπ€ππΎ";
1193 let a_font = FontFamily::new_unchecked("Helvetica");
1194 let layout = CoreGraphicsText::new_with_unique_state()
1195 .new_text_layout(text)
1196 .font(a_font, 16.0)
1197 .build()
1198 .unwrap();
1199 let p0 = layout.hit_test_text_position(4);
1200 let p1 = layout.hit_test_text_position(8);
1201 let p2 = layout.hit_test_text_position(13);
1202
1203 assert!(p1.point.x > p0.point.x);
1204 assert!(p1.point.y == p0.point.y);
1205 assert!(p2.point.y > p1.point.y);
1206 }
1207
1208 #[test]
1209 fn missing_font_is_missing() {
1210 assert!(CoreGraphicsText::new_with_unique_state()
1211 .font_family("Segoe UI")
1212 .is_none());
1213 }
1214
1215 #[test]
1216 fn line_text_empty_string() {
1217 let layout = CoreGraphicsText::new_with_unique_state()
1218 .new_text_layout("")
1219 .build()
1220 .unwrap();
1221 assert_eq!(layout.line_text(0), Some(""));
1222 }
1223
1224 #[test]
1227 fn line_test_tabs() {
1228 let line_text = "a\t\t\t\t\n";
1229 let layout = CoreGraphicsText::new_with_unique_state()
1230 .new_text_layout(line_text)
1231 .build()
1232 .unwrap();
1233 assert_eq!(layout.line_count(), 2);
1234 assert_eq!(layout.line_text(0), Some(line_text));
1235 let metrics = layout.line_metric(0).unwrap();
1236 assert_eq!(metrics.trailing_whitespace, line_text.len() - 1);
1237 }
1238}