use std::sync::atomic::{AtomicU8, Ordering};
use cranpose_ui_graphics::Rect;
use read_fonts::model::metrics::GlyphExtents;
use crate::{
glyph_id::GlyphId,
index_range::widen,
laid_glyphs::RunGlyphs,
layout_body::LayoutBody,
line_box::FontExtent,
shaped_item::Piece,
text_layout::{GlyphRun, LayoutLine, LineRun},
typeface::Typeface,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum InkReach {
#[default]
Within,
Lines,
Glyphs,
}
impl InkReach {
pub(crate) fn of_line(
baseline: f32,
height: f32,
(fonts, band): (FontExtent, impl FnOnce() -> FontExtent),
) -> Self {
let cuts =
|extent: FontExtent| baseline < extent.ascent || height - baseline < extent.descent;
if cuts(fonts) && cuts(band()) {
Self::Lines
} else {
Self::Within
}
}
}
const UNREAD: u8 = 0;
const TIGHT: u8 = 1;
const LOOSE: u8 = 2;
const OUT: u8 = 3;
pub(crate) struct GlyphInks {
states: Box<[AtomicU8]>,
ascent: f32,
descent: f32,
band: (f32, f32),
}
impl GlyphInks {
pub(crate) fn of(typeface: &Typeface) -> Self {
let glyphs = usize::try_from(typeface.font().num_glyphs()).unwrap_or(0);
let metrics = typeface.line_metrics();
let lines = (metrics.ascent, metrics.descent);
let band = (' '..='~')
.filter_map(|ch| {
let glyph = typeface.glyph(ch)?;
let ink = typeface.glyph_extents(glyph)?;
(ink.width > 0.0
&& ink.height > 0.0
&& !reaches_out(&ink, typeface.advance(glyph), lines))
.then_some((ink.y_bearing, ink.y_bearing - ink.height))
})
.reduce(|(top, bottom), (ink_top, ink_bottom)| {
(top.max(ink_top), bottom.min(ink_bottom))
})
.unwrap_or(lines);
Self {
states: (0..glyphs.div_ceil(4))
.map(|_| AtomicU8::new(UNREAD))
.collect(),
ascent: metrics.ascent,
descent: metrics.descent,
band,
}
}
pub(crate) fn leaves_band(&self, typeface: &Typeface, glyph: GlyphId) -> bool {
self.state(typeface, glyph) != TIGHT
}
fn state(&self, typeface: &Typeface, glyph: GlyphId) -> u8 {
let index = widen(glyph.0);
let Some(cell) = self.states.get(index / 4) else {
return OUT;
};
let shift = (index % 4) * 2;
match (cell.load(Ordering::Relaxed) >> shift) & 3 {
UNREAD => self.read(typeface, glyph, (cell, shift)),
state => state,
}
}
#[inline(never)]
fn read(&self, typeface: &Typeface, glyph: GlyphId, (cell, shift): (&AtomicU8, usize)) -> u8 {
let state = typeface
.glyph_extents(glyph)
.filter(|ink| ink.width > 0.0 && ink.height > 0.0)
.map_or(TIGHT, |ink| {
let (top, bottom) = (ink.y_bearing, ink.y_bearing - ink.height);
if reaches_out(&ink, typeface.advance(glyph), (self.ascent, self.descent)) {
OUT
} else if top > self.band.0 || bottom < self.band.1 {
LOOSE
} else {
TIGHT
}
});
cell.fetch_or(state << shift, Ordering::Relaxed);
state
}
}
fn reaches_out(extents: &GlyphExtents<f32>, advance: f32, (ascent, descent): (f32, f32)) -> bool {
extents.width > 0.0
&& extents.height > 0.0
&& (extents.x_bearing < 0.0
|| extents.x_bearing + extents.width > advance
|| extents.y_bearing > ascent
|| extents.y_bearing - extents.height < descent)
}
pub(crate) fn line_band(pieces: &[Piece], faces: &[Typeface]) -> FontExtent {
pieces
.iter()
.fold(FontExtent::new(0.0, 0.0, 0.0), |line, piece| {
let (top, bottom) = faces[piece.face].glyph_inks().band;
let shift = piece.baseline_shift;
FontExtent::new(
line.ascent.max(top * piece.scale + shift.max(0.0)),
line.descent.max(-bottom * piece.scale - shift.min(0.0)),
0.0,
)
})
}
#[inline(never)]
pub(crate) fn find_ink_past_lines(
body: &LayoutBody,
lines: (f32, f32),
reach: InkReach,
) -> Option<Rect> {
let mut past = PastInk {
lines,
height: body.height,
glyphs_leave: reach == InkReach::Glyphs,
ink: None,
};
for (index, line) in body.lines.iter().enumerate() {
let runs = &body.runs[line.runs.range()];
for (run, glyph_run) in runs.iter().zip(body.glyph_runs(index)) {
past.add_run(body, (line, run), &glyph_run);
}
}
past.ink
}
struct PastInk {
lines: (f32, f32),
height: f32,
glyphs_leave: bool,
ink: Option<Rect>,
}
impl PastInk {
fn add(&mut self, rect: Rect) {
let (left, right) = self.lines;
if rect.x < left
|| rect.x + rect.width > right
|| rect.y < 0.0
|| rect.y + rect.height > self.height
{
self.ink = Some(self.ink.map_or(rect, |ink| ink.union(rect)));
}
}
fn add_run(
&mut self,
body: &LayoutBody,
(line, run): (&LayoutLine, &LineRun),
glyph_run: &GlyphRun<'_>,
) {
let face = glyph_run.typeface;
let inks = face.glyph_inks();
let scale = glyph_run.size / f32::from(face.units_per_em());
let baseline = line.baseline - run.baseline_shift;
let cuts = |(top, bottom): (f32, f32)| {
baseline - top * scale < 0.0 || baseline - bottom * scale > body.height
};
let short = cuts((inks.ascent, inks.descent));
let moved = moves_glyphs(body, run);
if short && !moved && cuts(inks.band) {
let (top, bottom) = inks.band;
let (left, right) = self.lines;
self.add(Rect {
x: left,
y: baseline - top * scale,
width: right - left,
height: (top - bottom) * scale,
});
}
let traced = |state: u8| moved || state == OUT || (short && state == LOOSE);
if moved
|| (self.glyphs_leave
&& any_traced(body.glyphs.run(run.glyphs.range()), (face, inks), traced))
{
self.add_outlines(glyph_run, (inks, scale), traced);
}
}
fn add_outlines(
&mut self,
glyph_run: &GlyphRun<'_>,
(inks, scale): (&GlyphInks, f32),
traced: impl Fn(u8) -> bool,
) {
let face = glyph_run.typeface;
for glyph in glyph_run.positions() {
if !traced(inks.state(face, glyph.id)) {
continue;
}
let Some(extents) = face.glyph_extents(glyph.id) else {
continue;
};
if extents.width <= 0.0 || extents.height <= 0.0 {
continue;
}
self.add(Rect {
x: glyph.x + extents.x_bearing * scale,
y: glyph.y - extents.y_bearing * scale,
width: extents.width * scale,
height: extents.height * scale,
});
}
}
}
fn moves_glyphs(body: &LayoutBody, run: &LineRun) -> bool {
run.spacing < 0.0
|| body
.glyphs
.offsets(run.glyphs.range())
.is_some_and(|offsets| offsets.iter().any(|offset| offset.x != 0 || offset.y != 0))
}
fn any_traced(
glyphs: RunGlyphs<'_>,
(face, inks): (&Typeface, &GlyphInks),
traced: impl Fn(u8) -> bool,
) -> bool {
match glyphs {
RunGlyphs::Packed(glyphs) => glyphs
.iter()
.any(|glyph| traced(inks.state(face, glyph.id()))),
RunGlyphs::Full(glyphs) => glyphs
.iter()
.any(|glyph| traced(inks.state(face, glyph.id()))),
}
}
pub(crate) fn may_ink_past_lines(body: &LayoutBody, reach: InkReach) -> bool {
reach != InkReach::Within || body.glyphs.has_offsets()
}