StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use block::{Block, ConcreteBlock}; |
| 2 | use core_foundation::array::CFArray; |
| 3 | use core_foundation::attributed_string::CFMutableAttributedStringRef; |
| 4 | use core_foundation::base::CFType; |
| 5 | use core_foundation::boolean::CFBoolean; |
| 6 | use core_foundation::dictionary::CFDictionary; |
| 7 | use core_foundation::mach_port::CFIndex; |
| 8 | use core_foundation::number::CFNumber; |
| 9 | use core_foundation::{ |
| 10 | attributed_string::CFMutableAttributedString, |
| 11 | base::CFTypeID, |
| 12 | base::{CFRange, TCFType}, |
| 13 | declare_TCFType, impl_TCFType, |
| 14 | string::CFString, |
| 15 | }; |
| 16 | use core_graphics::base::CGFloat; |
| 17 | use core_graphics::color::CGColor; |
| 18 | use core_graphics::display::{CGPoint, CGRect, CGSize}; |
| 19 | use core_graphics::path::CGPath; |
| 20 | use core_text::framesetter::CTFramesetter; |
| 21 | use core_text::line::CTLineRef; |
| 22 | use core_text::run::{CTRun, CTRunRef}; |
| 23 | use core_text::string_attributes::kCTKernAttributeName; |
| 24 | use core_text::{ |
| 25 | font::CTFont, |
| 26 | line::CTLine, |
| 27 | string_attributes::{kCTFontAttributeName, kCTParagraphStyleAttributeName}, |
| 28 | }; |
| 29 | use itertools::Itertools; |
| 30 | use ordered_float::OrderedFloat; |
| 31 | use pathfinder_geometry::vector::vec2f; |
| 32 | use std::borrow::Cow; |
| 33 | use std::cell::RefCell; |
| 34 | use std::ffi::c_void; |
| 35 | use std::marker::PhantomData; |
| 36 | use std::ops::Range; |
| 37 | use std::rc::Rc; |
| 38 | use std::slice; |
| 39 | use strato_ui_core::fonts::GlyphId; |
| 40 | use strato_ui_core::platform::LineStyle; |
| 41 | use strato_ui_core::text_layout::{ |
| 42 | CaretPosition, ClipConfig, Glyph, Line, Run, StyleAndFont, TextAlignment, TextBorder, |
| 43 | TextFrame, TextStyle, |
| 44 | }; |
| 45 | use vec1::Vec1; |
| 46 | |
| 47 | use super::fonts::FontDB; |
| 48 | use super::utils::{cg_color_to_color_u, color_u_to_cg_color}; |
| 49 | |
| 50 | pub enum __CTParagraphStyle {} |
| 51 | type CTParagraphStyleRef = *const __CTParagraphStyle; |
| 52 | |
| 53 | declare_TCFType!(CTParagraphStyle, CTParagraphStyleRef); |
| 54 | impl_TCFType!( |
| 55 | CTParagraphStyle, |
| 56 | CTParagraphStyleRef, |
| 57 | CTParagraphStyleGetTypeID |
| 58 | ); |
| 59 | |
| 60 | /// From https://developer.apple.com/documentation/coretext/ctparagraphstylespecifier |
| 61 | /// Note: there are many more of these possible style specifiers |
| 62 | /// but we are not using them! |
| 63 | #[derive(Clone, Copy)] |
| 64 | #[repr(u32)] |
| 65 | enum CTParagraphStyleSpecifier { |
| 66 | FirstLineHeadIndent = 1, |
| 67 | TabStops = 4, |
| 68 | DefaultTabInterval = 5, |
| 69 | LineHeightMultiple = 7, |
| 70 | } |
| 71 | |
| 72 | /// See https://developer.apple.com/documentation/coretext/ctparagraphstylesetting |
| 73 | /// for the API specification on paragraph style settings. |
| 74 | /// We tie the lifetime of this struct to ParagraphStyleSetting to ensure |
| 75 | /// that we don't drop the `value` before consuming the setting (in the context |
| 76 | /// of creating an attributed string). |
| 77 | #[repr(C)] |
| 78 | struct CTParagraphStyleSetting<'a> { |
| 79 | spec: CTParagraphStyleSpecifier, |
| 80 | value_size: usize, |
| 81 | value: *const c_void, |
| 82 | /// PhantomData is used to add a marker that CTParagraphStyleSetting should not |
| 83 | /// live any longer than ParagraphStyleSetting is alive. |
| 84 | _phantom: PhantomData<&'a ParagraphStyleSetting>, |
| 85 | } |
| 86 | |
| 87 | enum ParagraphStyleValue { |
| 88 | Float(Box<CGFloat>), |
| 89 | Array { |
| 90 | _array: CFArray<CFType>, |
| 91 | stable_ref: Box<*const c_void>, |
| 92 | }, |
| 93 | } |
| 94 | |
| 95 | /// We wrap CTParagraphStyleSetting with a custom struct to correctly hold a raw |
| 96 | /// pointer to a heap-allocated value, which is needed for Core Foundation FFIs. |
| 97 | struct ParagraphStyleSetting { |
| 98 | /// The specific style we are trying to define for the paragraph. |
| 99 | spec: CTParagraphStyleSpecifier, |
| 100 | value: ParagraphStyleValue, |
| 101 | } |
| 102 | |
| 103 | impl ParagraphStyleSetting { |
| 104 | fn new_float_setting(spec: CTParagraphStyleSpecifier, value: CGFloat) -> ParagraphStyleSetting { |
| 105 | ParagraphStyleSetting { |
| 106 | spec, |
| 107 | value: ParagraphStyleValue::Float(Box::new(value)), |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | fn new_empty_tab_stops() -> ParagraphStyleSetting { |
| 112 | // Core Text has a built-in default list of tab stops. Setting DefaultTabInterval alone |
| 113 | // doesn't override those initial stops, so we set an explicit (empty) TabStops array |
| 114 | // to force Core Text to use DefaultTabInterval from the first tab. |
| 115 | let array: CFArray<CFType> = CFArray::from_CFTypes(&[]); |
| 116 | let array_ref = array.as_concrete_TypeRef() as *const c_void; |
| 117 | |
| 118 | ParagraphStyleSetting { |
| 119 | spec: CTParagraphStyleSpecifier::TabStops, |
| 120 | value: ParagraphStyleValue::Array { |
| 121 | _array: array, |
| 122 | stable_ref: Box::new(array_ref), |
| 123 | }, |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | fn to_ct_setting(&self) -> CTParagraphStyleSetting<'_> { |
| 128 | match &self.value { |
| 129 | ParagraphStyleValue::Float(val) => { |
| 130 | let raw_ptr = val.as_ref() as *const CGFloat as *const c_void; |
| 131 | |
| 132 | CTParagraphStyleSetting { |
| 133 | spec: self.spec, |
| 134 | value_size: std::mem::size_of::<CGFloat>(), |
| 135 | value: raw_ptr, |
| 136 | _phantom: PhantomData, |
| 137 | } |
| 138 | } |
| 139 | ParagraphStyleValue::Array { stable_ref, .. } => CTParagraphStyleSetting { |
| 140 | spec: self.spec, |
| 141 | value_size: std::mem::size_of::<*const c_void>(), |
| 142 | value: stable_ref.as_ref() as *const *const c_void as *const c_void, |
| 143 | _phantom: PhantomData, |
| 144 | }, |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | /// ParagraphStyle is a wrapper struct that helps tie the underlying lifetimes of the settings |
| 150 | /// being used to the CTParagraphStyle. |
| 151 | struct ParagraphStyle<'a> { |
| 152 | style: CTParagraphStyle, |
| 153 | _settings: Vec<CTParagraphStyleSetting<'a>>, |
| 154 | } |
| 155 | |
| 156 | impl<'a> ParagraphStyle<'a> { |
| 157 | fn new(ct_style_settings: Vec<CTParagraphStyleSetting<'a>>) -> ParagraphStyle<'a> { |
| 158 | let paragraph_style = unsafe { |
| 159 | CTParagraphStyle::wrap_under_create_rule(CTParagraphStyleCreate( |
| 160 | ct_style_settings.as_slice().as_ptr(), |
| 161 | ct_style_settings.len(), |
| 162 | )) |
| 163 | }; |
| 164 | |
| 165 | ParagraphStyle { |
| 166 | style: paragraph_style, |
| 167 | _settings: ct_style_settings, |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | extern "C" { |
| 173 | fn CFAttributedStringBeginEditing(mutable_string: CFMutableAttributedStringRef); |
| 174 | |
| 175 | fn CFAttributedStringEndEditing(mutable_string: CFMutableAttributedStringRef); |
| 176 | |
| 177 | /// Enumerates caret offsets for characters in a line. |
| 178 | /// See [CTLineEnumerateCaretOffsets](https://developer.apple.com/documentation/coretext/1508685-ctlineenumeratecaretoffsets?language=objc). |
| 179 | #[allow(improper_ctypes)] |
| 180 | // Rust doesn't consider &Block a valid FFI type, but it is the correct thing |
| 181 | // as long as the block crate invariants are upheld. |
| 182 | fn CTLineEnumerateCaretOffsets( |
| 183 | line: CTLineRef, |
| 184 | block: &Block<(f64, CFIndex, bool, *mut bool), ()>, |
| 185 | ); |
| 186 | |
| 187 | fn CTLineGetTrailingWhitespaceWidth(line: CTLineRef) -> f64; |
| 188 | |
| 189 | fn CTParagraphStyleGetTypeID() -> CFTypeID; |
| 190 | fn CTParagraphStyleCreate( |
| 191 | settings: *const CTParagraphStyleSetting, |
| 192 | count: usize, |
| 193 | ) -> CTParagraphStyleRef; |
| 194 | |
| 195 | fn CTRunGetGlyphCount(run: CTRunRef) -> CFIndex; |
| 196 | fn CTRunGetAdvancesPtr(run: CTRunRef) -> *const CGSize; |
| 197 | fn CTRunGetAdvances(run: CTRunRef, range: CFRange, buffer: *mut CGSize); |
| 198 | } |
| 199 | |
| 200 | const FOREGROUND_COLOR_KEY: &str = "foreground-color"; |
| 201 | const SYNTAX_COLOR_KEY: &str = "syntax-color"; |
| 202 | const BACKGROUND_COLOR_KEY: &str = "background-color"; |
| 203 | const ERROR_UNDERLINE_COLOR_KEY: &str = "error-underline-color"; |
| 204 | const BORDER_COLOR_KEY: &str = "border-color"; |
| 205 | const BORDER_WIDTH_KEY: &str = "border-width"; |
| 206 | const BORDER_RADIUS_KEY: &str = "border-radius"; |
| 207 | const BORDER_LINE_HEIGHT_RATIO_KEY: &str = "border-line-height-ratio"; |
| 208 | const SHOW_STRIKETHROUGH_KEY: &str = "show-strikethrough"; |
| 209 | const HYPERLINK_UNDERLINE_STYLE_KEY: &str = "hyperlink-underline-style"; |
| 210 | const HYPERLINK_ID: &str = "hyperlink-id"; |
| 211 | |
| 212 | fn text_style_as_cf_type_pairs(style: &TextStyle) -> Vec<(CFString, CFType)> { |
| 213 | let mut key_value_pairs = vec![]; |
| 214 | if let Some(foreground_color) = style.foreground_color.as_ref() { |
| 215 | key_value_pairs.push(( |
| 216 | CFString::new(FOREGROUND_COLOR_KEY), |
| 217 | color_u_to_cg_color(*foreground_color).as_CFType(), |
| 218 | )) |
| 219 | } |
| 220 | |
| 221 | if let Some(syntax_color) = style.syntax_color.as_ref() { |
| 222 | key_value_pairs.push(( |
| 223 | CFString::new(SYNTAX_COLOR_KEY), |
| 224 | color_u_to_cg_color(*syntax_color).as_CFType(), |
| 225 | )) |
| 226 | } |
| 227 | |
| 228 | if let Some(background_color) = style.background_color.as_ref() { |
| 229 | key_value_pairs.push(( |
| 230 | CFString::new(BACKGROUND_COLOR_KEY), |
| 231 | color_u_to_cg_color(*background_color).as_CFType(), |
| 232 | )) |
| 233 | } |
| 234 | |
| 235 | if let Some(error_underline_color) = style.error_underline_color.as_ref() { |
| 236 | key_value_pairs.push(( |
| 237 | CFString::new(ERROR_UNDERLINE_COLOR_KEY), |
| 238 | color_u_to_cg_color(*error_underline_color).as_CFType(), |
| 239 | )) |
| 240 | } |
| 241 | |
| 242 | if let Some(border) = style.border.as_ref() { |
| 243 | key_value_pairs.push(( |
| 244 | CFString::new(BORDER_COLOR_KEY), |
| 245 | color_u_to_cg_color(border.color).as_CFType(), |
| 246 | )); |
| 247 | |
| 248 | key_value_pairs.push(( |
| 249 | CFString::new(BORDER_RADIUS_KEY), |
| 250 | CFNumber::from(border.radius as i32).as_CFType(), |
| 251 | )); |
| 252 | |
| 253 | key_value_pairs.push(( |
| 254 | CFString::new(BORDER_WIDTH_KEY), |
| 255 | CFNumber::from(border.width as i32).as_CFType(), |
| 256 | )); |
| 257 | |
| 258 | if let Some(line_height_override) = border.line_height_ratio_override { |
| 259 | key_value_pairs.push(( |
| 260 | CFString::new(BORDER_LINE_HEIGHT_RATIO_KEY), |
| 261 | CFNumber::from(line_height_override as i32).as_CFType(), |
| 262 | )); |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | if let Some(underline_color) = style.underline_color.as_ref() { |
| 267 | key_value_pairs.push(( |
| 268 | CFString::new(HYPERLINK_UNDERLINE_STYLE_KEY), |
| 269 | color_u_to_cg_color(*underline_color).as_CFType(), |
| 270 | )) |
| 271 | } |
| 272 | |
| 273 | if let Some(hyperlink_id) = style.hyperlink_id { |
| 274 | key_value_pairs.push(( |
| 275 | CFString::new(HYPERLINK_ID), |
| 276 | CFNumber::from(hyperlink_id).as_CFType(), |
| 277 | )) |
| 278 | } |
| 279 | |
| 280 | key_value_pairs.push(( |
| 281 | CFString::new(SHOW_STRIKETHROUGH_KEY), |
| 282 | CFBoolean::from(style.show_strikethrough).as_CFType(), |
| 283 | )); |
| 284 | |
| 285 | key_value_pairs |
| 286 | } |
| 287 | |
| 288 | fn attributes_to_text_style(attributes_dictionary: CFDictionary<CFString, CFType>) -> TextStyle { |
| 289 | let mut text_styles = TextStyle::new(); |
| 290 | |
| 291 | if let Some(cg_color) = attributes_dictionary |
| 292 | .find(CFString::new(FOREGROUND_COLOR_KEY)) |
| 293 | .and_then(|value| value.downcast::<CGColor>()) |
| 294 | { |
| 295 | text_styles = text_styles.with_foreground_color(cg_color_to_color_u(cg_color)); |
| 296 | } |
| 297 | |
| 298 | if let Some(cg_color) = attributes_dictionary |
| 299 | .find(CFString::new(SYNTAX_COLOR_KEY)) |
| 300 | .and_then(|value| value.downcast::<CGColor>()) |
| 301 | { |
| 302 | text_styles = text_styles.with_syntax_color(cg_color_to_color_u(cg_color)); |
| 303 | } |
| 304 | |
| 305 | if let Some(cg_color) = attributes_dictionary |
| 306 | .find(CFString::new(BACKGROUND_COLOR_KEY)) |
| 307 | .and_then(|value| value.downcast::<CGColor>()) |
| 308 | { |
| 309 | text_styles = text_styles.with_background_color(cg_color_to_color_u(cg_color)); |
| 310 | } |
| 311 | |
| 312 | let border_color = attributes_dictionary |
| 313 | .find(CFString::new(BORDER_COLOR_KEY)) |
| 314 | .and_then(|value| value.downcast::<CGColor>()); |
| 315 | let border_width = attributes_dictionary |
| 316 | .find(CFString::new(BORDER_WIDTH_KEY)) |
| 317 | .and_then(|value| value.downcast::<CFNumber>()) |
| 318 | .and_then(|num| num.to_i32()); |
| 319 | let border_radius = attributes_dictionary |
| 320 | .find(CFString::new(BORDER_RADIUS_KEY)) |
| 321 | .and_then(|value| value.downcast::<CFNumber>()) |
| 322 | .and_then(|num| num.to_i32()); |
| 323 | let border_line_height_ratio = attributes_dictionary |
| 324 | .find(CFString::new(BORDER_LINE_HEIGHT_RATIO_KEY)) |
| 325 | .and_then(|value| value.downcast::<CFNumber>()) |
| 326 | .and_then(|num| num.to_i32()); |
| 327 | |
| 328 | if let Some(((color, width), radius)) = border_color.zip(border_width).zip(border_radius) { |
| 329 | text_styles = text_styles.with_border(TextBorder { |
| 330 | color: cg_color_to_color_u(color), |
| 331 | radius: radius as u8, |
| 332 | width: width as u8, |
| 333 | line_height_ratio_override: border_line_height_ratio.map(|val| val as u8), |
| 334 | }); |
| 335 | } |
| 336 | |
| 337 | if let Some(cg_color) = attributes_dictionary |
| 338 | .find(CFString::new(ERROR_UNDERLINE_COLOR_KEY)) |
| 339 | .and_then(|value| value.downcast::<CGColor>()) |
| 340 | { |
| 341 | text_styles = text_styles.with_error_underline_color(cg_color_to_color_u(cg_color)); |
| 342 | } |
| 343 | |
| 344 | if let Some(show_strikethrough) = attributes_dictionary |
| 345 | .find(CFString::new(SHOW_STRIKETHROUGH_KEY)) |
| 346 | .and_then(|value| value.downcast::<CFBoolean>()) |
| 347 | { |
| 348 | text_styles = text_styles.with_show_strikethrough(bool::from(show_strikethrough)); |
| 349 | } |
| 350 | |
| 351 | if let Some(cg_color) = attributes_dictionary |
| 352 | .find(CFString::new(HYPERLINK_UNDERLINE_STYLE_KEY)) |
| 353 | .and_then(|value| value.downcast::<CGColor>()) |
| 354 | { |
| 355 | text_styles = text_styles.with_underline_color(cg_color_to_color_u(cg_color)); |
| 356 | } |
| 357 | |
| 358 | if let Some(hyperlink_id) = attributes_dictionary |
| 359 | .find(CFString::new(HYPERLINK_ID)) |
| 360 | .and_then(|value| value.downcast::<CFNumber>()) |
| 361 | .and_then(|hyperlink| hyperlink.to_i32()) |
| 362 | { |
| 363 | text_styles = text_styles.with_hyperlink_id(hyperlink_id); |
| 364 | } |
| 365 | |
| 366 | text_styles |
| 367 | } |
| 368 | |
| 369 | /// Lays out a *single* line using Core Text. |
| 370 | pub fn layout_line( |
| 371 | text: &str, |
| 372 | line_style: LineStyle, |
| 373 | style_runs: &[(Range<usize>, StyleAndFont)], |
| 374 | font_db: &FontDB, |
| 375 | clip_config: ClipConfig, |
| 376 | ) -> Line { |
| 377 | layout_line_with_offset(text, line_style, style_runs, font_db, 0, clip_config) |
| 378 | } |
| 379 | |
| 380 | /// Lays out a line assuming the starting character index of the `text` starts at `char_offset`. |
| 381 | fn layout_line_with_offset( |
| 382 | text: &str, |
| 383 | line_style: LineStyle, |
| 384 | style_runs: &[(Range<usize>, StyleAndFont)], |
| 385 | font_db: &FontDB, |
| 386 | char_offset: usize, |
| 387 | clip_config: ClipConfig, |
| 388 | ) -> Line { |
| 389 | if text.is_empty() { |
| 390 | Line::empty( |
| 391 | line_style.font_size, |
| 392 | line_style.line_height_ratio, |
| 393 | char_offset, |
| 394 | ) |
| 395 | } else { |
| 396 | let attributed_string = |
| 397 | create_attributed_string(text, style_runs, font_db, line_style, None); |
| 398 | let line = CTLine::new_with_attributed_string(attributed_string.as_concrete_TypeRef()); |
| 399 | let utf16_offset_to_char_idx = build_utf16_lookup(text); |
| 400 | line_from_ct_line( |
| 401 | line, |
| 402 | line_style, |
| 403 | font_db, |
| 404 | char_offset, |
| 405 | Some(clip_config), |
| 406 | &utf16_offset_to_char_idx, |
| 407 | ) |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | fn push_paragraph_style_settings( |
| 412 | paragraph_style_settings: &mut Vec<ParagraphStyleSetting>, |
| 413 | first_line_head_indent: Option<f32>, |
| 414 | tab_interval: Option<CGFloat>, |
| 415 | ) { |
| 416 | if let Some(first_line_head_indent_value) = first_line_head_indent { |
| 417 | paragraph_style_settings.push(ParagraphStyleSetting::new_float_setting( |
| 418 | CTParagraphStyleSpecifier::FirstLineHeadIndent, |
| 419 | first_line_head_indent_value as CGFloat, |
| 420 | )); |
| 421 | } |
| 422 | |
| 423 | if let Some(interval) = tab_interval { |
| 424 | // Core Text has a built-in list of tab stops. To ensure DefaultTabInterval applies from |
| 425 | // the first tab, we must also set an explicit (empty) TabStops array. |
| 426 | paragraph_style_settings.push(ParagraphStyleSetting::new_empty_tab_stops()); |
| 427 | paragraph_style_settings.push(ParagraphStyleSetting::new_float_setting( |
| 428 | CTParagraphStyleSpecifier::DefaultTabInterval, |
| 429 | interval, |
| 430 | )); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | /// Applies paragraph style settings to an attributed string if the settings vec is non-empty. |
| 435 | fn apply_paragraph_style_settings( |
| 436 | attributed_string: &mut CFMutableAttributedString, |
| 437 | cf_range: CFRange, |
| 438 | paragraph_style_settings: &[ParagraphStyleSetting], |
| 439 | ) { |
| 440 | if !paragraph_style_settings.is_empty() { |
| 441 | let ct_style_settings = paragraph_style_settings |
| 442 | .iter() |
| 443 | .map(|setting| setting.to_ct_setting()) |
| 444 | .collect(); |
| 445 | let paragraph_style = ParagraphStyle::new(ct_style_settings); |
| 446 | unsafe { |
| 447 | attributed_string.set_attribute( |
| 448 | cf_range, |
| 449 | kCTParagraphStyleAttributeName, |
| 450 | ¶graph_style.style, |
| 451 | ); |
| 452 | } |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | /// Creates a `CFAttributedString` out of `text` with the correct font ranges based on `runs`. |
| 457 | fn create_attributed_string( |
| 458 | text: &str, |
| 459 | style_runs: &[(Range<usize>, StyleAndFont)], |
| 460 | font_db: &FontDB, |
| 461 | line_style: LineStyle, |
| 462 | first_line_head_indent: Option<f32>, |
| 463 | ) -> CFMutableAttributedString { |
| 464 | let mut attributed_string = CFMutableAttributedString::new(); |
| 465 | attributed_string.replace_str(&CFString::new(text), CFRange::init(0, 0)); |
| 466 | |
| 467 | // Wrap the edits to the `CFMutableAttributedString` with `BeginEditing` and `EndEditing` calls |
| 468 | // so that `CFMutableAttributedString` doesn't have to maintain consistency in between edits. |
| 469 | // See https://developer.apple.com/documentation/corefoundation/cfmutableattributedstring?language=objc. |
| 470 | unsafe { |
| 471 | CFAttributedStringBeginEditing(attributed_string.as_concrete_TypeRef()); |
| 472 | } |
| 473 | |
| 474 | let mut utf16_lens = text.chars().map(|c| c.len_utf16()); |
| 475 | let mut prev_char_ix: usize = 0; |
| 476 | let mut prev_utf16_ix: usize = 0; |
| 477 | |
| 478 | let tab_interval = line_style.fixed_width_tab_size.and_then(|tab_size| { |
| 479 | // In fully fixed-width paragraphs, style runs should all resolve to the same monospace font. |
| 480 | // Use the first run to pick a font for computing the tab interval. |
| 481 | let (_, style_and_font) = style_runs.iter().next()?; |
| 482 | let font_id = font_db.select_font(style_and_font.font_family, style_and_font.properties); |
| 483 | font_db |
| 484 | .space_advance_width(font_id, line_style.font_size) |
| 485 | .map(|w| (w * tab_size as f64) as CGFloat) |
| 486 | }); |
| 487 | |
| 488 | // Apply paragraph-level settings (like tab stops) over the full string so they |
| 489 | // still apply even if style runs don't cover whitespace. |
| 490 | { |
| 491 | let mut paragraph_style_settings = vec![]; |
| 492 | push_paragraph_style_settings( |
| 493 | &mut paragraph_style_settings, |
| 494 | first_line_head_indent, |
| 495 | tab_interval, |
| 496 | ); |
| 497 | let full_range = CFRange::init(0, attributed_string.char_len()); |
| 498 | apply_paragraph_style_settings( |
| 499 | &mut attributed_string, |
| 500 | full_range, |
| 501 | ¶graph_style_settings, |
| 502 | ); |
| 503 | } |
| 504 | |
| 505 | for (range, style_and_font) in style_runs { |
| 506 | let utf16_start: usize = prev_utf16_ix |
| 507 | + utf16_lens |
| 508 | .by_ref() |
| 509 | .take(range.start - prev_char_ix) |
| 510 | .sum::<usize>(); |
| 511 | let utf16_end: usize = utf16_start |
| 512 | + utf16_lens |
| 513 | .by_ref() |
| 514 | .take(range.end - range.start) |
| 515 | .sum::<usize>(); |
| 516 | prev_char_ix = range.end; |
| 517 | prev_utf16_ix = utf16_end; |
| 518 | |
| 519 | let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); |
| 520 | let font_id = font_db.select_font(style_and_font.font_family, style_and_font.properties); |
| 521 | let native_font = font_db.native_font(font_id, line_style.font_size); |
| 522 | |
| 523 | let attributes_pairs = text_style_as_cf_type_pairs(&style_and_font.style); |
| 524 | unsafe { |
| 525 | attributed_string.set_attribute(cf_range, kCTFontAttributeName, &native_font); |
| 526 | |
| 527 | // The way the system computes line height can be slightly different from the way we compute line height. |
| 528 | // See https://www.zsiegel.com/2012/10/23/Core-Text-Calculating-line-heights for how the system computes line height. |
| 529 | // To make the system use the same line height that we do in the app, we use a multiplier. |
| 530 | // This is necessary to prevent the case where the system doesn't render any text because it thinks there isn't enough vertical space, |
| 531 | // but there is actually enough space based on the app line height. |
| 532 | let system_font_line_height = |
| 533 | native_font.ascent() + native_font.descent() + native_font.leading(); |
| 534 | let app_font_line_height = |
| 535 | line_style.font_size as CGFloat * line_style.line_height_ratio as CGFloat; |
| 536 | let line_height_multiple = app_font_line_height / system_font_line_height; |
| 537 | let line_height_multiple_setting = ParagraphStyleSetting::new_float_setting( |
| 538 | CTParagraphStyleSpecifier::LineHeightMultiple, |
| 539 | line_height_multiple, |
| 540 | ); |
| 541 | let mut paragraph_style_settings = vec![line_height_multiple_setting]; |
| 542 | |
| 543 | // Note: we apply tab stops here *and* over the full string. We need both because we |
| 544 | // set a per-run paragraph style (to normalize line height), and that overrides the |
| 545 | // full-range paragraph style for this character range. |
| 546 | push_paragraph_style_settings( |
| 547 | &mut paragraph_style_settings, |
| 548 | first_line_head_indent, |
| 549 | tab_interval, |
| 550 | ); |
| 551 | |
| 552 | apply_paragraph_style_settings( |
| 553 | &mut attributed_string, |
| 554 | cf_range, |
| 555 | ¶graph_style_settings, |
| 556 | ); |
| 557 | |
| 558 | // When we apply inline code block, we need to leave additional spacing before and after the text to make sure |
| 559 | // we can render the block without overlapping with text outside of the code block. We add these spacing by setting |
| 560 | // the kerning attribute on the glyph right before the code block range and the last glyph in the block. |
| 561 | // |
| 562 | // For example if we want to highlight `bc` in abcd |
| 563 | // We would apply kerning on `a` and `c`. The text spacing would look like [a ][b][c ][d]. |
| 564 | if style_and_font.style.border.is_some() { |
| 565 | if utf16_start > 0 { |
| 566 | let kerning_range = CFRange::init((utf16_start - 1) as isize, 1_isize); |
| 567 | attributed_string.set_attribute( |
| 568 | kerning_range, |
| 569 | kCTKernAttributeName, |
| 570 | &CFNumber::from(6.), |
| 571 | ); |
| 572 | } |
| 573 | |
| 574 | if utf16_end > 0 { |
| 575 | let kerning_range = CFRange::init((utf16_end - 1) as isize, 1_isize); |
| 576 | attributed_string.set_attribute( |
| 577 | kerning_range, |
| 578 | kCTKernAttributeName, |
| 579 | &CFNumber::from(6.), |
| 580 | ); |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | for (key, value) in attributes_pairs { |
| 585 | attributed_string.set_attribute(cf_range, key.as_concrete_TypeRef(), &value); |
| 586 | } |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | unsafe { |
| 591 | CFAttributedStringEndEditing(attributed_string.as_concrete_TypeRef()); |
| 592 | } |
| 593 | attributed_string |
| 594 | } |
| 595 | |
| 596 | /// Lays out a string of text into a frame (a series of lines) using Core Text. |
| 597 | #[allow(clippy::too_many_arguments)] |
| 598 | pub fn layout_text( |
| 599 | text: &str, |
| 600 | line_style: LineStyle, |
| 601 | style_runs: &[(Range<usize>, StyleAndFont)], |
| 602 | font_db: &FontDB, |
| 603 | max_width: f32, |
| 604 | max_height: f32, |
| 605 | alignment: TextAlignment, |
| 606 | mut first_line_head_indent: Option<f32>, |
| 607 | ) -> TextFrame { |
| 608 | if text.is_empty() { |
| 609 | TextFrame::empty(line_style.font_size, line_style.line_height_ratio) |
| 610 | } else { |
| 611 | // Ensure the max height is finite--under certain conditions `CTFrameSetter` won't terminate |
| 612 | // if the height is unbounded. |
| 613 | let max_height = max_height.min(f32::MAX); |
| 614 | |
| 615 | let mut insert_extra_initial_line = false; |
| 616 | // Core Text always tries to put at least 1 character on the first line, which does not work well |
| 617 | // in the case of a large head indent which is >= the width of the first line. In this case, we |
| 618 | // handle the "empty" first line manually (and ask Core Text to lay out the rest of the lines). |
| 619 | if let Some(indent) = first_line_head_indent { |
| 620 | // We approximate the width of a single character to be half of the font size. This is to encourage |
| 621 | // wrapping the character as soon as it starts to get significantly clipped (due to the max width). |
| 622 | if indent >= max_width - (line_style.font_size / 2.) { |
| 623 | first_line_head_indent = None; |
| 624 | insert_extra_initial_line = true; |
| 625 | } |
| 626 | } |
| 627 | let attributed_string = create_attributed_string( |
| 628 | text, |
| 629 | style_runs, |
| 630 | font_db, |
| 631 | line_style, |
| 632 | first_line_head_indent, |
| 633 | ); |
| 634 | let framesetter = |
| 635 | CTFramesetter::new_with_attributed_string(attributed_string.as_concrete_TypeRef()); |
| 636 | |
| 637 | // Create frame from framesetter. |
| 638 | let cf_range = CFRange::init(0, attributed_string.char_len()); |
| 639 | let frame = framesetter.create_frame( |
| 640 | cf_range, |
| 641 | CGPath::from_rect( |
| 642 | CGRect::new( |
| 643 | &CGPoint::new(0., 0.), |
| 644 | // Even though we use a multiplier for line height to account for differences between |
| 645 | // the system computed line height and our app computed line height, there are cases where |
| 646 | // the system still thinks more height is required. We add a small buffer to account for this |
| 647 | // because we should be able to render text when max_height is exactly the app computed line height. |
| 648 | &CGSize::new(max_width as f64, max_height as f64 + 2.), |
| 649 | ), |
| 650 | None, |
| 651 | ) |
| 652 | .as_ref(), |
| 653 | ); |
| 654 | let mut max_line_width: f32 = 0.; |
| 655 | |
| 656 | let mut frame_lines = vec![]; |
| 657 | if insert_extra_initial_line { |
| 658 | // If the head indent was >= the width of the first line, we manually add back in the first empty line! |
| 659 | frame_lines.push(Line::empty( |
| 660 | line_style.font_size, |
| 661 | line_style.line_height_ratio, |
| 662 | 0, |
| 663 | )); |
| 664 | } |
| 665 | |
| 666 | let lines = frame.get_lines(); |
| 667 | let num_lines = lines.len(); |
| 668 | |
| 669 | let utf16_offset_to_char_idx = build_utf16_lookup(text); |
| 670 | |
| 671 | frame_lines.append( |
| 672 | &mut lines |
| 673 | .into_iter() |
| 674 | .enumerate() |
| 675 | .map(|(index, line)| { |
| 676 | let (line_start_utf_16_index, line_length) = { |
| 677 | let range = line.get_string_range(); |
| 678 | // The `CFRange` returned by CoreText returns a location that is the number of |
| 679 | // utf-16 bytes from the start of the string. |
| 680 | (range.location as usize, range.length as usize) |
| 681 | }; |
| 682 | |
| 683 | // If the last line would be clipped, render the rest of the text into it's own line so |
| 684 | // that we can overflow the text by fading the text. |
| 685 | let is_last_line = index == num_lines - 1; |
| 686 | let line = if is_last_line |
| 687 | && line_start_utf_16_index + line_length |
| 688 | < attributed_string.char_len() as usize |
| 689 | { |
| 690 | // The string range returned by CoreText is in terms of the number of UTF-16 bytes, |
| 691 | // so iterate through the string to find the corresponding char index. |
| 692 | let star_char_index = |
| 693 | char_index_from_utf_16_byte_index(text, line_start_utf_16_index); |
| 694 | |
| 695 | // CoreText does not support multiline overflow when using `CTFrame`. To support |
| 696 | // multiline text that is clipped, we first use CoreText to render the text into a |
| 697 | // frame and then relayout the last line with the rest of string on that line. |
| 698 | // See https://groups.google.com/g/cocoa-unbound/c/Qin6gjYj7XU?pli=1 for more |
| 699 | // details. |
| 700 | let style_runs = style_runs |
| 701 | .iter() |
| 702 | .filter_map(|(range, font)| { |
| 703 | if range.end >= star_char_index { |
| 704 | Some(( |
| 705 | Range { |
| 706 | start: range.start.saturating_sub(star_char_index), |
| 707 | end: range.end.saturating_sub(star_char_index), |
| 708 | }, |
| 709 | *font, |
| 710 | )) |
| 711 | } else { |
| 712 | None |
| 713 | } |
| 714 | }) |
| 715 | .collect::<Vec<_>>(); |
| 716 | |
| 717 | let chars: Vec<_> = text.chars().collect(); |
| 718 | layout_line_with_offset( |
| 719 | &chars[star_char_index..].iter().collect::<String>(), |
| 720 | line_style, |
| 721 | &style_runs, |
| 722 | font_db, |
| 723 | star_char_index, |
| 724 | ClipConfig::default(), |
| 725 | ) |
| 726 | } else { |
| 727 | line_from_ct_line( |
| 728 | line, |
| 729 | line_style, |
| 730 | font_db, |
| 731 | 0, |
| 732 | // Only apply clipping to the last line in the frame. |
| 733 | is_last_line.then_some(ClipConfig::default()), |
| 734 | &utf16_offset_to_char_idx, |
| 735 | ) |
| 736 | }; |
| 737 | |
| 738 | max_line_width = max_line_width.max(line.width); |
| 739 | line |
| 740 | }) |
| 741 | .collect_vec(), |
| 742 | ); |
| 743 | |
| 744 | match Vec1::try_from_vec(frame_lines) { |
| 745 | Ok(frame_lines) => TextFrame::new(frame_lines, max_line_width, alignment), |
| 746 | Err(_) => TextFrame::empty(line_style.font_size, line_style.line_height_ratio), |
| 747 | } |
| 748 | } |
| 749 | } |
| 750 | |
| 751 | fn line_from_ct_line( |
| 752 | line: CTLine, |
| 753 | line_style: LineStyle, |
| 754 | font_db: &FontDB, |
| 755 | char_offset: usize, |
| 756 | clip_config: Option<ClipConfig>, |
| 757 | utf16_offset_to_char_idx: &[usize], |
| 758 | ) -> Line { |
| 759 | let mut runs = Vec::with_capacity(line.glyph_runs().len() as usize); |
| 760 | let typographic_bounds = line.get_typographic_bounds(); |
| 761 | let width = typographic_bounds.width as f32; |
| 762 | |
| 763 | let mut previous_run_font_and_attribute = None; |
| 764 | |
| 765 | for run in line.glyph_runs().into_iter() { |
| 766 | let attributes = run.attributes().expect("attributes should exist"); |
| 767 | |
| 768 | let font_id = font_db.font_id_for_native_font(unsafe { |
| 769 | attributes |
| 770 | .get(kCTFontAttributeName) |
| 771 | .downcast::<CTFont>() |
| 772 | .unwrap() |
| 773 | }); |
| 774 | |
| 775 | let run_advances = advances(&run); |
| 776 | let glyphs = itertools::multizip(( |
| 777 | run.glyphs().iter(), |
| 778 | run.positions().iter(), |
| 779 | run.string_indices().iter(), |
| 780 | run_advances.iter(), |
| 781 | )) |
| 782 | .map(|(glyph_id, position, utf16_offset, advance)| { |
| 783 | let utf16_offset = usize::try_from(*utf16_offset).expect("Negative character offset"); |
| 784 | let char_index = utf16_offset_to_char_idx |
| 785 | .get(utf16_offset) |
| 786 | .expect("mapping covers whole string"); |
| 787 | |
| 788 | Glyph { |
| 789 | id: *glyph_id as GlyphId, |
| 790 | position_along_baseline: vec2f(position.x as f32, position.y as f32), |
| 791 | index: char_offset + char_index, |
| 792 | width: advance.width as f32, |
| 793 | } |
| 794 | }) |
| 795 | .collect_vec(); |
| 796 | |
| 797 | // Only separate out text runs with different attribute that we will use in the paint stage. |
| 798 | // For text attributes that don't matter in the paint stage (e.g. kerning), treat them as one |
| 799 | // text run. |
| 800 | let text_style = attributes_to_text_style(attributes); |
| 801 | let width = run_advances |
| 802 | .iter() |
| 803 | .map(|advance| advance.width as f32) |
| 804 | .sum(); |
| 805 | match previous_run_font_and_attribute { |
| 806 | Some((prev_font_id, prev_attribute)) |
| 807 | if prev_font_id == font_id && text_style == prev_attribute && !runs.is_empty() => |
| 808 | { |
| 809 | let last_run: &mut Run = |
| 810 | runs.last_mut().expect("Already checked runs are not empty"); |
| 811 | last_run.glyphs.extend(glyphs); |
| 812 | last_run.width += width; |
| 813 | } |
| 814 | _ => { |
| 815 | runs.push(Run { |
| 816 | font_id, |
| 817 | glyphs, |
| 818 | styles: text_style, |
| 819 | width, |
| 820 | }); |
| 821 | |
| 822 | previous_run_font_and_attribute = Some((font_id, text_style)); |
| 823 | } |
| 824 | } |
| 825 | } |
| 826 | |
| 827 | let caret_positions = caret_positions_for_line(&line, char_offset, utf16_offset_to_char_idx); |
| 828 | |
| 829 | Line { |
| 830 | width, |
| 831 | trailing_whitespace_width: trailing_whitespace_width_for_line(&line).max(0.) as f32, |
| 832 | runs, |
| 833 | font_size: line_style.font_size, |
| 834 | clip_config, |
| 835 | line_height_ratio: line_style.line_height_ratio, |
| 836 | baseline_ratio: line_style.baseline_ratio, |
| 837 | ascent: typographic_bounds.ascent as f32, |
| 838 | descent: typographic_bounds.descent as f32, |
| 839 | caret_positions, |
| 840 | // TODO(CORE-2004): If we want to support external font fallback on |
| 841 | // Mac, we need to populate this with the missing chars. |
| 842 | chars_with_missing_glyphs: vec![], |
| 843 | } |
| 844 | } |
| 845 | |
| 846 | /// Returns the char index within `text` given the number of UTF-16 bytes from the start of the |
| 847 | /// string. |
| 848 | fn char_index_from_utf_16_byte_index(text: &str, utf_16_index: usize) -> usize { |
| 849 | let mut start_index_utf_16 = 0; |
| 850 | let mut start_index = 0; |
| 851 | for (index, char) in text.chars().enumerate() { |
| 852 | if utf_16_index <= start_index_utf_16 { |
| 853 | start_index = index; |
| 854 | break; |
| 855 | } |
| 856 | |
| 857 | start_index_utf_16 += char.len_utf16(); |
| 858 | } |
| 859 | start_index |
| 860 | } |
| 861 | |
| 862 | /// Builds a lookup table for finding a character's index within the input |
| 863 | /// string given its position in UTF-16 code units. |
| 864 | /// |
| 865 | /// This is necessary because the glyph order is not guaranteed to match the |
| 866 | /// character order (e.g.: for RTL text), and because a single character may be |
| 867 | /// represented by multiple UTF-16 code units. |
| 868 | /// |
| 869 | /// In the output `Vec`, indices correspond to UTF-16 code unit offsets and values |
| 870 | /// correspond to character indices. |
| 871 | fn build_utf16_lookup(text: &str) -> Vec<usize> { |
| 872 | // Use the UTF-8 length as a starting estimate, since each ASCII character |
| 873 | // is represented by both 1 UTF-8 code point and 1 UTF-16 code point. |
| 874 | let mut table = Vec::with_capacity(text.len()); |
| 875 | |
| 876 | for (char_index, ch) in text.chars().enumerate() { |
| 877 | // For each UTF-16 code unit in the character, add a mapping to the |
| 878 | // character's index. The UTF-16 position is implicitly tracked by the |
| 879 | // length of the lookup table. |
| 880 | for _ in 0..ch.len_utf16() { |
| 881 | table.push(char_index); |
| 882 | } |
| 883 | } |
| 884 | |
| 885 | table |
| 886 | } |
| 887 | |
| 888 | /// Extract caret positions from a Core Text line. |
| 889 | fn caret_positions_for_line( |
| 890 | line: &CTLine, |
| 891 | char_offset: usize, |
| 892 | utf16_offset_to_char_idx: &[usize], |
| 893 | ) -> Vec<CaretPosition> { |
| 894 | #[derive(Debug)] |
| 895 | struct CaretEdge { |
| 896 | /// Index of the UTF-16 code point corresponding to this caret edge. |
| 897 | utf16_index: usize, |
| 898 | /// Whether this is a leading edge or a trailing edge. |
| 899 | /// For LTR text, the leading edge is the leftmost edge of the cluster, |
| 900 | /// and for RTL text, it is the rightmost edge. |
| 901 | leading_edge: bool, |
| 902 | /// The pixel offset of this edge from the start of the line. |
| 903 | pixel_offset: f64, |
| 904 | } |
| 905 | |
| 906 | let positions = Rc::new(RefCell::new(vec![])); |
| 907 | |
| 908 | // Core Text produces leading and trailing edges for each caret position in |
| 909 | // the line. For our purposes, we only need the leading edge for rendering |
| 910 | // the caret. However, we use both edges to find the character extent of |
| 911 | // each caret. |
| 912 | let block = { |
| 913 | let positions = positions.clone(); |
| 914 | ConcreteBlock::new(move |offset, char_index: isize, leading_edge, _stop| { |
| 915 | positions.borrow_mut().push(CaretEdge { |
| 916 | utf16_index: char_index.try_into().expect("Negative UTF-16 offset"), |
| 917 | leading_edge, |
| 918 | pixel_offset: offset, |
| 919 | }); |
| 920 | }) |
| 921 | }; |
| 922 | |
| 923 | // We have to use RcBlock to avoid a double-free, but that takes ownership |
| 924 | // of utf16_offset_to_char_idx |
| 925 | let block = block.copy(); |
| 926 | unsafe { |
| 927 | CTLineEnumerateCaretOffsets(line.as_concrete_TypeRef(), &block); |
| 928 | } |
| 929 | drop(block); |
| 930 | |
| 931 | let mut positions = Rc::try_unwrap(positions) |
| 932 | .expect("Block reference should be dropped") |
| 933 | .into_inner(); |
| 934 | |
| 935 | debug_assert!( |
| 936 | positions.len() % 2 == 0, |
| 937 | "Missing a leading or trailing caret edge" |
| 938 | ); |
| 939 | // Core Text sometimes swaps the order of the trailing edge of one caret and |
| 940 | // the leading edge of the next, causing edge pairs to be interspersed. So |
| 941 | // that we can pair the leading and trailing edges into carets, sort by |
| 942 | // character index, assuming that carets don't overlap with each other. |
| 943 | // positions should already be almost entirely sorted, except for swapped |
| 944 | // edges and RTL text. |
| 945 | positions.sort_unstable_by_key(|position| position.utf16_index); |
| 946 | let mut carets: Vec<_> = positions |
| 947 | .chunks_exact(2) |
| 948 | .map(|edges| { |
| 949 | // Guaranteed by chunks_exact that there are 2 elements. |
| 950 | let first = &edges[0]; |
| 951 | let second = &edges[1]; |
| 952 | |
| 953 | // Core Text enumerates edges in left-to-right visual order, but |
| 954 | // sets the leading edge based on logical order, so use that to |
| 955 | // handle RTL text. |
| 956 | // For any given leading/trailing edge pair, the pixel offset from |
| 957 | // the start of the line is the one for the leading edge. |
| 958 | let pixel_offset = if first.leading_edge { |
| 959 | first.pixel_offset |
| 960 | } else { |
| 961 | debug_assert!( |
| 962 | second.leading_edge, |
| 963 | "No leading edge in {first:?} or {second:?}" |
| 964 | ); |
| 965 | second.pixel_offset |
| 966 | }; |
| 967 | |
| 968 | let first_index = utf16_offset_to_char_idx |
| 969 | .get(first.utf16_index) |
| 970 | .expect("Mapping covers whole string") |
| 971 | + char_offset; |
| 972 | let second_index = utf16_offset_to_char_idx |
| 973 | .get(second.utf16_index) |
| 974 | .expect("Mapping covers whole string") |
| 975 | + char_offset; |
| 976 | |
| 977 | let (start, end) = if first_index < second_index { |
| 978 | (first_index, second_index) |
| 979 | } else { |
| 980 | (second_index, first_index) |
| 981 | }; |
| 982 | |
| 983 | CaretPosition { |
| 984 | position_in_line: pixel_offset as f32, |
| 985 | start_offset: start, |
| 986 | last_offset: end, |
| 987 | } |
| 988 | }) |
| 989 | .collect(); |
| 990 | // Callers assume that carets are sorted by left-to-right visual display order, |
| 991 | // so sort back here. |
| 992 | carets.sort_unstable_by_key(|caret| OrderedFloat(caret.position_in_line)); |
| 993 | carets |
| 994 | } |
| 995 | |
| 996 | fn trailing_whitespace_width_for_line(line: &CTLine) -> f64 { |
| 997 | unsafe { CTLineGetTrailingWhitespaceWidth(line.as_concrete_TypeRef()) } |
| 998 | } |
| 999 | |
| 1000 | /// Returns the advances for each glyph in the run. |
| 1001 | /// |
| 1002 | /// If the run has a non-null advances pointer, returns a slice of the advances. |
| 1003 | /// Otherwise, returns a vector of advances computed by calling |
| 1004 | /// [CTRunGetAdvances](https://developer.apple.com/documentation/coretext/1508691-ctrungetadvances?language=objc). |
| 1005 | /// |
| 1006 | /// This follows the same pattern as the glyph IDs and positions functions provided by |
| 1007 | /// the `core-text` crate. |
| 1008 | fn advances(run: &CTRun) -> Cow<'_, [CGSize]> { |
| 1009 | unsafe { |
| 1010 | let run_ref = run.as_concrete_TypeRef(); |
| 1011 | // CTRunGetAdvancesPtr can return null under some not understood circumstances. |
| 1012 | // If it does the Apple documentation tells us to allocate our own buffer and call |
| 1013 | // CTRunGetAdvances |
| 1014 | let count = CTRunGetGlyphCount(run_ref); |
| 1015 | let advances_ptr = CTRunGetAdvancesPtr(run_ref); |
| 1016 | if !advances_ptr.is_null() { |
| 1017 | Cow::from(slice::from_raw_parts(advances_ptr, count as usize)) |
| 1018 | } else { |
| 1019 | let mut vec = Vec::with_capacity(count as usize); |
| 1020 | // "If the length of the range is set to 0, then the copy operation will continue |
| 1021 | // from the start index of the range to the end of the run" |
| 1022 | CTRunGetAdvances(run_ref, CFRange::init(0, 0), vec.as_mut_ptr()); |
| 1023 | vec.set_len(count as usize); |
| 1024 | Cow::from(vec) |
| 1025 | } |
| 1026 | } |
| 1027 | } |
| 1028 | |
| 1029 | #[cfg(test)] |
| 1030 | #[path = "text_layout_test.rs"] |
| 1031 | mod tests; |
| 1032 |