use std::collections::HashMap;
use std::ffi::c_void;
use std::ptr::{self, NonNull};
use tiny_skia::Pixmap;
use objc2_core_foundation::{
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, CFAttributedString,
CFDictionary, CFRange, CFRetained, CFString, CGAffineTransform, CGPoint, CGRect, CGSize,
};
use objc2_core_graphics::{
CGBitmapContextCreate, CGColor, CGColorSpace, CGContext, CGImageAlphaInfo, CGPath,
};
use objc2_core_text::{
kCTFontAttributeName, kCTForegroundColorAttributeName, kCTParagraphStyleAttributeName, CTFont,
CTFramesetter, CTLine, CTParagraphStyle, CTParagraphStyleSetting, CTParagraphStyleSpecifier,
CTTextAlignment,
};
use super::TextEngine;
use crate::geometry::{Color, Rect, Size};
use crate::spec::Align;
const DEFAULT_FAMILY: &str = "PingFang SC";
const IDENTITY: CGAffineTransform = CGAffineTransform {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
tx: 0.0,
ty: 0.0,
};
pub struct CoreTextEngine {
scale: f32,
fonts: HashMap<(String, u32), CFRetained<CTFont>>,
color_space: CFRetained<CGColorSpace>,
}
impl CoreTextEngine {
pub fn new() -> Self {
let color_space = CGColorSpace::new_device_rgb().expect("CGColorSpaceCreateDeviceRGB 失败");
Self {
scale: 1.0,
fonts: HashMap::new(),
color_space,
}
}
fn font(&mut self, family: Option<&str>, psize: f32) -> CFRetained<CTFont> {
let fam = family.unwrap_or(DEFAULT_FAMILY).to_string();
let key = (fam.clone(), psize.to_bits());
if let Some(f) = self.fonts.get(&key) {
return f.clone();
}
let name = CFString::from_str(&fam);
let font = unsafe { CTFont::with_name(&name, psize as f64, ptr::null()) };
self.fonts.insert(key, font.clone());
font
}
fn attributed(
&mut self,
text: &str,
font: &CTFont,
color: &CGColor,
align: Align,
) -> CFRetained<CFAttributedString> {
let ct_align = match align {
Align::Start | Align::Stretch => CTTextAlignment::Natural,
Align::Center => CTTextAlignment::Center,
Align::End => CTTextAlignment::Right,
};
let setting = CTParagraphStyleSetting {
spec: CTParagraphStyleSpecifier::Alignment,
valueSize: std::mem::size_of::<CTTextAlignment>(),
value: NonNull::from(&ct_align).cast(),
};
let para = unsafe { CTParagraphStyle::new(&setting, 1) };
let mut keys: [*const c_void; 3] = unsafe {
[
(kCTFontAttributeName as *const CFString).cast(),
(kCTForegroundColorAttributeName as *const CFString).cast(),
(kCTParagraphStyleAttributeName as *const CFString).cast(),
]
};
let mut vals: [*const c_void; 3] = [
(font as *const CTFont).cast(),
(color as *const CGColor).cast(),
(&*para as *const CTParagraphStyle).cast(),
];
let dict = unsafe {
CFDictionary::new(
None,
keys.as_mut_ptr(),
vals.as_mut_ptr(),
3,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
)
}
.expect("CFDictionaryCreate 失败");
let cfstr = CFString::from_str(text);
unsafe { CFAttributedString::new(None, Some(&cfstr), Some(&dict)) }
.expect("CFAttributedStringCreate 失败")
}
}
impl Default for CoreTextEngine {
fn default() -> Self {
Self::new()
}
}
fn line_metrics(line: &CTLine) -> (f64, f64, f64, f64) {
let mut ascent = 0.0f64;
let mut descent = 0.0f64;
let mut leading = 0.0f64;
let width = unsafe { line.typographic_bounds(&mut ascent, &mut descent, &mut leading) };
(width, ascent, descent, leading)
}
impl TextEngine for CoreTextEngine {
fn set_scale(&mut self, scale: f32) {
self.scale = scale.max(0.1);
}
fn measure(
&mut self,
text: &str,
family: Option<&str>,
size: f32,
max_width: Option<f32>,
) -> Size {
if text.is_empty() {
return Size::new(0, size.ceil() as i32);
}
let s = self.scale;
let psize = size * s;
let font = self.font(family, psize);
let black = CGColor::new_srgb(0.0, 0.0, 0.0, 1.0);
let attr = self.attributed(text, &font, &black, Align::Start);
match max_width {
Some(w) if w > 0.0 => {
let fs = unsafe { CTFramesetter::with_attributed_string(&attr) };
let constraints = CGSize {
width: (w * s) as f64,
height: f64::MAX,
};
let fit = unsafe {
fs.suggest_frame_size_with_constraints(
CFRange {
location: 0,
length: 0,
},
None,
constraints,
ptr::null_mut(),
)
};
Size::new(
(fit.width / s as f64).ceil() as i32,
(fit.height / s as f64).ceil() as i32,
)
}
_ => {
let line = unsafe { CTLine::with_attributed_string(&attr) };
let (width, ascent, descent, leading) = line_metrics(&line);
let line_h = ascent + descent + leading;
Size::new(
(width / s as f64).ceil() as i32,
(line_h / s as f64).ceil() as i32,
)
}
}
}
fn draw(
&mut self,
pixmap: &mut Pixmap,
text: &str,
rect: Rect,
color: Color,
align: Align,
family: Option<&str>,
size: f32,
clip: Option<Rect>,
) {
if text.is_empty() || rect.is_empty() {
return;
}
let s = self.scale;
let prect = rect.scaled(s);
let pclip = clip.map(|c| c.scaled(s));
let psize = size * s;
let pw = pixmap.width() as i32;
let ph = pixmap.height() as i32;
let phf = ph as f64;
let bytes_per_row = pw as usize * 4;
let data = pixmap.data_mut().as_mut_ptr() as *mut c_void;
let ctx = match unsafe {
CGBitmapContextCreate(
data,
pw as usize,
ph as usize,
8,
bytes_per_row,
Some(&self.color_space),
CGImageAlphaInfo::PremultipliedLast.0,
)
} {
Some(c) => c,
None => return,
};
let font = self.font(family, psize);
let cg_color = CGColor::new_srgb(
color.r as f64 / 255.0,
color.g as f64 / 255.0,
color.b as f64 / 255.0,
color.a as f64 / 255.0,
);
let attr = self.attributed(text, &font, &cg_color, align);
let probe = unsafe { CTLine::with_attributed_string(&attr) };
let (line_w, ascent, descent, leading) = line_metrics(&probe);
let single = !text.contains('\n') && line_w <= prect.w as f64;
CGContext::save_g_state(Some(&ctx));
CGContext::set_allows_antialiasing(Some(&ctx), true);
if let Some(c) = pclip {
let cg = CGRect {
origin: CGPoint {
x: c.x as f64,
y: phf - (c.y + c.h) as f64,
},
size: CGSize {
width: c.w as f64,
height: c.h as f64,
},
};
CGContext::clip_to_rect(Some(&ctx), cg);
}
CGContext::set_text_matrix(Some(&ctx), IDENTITY);
if single {
let line_h = ascent + descent + leading;
let text_x0 = match align {
Align::Start | Align::Stretch => prect.x as f64,
Align::Center => prect.x as f64 + (prect.w as f64 - line_w) / 2.0,
Align::End => prect.x as f64 + prect.w as f64 - line_w,
};
let baseline_from_top = prect.y as f64 + (prect.h as f64 - line_h) / 2.0 + ascent;
let cg_y = phf - baseline_from_top;
CGContext::set_text_position(Some(&ctx), text_x0, cg_y);
unsafe { probe.draw(&ctx) };
} else {
let fs = unsafe { CTFramesetter::with_attributed_string(&attr) };
let constraints = CGSize {
width: prect.w as f64,
height: f64::MAX,
};
let fit = unsafe {
fs.suggest_frame_size_with_constraints(
CFRange {
location: 0,
length: 0,
},
None,
constraints,
ptr::null_mut(),
)
};
let text_h = fit.height;
let top_from_top = prect.y as f64 + (prect.h as f64 - text_h) / 2.0;
let path_rect = CGRect {
origin: CGPoint {
x: prect.x as f64,
y: phf - (top_from_top + text_h),
},
size: CGSize {
width: prect.w as f64,
height: text_h.ceil() + 1.0,
},
};
let path = unsafe { CGPath::with_rect(path_rect, ptr::null()) };
let frame = unsafe {
fs.frame(
CFRange {
location: 0,
length: 0,
},
&path,
None,
)
};
unsafe { frame.draw(&ctx) };
}
CGContext::restore_g_state(Some(&ctx));
}
}