mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Paragraph: the Hyphenation dialog sets each paragraph's hyphenation limits
The Paragraph panel menu's Hyphenation… was always disabled, and automatic hyphenation used fixed limits: words of 6 letters or more, 3 letters on each side of the hyphen, any number of hyphenated lines in a row, and capitalized words hyphenated. Each paragraph now has hyphenation options: Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone and Hyphenate Capitalized Words. The defaults are the fixed limits, and documents keep their line breaks. The options are saved only when they differ from the defaults. text.setFormat takes wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone and hyphenateCapitalized, and paragraph styles carry the options. Hyphen Limit applies with both composers. Hyphenation Zone applies with the Single-line Composer: a line whose text before the next word already ends within the zone breaks before that word instead of hyphenating it. Hyphenation… is enabled with type selected and opens a Hyphenation dialog (ui.hyphenation) on the values the Paragraph panel shows. OK runs text.setFormat with the values that changed, as one undo step. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -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, 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), 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), 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)",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -210,6 +210,12 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"spaceBefore",
|
||||
"spaceAfter",
|
||||
"hyphenate",
|
||||
"wordsLongerThan",
|
||||
"afterFirst",
|
||||
"beforeLast",
|
||||
"hyphenLimit",
|
||||
"hyphenationZone",
|
||||
"hyphenateCapitalized",
|
||||
"mojikumi",
|
||||
"kinsoku",
|
||||
"direction",
|
||||
@@ -346,6 +352,25 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = flag("hyphenate") {
|
||||
para.hyphenate = v;
|
||||
}
|
||||
let letters = |k: &str, lo: f64, hi: f64| num(k).map(|v| v.round().clamp(lo, hi) as u32);
|
||||
if let Some(v) = letters("wordsLongerThan", 1.0, 25.0) {
|
||||
para.hyphenation.words_longer_than = v;
|
||||
}
|
||||
if let Some(v) = letters("afterFirst", 1.0, 15.0) {
|
||||
para.hyphenation.after_first = v;
|
||||
}
|
||||
if let Some(v) = letters("beforeLast", 1.0, 15.0) {
|
||||
para.hyphenation.before_last = v;
|
||||
}
|
||||
if let Some(v) = letters("hyphenLimit", 0.0, 25.0) {
|
||||
para.hyphenation.hyphen_limit = v;
|
||||
}
|
||||
if let Some(v) = num("hyphenationZone") {
|
||||
para.hyphenation.zone = v.clamp(0.0, MAX_PARA);
|
||||
}
|
||||
if let Some(v) = flag("hyphenateCapitalized") {
|
||||
para.hyphenation.capitalized = v;
|
||||
}
|
||||
if let Some(v) = mojikumi {
|
||||
para.mojikumi = v;
|
||||
}
|
||||
|
||||
@@ -888,6 +888,48 @@ mod area_tests {
|
||||
assert_eq!(old.burasagari, Burasagari::None);
|
||||
}
|
||||
|
||||
/// Paragraph panel menu › Hyphenation…: `text.setFormat` sets each paragraph's hyphenation
|
||||
/// options, rounded and clamped, in one undo step; they are saved only when not the defaults,
|
||||
/// and paragraph styles carry them.
|
||||
#[test]
|
||||
fn hyphenation_options_are_set_per_paragraph_and_saved_only_when_not_default() {
|
||||
use vectorcraft_doc::HyphenationOptions;
|
||||
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, "text": "One\nTwo", "area": {"width": 200, "height": 100}})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let paras = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (t.para_at(0).clone(), t.para_at(1).clone()),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
assert_eq!(paras(&s).0.hyphenation, HyphenationOptions::default());
|
||||
assert!(serde_json::to_value(paras(&s).0).unwrap().get("hyphenation").is_none());
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute(
|
||||
"text.setFormat",
|
||||
&json!({"wordsLongerThan": 7.6, "afterFirst": 0, "beforeLast": 40, "hyphenLimit": 2, "hyphenationZone": 18, "hyphenateCapitalized": false, "start": 5, "end": 5}),
|
||||
)
|
||||
.unwrap();
|
||||
let want = HyphenationOptions { words_longer_than: 8, after_first: 1, before_last: 15, hyphen_limit: 2, zone: 18.0, capitalized: false };
|
||||
let (first, second) = paras(&s);
|
||||
assert_eq!((first.hyphenation, &second.hyphenation), (HyphenationOptions::default(), &want), "only the paragraph the caret is in");
|
||||
assert_eq!(serde_json::to_value(&second).unwrap()["hyphenation"]["hyphen_limit"], 2);
|
||||
let back: vectorcraft_doc::ParaStyle = serde_json::from_value(serde_json::to_value(&second).unwrap()).unwrap();
|
||||
assert_eq!(back.hyphenation, want);
|
||||
s.execute("text.setFormat", &json!({"hyphenLimit": -3, "hyphenationZone": 5000})).unwrap();
|
||||
assert_eq!((paras(&s).0.hyphenation.hyphen_limit, paras(&s).1.hyphenation.zone), (0, 1296.0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(paras(&s).1.hyphenation, HyphenationOptions::default());
|
||||
// A paragraph style carries them; an older file's paragraph reads the defaults.
|
||||
s.execute("paraStyle.new", &json!({"name": "Loose", "attrs": {"hyphenation": {"words_longer_than": 9}}})).unwrap();
|
||||
s.execute("paraStyle.apply", &json!({"name": "Loose", "id": id})).unwrap();
|
||||
assert_eq!(paras(&s).0.hyphenation, HyphenationOptions { words_longer_than: 9, ..HyphenationOptions::default() });
|
||||
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left", "hyphenate": true})).unwrap();
|
||||
assert_eq!(old.hyphenation, HyphenationOptions::default());
|
||||
}
|
||||
|
||||
/// #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
|
||||
|
||||
Reference in New Issue
Block a user