mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-05 10:10:35 +00:00
Merge pull request #2243 from aiongg/integration-designcraft-20261103-5
Integration batch integration-designcraft-20261103-5
This commit is contained in:
13 files changed
+186
-19
No files matched your search
@@ -6169,6 +6169,28 @@ fn mojikumi_one_em_line_end_set_sets_closing_punctuation_full_width_at_line_end(
|
||||
assert!((open - one.0 - em * 0.5).abs() < 0.01 && (stop - one.1).abs() < 0.01, "one-em set: {one:?} of {open}/{stop}");
|
||||
}
|
||||
|
||||
/// Consecutive punctuation touches (JLREQ 3.1.4): in 漢」。漢(「漢 the closing bracket and
|
||||
/// the full stop, and the two opening brackets, close up by half an em each.
|
||||
#[test]
|
||||
fn mojikumi_closes_up_consecutive_punctuation() {
|
||||
let family = "Mojikumi Consecutive";
|
||||
let font = designcraft_fonts::testing::font_mapping(family, &[('漢', '‰'), ('」', '‰'), ('。', '‰'), ('(', '‰'), ('「', '‰')]).unwrap();
|
||||
designcraft_fonts::FontDb::global().add_font(font);
|
||||
let text = "漢」。漢(「漢";
|
||||
let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default());
|
||||
d.story_mut(sid).unwrap().format_chars(0..text.len(), |f| f.over.font_family = Some(family.into()));
|
||||
let mut width = |set: &str| {
|
||||
d.story_mut(sid).unwrap().paras[0].para.mojikumi = Some(set.into());
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let line = all_lines(&cs)[0];
|
||||
let g = &line.glyphs[0];
|
||||
(line.end_x - line.x0, g.face.units_per_em() * g.sx)
|
||||
};
|
||||
let (natural, em) = width("Nothing");
|
||||
let (set, _) = width("LineEndAllOneHalfEmEnum");
|
||||
assert!((natural - set - em).abs() < 0.01, "{natural} - {set} should be one em ({em})");
|
||||
}
|
||||
|
||||
fn ruled(text: &str, rect: Rect, columns: u32, edit: impl FnOnce(&mut Document, StoryId)) -> (Document, ItemId, Vec<Rect>) {
|
||||
let (mut d, sid, fid) = doc_with(text, rect, ParaAttrs::default());
|
||||
{
|
||||
|
||||
@@ -126,6 +126,23 @@ impl<'a> Rules<'a> {
|
||||
if right == 22 {
|
||||
return Aki::fixed(if self.base == Base::LineEndHalfEm { 0.0 } else { trailing(left) });
|
||||
}
|
||||
// Consecutive punctuation in the Japanese sets (JLREQ 3.1.4): opening brackets together,
|
||||
// and closing brackets, commas and full stops together, touch. A middle dot (・:;) after
|
||||
// a closing bracket or before an opening one keeps only its own quarter em. Other
|
||||
// neighbours keep the larger blank, so a closing bracket and an opening one are half an
|
||||
// em apart. The Chinese sets keep the larger blank throughout.
|
||||
let opening = |c| matches!(c, 1 | 26 | 27);
|
||||
let closing = |c| matches!(c, 2 | 6 | 21 | 28..=31);
|
||||
let closing_bracket = |c| matches!(c, 2 | 28 | 29);
|
||||
let middle_dot = |c| matches!(c, 5 | 32);
|
||||
if self.base != Base::Chinese {
|
||||
if (opening(left) && opening(right)) || (closing(left) && closing(right)) {
|
||||
return Aki::default();
|
||||
}
|
||||
if (closing_bracket(left) && middle_dot(right)) || (middle_dot(left) && opening(right)) {
|
||||
return Aki { min: 0.0, desired: 0.25, max: 0.25, priority: 1, discrete: false };
|
||||
}
|
||||
}
|
||||
let punctuation = trailing(left).max(leading(right));
|
||||
if punctuation > 0.0 {
|
||||
return Aki { min: 0.0, desired: punctuation, max: punctuation, priority: 1, discrete: false };
|
||||
@@ -237,6 +254,36 @@ mod tests {
|
||||
assert_ne!(class('「'), class('('));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_punctuation_touches_except_closing_before_opening() {
|
||||
let styles = Styles::default();
|
||||
for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum"] {
|
||||
let rules = Rules::resolve(&styles, set).unwrap().unwrap();
|
||||
let aki = |a: char, b: char| rules.pair(class(a), class(b)).desired;
|
||||
for (a, b) in
|
||||
[('」', '。'), ('」', '、'), ('。', '」'), ('、', '」'), ('」', '』'), (')', '」'), (',', ')'), ('「', '『'), ('(', '「')]
|
||||
{
|
||||
assert_eq!(aki(a, b), 0.0, "{set}: {a}{b}");
|
||||
}
|
||||
for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] {
|
||||
assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}");
|
||||
}
|
||||
// A middle dot keeps its quarter em beside a bracket (JLREQ 3.1.4, Figure 69 ⑦).
|
||||
for (a, b) in [('」', '・'), (')', ':'), ('』', ';'), ('・', '「'), (';', '('), (':', '〈')] {
|
||||
assert_eq!(aki(a, b), 0.25, "{set}: {a}{b}");
|
||||
}
|
||||
// After a comma or full stop the larger blank stays.
|
||||
assert_eq!(aki('、', '・'), 0.5, "{set}: 、・");
|
||||
}
|
||||
// The Chinese sets are unchanged: the larger blank stays between any two marks.
|
||||
for set in ["SimpChineseDefault", "TradChineseDefault"] {
|
||||
let rules = Rules::resolve(&styles, set).unwrap().unwrap();
|
||||
for (a, b) in [('」', '。'), (',', ')'), ('「', '『'), ('」', '「'), ('」', '・'), ('・', '「')] {
|
||||
assert_eq!(rules.pair(class(a), class(b)).desired, 0.5, "{set}: {a}{b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_em_line_end_set_keeps_closing_punctuation_full_at_line_end_only() {
|
||||
let styles = Styles::default();
|
||||
|
||||
@@ -7255,7 +7255,7 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
v if v["kind"] == "points" => v["value"].as_f64(),
|
||||
_ => None,
|
||||
};
|
||||
let field = crate::widgets::NumField::number("pslead", lv, " pt", 2).width(80.0).range(0.0, 5000.0);
|
||||
let field = crate::widgets::NumField::points("pslead", lv, 2).width(80.0).range(0.0, 5000.0);
|
||||
let edit = if cf.sparse { field.show_or_clear(ui) } else { field.show(ui).map(Some) };
|
||||
if let Some(n) = edit {
|
||||
cf.set(d, "leading", n.map_or(Value::Null, |n| json!({"kind": "points", "value": n})));
|
||||
|
||||
@@ -192,7 +192,7 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut Ui) {
|
||||
}
|
||||
icon(ui, rect, 36.0, 0, "panel-stroke", &tr("Stroke Weight"));
|
||||
if let Some(v) = cell(ui, rect, 56.0, 0, 74.0, selected, |ui| {
|
||||
NumField::number("csw", info.as_ref().map(|i| i.stroke_weight), " pt", 3).width(74.0).spinner().range(0.0, 1000.0).show(ui)
|
||||
NumField::points("csw", info.as_ref().map(|i| i.stroke_weight), 3).width(74.0).spinner().range(0.0, 1000.0).show(ui)
|
||||
}) {
|
||||
let _ = app.run("object.stroke", json!({"weight": v}));
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ pub fn stroke_fields(app: &mut DesignApp, ui: &mut Ui, id: &str, v: &Value) -> O
|
||||
egui::Grid::new((id, "cell_stroke")).num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
|
||||
caption(ui, tr(&lang, "Weight:"));
|
||||
let wid = format!("{id}_weight");
|
||||
if let Some(w) = NumField::number(&wid, v["weight"].as_f64(), " pt", 3).width(86.0).spinner().presets(WEIGHTS).range(0.0, 1000.0).show(ui) {
|
||||
if let Some(w) = NumField::points(&wid, v["weight"].as_f64(), 3).width(86.0).spinner().presets(WEIGHTS).range(0.0, 1000.0).show(ui) {
|
||||
out = Some(("weight", json!(w)));
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
@@ -452,7 +452,7 @@ pub(super) fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo)
|
||||
}
|
||||
const WEIGHTS: &[f64] = &[0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 20.0, 30.0, 40.0, 50.0, 100.0];
|
||||
if let Some(v) = place(ui, sub(b, 96.0, 0.0, 69.0, FIELD_H), |ui| {
|
||||
NumField::number("sw", Some(i.stroke_weight), " pt", 3).width(69.0).spinner().presets(WEIGHTS).range(0.0, 800.0).show(ui)
|
||||
NumField::points("sw", Some(i.stroke_weight), 3).width(69.0).spinner().presets(WEIGHTS).range(0.0, 800.0).show(ui)
|
||||
}) {
|
||||
let _ = app.run("object.stroke", json!({"weight": v}));
|
||||
}
|
||||
|
||||
@@ -533,9 +533,12 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let _ = app.run("object.dropShadow", json!({"on": on}));
|
||||
}
|
||||
if ds.on {
|
||||
// Distance and Size in the document's unit, as in the Effects dialog: a bare `7` means the
|
||||
// same in both.
|
||||
let u = units(app);
|
||||
egui::Grid::new("dsg").num_columns(4).spacing(vec2(6.0, 4.0)).show(ui, |ui| {
|
||||
caption(ui, crate::i18n::tr(&app.ui.language, "Distance"));
|
||||
if let Some(v) = number(ui, "dsd", Some(ds.distance), " pt", 56.0, 1) {
|
||||
if let Some(v) = measure(ui, "dsd", Some(ds.distance), u, 56.0) {
|
||||
let _ = app.run("object.dropShadow", json!({"on": true, "distance": v}));
|
||||
}
|
||||
caption(ui, crate::i18n::tr(&app.ui.language, "Angle"));
|
||||
@@ -562,7 +565,7 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let _ = app.run("object.dropShadow", json!({"on": true, "opacity": v / 100.0}));
|
||||
}
|
||||
caption(ui, crate::i18n::tr(&app.ui.language, "Size"));
|
||||
if let Some(v) = number(ui, "dss", Some(ds.size), " pt", 50.0, 1) {
|
||||
if let Some(v) = measure(ui, "dss", Some(ds.size), u, 56.0) {
|
||||
let _ = app.run("object.dropShadow", json!({"on": true, "size": v}));
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
@@ -222,6 +222,39 @@ fn properties_character_section_sets_the_language() {
|
||||
assert_eq!(language(h.state_mut()), "French");
|
||||
}
|
||||
|
||||
/// Drop shadow Distance and Size in the Effects panel read and show the document's unit, as
|
||||
/// the Effects dialog does (#1523): in picas a typed `7` is 7 picas in both.
|
||||
#[test]
|
||||
fn effects_panel_shadow_fields_use_the_document_unit() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [72, 72, 144, 144]})).unwrap()["id"].clone();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
app.run("object.dropShadow", json!({"on": true, "distance": 7.0, "size": 5.0})).unwrap();
|
||||
let shadow = |app: &DesignApp| {
|
||||
let d = app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().effects.drop_shadow.clone();
|
||||
(d.distance, d.size)
|
||||
};
|
||||
let mut h = panel_harness(app, effects_panel);
|
||||
h.run_steps(3);
|
||||
let values =
|
||||
|h: &Harness<'static, DesignApp>| -> Vec<String> { h.get_all_by_role(egui::accesskit::Role::TextInput).filter_map(|n| n.value()).collect() };
|
||||
let shown = values(&h);
|
||||
assert!(shown.contains(&"0p7".to_string()) && shown.contains(&"0p5".to_string()), "{shown:?}");
|
||||
for (old, typed, want) in [("0p7", "7", (84.0, 5.0)), ("0p5", "3mm", (84.0, 3.0 * designcraft_geom::units::PT_PER_MM))] {
|
||||
let field = h.get_all_by_role(egui::accesskit::Role::TextInput).find(|n| n.value().as_deref() == Some(old)).unwrap();
|
||||
field.focus();
|
||||
h.run_steps(1);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.get_all_by_role(egui::accesskit::Role::TextInput).find(|n| n.is_focused()).unwrap().type_text(typed);
|
||||
h.run_steps(1);
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(3);
|
||||
let got = shadow(h.state_mut());
|
||||
assert!((got.0 - want.0).abs() < 1e-3 && (got.1 - want.1).abs() < 1e-3, "{typed}: {got:?}, want {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties › Character and Paragraph •••, as in InDesign: the sections expand in place,
|
||||
/// with the scales, baseline shift, skew, language, OpenType and the case buttons, and the
|
||||
/// indents, spacing and drop caps.
|
||||
@@ -580,6 +613,30 @@ fn character_panel_has_kerning_and_takes_a_typed_manual_value() {
|
||||
assert_eq!(kerning(h.state_mut()), json!({"kind": "manual", "value": -40.0}));
|
||||
}
|
||||
|
||||
/// The size field reads the unit typed, as InDesign's does: 13Q is 9.213 pt (it used to keep
|
||||
/// the digits only and set 13 pt). A bare number is still points.
|
||||
#[test]
|
||||
fn character_size_field_reads_the_unit_typed() {
|
||||
let mut h = panel_harness(text_app(), character_panel);
|
||||
h.run_steps(3);
|
||||
let size = |app: &mut DesignApp| text_attrs(app).unwrap()["chars"]["size"].as_f64().unwrap();
|
||||
for (typed, pt) in [("13Q", 13.0 * designcraft_geom::units::PT_PER_Q), ("5mm", 5.0 * designcraft_geom::units::PT_PER_MM), ("14", 14.0)] {
|
||||
let field = h
|
||||
.get_all_by_role(egui::accesskit::Role::TextInput)
|
||||
.find(|n| n.value().is_some_and(|v| v.ends_with(" pt") && !v.starts_with('(') && v != "0 pt"))
|
||||
.unwrap();
|
||||
field.focus();
|
||||
h.run_steps(1);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.get_all_by_role(egui::accesskit::Role::TextInput).find(|n| n.is_focused()).unwrap().type_text(typed);
|
||||
h.run_steps(1);
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(3);
|
||||
let got = size(h.state_mut());
|
||||
assert!((got - pt).abs() < 1e-6, "{typed}: {got} pt, want {pt}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_manual_kerning_value_at_a_caret_kerns_the_pair() {
|
||||
let mut app = text_app();
|
||||
|
||||
@@ -148,7 +148,7 @@ pub(super) fn text_appearance_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
}
|
||||
const WEIGHTS: &[f64] = &[0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0];
|
||||
if let Some(v) = place(ui, sub(b, 96.0, 0.0, 69.0, FIELD_H), |ui| {
|
||||
NumField::number("tsw", c["strokeWeight"].as_f64(), " pt", 3).width(69.0).spinner().presets(WEIGHTS).range(0.0, 100.0).show(ui)
|
||||
NumField::points("tsw", c["strokeWeight"].as_f64(), 3).width(69.0).spinner().presets(WEIGHTS).range(0.0, 100.0).show(ui)
|
||||
}) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"strokeWeight": v}}));
|
||||
}
|
||||
@@ -204,7 +204,7 @@ pub(super) fn character_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
let lv = if auto { size.map(|s| s * a["para"]["autoLeading"].as_f64().unwrap_or(1.2)) } else { c["leading"]["value"].as_f64() };
|
||||
icons::paint(ui.painter(), sub(b, 0.0, 1.0, 19.0, 19.0), "font-size", t.icon);
|
||||
if let Some(v) = place(ui, sub(b, 24.0, 0.0, half - 24.0, FIELD_H), |ui| {
|
||||
NumField::number("csize", size, " pt", 2).width(half - 24.0).spinner().presets(SIZES).range(0.1, 1296.0).show(ui)
|
||||
NumField::points("csize", size, 2).width(half - 24.0).spinner().presets(SIZES).range(0.1, 1296.0).show(ui)
|
||||
}) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"size": v}}));
|
||||
}
|
||||
@@ -214,7 +214,7 @@ pub(super) fn character_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
if let Some(v) = place(ui, sub(b, half + 34.0, 0.0, lw, FIELD_H), |ui| {
|
||||
let key = if auto { "cleadauto" } else { "clead" };
|
||||
// Auto leading is shown in parentheses, like "(14.4 pt)".
|
||||
NumField::number(key, lshow, " pt", 2).width(lw).spinner().presets(SIZES).range(0.0, 5000.0).parens(auto).show(ui)
|
||||
NumField::points(key, lshow, 2).width(lw).spinner().presets(SIZES).range(0.0, 5000.0).parens(auto).show(ui)
|
||||
}) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"leading": {"kind": "points", "value": v}}}));
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let (left_w, right_w) = (caption_width(app, ui, &["Size", "Kerning", "Baseline"]), caption_width(app, ui, &["Leading", "Tracking"]));
|
||||
ui.columns(2, |cols| {
|
||||
let [left, right] = cols else { return };
|
||||
if let Some(v) = char_field(app, left, "Size", left_w, NumField::number("pcs", c["size"].as_f64(), " pt", 2)) {
|
||||
if let Some(v) = char_field(app, left, "Size", left_w, NumField::points("pcs", c["size"].as_f64(), 2)) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"size": v}}));
|
||||
}
|
||||
left.horizontal(|ui| {
|
||||
@@ -58,11 +58,11 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let width = (ui.available_width() - ui.spacing().item_spacing.x).max(40.0);
|
||||
kerning_control(app, ui, "pck", &c["kerning"], width);
|
||||
});
|
||||
if let Some(v) = char_field(app, left, "Baseline", left_w, NumField::number("pcb", c["baselineShift"].as_f64(), " pt", 2)) {
|
||||
if let Some(v) = char_field(app, left, "Baseline", left_w, NumField::points("pcb", c["baselineShift"].as_f64(), 2)) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"baselineShift": v}}));
|
||||
}
|
||||
// Auto leading is shown in parentheses, as the Paragraph panel shows it.
|
||||
if let Some(v) = char_field(app, right, "Leading", right_w, NumField::number("pcl", lv, " pt", 2).parens(auto)) {
|
||||
if let Some(v) = char_field(app, right, "Leading", right_w, NumField::points("pcl", lv, 2).parens(auto)) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"leading": {"kind": "points", "value": v}}}));
|
||||
}
|
||||
if let Some(v) = char_field(app, right, "Tracking", right_w, NumField::number("pct", c["tracking"].as_f64(), "", 0)) {
|
||||
|
||||
@@ -156,7 +156,7 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover());
|
||||
icons::paint(ui.painter(), r, "font-size", t.icon);
|
||||
const SIZES: &[f64] = &[6.0, 8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 18.0, 24.0, 30.0, 36.0, 48.0, 60.0, 72.0];
|
||||
if let Some(v) = NumField::number("tb_size", c["size"].as_f64(), " pt", 2).width(70.0).presets(SIZES).range(0.1, 1296.0).show(ui) {
|
||||
if let Some(v) = NumField::points("tb_size", c["size"].as_f64(), 2).width(70.0).presets(SIZES).range(0.1, 1296.0).show(ui) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"size": v}}));
|
||||
}
|
||||
sep(ui);
|
||||
@@ -237,7 +237,7 @@ fn object_controls(app: &mut DesignApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
const WEIGHTS: &[f64] = &[0.25, 0.5, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0];
|
||||
if let Some(v) = NumField::number("tb_sw", Some(i.stroke_weight), " pt", 3).width(64.0).spinner().presets(WEIGHTS).range(0.0, 800.0).show(ui) {
|
||||
if let Some(v) = NumField::points("tb_sw", Some(i.stroke_weight), 3).width(64.0).spinner().presets(WEIGHTS).range(0.0, 800.0).show(ui) {
|
||||
let _ = app.run("object.stroke", json!({"weight": v}));
|
||||
}
|
||||
sep(ui);
|
||||
|
||||
@@ -28,8 +28,11 @@ fn label_font() -> egui::FontId {
|
||||
pub enum NumKind<'a> {
|
||||
/// A length in points shown in `Unit`; accepts any unit and arithmetic.
|
||||
Measure(Unit),
|
||||
/// A plain number with a display suffix (`%`, `°`, ` pt`).
|
||||
/// A plain number with a display suffix (`%`, `°`).
|
||||
Number { suffix: &'a str, decimals: usize },
|
||||
/// A length in points shown as `12.5 pt` (type size, leading, stroke weight…). Like a measure
|
||||
/// it reads any unit and arithmetic (`13Q`, `5mm`, `9pt+1H`); a bare number is points.
|
||||
Points { decimals: usize },
|
||||
}
|
||||
|
||||
impl NumKind<'_> {
|
||||
@@ -42,6 +45,7 @@ impl NumKind<'_> {
|
||||
let s = if s == "-0" { "0".to_string() } else { s };
|
||||
format!("{s}{suffix}")
|
||||
}
|
||||
NumKind::Points { decimals } => NumKind::Number { suffix: " pt", decimals }.format(v),
|
||||
}
|
||||
}
|
||||
pub fn parse(&self, s: &str) -> Option<f64> {
|
||||
@@ -53,6 +57,7 @@ impl NumKind<'_> {
|
||||
s.chars().filter(|c| c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | '*' | '/' | '(' | ')' | ' ')).collect();
|
||||
parse_measure(cleaned.trim(), Unit::Points).ok().or_else(|| cleaned.trim().parse().ok())
|
||||
}
|
||||
NumKind::Points { .. } => parse_measure(s, Unit::Points).ok(),
|
||||
}
|
||||
}
|
||||
/// One spinner click (points for measures).
|
||||
@@ -63,7 +68,7 @@ impl NumKind<'_> {
|
||||
Unit::Inches | Unit::InchesDecimal => 72.0 / 16.0,
|
||||
Unit::Millimeters | Unit::Centimeters | Unit::Q | Unit::Ha => Unit::Millimeters.to_pt(1.0),
|
||||
},
|
||||
NumKind::Number { .. } => 1.0,
|
||||
NumKind::Number { .. } | NumKind::Points { .. } => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,9 +96,18 @@ impl<'a> NumField<'a> {
|
||||
pub fn measure(id: &'a str, value: Option<f64>, unit: Unit) -> Self {
|
||||
Self::new(id, value, NumKind::Measure(unit))
|
||||
}
|
||||
/// A number shown with `suffix` (`%`, `°`). A ` pt` suffix makes it a length in points that
|
||||
/// reads any unit ([`Self::points`]), so no point field reads `13Q` as 13 pt.
|
||||
pub fn number(id: &'a str, value: Option<f64>, suffix: &'a str, decimals: usize) -> Self {
|
||||
if suffix == " pt" {
|
||||
return Self::points(id, value, decimals);
|
||||
}
|
||||
Self::new(id, value, NumKind::Number { suffix, decimals })
|
||||
}
|
||||
/// A length in points (type size, leading, stroke weight…): shows `12 pt`, reads any unit.
|
||||
pub fn points(id: &'a str, value: Option<f64>, decimals: usize) -> Self {
|
||||
Self::new(id, value, NumKind::Points { decimals })
|
||||
}
|
||||
/// Initialize a numeric field with no name, label or empty text, a 72-point width, no spinner or presets, the full range and the enabled state.
|
||||
fn new(id: &'a str, value: Option<f64>, kind: NumKind<'a>) -> Self {
|
||||
NumField {
|
||||
@@ -290,8 +304,12 @@ impl<'a> NumField<'a> {
|
||||
// whatever was typed, as in InDesign.
|
||||
let escaped = ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
let typed_over_auto = self.parens && !buf.trim_start().starts_with('(');
|
||||
// Otherwise a field left as it shows its value commits nothing: reading the rounded
|
||||
// text back would round the value (13Q is 9.2126 pt, shown as 9.21 pt).
|
||||
let unchanged = !typed_over_auto && self.value.is_some_and(|old| buf.trim() == self.fmt(old).trim());
|
||||
if escaped {
|
||||
} else if let Some(v) = self.parse(&buf).map(clamp)
|
||||
&& !unchanged
|
||||
&& (typed_over_auto || self.value.is_none_or(|old| (old - v).abs() > 1e-9))
|
||||
{
|
||||
out = Some(v);
|
||||
@@ -845,6 +863,25 @@ mod tests {
|
||||
assert_eq!(m.step(), 1.0);
|
||||
}
|
||||
|
||||
/// A length field in points reads the unit typed, as InDesign's do (`12pt` in a Q field shows
|
||||
/// 16.933 Q): `13Q` is 9.213 pt, not 13 pt.
|
||||
#[test]
|
||||
fn points_kind_reads_any_unit() {
|
||||
let k = NumKind::Points { decimals: 2 };
|
||||
assert_eq!(k.format(12.5), "12.5 pt");
|
||||
let close = |s: &str, pt: f64| (k.parse(s).unwrap() - pt).abs() < 1e-9;
|
||||
assert!(close("12", 12.0) && close("12 pt", 12.0) && close("1p", 12.0));
|
||||
assert!(close("13Q", 13.0 * designcraft_geom::units::PT_PER_Q), "{:?}", k.parse("13Q"));
|
||||
assert!(close("22H", 22.0 * designcraft_geom::units::PT_PER_Q));
|
||||
assert!(close("5mm", 5.0 * designcraft_geom::units::PT_PER_MM));
|
||||
assert!(close("9pt+1H", 9.0 + designcraft_geom::units::PT_PER_Q));
|
||||
assert_eq!(k.parse("13x"), None, "an unknown unit is an invalid value, not 13 pt");
|
||||
// Every ` pt` field reads units, whichever constructor made it (the Control panel's Font
|
||||
// Size, Leading and Baseline Shift, Character ••• Baseline Shift…).
|
||||
assert_eq!(NumField::number("a", None, " pt", 2).kind, NumKind::Points { decimals: 2 });
|
||||
assert_eq!(NumField::number("a", None, "%", 0).kind, NumKind::Number { suffix: "%", decimals: 0 });
|
||||
}
|
||||
|
||||
/// A disabled field's preset button is disabled in AccessKit and cannot open a popup.
|
||||
#[test]
|
||||
fn disabled_numeric_presets_are_disabled_and_do_not_open() {
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
# CJK typesetting plan
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** trivial (status line and revision history added; content unchanged in the 2026-10-10 review) · **Target:** Adobe InDesign 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (mojikumi applied; consecutive punctuation) · **Target:** Adobe InDesign 2026
|
||||
|
||||
Goal: the Chinese, Japanese and Korean typesetting that InDesign offers with its East Asian
|
||||
feature set (the "Japanese/CJK" feature set, `Feature Set Locale Setting` 257), so that a
|
||||
@@ -37,7 +37,7 @@ presets or tables are copied. Our default mojikumi and kinsoku tables are built
|
||||
| Vertical metrics and shaping | done: upright runs shaped top to bottom (`vert`; `vkrn`/`vpal` when asked) with `vmtx`/`VORG` advances and origins, centred on the ideographic em box (`BASE`, else OS/2); turned runs shaped horizontally, without `vrt2`; Horizontal Kana (`hkna`) takes the vertical kana (`vkna`) in upright runs. Default location of variable fonts only (no `VVAR`) |
|
||||
| Kinsoku | named (hard, soft, Chinese, Korean) and custom kinsoku sets, bunri-kinshi, rensuuji, hanging punctuation (regular, force); without a set, fixed no-start / no-end lists and a break anywhere between CJK characters; kinsoku type (push in / push out priorities) kept but not applied |
|
||||
| Korean line breaking | at spaces (Hangul everywhere, hanja in Korean text); per-paragraph character-based breaking (`koreanCharBreaks`); DesignCraft-only, not in IDML |
|
||||
| Mojikumi | tables and paragraph references kept through IDML; not applied (Preflight says so) |
|
||||
| Mojikumi | applied in composition: the built-in base sets (line end half-width, line end one em, Simplified and Traditional Chinese) and the document's tables with their override rows, kept through IDML; Preflight notes tables it can't apply. In the Japanese sets consecutive punctuation closes up (JLREQ 3.1.4: 」。, 「『 touch; 」・ a quarter em, 」「 half an em apart). Still open: 、・ and ・・ take the sum of both blanks in JLREQ (Table 1 notes) |
|
||||
| Aki, tsume, jidori | done ([cjk-typography.md](cjk-typography.md)) |
|
||||
| Character alignment, leading model | em box top / centre / bottom, ICF top / bottom (`BASE` `icft`/`icfb`, else the em box inset by 5%), roman baseline; aki above / below, centre (centre down as centre); under aki below a line's leading is the space to the line below it; with the Japanese composers (`Composer::Japanese`, `JapaneseSingleLine`, IDML `HL Composer J` / `HL Single J`), the others set lines baseline to baseline on the Roman baseline |
|
||||
| Tate-chu-yoko | manual, with offsets; auto tate-chu-yoko of short digit runs (with roman letters when asked) in vertical text, narrowed to fit the em (`type.autoTateChuYoko`, IDML `AutoTcy`, `AutoTcyIncludeRoman`); manual groups of 3+ digits overflow the em |
|
||||
@@ -75,7 +75,7 @@ punctuation centred vs corner-placed).
|
||||
A *mojikumi set* is a table indexed by (class before, class after) giving the space between them
|
||||
as a min/optimum/max triple in em fractions, plus line-start and line-end rules. Sets are
|
||||
document resources (`Styles::mojikumi_tables`, read from and written to IDML with their override
|
||||
rows), referenced by name from the paragraph's `mojikumi`; what remains is applying them. We
|
||||
rows), referenced by name from the paragraph's `mojikumi`, and applied in composition. We
|
||||
ship a few built-in sets derived from JLREQ (no line-start indent, one-em indent, half-width
|
||||
punctuation, full-width punctuation…), all original data, for the IDML built-in names.
|
||||
|
||||
@@ -292,6 +292,7 @@ Each follows what an InDesign user expects.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | minor | Mojikumi is applied (since #129); consecutive punctuation closes up in the Japanese sets |
|
||||
| 2026-10-10 | trivial | Reviewed in the full re-measure; status line and this table added; summarized in [typography-parity.md](typography-parity.md) and [localization-parity.md](localization-parity.md) |
|
||||
| 2026-10-10 | minor | Updated (see git history) |
|
||||
| 2026-10-06 | major | Created: CJK typesetting plan and status (the Japanese/CJK feature set) |
|
||||
Reference in new issue
Block a user