1use crate::{
2 point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun,
3 FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
4 RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
5};
6use anyhow::anyhow;
7use cocoa::appkit::{CGFloat, CGPoint};
8use collections::{BTreeSet, HashMap};
9use core_foundation::{
10 array::CFIndex,
11 attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
12 base::{CFRange, TCFType},
13 string::CFString,
14};
15use core_graphics::{
16 base::{kCGImageAlphaPremultipliedLast, CGGlyph},
17 color_space::CGColorSpace,
18 context::CGContext,
19};
20use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
21use font_kit::{
22 font::Font as FontKitFont,
23 handle::Handle,
24 hinting::HintingOptions,
25 metrics::Metrics,
26 properties::{Style as FontkitStyle, Weight as FontkitWeight},
27 source::SystemSource,
28 sources::mem::MemSource,
29};
30use parking_lot::{RwLock, RwLockUpgradableReadGuard};
31use pathfinder_geometry::{
32 rect::{RectF, RectI},
33 transform2d::Transform2F,
34 vector::{Vector2F, Vector2I},
35};
36use smallvec::SmallVec;
37use std::{char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
38
39use super::open_type;
40
41#[allow(non_upper_case_globals)]
42const kCGImageAlphaOnly: u32 = 7;
43
44pub(crate) struct MacTextSystem(RwLock<MacTextSystemState>);
45
46struct MacTextSystemState {
47 memory_source: MemSource,
48 system_source: SystemSource,
49 fonts: Vec<FontKitFont>,
50 font_selections: HashMap<Font, FontId>,
51 font_ids_by_postscript_name: HashMap<String, FontId>,
52 font_ids_by_family_name: HashMap<SharedString, SmallVec<[FontId; 4]>>,
53 postscript_names_by_font_id: HashMap<FontId, String>,
54}
55
56impl MacTextSystem {
57 pub(crate) fn new() -> Self {
58 Self(RwLock::new(MacTextSystemState {
59 memory_source: MemSource::empty(),
60 system_source: SystemSource::new(),
61 fonts: Vec::new(),
62 font_selections: HashMap::default(),
63 font_ids_by_postscript_name: HashMap::default(),
64 font_ids_by_family_name: HashMap::default(),
65 postscript_names_by_font_id: HashMap::default(),
66 }))
67 }
68}
69
70impl Default for MacTextSystem {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl PlatformTextSystem for MacTextSystem {
77 fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
78 self.0.write().add_fonts(fonts)
79 }
80
81 fn all_font_names(&self) -> Vec<String> {
82 let collection = core_text::font_collection::create_for_all_families();
83 let Some(descriptors) = collection.get_descriptors() else {
84 return vec![];
85 };
86 let mut names = BTreeSet::new();
87 for descriptor in descriptors.into_iter() {
88 names.insert(descriptor.font_name());
89 names.insert(descriptor.family_name());
90 names.insert(descriptor.style_name());
91 }
92 if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() {
93 names.extend(fonts_in_memory);
94 }
95 names.into_iter().collect()
96 }
97
98 fn all_font_families(&self) -> Vec<String> {
99 self.0
100 .read()
101 .system_source
102 .all_families()
103 .expect("core text should never return an error")
104 }
105
106 fn font_id(&self, font: &Font) -> Result<FontId> {
107 let lock = self.0.upgradable_read();
108 if let Some(font_id) = lock.font_selections.get(font) {
109 Ok(*font_id)
110 } else {
111 let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
112 let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family)
113 {
114 font_ids.as_slice()
115 } else {
116 let font_ids = lock.load_family(&font.family, font.features)?;
117 lock.font_ids_by_family_name
118 .insert(font.family.clone(), font_ids);
119 lock.font_ids_by_family_name[&font.family].as_ref()
120 };
121
122 let candidate_properties = candidates
123 .iter()
124 .map(|font_id| lock.fonts[font_id.0].properties())
125 .collect::<SmallVec<[_; 4]>>();
126
127 let ix = font_kit::matching::find_best_match(
128 &candidate_properties,
129 &font_kit::properties::Properties {
130 style: font.style.into(),
131 weight: font.weight.into(),
132 stretch: Default::default(),
133 },
134 )?;
135
136 let font_id = candidates[ix];
137 lock.font_selections.insert(font.clone(), font_id);
138 Ok(font_id)
139 }
140 }
141
142 fn font_metrics(&self, font_id: FontId) -> FontMetrics {
143 self.0.read().fonts[font_id.0].metrics().into()
144 }
145
146 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
147 Ok(self.0.read().fonts[font_id.0]
148 .typographic_bounds(glyph_id.into())?
149 .into())
150 }
151
152 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
153 self.0.read().advance(font_id, glyph_id)
154 }
155
156 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
157 self.0.read().glyph_for_char(font_id, ch)
158 }
159
160 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
161 self.0.read().raster_bounds(params)
162 }
163
164 fn rasterize_glyph(
165 &self,
166 glyph_id: &RenderGlyphParams,
167 raster_bounds: Bounds<DevicePixels>,
168 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
169 self.0.read().rasterize_glyph(glyph_id, raster_bounds)
170 }
171
172 fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
173 self.0.write().layout_line(text, font_size, font_runs)
174 }
175
176 fn wrap_line(
177 &self,
178 text: &str,
179 font_id: FontId,
180 font_size: Pixels,
181 width: Pixels,
182 ) -> Vec<usize> {
183 self.0.read().wrap_line(text, font_id, font_size, width)
184 }
185}
186
187impl MacTextSystemState {
188 fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
189 self.memory_source.add_fonts(
190 fonts
191 .iter()
192 .map(|bytes| Handle::from_memory(bytes.clone(), 0)),
193 )?;
194 Ok(())
195 }
196
197 fn load_family(
198 &mut self,
199 name: &SharedString,
200 features: FontFeatures,
201 ) -> Result<SmallVec<[FontId; 4]>> {
202 let mut font_ids = SmallVec::new();
203 let family = self
204 .memory_source
205 .select_family_by_name(name.as_ref())
206 .or_else(|_| self.system_source.select_family_by_name(name.as_ref()))?;
207 for font in family.fonts() {
208 let mut font = font.load()?;
209 open_type::apply_features(&mut font, features);
210 let Some(_) = font.glyph_for_char('m') else {
211 continue;
212 };
213 let font_id = FontId(self.fonts.len());
214 font_ids.push(font_id);
215 let postscript_name = font.postscript_name().unwrap();
216 self.font_ids_by_postscript_name
217 .insert(postscript_name.clone(), font_id);
218 self.postscript_names_by_font_id
219 .insert(font_id, postscript_name);
220 self.fonts.push(font);
221 }
222 Ok(font_ids)
223 }
224
225 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
226 Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
227 }
228
229 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
230 self.fonts[font_id.0].glyph_for_char(ch).map(Into::into)
231 }
232
233 fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
234 let postscript_name = requested_font.postscript_name();
235 if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
236 *font_id
237 } else {
238 let font_id = FontId(self.fonts.len());
239 self.font_ids_by_postscript_name
240 .insert(postscript_name.clone(), font_id);
241 self.postscript_names_by_font_id
242 .insert(font_id, postscript_name);
243 self.fonts
244 .push(font_kit::font::Font::from_core_graphics_font(
245 requested_font.copy_to_CGFont(),
246 ));
247 font_id
248 }
249 }
250
251 fn is_emoji(&self, font_id: FontId) -> bool {
252 self.postscript_names_by_font_id
253 .get(&font_id)
254 .map_or(false, |postscript_name| {
255 postscript_name == "AppleColorEmoji"
256 })
257 }
258
259 fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
260 let font = &self.fonts[params.font_id.0];
261 let scale = Transform2F::from_scale(params.scale_factor);
262 Ok(font
263 .raster_bounds(
264 params.glyph_id.into(),
265 params.font_size.into(),
266 scale,
267 HintingOptions::None,
268 font_kit::canvas::RasterizationOptions::GrayscaleAa,
269 )?
270 .into())
271 }
272
273 fn rasterize_glyph(
274 &self,
275 params: &RenderGlyphParams,
276 glyph_bounds: Bounds<DevicePixels>,
277 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
278 if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
279 Err(anyhow!("glyph bounds are empty"))
280 } else {
281 // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
282 let mut bitmap_size = glyph_bounds.size;
283 if params.subpixel_variant.x > 0 {
284 bitmap_size.width += DevicePixels(1);
285 }
286 if params.subpixel_variant.y > 0 {
287 bitmap_size.height += DevicePixels(1);
288 }
289 let bitmap_size = bitmap_size;
290
291 let mut bytes;
292 let cx;
293 if params.is_emoji {
294 bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
295 cx = CGContext::create_bitmap_context(
296 Some(bytes.as_mut_ptr() as *mut _),
297 bitmap_size.width.0 as usize,
298 bitmap_size.height.0 as usize,
299 8,
300 bitmap_size.width.0 as usize * 4,
301 &CGColorSpace::create_device_rgb(),
302 kCGImageAlphaPremultipliedLast,
303 );
304 } else {
305 bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
306 cx = CGContext::create_bitmap_context(
307 Some(bytes.as_mut_ptr() as *mut _),
308 bitmap_size.width.0 as usize,
309 bitmap_size.height.0 as usize,
310 8,
311 bitmap_size.width.0 as usize,
312 &CGColorSpace::create_device_gray(),
313 kCGImageAlphaOnly,
314 );
315 }
316
317 // Move the origin to bottom left and account for scaling, this
318 // makes drawing text consistent with the font-kit's raster_bounds.
319 cx.translate(
320 -glyph_bounds.origin.x.0 as CGFloat,
321 (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,
322 );
323 cx.scale(
324 params.scale_factor as CGFloat,
325 params.scale_factor as CGFloat,
326 );
327
328 let subpixel_shift = params
329 .subpixel_variant
330 .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
331 cx.set_allows_font_subpixel_positioning(true);
332 cx.set_should_subpixel_position_fonts(true);
333 cx.set_allows_font_subpixel_quantization(false);
334 cx.set_should_subpixel_quantize_fonts(false);
335 self.fonts[params.font_id.0]
336 .native_font()
337 .clone_with_font_size(f32::from(params.font_size) as CGFloat)
338 .draw_glyphs(
339 &[u32::from(params.glyph_id) as CGGlyph],
340 &[CGPoint::new(
341 (subpixel_shift.x / params.scale_factor) as CGFloat,
342 (subpixel_shift.y / params.scale_factor) as CGFloat,
343 )],
344 cx,
345 );
346
347 if params.is_emoji {
348 // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
349 for pixel in bytes.chunks_exact_mut(4) {
350 pixel.swap(0, 2);
351 let a = pixel[3] as f32 / 255.;
352 pixel[0] = (pixel[0] as f32 / a) as u8;
353 pixel[1] = (pixel[1] as f32 / a) as u8;
354 pixel[2] = (pixel[2] as f32 / a) as u8;
355 }
356 }
357
358 Ok((bitmap_size, bytes))
359 }
360 }
361
362 fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
363 // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
364 let mut string = CFMutableAttributedString::new();
365 {
366 string.replace_str(&CFString::new(text), CFRange::init(0, 0));
367 let utf16_line_len = string.char_len() as usize;
368
369 let mut ix_converter = StringIndexConverter::new(text);
370 for run in font_runs {
371 let utf8_end = ix_converter.utf8_ix + run.len;
372 let utf16_start = ix_converter.utf16_ix;
373
374 if utf16_start >= utf16_line_len {
375 break;
376 }
377
378 ix_converter.advance_to_utf8_ix(utf8_end);
379 let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
380
381 let cf_range =
382 CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
383
384 let font: &FontKitFont = &self.fonts[run.font_id.0];
385 unsafe {
386 string.set_attribute(
387 cf_range,
388 kCTFontAttributeName,
389 &font.native_font().clone_with_font_size(font_size.into()),
390 );
391 }
392
393 if utf16_end == utf16_line_len {
394 break;
395 }
396 }
397 }
398
399 // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
400 let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
401
402 let mut runs = Vec::new();
403 for run in line.glyph_runs().into_iter() {
404 let attributes = run.attributes().unwrap();
405 let font = unsafe {
406 attributes
407 .get(kCTFontAttributeName)
408 .downcast::<CTFont>()
409 .unwrap()
410 };
411 let font_id = self.id_for_native_font(font);
412
413 let mut ix_converter = StringIndexConverter::new(text);
414 let mut glyphs = SmallVec::new();
415 for ((glyph_id, position), glyph_utf16_ix) in run
416 .glyphs()
417 .iter()
418 .zip(run.positions().iter())
419 .zip(run.string_indices().iter())
420 {
421 let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
422 ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
423 glyphs.push(ShapedGlyph {
424 id: (*glyph_id).into(),
425 position: point(position.x as f32, position.y as f32).map(px),
426 index: ix_converter.utf8_ix,
427 is_emoji: self.is_emoji(font_id),
428 });
429 }
430
431 runs.push(ShapedRun { font_id, glyphs })
432 }
433
434 let typographic_bounds = line.get_typographic_bounds();
435 LineLayout {
436 runs,
437 font_size,
438 width: typographic_bounds.width.into(),
439 ascent: typographic_bounds.ascent.into(),
440 descent: typographic_bounds.descent.into(),
441 len: text.len(),
442 }
443 }
444
445 fn wrap_line(
446 &self,
447 text: &str,
448 font_id: FontId,
449 font_size: Pixels,
450 width: Pixels,
451 ) -> Vec<usize> {
452 let mut string = CFMutableAttributedString::new();
453 string.replace_str(&CFString::new(text), CFRange::init(0, 0));
454 let cf_range = CFRange::init(0, text.encode_utf16().count() as isize);
455 let font = &self.fonts[font_id.0];
456 unsafe {
457 string.set_attribute(
458 cf_range,
459 kCTFontAttributeName,
460 &font.native_font().clone_with_font_size(font_size.into()),
461 );
462
463 let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
464 let mut ix_converter = StringIndexConverter::new(text);
465 let mut break_indices = Vec::new();
466 while ix_converter.utf8_ix < text.len() {
467 let utf16_len = CTTypesetterSuggestLineBreak(
468 typesetter,
469 ix_converter.utf16_ix as isize,
470 width.into(),
471 ) as usize;
472 ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
473 if ix_converter.utf8_ix >= text.len() {
474 break;
475 }
476 break_indices.push(ix_converter.utf8_ix);
477 }
478 break_indices
479 }
480 }
481}
482
483#[derive(Clone)]
484struct StringIndexConverter<'a> {
485 text: &'a str,
486 utf8_ix: usize,
487 utf16_ix: usize,
488}
489
490impl<'a> StringIndexConverter<'a> {
491 fn new(text: &'a str) -> Self {
492 Self {
493 text,
494 utf8_ix: 0,
495 utf16_ix: 0,
496 }
497 }
498
499 fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
500 for (ix, c) in self.text[self.utf8_ix..].char_indices() {
501 if self.utf8_ix + ix >= utf8_target {
502 self.utf8_ix += ix;
503 return;
504 }
505 self.utf16_ix += c.len_utf16();
506 }
507 self.utf8_ix = self.text.len();
508 }
509
510 fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
511 for (ix, c) in self.text[self.utf8_ix..].char_indices() {
512 if self.utf16_ix >= utf16_target {
513 self.utf8_ix += ix;
514 return;
515 }
516 self.utf16_ix += c.len_utf16();
517 }
518 self.utf8_ix = self.text.len();
519 }
520}
521
522#[repr(C)]
523pub(crate) struct __CFTypesetter(c_void);
524
525type CTTypesetterRef = *const __CFTypesetter;
526
527#[link(name = "CoreText", kind = "framework")]
528extern "C" {
529 fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
530
531 fn CTTypesetterSuggestLineBreak(
532 typesetter: CTTypesetterRef,
533 start_index: CFIndex,
534 width: f64,
535 ) -> CFIndex;
536}
537
538impl From<Metrics> for FontMetrics {
539 fn from(metrics: Metrics) -> Self {
540 FontMetrics {
541 units_per_em: metrics.units_per_em,
542 ascent: metrics.ascent,
543 descent: metrics.descent,
544 line_gap: metrics.line_gap,
545 underline_position: metrics.underline_position,
546 underline_thickness: metrics.underline_thickness,
547 cap_height: metrics.cap_height,
548 x_height: metrics.x_height,
549 bounding_box: metrics.bounding_box.into(),
550 }
551 }
552}
553
554impl From<RectF> for Bounds<f32> {
555 fn from(rect: RectF) -> Self {
556 Bounds {
557 origin: point(rect.origin_x(), rect.origin_y()),
558 size: size(rect.width(), rect.height()),
559 }
560 }
561}
562
563impl From<RectI> for Bounds<DevicePixels> {
564 fn from(rect: RectI) -> Self {
565 Bounds {
566 origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
567 size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
568 }
569 }
570}
571
572impl From<Vector2I> for Size<DevicePixels> {
573 fn from(value: Vector2I) -> Self {
574 size(value.x().into(), value.y().into())
575 }
576}
577
578impl From<RectI> for Bounds<i32> {
579 fn from(rect: RectI) -> Self {
580 Bounds {
581 origin: point(rect.origin_x(), rect.origin_y()),
582 size: size(rect.width(), rect.height()),
583 }
584 }
585}
586
587impl From<Point<u32>> for Vector2I {
588 fn from(size: Point<u32>) -> Self {
589 Vector2I::new(size.x as i32, size.y as i32)
590 }
591}
592
593impl From<Vector2F> for Size<f32> {
594 fn from(vec: Vector2F) -> Self {
595 size(vec.x(), vec.y())
596 }
597}
598
599impl From<FontWeight> for FontkitWeight {
600 fn from(value: FontWeight) -> Self {
601 FontkitWeight(value.0)
602 }
603}
604
605impl From<FontStyle> for FontkitStyle {
606 fn from(style: FontStyle) -> Self {
607 match style {
608 FontStyle::Normal => FontkitStyle::Normal,
609 FontStyle::Italic => FontkitStyle::Italic,
610 FontStyle::Oblique => FontkitStyle::Oblique,
611 }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
618
619 #[test]
620 fn test_wrap_line() {
621 let fonts = MacTextSystem::new();
622 let font_id = fonts.font_id(&font("Helvetica")).unwrap();
623
624 let line = "one two three four five\n";
625 let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
626 assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
627
628 let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
629 let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
630 assert_eq!(
631 wrap_boundaries,
632 &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
633 );
634 }
635
636 #[test]
637 fn test_layout_line_bom_char() {
638 let fonts = MacTextSystem::new();
639 let font_id = fonts.font_id(&font("Helvetica")).unwrap();
640 let line = "\u{feff}";
641 let mut style = FontRun {
642 font_id,
643 len: line.len(),
644 };
645
646 let layout = fonts.layout_line(line, px(16.), &[style]);
647 assert_eq!(layout.len, line.len());
648 assert!(layout.runs.is_empty());
649
650 let line = "a\u{feff}b";
651 style.len = line.len();
652 let layout = fonts.layout_line(line, px(16.), &[style]);
653 assert_eq!(layout.len, line.len());
654 assert_eq!(layout.runs.len(), 1);
655 assert_eq!(layout.runs[0].glyphs.len(), 2);
656 assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a
657 // There's no glyph for \u{feff}
658 assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b
659 }
660}