mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Merge pull request #2775 from VL420/feat/roman-hanging
G4: Paragraph panel menu › Roman Hanging Punctuation
This commit is contained in:
@@ -458,6 +458,10 @@ pub struct ParaStyle {
|
||||
/// Kinsoku (Paragraph panel › Kinsoku Set): which Japanese characters may not start or end a
|
||||
/// line.
|
||||
pub kinsoku: Kinsoku,
|
||||
/// Roman Hanging Punctuation (Paragraph panel menu): quotation marks, hyphens, full stops and
|
||||
/// commas starting or ending a line hang outside the margin, and asterisks, tildes, ellipses,
|
||||
/// dashes, colons and semicolons hang by half. East Asian punctuation hangs by burasagari.
|
||||
pub roman_hanging: bool,
|
||||
}
|
||||
|
||||
/// [`ParaStyle`] as saved. [`Justify::Auto`] is written as the alignment it has in the paragraph
|
||||
@@ -499,6 +503,8 @@ struct ParaStyleFile {
|
||||
burasagari: Burasagari,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
kinsoku: Kinsoku,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
roman_hanging: bool,
|
||||
}
|
||||
|
||||
impl From<ParaStyle> for ParaStyleFile {
|
||||
@@ -525,6 +531,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
roman_hanging,
|
||||
..
|
||||
} = p;
|
||||
Self {
|
||||
@@ -545,6 +552,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
roman_hanging,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,6 +576,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
roman_hanging,
|
||||
..
|
||||
} = f;
|
||||
Self {
|
||||
@@ -587,6 +596,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
roman_hanging,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -278,6 +278,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"digits",
|
||||
"fontAxes",
|
||||
"burasagari",
|
||||
"romanHangingPunctuation",
|
||||
"composer",
|
||||
];
|
||||
if !keys.iter().any(|k| p.get(*k).is_some()) {
|
||||
@@ -444,6 +445,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = burasagari {
|
||||
para.burasagari = v;
|
||||
}
|
||||
if let Some(v) = flag("romanHangingPunctuation") {
|
||||
para.roman_hanging = v;
|
||||
}
|
||||
if let Some(v) = composer {
|
||||
para.composer = v;
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ pub fn para_attrs(v: &ParaStyle) -> Map<String, Value> {
|
||||
("leading_model", json!(v.leading_model)),
|
||||
("burasagari", json!(v.burasagari)),
|
||||
("kinsoku", json!(v.kinsoku)),
|
||||
("roman_hanging", json!(v.roman_hanging)),
|
||||
] {
|
||||
m.insert(k.into(), x);
|
||||
}
|
||||
|
||||
@@ -1061,6 +1061,43 @@ mod area_tests {
|
||||
assert_eq!(old.hyphenation, HyphenationOptions::default());
|
||||
}
|
||||
|
||||
/// Paragraph panel menu › Roman Hanging Punctuation: `text.setFormat {romanHangingPunctuation}`
|
||||
/// sets it per paragraph in one undo step, the opening quotation mark then hangs left of the
|
||||
/// frame, it is saved only when on, and paragraph styles carry it.
|
||||
#[test]
|
||||
fn roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s
|
||||
.execute("text.create", &json!({"x": 10, "y": 10, "size": 40, "text": "“Quoted”\n“Two”", "area": {"width": 300, "height": 200}}))
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let text = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (**t).clone(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
let left = |s: &Session| text(s).cached_bounds.unwrap().x0;
|
||||
assert!(!text(&s).para_at(0).roman_hanging);
|
||||
assert!(serde_json::to_value(text(&s).para_at(0)).unwrap().get("roman_hanging").is_none());
|
||||
let inside = left(&s);
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
// Only the first paragraph (the caret's range).
|
||||
s.execute("text.setFormat", &json!({"romanHangingPunctuation": true, "start": 0, "end": 0})).unwrap();
|
||||
assert!(text(&s).para_at(0).roman_hanging && !text(&s).para_at(1).roman_hanging);
|
||||
assert!(left(&s) < inside - 5.0, "{} {inside}", left(&s));
|
||||
assert_eq!(serde_json::to_value(text(&s).para_at(0)).unwrap()["roman_hanging"], true);
|
||||
let back: vectorcraft_doc::ParaStyle = serde_json::from_value(serde_json::to_value(text(&s).para_at(0)).unwrap()).unwrap();
|
||||
assert!(back.roman_hanging);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!text(&s).para_at(0).roman_hanging);
|
||||
assert!((left(&s) - inside).abs() < 1e-9);
|
||||
// A paragraph style carries it.
|
||||
s.execute("paraStyle.new", &json!({"name": "Hung", "attrs": {"roman_hanging": true}})).unwrap();
|
||||
s.execute("paraStyle.apply", &json!({"name": "Hung", "id": id})).unwrap();
|
||||
assert!(text(&s).para_at(1).roman_hanging);
|
||||
}
|
||||
|
||||
/// #432: while the interface is in Japanese, new type (text.create, text.createInPath) starts
|
||||
/// with em box top-to-top leading and em box centre alignment, and new styles made from nothing
|
||||
/// carry them; params override; the values are journaled; imported text keeps the Roman
|
||||
|
||||
@@ -1354,6 +1354,12 @@ fn optical_share(g: &SGlyph) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Roman Hanging Punctuation: the share of glyph `g`'s advance that hangs outside the margin when
|
||||
/// `g` starts or ends a line: [`optical_share`]'s punctuation, and no letters.
|
||||
fn roman_share(g: &SGlyph) -> f64 {
|
||||
if g.ch.is_alphabetic() { 0.0 } else { optical_share(g) }
|
||||
}
|
||||
|
||||
/// A punctuation mark that hangs ([`optical_share`]), not a letter.
|
||||
fn optical_punct(g: &SGlyph) -> bool {
|
||||
!g.ch.is_alphabetic() && optical_share(g) > 0.0
|
||||
@@ -1364,20 +1370,20 @@ fn is_quote(c: char) -> bool {
|
||||
matches!(c, '\'' | '"' | '‘' | '’' | '‚' | '‛' | '“' | '”' | '„' | '‟')
|
||||
}
|
||||
|
||||
/// Optical Margin Alignment: how far a line hangs outside its left and right margins, from the
|
||||
/// glyph at each end of `line` (its glyphs in visual order, each with the advance it is set with)
|
||||
/// and its [`optical_share`], and how many glyphs hang at each end → (left, right, left glyphs,
|
||||
/// Optical Margin Alignment and Roman Hanging Punctuation: how far a line hangs outside its left
|
||||
/// and right margins, from the glyph at each end of `line` (its glyphs in visual order, each with
|
||||
/// the advance it is set with) and its `share` ([`optical_share`] or [`roman_share`]), and how many glyphs hang at each end → (left, right, left glyphs,
|
||||
/// right glyphs). A punctuation mark followed by a quotation mark hangs together with it: `."`
|
||||
/// ending a line and `'"` starting one hang both their glyphs. A line of one glyph hangs at
|
||||
/// neither end.
|
||||
fn optical_hangs(line: &[(&SGlyph, f64)]) -> (f64, f64, usize, usize) {
|
||||
fn optical_hangs(line: &[(&SGlyph, f64)], share: fn(&SGlyph) -> f64) -> (f64, f64, usize, usize) {
|
||||
let n = line.len();
|
||||
if n < 2 {
|
||||
return (0.0, 0.0, 0, 0);
|
||||
}
|
||||
let hang = |k: usize| line.get(k).map_or(0.0, |&(g, adv)| optical_share(g) * adv.max(0.0));
|
||||
let hang = |k: usize| line.get(k).map_or(0.0, |&(g, adv)| share(g) * adv.max(0.0));
|
||||
let punct = |k: usize| line.get(k).is_some_and(|&(g, _)| optical_punct(g));
|
||||
let quote = |k: usize| line.get(k).is_some_and(|&(g, _)| is_quote(g.ch) && optical_share(g) > 0.0);
|
||||
let quote = |k: usize| line.get(k).is_some_and(|&(g, _)| is_quote(g.ch) && share(g) > 0.0);
|
||||
let mut left = hang(0);
|
||||
// The glyphs that hang at the left; the right end takes none of them.
|
||||
let mut taken = usize::from(left > 0.0);
|
||||
@@ -1843,15 +1849,18 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
||||
// hang at both. A line that ends in a burasagari mark hangs only at its left.
|
||||
// Justification adds no space after a punctuation mark hanging at the left, nor before
|
||||
// one hanging at the right (`held`). A letter that hangs by part of its width keeps its
|
||||
// gaps.
|
||||
let (opt_l, opt_r, held) = if t.optical_margin && !rtl {
|
||||
// gaps. Roman Hanging Punctuation (a paragraph attribute) hangs the same punctuation
|
||||
// by the same amounts, and no letters; with Optical Margin Alignment on as well, the
|
||||
// letters hang too.
|
||||
let (opt_l, opt_r, held) = if (t.optical_margin || para.roman_hanging) && !rtl {
|
||||
let share: fn(&SGlyph) -> f64 = if t.optical_margin { optical_share } else { roman_share };
|
||||
let at: Vec<usize> = order.iter().map(|&offset| i + offset).filter(|&j| j < body).collect();
|
||||
let mut visible: Vec<(&SGlyph, f64)> =
|
||||
at.iter().filter_map(|&j| sg.get(j).map(|g| (g, if j + 1 == trimmed { g.adv - end_trim } else { g.adv }))).collect();
|
||||
if let Some(h) = &hyphen {
|
||||
visible.push((h, h.adv));
|
||||
}
|
||||
let (l, r, left_glyphs, right_glyphs) = optical_hangs(&visible);
|
||||
let (l, r, left_glyphs, right_glyphs) = optical_hangs(&visible, share);
|
||||
let (r, right_glyphs) = if hang.is_some() { (0.0, 0) } else { (r, right_glyphs) };
|
||||
let n_visible = visible.len();
|
||||
let punct = |k: usize| visible.get(k).is_some_and(|&(g, _)| optical_punct(g));
|
||||
|
||||
@@ -907,6 +907,46 @@ fn optical_margin_alignment_hangs_punctuation_and_letter_edges_outside_the_margi
|
||||
assert_eq!(rtl(true), rtl(false));
|
||||
}
|
||||
|
||||
/// Roman Hanging Punctuation (a paragraph attribute) hangs the punctuation Optical Margin
|
||||
/// Alignment hangs, by the same amounts, and no letters; only the paragraphs that have it hang.
|
||||
#[test]
|
||||
fn roman_hanging_punctuation_hangs_punctuation_but_not_letters() {
|
||||
let frame = Rect::new(0.0, 0.0, 300.0, 400.0);
|
||||
let lay = |text: &str, justify: Justify, on: bool| {
|
||||
let mut t = area(text, style(20.0), frame, justify);
|
||||
t.para.roman_hanging = on;
|
||||
layout(db(), &t).glyphs
|
||||
};
|
||||
let near = |a: f64, b: f64| (a - b).abs() < 1e-6;
|
||||
let end = |g: &PositionedGlyph| g.origin.x + g.advance;
|
||||
// Left aligned: the opening quotation mark hangs wholly.
|
||||
let on = lay("“Hello” there", Justify::Left, true);
|
||||
assert!(near(on[1].origin.x, 0.0) && near(on[0].origin.x, -on[0].advance) && on[0].advance > 1.0, "{:?}", on[0].origin);
|
||||
// An em dash hangs by half its advance.
|
||||
let dash = lay("—ab", Justify::Left, true);
|
||||
assert!(near(dash[0].origin.x, -0.5 * dash[0].advance), "{}", dash[0].origin.x);
|
||||
// Letters don't hang, where Optical Margin Alignment hangs an A by a tenth.
|
||||
assert!(near(lay("AVA", Justify::Left, true)[0].origin.x, 0.0));
|
||||
// Right aligned: the full stop and the closing quotation mark after it hang at the right.
|
||||
let r = lay("“Hi.”", Justify::Right, true);
|
||||
assert!(near(end(&r[2]), 300.0), "{}", end(&r[2]));
|
||||
// Justified: a hyphen at each end hangs, the words fill the frame between them.
|
||||
let j = lay("-ab cd-", Justify::JustifyAll, true);
|
||||
let n = j.len();
|
||||
assert!(near(j[1].origin.x, 0.0) && near(j[n - 1].origin.x, 300.0), "{} {}", j[1].origin.x, j[n - 1].origin.x);
|
||||
// Off: nothing hangs.
|
||||
assert!(near(lay("“Hello” there", Justify::Left, false)[0].origin.x, 0.0));
|
||||
// Per paragraph: the second paragraph, without it, doesn't hang.
|
||||
let mut t = area("“One\n“Two", style(20.0), frame, Justify::Left);
|
||||
let mut first = t.para.clone();
|
||||
first.roman_hanging = true;
|
||||
t.set_paragraph_styles(vec![first, t.para.clone()]);
|
||||
let l = layout(db(), &t);
|
||||
let at = |b: usize| l.glyphs.iter().find(|g| g.byte == b).unwrap().origin.x;
|
||||
assert!(at(0) < -1.0, "first paragraph hangs: {}", at(0));
|
||||
assert!(near(at("“One\n".len()), 0.0), "second doesn't: {}", at("“One\n".len()));
|
||||
}
|
||||
|
||||
/// With craft-fonts and no installed fonts (the web), Thai is set in Noto Sans Thai, not as empty
|
||||
/// boxes. Skipped when built without craft-fonts or with a checkout that has no Thai font.
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Paragraph panel: seven alignment buttons, Paragraph Direction (with the Indic options), indents,
|
||||
//! space before/after, Hyphenate, Mojikumi Set and Kinsoku Set (with the East Asian options); the panel menu
|
||||
//! opens Hyphenation… and picks the Single-line or Every-line Composer.
|
||||
//! turns Roman Hanging Punctuation on and off, opens Hyphenation… and picks the Single-line or
|
||||
//! Every-line Composer.
|
||||
|
||||
use egui::{Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
@@ -282,9 +283,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
set_pstate(ui.ctx(), "pa-hide-options", !hidden);
|
||||
}
|
||||
ui.separator();
|
||||
for l in [tl!("Roman Hanging Punctuation"), tl!("Justification…")] {
|
||||
menu_item(ui, l, false, false);
|
||||
// Quotation marks, hyphens, full stops and commas at a line's ends hang outside the margin.
|
||||
let roman = style.as_ref().is_some_and(|(_, p)| p.roman_hanging);
|
||||
if menu_item(ui, tl!("Roman Hanging Punctuation"), has, roman) {
|
||||
format(app, json!({"romanHangingPunctuation": !roman}));
|
||||
}
|
||||
menu_item(ui, tl!("Justification…"), false, false);
|
||||
if menu_item(ui, tl!("Hyphenation…"), has, false)
|
||||
&& let Err(e) = app.run("ui.hyphenation", json!({}))
|
||||
{
|
||||
@@ -336,7 +340,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
/// Reset Panel: the paragraphs the panel acts on ([`format`]) take Auto alignment, no indents or
|
||||
/// paragraph spacing, Hyphenate off with the default Hyphenation options, Auto direction,
|
||||
/// Bottom-to-Bottom Leading, Regular burasagari, the Hard kinsoku set and the Every-line Composer.
|
||||
/// Bottom-to-Bottom Leading, Regular burasagari, the Hard kinsoku set, the Every-line Composer and
|
||||
/// Roman Hanging Punctuation off.
|
||||
fn reset_panel(app: &mut VectorcraftApp) {
|
||||
let h = HyphenationOptions::default();
|
||||
para_cmd(app, "text.setStyle", json!({"justify": "auto"}));
|
||||
@@ -346,7 +351,8 @@ fn reset_panel(app: &mut VectorcraftApp) {
|
||||
"leftIndent": 0, "rightIndent": 0, "firstLineIndent": 0, "spaceBefore": 0, "spaceAfter": 0, "hyphenate": false,
|
||||
"wordsLongerThan": h.words_longer_than, "afterFirst": h.after_first, "beforeLast": h.before_last,
|
||||
"hyphenLimit": h.hyphen_limit, "hyphenationZone": h.zone, "hyphenateCapitalized": h.capitalized,
|
||||
"direction": "auto", "leadingModel": "romanBaseline", "burasagari": "standard", "kinsoku": "hard", "composer": "everyLine"
|
||||
"direction": "auto", "leadingModel": "romanBaseline", "burasagari": "standard", "kinsoku": "hard", "composer": "everyLine",
|
||||
"romanHangingPunctuation": false
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -574,8 +580,9 @@ mod tests {
|
||||
assert!(tips(&mut app) > without, "two more buttons");
|
||||
}
|
||||
|
||||
/// Reset Panel turns Hyphenate off and sets the Hyphenation options back to their defaults in
|
||||
/// every paragraph it acts on, so the paragraphs save no hyphenation options.
|
||||
/// Reset Panel turns Hyphenate and Roman Hanging Punctuation off and sets the Hyphenation
|
||||
/// options back to their defaults in every paragraph it acts on, so the paragraphs save no
|
||||
/// hyphenation options.
|
||||
#[test]
|
||||
fn reset_panel_resets_the_hyphenation_options() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
@@ -583,7 +590,10 @@ mod tests {
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "First\nSecond"})).unwrap()["id"].clone();
|
||||
app.run(
|
||||
"text.setFormat",
|
||||
json!({"hyphenate": true, "wordsLongerThan": 8, "afterFirst": 2, "beforeLast": 4, "hyphenLimit": 2, "hyphenationZone": 18, "hyphenateCapitalized": false}),
|
||||
json!({
|
||||
"hyphenate": true, "wordsLongerThan": 8, "afterFirst": 2, "beforeLast": 4, "hyphenLimit": 2, "hyphenationZone": 18, "hyphenateCapitalized": false,
|
||||
"romanHangingPunctuation": true
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let paras = |app: &VectorcraftApp| {
|
||||
@@ -594,12 +604,14 @@ mod tests {
|
||||
for para in paras(&app) {
|
||||
assert!(para.hyphenate);
|
||||
assert_ne!(para.hyphenation, HyphenationOptions::default());
|
||||
assert!(para.roman_hanging);
|
||||
}
|
||||
reset_panel(&mut app);
|
||||
for para in paras(&app) {
|
||||
assert!(!para.hyphenate);
|
||||
assert_eq!(para.hyphenation, HyphenationOptions::default());
|
||||
assert_eq!(serde_json::to_value(¶).unwrap().get("hyphenation"), None, "saves no hyphenation options");
|
||||
assert!(!para.roman_hanging, "Roman Hanging Punctuation off");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -187,7 +187,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
- **Improved:** mixed paragraph alignments leave the existing Paragraph/Properties alignment buttons unselected; choosing an alignment still uses the range-aware command. Headless regressions, full integration CI and visual review pass. Other mixed paragraph fields remain; estimate unchanged.
|
||||
- **Improved (#1258):** full font names (name ID 4) resolve the face instead of falling back, and Missing Fonts searches recognize the same aliases. Synthetic regressions cover loaded/cataloged fonts, localized names, real-family precedence and variable default faces. Windows font-service activation still needs reporter QA; estimate unchanged.
|
||||
- **Missing:** hyphenation and justification options, Optical Margin Alignment (type core); CJK vertical
|
||||
- **Done:** Paragraph panel menu › Roman Hanging Punctuation (#2756), per paragraph; estimate unchanged.
|
||||
- **Missing:** Justification… options (#2755, type core); CJK vertical
|
||||
composition (ruby, mojikumi sets and dialog, kinsoku settings #633, manual tate-chu-yoko), Middle Eastern
|
||||
features beyond bidi and digit types (Character Direction, kashidas, composers), variable font axes, spell check with an open dictionary,
|
||||
Touch Type, Retype, Snap to Glyph; optional preservation of font sizes during group scaling (#1034 enhancement).
|
||||
|
||||
+16
@@ -3531,6 +3531,22 @@ New type and documents from before it have it off, and it is saved only when on.
|
||||
{"name":"run_command","arguments":{"command":"type.opticalMarginAlignment","params":{"on":true}}}
|
||||
```
|
||||
|
||||
## Roman Hanging Punctuation
|
||||
|
||||
`text.setFormat {romanHangingPunctuation: bool}` (Paragraph panel menu › Roman Hanging Punctuation) turns it on or off
|
||||
for the paragraphs of the selected type (or `ids`, or the paragraphs a `start` / `end` range touches). In those
|
||||
paragraphs the punctuation Optical Margin Alignment hangs hangs outside the margin by the same amounts (quotation marks,
|
||||
hyphens, full stops and commas wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons by half),
|
||||
on the same sides (left-aligned lines at their left, right-aligned at their right, centered and justified at both), and
|
||||
letters don't hang. With Optical Margin Alignment on as well, the letters hang too. East Asian punctuation hangs only
|
||||
through burasagari; type on a path and right-to-left paragraphs are laid out as before. Off by default, saved with the
|
||||
paragraph (`roman_hanging` in `para`) only when on, carried by paragraph styles, and turned off by Reset Panel. One undo
|
||||
step. The menu item is checked while the paragraph the Paragraph panel shows has it on.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"romanHangingPunctuation":true}}}
|
||||
```
|
||||
|
||||
## Small Caps, Superscript and Subscript
|
||||
`text.setFormat {smallCaps, position}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end, smallCaps,
|
||||
position}` (a range) set what the Character panel's Small Caps, Superscript and Subscript buttons and menu items set.
|
||||
|
||||
@@ -35,6 +35,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Colour fonts (OpenType SVG / COLR) | partial | |
|
||||
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
|
||||
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, in point and area type of left-to-right paragraphs; type on a path and right-to-left paragraphs do not hang |
|
||||
| Roman Hanging Punctuation | done | Paragraph panel menu › Roman Hanging Punctuation, `text.setFormat {romanHangingPunctuation}` (#2756): per paragraph, Optical Margin Alignment's punctuation and amounts without the letters; paragraph styles carry it. Evidence: `tests::roman_hanging_punctuation_hangs_punctuation_but_not_letters`, `typecmd::area_tests::roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on` |
|
||||
| Hyphenation… and Justification… options | partial | Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310); Justification… missing (menu stub) |
|
||||
| Tab stops and leaders; Tabs panel | partial | Tabs panel and stops with leaders in the model (`text.tabs.set`); a stop drags from where the press landed (#1458); leaders fill the tab's gap, repeated on a grid so they line up from line to line (#1877) |
|
||||
| Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` |
|
||||
@@ -61,6 +62,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-17 | minor | Roman Hanging Punctuation (#2756): per-paragraph hanging of Latin punctuation outside the margin; scores unchanged |
|
||||
| 2026-10-17 | minor | A click on area type's red out port loads the Selection tool with the overflow text; the next click threads it into a closed path or a new frame of the same shape there, a drag into a new rectangle (`text.thread.add`, #2572 in part). The out port moved up the frame's right side, clear of the corner handle. Scores and hours unchanged. |
|
||||
| 2026-10-16 | minor | Native font-family labels fall back from English-only typographic names to localized legacy names (#2208). English labels and native typographic precedence are preserved; synthetic loaded/catalog tests. Scores and estimates unchanged. |
|
||||
| 2026-10-16 | minor | Align Stroke on live type: the characters' stroke goes inside or outside the glyphs without creating outlines, for every run or a range; kept in the native file and in canvas, PDF, EPS and SVG output (#2294); beyond Illustrator, scores and hours unchanged |
|
||||
|
||||
Reference in New Issue
Block a user