feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -0,0 +1,81 @@
|
||||
//! Dedicated thread for releasing CoreFoundation objects.
|
||||
//!
|
||||
//! Pools of CF objects (strings, attributed strings, typesetters, lines,
|
||||
//! fonts) are sent here so that their (excessively slow) release callback
|
||||
//! logic doesn't block the shaping/renderer thread. Equivalent of
|
||||
//! `os/cf_release_thread.zig` for the shaper's needs: a mailbox of owned
|
||||
//! pools and a thread that drops them.
|
||||
//!
|
||||
//! TODO(integration): this is a self-contained version living in the font
|
||||
//! crate; it can be replaced by `ghostty_os::cf_release_thread` once the os
|
||||
//! crate's port lands and the font crate depends on it.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
sync::mpsc::{Sender, channel},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use objc2_core_foundation::{CFRetained, CFType};
|
||||
|
||||
use crate::coretext_util::CfBox;
|
||||
|
||||
/// An owned CF object queued for release.
|
||||
pub(crate) type Pooled = CfBox<CFType>;
|
||||
|
||||
/// Convert any retained CF object into a pool entry.
|
||||
pub(crate) fn pooled<T: objc2_core_foundation::Type>(v: CFRetained<T>) -> Pooled {
|
||||
// SAFETY: every CF object is a CFType; the retain count is carried over
|
||||
// by the cast and released exactly once when the `Pooled` drops.
|
||||
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFType>(v) })
|
||||
}
|
||||
|
||||
pub(crate) struct ReleaseThread {
|
||||
tx: Option<Sender<Vec<Pooled>>>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ReleaseThread {
|
||||
pub(crate) fn new() -> Option<ReleaseThread> {
|
||||
let (tx, rx) = channel::<Vec<Pooled>>();
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("cf_release".into())
|
||||
.spawn(move || {
|
||||
// Dropping each pool releases every object in it.
|
||||
while let Ok(pool) = rx.recv() {
|
||||
drop(pool);
|
||||
}
|
||||
log::debug!("cf release thread exited");
|
||||
})
|
||||
.inspect_err(|e| log::warn!("error spawning cf release thread: {e}"))
|
||||
.ok()?;
|
||||
Some(ReleaseThread {
|
||||
tx: Some(tx),
|
||||
handle: Some(handle),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a pool for release. If the thread is gone the pool is released
|
||||
/// inline.
|
||||
pub(crate) fn release(&self, pool: Vec<Pooled>) {
|
||||
if pool.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(tx) = &self.tx
|
||||
&& let Err(e) = tx.send(pool)
|
||||
{
|
||||
// Receiver gone: release here.
|
||||
drop(e.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReleaseThread {
|
||||
fn drop(&mut self) {
|
||||
// Closing the channel stops the thread after it drains the mailbox.
|
||||
self.tx.take();
|
||||
if let Some(h) = self.handle.take() {
|
||||
let _ = h.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
//! CoreText shaper (Apple targets). Port of `font/shaper/coretext.zig`.
|
||||
//!
|
||||
//! CoreText shaping differs in subtle ways from HarfBuzz so it may result
|
||||
//! in inconsistent rendering across platforms. But it also fixes many
|
||||
//! issues (some macOS specific):
|
||||
//!
|
||||
//! - Theta hat offset is incorrect in HarfBuzz but correct by default
|
||||
//! on macOS applications using CoreText. (See:
|
||||
//! <https://github.com/harfbuzz/harfbuzz/discussions/4525>)
|
||||
//!
|
||||
//! - Hyphens (U+2010) can be synthesized by CoreText but not by HarfBuzz.
|
||||
//! See: <https://github.com/mitchellh/ghostty/issues/1643>
|
||||
//!
|
||||
//! The API mirrors [`crate::shaper::harfbuzz::Shaper`]: the run iterator
|
||||
//! drives `prepare` / `add_codepoint` / `finalize` through
|
||||
//! [`RunIteratorHook`](crate::shaper::run::RunIteratorHook) and the caller
|
||||
//! shapes with [`Shaper::shape_run`]. The cluster/ligature heuristics live
|
||||
//! in [`super::coretext_cells`] (unit tested on every platform).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ptr::{self, NonNull};
|
||||
|
||||
use objc2_core_foundation::{
|
||||
CFArray,
|
||||
CFAttributedString,
|
||||
CFDictionary,
|
||||
CFNumber,
|
||||
CFRange,
|
||||
CFRetained,
|
||||
CFString,
|
||||
CFType,
|
||||
CGPoint,
|
||||
CGSize,
|
||||
};
|
||||
use objc2_core_text::{
|
||||
CTFont,
|
||||
CTFontDescriptor,
|
||||
CTLine,
|
||||
CTRun,
|
||||
CTRunStatus,
|
||||
CTTypesetter,
|
||||
kCTFontAttributeName,
|
||||
kCTFontFeatureSettingsAttribute,
|
||||
kCTFontOpenTypeFeatureTag,
|
||||
kCTFontOpenTypeFeatureValue,
|
||||
kCTTypesetterOptionForcedEmbeddingLevel,
|
||||
};
|
||||
|
||||
use super::{
|
||||
cf_release::{Pooled, ReleaseThread, pooled},
|
||||
coretext_cells::{self as cells, CtRun, RunGlyph, RunState},
|
||||
};
|
||||
use crate::{
|
||||
coretext_util::CfBox,
|
||||
face::Face,
|
||||
shape::{self, Cell, DEFAULT_FEATURES, Feature, FeatureList, TextRun},
|
||||
shared_grid::SharedGrid,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeError {
|
||||
#[error("coretext object creation failed")]
|
||||
CoreTextFailed,
|
||||
}
|
||||
|
||||
/// Errors from [`Shaper::shape_run`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeRunError {
|
||||
#[error(transparent)]
|
||||
GetFace(#[from] crate::collection::GetFaceError),
|
||||
#[error(transparent)]
|
||||
Shape(#[from] ShapeError),
|
||||
}
|
||||
|
||||
/// Create a CoreFoundation Dictionary suitable for settings the font
|
||||
/// features of a CoreText font (`makeFeaturesDict`).
|
||||
fn make_features_dict(feats: &[Feature]) -> CfBox<CFDictionary> {
|
||||
let mut items: Vec<CFRetained<CFDictionary<CFString, CFType>>> = Vec::new();
|
||||
|
||||
for feat in feats {
|
||||
// Keys can only be ASCII.
|
||||
let key = CFString::from_str(&String::from_utf8_lossy(&feat.tag));
|
||||
let value = CFNumber::new_i32(feat.value as i32);
|
||||
|
||||
// SAFETY: valid static keys.
|
||||
let (k_tag, k_val) = unsafe { (kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue) };
|
||||
let dict = CFDictionary::<CFString, CFType>::from_slices(
|
||||
&[k_tag, k_val],
|
||||
&[key.as_ref(), value.as_ref()],
|
||||
);
|
||||
items.push(dict);
|
||||
}
|
||||
|
||||
let item_refs: Vec<&CFDictionary<CFString, CFType>> = items.iter().map(|d| &**d).collect();
|
||||
let list = CFArray::from_objects(&item_refs);
|
||||
|
||||
// SAFETY: valid static key.
|
||||
let k = unsafe { kCTFontFeatureSettingsAttribute };
|
||||
let dict = CFDictionary::<CFString, CFType>::from_slices(&[k], &[list.as_ref()]);
|
||||
// SAFETY: the attribute dictionary is used as an untyped CFDictionary.
|
||||
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(dict) })
|
||||
}
|
||||
|
||||
/// Shaper that uses CoreText.
|
||||
pub struct Shaper {
|
||||
/// The string used for shaping the current run.
|
||||
run_state: RunState,
|
||||
|
||||
/// CoreFoundation Dictionary which represents our font feature settings.
|
||||
features: CfBox<CFDictionary>,
|
||||
/// A version of the features dictionary with the default features excluded.
|
||||
features_no_default: CfBox<CFDictionary>,
|
||||
|
||||
/// The shared memory used for shaping results.
|
||||
cell_buf: Vec<Cell>,
|
||||
|
||||
/// Cached attributes dict for creating CTTypesetter objects.
|
||||
/// The values in this never change so we can avoid overhead
|
||||
/// by just creating it once and saving it for reuse.
|
||||
typesetter_attr_dict: CfBox<CFDictionary>,
|
||||
|
||||
/// List where we cache fonts, so we don't have to remake them for
|
||||
/// every single shaping operation.
|
||||
///
|
||||
/// Fonts are cached as attribute dictionaries to be applied directly to
|
||||
/// attributed strings.
|
||||
cached_fonts: Vec<Option<CfBox<CFDictionary>>>,
|
||||
|
||||
/// The grid that our cached fonts correspond to.
|
||||
/// If the grid changes then we need to reset our cache.
|
||||
cached_font_grid: usize,
|
||||
|
||||
/// The list of CoreFoundation objects to release on the dedicated
|
||||
/// release thread. This is built up over the course of shaping and
|
||||
/// sent to the release thread when `end_frame` is called.
|
||||
cf_release_pool: Vec<Pooled>,
|
||||
|
||||
/// Dedicated thread for releasing CoreFoundation objects. Some objects,
|
||||
/// such as those produced by CoreText, have excessively slow release
|
||||
/// callback logic.
|
||||
cf_release_thread: Option<ReleaseThread>,
|
||||
}
|
||||
|
||||
impl Shaper {
|
||||
/// The cell buffer holds the shaped results.
|
||||
pub fn new(opts: &shape::Options) -> Result<Shaper, ShapeError> {
|
||||
let mut feature_list = FeatureList::default();
|
||||
for feature_str in &opts.features {
|
||||
feature_list.append_from_string(feature_str);
|
||||
}
|
||||
|
||||
// We need to construct two attrs dictionaries for font features;
|
||||
// one without the default features included, and one with them.
|
||||
let feats = &feature_list.features;
|
||||
let mut feats_df: Vec<Feature> = DEFAULT_FEATURES.to_vec();
|
||||
feats_df.extend_from_slice(feats);
|
||||
|
||||
let features = make_features_dict(&feats_df);
|
||||
let features_no_default = make_features_dict(feats);
|
||||
|
||||
// For now we only support LTR text. If we shape RTL text then
|
||||
// rendering will be very wrong so we need to explicitly force
|
||||
// LTR no matter what.
|
||||
//
|
||||
// See: https://github.com/mitchellh/ghostty/issues/1737
|
||||
// See: https://github.com/mitchellh/ghostty/issues/1442
|
||||
//
|
||||
// We used to do this by setting the writing direction attribute
|
||||
// on the attributed string we used, but it seems like that will
|
||||
// still allow some weird results, for example a single space at
|
||||
// the end of a line composed of RTL characters will be cause it
|
||||
// to output a run containing just that space, BEFORE it outputs
|
||||
// the rest of the line as a separate run, very weirdly with the
|
||||
// "right to left" flag set in the single space run's run status...
|
||||
//
|
||||
// So instead what we do is use a CTTypesetter to create our line,
|
||||
// using the kCTTypesetterOptionForcedEmbeddingLevel attribute to
|
||||
// force CoreText not to try doing any sort of BiDi, instead just
|
||||
// treat all text as embedding level 0 (left to right).
|
||||
let typesetter_attr_dict = {
|
||||
let num = CFNumber::new_i32(0);
|
||||
// SAFETY: valid static key.
|
||||
let k = unsafe { kCTTypesetterOptionForcedEmbeddingLevel };
|
||||
let d = CFDictionary::<CFString, CFType>::from_slices(&[k], &[num.as_ref()]);
|
||||
// SAFETY: used as an untyped CFDictionary.
|
||||
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(d) })
|
||||
};
|
||||
|
||||
Ok(Shaper {
|
||||
run_state: RunState::default(),
|
||||
features,
|
||||
features_no_default,
|
||||
cell_buf: Vec::new(),
|
||||
typesetter_attr_dict,
|
||||
cached_fonts: Vec::new(),
|
||||
cached_font_grid: 0,
|
||||
cf_release_pool: Vec::new(),
|
||||
cf_release_thread: ReleaseThread::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send the release pool to the release thread.
|
||||
pub fn end_frame(&mut self) {
|
||||
if self.cf_release_pool.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all the items in the pool as an owned vec so we can
|
||||
// send it to the dedicated release thread.
|
||||
let items = std::mem::take(&mut self.cf_release_pool);
|
||||
match &self.cf_release_thread {
|
||||
Some(t) => t.release(items),
|
||||
// No thread: release inline.
|
||||
None => drop(items),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RunIterator hooks ----
|
||||
|
||||
pub fn prepare(&mut self) {
|
||||
self.run_state.reset();
|
||||
}
|
||||
|
||||
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
self.run_state.add_codepoint(cp, cluster);
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn finalize(&mut self) {}
|
||||
|
||||
/// Convenience: shape `text` as a single run where every item is a
|
||||
/// `(codepoint, cluster)` pair.
|
||||
pub fn shape_text(
|
||||
&mut self,
|
||||
face: Option<&Face>,
|
||||
text: &[(u32, u32)],
|
||||
) -> Result<&[Cell], ShapeError> {
|
||||
self.prepare();
|
||||
for &(cp, cluster) in text {
|
||||
self.add_codepoint(cp, cluster);
|
||||
}
|
||||
self.finalize();
|
||||
self.shape(face)
|
||||
}
|
||||
|
||||
/// Shape the given text run (`shape(run)` in Zig). The run must be the
|
||||
/// immediately previous run that was iterated since it shares state with
|
||||
/// the shaper.
|
||||
///
|
||||
/// Note that this will accumulate garbage in the release pool. The
|
||||
/// caller must ensure you're properly calling `end_frame` to release
|
||||
/// all the objects.
|
||||
pub fn shape_run(
|
||||
&mut self,
|
||||
grid: &SharedGrid,
|
||||
run: &TextRun,
|
||||
) -> Result<&[Cell], ShapeRunError> {
|
||||
// Special fonts aren't shaped and their codepoint == glyph so we
|
||||
// can just return the codepoints as-is.
|
||||
if run.font_index.special().is_some() {
|
||||
cells::special_cells(&self.run_state.codepoints, &mut self.cell_buf);
|
||||
return Ok(&self.cell_buf);
|
||||
}
|
||||
|
||||
let attr_dict = self.get_font(grid, run.font_index)?;
|
||||
Ok(self.shape_with_attrs(&attr_dict)?)
|
||||
}
|
||||
|
||||
/// Shape the current run (as built through the hooks) with the given
|
||||
/// face (uncached), or pass `None` for a special-case font (sprite).
|
||||
pub fn shape(&mut self, face: Option<&Face>) -> Result<&[Cell], ShapeError> {
|
||||
let Some(face) = face else {
|
||||
cells::special_cells(&self.run_state.codepoints, &mut self.cell_buf);
|
||||
return Ok(&self.cell_buf);
|
||||
};
|
||||
let attr_dict = self.make_attr_dict(face);
|
||||
self.shape_with_attrs(&attr_dict)
|
||||
}
|
||||
|
||||
fn shape_with_attrs(&mut self, attr_dict: &CFDictionary) -> Result<&[Cell], ShapeError> {
|
||||
let state = &self.run_state;
|
||||
|
||||
// Make room for the attributed string, CTTypesetter, and CTLine.
|
||||
self.cf_release_pool.reserve(4);
|
||||
|
||||
// SAFETY: `unichars` is a valid buffer of `len` UTF-16 units.
|
||||
let str = unsafe {
|
||||
CFString::with_characters(None, state.unichars.as_ptr(), state.unichars.len() as isize)
|
||||
}
|
||||
.ok_or(ShapeError::CoreTextFailed)?;
|
||||
|
||||
// Create an attributed string from our string
|
||||
// SAFETY: valid string and attribute dictionary.
|
||||
let attr_str = unsafe { CFAttributedString::new(None, Some(&str), Some(attr_dict)) }
|
||||
.ok_or(ShapeError::CoreTextFailed)?;
|
||||
|
||||
// Create a typesetter from the attributed string and the cached
|
||||
// attr dict. (See comment in new for more info on the attr dict.)
|
||||
// SAFETY: valid string and options dictionary.
|
||||
let typesetter = unsafe {
|
||||
CTTypesetter::with_attributed_string_and_options(
|
||||
&attr_str,
|
||||
Some(&self.typesetter_attr_dict),
|
||||
)
|
||||
}
|
||||
.ok_or(ShapeError::CoreTextFailed)?;
|
||||
|
||||
// Create a line from the typesetter
|
||||
// SAFETY: a zero length range means the whole string.
|
||||
let line: CFRetained<CTLine> = unsafe {
|
||||
typesetter.line(CFRange {
|
||||
location: 0,
|
||||
length: 0,
|
||||
})
|
||||
};
|
||||
|
||||
// CoreText, despite our insistence with an enforced embedding level,
|
||||
// may sometimes output runs that are non-monotonic.
|
||||
//
|
||||
// CoreText may generate multiple runs even though our input to
|
||||
// CoreText is already split into runs by our own run iterator.
|
||||
// The runs as far as I can tell are always sequential to each
|
||||
// other so we can iterate over them and just append to our
|
||||
// cell buffer.
|
||||
// SAFETY: plain CoreText call.
|
||||
let glyph_runs = unsafe { line.glyph_runs() };
|
||||
// SAFETY: the glyph runs array contains CTRun objects.
|
||||
let glyph_runs: &CFArray<CTRun> = unsafe { glyph_runs.cast_unchecked() };
|
||||
let mut runs: Vec<CtRun> = Vec::with_capacity(glyph_runs.len());
|
||||
for i in 0..glyph_runs.len() {
|
||||
let Some(ctrun) = glyph_runs.get(i) else {
|
||||
continue;
|
||||
};
|
||||
runs.push(read_run(&ctrun));
|
||||
}
|
||||
|
||||
cells::cells_from_runs(&self.run_state.codepoints, &runs, &mut self.cell_buf);
|
||||
|
||||
// The objects are released on the release thread (see `end_frame`).
|
||||
self.cf_release_pool.push(pooled(str));
|
||||
self.cf_release_pool.push(pooled(attr_str));
|
||||
self.cf_release_pool.push(pooled(typesetter));
|
||||
self.cf_release_pool.push(pooled(line));
|
||||
|
||||
Ok(&self.cell_buf)
|
||||
}
|
||||
|
||||
/// Build an attr dict (kCTFontAttributeName -> font with our features)
|
||||
/// for the given face.
|
||||
fn make_attr_dict(&mut self, face: &Face) -> CfBox<CFDictionary> {
|
||||
let attrs = if face.quirks_disable_default_font_features {
|
||||
&self.features_no_default
|
||||
} else {
|
||||
&self.features
|
||||
};
|
||||
|
||||
// SAFETY: valid attributes dictionary.
|
||||
let desc = unsafe { CTFontDescriptor::with_attributes(attrs) };
|
||||
// SAFETY: valid descriptor, null matrix is allowed.
|
||||
let copied: CFRetained<CTFont> = unsafe {
|
||||
face.ct_font()
|
||||
.copy_with_attributes(0.0, ptr::null(), Some(&desc))
|
||||
};
|
||||
|
||||
// Get our font and use that get the attributes to set for the
|
||||
// attributed string so the whole string uses the same font.
|
||||
// SAFETY: valid static key.
|
||||
let k = unsafe { kCTFontAttributeName };
|
||||
let d = CFDictionary::<CFString, CFType>::from_slices(&[k], &[copied.as_ref()]);
|
||||
|
||||
self.cf_release_pool.push(pooled(desc));
|
||||
self.cf_release_pool.push(pooled(copied));
|
||||
// SAFETY: used as an untyped CFDictionary.
|
||||
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(d) })
|
||||
}
|
||||
|
||||
/// Get an attr dict for a font from a specific index.
|
||||
/// These items are cached.
|
||||
fn get_font(
|
||||
&mut self,
|
||||
grid: &SharedGrid,
|
||||
index: crate::collection::Index,
|
||||
) -> Result<CfBox<CFDictionary>, ShapeRunError> {
|
||||
// If this grid doesn't match the one we've cached fonts for,
|
||||
// then we reset the cache list since it's no longer valid.
|
||||
// We use an integer rather than keeping the pointer because we
|
||||
// don't want anyone to inadvertently use the pointer.
|
||||
let grid_id = ptr::from_ref(grid) as usize;
|
||||
if grid_id != self.cached_font_grid {
|
||||
if self.cached_font_grid > 0 {
|
||||
// Put all the currently cached fonts in to
|
||||
// the release pool before clearing the list.
|
||||
for ft in self.cached_fonts.drain(..).flatten() {
|
||||
self.cf_release_pool.push(pooled(ft.0));
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_font_grid = grid_id;
|
||||
}
|
||||
|
||||
let index_int = index.int() as usize;
|
||||
|
||||
// The cached fonts are indexed directly by the font index, since
|
||||
// this number is usually low. Therefore, we set any index we haven't
|
||||
// seen to null.
|
||||
if self.cached_fonts.len() <= index_int {
|
||||
self.cached_fonts.resize_with(index_int + 1, || None);
|
||||
}
|
||||
|
||||
// If we have it, return the cached attr dict.
|
||||
if let Some(cached) = &self.cached_fonts[index_int] {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
// The CoreText shaper relies on CoreText and CoreText claims
|
||||
// that CTFonts are threadsafe. See:
|
||||
// https://developer.apple.com/documentation/coretext/
|
||||
//
|
||||
// Quote:
|
||||
// All individual functions in Core Text are thread-safe. Font
|
||||
// objects (CTFont, CTFontDescriptor, and associated objects) can
|
||||
// be used simultaneously by multiple operations, work queues, or
|
||||
// threads. However, the layout objects (CTTypesetter, CTFramesetter,
|
||||
// CTRun, CTLine, CTFrame, and associated objects) should be used in
|
||||
// a single operation, work queue, or thread.
|
||||
//
|
||||
// Because of this, we only hold the grid lock to grab the face and
|
||||
// set it up, then release it.
|
||||
let attr_dict = {
|
||||
let mut guard = grid.write();
|
||||
let face = guard.resolver.collection.get_face(index)?;
|
||||
self.make_attr_dict(face)
|
||||
};
|
||||
|
||||
self.cached_fonts[index_int] = Some(attr_dict.clone());
|
||||
Ok(attr_dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shaper {
|
||||
fn drop(&mut self) {
|
||||
// Release whatever is left in the pool on the release thread
|
||||
// (it is joined when the thread handle drops).
|
||||
let items = std::mem::take(&mut self.cf_release_pool);
|
||||
if !items.is_empty() {
|
||||
log::debug!("releasing {} remaining CF objects", items.len());
|
||||
}
|
||||
let cached = std::mem::take(&mut self.cached_fonts);
|
||||
let mut all = items;
|
||||
all.extend(cached.into_iter().flatten().map(|d| pooled(d.0)));
|
||||
if let Some(t) = &self.cf_release_thread {
|
||||
t.release(all);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a CTRun's glyphs, advances, positions and string indices
|
||||
/// (`getGlyphsPtr() orelse getGlyphs(alloc)` etc.).
|
||||
fn read_run(ctrun: &CTRun) -> CtRun {
|
||||
// SAFETY: plain CoreText getters on a valid run.
|
||||
let (status, n) = unsafe { (ctrun.status(), ctrun.glyph_count() as usize) };
|
||||
let non_ltr = status.intersects(CTRunStatus::NonMonotonic | CTRunStatus::RightToLeft);
|
||||
if n == 0 {
|
||||
return CtRun {
|
||||
non_ltr,
|
||||
glyphs: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let full = CFRange {
|
||||
location: 0,
|
||||
length: 0,
|
||||
};
|
||||
let mut glyphs = vec![0u16; n];
|
||||
let mut advances = vec![CGSize::new(0.0, 0.0); n];
|
||||
let mut positions = vec![CGPoint::new(0.0, 0.0); n];
|
||||
let mut indices = vec![0isize; n];
|
||||
// SAFETY: each buffer holds `n` elements, which is the glyph count of the
|
||||
// run; a zero-length range copies everything.
|
||||
unsafe {
|
||||
ctrun.glyphs(full, NonNull::new_unchecked(glyphs.as_mut_ptr()));
|
||||
ctrun.advances(full, NonNull::new_unchecked(advances.as_mut_ptr()));
|
||||
ctrun.positions(full, NonNull::new_unchecked(positions.as_mut_ptr()));
|
||||
ctrun.string_indices(full, NonNull::new_unchecked(indices.as_mut_ptr()));
|
||||
}
|
||||
|
||||
CtRun {
|
||||
non_ltr,
|
||||
glyphs: (0..n)
|
||||
.map(|i| RunGlyph {
|
||||
glyph: glyphs[i],
|
||||
advance_width: advances[i].width,
|
||||
x: positions[i].x,
|
||||
y: positions[i].y,
|
||||
index: indices[i] as usize,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
//! Platform-independent part of the CoreText shaper
|
||||
//! (`font/shaper/coretext.zig`): building the UTF-16 string / codepoint
|
||||
//! table for a run, and converting CoreText glyph runs to shaped cells.
|
||||
//!
|
||||
//! The CoreText calls themselves live in `shaper/coretext.rs` (Apple
|
||||
//! targets); this module takes plain data so the (subtle) cluster /
|
||||
//! ligature heuristics can be unit tested on every platform.
|
||||
|
||||
use crate::shape::Cell;
|
||||
|
||||
/// One entry of the codepoint table. Surrogate pairs occupy two entries so
|
||||
/// that indices match the UTF-16 string handed to CoreText; the second one
|
||||
/// has `codepoint == 0`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Codepoint {
|
||||
pub codepoint: u32,
|
||||
pub cluster: u32,
|
||||
}
|
||||
|
||||
/// The state of the current run (`RunState`).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RunState {
|
||||
pub codepoints: Vec<Codepoint>,
|
||||
pub unichars: Vec<u16>,
|
||||
}
|
||||
|
||||
impl RunState {
|
||||
pub fn reset(&mut self) {
|
||||
self.codepoints.clear();
|
||||
self.unichars.clear();
|
||||
}
|
||||
|
||||
/// `RunIteratorHook.addCodepoint`.
|
||||
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
// Build our UTF-16 string for CoreText
|
||||
let (units, len) = crate::face::coretext_math::utf16_units(cp);
|
||||
self.unichars.extend_from_slice(&units[..len]);
|
||||
|
||||
// Build our reverse lookup table for codepoints to clusters
|
||||
self.codepoints.push(Codepoint {
|
||||
codepoint: cp,
|
||||
cluster,
|
||||
});
|
||||
|
||||
// If the UTF-16 codepoint is a pair then we need to insert
|
||||
// a dummy entry so that the CTRunGetStringIndices() function
|
||||
// maps correctly.
|
||||
if len == 2 {
|
||||
self.codepoints.push(Codepoint {
|
||||
codepoint: 0,
|
||||
cluster,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Special fonts aren't shaped and their codepoint == glyph so we can just
|
||||
/// return the codepoints as-is.
|
||||
pub fn special_cells(codepoints: &[Codepoint], out: &mut Vec<Cell>) {
|
||||
out.clear();
|
||||
for entry in codepoints {
|
||||
// We use null codepoints to pad out our list so indices match
|
||||
// the UTF-16 string we constructed for CoreText. We don't want
|
||||
// to emit these if this is a special font, since they're not
|
||||
// part of the original run.
|
||||
if entry.codepoint == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(Cell {
|
||||
x: entry.cluster as u16,
|
||||
glyph_index: entry.codepoint,
|
||||
..Cell::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// One glyph of a CoreText run (the zipped glyphs / advances / positions /
|
||||
/// string indices arrays).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RunGlyph {
|
||||
pub glyph: u16,
|
||||
pub advance_width: f64,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
/// Index into the UTF-16 string (and therefore the codepoint table).
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
/// One CoreText glyph run of the line.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CtRun {
|
||||
/// True if the run status has `non_monotonic` or `right_to_left`.
|
||||
pub non_ltr: bool,
|
||||
pub glyphs: Vec<RunGlyph>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct Offset {
|
||||
cluster: u32,
|
||||
x: f64,
|
||||
}
|
||||
|
||||
/// Convert CoreText runs to cells (the loop at the end of `Shaper.shape`).
|
||||
pub fn cells_from_runs(codepoints: &[Codepoint], runs: &[CtRun], out: &mut Vec<Cell>) {
|
||||
// This keeps track of the current x offset (sum of advance.width) and
|
||||
// the furthest cluster we've seen so far (max).
|
||||
let mut run_offset = Offset::default();
|
||||
|
||||
// This keeps track of the cell starting x and cluster.
|
||||
let mut cell_offset = Offset::default();
|
||||
|
||||
out.clear();
|
||||
|
||||
// CoreText, despite our insistence with an enforced embedding level,
|
||||
// may sometimes output runs that are non-monotonic. In order to fix
|
||||
// this, we check the run status for each run and if any aren't ltr
|
||||
// we set this to true, which indicates that we must sort our buffer.
|
||||
let mut non_ltr = false;
|
||||
|
||||
// CoreText may generate multiple runs even though our input to
|
||||
// CoreText is already split into runs by our own run iterator.
|
||||
// The runs as far as I can tell are always sequential to each
|
||||
// other so we can iterate over them and just append to our
|
||||
// cell buffer.
|
||||
for run in runs {
|
||||
if run.non_ltr {
|
||||
non_ltr = true;
|
||||
}
|
||||
|
||||
for g in &run.glyphs {
|
||||
let index = g.index;
|
||||
|
||||
// Our cluster is also our cell X position. If the cluster changes
|
||||
// then we need to reset our current cell offsets.
|
||||
let cluster = codepoints[index].cluster;
|
||||
if cell_offset.cluster != cluster {
|
||||
// We previously asserted that the new cluster is greater
|
||||
// than cell_offset.cluster, but this isn't always true.
|
||||
// See e.g. the "shape Chakma vowel sign with ligature
|
||||
// (vowel sign renders first)" test.
|
||||
|
||||
let is_after_glyph_from_current_or_next_clusters = cluster <= run_offset.cluster;
|
||||
|
||||
let is_first_codepoint_in_cluster = {
|
||||
let mut i = index;
|
||||
let mut result = true;
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
let codepoint = codepoints[i];
|
||||
|
||||
// Skip surrogate pair padding
|
||||
if codepoint.codepoint == 0 {
|
||||
continue;
|
||||
}
|
||||
result = codepoint.cluster != cluster;
|
||||
break;
|
||||
}
|
||||
result
|
||||
};
|
||||
|
||||
// We need to reset the `cell_offset` at the start of a new
|
||||
// cluster, but we do that conditionally if the codepoint
|
||||
// `is_first_codepoint_in_cluster` and the cluster is not
|
||||
// `is_after_glyph_from_current_or_next_clusters`, which is
|
||||
// a heuristic to detect ligatures and avoid positioning
|
||||
// glyphs that mark ligatures incorrectly. The idea is that
|
||||
// if the first codepoint in a cluster doesn't appear in
|
||||
// the stream, it's very likely that it combined with
|
||||
// codepoints from a previous cluster into a ligature.
|
||||
// Then, the subsequent codepoints are very likely marking
|
||||
// glyphs that are placed relative to that ligature, so if
|
||||
// we were to reset the `cell_offset` to align it with the
|
||||
// grid, the positions would be off. The
|
||||
// `!is_after_glyph_from_current_or_next_clusters` check is
|
||||
// needed in case these marking glyphs come from a later
|
||||
// cluster but are rendered first (see the Chakma and
|
||||
// Bengali tests). In that case when we get to the
|
||||
// codepoint that `is_first_codepoint_in_cluster`, but in a
|
||||
// cluster that
|
||||
// `is_after_glyph_from_current_or_next_clusters`, we don't
|
||||
// want to reset to the grid and cause the positions to be
|
||||
// off. (Note that we could go back and align the cells to
|
||||
// the grid starting from the one from the cluster that
|
||||
// rendered out of order, but that is more complicated so
|
||||
// we don't do that for now. Also, it's TBD if there are
|
||||
// exceptions to this heuristic for detecting ligatures,
|
||||
// but using the logging below seems to show it works
|
||||
// well.)
|
||||
if is_first_codepoint_in_cluster && !is_after_glyph_from_current_or_next_clusters {
|
||||
cell_offset = Offset {
|
||||
cluster,
|
||||
x: run_offset.x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let x_offset = g.x - cell_offset.x;
|
||||
|
||||
out.push(Cell {
|
||||
x: cell_offset.cluster as u16,
|
||||
x_offset: x_offset.round() as i16,
|
||||
y_offset: g.y.round() as i16,
|
||||
glyph_index: u32::from(g.glyph),
|
||||
});
|
||||
|
||||
// Add our advances to keep track of our run offsets.
|
||||
// Advances apply to the NEXT cell.
|
||||
run_offset.x += g.advance_width;
|
||||
run_offset.cluster = run_offset.cluster.max(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
// If our buffer contains some non-ltr sections we need to sort it :/
|
||||
if non_ltr {
|
||||
// This is EXCEPTIONALLY rare. Only happens for languages with
|
||||
// complex shaping which we don't even really support properly
|
||||
// right now, so are very unlikely to be used heavily by users
|
||||
// of Ghostty.
|
||||
//
|
||||
// Zig's `std.mem.sort` is not stable but Rust's `sort_by_key` is;
|
||||
// equal keys keep their relative order which is a valid result.
|
||||
out.sort_by_key(|c| c.x);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn state(cps: &[(u32, u32)]) -> RunState {
|
||||
let mut s = RunState::default();
|
||||
for &(cp, cl) in cps {
|
||||
s.add_codepoint(cp, cl);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn glyph(glyph: u16, adv: f64, x: f64, y: f64, index: usize) -> RunGlyph {
|
||||
RunGlyph {
|
||||
glyph,
|
||||
advance_width: adv,
|
||||
x,
|
||||
y,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_codepoint_bmp() {
|
||||
let s = state(&[(0x41, 0), (0x42, 1)]);
|
||||
assert_eq!(s.unichars, vec![0x41, 0x42]);
|
||||
assert_eq!(s.codepoints.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_codepoint_surrogate_pair_pads_table() {
|
||||
let s = state(&[(0x1F978, 0), (0x41, 2)]);
|
||||
assert_eq!(s.unichars, vec![0xD83E, 0xDD78, 0x41]);
|
||||
assert_eq!(
|
||||
s.codepoints,
|
||||
vec![
|
||||
Codepoint {
|
||||
codepoint: 0x1F978,
|
||||
cluster: 0
|
||||
},
|
||||
Codepoint {
|
||||
codepoint: 0,
|
||||
cluster: 0
|
||||
},
|
||||
Codepoint {
|
||||
codepoint: 0x41,
|
||||
cluster: 2
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn special_skips_padding() {
|
||||
let s = state(&[(0x1F978, 3), (0x41, 5)]);
|
||||
let mut out = Vec::new();
|
||||
special_cells(&s.codepoints, &mut out);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert_eq!(out[0].x, 3);
|
||||
assert_eq!(out[0].glyph_index, 0x1F978);
|
||||
assert_eq!(out[1].x, 5);
|
||||
assert_eq!(out[1].glyph_index, 0x41);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_run_one_glyph_per_cluster() {
|
||||
let s = state(&[(0x41, 0), (0x42, 1), (0x43, 2)]);
|
||||
let run = CtRun {
|
||||
non_ltr: false,
|
||||
glyphs: vec![
|
||||
glyph(10, 8.0, 0.0, 0.0, 0),
|
||||
glyph(11, 8.0, 8.0, 0.0, 1),
|
||||
glyph(12, 8.0, 16.0, 0.0, 2),
|
||||
],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert_eq!(out.len(), 3);
|
||||
for (i, c) in out.iter().enumerate() {
|
||||
assert_eq!(c.x as usize, i);
|
||||
assert_eq!(c.x_offset, 0);
|
||||
assert_eq!(c.y_offset, 0);
|
||||
assert_eq!(c.glyph_index, 10 + i as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ligature_glyph_spans_clusters() {
|
||||
// "fi" shaped to one glyph (index 0) -> a single cell at cluster 0;
|
||||
// the following char starts a new cell at cluster 2.
|
||||
let s = state(&[(0x66, 0), (0x69, 1), (0x41, 2)]);
|
||||
let run = CtRun {
|
||||
non_ltr: false,
|
||||
glyphs: vec![glyph(50, 16.0, 0.0, 0.0, 0), glyph(11, 8.0, 16.0, 0.0, 2)],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert_eq!(out[0].x, 0);
|
||||
assert_eq!(out[1].x, 2);
|
||||
assert_eq!(out[1].x_offset, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_after_ligature_keeps_ligature_cell() {
|
||||
// Clusters 0,1 ligate; a mark in cluster 2 positioned relative to
|
||||
// the ligature: first codepoint of cluster 2 isn't "first" in the
|
||||
// stream (it's index 2, preceded by cluster 1) so it IS first in
|
||||
// cluster; cluster 2 > run_offset.cluster(0) so offset resets.
|
||||
let s = state(&[(0x66, 0), (0x69, 1), (0x301, 2)]);
|
||||
let run = CtRun {
|
||||
non_ltr: false,
|
||||
glyphs: vec![glyph(50, 16.0, 0.0, 0.0, 0), glyph(99, 0.0, 14.0, 3.0, 2)],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert_eq!(out[1].x, 2);
|
||||
// cell offset reset to run_offset.x = 16 => x_offset = 14 - 16
|
||||
assert_eq!(out[1].x_offset, -2);
|
||||
assert_eq!(out[1].y_offset, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_from_earlier_cluster_does_not_reset_offset() {
|
||||
// Glyph for cluster 1 comes after glyph for cluster 2 (rendered out
|
||||
// of order): cluster 1 <= run_offset.cluster (2) => no reset, the
|
||||
// cell stays at the cluster 2 offset.
|
||||
let s = state(&[(0x41, 0), (0x42, 1), (0x43, 2)]);
|
||||
let run = CtRun {
|
||||
non_ltr: false,
|
||||
glyphs: vec![
|
||||
glyph(1, 8.0, 0.0, 0.0, 0),
|
||||
glyph(3, 8.0, 8.0, 0.0, 2),
|
||||
glyph(2, 8.0, 16.0, 0.0, 1),
|
||||
],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert_eq!(out.len(), 3);
|
||||
// cluster 2's glyph: reset (2 > 0): offset x = 8
|
||||
assert_eq!(out[1].x, 2);
|
||||
// cluster 1 glyph after cluster 2: 1 <= 2 so no reset, stays at 2
|
||||
assert_eq!(out[2].x, 2);
|
||||
assert_eq!(out[2].x_offset, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ltr_sorts_by_x() {
|
||||
let s = state(&[(0x41, 0), (0x42, 1)]);
|
||||
let run = CtRun {
|
||||
non_ltr: true,
|
||||
glyphs: vec![glyph(2, 8.0, 0.0, 0.0, 1), glyph(1, 8.0, 8.0, 0.0, 0)],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert!(out.windows(2).all(|w| w[0].x <= w[1].x));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surrogate_padding_skipped_when_looking_back() {
|
||||
// Emoji (pair) at cluster 0 followed by 'A' at cluster 1. The 'A'
|
||||
// glyph looks back past the padding entry to the real codepoint.
|
||||
let s = state(&[(0x1F978, 0), (0x41, 1)]);
|
||||
let run = CtRun {
|
||||
non_ltr: false,
|
||||
glyphs: vec![glyph(7, 16.0, 0.0, 0.0, 0), glyph(8, 8.0, 16.0, 0.0, 2)],
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
cells_from_runs(&s.codepoints, &[run], &mut out);
|
||||
assert_eq!(out[1].x, 1);
|
||||
assert_eq!(out[1].x_offset, 0);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
use super::hb;
|
||||
use crate::{
|
||||
face::freetype::Face,
|
||||
face::Face,
|
||||
shape::{self, Cell, DEFAULT_FEATURES, FeatureList, TextRun},
|
||||
shared_grid::SharedGrid,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::ptr::NonNull;
|
||||
|
||||
use harfbuzz_sys as sys;
|
||||
|
||||
use crate::face::freetype::HbFont;
|
||||
use crate::face::HbFont;
|
||||
|
||||
/// A HarfBuzz feature (`hb_feature_t`) applied globally.
|
||||
pub type Feature = sys::hb_feature_t;
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod cache;
|
||||
#[cfg(font_coretext_shaper)]
|
||||
mod cf_release;
|
||||
#[cfg(font_coretext_shaper)]
|
||||
pub mod coretext;
|
||||
pub mod coretext_cells;
|
||||
pub mod feature;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(font_harfbuzz)]
|
||||
pub mod harfbuzz;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(font_harfbuzz)]
|
||||
mod hb;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod noop;
|
||||
@@ -14,5 +19,5 @@ pub mod run;
|
||||
|
||||
pub mod web_canvas;
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
#[cfg(all(test, font_harfbuzz, font_fontconfig))]
|
||||
pub(crate) mod test_util;
|
||||
@@ -3,10 +3,25 @@
|
||||
//! font's glyph index.
|
||||
|
||||
use crate::{
|
||||
face::freetype::Face,
|
||||
shape::{self, Cell},
|
||||
face::Face,
|
||||
shape::{self, Cell, TextRun},
|
||||
shared_grid::SharedGrid,
|
||||
};
|
||||
|
||||
/// Never produced (the no-op shaper can't fail to initialize); exists so the
|
||||
/// API matches the other shapers (`shape::ShapeError`).
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeError {}
|
||||
|
||||
/// Errors from [`Shaper::shape_run`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeRunError {
|
||||
#[error(transparent)]
|
||||
GetFace(#[from] crate::collection::GetFaceError),
|
||||
#[error(transparent)]
|
||||
Shape(#[from] ShapeError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Codepoint {
|
||||
codepoint: u32,
|
||||
@@ -24,8 +39,9 @@ pub struct Shaper {
|
||||
}
|
||||
|
||||
impl Shaper {
|
||||
pub fn new(_opts: &shape::Options) -> Shaper {
|
||||
Shaper::default()
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn new(_opts: &shape::Options) -> Result<Shaper, ShapeError> {
|
||||
Ok(Shaper::default())
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) {}
|
||||
@@ -45,6 +61,20 @@ impl Shaper {
|
||||
|
||||
pub fn finalize(&mut self) {}
|
||||
|
||||
/// Shape the given text run (looks the face up in `grid`).
|
||||
pub fn shape_run(
|
||||
&mut self,
|
||||
grid: &SharedGrid,
|
||||
run: &TextRun,
|
||||
) -> Result<&[Cell], ShapeRunError> {
|
||||
if run.font_index.special().is_none() {
|
||||
let mut guard = grid.write();
|
||||
let face = guard.resolver.collection.get_face(run.font_index)?;
|
||||
return Ok(self.shape(Some(face)));
|
||||
}
|
||||
Ok(self.shape(None))
|
||||
}
|
||||
|
||||
/// Shape the current run. `face` is `None` for special fonts (sprite),
|
||||
/// whose codepoint == glyph index.
|
||||
///
|
||||
@@ -76,7 +106,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn noop_special_passthrough() {
|
||||
let mut s = Shaper::new(&shape::Options::default());
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
s.prepare();
|
||||
s.add_codepoint(0x41, 0);
|
||||
s.add_codepoint(0x42, 2);
|
||||
|
||||
@@ -160,6 +160,7 @@ pub trait RunIteratorHook {
|
||||
fn finalize(&mut self);
|
||||
}
|
||||
|
||||
#[cfg(font_harfbuzz)]
|
||||
impl RunIteratorHook for crate::shaper::harfbuzz::Shaper {
|
||||
fn prepare(&mut self) {
|
||||
Self::prepare(self);
|
||||
@@ -172,6 +173,19 @@ impl RunIteratorHook for crate::shaper::harfbuzz::Shaper {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(font_coretext_shaper)]
|
||||
impl RunIteratorHook for crate::shaper::coretext::Shaper {
|
||||
fn prepare(&mut self) {
|
||||
Self::prepare(self);
|
||||
}
|
||||
fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
Self::add_codepoint(self, cp, cluster);
|
||||
}
|
||||
fn finalize(&mut self) {
|
||||
Self::finalize(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl RunIteratorHook for crate::shaper::noop::Shaper {
|
||||
fn prepare(&mut self) {
|
||||
Self::prepare(self);
|
||||
|
||||
Reference in new issue
Block a user