use super::outline::OutlineFont;
use super::{BfString, Glyph, OutlineGlyph};
use kurbo::{Affine, Shape};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
#[inline]
pub(crate) fn is_combining_mark(c: char) -> bool {
if c < '\u{0300}' {
return false;
}
matches!(
c,
'\u{0300}'..='\u{036F}'
| '\u{1AB0}'..='\u{1AFF}'
| '\u{1DC0}'..='\u{1DFF}'
| '\u{20D0}'..='\u{20FF}'
| '\u{FE20}'..='\u{FE2F}'
)
}
thread_local! {
static MARK_ROWS_SET_ASIDE: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn mark_rows_set_aside() -> bool {
MARK_ROWS_SET_ASIDE.get()
}
struct RestoreMarkRows(bool);
impl Drop for RestoreMarkRows {
fn drop(&mut self) {
MARK_ROWS_SET_ASIDE.set(self.0);
}
}
pub(crate) fn without_mark_rows<T>(read: impl FnOnce() -> T) -> T {
let _restore = RestoreMarkRows(MARK_ROWS_SET_ASIDE.replace(true));
read()
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct NamedEntry {
pub(crate) text: BfString,
pub(crate) from_mark_row: bool,
}
pub(crate) fn table_entry(entry: &Option<NamedEntry>) -> Option<BfString> {
match entry {
Some(entry) if !(entry.from_mark_row && mark_rows_set_aside()) => Some(entry.text.clone()),
_ => None,
}
}
#[inline]
pub(crate) fn may_be_row_mark(text: &Option<BfString>) -> bool {
lone_combining_mark(text).is_some()
}
#[inline]
fn lone_combining_mark(text: &Option<BfString>) -> Option<char> {
let mark = match text {
Some(BfString::Char(c)) => *c,
Some(BfString::String(s)) => {
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => c,
_ => return None,
}
}
None => return None,
};
is_combining_mark(mark).then_some(mark)
}
pub(crate) fn row_mark_of(
text: &Option<BfString>,
again: impl FnOnce() -> Option<BfString>,
) -> Option<char> {
let mark = lone_combining_mark(text)?;
(without_mark_rows(again) != *text).then_some(mark)
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Side {
Base,
Mark,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum State {
Offered,
Taken,
Refused,
}
#[derive(Debug)]
struct Placement {
mark: char,
taker: Side,
state: State,
}
impl Placement {
fn read_by(&mut self, side: Side) -> State {
if self.state == State::Offered {
self.state = if side == self.taker {
State::Taken
} else {
State::Refused
};
}
self.state
}
}
#[derive(Default)]
struct NumberHasher(u64);
impl Hasher for NumberHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.0 = (self.0.rotate_left(8) ^ u64::from(byte)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
}
fn write_u64(&mut self, number: u64) {
self.0 = number.wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
}
type Numbered<V> = HashMap<u64, V, BuildHasherDefault<NumberHasher>>;
#[derive(Debug, Default)]
struct MarkLog {
placements: Vec<Placement>,
by_base: Numbered<Vec<usize>>,
by_mark: Numbered<usize>,
rereads: Numbered<(u64, Candidate)>,
}
thread_local! {
static MARK_LOGS: RefCell<Numbered<MarkLog>> = RefCell::new(HashMap::default());
}
static NEXT_LOG: AtomicU64 = AtomicU64::new(0);
static PLACEMENTS_HELD: AtomicUsize = AtomicUsize::new(0);
#[inline]
pub(crate) fn placements_held() -> bool {
PLACEMENTS_HELD.load(Ordering::Relaxed) > 0
}
#[cfg(test)]
fn records_held() -> usize {
MARK_LOGS.with(|logs| logs.borrow().len())
}
#[cfg(test)]
thread_local! {
static RECORD_LOOKUPS: Cell<usize> = const { Cell::new(0) };
}
fn with_log<T>(log: u64, f: impl FnOnce(&mut MarkLog) -> T) -> Option<T> {
#[cfg(test)]
RECORD_LOOKUPS.set(RECORD_LOOKUPS.get() + 1);
MARK_LOGS
.try_with(|logs| logs.borrow_mut().get_mut(&log).map(f))
.ok()
.flatten()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct MarkSite {
log: u64,
number: u64,
}
impl MarkSite {
pub(crate) fn base_marks(&self) -> String {
if !placements_held() {
return String::new();
}
with_log(self.log, |log| {
let Some(offered) = log.by_base.get(&self.number) else {
return String::new();
};
let mut marks = String::new();
for &index in offered {
let placement = &mut log.placements[index];
if placement.read_by(Side::Base) == State::Taken {
marks.push(placement.mark);
}
}
marks
})
.unwrap_or_default()
}
pub(crate) fn reread_previous(&self, previous: MarkSite) -> Option<Option<BfString>> {
if !placements_held() || previous.log != self.log {
return None;
}
let glyph = with_log(self.log, |log| {
log.rereads
.get(&self.number)
.filter(|(number, _)| *number == previous.number)
.map(|(_, glyph)| glyph.clone())
})
.flatten()?;
Some(glyph.glyph(previous).as_unicode())
}
pub(crate) fn mark_taken(&self) -> bool {
if !placements_held() {
return false;
}
with_log(self.log, |log| {
let index = *log.by_mark.get(&self.number)?;
Some(log.placements[index].read_by(Side::Mark) == State::Taken)
})
.flatten()
.unwrap_or(false)
}
}
#[derive(Clone, Copy, Debug)]
struct Spot {
number: u64,
left: f64,
right: f64,
baseline: f64,
size: f64,
}
impl Spot {
fn of(glyph: &OutlineGlyph, number: u64, placed: Affine, ink: bool) -> Option<Self> {
let outline = || glyph.outline();
Self::boxed(
number,
placed,
|| glyph.advance_width(),
ink.then_some(&outline as &dyn Fn() -> kurbo::BezPath),
)
}
fn boxed(
number: u64,
placed: Affine,
advance: impl FnOnce() -> Option<f32>,
ink: Option<&dyn Fn() -> kurbo::BezPath>,
) -> Option<Self> {
let [a, b, c, d, x, y] = placed.as_coeffs();
let upright = a > 0.0 && d > 0.0 && b.abs() <= a * 1e-6 && c.abs() <= d * 1e-6;
if !upright {
return None;
}
let size = a * 1000.0;
let advance = f64::from(advance().unwrap_or(0.0)) / 1000.0 * size;
let (mut left, mut right) = (x, x + advance.max(0.0));
if let Some(outline) = ink {
let drawn = (placed * outline()).bounding_box();
if drawn.width() > 0.0 && drawn.x0.is_finite() && drawn.x1.is_finite() {
left = left.min(drawn.x0);
right = right.max(drawn.x1);
}
}
(right > left).then_some(Self {
number,
left,
right,
baseline: y,
size,
})
}
fn holds_centre_of(&self, base: &Spot) -> bool {
let centre = (base.left + base.right) * 0.5;
let same_line = (self.baseline - base.baseline).abs() <= self.size.max(base.size) * 0.35;
same_line && self.left <= centre && centre <= self.right
}
}
#[derive(Clone, Debug)]
struct Candidate {
number: u64,
placed: Affine,
font: OutlineFont,
char_code: u32,
}
fn has_own_text(glyph: &OutlineGlyph) -> bool {
glyph.marks.is_some()
&& match glyph.cached_text() {
Some(BfString::Char(c)) => !c.is_whitespace(),
Some(BfString::String(s)) => !s.trim().is_empty(),
None => false,
}
}
impl Candidate {
fn of(glyph: &OutlineGlyph, placed: Affine) -> Option<Self> {
let site = glyph.marks?;
has_own_text(glyph).then(|| Self {
number: site.number,
placed,
font: glyph.font.clone(),
char_code: glyph.char_code,
})
}
fn refill(&mut self, glyph: &OutlineGlyph, number: u64, placed: Affine) {
self.number = number;
self.placed = placed;
if !self.font.is_same(&glyph.font) {
self.font = glyph.font.clone();
}
self.char_code = glyph.char_code;
}
fn glyph(&self, site: MarkSite) -> OutlineGlyph {
OutlineGlyph {
id: skrifa::GlyphId::NOTDEF,
font: self.font.clone(),
char_code: self.char_code,
resource_name: None,
marks: Some(site),
text: self.font.char_code_to_unicode(self.char_code),
}
}
fn spot(&self) -> Option<Spot> {
Spot::boxed(
self.number,
self.placed,
|| self.font.glyph_advance_width(self.char_code),
None,
)
}
}
#[derive(Debug)]
enum LastBase {
Placed(Spot),
Unplaced(Candidate),
}
impl LastBase {
fn spot(self) -> Option<Spot> {
match self {
Self::Placed(spot) => Some(spot),
Self::Unplaced(candidate) => candidate.spot(),
}
}
}
#[derive(Clone, Debug)]
enum Seen {
Mark(Spot, char),
Base(Spot),
Candidate(Candidate),
Other,
}
impl Seen {
fn of(glyph: &Glyph<'_>, placed: Affine) -> Self {
let Glyph::Outline(outline) = glyph else {
return Self::Other;
};
let Some(site) = outline.marks else {
return Self::Other;
};
let text = outline.cached_text();
if let Some(mark) = row_mark_of(text, || outline.font_text()) {
return Spot::of(outline, site.number, placed, true)
.map_or(Self::Other, |spot| Self::Mark(spot, mark));
}
Candidate::of(outline, placed).map_or(Self::Other, Self::Candidate)
}
}
#[derive(Debug, Default)]
pub(crate) struct MarkPlacement {
log: Option<u64>,
next: u64,
base: Option<LastBase>,
pending: Option<(Spot, char)>,
held: Option<Candidate>,
}
impl Drop for MarkPlacement {
fn drop(&mut self) {
let Some(log) = self.log else {
return;
};
let removed = MARK_LOGS
.try_with(|logs| logs.borrow_mut().remove(&log))
.ok()
.flatten();
if let Some(log) = removed {
PLACEMENTS_HELD.fetch_sub(log.placements.len(), Ordering::Relaxed);
}
}
}
impl MarkPlacement {
pub(crate) fn site(&mut self) -> MarkSite {
let log = *self.log.get_or_insert_with(|| {
let log = NEXT_LOG.fetch_add(1, Ordering::Relaxed);
MARK_LOGS.with(|logs| logs.borrow_mut().insert(log, MarkLog::default()));
log
});
let number = self.next;
self.next += 1;
MarkSite { log, number }
}
#[inline]
pub(crate) fn observe(&mut self, glyph: &Glyph<'_>, placed: Affine) {
if self.pending.is_none()
&& let Glyph::Outline(outline) = glyph
&& !may_be_row_mark(outline.cached_text())
{
match (&mut self.base, outline.marks) {
(Some(LastBase::Unplaced(held)), Some(site)) if has_own_text(outline) => {
held.refill(outline, site.number, placed);
}
(base, _) => *base = Candidate::of(outline, placed).map(LastBase::Unplaced),
}
return;
}
let seen = Seen::of(glyph, placed);
let this = match (&seen, glyph) {
(Seen::Mark(..) | Seen::Candidate(_), Glyph::Outline(outline)) => Some(Candidate {
number: outline.marks.map_or(0, |site| site.number),
placed,
font: outline.font.clone(),
char_code: outline.char_code,
}),
_ => None,
};
self.step_with(seen, this);
}
#[cfg(test)]
fn step(&mut self, seen: Seen) {
self.step_with(seen, None);
}
fn step_with(&mut self, seen: Seen, this: Option<Candidate>) {
let pending = self.pending.take();
let base = self.base.take();
let held = self.held.take();
let seen = match seen {
Seen::Candidate(candidate) if pending.is_none() => {
self.base = Some(LastBase::Unplaced(candidate));
return;
}
Seen::Candidate(candidate) => candidate.spot().map_or(Seen::Other, Seen::Base),
seen => seen,
};
match seen {
Seen::Other | Seen::Candidate(_) => {}
Seen::Mark(spot, mark) => {
let before = match &base {
Some(LastBase::Unplaced(candidate)) => Some(candidate.clone()),
_ => held,
};
match base.and_then(LastBase::spot) {
Some(base) if spot.holds_centre_of(&base) => {
self.offer(mark, spot.number, base.number, Side::Base);
self.note_reread(spot.number, base.number, before);
}
_ => {
self.pending = Some((spot, mark));
self.held = this;
}
}
}
Seen::Base(spot) => {
if let Some((mark_spot, mark)) = pending
&& mark_spot.holds_centre_of(&spot)
{
self.offer(mark, mark_spot.number, spot.number, Side::Mark);
self.note_reread(spot.number, mark_spot.number, held);
}
self.base = Some(LastBase::Placed(spot));
self.held = this;
}
}
}
fn note_reread(&self, at: u64, previous: u64, glyph: Option<Candidate>) {
let (Some(log), Some(glyph)) = (self.log, glyph) else {
return;
};
with_log(log, |log| log.rereads.insert(at, (previous, glyph)));
}
fn offer(&self, mark: char, mark_glyph: u64, base: u64, taker: Side) {
let Some(log) = self.log else {
return;
};
with_log(log, |log| {
let index = log.placements.len();
log.placements.push(Placement {
mark,
taker,
state: State::Offered,
});
log.by_base.entry(base).or_default().push(index);
log.by_mark.insert(mark_glyph, index);
PLACEMENTS_HELD.fetch_add(1, Ordering::Relaxed);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
fn placements_alone() -> MutexGuard<'static, ()> {
static TURN: Mutex<()> = Mutex::new(());
TURN.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn a_row_mark_is_one_combining_mark_the_rows_gave() {
let circle = Some(BfString::Char('\u{20DD}'));
let from_row = || {
if mark_rows_set_aside() {
None
} else {
circle.clone()
}
};
assert_eq!(row_mark_of(&circle, from_row), Some('\u{20DD}'));
assert_eq!(row_mark_of(&circle, || circle.clone()), None);
for value in ["\u{FFFF}", "\u{FFFF}\u{20DD}"] {
let text = Some(BfString::String(value.into()));
assert_eq!(row_mark_of(&text, || None), None, "{value:?}");
}
assert_eq!(row_mark_of(&Some(BfString::Char('A')), || None), None);
assert_eq!(row_mark_of(&None, || None), None);
}
#[test]
fn a_panic_inside_the_set_aside_read_puts_the_rows_back() {
assert!(!mark_rows_set_aside());
let outcome = std::panic::catch_unwind(|| {
without_mark_rows(|| {
assert!(mark_rows_set_aside());
panic!("inside the read");
})
});
assert!(outcome.is_err());
assert!(!mark_rows_set_aside());
without_mark_rows(|| {
without_mark_rows(|| ());
assert!(mark_rows_set_aside());
});
assert!(!mark_rows_set_aside());
}
fn spot(left: f64, right: f64, baseline: f64) -> Spot {
Spot {
number: 0,
left,
right,
baseline,
size: 10.0,
}
}
#[test]
fn a_mark_holds_a_centre_inside_its_box_on_its_line() {
let circle = spot(100.0, 110.0, 700.0);
assert!(circle.holds_centre_of(&spot(102.8, 107.8, 700.0)));
assert!(!circle.holds_centre_of(&spot(110.0, 115.0, 700.0)));
assert!(!circle.holds_centre_of(&spot(94.0, 99.0, 700.0)));
assert!(!circle.holds_centre_of(&spot(102.8, 107.8, 712.0)));
}
#[test]
fn an_offer_is_decided_by_the_side_read_first() {
let _turn = placements_alone();
let mut placement = MarkPlacement::default();
let base = placement.site();
let mark = placement.site();
placement.offer('\u{20DD}', mark.number, base.number, Side::Base);
assert_eq!(base.base_marks(), "\u{20DD}");
assert!(mark.mark_taken());
assert_eq!(base.base_marks(), "\u{20DD}");
let base = placement.site();
let mark = placement.site();
placement.offer('\u{20DD}', mark.number, base.number, Side::Base);
assert!(!mark.mark_taken());
assert_eq!(base.base_marks(), "");
assert!(!mark.mark_taken());
}
fn at(number: u64, left: f64) -> Spot {
Spot {
number,
left,
right: left + 10.0,
baseline: 700.0,
size: 10.0,
}
}
#[test]
fn a_mark_is_offered_only_to_the_glyph_next_to_it() {
let _turn = placements_alone();
let mut placement = MarkPlacement::default();
let c = placement.site();
let first = placement.site();
let second = placement.site();
placement.step(Seen::Base(at(c.number, 101.0)));
placement.step(Seen::Mark(at(first.number, 100.0), '\u{20DD}'));
placement.step(Seen::Mark(at(second.number, 100.0), '\u{20DD}'));
assert_eq!(c.base_marks(), "\u{20DD}");
assert!(first.mark_taken());
assert!(!second.mark_taken());
let slash = placement.site();
let equals = placement.site();
placement.step(Seen::Mark(at(slash.number, 200.0), '\u{0338}'));
placement.step(Seen::Other);
placement.step(Seen::Base(at(equals.number, 201.0)));
assert!(!slash.mark_taken());
assert_eq!(equals.base_marks(), "");
}
#[test]
fn the_registry_is_empty_after_an_interpretation_and_after_a_panic() {
let before = records_held();
{
let mut placement = MarkPlacement::default();
let _ = placement.site();
assert_eq!(records_held(), before + 1);
}
assert_eq!(records_held(), before);
let outcome = std::panic::catch_unwind(|| {
let mut placement = MarkPlacement::default();
let _ = placement.site();
assert_eq!(records_held(), before + 1);
panic!("inside the interpretation");
});
assert!(outcome.is_err());
assert_eq!(records_held(), before);
}
#[test]
fn a_page_without_a_placement_never_looks_a_record_up() {
let _turn = placements_alone();
let before = records_held();
let empty = MarkPlacement::default();
assert_eq!(records_held(), before, "no glyph, no record");
drop(empty);
let mut placement = MarkPlacement::default();
let sites: Vec<MarkSite> = (0..64).map(|_| placement.site()).collect();
for (i, site) in sites.iter().enumerate() {
placement.step(Seen::Base(at(site.number, 10.0 * i as f64)));
}
let lookups = RECORD_LOOKUPS.get();
for site in &sites {
assert_eq!(site.base_marks(), "");
assert!(!site.mark_taken());
}
assert_eq!(RECORD_LOOKUPS.get(), lookups, "no placement, no lookup");
let base = placement.site();
let mark = placement.site();
placement.offer('\u{20DD}', mark.number, base.number, Side::Base);
assert_eq!(base.base_marks(), "\u{20DD}");
assert!(RECORD_LOOKUPS.get() > lookups, "a placement is looked up");
}
#[test]
fn a_glyph_on_a_page_without_an_offer_changes_nothing_before_it() {
let _turn = placements_alone();
let mut placement = MarkPlacement::default();
let sites: Vec<MarkSite> = (0..64).map(|_| placement.site()).collect();
let lookups = RECORD_LOOKUPS.get();
for pair in sites.windows(2) {
assert_eq!(pair[1].reread_previous(pair[0]), None);
}
assert_eq!(RECORD_LOOKUPS.get(), lookups, "no offer, no lookup");
}
#[test]
fn the_record_goes_with_its_interpretation() {
let _turn = placements_alone();
let mut placement = MarkPlacement::default();
let base = placement.site();
let mark = placement.site();
placement.offer('\u{0338}', mark.number, base.number, Side::Mark);
drop(placement);
assert!(!mark.mark_taken());
assert_eq!(base.base_marks(), "");
}
}