Font dialog: separate Asian and Latin text fonts (#505) (#536)

* Font list: installed Chinese fonts are listed, by their Chinese names in a Chinese interface (#505)

The font database read only the English family name of each font, so a
Chinese interface listed SimSun, Microsoft YaHei and WenQuanYi Zen Hei
by their English names and never as 宋体, 微软雅黑 or 文泉驿正黑, and a
document naming a font by its Chinese name fell back to another font.

- fonts: keep each face's localized family names (name IDs 16 and 1,
  every face of a collection, read during the scan without loading the
  file); FontDb::families_in(lang) lists them in place of the English
  names for a matching interface language (zh-hans, zh-hant, ja, ko);
  face, has_family and styles accept either name.
- ui: the ribbon and mini toolbar font boxes show the interface
  language's names and store the English name.
- East Asian font: choosing a Chinese, Japanese or Korean font also sets
  it as the East Asian font (w:eastAsia), read and written in DOCX;
  layout draws CJK text in a run's installed East Asian font.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Font dialog: separate Asian and Latin text fonts (#505)

Word's Font dialog sets the font of Chinese, Japanese and Korean text
apart from the font of Latin text; WordCraft's had one font box.

- Font dialog: an "Asian text font" box (the installed Chinese, Japanese
  and Korean fonts, by their interface-language names, type to find)
  above the "Latin text font" box, shown for a Chinese or Japanese
  interface, East Asian text in the selection or a selection with an
  Asian font. OK sets only the fonts changed, in one undo step.
- format.font takes "eastAsia" (w:eastAsia) besides "name"; either or
  both; format.state reports fontEastAsia.
- fonts: the scan reads each font's OS/2 code pages, so
  FontDb::east_asian_families lists CJK fonts without loading them.
- Tests: dialog sets each font on its own; layout sets CJK text in the
  run's Asian font and Latin text in its Latin font (skipped without an
  installed CJK font); command and undo; the scan finds a CJK font.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Valentin Klein 2026-10-12 03:42:56 +02:00 committed by GitHub
parent 60cd1bf568
commit dab5092e83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 307 additions and 30 deletions

View File

@ -149,6 +149,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-11 | Line Numbers, Hyphenation (zone, CAPS, consecutive limit, Manual), Language, Envelopes and Labels dialogs (#407); hyphenation options kept in DOCX; 30 modal dialogs |
| 2026-10-11 | Accessibility (#386): Alt Text pane for every object and table, Mark as decorative, alt text round-trips in DOCX, deeper checker with fix actions |
| 2026-10-11 | References dialogs (#402): Caption (labels, chapter numbering), Index and Mark Entry, Custom Table of Contents, Source Manager and Create/Edit Source, with TOC/INDEX/XE/SEQ switches honoured on update |
| 2026-10-11 | Font dialog: separate Asian text font and Latin text font (#505); `format.font {"eastAsia"}`; East Asian fonts found by the font scan (OS/2 code pages) |
| 2026-10-11 | Document grid (#391): Page Setup › Document Grid sets lines per page and characters per line (`w:docGrid` round trip); lines snap to the line pitch, East Asian characters to the character pitch; View › Gridlines shows it |
| 2026-10-11 | Move tracking (#400): `w:moveFrom`/`w:moveTo` and their named ranges kept in DOCX, a tracked cut and paste of a paragraph or sentence records a move, accept/reject at either end resolves both, green double strikethrough/underline and "Moved from/to" balloons |
| 2026-10-11 | Footnote and Endnote dialog from the References › Footnotes launcher: options per section or document (below text, end of section, number format, start at, restart each section or page), custom marks, convert footnotes ↔ endnotes; kept in DOCX (#385) |

View File

@ -95,7 +95,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("format.shadow", "Shadow", "Home › Font › Text Effects", |s, v| toggle(s, v, |r| r.shadow, |c, on| c.shadow = Some(on))),
CommandSpec::new("format.emboss", "Emboss", "Home › Font › Font", |s, v| toggle(s, v, |r| r.emboss, |c, on| c.emboss = Some(on))),
CommandSpec::new("format.engrave", "Engrave", "Home › Font › Font", |s, v| toggle(s, v, |r| r.engrave, |c, on| c.engrave = Some(on))),
CommandSpec::new("format.font", "Font", "Home › Font", font).key("Mod+Shift+F").params(r#"{"name": string}"#),
CommandSpec::new("format.font", "Font", "Home › Font", font).key("Mod+Shift+F").params(r#"{"name"?: string, "eastAsia"?: string}"#),
CommandSpec::new("format.size", "Font Size", "Home › Font", size).key("Mod+Shift+P").params(r#"{"size": number}"#),
CommandSpec::new("format.growFont", "Increase Font Size", "Home › Font", |s, _| step_size(s, 1)).key("Mod+Shift+. / Mod+Shift+>"),
CommandSpec::new("format.shrinkFont", "Decrease Font Size", "Home › Font", |s, _| step_size(s, -1)).key("Mod+Shift+, / Mod+Shift+<"),
@ -275,23 +275,37 @@ fn underline(s: &mut Session, v: &Value) -> CmdResult {
apply(s, &|c| c.underline = Some(u))
}
/// `format.font`: `name` is the font (Latin text, and Persian/Arabic text too); `eastAsia` sets the
/// East Asian text's font on its own, as the Font dialog's Asian text font box does (#505).
fn font(s: &mut Session, v: &Value) -> CmdResult {
let name = p::req_str(v, "name")?.trim().to_string();
if name.is_empty() || name.len() > 128 {
return Err(CmdError::Params("bad font name".into()));
let arg = |k: &str| -> Result<Option<String>, CmdError> {
let Some(n) = v.get(k) else { return Ok(None) };
let n = n.as_str().map(str::trim).unwrap_or("");
if n.is_empty() || n.len() > 128 {
return Err(CmdError::Params(format!("bad font name `{k}`")));
}
Ok(Some(n.to_string()))
};
let (name, east_asia) = (arg("name")?, arg("eastAsia")?);
if name.is_none() && east_asia.is_none() {
return Err(CmdError::Params("`name` or `eastAsia` (string) is required".into()));
}
// The font applies to Persian/Arabic text in the selection too (its complex-script font). A
// Chinese, Japanese or Korean font is the East Asian text's font as well (`w:eastAsia`), as in
// Word; choosing a Latin font leaves that text's font as it was.
let east_asian = {
let r = wordcraft_fonts::word::resolve(&name, false, false);
let ea = east_asia.or_else(|| {
name.clone().filter(|n| {
let r = wordcraft_fonts::word::resolve(n, false, false);
!r.substituted && r.face.is_east_asian()
};
})
});
apply(s, &|c| {
c.font = Some(name.clone());
c.font_cs = Some(name.clone());
if east_asian {
c.font_ea = Some(name.clone());
if let Some(n) = &name {
c.font = Some(n.clone());
c.font_cs = Some(n.clone());
}
if let Some(n) = &ea {
c.font_ea = Some(n.clone());
}
})
}
@ -571,6 +585,7 @@ pub fn state(s: &Session) -> Value {
let style_name = s.doc.styles.get(&style).map(|st| st.name.clone()).unwrap_or(style.clone());
json!({
"font": same(&|r| json!(r.font)),
"fontEastAsia": same(&|r| json!(r.font_ea)),
"size": same(&|r| json!(r.size)),
"bold": all.iter().all(|r| r.bold),
"italic": all.iter().all(|r| r.italic),

View File

@ -3609,3 +3609,28 @@ fn outline_collapse_hides_descendants_and_expand_restores() {
run(&mut s, "outline.selectSubtree", json!({"block": 0}));
assert_eq!(s.selected_text(), "One\na\nTwo\nb");
}
#[test]
fn format_font_sets_the_asian_and_latin_fonts_separately() {
// #505: Word's Font dialog sets an Asian text font and a Latin text font of their own.
let mut s = s();
run(&mut s, "document.setText", json!({"text": "Hello 你好"}));
run(&mut s, "select.all", json!({}));
let fonts = |s: &Session| {
let c = s.doc.para(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap().props_of_char(0).clone();
(c.font, c.font_ea)
};
let before = fonts(&s);
run(&mut s, "format.font", json!({"name": "Georgia", "eastAsia": "SimSun"}));
assert_eq!(fonts(&s), (Some("Georgia".into()), Some("SimSun".into())));
assert_eq!(run(&mut s, "format.state", json!({}))["fontEastAsia"], "SimSun");
// The Asian font alone leaves the Latin font as it is.
run(&mut s, "format.font", json!({"eastAsia": "Noto Sans CJK SC"}));
assert_eq!(fonts(&s), (Some("Georgia".into()), Some("Noto Sans CJK SC".into())));
// Each change is one undo step.
run(&mut s, "edit.undo", json!({}));
assert_eq!(fonts(&s), (Some("Georgia".into()), Some("SimSun".into())));
run(&mut s, "edit.undo", json!({}));
assert_eq!(fonts(&s), before);
assert!(s.run("format.font", &json!({})).is_err() && s.run("format.font", &json!({"eastAsia": " "})).is_err());
}

View File

@ -219,12 +219,18 @@ impl FontFace {
/// (Shift-JIS, GB2312, Wansung, Big5, Johab), or, without them, it maps common ideographs.
/// Word applies such a font to East Asian text too (`w:eastAsia`).
pub fn is_east_asian(&self) -> bool {
self.skrifa().is_some_and(|f| east_asian_font(&f))
}
}
/// Is `f` an East Asian (Chinese, Japanese or Korean) font: do its OS/2 code pages say so
/// (Shift-JIS, GB2312, Wansung, Big5, Johab), or, without them, does it map common ideographs?
fn east_asian_font(f: &skrifa::FontRef<'_>) -> bool {
use skrifa::raw::TableProvider;
const CJK_CODE_PAGES: u32 = 0b1_1111 << 17;
match self.skrifa().and_then(|f| f.os2().ok()).and_then(|t| t.ul_code_page_range_1()) {
match f.os2().ok().and_then(|t| t.ul_code_page_range_1()) {
Some(pages) if pages != 0 => pages & CJK_CODE_PAGES != 0,
_ => ['中', '国', '日'].iter().all(|c| self.covers(*c)),
}
_ => ['中', '国', '日'].iter().all(|c| f.charmap().map(*c).is_some()),
}
}
@ -265,6 +271,8 @@ struct CatalogEntry {
style: String,
legacy: String,
local: Vec<LocalName>,
/// A Chinese, Japanese or Korean font ([`FontFace::is_east_asian`]).
east_asian: bool,
path: std::path::PathBuf,
}
@ -576,8 +584,9 @@ fn file_face_names(path: &std::path::Path) -> Vec<Found> {
read_at(offset.into(), len as usize)
};
let name = table(b"name")?;
// Variable fonts: the named instances.
let mut tables: Vec<([u8; 4], Vec<u8>)> = table(b"fvar").map(|t| (*b"fvar", t)).into_iter().collect();
// Variable fonts: the named instances; OS/2: the code pages (East Asian fonts).
let mut tables: Vec<([u8; 4], Vec<u8>)> = table(b"OS/2").map(|t| (*b"OS/2", t)).into_iter().collect();
tables.extend(table(b"fvar").map(|t| (*b"fvar", t)));
tables.push((*b"name", name));
// A font holding just those tables, to read them as the font itself would.
let font = mini_font(&tables);
@ -660,6 +669,9 @@ struct Found {
legacy: String,
/// The family's names in other languages ([`FontFace::local_names`]).
local: Vec<LocalName>,
/// A Chinese, Japanese or Korean font ([`FontFace::is_east_asian`]); the scan's catalog keeps it.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
east_asian: bool,
/// Variable fonts: the named instance's axis settings.
coords: Vec<([u8; 4], f32)>,
}
@ -669,6 +681,7 @@ struct Found {
fn font_faces(f: &skrifa::FontRef<'_>, index: u32) -> Vec<Found> {
let Some((family, style, id1)) = face_names(f) else { return vec![] };
let local = local_names(f, &family, id1.as_deref());
let east_asian = east_asian_font(f);
let axes = f.axes();
let mut named: Vec<Found> = Vec::new();
for ni in f.named_instances().iter() {
@ -681,13 +694,13 @@ fn font_faces(f: &skrifa::FontRef<'_>, index: u32) -> Vec<Found> {
let legacy = legacy_family(&family, &style);
// Name ID 1 names the default instance only: keep the names of the whole family.
let local = local.iter().filter(|n| n.english.eq_ignore_ascii_case(&family)).cloned().collect();
named.push(Found { index, family: family.clone(), style, legacy, local, coords });
named.push(Found { index, family: family.clone(), style, legacy, local, east_asian, coords });
}
if !named.is_empty() {
return named;
}
let legacy = id1.filter(|n| !n.eq_ignore_ascii_case(&family)).unwrap_or_else(|| legacy_family(&family, &style));
vec![Found { index, family, style, legacy, local, coords: Vec::new() }]
vec![Found { index, family, style, legacy, local, east_asian, coords: Vec::new() }]
}
/// Parse every face in `data` (a font file or collection); a variable font yields one face per
@ -872,6 +885,17 @@ impl FontDb {
v
}
/// The installed Chinese, Japanese and Korean families (English names), for the Font dialog's
/// Asian text font list.
pub fn east_asian_families(&self) -> Vec<String> {
let mut v: Vec<String> = self.read_faces().iter().filter(|f| f.is_east_asian()).map(|f| f.family.clone()).collect();
#[cfg(not(target_arch = "wasm32"))]
v.extend(self.read_catalog().iter().filter(|c| c.east_asian).map(|c| c.family.clone()));
v.sort_by_key(|a| a.to_lowercase());
v.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
v
}
/// Every family's name in the interface language `lang` (`zh-hans`, `zh-hant`, `ja`…), as
/// (English name, localized name) pairs: SimSun is 宋体 in Simplified Chinese. Families without
/// a name in that language are left out.
@ -1040,7 +1064,14 @@ impl FontDb {
continue;
}
for f in file_face_names(&p) {
found.push(CatalogEntry { family: f.family, style: f.style, legacy: f.legacy, local: f.local, path: p.clone() });
found.push(CatalogEntry {
family: f.family,
style: f.style,
legacy: f.legacy,
local: f.local,
east_asian: f.east_asian,
path: p.clone(),
});
}
}
}

View File

@ -369,3 +369,37 @@ fn chinese_fonts_are_listed_by_their_chinese_names_in_a_chinese_interface() {
assert_eq!(name_language_matches(ui, tag), ok, "{ui} {tag}");
}
}
/// `data` with its OS/2 code page bits (`ulCodePageRange1`) set to `pages`.
fn with_code_pages(data: &[u8], pages: u32) -> Vec<u8> {
let font = skrifa::FontRef::new(data).unwrap();
let tables: Vec<([u8; 4], Vec<u8>)> = font
.table_directory()
.table_records()
.iter()
.map(|r| {
let mut t = font.table_data(r.tag()).unwrap().as_bytes().to_vec();
if r.tag() == skrifa::Tag::new(b"OS/2") {
t[78..82].copy_from_slice(&pages.to_be_bytes());
}
(r.tag().to_be_bytes(), t)
})
.collect();
mini_font(&tables)
}
#[test]
fn east_asian_fonts_are_listed_for_the_asian_text_font_box() {
// #505: the Font dialog's Asian text font box lists the Chinese, Japanese and Korean fonts,
// found by their OS/2 code pages (here GB2312) during the scan, without loading them.
let dir = font_dir("eastasian");
let song = with_family_names("SourceSans3-Regular.ttf", &[(0x0409, "Songti Test"), (0x0804, "测试宋体")]);
std::fs::write(dir.join("song.ttf"), with_code_pages(&song, 1 << 18)).unwrap();
let db = FontDb::with_font_dirs(vec![dir]);
let listed = |db: &FontDb| db.east_asian_families().iter().filter(|f| *f == "Songti Test").count();
assert_eq!(listed(&db), 1);
assert!(!has(&db.east_asian_families(), FAMILY), "Latin fonts are left out");
// Loaded, it is still listed once.
assert!(db.face("Songti Test", "Regular").is_east_asian());
assert_eq!(listed(&db), 1);
}

View File

@ -3661,3 +3661,42 @@ fn table_cell_footnotes_are_placed() {
let body = l.caret(&Pos::body(1, 0)).unwrap();
assert!(c.top > body.top + 100.0, "at the page bottom: {c:?} {body:?}");
}
#[test]
fn east_asian_text_takes_the_asian_font_and_latin_text_the_latin_font() {
// #505: one run with a Latin font and an Asian font (w:eastAsia) of its own sets each script in
// its font. Prefer an installed CJK font other than the one fallback would pick anyway.
let resolve = |f: &str| wordcraft_fonts::word::resolve(f, false, false);
let fallback = wordcraft_fonts::FontDb::global().fallback_for('你', 0).map(|f| f.family.clone());
let mut fonts: Vec<String> = wordcraft_fonts::FontDb::global()
.east_asian_families()
.into_iter()
.filter(|f| {
let r = resolve(f);
!r.substituted && r.face.covers('你') && r.face.covers('好')
})
.collect();
fonts.sort_by_key(|f| Some(f) == fallback.as_ref());
let Some(asian) = fonts.first().cloned() else {
eprintln!("skipped: no Chinese font installed");
return;
};
let latin = wordcraft_doc::styles::BODY_FONT;
let text = "Hello 你好 World";
let mut d = Document::from_text(text);
d.format_range(&Pos::body(0, 0), &Pos::body(0, text.len()), &|c| {
c.font = Some(latin.into());
c.font_ea = Some(asian.clone());
})
.unwrap();
let l = lay(&d);
let para =
l.pages.iter().flat_map(|p| p.items.iter()).find_map(|it| if let Placed::Lines { para, .. } = it { Some(para) } else { None }).unwrap();
let (latin_face, asian_face) = (resolve(latin).face.id(), resolve(&asian).face.id());
for c in &para.clusters {
let Some(ch) = text.get(c.start..c.end).and_then(|s| s.chars().next()).filter(|ch| !ch.is_whitespace()) else { continue };
let face = para.styles[c.style as usize].face.id();
let want = if ch.is_ascii() { latin_face } else { asian_face };
assert_eq!(face, want, "{ch:?} in {}", if ch.is_ascii() { latin } else { &asian });
}
}

View File

@ -121,7 +121,15 @@ fn special_menu(ui: &mut Ui, extra: &FindExtra) -> Option<&'static str> {
#[serde(tag = "dialog", rename_all = "camelCase")]
pub enum Dialog {
Font {
/// The font (Latin text) and the Asian text font, as the font lists name them, and their
/// values when the dialog opened: OK changes only the ones changed (#505).
font: String,
font_was: String,
font_ea: String,
font_ea_was: String,
/// Show the Asian text font box: a Chinese or Japanese interface, or East Asian text or
/// an Asian font in the selection.
asian: bool,
size: String,
bold: bool,
italic: bool,
@ -908,8 +916,22 @@ impl Dialog {
let b = |k: &str| st.get(k).and_then(Value::as_bool).unwrap_or(false);
Some(match name {
"font" => {
let font = app.previews.font_shown(&s("font"));
let font_ea = app.previews.font_shown(&s("fontEastAsia"));
let lang = crate::i18n::current().code();
let text = app.session.selected_text();
let text = if text.is_empty() {
app.session.doc.para_at(&app.session.sel.focus).map(|p| p.plain_text().to_string()).unwrap_or_default()
} else {
text
};
let asian = ["zh", "ja", "ko"].iter().any(|l| lang.starts_with(l)) || !font_ea.is_empty() || text.chars().any(east_asian_char);
let mut d = Dialog::Font {
font: s("font"),
font_was: font.clone(),
font,
font_ea_was: font_ea.clone(),
font_ea,
asian,
size: st.get("size").and_then(Value::as_f64).map(|v| v.to_string()).unwrap_or_default(),
bold: b("bold"),
italic: b("italic"),
@ -1329,7 +1351,7 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) {
/// The character properties the Font dialog's fields stand for.
fn font_props(d: &Dialog) -> serde_json::Map<String, Value> {
let Dialog::Font { font, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } = d else {
let Dialog::Font { font, font_ea, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } = d else {
return serde_json::Map::new();
};
let mut props = serde_json::Map::new();
@ -1352,6 +1374,9 @@ fn font_props(d: &Dialog) -> serde_json::Map<String, Value> {
if !font.trim().is_empty() {
props.insert("font".into(), json!(font.trim()));
}
if !font_ea.trim().is_empty() {
props.insert("fontEa".into(), json!(font_ea.trim()));
}
if let Ok(s) = size.trim().parse::<f64>() {
props.insert("size".into(), json!(s));
}
@ -1361,10 +1386,26 @@ fn font_props(d: &Dialog) -> serde_json::Map<String, Value> {
props
}
/// Font dialog OK: set the properties the user changed, leave the others alone.
/// Is `c` Chinese, Japanese or Korean text (ideographs, kana, Hangul, CJK punctuation, fullwidth
/// forms)?
fn east_asian_char(c: char) -> bool {
matches!(u32::from(c), 0x1100..=0x11FF | 0x2E80..=0x9FFF | 0xA960..=0xA97F | 0xAC00..=0xD7FF | 0xF900..=0xFAFF | 0xFE30..=0xFE4F | 0xFF00..=0xFFEF | 0x20000..=0x3FFFF)
}
/// The Font dialog's OK: one formatting step that sets only the properties the user changed, so
/// formatting the dialog can't show or left alone stays as it is. Fonts are set by their stored
/// (English) names, the Latin font for Persian/Arabic text too (as `format.font`).
fn apply_font(app: &mut WordApp, d: &Dialog) {
let Dialog::Font { start, .. } = d else { return };
let changed: serde_json::Map<String, Value> = font_props(d).into_iter().filter(|(k, v)| start.get(k) != Some(v)).collect();
let mut changed: serde_json::Map<String, Value> = font_props(d).into_iter().filter(|(k, v)| start.get(k) != Some(v)).collect();
if let Some(f) = changed.get("font").and_then(Value::as_str).map(str::to_string) {
let f = app.previews.font_stored(&f);
changed.insert("font".into(), json!(f));
changed.insert("fontCs".into(), json!(f));
}
if let Some(f) = changed.get("fontEa").and_then(Value::as_str).map(str::to_string) {
changed.insert("fontEa".into(), json!(app.previews.font_stored(&f)));
}
if !changed.is_empty() {
let _ = app.run("format.set", json!({"props": changed}));
}
@ -1373,7 +1414,7 @@ fn apply_font(app: &mut WordApp, d: &Dialog) {
/// Returns true to close.
fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
match d {
Dialog::Font { font, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } => {
Dialog::Font { font, font_ea, asian, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } => {
// Build the sample text format for the preview pane below.
let preview_size = size.trim().parse::<f32>().unwrap_or(12.0).clamp(6.0, 72.0);
let preview_color = wordcraft_doc::Rgb::parse(color).map(crate::theme::c32).unwrap_or_else(|| ui.visuals().text_color());
@ -1395,8 +1436,22 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
fmt.strikethrough = egui::Stroke::new(1.0, preview_color);
}
egui::Grid::new("fontdlg").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label(tl!("Font:"));
ui.text_edit_singleline(font);
// Word's two font boxes when East Asian text is in use: the Latin text font and
// the Asian text font, each with its own list (type to find a font).
let prev = app.previews.font_preview_fn();
if *asian {
ui.label(tl!("Asian text font:"));
let fams = app.previews.east_asian_families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_asian", 220.0, font_ea, &fams, Some(&*prev)) {
*font_ea = f;
}
ui.end_row();
}
ui.label(if *asian { tl!("Latin text font:") } else { tl!("Font:") });
let fams = app.previews.families();
if let Some(f) = crate::widgets::font_combo(ui, "fontdlg_font", 220.0, font, &fams, Some(&*prev)) {
*font = f;
}
ui.end_row();
ui.label(tl!("Size:"));
ui.text_edit_singleline(size);
@ -3011,6 +3066,7 @@ fn enclose_preview(ui: &mut Ui, style: &str, text: &str, shape: &str) {
#[cfg(test)]
mod tests {
use super::*;
use wordcraft_doc::Pos;
use wordcraft_doc::props::Align;
use wordcraft_engine::Session;
@ -3135,6 +3191,35 @@ mod tests {
}
}
#[test]
fn font_dialog_sets_the_asian_and_latin_fonts_separately() {
// #505: the Asian text font box and the Latin text font box each set their own font, and
// OK leaves the one not changed as it was.
let mut app = app_with("Hello 你好");
let _ = app.run("select.all", json!({}));
let fonts = |app: &WordApp| {
let c = app.session.doc.para_at(&Pos::body(0, 0)).map(|p| p.props_of_char(0).clone()).unwrap_or_default();
(c.font, c.font_ea)
};
let latin = fonts(&app).0;
let Some(mut d) = Dialog::open("font", &mut app) else { panic!("no font dialog") };
let Dialog::Font { asian, font_ea, .. } = &mut d else { panic!() };
assert!(*asian, "Chinese text in the selection shows the Asian text font box");
*font_ea = "SimSun".into();
apply_font(&mut app, &d);
assert_eq!(fonts(&app), (latin, Some("SimSun".into())));
let Some(mut d) = Dialog::open("font", &mut app) else { panic!() };
let Dialog::Font { font, font_ea, .. } = &mut d else { panic!() };
assert_eq!(font_ea, "SimSun");
*font = "Georgia".into();
apply_font(&mut app, &d);
assert_eq!(fonts(&app), (Some("Georgia".into()), Some("SimSun".into())));
// One undo step each.
let _ = app.run("edit.undo", json!({}));
assert_eq!(fonts(&app).1.as_deref(), Some("SimSun"));
assert_ne!(fonts(&app).0.as_deref(), Some("Georgia"));
}
#[test]
fn paragraph_dialog_changes_direction() {
let mut app = app_with("Hello");

View File

@ -63,6 +63,7 @@
Arrow Flecha
Artistic Effects Efectos artísticos
As you type Mientras escribes
Asian text font: Fuente de texto asiático:
Asian Typography Tipografía asiática
Author Autor
AutoCorrect Options Opciones de autocorrección
@ -105,6 +106,7 @@
Interface Interfaz
Interface language Idioma de la interfaz
Inside: Interior:
Latin text font: Fuente de texto latino:
Left border Borde izquierdo
Legal style numbering Numeración de estilo legal
Link level to style: Vincular nivel con estilo:

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -50,6 +50,7 @@
Arrow Nool
Artistic Effects Kunstilised efektid
As you type Kirjutamise ajal
Asian text font: Aasia teksti font:
Author Autor
AutoCorrect Options Automaatparanduse valikud
AutoFit Automaatne sobitus
@ -383,6 +384,7 @@
Lasso Lasso
Last Column Viimane veerg
Last commit Viimane commit
Latin text font: Ladina teksti font:
Layout Paigutus
Layout Summary Paigutuse kokkuvõte
Left Vasak

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -49,6 +49,7 @@
Arrow 矢印
Artistic Effects アート効果
As you type 入力中
Asian text font: アジア文字のフォント:
Asian Typography アジア文字の組版
Author 作成者
AutoCorrect Options オートコレクトのオプション
@ -102,6 +103,7 @@
Interface インターフェイス
Interface language 表示言語
Inside: 内側:
Latin text font: ラテン文字のフォント:
Left border 左罫線
Left Margin 左余白
Indents and Spacing インデントと間隔

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -59,6 +59,7 @@
Arrow Seta
Artistic Effects Efeitos artísticos
As you type Enquanto você digita
Asian text font: Fonte de texto asiático:
Asian Typography Tipografia asiática
Author Autor
AutoCorrect Options Opções de autocorreção
@ -106,6 +107,7 @@
Interface Interface
Interface language Idioma da interface
Inside: Interno:
Latin text font: Fonte de texto latino:
Left border Borda esquerda
Legal style numbering Numeração no estilo legal
Link level to style: Vincular nível ao estilo:

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -2359,3 +2359,6 @@ preview Title Пример
Typing replaces selected text Ввод заменяет выделенный текст
Update fields before printing Обновлять поля перед печатью
Use the Insert key to switch overtype on and off Клавиша Insert включает и выключает режим замены
# Asian and Latin text fonts (#505).
Asian text font: Шрифт азиатского текста:
Latin text font: Шрифт латинского текста:

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -66,6 +66,7 @@
Artistic Effects Umetnički efekti
As Recommended Kao što je preporučeno
As you type Tokom kucanja
Asian text font: Font azijskog teksta:
Asian Typography Azijska tipografija
Author Autor
AutoCorrect Options Opcije automatske ispravke
@ -129,6 +130,7 @@
Interface Interfejs
Interface language Jezik interfejsa
Inside: Unutra:
Latin text font: Font latiničnog teksta:
Left border Leva ivica
Left Margin Leva margina
Legal style numbering Pravno numerisanje

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -70,6 +70,7 @@
Artistic Effects Уметнички ефекти
As Recommended Као што је препоручено
As you type Током куцања
Asian text font: Фонт азијског текста:
Asian Typography Азијска типографија
Author Аутор
AutoCorrect Options Опције аутоматске исправке
@ -133,6 +134,7 @@
Interface Интерфејс
Interface language Језик интерфејса
Inside: Унутра:
Latin text font: Фонт латиничног текста:
Left border Лева ивица
Left Margin Лева маргина
Legal style numbering Правно нумерисање

Can't render this file because it contains an unexpected character in line 11 and column 25.

View File

@ -1981,3 +1981,6 @@
Typing replaces selected text Yazmak seçili metnin yerine geçer
Update fields before printing Yazdırmadan önce alanları güncelle
Use the Insert key to switch overtype on and off Üzerine yazmayı açıp kapatmak için Insert tuşunu kullan
# Asian and Latin text fonts (#505).
Asian text font: Asya metni yazı tipi:
Latin text font: Latin metni yazı tipi:

Can't render this file because it has a wrong number of fields in line 18.

View File

@ -61,6 +61,7 @@
Arrow Стрілка
Artistic Effects Художні ефекти
As you type Під час введення
Asian text font: Шрифт азійського тексту:
Asian Typography Азійська типографіка
Author Автор
AutoCorrect Options Параметри автовиправлення
@ -108,6 +109,7 @@
Interface Інтерфейс
Interface language Мова інтерфейсу
Inside: Усередині:
Latin text font: Шрифт латинського тексту:
Left border Ліва межа
Legal style numbering Юридична нумерація
Link level to style: Зв'язати рівень зі стилем:

Can't render this file because it contains an unexpected character in line 6 and column 31.

View File

@ -61,6 +61,7 @@
Arrow 箭头
Artistic Effects 艺术效果
As you type 键入时
Asian text font: 亚洲文字字体:
Asian Typography 亚洲版式
Author 作者
AutoCorrect Options 自动更正选项
@ -114,6 +115,7 @@
Interface 界面
Interface language 界面语言
Inside: 内侧:
Latin text font: 拉丁文字字体:
Left border 左框线
Left Margin 左边距
Indents and Spacing 缩进和间距

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -50,6 +50,7 @@
Arrow 箭號
Artistic Effects 美術效果
As you type 輸入時
Asian text font: 亞洲文字字型:
Asian Typography 亞洲版式
Author 作者
AutoCorrect Options 自動校正選項
@ -103,6 +104,7 @@
Interface 介面
Interface language 介面語言
Inside: 內側:
Latin text font: 拉丁文字字型:
Left border 左框線
Left Margin 左邊界
Indents and Spacing 縮排和間距

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -20,6 +20,8 @@ pub struct Previews {
/// Lowercase English family → its name in that language (simsun → 宋体), and back.
local_names: HashMap<String, String>,
english_names: HashMap<String, String>,
/// The Chinese, Japanese and Korean fonts of the font list (the Asian text font box).
east_asian: Option<Vec<String>>,
styles_hash: (u64, u64),
/// Previews rendered this frame (rendering is spread over frames).
budget: std::cell::Cell<u32>,
@ -45,10 +47,29 @@ impl Previews {
self.local_names = names.iter().map(|(e, l)| (e.to_lowercase(), l.clone())).collect();
self.english_names = names.into_iter().map(|(e, l)| (l, e)).collect();
self.families = Some((lang, f));
self.east_asian = None;
}
self.families.as_ref().map(|(_, f)| f.clone()).unwrap_or_default()
}
/// The Font dialog's Asian text font list: the installed Chinese, Japanese and Korean fonts,
/// named as in [`Previews::families`].
pub fn east_asian_families(&mut self) -> Vec<String> {
self.families();
if self.east_asian.is_none() {
let mut f: Vec<String> = wordcraft_fonts::FontDb::global()
.east_asian_families()
.into_iter()
.filter(|f| !f.starts_with('.'))
.map(|f| self.local_names.get(&f.to_lowercase()).cloned().unwrap_or(f))
.collect();
f.sort_by_key(|a| a.to_lowercase());
f.dedup();
self.east_asian = Some(f);
}
self.east_asian.clone().unwrap_or_default()
}
/// The name the font box shows for the document's font `family`: its name in the interface
/// language when the font has one.
pub fn font_shown(&mut self, family: &str) -> String {

View File

@ -76,6 +76,7 @@ proofing is not included; terminology and checks: [`localization-tr.md`](localiz
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Font dialog sets the Asian text font (`w:eastAsia`) and the Latin text font separately; the Asian list holds the installed Chinese, Japanese and Korean fonts by their interface-language names (#505) |
| 2026-10-11 | minor | Font list shows installed fonts by their names in the interface language (宋体 for SimSun in Chinese, Japanese and Korean names likewise) and finds them by either name; a Chinese, Japanese or Korean font is stored as `w:eastAsia` too (#505) |
| 2026-10-11 | minor | Chinese/Japanese interface fonts: a test checks every catalog character has a glyph; an installed CJK font follows the embedded faces whenever they miss one; macOS font asset folders searched for PingFang (#486) |
| 2026-10-10 | minor | Russian interface (#300; 1,524 entries, built-in style names shown translated; covers everything the interface audit finds) |

View File

@ -49,6 +49,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| 2026-10-11 | trivial | Line Numbers, Hyphenation (with Manual Hyphenation), Language, Envelopes and Labels dialogs (#407); the three duplicated modal-dialog rows merged into one |
| 2026-10-11 | trivial | Accessibility pane (errors/warnings/tips, jump-to, fixes) and Alt Text pane, opened from Picture/Shape Format › Accessibility and right-click › View Alt Text… (#386) |
| 2026-10-11 | trivial | References dialogs: Caption (New/Delete Label, Numbering with chapter numbers), Index and Mark Index Entry (Mark All), Custom Table of Contents (Options, TOC styles via Modify Style), Source Manager and Create/Edit Source; modal dialog count 34 (#402) |
| 2026-10-11 | trivial | Font dialog: Asian text font and Latin text font boxes, each with its own list and type-to-find, shown for a Chinese/Japanese interface or East Asian text in the selection; OK sets only the fonts changed; `format.font {"eastAsia"}` (#505) |
| 2026-10-11 | trivial | Customize Ribbon (custom tabs and groups, hidden and reordered tabs) and a customizable Quick Access Toolbar with a working … menu (#379, #376) |
| 2026-10-11 | trivial | Customize Keyboard dialog (Tools › Customize Keyboard, File › Options › Keyboard shortcuts): custom keys override built-ins and persist (#368) |
| 2026-10-11 | minor | Keyboard shortcuts 101 → 125 commands (#330): Reset Paragraph/Character Formatting, symbols, DATE/TIME/PAGE fields, view switches, Word Count, mail merge, F4 Repeat now reachable |