piet_coregraphics/
ct_helpers.rs

1// Copyright 2020 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Wrappers around CF/CT types, with nice interfaces.
5
6#![allow(clippy::upper_case_acronyms)]
7
8use std::ffi::c_void;
9use std::rc::Rc;
10
11use core_foundation::{
12    array::{CFArray, CFArrayRef, CFIndex},
13    attributed_string::CFMutableAttributedString,
14    base::{CFTypeID, TCFType},
15    declare_TCFType,
16    dictionary::{CFDictionary, CFDictionaryRef},
17    impl_TCFType,
18    number::CFNumber,
19    string::{CFString, CFStringRef},
20};
21use core_foundation_sys::base::CFRange;
22use core_graphics::{
23    base::CGFloat,
24    color::CGColor,
25    context::CGContextRef,
26    data_provider::CGDataProvider,
27    font::CGFont,
28    geometry::{CGAffineTransform, CGPoint, CGRect, CGSize},
29    path::CGPathRef,
30};
31use core_text::{
32    font::{
33        self, CTFont, CTFontRef, CTFontUIFontType, kCTFontSystemFontType,
34        kCTFontUserFixedPitchFontType,
35    },
36    font_collection::{self, CTFontCollection, CTFontCollectionRef},
37    font_descriptor::{self, CTFontDescriptor, CTFontDescriptorRef},
38    frame::CTFrame,
39    framesetter::CTFramesetter,
40    line::{CTLine, CTLineRef, TypographicBounds},
41    string_attributes,
42};
43use foreign_types::{ForeignType, ForeignTypeRef};
44
45use piet::kurbo::{Affine, Rect};
46use piet::{Color, FontFamily, FontFamilyInner, TextAlignment, util};
47
48#[derive(Clone)]
49pub(crate) struct AttributedString {
50    pub(crate) inner: CFMutableAttributedString,
51    /// a guess as to text direction
52    rtl: bool,
53}
54
55#[derive(Debug, Clone)]
56pub(crate) struct Framesetter(CTFramesetter);
57#[derive(Debug, Clone)]
58pub(crate) struct Frame {
59    frame: CTFrame,
60    lines: Rc<[Line]>,
61}
62#[derive(Debug, Clone)]
63pub(crate) struct Line(CTLine);
64
65#[derive(Debug, Clone)]
66pub(crate) struct FontCollection(CTFontCollection);
67
68pub enum __CTParagraphStyle {}
69type CTParagraphStyleRef = *const __CTParagraphStyle;
70
71declare_TCFType!(CTParagraphStyle, CTParagraphStyleRef);
72impl_TCFType!(
73    CTParagraphStyle,
74    CTParagraphStyleRef,
75    CTParagraphStyleGetTypeID
76);
77
78#[repr(u32)]
79enum CTParagraphStyleSpecifier {
80    Alignment = 0,
81    //FirstLineHeadIndent = 1,
82    //HeadIndent = 2,
83    //TailIndent = 3,
84    //TabStops = 4,
85    //TabInterval = 5,
86    //LineBreakMode = 6,
87    // there are many more of these
88}
89
90#[repr(u8)]
91enum CTTextAlignment {
92    Left = 0,
93    Right = 1,
94    Center = 2,
95    Justified = 3,
96    Natural = 4,
97}
98
99#[repr(C)]
100struct CTParagraphStyleSetting {
101    spec: CTParagraphStyleSpecifier,
102    value_size: usize,
103    value: *const c_void,
104}
105
106impl CTParagraphStyleSetting {
107    fn alignment(alignment: TextAlignment, is_rtl: bool) -> Self {
108        static LEFT: CTTextAlignment = CTTextAlignment::Left;
109        static RIGHT: CTTextAlignment = CTTextAlignment::Right;
110        static CENTER: CTTextAlignment = CTTextAlignment::Center;
111        static JUSTIFIED: CTTextAlignment = CTTextAlignment::Justified;
112        static NATURAL: CTTextAlignment = CTTextAlignment::Natural;
113
114        let alignment: *const CTTextAlignment = match alignment {
115            TextAlignment::Start => &NATURAL,
116            TextAlignment::End if is_rtl => &LEFT,
117            TextAlignment::End => &RIGHT,
118            TextAlignment::Center => &CENTER,
119            TextAlignment::Justified => &JUSTIFIED,
120        };
121
122        CTParagraphStyleSetting {
123            spec: CTParagraphStyleSpecifier::Alignment,
124            value: alignment as *const c_void,
125            value_size: std::mem::size_of::<CTTextAlignment>(),
126        }
127    }
128}
129
130impl AttributedString {
131    pub(crate) fn new(text: &str) -> Self {
132        let mut inner = CFMutableAttributedString::new();
133        let range = CFRange::init(0, 0);
134        let cf_string = CFString::new(text);
135        inner.replace_str(&cf_string, range);
136        let rtl = util::first_strong_rtl(text);
137        AttributedString { inner, rtl }
138    }
139
140    pub(crate) fn set_alignment(&mut self, alignment: TextAlignment) {
141        let alignment = CTParagraphStyleSetting::alignment(alignment, self.rtl);
142        let settings = [alignment];
143        unsafe {
144            let style = CTParagraphStyleCreate(settings.as_ptr(), 1);
145            let style = CTParagraphStyle::wrap_under_create_rule(style);
146            self.inner.set_attribute(
147                self.range(),
148                string_attributes::kCTParagraphStyleAttributeName,
149                &style,
150            );
151        }
152    }
153
154    pub(crate) fn set_font(&mut self, range: CFRange, font: &CTFont) {
155        unsafe {
156            self.inner
157                .set_attribute(range, string_attributes::kCTFontAttributeName, font);
158        }
159    }
160
161    #[allow(non_upper_case_globals)]
162    pub(crate) fn set_underline(&mut self, range: CFRange, underline: bool) {
163        const kCTUnderlineStyleNone: i32 = 0x00;
164        const kCTUnderlineStyleSingle: i32 = 0x01;
165
166        let value = if underline {
167            kCTUnderlineStyleSingle
168        } else {
169            kCTUnderlineStyleNone
170        };
171        unsafe {
172            self.inner.set_attribute(
173                range,
174                string_attributes::kCTUnderlineStyleAttributeName,
175                &CFNumber::from(value).as_CFType(),
176            )
177        }
178    }
179
180    pub(crate) fn set_fg_color(&mut self, range: CFRange, color: Color) {
181        let (r, g, b, a) = color.as_rgba();
182        let color = CGColor::rgb(r, g, b, a);
183        unsafe {
184            self.inner.set_attribute(
185                range,
186                string_attributes::kCTForegroundColorAttributeName,
187                &color.as_CFType(),
188            )
189        }
190    }
191
192    pub(crate) fn range(&self) -> CFRange {
193        CFRange::init(0, self.inner.char_len())
194    }
195}
196
197impl Framesetter {
198    pub(crate) fn new(attributed_string: &AttributedString) -> Self {
199        Framesetter(CTFramesetter::new_with_attributed_string(
200            attributed_string.inner.as_concrete_TypeRef(),
201        ))
202    }
203
204    /// returns the suggested size and the range of the string that fits.
205    #[allow(dead_code)]
206    pub(crate) fn suggest_frame_size(
207        &self,
208        range: CFRange,
209        constraints: CGSize,
210    ) -> (CGSize, CFRange) {
211        self.0
212            .suggest_frame_size_with_constraints(range, std::ptr::null(), constraints)
213    }
214
215    pub(crate) fn create_frame(&self, range: CFRange, path: &CGPathRef) -> Frame {
216        let frame = self.0.create_frame(range, path);
217        let lines = frame.get_lines().into_iter().map(Line);
218        Frame {
219            frame,
220            lines: lines.collect(),
221        }
222    }
223}
224
225impl Frame {
226    pub(crate) fn lines(&self) -> &[Line] {
227        &self.lines
228    }
229
230    pub(crate) fn get_line(&self, line_number: usize) -> Option<Line> {
231        self.lines.get(line_number).cloned()
232    }
233
234    pub(crate) fn get_line_origins(&self, range: CFRange) -> Vec<CGPoint> {
235        self.frame.get_line_origins(range)
236    }
237
238    #[allow(dead_code)]
239    pub(crate) fn draw(&self, ctx: &mut CGContextRef) {
240        self.frame.draw(ctx)
241    }
242}
243
244impl Line {
245    pub(crate) fn get_string_range(&self) -> CFRange {
246        self.0.get_string_range()
247    }
248
249    pub(crate) fn get_typographic_bounds(&self) -> TypographicBounds {
250        self.0.get_typographic_bounds()
251    }
252
253    pub(crate) fn get_trailing_whitespace_width(&self) -> f64 {
254        unsafe { CTLineGetTrailingWhitespaceWidth(self.0.as_concrete_TypeRef()) }
255    }
256
257    pub(crate) fn get_image_bounds(&self) -> Rect {
258        unsafe {
259            let r = CTLineGetImageBounds(self.0.as_concrete_TypeRef(), std::ptr::null_mut());
260            Rect::from_origin_size((r.origin.x, r.origin.y), (r.size.width, r.size.height))
261        }
262    }
263
264    pub(crate) fn draw(&self, ctx: &mut CGContextRef) {
265        unsafe { CTLineDraw(self.0.as_concrete_TypeRef(), ctx.as_ptr()) }
266    }
267
268    pub(crate) fn get_string_index_for_position(&self, position: CGPoint) -> CFIndex {
269        self.0.get_string_index_for_position(position)
270    }
271
272    /// Return the 'primary' offset on the given line that the boundary of the
273    /// character at the provided index.
274    ///
275    /// There is a 'secondary' offset that is not returned by the core-text crate,
276    /// that is used for BiDi. We can worry about that when we worry about *that*.
277    /// There are docs at:
278    /// <https://developer.apple.com/documentation/coretext/1509629-ctlinegetoffsetforstringindex>
279    pub(crate) fn get_offset_for_string_index(&self, index: CFIndex) -> CGFloat {
280        self.0.get_string_offset_for_string_index(index)
281    }
282}
283
284/// The apple system fonts can resolve to different concrete families at
285/// different point sizes (SF Text vs. SF Displaykj,w)
286pub(crate) fn ct_family_name(family: &FontFamily, size: f64) -> CFString {
287    match &family.inner() {
288        FontFamilyInner::Named(name) => CFString::new(name),
289        other => system_font_family_name(other, size),
290    }
291}
292
293/// Create a generic system font.
294fn system_font_family_name(family: &FontFamilyInner, size: f64) -> CFString {
295    let font = system_font_impl(family, size).unwrap_or_else(create_font_comma_never_fail_period);
296    unsafe {
297        let name = CTFontCopyName(font.as_concrete_TypeRef(), kCTFontFamilyNameKey);
298        CFString::wrap_under_create_rule(name)
299    }
300}
301
302fn system_font_impl(family: &FontFamilyInner, size: f64) -> Option<CTFont> {
303    match family {
304        FontFamilyInner::SansSerif | FontFamilyInner::SystemUi => {
305            create_system_font(kCTFontSystemFontType, size)
306        }
307        //TODO: on 10.15 + we should be using new york here
308        //FIXME: font::new_from_name actually never fails
309        FontFamilyInner::Serif => font::new_from_name("Charter", size)
310            .or_else(|_| font::new_from_name("Times", size))
311            .or_else(|_| font::new_from_name("Times New Roman", size))
312            .ok(),
313        //TODO: on 10.15 we should be using SF-Mono here
314        FontFamilyInner::Monospace => font::new_from_name("Menlo", size)
315            .ok()
316            .or_else(|| create_system_font(kCTFontUserFixedPitchFontType, size)),
317        _ => panic!("system fontz only"),
318    }
319}
320
321fn create_system_font(typ: CTFontUIFontType, size: f64) -> Option<CTFont> {
322    unsafe {
323        let font = CTFontCreateUIFontForLanguage(typ, size, std::ptr::null());
324        if font.is_null() {
325            None
326        } else {
327            Some(CTFont::wrap_under_create_rule(font))
328        }
329    }
330}
331
332fn create_font_comma_never_fail_period() -> CTFont {
333    let empty_attributes = CFDictionary::from_CFType_pairs(&[]);
334    let descriptor = font_descriptor::new_from_attributes(&empty_attributes);
335    font::new_from_descriptor(&descriptor, 0.0)
336}
337
338pub(crate) fn add_font(font_data: &[u8]) -> Result<String, ()> {
339    unsafe {
340        let data = CGDataProvider::from_slice(font_data);
341        let font_ref = CGFont::from_data_provider(data)?;
342        let success = CTFontManagerRegisterGraphicsFont(font_ref.as_ptr(), std::ptr::null_mut());
343        if success {
344            let ct_font = font::new_from_CGFont(&font_ref, 0.0);
345            Ok(ct_font.family_name())
346        } else {
347            Err(())
348        }
349    }
350}
351
352//TODO: this will probably be shared at some point?
353impl FontCollection {
354    pub(crate) fn new_with_all_fonts() -> FontCollection {
355        FontCollection(font_collection::create_for_all_families())
356    }
357
358    pub(crate) fn font_for_family_name(&mut self, name: &str) -> Option<FontFamily> {
359        let name = CFString::from(name);
360        unsafe {
361            let array = CTFontCollectionCreateMatchingFontDescriptorsForFamily(
362                self.0.as_concrete_TypeRef(),
363                name.as_concrete_TypeRef(),
364                std::ptr::null(),
365            );
366
367            if array.is_null() {
368                None
369            } else {
370                let array = CFArray::<CTFontDescriptor>::wrap_under_create_rule(array);
371                array
372                    .get(0)
373                    .map(|desc| FontFamily::new_unchecked(desc.family_name()))
374            }
375        }
376    }
377}
378
379// the version of this in the coretext crate doesn't let you supply an affine
380#[allow(clippy::many_single_char_names)]
381pub(crate) fn make_font(desc: &CTFontDescriptor, pt_size: f64, affine: Affine) -> CTFont {
382    let [a, b, c, d, e, f] = affine.as_coeffs();
383    let affine = CGAffineTransform::new(a, b, c, d, e, f);
384    unsafe {
385        let font_ref = CTFontCreateWithFontDescriptor(
386            desc.as_concrete_TypeRef(),
387            pt_size as CGFloat,
388            // ownership here isn't documented, which I presume to mean it is copied,
389            // and we can pass a stack pointer.
390            &affine as *const _,
391        );
392        CTFont::wrap_under_create_rule(font_ref)
393    }
394}
395
396#[link(name = "CoreText", kind = "framework")]
397unsafe extern "C" {
398    static kCTFontFamilyNameKey: CFStringRef;
399
400    pub static kCTFontVariationAxisIdentifierKey: CFStringRef;
401    //static kCTFontVariationAxisMinimumValueKey: CFStringRef;
402    //static kCTFontVariationAxisMaximumValueKey: CFStringRef;
403    //static kCTFontVariationAxisDefaultValueKey: CFStringRef;
404    //pub static kCTFontVariationAxisNameKey: CFStringRef;
405
406    fn CTFontCreateUIFontForLanguage(
407        font_type: CTFontUIFontType,
408        size: CGFloat,
409        language: CFStringRef,
410    ) -> CTFontRef;
411    fn CTParagraphStyleGetTypeID() -> CFTypeID;
412    fn CTParagraphStyleCreate(
413        settings: *const CTParagraphStyleSetting,
414        count: usize,
415    ) -> CTParagraphStyleRef;
416    fn CTLineGetImageBounds(line: CTLineRef, ctx: *mut c_void) -> CGRect;
417    fn CTLineDraw(line: CTLineRef, ctx: core_graphics::sys::CGContextRef);
418    fn CTLineGetTrailingWhitespaceWidth(line: CTLineRef) -> f64;
419    fn CTFontCollectionCreateMatchingFontDescriptorsForFamily(
420        collection: CTFontCollectionRef,
421        family: CFStringRef,
422        option: CFDictionaryRef,
423    ) -> CFArrayRef;
424    fn CTFontCopyName(font: CTFontRef, nameKey: CFStringRef) -> CFStringRef;
425    fn CTFontCreateWithFontDescriptor(
426        descriptor: CTFontDescriptorRef,
427        size: CGFloat,
428        matrix: *const CGAffineTransform,
429    ) -> CTFontRef;
430    fn CTFontManagerRegisterGraphicsFont(
431        font: core_graphics::sys::CGFontRef,
432        error: *mut c_void,
433    ) -> bool;
434}