mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
Text: combining marks that start a run are shaped with their base letter
A run that begins with combining marks, after a run that differs only in OpenType features (ligatures, kerning, OTF features), now gives those marks to the run before it. The shaper attaches each mark to its base, and a missing font's fallback draws the mark in the same face as its letter instead of the substitute's missing-glyph box. Runs in another font, size or colour keep their marks.
This commit is contained in:
@@ -258,6 +258,7 @@ pub(crate) fn shape_para(
|
||||
resolved.push((a, b, props, fmt, para_chars.clone()));
|
||||
}
|
||||
}
|
||||
let resolved = marks_join_their_base(&story.text, resolved);
|
||||
for (a, b, props, fmt, resolved_base) in resolved {
|
||||
let style = table.intern(db, &props);
|
||||
// A missing font's substitute stands in for the whole font: fallback fonts help it.
|
||||
@@ -320,6 +321,38 @@ pub(crate) fn shape_para(
|
||||
ShapedPara { glyphs, range }
|
||||
}
|
||||
|
||||
/// Combining marks that start a run are shaped with the letter before them when the two runs
|
||||
/// differ only in OpenType features (ligatures, kerning, `otf_features`): the shaper attaches each
|
||||
/// mark to its base, in its base's face. A run in another font, size or colour keeps its marks.
|
||||
fn marks_join_their_base<'f>(
|
||||
text: &str,
|
||||
resolved: Vec<(usize, usize, CharProps, &'f designcraft_doc::CharFormat, CharProps)>,
|
||||
) -> Vec<(usize, usize, CharProps, &'f designcraft_doc::CharFormat, CharProps)> {
|
||||
let same_but_features = |a: &CharProps, b: &CharProps| {
|
||||
*b == CharProps { ligatures: b.ligatures, kerning: b.kerning, otf_features: b.otf_features.clone(), ..a.clone() }
|
||||
};
|
||||
let is_base = |c: char| !c.is_whitespace() && !c.is_control() && !matches!(c, designcraft_doc::FOOTNOTE_REF | designcraft_doc::ENDNOTE_REF);
|
||||
let mut out: Vec<(usize, usize, CharProps, &designcraft_doc::CharFormat, CharProps)> = Vec::with_capacity(resolved.len());
|
||||
for (a, b, props, fmt, base) in resolved {
|
||||
let run = text.get(a..b).unwrap_or("");
|
||||
let marks = run.char_indices().find(|(_, c)| !is_mark(*c)).map_or(run.len(), |(i, _)| i);
|
||||
if let Some(prev) = out.last_mut()
|
||||
&& marks > 0
|
||||
&& prev.1 == a
|
||||
&& text.get(prev.0..prev.1).and_then(|s| s.chars().next_back()).is_some_and(is_base)
|
||||
&& same_but_features(&prev.2, &props)
|
||||
{
|
||||
prev.1 = a + marks;
|
||||
if a + marks < b {
|
||||
out.push((a + marks, b, props, fmt, base));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push((a, b, props, fmt, base));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Tate-chu-yoko: in vertical text each run of marked glyphs takes one em along the line, its
|
||||
/// glyphs set side by side across it; elsewhere the mark is dropped.
|
||||
pub(crate) fn collapse_tcy(glyphs: &mut [Glyph], vertical: bool) {
|
||||
|
||||
@@ -1840,6 +1840,40 @@ fn missing_glyphs_are_the_fonts_box_unless_fallback_is_on() {
|
||||
assert_ne!(gid, 0, "a fallback font draws it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mark_starting_a_run_is_shaped_with_its_base() {
|
||||
use designcraft_fonts::testing::font_with;
|
||||
const HEBREW: &str = "DC Test Hebrew Points";
|
||||
designcraft_fonts::FontDb::global().add_font(font_with(HEBREW, &['ש', '\u{5C2}', 'ל']).unwrap());
|
||||
// Shin, then sin dot and lamed in a run that only turns ligatures off.
|
||||
let text = "ש\u{5C2}ל";
|
||||
let mark = "ש".len();
|
||||
let glyphs = |family: &str, mark_family: Option<&str>| {
|
||||
let (mut d, sid, _) = doc_with(text, Rect::new(36.0, 36.0, 300.0, 100.0), ParaAttrs::default());
|
||||
let story = d.story_mut(sid).unwrap();
|
||||
story.format_chars(0..text.len(), |f| f.over.font_family = Some(family.into()));
|
||||
story.format_chars(mark..text.len(), |f| {
|
||||
f.over.ligatures = Some(false);
|
||||
if let Some(m) = mark_family {
|
||||
f.over.font_family = Some(m.into());
|
||||
}
|
||||
});
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
all_lines(&cs).iter().flat_map(|l| l.glyphs.iter()).map(|g| (g.face, g.gid, g.byte)).collect::<Vec<_>>()
|
||||
};
|
||||
// A missing font: its substitute lacks both, so the letter comes from a fallback font, and the
|
||||
// mark from the same font, in its letter's cluster.
|
||||
let g = glyphs("DC Test No Such Family", None);
|
||||
let (base, dot) = (g[0], g[1]);
|
||||
assert_ne!(base.1, 0, "{g:?}");
|
||||
assert_eq!((dot.0.family.as_str(), dot.2), (base.0.family.as_str(), 0), "the sin dot goes with its shin: {g:?}");
|
||||
assert_ne!(dot.1, 0, "{g:?}");
|
||||
// The run's own font lacks the mark: its box, whatever font the letter before it has.
|
||||
let g = glyphs(HEBREW, Some(designcraft_fonts::DEFAULT_FAMILY));
|
||||
let dot = g.iter().find(|x| x.2 == mark).unwrap();
|
||||
assert_eq!((dot.0.family.as_str(), dot.1), (designcraft_fonts::DEFAULT_FAMILY, 0), "{g:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kenten_missing_from_the_font_are_its_box_unless_fallback_is_on() {
|
||||
use designcraft_fonts::testing::font_with;
|
||||
|
||||
Reference in New Issue
Block a user