* 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:
parent
60cd1bf568
commit
dab5092e83
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
!r.substituted && r.face.is_east_asian()
|
||||
};
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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()) {
|
||||
Some(pages) if pages != 0 => pages & CJK_CODE_PAGES != 0,
|
||||
_ => ['中', '国', '日'].iter().all(|c| self.covers(*c)),
|
||||
}
|
||||
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 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| 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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ¶.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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue