use crate::FontHandle;
use crate::error::TextError;
use crate::types::{Direction, FontId, FontMetrics, ShapedGlyph, ShapedLine};
use objc2_core_foundation::{CFIndex, CFRange, CFRetained, CGPoint, CGSize};
use objc2_core_text::{
CTFont, CTParagraphStyle, CTParagraphStyleSetting, CTParagraphStyleSpecifier, CTRun,
CTWritingDirection, kCTFontAttributeName, kCTParagraphStyleAttributeName,
};
use std::ffi::c_void;
use std::ptr::NonNull;
pub(crate) struct CapturedFont {
pub(crate) handle: FontHandle,
pub(crate) font: CFRetained<CTFont>,
}
pub(crate) struct ShapeResult {
pub(crate) line: ShapedLine,
pub(crate) captured_fonts: Vec<CapturedFont>,
}
unsafe extern "C" {
fn CFStringCreateWithCString(
alloc: *const c_void,
c_str: *const i8,
encoding: u32,
) -> *const c_void;
fn CFDictionaryCreate(
allocator: *const c_void,
keys: *const *const c_void,
values: *const *const c_void,
num_values: CFIndex,
key_callbacks: *const c_void,
value_callbacks: *const c_void,
) -> *const c_void;
fn CFAttributedStringCreate(
alloc: *const c_void,
string: *const c_void,
attributes: *const c_void,
) -> *const c_void;
fn CTLineCreateWithAttributedString(attr_string: *const c_void) -> *const c_void;
fn CTLineGetGlyphRuns(line: *const c_void) -> *const c_void;
fn CFArrayGetCount(array: *const c_void) -> CFIndex;
fn CFArrayGetValueAtIndex(array: *const c_void, idx: CFIndex) -> *const c_void;
fn CTRunGetGlyphCount(run: *const c_void) -> CFIndex;
fn CTRunGetGlyphs(run: *const c_void, range: CFRange, buffer: *mut u16);
fn CTRunGetPositions(run: *const c_void, range: CFRange, buffer: *mut CGPoint);
fn CTRunGetAdvances(run: *const c_void, range: CFRange, buffer: *mut CGSize);
fn CTRunGetStringIndicesPtr(run: *const c_void) -> *const CFIndex;
fn CTRunGetStringIndices(run: *const c_void, range: CFRange, buffer: *mut CFIndex);
fn CTRunGetAttributes(run: *const c_void) -> *const c_void;
fn CFDictionaryGetValue(dict: *const c_void, key: *const c_void) -> *const c_void;
fn CFRetain(cf: *const c_void) -> *const c_void;
fn CFRelease(cf: *const c_void);
static kCFTypeDictionaryKeyCallBacks: c_void;
static kCFTypeDictionaryValueCallBacks: c_void;
}
const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
struct ScopedCf(*const c_void);
impl ScopedCf {
fn ptr(&self) -> *const c_void {
self.0
}
fn is_null(&self) -> bool {
self.0.is_null()
}
}
impl Drop for ScopedCf {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { CFRelease(self.0) };
}
}
}
fn forced_paragraph_style(direction: Direction) -> CFRetained<CTParagraphStyle> {
let mut writing_direction = match direction {
Direction::Ltr => CTWritingDirection::LeftToRight,
Direction::Rtl => CTWritingDirection::RightToLeft,
};
let setting = CTParagraphStyleSetting {
spec: CTParagraphStyleSpecifier::BaseWritingDirection,
valueSize: std::mem::size_of::<CTWritingDirection>(),
value: NonNull::from(&mut writing_direction).cast::<c_void>(),
};
unsafe { CTParagraphStyle::new(&setting, 1) }
}
pub(crate) fn shape_line(
ct_font: &CTFont,
text: &str,
metrics: &FontMetrics,
size_lpx: f32,
scale: f32,
forced_direction: Option<Direction>,
) -> Result<ShapeResult, TextError> {
if text.is_empty() {
return Ok(ShapeResult {
line: ShapedLine {
glyphs: vec![],
width_lpx: 0.0,
ascent_lpx: metrics.ascent_lpx,
descent_lpx: metrics.descent_lpx,
y_offset_lpx: 0.0,
base_direction: crate::types::Direction::Ltr,
runs: Vec::new(),
},
captured_fonts: Vec::new(),
});
}
let c_str = std::ffi::CString::new(text)
.map_err(|_| TextError::ShapingFailed("Text contains null bytes".into()))?;
let utf16_to_utf8 = crate::cluster::utf16_to_utf8_byte_map(text);
unsafe {
let cf_string = ScopedCf(CFStringCreateWithCString(
std::ptr::null(),
c_str.as_ptr(),
K_CF_STRING_ENCODING_UTF8,
));
if cf_string.is_null() {
return Err(TextError::ShapingFailed("Failed to create CFString".into()));
}
let font_key = kCTFontAttributeName as *const _ as *const c_void;
let font_value = ct_font as *const CTFont as *const c_void;
let mut keys = vec![font_key];
let mut values = vec![font_value];
let para_style = forced_direction.map(forced_paragraph_style);
if let Some(style) = ¶_style {
let para_key = kCTParagraphStyleAttributeName as *const _ as *const c_void;
let para_value = (&**style as *const CTParagraphStyle) as *const c_void;
keys.push(para_key);
values.push(para_value);
}
let attrs = ScopedCf(CFDictionaryCreate(
std::ptr::null(),
keys.as_ptr(),
values.as_ptr(),
keys.len() as CFIndex,
&kCFTypeDictionaryKeyCallBacks as *const c_void,
&kCFTypeDictionaryValueCallBacks as *const c_void,
));
let attr_string = ScopedCf(CFAttributedStringCreate(
std::ptr::null(),
cf_string.ptr(),
attrs.ptr(),
));
drop(attrs);
drop(cf_string);
if attr_string.is_null() {
return Err(TextError::ShapingFailed(
"Failed to create CFAttributedString".into(),
));
}
let line = ScopedCf(CTLineCreateWithAttributedString(attr_string.ptr()));
drop(attr_string);
if line.is_null() {
return Err(TextError::ShapingFailed("Failed to create CTLine".into()));
}
let runs = CTLineGetGlyphRuns(line.ptr());
let run_count = CFArrayGetCount(runs);
let mut glyphs = Vec::new();
let mut captured_fonts: Vec<CapturedFont> = Vec::new();
let mut total_width_pt: f64 = 0.0;
let primary_handle = super::font_id::font_handle_from_ct_font(ct_font, size_lpx, scale);
for i in 0..run_count {
let run = CFArrayGetValueAtIndex(runs, i);
if run.is_null() {
continue;
}
let _run_ref: &CTRun = &*(run as *const CTRun);
let glyph_count = CTRunGetGlyphCount(run) as usize;
if glyph_count == 0 {
continue;
}
let attrs = CTRunGetAttributes(run);
let font_key = kCTFontAttributeName as *const _ as *const c_void;
let run_font_handle = if !attrs.is_null() {
let run_font_ptr = CFDictionaryGetValue(attrs, font_key);
if run_font_ptr.is_null() {
FontHandle::default()
} else {
let run_font_ref: &CTFont = &*(run_font_ptr as *const CTFont);
let handle =
super::font_id::font_handle_from_ct_font(run_font_ref, size_lpx, scale);
if handle != primary_handle
&& !captured_fonts.iter().any(|cf| cf.handle == handle)
{
let _ = CFRetain(run_font_ptr);
let nonnull = std::ptr::NonNull::new(run_font_ptr as *mut CTFont)
.expect("non-null after null check above");
let retained = CFRetained::from_raw(nonnull);
captured_fonts.push(CapturedFont {
handle,
font: retained,
});
}
handle
}
} else {
FontHandle::default()
};
let mut glyph_ids: Vec<u16> = vec![0; glyph_count];
let mut positions: Vec<CGPoint> = vec![CGPoint { x: 0.0, y: 0.0 }; glyph_count];
let mut advances: Vec<CGSize> = vec![
CGSize {
width: 0.0,
height: 0.0
};
glyph_count
];
let range = CFRange::new(0, glyph_count as CFIndex);
CTRunGetGlyphs(run, range, glyph_ids.as_mut_ptr());
CTRunGetPositions(run, range, positions.as_mut_ptr());
CTRunGetAdvances(run, range, advances.as_mut_ptr());
let indices_ptr = CTRunGetStringIndicesPtr(run);
let mut indices_owned: Vec<CFIndex> = Vec::new();
let string_indices: &[CFIndex] = if !indices_ptr.is_null() {
std::slice::from_raw_parts(indices_ptr, glyph_count)
} else {
indices_owned.resize(glyph_count, 0);
CTRunGetStringIndices(run, range, indices_owned.as_mut_ptr());
&indices_owned[..]
};
for j in 0..glyph_count {
let glyph_id = glyph_ids[j] as u32;
let x_advance_pt = advances[j].width as f32;
let position = [positions[j].x as f32, positions[j].y as f32];
let utf16_pos = string_indices[j] as usize;
let cluster = utf16_to_utf8.get(utf16_pos).copied().unwrap_or(0);
glyphs.push(ShapedGlyph {
glyph_id,
font_id: FontId::PRIMARY,
font_handle: run_font_handle,
x_advance_lpx: x_advance_pt,
position_lpx: position,
cluster,
direction: crate::types::Direction::Ltr,
});
total_width_pt += advances[j].width;
}
}
Ok(ShapeResult {
line: ShapedLine {
glyphs,
width_lpx: total_width_pt as f32,
ascent_lpx: metrics.ascent_lpx,
descent_lpx: metrics.descent_lpx,
y_offset_lpx: 0.0,
base_direction: crate::types::Direction::Ltr,
runs: Vec::new(),
},
captured_fonts,
})
}
}