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)
401 lines
14 KiB
Rust
401 lines
14 KiB
Rust
//! 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);
|
|
}
|
|
}
|