Merge pull request #5592 from dotCooCoo/fix/setstyle-warning-skips-protected

Type: the font warning leaves out characters Missing Glyph Protection kept
This commit is contained in:
BFlat
2026-11-04 19:39:40 +07:00
committed by GitHub
3 changed files with 59 additions and 13 deletions
+30 -7
View File
@@ -930,8 +930,8 @@ impl CharChange {
/// Type › Enable Missing Glyph Protection: after a font change, the characters of `runs` their new
/// font has no glyph for, but their font `before` the change had, keep that font: its family,
/// style, version and Variable Font settings.
pub(crate) fn protect_missing_glyphs(before: &[TextRun], runs: &mut Vec<TextRun>) {
/// style, version and Variable Font settings. Returns the byte ranges that kept their font, in order.
pub(crate) fn protect_missing_glyphs(before: &[TextRun], runs: &mut Vec<TextRun>) -> Vec<(usize, usize)> {
fn chars(runs: &[TextRun]) -> impl Iterator<Item = (usize, char, &CharStyle)> {
runs.iter()
.scan(0, |at, r| {
@@ -963,6 +963,7 @@ pub(crate) fn protect_missing_glyphs(before: &[TextRun], runs: &mut Vec<TextRun>
_ => keep.push((at, end, old)),
}
}
let kept = keep.iter().map(|(a, b, _)| (*a, *b)).collect();
for (a, b, old) in keep {
edit::style_range(runs, a, b, |st| {
st.font_family.clone_from(&old.font_family);
@@ -971,6 +972,30 @@ pub(crate) fn protect_missing_glyphs(before: &[TextRun], runs: &mut Vec<TextRun>
st.font_axes.clone_from(&old.font_axes);
});
}
kept
}
/// The runs of `runs` from byte `a` to `b`, leaving out the ranges `kept` (in order) that Missing
/// Glyph Protection gave their earlier font: the characters a font command set its font on.
pub(crate) fn unprotected_runs(runs: &[TextRun], a: usize, b: usize, kept: &[(usize, usize)]) -> Vec<TextRun> {
let mut out = vec![];
let mut at = a;
for &(k0, k1) in kept {
if k1 <= at {
continue;
}
if k0 >= b {
break;
}
if k0 > at {
out.extend(edit::slice_runs(runs, at, k0));
}
at = k1;
}
if at < b {
out.extend(edit::slice_runs(runs, at, b));
}
out
}
fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1021,12 +1046,10 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
}
let before = protect.then(|| t.runs.clone());
edit::style_range(&mut t.runs, a, b, |st| change.apply(st));
if let Some(before) = before {
protect_missing_glyphs(&before, &mut t.runs);
}
let kept = before.map(|before| protect_missing_glyphs(&before, &mut t.runs)).unwrap_or_default();
if sets_font {
// Empty text has one run, which the empty range styles.
let styled = if a == b { t.runs.clone() } else { edit::slice_runs(&t.runs, a, b) };
// Empty text has one run, which the empty range styles. Characters that kept their font aren't the command's.
let styled = if a == b { t.runs.clone() } else { unprotected_runs(&t.runs, a, b, &kept) };
set.extend(styled.iter().map(|r| super::fonts::font_of(&r.style)));
}
refresh_bounds(t);
+6 -6
View File
@@ -874,16 +874,16 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
st.features = super::textedit::change_features(&st.features, c);
}
});
if let Some(before) = before {
super::textedit::protect_missing_glyphs(&before, &mut t.runs);
}
let kept = before.map(|before| super::textedit::protect_missing_glyphs(&before, &mut t.runs)).unwrap_or_default();
if font.is_some() || style.is_some() {
// All the text: every run, the one run of empty text included.
// All the text: every run, the one run of empty text included. Characters that kept their font aren't the
// command's.
let styled = match range {
None => t.runs.clone(),
None if kept.is_empty() => t.runs.clone(),
None => super::textedit::unprotected_runs(&t.runs, 0, vectorcraft_text::edit::runs_len(&t.runs), &kept),
Some(r) => {
let (a, b) = r.bytes(t);
vectorcraft_text::edit::slice_runs(&t.runs, a, b)
super::textedit::unprotected_runs(&t.runs, a, b, &kept)
}
};
set.extend(styled.iter().map(|r| super::fonts::font_of(&r.style)));
+23
View File
@@ -1474,6 +1474,29 @@ fn missing_glyph_protection_keeps_the_version_of_the_font_kept() {
assert_eq!(versions, [moved.clone(), kept, moved]);
}
/// The font warning of `text.setStyle` and `text.setRangeStyle` names only the fonts the command
/// set: a character that keeps an unavailable font through Missing Glyph Protection isn't reported.
#[test]
fn missing_glyph_protection_leaves_the_font_kept_out_of_the_warning() {
let (from, to, c) = ("No Such Font Family", "JetBrains Mono", '\u{180}');
let db = vectorcraft_text::FontDb::global();
// The unavailable family is drawn in the fallback font, which has the character; the new font is installed and lacks it.
let installed = db.resolve(to, "Regular").is_some_and(|(_, m)| m == vectorcraft_text::FontMatch::Exact);
if !installed || !db.face(from, "Regular").is_some_and(|f| f.covers(c)) || db.face(to, "Regular").is_some_and(|f| f.covers(c)) {
return; // The fonts installed here cover other characters.
}
let mut s = new_doc();
let id = NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": "a\u{180}c", "font": from})).unwrap()["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
for change in [json!({"font": to}), json!({"id": id.0, "start": 0, "end": 4, "font": to})] {
let cmd = if change.get("id").is_some() { "text.setRangeStyle" } else { "text.setStyle" };
let out = s.execute(cmd, &change).unwrap();
assert_eq!(style_at(&s, id, 1).font_family, from, "{cmd}: the character keeps its font");
assert_eq!(out.get("warning"), None, "{cmd}: {out}");
s.execute("edit.undo", &json!({})).unwrap();
}
}
// ---------- #394, fourth batch ----------
/// Type › Type Object Selection by Path Only (#394): on, a click among point type's glyphs no