mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 03:48:01 +00:00
Merge pull request #5070 from dotCooCoo/feat/kinsoku-shori-type
Paragraph: Kinsoku Shori Type chooses Push In First, Push Out First or Push Out Only
This commit is contained in:
@@ -117,9 +117,9 @@ pub use text::FontHeightReference;
|
||||
pub use text::GlyphChoice;
|
||||
pub use text::{
|
||||
AreaFit, AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, Composer, Digits, FirstBaseline, HyphenationOptions, InlineArt,
|
||||
JustificationOptions, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign, RubyPosition,
|
||||
RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, VerticalAlign,
|
||||
WrapShape,
|
||||
JustificationOptions, Justify, Kinsoku, KinsokuShoriType, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign,
|
||||
RubyPosition, RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap,
|
||||
VerticalAlign, WrapShape,
|
||||
};
|
||||
pub use text::{CharClass, CompositeFont, CompositePart, CompositeParts};
|
||||
pub use trace::TraceView;
|
||||
|
||||
@@ -23,8 +23,8 @@ pub use composite::{
|
||||
CompositeParts,
|
||||
};
|
||||
pub use para_style::{
|
||||
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, LeadingModel, Mojikumi, ParaDirection,
|
||||
ParaStyle, SingleWordJustify,
|
||||
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, KinsokuShoriType, LeadingModel,
|
||||
Mojikumi, ParaDirection, ParaStyle, SingleWordJustify,
|
||||
};
|
||||
pub use runs::{INLINE_CHAR, INLINE_MAX_SCALE, InlineArt, RubyAlign, RubyPosition, RubySpan, TextKind, TextRun, TextStyleDef, TypeAntiAlias};
|
||||
pub use tabs_lists::{
|
||||
|
||||
@@ -234,6 +234,9 @@ pub struct ParaStyle {
|
||||
/// Kinsoku (Paragraph panel › Kinsoku Set): which Japanese characters may not start or end a
|
||||
/// line.
|
||||
pub kinsoku: Kinsoku,
|
||||
/// Kinsoku Shori Type (Paragraph panel menu): how a line keeps those characters off its start
|
||||
/// and end, by pushing them in onto the line or out to the next one. No effect without kinsoku.
|
||||
pub kinsoku_type: KinsokuShoriType,
|
||||
/// 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.
|
||||
@@ -322,6 +325,8 @@ struct ParaStyleFile {
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
kinsoku: Kinsoku,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
kinsoku_type: KinsokuShoriType,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
roman_hanging: bool,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
kashida: Kashida,
|
||||
@@ -353,6 +358,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
kinsoku_type,
|
||||
roman_hanging,
|
||||
kashida,
|
||||
..
|
||||
@@ -377,6 +383,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
kinsoku_type,
|
||||
roman_hanging,
|
||||
kashida,
|
||||
}
|
||||
@@ -405,6 +412,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
kinsoku_type,
|
||||
roman_hanging,
|
||||
kashida,
|
||||
..
|
||||
@@ -428,6 +436,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
leading_model,
|
||||
burasagari,
|
||||
kinsoku,
|
||||
kinsoku_type,
|
||||
roman_hanging,
|
||||
kashida,
|
||||
}
|
||||
@@ -492,6 +501,25 @@ pub enum Kinsoku {
|
||||
Soft,
|
||||
}
|
||||
|
||||
/// How a line keeps the characters [`Kinsoku`] rules out of its start and end (Kinsoku Shori
|
||||
/// Type). Pushing in sets the characters that would start the next line (or that an opening
|
||||
/// bracket ending this line would pull with it) on this line, with the line's gaps next to CJK
|
||||
/// characters closed to make room, each by the same share of the em of the character before it
|
||||
/// and by at most a tenth of that em; pushing out moves the character before them to the next
|
||||
/// line.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum KinsokuShoriType {
|
||||
/// Push in when the line can close up enough, else push out.
|
||||
PushInFirst,
|
||||
/// Push out, unless that would leave the line more than an em short (of the character before
|
||||
/// the break): then push in when the line can close up enough.
|
||||
PushOutFirst,
|
||||
/// Always push out; never push in.
|
||||
#[default]
|
||||
PushOutOnly,
|
||||
}
|
||||
|
||||
/// Hanging punctuation (burasagari): an East Asian comma or full stop ending a line (、。,.、。)
|
||||
/// stands outside the line's measure, in the space its punctuation spacing gives it (half width
|
||||
/// with Line-end Punctuation Half Width). Closing brackets and Latin punctuation don't hang. Shown
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -226,6 +226,7 @@ pub fn para_attrs(v: &ParaStyle) -> Map<String, Value> {
|
||||
("leading_model", json!(v.leading_model)),
|
||||
("burasagari", json!(v.burasagari)),
|
||||
("kinsoku", json!(v.kinsoku)),
|
||||
("kinsoku_type", json!(v.kinsoku_type)),
|
||||
("roman_hanging", json!(v.roman_hanging)),
|
||||
("kashida", json!(v.kashida)),
|
||||
] {
|
||||
|
||||
@@ -98,6 +98,43 @@ fn kinsoku_set_is_set_per_paragraph_and_saved_only_when_not_hard() {
|
||||
assert_eq!(old.kinsoku, Kinsoku::Hard);
|
||||
}
|
||||
|
||||
/// Paragraph panel menu › Kinsoku Shori Type: `text.setFormat {kinsokuType}` sets it per
|
||||
/// paragraph in one undo step, refuses unknown values, saves it only when it is not Push Out Only
|
||||
/// (new type's and older documents' type), reopens with the file, and paragraph styles carry it.
|
||||
#[test]
|
||||
fn kinsoku_shori_type_is_set_per_paragraph_and_saved_only_when_not_push_out_only() {
|
||||
use vectorcraft_doc::KinsokuShoriType;
|
||||
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": "あ」\nい", "area": {"width": 100, "height": 100}})).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"),
|
||||
};
|
||||
assert_eq!(text(&s).para_at(0).kinsoku_type, KinsokuShoriType::PushOutOnly);
|
||||
assert!(serde_json::to_value(text(&s).para_at(0)).unwrap().get("kinsoku_type").is_none());
|
||||
s.execute("text.setFormat", &json!({"id": id, "start": 0, "end": 1, "kinsokuType": "pushInFirst"})).unwrap();
|
||||
assert_eq!((text(&s).para_at(0).kinsoku_type, text(&s).para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
|
||||
assert_eq!(serde_json::to_value(text(&s).para_at(0)).unwrap()["kinsoku_type"], "pushInFirst");
|
||||
let reopened = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let Some(NodeKind::Text(back)) = reopened.node(NodeId(id)).map(|n| &n.kind) else { panic!("text") };
|
||||
assert_eq!((back.para_at(0).kinsoku_type, back.para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
|
||||
assert!(s.execute("text.setFormat", &json!({"id": id, "kinsokuType": "pushIn"})).is_err());
|
||||
assert_eq!(text(&s).para_at(0).kinsoku_type, KinsokuShoriType::PushInFirst, "an unknown value changes nothing");
|
||||
s.execute("text.setFormat", &json!({"id": id, "kinsokuType": "pushOutFirst"})).unwrap();
|
||||
assert_eq!(text(&s).para_at(1).kinsoku_type, KinsokuShoriType::PushOutFirst);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((text(&s).para_at(0).kinsoku_type, text(&s).para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
|
||||
// A paragraph style carries it.
|
||||
s.execute("paraStyle.new", &json!({"name": "Pushed in", "attrs": {"kinsoku_type": "pushOutFirst"}})).unwrap();
|
||||
s.execute("paraStyle.apply", &json!({"name": "Pushed in", "id": id})).unwrap();
|
||||
assert_eq!(text(&s).para_at(1).kinsoku_type, KinsokuShoriType::PushOutFirst);
|
||||
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left"})).unwrap();
|
||||
assert_eq!(old.kinsoku_type, KinsokuShoriType::PushOutOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burasagari_is_set_per_paragraph_and_saved_only_when_on() {
|
||||
use vectorcraft_doc::Burasagari;
|
||||
|
||||
@@ -7,8 +7,8 @@ use kurbo::{Affine, BezPath, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
|
||||
use unicode_bidi::{BidiInfo, Level};
|
||||
use vectorcraft_doc::text::{Kashida, ListAlign};
|
||||
use vectorcraft_doc::{
|
||||
Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect,
|
||||
RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject,
|
||||
Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, KinsokuShoriType, LeadingModel, Mojikumi, ParaDirection, ParaStyle,
|
||||
PathAlign, PathEffect, RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject,
|
||||
};
|
||||
use vectorcraft_geom::{ArcPath, PathData};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::region::Region;
|
||||
use super::vertical::stands_upright;
|
||||
use super::{
|
||||
Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, LeadingModel, SGlyph, hyphen_glyph,
|
||||
no_line_end, no_line_start, points_for, soft_line_start,
|
||||
Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, KinsokuShoriType, LeadingModel, SGlyph,
|
||||
hyphen_glyph, is_cjk, no_line_end, no_line_start, points_for, soft_line_start,
|
||||
};
|
||||
|
||||
/// Vertical metrics of a line: (ascent, descent, leading, cap height, x height).
|
||||
@@ -269,7 +270,10 @@ pub(super) fn word_width(g: &[SGlyph], i: usize) -> f64 {
|
||||
/// `hyphenate` holds the paragraph's Hyphenation options and the Hyphenation Zone in force when
|
||||
/// the line may hyphenate a word; a line whose text before the overflowing word already ends
|
||||
/// within the zone breaks before that word. A tab is as wide as it is set: up to its next stop
|
||||
/// (`tabs`), with the line's content starting `x_start` from the tab origin.
|
||||
/// (`tabs`), with the line's content starting `x_start` from the tab origin. Where kinsoku alone
|
||||
/// keeps the line from breaking before the character that overflows, `kinsoku_type` decides
|
||||
/// whether the line pushes characters in ([`push_in`]) or out; `hanging` is true when Optical
|
||||
/// Margin Alignment or Roman Hanging Punctuation is on, and `vertical` in vertical type.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn break_line(
|
||||
text: &str,
|
||||
@@ -281,6 +285,9 @@ pub(super) fn break_line(
|
||||
hyphenate: Option<(&HyphenationOptions, f64)>,
|
||||
burasagari: Burasagari,
|
||||
kinsoku: Kinsoku,
|
||||
kinsoku_type: KinsokuShoriType,
|
||||
hanging: bool,
|
||||
vertical: bool,
|
||||
) -> (usize, bool) {
|
||||
if !width.is_finite() {
|
||||
if let Some(pos) = g[i..].iter().position(|gl| gl.ch == '\u{2028}') {
|
||||
@@ -331,6 +338,11 @@ pub(super) fn break_line(
|
||||
}
|
||||
return (end, false);
|
||||
}
|
||||
if let Some(end) =
|
||||
push_in(g, i, j, cluster_end, x + cluster_width, width, last_break.map(|b| b.0), kinsoku, kinsoku_type, hanging, vertical)
|
||||
{
|
||||
return (end, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
x += cluster_width;
|
||||
@@ -493,6 +505,137 @@ fn kinsoku_between(before: char, after: Option<char>, kinsoku: Kinsoku) -> bool
|
||||
}
|
||||
}
|
||||
|
||||
/// The share of an em by which a push-in ([`push_in`]) may close each gap next to a CJK character
|
||||
/// on the line, measured on the character before the gap.
|
||||
pub(super) const PUSH_IN_SQUEEZE: f64 = 0.1;
|
||||
|
||||
/// The most glyphs after the overflowing cluster a push-in ([`push_in`]) takes onto the line; a
|
||||
/// longer run of characters kinsoku keeps together pushes out.
|
||||
const PUSH_IN_RUN: usize = 16;
|
||||
|
||||
/// Kinsoku Shori Type: the end of the line from `i` that overflows `width` at the cluster
|
||||
/// `j..cluster_end` (the line is `x` wide through it) when the line pushes in, or None when it
|
||||
/// pushes out. A line pushes in only where kinsoku rules out the break before `j`: a character
|
||||
/// that may not end a line comes before it (an opening bracket or quotation mark, Latin ones
|
||||
/// included), or one that may not start a line comes at `j`. It then takes the characters up to
|
||||
/// the next break kinsoku allows (not at a soft hyphen), and the spaces after it, when closing
|
||||
/// its gaps next to CJK characters, each by up to [`PUSH_IN_SQUEEZE`] of an em of the character
|
||||
/// before it, makes room for them. Push In First tries this before it pushes out. Push Out First
|
||||
/// tries it only when pushing out, to the line's last allowed break `pushed_out`, would leave the
|
||||
/// line short by more than an em of the character before `j`. Push Out Only never pushes in, and
|
||||
/// neither does a line holding a tab or right-to-left text, nor one where the characters taken in
|
||||
/// reach a space after a character that may not end a line. With `hanging` (Optical Margin
|
||||
/// Alignment or Roman Hanging Punctuation), the gaps that hanging punctuation keeps open are not
|
||||
/// counted. `vertical`: the line is a column of vertical type ([`squeeze_em`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn push_in(
|
||||
g: &[SGlyph],
|
||||
i: usize,
|
||||
j: usize,
|
||||
cluster_end: usize,
|
||||
x: f64,
|
||||
width: f64,
|
||||
pushed_out: Option<usize>,
|
||||
kinsoku: Kinsoku,
|
||||
kinsoku_type: KinsokuShoriType,
|
||||
hanging: bool,
|
||||
vertical: bool,
|
||||
) -> Option<usize> {
|
||||
if kinsoku == Kinsoku::None || kinsoku_type == KinsokuShoriType::PushOutOnly {
|
||||
return None;
|
||||
}
|
||||
let before = g.get(j.checked_sub(1)?)?;
|
||||
if before.len == 0 || g.get(j)?.no_break || splits_no_break(g, j) || kinsoku_allows(g, j - 1, kinsoku) {
|
||||
return None;
|
||||
}
|
||||
let allowed = |k: usize| {
|
||||
k >= g.len()
|
||||
|| (g[k].byte != g[k - 1].byte
|
||||
&& kinsoku_allows(g, k - 1, kinsoku)
|
||||
&& (g[k].is_space() || (g[k - 1].break_after() && !g[k - 1].is_soft_hyphen())))
|
||||
};
|
||||
let (mut end, mut x) = (cluster_end, x);
|
||||
while !allowed(end) {
|
||||
let gl = &g[end];
|
||||
// A space here follows a character kinsoku keeps off a line's end: the line pushes out.
|
||||
if gl.is_space() || gl.ch == '\t' || gl.ch == '\u{2028}' || gl.no_break || end - cluster_end >= PUSH_IN_RUN {
|
||||
return None;
|
||||
}
|
||||
x += gl.adv;
|
||||
end += 1;
|
||||
}
|
||||
// A tab's width depends on where it falls; a line with one (other than a list marker's)
|
||||
// pushes out.
|
||||
if g[i..end].iter().any(|h| h.ch == '\t' && h.len > 0) {
|
||||
return None;
|
||||
}
|
||||
// With `hanging`, punctuation keeps the gap after a mark that may hang at the line's start,
|
||||
// and the gap before one that may hang at its end ([`optical_hangs`]): those gaps are not
|
||||
// counted.
|
||||
let mut last = end;
|
||||
while last > i && g[last - 1].is_space() {
|
||||
last -= 1;
|
||||
}
|
||||
// A line holding right-to-left text is reordered before it is drawn, and justification closes
|
||||
// the gaps between its glyphs in that order: it pushes out.
|
||||
if g[i..last].iter().any(|h| h.len > 0 && h.level.is_rtl()) {
|
||||
return None;
|
||||
}
|
||||
let punct = |k: usize| g.get(k).is_some_and(optical_punct);
|
||||
let quote = |k: usize| g.get(k).is_some_and(|h| is_quote(h.ch));
|
||||
let may_hang = |a: usize| {
|
||||
hanging
|
||||
&& ((a == i && punct(i))
|
||||
|| (a == i + 1 && punct(i) && quote(i + 1))
|
||||
|| (a + 2 == last && punct(last - 1))
|
||||
|| (a + 3 == last && punct(last - 2) && quote(last - 1)))
|
||||
};
|
||||
let room: f64 = (i..end - 1).filter(|&a| closes(g, a) && !may_hang(a)).map(|a| PUSH_IN_SQUEEZE * squeeze_em(&g[a], vertical)).sum();
|
||||
if x - width > room + EPS {
|
||||
return None;
|
||||
}
|
||||
if kinsoku_type == KinsokuShoriType::PushOutFirst {
|
||||
let mut kept = pushed_out.filter(|&b| b > i).unwrap_or(i);
|
||||
while kept > i && g[kept - 1].is_space() {
|
||||
kept -= 1;
|
||||
}
|
||||
let short = width - g[i..kept].iter().map(|h| h.adv).sum::<f64>();
|
||||
if short <= squeeze_em(before, vertical) + EPS {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
while g.get(end).is_some_and(SGlyph::is_space) {
|
||||
end += 1;
|
||||
}
|
||||
if g.get(end).is_some_and(|h| h.ch == '\u{2028}') {
|
||||
end += 1;
|
||||
}
|
||||
Some(end)
|
||||
}
|
||||
|
||||
/// May a push-in close the gap after glyph `a`? A gap between two characters, at least one of them
|
||||
/// CJK, outside a tate-chu-yoko block, a list marker and a shaped cluster, and not next to a space,
|
||||
/// a tab or an inline graphic before it.
|
||||
fn closes(g: &[SGlyph], a: usize) -> bool {
|
||||
let (Some(p), Some(n)) = (g.get(a), g.get(a + 1)) else { return false };
|
||||
p.len > 0
|
||||
&& p.byte != n.byte
|
||||
&& p.inline.is_none()
|
||||
&& !p.is_space()
|
||||
&& !n.is_space()
|
||||
&& p.ch != '\t'
|
||||
&& !n.continues_tcy()
|
||||
&& (is_cjk(p.ch) || is_cjk(n.ch))
|
||||
}
|
||||
|
||||
/// The em a push-in measures in, along the line: `g`'s em at its Horizontal Scale, as Mojikumi's
|
||||
/// spacing is, or at its Vertical Scale when it stands upright in vertical type and the font's
|
||||
/// vertical metrics set its length down the column ([`super::vertical::upright_cell`]).
|
||||
pub(super) fn squeeze_em(g: &SGlyph, vertical: bool) -> f64 {
|
||||
let down_the_column = vertical && stands_upright(g) && g.face.vertical_glyph(g.gid).is_some();
|
||||
g.face.units_per_em() * if down_the_column { g.sy } else { g.sx }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn kinsoku_keeps_closing_marks_off_line_starts_and_opening_ones_off_line_ends() {
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
use super::bidi::{is_rtl, para_bidi_with, visual_order};
|
||||
use super::breaking::{
|
||||
Metrics, Pen, break_line, candidates, hangs, leader_outline, optical_hangs, optical_hangs_rtl, optical_punct, optical_share, roman_share,
|
||||
set_width, tab_advance, word_width,
|
||||
Metrics, PUSH_IN_SQUEEZE, Pen, break_line, candidates, hangs, leader_outline, optical_hangs, optical_hangs_rtl, optical_punct, optical_share,
|
||||
roman_share, set_width, squeeze_em, tab_advance, word_width,
|
||||
};
|
||||
use super::region::Region;
|
||||
use super::vertical::{EM_CENTER, align_shift, compress_punctuation, glyph_em, punct_half, space_japanese_and_latin};
|
||||
use super::{
|
||||
Affine, BidiInfo, Burasagari, CharStyle, Composer, Ctx, EPS, Flex, Justify, LeadingModel, LineFit, LineInfo, ListMarker, Mojikumi, ParaStyle,
|
||||
Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose, fit_line, hyphen_glyph, is_cjk,
|
||||
is_word_space, kashida, mark_word_ends, style_metrics,
|
||||
Affine, BidiInfo, Burasagari, CharStyle, Composer, Ctx, EPS, Flex, Justify, Kinsoku, KinsokuShoriType, LeadingModel, LineFit, LineInfo,
|
||||
ListMarker, Mojikumi, ParaStyle, Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose,
|
||||
fit_line, hyphen_glyph, is_cjk, is_word_space, kashida, mark_word_ends, no_line_end, no_line_start, style_metrics,
|
||||
};
|
||||
|
||||
/// Does paragraph `para` push characters in (Kinsoku Shori Type Push In First or Push Out First,
|
||||
/// with kinsoku on)?
|
||||
fn pushes_in(para: &ParaStyle) -> bool {
|
||||
para.kinsoku != Kinsoku::None && para.kinsoku_type != KinsokuShoriType::PushOutOnly
|
||||
}
|
||||
|
||||
/// Paragraphs longer than this (glyphs) are always broken line by line.
|
||||
const MAX_COMPOSE_GLYPHS: usize = 200_000;
|
||||
|
||||
@@ -32,6 +38,8 @@ fn compose_para(
|
||||
// A paragraph whose commas or full stops may hang is composed line by line (as the Japanese
|
||||
// single-line composer does).
|
||||
let may_hang = para.burasagari != Burasagari::None && sg.iter().any(hangs);
|
||||
// So is a paragraph that may push characters kinsoku keeps off a line's ends in onto a line.
|
||||
let may_push_in = pushes_in(para) && sg.iter().any(|g| no_line_start(g.ch) || no_line_end(g.ch));
|
||||
// A tab's width depends on where it falls in its line, which the composer's fixed glyph widths
|
||||
// can't follow: such a paragraph is broken line by line ([`break_line`] measures its tabs).
|
||||
// The tab of a list marker is on the first line, with the marker, and has a fixed width.
|
||||
@@ -42,6 +50,7 @@ fn compose_para(
|
||||
|| sg.len() < 2
|
||||
|| sg.len() > MAX_COMPOSE_GLYPHS
|
||||
|| may_hang
|
||||
|| may_push_in
|
||||
|| has_tab
|
||||
|| has_soft_break
|
||||
{
|
||||
@@ -285,6 +294,8 @@ struct PlacementContext<'a> {
|
||||
zone: f64,
|
||||
index: usize,
|
||||
para_line: usize,
|
||||
/// Optical Margin Alignment or Roman Hanging Punctuation is on.
|
||||
hanging: bool,
|
||||
}
|
||||
|
||||
struct LinePosition {
|
||||
@@ -293,13 +304,16 @@ struct LinePosition {
|
||||
x1: f64,
|
||||
end: usize,
|
||||
hyph: bool,
|
||||
/// Kinsoku Shori Type pushed characters in onto the line ([`break_line`]).
|
||||
pushed_in: bool,
|
||||
m: Metrics,
|
||||
ind_l: f64,
|
||||
ind_r: f64,
|
||||
}
|
||||
|
||||
fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementContext<'_>) -> Option<LinePosition> {
|
||||
let PlacementContext { para, pr, sg, continued, rtl, mirror, pm, composed, hyphen_run, hyphen_limit, zone, index: i, para_line } = context;
|
||||
let PlacementContext { para, pr, sg, continued, rtl, mirror, pm, composed, hyphen_run, hyphen_limit, zone, index: i, para_line, hanging } =
|
||||
context;
|
||||
let n = sg.len();
|
||||
let est = if *i < n { Metrics::of(&sg[*i]) } else { *pm };
|
||||
let first_line = *para_line == 0 && !*continued;
|
||||
@@ -328,10 +342,17 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
|
||||
};
|
||||
let width = x1 - x0 - ind_l - ind_r;
|
||||
let hyphenate = (para.hyphenate && (*hyphen_limit == 0 || *hyphen_run < *hyphen_limit)).then_some((¶.hyphenation, *zone));
|
||||
let (end, hyph) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
|
||||
Some(&(end, hyph)) if end > *i => (end, hyph),
|
||||
_ if *i < n => break_line(cx.text, sg, *i, width, ¶.tabs, ind_l, hyphenate, para.burasagari, para.kinsoku),
|
||||
_ => (n, false),
|
||||
let (end, hyph, pushed_in) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
|
||||
Some(&(end, hyph)) if end > *i => (end, hyph, false),
|
||||
_ if *i < n => {
|
||||
let line =
|
||||
|ty| break_line(cx.text, sg, *i, width, ¶.tabs, ind_l, hyphenate, para.burasagari, para.kinsoku, ty, *hanging, cx.vertical);
|
||||
let (end, hyph) = line(para.kinsoku_type);
|
||||
// The line pushed characters in where it ends after the line that pushes out.
|
||||
let pushed_in = pushes_in(para) && end > line(KinsokuShoriType::PushOutOnly).0;
|
||||
(end, hyph, pushed_in)
|
||||
}
|
||||
_ => (n, false, false),
|
||||
};
|
||||
let m = Metrics::max(&sg[*i..end]).unwrap_or(*pm);
|
||||
baseline += if same_baseline || (pen.regions.is_none() && first_in_region) {
|
||||
@@ -353,7 +374,7 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
|
||||
pen.queued.clear();
|
||||
continue;
|
||||
}
|
||||
return Some(LinePosition { baseline, x0, x1, end, hyph, m, ind_l, ind_r });
|
||||
return Some(LinePosition { baseline, x0, x1, end, hyph, pushed_in, m, ind_l, ind_r });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +402,10 @@ struct LineJustification {
|
||||
per_space: f64,
|
||||
per_gap: f64,
|
||||
per_cjk: f64,
|
||||
/// Kinsoku Shori Type: the share of an em (of the glyph before it, at most
|
||||
/// [`PUSH_IN_SQUEEZE`]) each gap next to a CJK character closes by on a line that pushed
|
||||
/// characters in; 0 on other lines.
|
||||
squeeze: f64,
|
||||
fit: LineFit,
|
||||
start_x: f64,
|
||||
after: Vec<Option<usize>>,
|
||||
@@ -415,6 +440,7 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
|
||||
zone: paragraph.zone,
|
||||
index,
|
||||
para_line: line_in_paragraph,
|
||||
hanging: t.optical_margin || paragraph.para.roman_hanging,
|
||||
};
|
||||
let Some(position) = place_flow_line(cx, pen, &mut placement) else { return false };
|
||||
hyphen_run = if position.hyph && position.end > index { hyphen_run + 1 } else { 0 };
|
||||
@@ -572,12 +598,13 @@ fn drawn_after(index: usize, glyph: usize, after: &[Option<usize>]) -> Option<us
|
||||
}
|
||||
|
||||
// Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the
|
||||
// gaps next to a CJK character, not inside a Latin word or a tate-chu-yoko block.
|
||||
// gaps next to a CJK character, not inside a Latin word, a tate-chu-yoko block or a shaped
|
||||
// cluster.
|
||||
fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &[SGlyph]) -> bool {
|
||||
!held.contains(&glyph)
|
||||
&& drawn_after(index, glyph, after)
|
||||
.and_then(|next| sg.get(glyph).zip(sg.get(next)))
|
||||
.is_some_and(|(a, b)| !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch)))
|
||||
&& drawn_after(index, glyph, after).and_then(|next| sg.get(glyph).zip(sg.get(next))).is_some_and(|(a, b)| {
|
||||
a.byte != b.byte && !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch))
|
||||
})
|
||||
}
|
||||
|
||||
// What justification may change of glyph `j`: nothing of a `held` glyph, and no glyph
|
||||
@@ -639,7 +666,18 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
let width = line.render_width + kashidas.iter().map(|k| k.1).sum::<f64>();
|
||||
let (mut per_space, mut per_gap, mut per_cjk) = (0.0, 0.0, 0.0);
|
||||
let mut fit = LineFit::default();
|
||||
if justify && (room - width).abs() > EPS {
|
||||
let mut squeeze = 0.0;
|
||||
// Kinsoku Shori Type: a line of area type that pushed characters in is wider than its
|
||||
// measure, and closes its gaps next to CJK characters to fit, however it is aligned: each by
|
||||
// the same share of the em of the glyph before it, at most [`PUSH_IN_SQUEEZE`].
|
||||
let pushed_in = line.position.pushed_in && paragraph.regions.is_some() && cjk_gaps > 0 && width > room + EPS;
|
||||
if pushed_in {
|
||||
let ems: f64 =
|
||||
(line.index..line.body).filter(|&j| is_cjk_gap(j, held, &after, line.index, sg)).map(|j| squeeze_em(&sg[j], cx.vertical)).sum();
|
||||
if ems > EPS {
|
||||
squeeze = ((room - width) / ems).max(-PUSH_IN_SQUEEZE);
|
||||
}
|
||||
} else if justify && (room - width).abs() > EPS {
|
||||
if cjk_gaps > 0 && room > width {
|
||||
// Spread over the CJK gaps and the word spaces alike.
|
||||
per_space = (room - width) / (cjk_gaps + spaces) as f64;
|
||||
@@ -667,7 +705,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
}
|
||||
}
|
||||
let position = &line.position;
|
||||
let start_x = if justify {
|
||||
let start_x = if justify || pushed_in {
|
||||
line.ax0 - line.opt_l
|
||||
} else if paragraph.regions.is_none() {
|
||||
match align {
|
||||
@@ -682,7 +720,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
|
||||
_ => line.ax1 + line.opt_r - width,
|
||||
}
|
||||
};
|
||||
LineJustification { per_space, per_gap, per_cjk, fit, start_x, after, kashidas }
|
||||
LineJustification { per_space, per_gap, per_cjk, squeeze, fit, start_x, after, kashidas }
|
||||
}
|
||||
|
||||
fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData<'_, '_>, line: &LineGeometry, justification: &LineJustification) {
|
||||
@@ -738,9 +776,9 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
}
|
||||
if glyph.is_space() {
|
||||
advance += justification.per_space;
|
||||
} else if justification.per_cjk != 0.0 {
|
||||
} else if justification.per_cjk != 0.0 || justification.squeeze != 0.0 {
|
||||
if is_cjk_gap(glyph_index, &line.held, &justification.after, index, sg) {
|
||||
advance += justification.per_cjk;
|
||||
advance += justification.per_cjk + justification.squeeze * squeeze_em(glyph, cx.vertical);
|
||||
}
|
||||
} else if !line.held.contains(&glyph_index)
|
||||
&& drawn_after(index, glyph_index, &justification.after).and_then(|next| sg.get(next)).is_some_and(|next| !next.continues_tcy())
|
||||
|
||||
@@ -78,7 +78,20 @@ fn indic_hyphenation_reaches_both_composers_without_splitting_marks() {
|
||||
}
|
||||
let cands = candidates(&text, &glyphs, Some(&opts), Kinsoku::None);
|
||||
assert_eq!(cands.iter().filter(|b| b.hyphen > 0.0).map(|b| b.end).collect::<Vec<_>>(), (2..16).step_by(2).collect::<Vec<_>>(), "{tag}");
|
||||
let (end, hyphen) = break_line(&text, &glyphs, 0, 4.5, &[], 0.0, Some((&opts, 0.0)), Burasagari::None, Kinsoku::None);
|
||||
let (end, hyphen) = break_line(
|
||||
&text,
|
||||
&glyphs,
|
||||
0,
|
||||
4.5,
|
||||
&[],
|
||||
0.0,
|
||||
Some((&opts, 0.0)),
|
||||
Burasagari::None,
|
||||
Kinsoku::None,
|
||||
KinsokuShoriType::PushOutOnly,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert_eq!((end, hyphen), (4, true), "{tag}");
|
||||
let params = Params { justify_last: false, ragged: true, tolerance: 1.0, uniform_from: 0, hyphen_limit: 0 };
|
||||
let lines = compose(&glyphs, &|_| 4.5, &|_| 0.0, &cands, &[], ¶ms).expect("every-line composition");
|
||||
@@ -148,7 +161,8 @@ fn greedy_wrap_keeps_an_oversized_first_cluster_and_its_source_range() {
|
||||
let text = "بَت";
|
||||
let glyphs = synthetic_glyphs(&[(0, 4, 2.0, 'ب'), (0, 4, 2.0, 'ب'), (4, 2, 1.0, 'ت')]);
|
||||
|
||||
let (end, hyphenated) = break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
|
||||
let (end, hyphenated) =
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
|
||||
assert!(!hyphenated);
|
||||
assert_eq!(end, 2, "oversized base-plus-mark cluster must stay together");
|
||||
@@ -161,11 +175,13 @@ fn greedy_wrap_keeps_an_oversized_cluster_after_a_line_start_atomic() {
|
||||
let text = "بَتَث";
|
||||
let glyphs = synthetic_glyphs(&[(0, 4, 1.0, 'ب'), (4, 4, 2.0, 'ت'), (4, 4, 2.0, 'ت'), (8, 2, 1.0, 'ث')]);
|
||||
|
||||
let (first_end, _) = break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
|
||||
let (first_end, _) =
|
||||
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
assert_eq!(first_end, 1);
|
||||
assert_eq!(source_range(&glyphs, 0, first_end), 0..4);
|
||||
|
||||
let (second_end, hyphenated) = break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
|
||||
let (second_end, hyphenated) =
|
||||
break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
|
||||
|
||||
assert!(!hyphenated);
|
||||
assert_eq!(second_end, 3, "oversized cluster at the next line start must stay together");
|
||||
@@ -220,7 +236,8 @@ fn narrow_arabic_area_wrap_keeps_base_and_mark_together_when_supported() {
|
||||
fn no_break_keeps_marked_characters_on_one_line() {
|
||||
let text = "A B";
|
||||
let plain = synthetic_glyphs(&[(0, 1, 10.0, 'A'), (1, 1, 4.0, ' '), (2, 1, 10.0, 'B')]);
|
||||
let (end, hyphen) = break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
|
||||
let (end, hyphen) =
|
||||
break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
assert!(!hyphen);
|
||||
assert_eq!(end, 2, "without the mark the line ends after the space");
|
||||
|
||||
@@ -228,7 +245,8 @@ fn no_break_keeps_marked_characters_on_one_line() {
|
||||
for g in &mut marked {
|
||||
g.no_break = true;
|
||||
}
|
||||
let (end, hyphen) = break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
|
||||
let (end, hyphen) =
|
||||
break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
assert!(!hyphen);
|
||||
assert_eq!(end, 3, "the marked words stay together even though they are wider than the line");
|
||||
|
||||
@@ -237,11 +255,121 @@ fn no_break_keeps_marked_characters_on_one_line() {
|
||||
let mut glyphs = synthetic_glyphs(&[(0, 1, 8.0, 'A'), (1, 1, 2.0, ' '), (2, 3, 10.0, '日'), (5, 3, 10.0, '本')]);
|
||||
glyphs[2].no_break = true;
|
||||
glyphs[3].no_break = true;
|
||||
let (end, _) = break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
|
||||
let (end, _) = break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
assert_eq!(end, 2, "the marked pair moves to the next line together");
|
||||
let (end, _) = break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
|
||||
let (end, _) = break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
|
||||
assert_eq!(end, 4, "on its own line the pair stays together even when it is wider than the line");
|
||||
|
||||
let breaks = candidates(text, &glyphs, None, Kinsoku::None);
|
||||
assert!(breaks.iter().all(|b| b.end != 3), "no break between the marked characters: {breaks:?}");
|
||||
}
|
||||
|
||||
/// Synthetic full-width characters `text`, each 10 pt wide on a 10 pt em.
|
||||
fn full_width(text: &str) -> Vec<SGlyph> {
|
||||
let specs: Vec<(usize, usize, f64, char)> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), 10.0, ch)).collect();
|
||||
let mut glyphs = synthetic_glyphs(&specs);
|
||||
for g in &mut glyphs {
|
||||
g.sx = 10.0 / g.face.units_per_em();
|
||||
g.sy = g.sx;
|
||||
}
|
||||
glyphs
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type, in a measure of 15 full-width characters: a closing bracket that would
|
||||
/// start the next line is pushed in onto the line with Push In First, and the character before
|
||||
/// it is pushed out with it with Push Out First and Push Out Only. Push Out First pushes in when
|
||||
/// pushing out would leave the line two characters short (an opening bracket goes too). A line
|
||||
/// that cannot close up enough pushes out, and with Kinsoku None the types do nothing.
|
||||
#[test]
|
||||
fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() {
|
||||
use KinsokuShoriType::{PushInFirst, PushOutFirst, PushOutOnly};
|
||||
let hung = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType, hanging: bool| {
|
||||
break_line(text, &full_width(text), 0, width, &[], 0.0, None, Burasagari::None, kinsoku, ty, hanging, false).0
|
||||
};
|
||||
let end = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType| hung(text, width, kinsoku, ty, false);
|
||||
let text = "一二三四五六七八九十一二三四五」六";
|
||||
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushInFirst), 16, "」 is pushed in");
|
||||
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushOutFirst), 14, "五 is pushed out with 」");
|
||||
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushOutOnly), 14, "五 is pushed out with 」");
|
||||
assert_eq!(end(text, 150.0, Kinsoku::Soft, PushInFirst), 16, "Soft keeps 」 off the line start too");
|
||||
assert_eq!(end(text, 150.0, Kinsoku::None, PushInFirst), 15, "no kinsoku: 」 starts the next line");
|
||||
|
||||
let bracketed = "一二三四五六七八九十一二三「四」六";
|
||||
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushOutFirst), 16, "pushing out 「四 would leave two ems");
|
||||
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushInFirst), 16);
|
||||
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushOutOnly), 13);
|
||||
|
||||
// Latin brackets and quotation marks are pushed in too: an opening one that would end the
|
||||
// line, and a closing one after a Latin letter that would start the next.
|
||||
for opening in ["一二三四五六七八九十一二三四(五六", "一二三四五六七八九十一二三四\u{201c}五六"] {
|
||||
assert_eq!(end(opening, 150.0, Kinsoku::Hard, PushInFirst), 16, "{opening}: the bracket and 五 are pushed in");
|
||||
assert_eq!(end(opening, 150.0, Kinsoku::Hard, PushOutOnly), 14, "{opening}: the bracket is pushed out with 五");
|
||||
}
|
||||
let latin = "一二三四五六七八九十一二三四a)";
|
||||
assert_eq!(end(latin, 150.0, Kinsoku::Hard, PushInFirst), 16, ") is pushed in");
|
||||
assert_eq!(end(latin, 150.0, Kinsoku::Hard, PushOutOnly), 14, "a is pushed out with )");
|
||||
// A push-in does not end at a soft hyphen: taking `b` and the soft hyphen in would fit, the
|
||||
// whole word `bc` does not, so the line pushes out.
|
||||
let shy = format!("{}a)b\u{ad}c", "一".repeat(31));
|
||||
assert_eq!(end(­, 320.0, Kinsoku::Hard, PushInFirst), 31);
|
||||
assert_eq!(end(­, 320.0, Kinsoku::Hard, PushOutOnly), 31);
|
||||
// Nor at a space after an opening bracket, which would leave the bracket ending the line.
|
||||
let spaced = format!("{}「「 A", "一".repeat(14));
|
||||
assert_eq!(end(&spaced, 150.0, Kinsoku::Hard, PushInFirst), 14);
|
||||
assert_eq!(end(&spaced, 150.0, Kinsoku::Hard, PushOutOnly), 14);
|
||||
// With hanging punctuation on, the gap before a full stop that may hang at the line's end is
|
||||
// not counted: the other nine gaps close by 9 pt, short of the 10 pt the full stop needs. With
|
||||
// it off, all ten gaps close and the full stop is pushed in.
|
||||
let stop = format!("{}五.", "一".repeat(9));
|
||||
assert_eq!(hung(&stop, 100.0, Kinsoku::Hard, PushInFirst, true), 9);
|
||||
assert_eq!(hung(&stop, 100.0, Kinsoku::Hard, PushOutOnly, true), 9);
|
||||
assert_eq!(end(&stop, 100.0, Kinsoku::Hard, PushInFirst), 11);
|
||||
assert_eq!(end(&stop, 100.0, Kinsoku::Hard, PushOutOnly), 9);
|
||||
|
||||
// Two gaps close by at most 2 pt, not the 5 pt 」 needs.
|
||||
let short = "一二」三";
|
||||
assert_eq!(end(short, 25.0, Kinsoku::Hard, PushInFirst), 1, "too little to close up: 二 is pushed out");
|
||||
|
||||
// A line holding a tab pushes out. Here the tab reaches the default stop at 36 pt, so pushing
|
||||
// out leaves the line one em short, though the tab glyph's own advance is 10 pt.
|
||||
let tabbed = "一\t二三四五六七八九十一二」六";
|
||||
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutOnly), 12);
|
||||
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutFirst), 12);
|
||||
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushInFirst), 12);
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type counts only the gaps justification closes: a line holding right-to-left
|
||||
/// text pushes out, the gaps close by a tenth of the em at the characters' Horizontal Scale, and
|
||||
/// no gap closes inside a shaped cluster.
|
||||
#[test]
|
||||
fn kinsoku_shori_type_counts_the_gaps_that_close() {
|
||||
use KinsokuShoriType::{PushInFirst, PushOutOnly};
|
||||
// A line holding right-to-left text is reordered before it is drawn, so the gaps that close
|
||||
// differ from the ones counted here: it pushes out.
|
||||
let mixed = "一二三א五六七八九十一二三四五」六";
|
||||
let mut glyphs = full_width(mixed);
|
||||
glyphs[3].level = unicode_bidi::Level::rtl();
|
||||
let mixed_end = |ty: KinsokuShoriType| break_line(mixed, &glyphs, 0, 150.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
|
||||
assert_eq!(mixed_end(PushInFirst), 14, "right-to-left text on the line: 五 is pushed out with 」");
|
||||
assert_eq!(mixed_end(PushOutOnly), 14);
|
||||
|
||||
// The gaps close by a tenth of the em at the characters' Horizontal Scale: at 200% Vertical
|
||||
// Scale five gaps still close by 5 pt, short of the 10 pt 」 needs.
|
||||
let tall = "一二三四五」六";
|
||||
let mut glyphs = full_width(tall);
|
||||
for g in &mut glyphs {
|
||||
g.sy *= 2.0;
|
||||
}
|
||||
let tall_end = |ty: KinsokuShoriType| break_line(tall, &glyphs, 0, 50.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
|
||||
assert_eq!(tall_end(PushInFirst), 4, "Vertical Scale does not widen the gaps: 五 is pushed out with 」");
|
||||
|
||||
// No gap closes inside a shaped cluster. 二 is two glyphs here, so the line has five gaps to
|
||||
// close by 5 pt, short of the 6 pt it overflows by.
|
||||
let mut glyphs = full_width(tall);
|
||||
glyphs[1].adv = 5.0;
|
||||
let half = glyphs[1].clone();
|
||||
glyphs.insert(2, half);
|
||||
let split_end = |width: f64| break_line(tall, &glyphs, 0, width, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, PushInFirst, false, false).0;
|
||||
assert_eq!(split_end(54.0), 5, "五 is pushed out with 」");
|
||||
assert_eq!(split_end(55.0), 7, "five gaps close by 5 pt: 」 is pushed in");
|
||||
}
|
||||
|
||||
@@ -855,6 +855,86 @@ fn kinsoku_set_decides_whether_a_small_kana_may_start_a_line() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type: in a measure of fifteen ems, the closing bracket after the fifteenth
|
||||
/// character ends the first line with Push In First, which closes the line's gaps to keep it inside
|
||||
/// the frame whatever its alignment; Push Out Only moves the fifteenth character to the next line
|
||||
/// with the bracket. Horizontal and vertical. Needs a font with full-width Japanese (skipped
|
||||
/// without one).
|
||||
#[test]
|
||||
fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() {
|
||||
use vectorcraft_doc::KinsokuShoriType;
|
||||
let text = "一二三四五六七八九十一二三四五」六";
|
||||
let lay = |ty: KinsokuShoriType, justify: Justify, vertical_type: bool| {
|
||||
let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() });
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.vertical = vertical_type;
|
||||
t.para.kinsoku_type = ty;
|
||||
t.para.justify = justify;
|
||||
let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, 300.0) } else { Rect::new(0.0, 0.0, 300.0, 200.0) };
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) };
|
||||
layout(FontDb::global(), &t)
|
||||
};
|
||||
if (lay(KinsokuShoriType::PushOutOnly, Justify::Left, false).glyphs[0].advance - 20.0).abs() > 2.0 {
|
||||
return; // no font with full-width Japanese here
|
||||
}
|
||||
for vertical_type in [false, true] {
|
||||
let lines = |ty| lay(ty, Justify::Left, vertical_type).glyphs.iter().map(|g| g.line).collect::<Vec<_>>();
|
||||
let mut pushed_in = vec![0; 16];
|
||||
pushed_in.push(1);
|
||||
assert_eq!(lines(KinsokuShoriType::PushInFirst), pushed_in, "vertical {vertical_type}");
|
||||
let mut pushed_out = vec![0; 14];
|
||||
pushed_out.extend([1, 1, 1]);
|
||||
assert_eq!(lines(KinsokuShoriType::PushOutOnly), pushed_out, "vertical {vertical_type}");
|
||||
assert_eq!(lines(KinsokuShoriType::PushOutFirst), pushed_out, "vertical {vertical_type}: a push-out leaves one em");
|
||||
}
|
||||
for justify in [Justify::Left, Justify::Center, Justify::Right, Justify::JustifyLeft] {
|
||||
let l = lay(KinsokuShoriType::PushInFirst, justify, false);
|
||||
let (first, bracket) = (&l.glyphs[0], &l.glyphs[15]);
|
||||
assert!(first.origin.x.abs() < 0.01, "{justify:?}: the line starts at the frame's left: {:?}", first.origin);
|
||||
assert!(bracket.origin.x + bracket.advance <= 300.01, "{justify:?}: 」 ends inside the frame: {:?}", bracket.origin);
|
||||
assert!(l.glyphs[1].origin.x < 20.0 - 0.5, "{justify:?}: the gaps close up");
|
||||
for g in &l.glyphs[..15] {
|
||||
assert!(g.advance >= 18.0 - 0.01, "{justify:?}: a gap closes by at most a tenth of an em: {}", g.advance);
|
||||
}
|
||||
}
|
||||
// A No Break run wider than the frame did not push anything in, and keeps its gaps.
|
||||
let mut t = TextObject::point(Point::ZERO, "一二三四五六七八九十", CharStyle { size: 20.0, no_break: true, ..CharStyle::default() });
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.para.kinsoku_type = KinsokuShoriType::PushInFirst;
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 150.0, 200.0).to_path(0.1)) };
|
||||
let l = layout(FontDb::global(), &t);
|
||||
assert!((l.glyphs[1].origin.x - 20.0).abs() < 0.01, "{:?}", l.glyphs[1].origin);
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type at 200% Vertical Scale in a column of 225 pt: the font's vertical metrics set
|
||||
/// each character 40 pt long, and its five gaps close by up to 4 pt each, room for the 15 pt 」
|
||||
/// overflows by, so 」 is pushed in; Push Out Only moves 五 to the next column with it. Needs
|
||||
/// craft-fonts' Shippori Mincho (skipped without it).
|
||||
#[test]
|
||||
fn kinsoku_shori_type_closes_a_column_by_the_em_down_the_column() {
|
||||
use vectorcraft_doc::KinsokuShoriType;
|
||||
if FontDb::global().face("Shippori Mincho", "Regular").is_none_or(|f| f.family != "Shippori Mincho") {
|
||||
eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)");
|
||||
return;
|
||||
}
|
||||
let lay = |ty: KinsokuShoriType| {
|
||||
let mut t = vertical_styled(
|
||||
"一二三四五」六",
|
||||
CharStyle { size: 20.0, font_family: "Shippori Mincho".into(), v_scale: 200.0, ..CharStyle::default() },
|
||||
);
|
||||
t.para.kinsoku_type = ty;
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 200.0, 225.0).to_path(0.1)) };
|
||||
layout(FontDb::global(), &t)
|
||||
};
|
||||
let lines = |ty| lay(ty).glyphs.iter().map(|g| g.line).collect::<Vec<_>>();
|
||||
let out = lay(KinsokuShoriType::PushOutOnly);
|
||||
assert!((out.glyphs[0].advance - 40.0).abs() < 0.01, "40 pt down the column: {}", out.glyphs[0].advance);
|
||||
assert_eq!(lines(KinsokuShoriType::PushInFirst), [0, 0, 0, 0, 0, 0, 1]);
|
||||
let bracket = &lay(KinsokuShoriType::PushInFirst).glyphs[5];
|
||||
assert!(bracket.origin.y + bracket.advance <= 225.01, "」 ends inside the column: {:?} +{}", bracket.origin, bracket.advance);
|
||||
assert_eq!(lines(KinsokuShoriType::PushOutOnly), [0, 0, 0, 0, 1, 1, 1]);
|
||||
}
|
||||
|
||||
/// Proportional Metrics in vertical type (#966): upright glyphs take the font's proportional
|
||||
/// heights (`vpal`, from shaping them top to bottom) and move with them, so each glyph's ink stays in
|
||||
/// its shorter cell. Line-end Punctuation Half Width takes nothing more off the punctuation `vpal`
|
||||
|
||||
@@ -1926,6 +1926,10 @@ font-technology Variable Променливи
|
||||
Stylistic Стилистично
|
||||
Mojikumi Set Набор Mojikumi
|
||||
Kinsoku Set Набор Kinsoku
|
||||
Kinsoku Shori Type Тип Kinsoku Shori
|
||||
Push In First Първо прибиране
|
||||
Push Out First Първо изтласкване
|
||||
Push Out Only Само изтласкване
|
||||
Hard Твърдо
|
||||
Soft Меко
|
||||
Monitor RGB RGB на монитора
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1905,6 +1905,10 @@ digits Malayalam Malajalam
|
||||
Stylistic Stilsko
|
||||
Mojikumi Set Mojikumi skup
|
||||
Kinsoku Set Kinsoku skup
|
||||
Kinsoku Shori Type Vrsta Kinsoku Shori
|
||||
Push In First Prvo uvlačenje
|
||||
Push Out First Prvo izbacivanje
|
||||
Push Out Only Samo izbacivanje
|
||||
Hard Tvrdo
|
||||
Right Indent Desna uvlaka
|
||||
Right-to-Left Paragraph Direction Smjer pasusa s desna nalijevo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2136,6 +2136,7 @@ digits Malayalam Malaiàlam
|
||||
Keywords: Paraules clau:
|
||||
Kind Tipus
|
||||
Kinsoku Set Conjunt Kinsoku
|
||||
Kinsoku Shori Type Tipus de Kinsoku Shori
|
||||
Kite Estel
|
||||
Knife Ganivet
|
||||
Knockout Group Grup knockout
|
||||
@@ -3206,6 +3207,9 @@ digits Malayalam Malaiàlam
|
||||
Puppet Warp Pins Pins de deformació de titella
|
||||
Puppet Warp Tool Eina de deformació de titella
|
||||
Purple Porpra
|
||||
Push In First Primer cap a dins
|
||||
Push Out First Primer cap a fora
|
||||
Push Out Only Només cap a fora
|
||||
Put back in the toolbar Torna a posar a la barra d’eines
|
||||
Quality Qualitat
|
||||
Quality {quality} Qualitat {quality}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1932,6 +1932,10 @@ digits Malayalam Malajálamské
|
||||
Stylistic Stylistický
|
||||
Mojikumi Set Sada Mojikumi
|
||||
Kinsoku Set Sada Kinsoku
|
||||
Kinsoku Shori Type Typ Kinsoku Shori
|
||||
Push In First Nejprve vtáhnout
|
||||
Push Out First Nejprve vysunout
|
||||
Push Out Only Pouze vysunout
|
||||
Hard Pevné
|
||||
Right Indent Odsazení zprava
|
||||
Right-to-Left Paragraph Direction Směr odstavce zprava doleva
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1738,6 +1738,10 @@ digits Malayalam Malayalam
|
||||
Stylistic Stilistisk
|
||||
Mojikumi Set Mojikumi-sæt
|
||||
Kinsoku Set Kinsoku-sæt
|
||||
Kinsoku Shori Type Kinsoku Shori-type
|
||||
Push In First Skub ind først
|
||||
Push Out First Skub ud først
|
||||
Push Out Only Kun skub ud
|
||||
Hard Hård
|
||||
Right Indent Højre indrykning
|
||||
Right-to-Left Paragraph Direction Afsnitsretning fra højre mod venstre
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1993,6 +1993,10 @@ file-filter Templates Vorlagen
|
||||
Stylistic Stilistisch
|
||||
Mojikumi Set Mojikumi-Satz
|
||||
Kinsoku Set Kinsoku-Satz
|
||||
Kinsoku Shori Type Kinsoku-Shori-Typ
|
||||
Push In First Zuerst einziehen
|
||||
Push Out First Zuerst hinausschieben
|
||||
Push Out Only Nur hinausschieben
|
||||
Hard Hart
|
||||
Soft Weich
|
||||
Monitor RGB Monitor-RGB
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2789,6 +2789,10 @@ theme Appearance Εμφάνιση
|
||||
Stylistic Στιλιστικό
|
||||
Mojikumi Set Σύνολο Mojikumi
|
||||
Kinsoku Set Σύνολο Kinsoku
|
||||
Kinsoku Shori Type Τύπος Kinsoku Shori
|
||||
Push In First Πρώτα προς τα μέσα
|
||||
Push Out First Πρώτα προς τα έξω
|
||||
Push Out Only Μόνο προς τα έξω
|
||||
Mono Μονοφωνικό
|
||||
Monochrome images Μονόχρωμες εικόνες
|
||||
Monospaced Σταθερού πλάτους
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1996,6 +1996,10 @@ font-technology Variable Variables
|
||||
Stylistic Estilística
|
||||
Mojikumi Set Conjunto de mojikumi
|
||||
Kinsoku Set Conjunto de kinsoku
|
||||
Kinsoku Shori Type Tipo de kinsoku shori
|
||||
Push In First Primero hacia dentro
|
||||
Push Out First Primero hacia fuera
|
||||
Push Out Only Solo hacia fuera
|
||||
Hard Fuerte
|
||||
Soft Suave
|
||||
Monitor RGB RGB del monitor
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1893,6 +1893,10 @@ digits Malayalam Malajalam
|
||||
Stylistic Tyylitelty
|
||||
Mojikumi Set Mojikumi-joukko
|
||||
Kinsoku Set Kinsoku-joukko
|
||||
Kinsoku Shori Type Kinsoku Shori -tyyppi
|
||||
Push In First Ensin sisään
|
||||
Push Out First Ensin ulos
|
||||
Push Out Only Vain ulos
|
||||
Hard Tiukka
|
||||
Right Indent Oikea sisennys
|
||||
Right-to-Left Paragraph Direction Kappaleen suunta oikealta vasemmalle
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1990,6 +1990,10 @@ font-technology Variable Variables
|
||||
Stylistic Stylistique
|
||||
Mojikumi Set Jeu de mojikumi
|
||||
Kinsoku Set Jeu de kinsoku
|
||||
Kinsoku Shori Type Type de kinsoku shori
|
||||
Push In First D’abord vers l’intérieur
|
||||
Push Out First D’abord vers l’extérieur
|
||||
Push Out Only Vers l’extérieur uniquement
|
||||
Hard Fort
|
||||
Soft Souple
|
||||
Monitor RGB RVB du moniteur
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2198,6 +2198,7 @@ file-filter Templates Sablonok
|
||||
Keywords: Kulcsszavak:
|
||||
Kind Típus
|
||||
Kinsoku Set Kinsoku-készlet
|
||||
Kinsoku Shori Type Kinsoku Shori típusa
|
||||
Kite Deltoid
|
||||
Knife Kés
|
||||
Knockout Group Kiütő csoport
|
||||
@@ -3126,6 +3127,9 @@ mark-style Japanese Japán
|
||||
Puppet Warp Pins Bábtorzítás tűi
|
||||
Puppet Warp Tool Bábtorzítás
|
||||
Purple Lila
|
||||
Push In First Először behúzás
|
||||
Push Out First Először kitolás
|
||||
Push Out Only Csak kitolás
|
||||
Put back in the toolbar Visszahelyezés az eszköztárba
|
||||
Quality Minőség
|
||||
Quality {quality} Minőség: {quality}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1738,6 +1738,10 @@ digits Malayalam Malayalam
|
||||
Stylistic Stilistik
|
||||
Mojikumi Set Set Mojikumi
|
||||
Kinsoku Set Set Kinsoku
|
||||
Kinsoku Shori Type Jenis Kinsoku Shori
|
||||
Push In First Dorong masuk dahulu
|
||||
Push Out First Dorong keluar dahulu
|
||||
Push Out Only Hanya dorong keluar
|
||||
Hard Keras
|
||||
Right Indent Indentasi kanan
|
||||
Right-to-Left Paragraph Direction Arah paragraf kanan ke kiri
|
||||
|
||||
|
@@ -2025,6 +2025,10 @@ file-filter Templates Modelli
|
||||
Stylistic Stilistica
|
||||
Mojikumi Set Set mojikumi
|
||||
Kinsoku Set Set kinsoku
|
||||
Kinsoku Shori Type Tipo di kinsoku shori
|
||||
Push In First Prima all'interno
|
||||
Push Out First Prima all'esterno
|
||||
Push Out Only Solo all'esterno
|
||||
Hard Forte
|
||||
Soft Debole
|
||||
Monitor RGB RGB monitor
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -76,6 +76,10 @@ digits Malayalam マラヤーラム数字
|
||||
Stylistic スタイリスティック
|
||||
Mojikumi Set 文字組み
|
||||
Kinsoku Set 禁則処理
|
||||
Kinsoku Shori Type 禁則調整方式
|
||||
Push In First 追い込み優先
|
||||
Push Out First 追い出し優先
|
||||
Push Out Only 追い出しのみ
|
||||
Hard 強い禁則
|
||||
Soft 弱い禁則
|
||||
Latin 欧文
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1646,6 +1646,10 @@ digits Malayalam მალაიალამური
|
||||
Stylistic სტილისტური
|
||||
Mojikumi Set Mojikumi-ის ნაკრები
|
||||
Kinsoku Set Kinsoku-ის ნაკრები
|
||||
Kinsoku Shori Type Kinsoku Shori-ის ტიპი
|
||||
Push In First ჯერ შეტანა
|
||||
Push Out First ჯერ გატანა
|
||||
Push Out Only მხოლოდ გატანა
|
||||
Hard მკაცრი
|
||||
Right Indent მარჯვენა შეწევა
|
||||
Right-to-Left Paragraph Direction აბზაცის მიმართულება მარჯვნიდან მარცხნივ
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -69,6 +69,10 @@ digits Malayalam 말라얄람어
|
||||
Stylistic 스타일
|
||||
Mojikumi Set 모지쿠미 세트
|
||||
Kinsoku Set 긴소쿠 세트
|
||||
Kinsoku Shori Type 긴소쿠 처리 유형
|
||||
Push In First 밀어 넣기 우선
|
||||
Push Out First 밀어내기 우선
|
||||
Push Out Only 밀어내기만
|
||||
Hard 강하게
|
||||
Soft 약하게
|
||||
Latin 라틴어
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1752,6 +1752,10 @@ digits Malayalam Malayalam
|
||||
Stylistic Stilistisch
|
||||
Mojikumi Set Mojikumi-set
|
||||
Kinsoku Set Kinsoku-set
|
||||
Kinsoku Shori Type Kinsoku Shori-type
|
||||
Push In First Eerst inschuiven
|
||||
Push Out First Eerst uitschuiven
|
||||
Push Out Only Alleen uitschuiven
|
||||
Hard Hard
|
||||
Right Indent Rechts inspringen
|
||||
Right-to-Left Paragraph Direction Alinearichting van rechts naar links
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1850,6 +1850,10 @@ digits Malayalam Malajalam
|
||||
Stylistic Stylistyczny
|
||||
Mojikumi Set Zestaw mojikumi
|
||||
Kinsoku Set Zestaw kinsoku
|
||||
Kinsoku Shori Type Typ kinsoku shori
|
||||
Push In First Najpierw wciągaj
|
||||
Push Out First Najpierw wypychaj
|
||||
Push Out Only Tylko wypychaj
|
||||
Hard Ścisły
|
||||
Right Indent Wcięcie z prawej
|
||||
Right-to-Left Paragraph Direction Kierunek akapitu od prawej do lewej
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3435,6 +3435,10 @@ font-technology Variable Variáveis
|
||||
Stylistic Estilístico
|
||||
Mojikumi Set Conjunto Mojikumi
|
||||
Kinsoku Set Conjunto Kinsoku
|
||||
Kinsoku Shori Type Tipo de Kinsoku Shori
|
||||
Push In First Primeiro para dentro
|
||||
Push Out First Primeiro para fora
|
||||
Push Out Only Somente para fora
|
||||
Hard Duro
|
||||
Soft Macio
|
||||
Monospaced Monoespaçado
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2022,6 +2022,10 @@ file-filter Templates Шаблоны
|
||||
Stylistic Стилистические
|
||||
Mojikumi Set Набор mojikumi
|
||||
Kinsoku Set Набор kinsoku
|
||||
Kinsoku Shori Type Тип kinsoku shori
|
||||
Push In First Сначала втягивать
|
||||
Push Out First Сначала выталкивать
|
||||
Push Out Only Только выталкивать
|
||||
Hard Строгий
|
||||
Soft Мягкий
|
||||
Monitor RGB RGB монитора
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1937,6 +1937,10 @@ font-technology Variable Variabla
|
||||
Stylistic Stilistisk
|
||||
Mojikumi Set Mojikumi-uppsättning
|
||||
Kinsoku Set Kinsoku-uppsättning
|
||||
Kinsoku Shori Type Kinsoku Shori-typ
|
||||
Push In First Skjut in först
|
||||
Push Out First Skjut ut först
|
||||
Push Out Only Skjut endast ut
|
||||
Hard Hård
|
||||
Soft Mjuk
|
||||
Monitor RGB Bildskärms-RGB
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1826,6 +1826,10 @@ font-technology Variable Değişken
|
||||
Mojikumi Mojikumi
|
||||
Mojikumi Set Mojikumi Seti
|
||||
Kinsoku Set Kinsoku Seti
|
||||
Kinsoku Shori Type Kinsoku Shori Türü
|
||||
Push In First Önce İçeri
|
||||
Push Out First Önce Dışarı
|
||||
Push Out Only Yalnızca Dışarı
|
||||
Hard Sert
|
||||
Soft Yumuşak
|
||||
Monitor RGB RGB'yi izleyin
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2019,6 +2019,10 @@ file-filter Templates Шаблони
|
||||
Stylistic Стилістичні
|
||||
Mojikumi Set Набір mojikumi
|
||||
Kinsoku Set Набір kinsoku
|
||||
Kinsoku Shori Type Тип kinsoku shori
|
||||
Push In First Спершу втягувати
|
||||
Push Out First Спершу виштовхувати
|
||||
Push Out Only Лише виштовхувати
|
||||
Hard Суворий
|
||||
Soft М’який
|
||||
Monitor RGB RGB монітора
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -2038,6 +2038,7 @@ digits Hindi Hindi
|
||||
Keywords: Từ khóa:
|
||||
Kind Loại
|
||||
Kinsoku Set Bộ Kinsoku
|
||||
Kinsoku Shori Type Kiểu Kinsoku Shori
|
||||
Knife Dao
|
||||
Knockout Group Nhóm Knockout
|
||||
Lab Lab
|
||||
@@ -2906,6 +2907,9 @@ mark-style Japanese Kiểu Nhật
|
||||
Puppet Warp Biến dạng con rối
|
||||
Puppet Warp Pins Ghim biến dạng con rối
|
||||
Puppet Warp Tool Công cụ Biến dạng con rối
|
||||
Push In First Đẩy vào trước
|
||||
Push Out First Đẩy ra trước
|
||||
Push Out Only Chỉ đẩy ra
|
||||
Put back in the toolbar Đặt trở lại vào thanh công cụ
|
||||
Quality Chất lượng
|
||||
Quality {quality} Chất lượng {quality}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -5047,6 +5047,10 @@ font-technology Variable 可变
|
||||
Stylistic 风格化
|
||||
Mojikumi Set 标点挤压集
|
||||
Kinsoku Set Kinsoku 集
|
||||
Kinsoku Shori Type Kinsoku 处理方式
|
||||
Push In First 先推入
|
||||
Push Out First 先推出
|
||||
Push Out Only 只推出
|
||||
Hard 强
|
||||
Soft 弱
|
||||
Saved Selection 1 已保存的选区 1
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2885,6 +2885,10 @@ file-filter Templates 範本
|
||||
Stylistic 風格化
|
||||
Mojikumi Set 標點間距
|
||||
Kinsoku Set Kinsoku 集
|
||||
Kinsoku Shori Type Kinsoku 處理方式
|
||||
Push In First 先推入
|
||||
Push Out First 先推出
|
||||
Push Out Only 只推出
|
||||
Hard 強
|
||||
Soft 弱
|
||||
Monitor RGB 顯示器 RGB
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2,13 +2,14 @@
|
||||
//! Bullets and Numbered List buttons and their presets (More Options opens Bullets and
|
||||
//! Numbering…), indents, space before/after, Hyphenate, Insert Kashidas (with the Indic options),
|
||||
//! Mojikumi Set and Kinsoku Set (with the East Asian options); the panel menu turns Roman Hanging
|
||||
//! Punctuation on and off, opens Justification… and Hyphenation… and picks the Single-line or
|
||||
//! Every-line Composer.
|
||||
//! Punctuation on and off, opens Justification… and Hyphenation…, picks the Kinsoku Shori Type
|
||||
//! (with the East Asian options) and picks the Single-line or Every-line Composer.
|
||||
|
||||
use egui::{Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{
|
||||
Composer, HyphenationOptions, JustificationOptions, Justify, Kinsoku, Mojikumi, NodeKind, ParaDirection, ParaStyle, SingleWordJustify,
|
||||
Composer, HyphenationOptions, JustificationOptions, Justify, Kinsoku, KinsokuShoriType, Mojikumi, NodeKind, ParaDirection, ParaStyle,
|
||||
SingleWordJustify,
|
||||
};
|
||||
|
||||
use super::character::{Metric, Shared, text_style};
|
||||
@@ -860,6 +861,22 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
});
|
||||
// How a line keeps the kinsoku characters off its ends: the selected paragraphs' type is
|
||||
// checked, and none is when they differ.
|
||||
let kinsoku_type = shared_para(app, |p| p.kinsoku_type);
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
ui.menu_button(format!(" {}", tl!("Kinsoku Shori Type")), |ui| {
|
||||
for (label, t, key) in [
|
||||
(tl!("Push In First"), KinsokuShoriType::PushInFirst, "pushInFirst"),
|
||||
(tl!("Push Out First"), KinsokuShoriType::PushOutFirst, "pushOutFirst"),
|
||||
(tl!("Push Out Only"), KinsokuShoriType::PushOutOnly, "pushOutOnly"),
|
||||
] {
|
||||
if menu_item(ui, label, true, kinsoku_type == Some(t)) {
|
||||
set_kinsoku_type(app, key);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
let composer = shared_composer(app);
|
||||
@@ -876,12 +893,18 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type (`key`: `"pushInFirst"`, `"pushOutFirst"` or `"pushOutOnly"`) for the
|
||||
/// paragraphs the panel acts on ([`format`]), in one undo step.
|
||||
fn set_kinsoku_type(app: &mut VectorcraftApp, key: &str) {
|
||||
format(app, json!({"kinsokuType": key}));
|
||||
}
|
||||
|
||||
/// Reset Panel: the paragraphs the panel acts on ([`format`]) take Auto alignment, no list, no
|
||||
/// indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification
|
||||
/// options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi
|
||||
/// Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging
|
||||
/// Punctuation off, no kashidas and no tab stops, as one undo step named Reset Panel (inside an
|
||||
/// undo group that is already open, as part of its step).
|
||||
/// Set, Regular burasagari, the Hard kinsoku set, Push Out Only, the Every-line Composer, Roman
|
||||
/// Hanging Punctuation off, no kashidas and no tab stops, as one undo step named Reset Panel (inside
|
||||
/// an undo group that is already open, as part of its step).
|
||||
fn reset_panel(app: &mut VectorcraftApp) {
|
||||
let h = HyphenationOptions::default();
|
||||
let j = vectorcraft_doc::JustificationOptions::default();
|
||||
@@ -901,7 +924,7 @@ fn reset_panel(app: &mut VectorcraftApp) {
|
||||
"wordSpacing": j.word_spacing, "letterSpacing": j.letter_spacing, "glyphScaling": j.glyph_scaling,
|
||||
"autoLeading": j.auto_leading, "singleWordJustification": "fullJustify",
|
||||
"direction": "auto", "leadingModel": "romanBaseline", "mojikumi": "lineEndHalf", "burasagari": "standard", "kinsoku": "hard",
|
||||
"composer": "everyLine", "romanHangingPunctuation": false
|
||||
"kinsokuType": "pushOutOnly", "composer": "everyLine", "romanHangingPunctuation": false
|
||||
}),
|
||||
);
|
||||
para_cmd(app, "text.tabs.clear", json!({}));
|
||||
|
||||
@@ -107,6 +107,32 @@ fn paragraph_mixed_kinsoku_and_mojikumi_show_blank_dropdowns() {
|
||||
assert_eq!(shared_para(&app, |p| p.mojikumi), Some(Mojikumi::LineEndHalf));
|
||||
}
|
||||
|
||||
/// Kinsoku Shori Type: the menu's item sets the type of the paragraphs the Type tool's selection
|
||||
/// touches in one undo step, and none is checked while the selected paragraphs differ.
|
||||
#[test]
|
||||
fn kinsoku_shori_type_sets_the_selected_paragraphs_in_one_undo_step() {
|
||||
let (mut app, id) = editing_text("あ\nい\n");
|
||||
app.session.prefs.show_east_asian_options = true;
|
||||
let types = |app: &VectorcraftApp| {
|
||||
let node = app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap();
|
||||
let NodeKind::Text(text) = &node.kind else { panic!("text") };
|
||||
(text.para_at(0).kinsoku_type, text.para_at(1).kinsoku_type)
|
||||
};
|
||||
select_range(&mut app, 0, 3);
|
||||
let history = app.session.active().unwrap().history.undo.len();
|
||||
set_kinsoku_type(&mut app, "pushInFirst");
|
||||
assert_eq!(types(&app), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), history + 1);
|
||||
app.session.set_tool_option("selectAll", &json!(true));
|
||||
assert_eq!(shared_para(&app, |p| p.kinsoku_type), None, "mixed: no item checked");
|
||||
set_kinsoku_type(&mut app, "pushOutFirst");
|
||||
assert_eq!(types(&app), (KinsokuShoriType::PushOutFirst, KinsokuShoriType::PushOutFirst));
|
||||
assert_eq!(shared_para(&app, |p| p.kinsoku_type), Some(KinsokuShoriType::PushOutFirst));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(types(&app), (KinsokuShoriType::PushOutOnly, KinsokuShoriType::PushOutOnly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_mixed_indent_fields_show_blank() {
|
||||
let (mut app, id) = editing_text("A\nB\n");
|
||||
|
||||
+24
@@ -5890,6 +5890,30 @@ JLREQ's level 3 line-breaking rules do (https://www.w3.org/TR/jlreq/#addendum_a3
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"kinsoku":"soft"}}}
|
||||
```
|
||||
|
||||
## Kinsoku Shori Type
|
||||
|
||||
`text.setFormat {kinsokuType: "pushInFirst"|"pushOutFirst"|"pushOutOnly"}` (Paragraph panel menu › Kinsoku Shori Type,
|
||||
shown with the East Asian options) sets how the area type lines of the paragraphs of the selected type (or `ids`, or the
|
||||
paragraphs a `start` / `end` range touches) keep the characters of their Kinsoku Set off their start and end. When the
|
||||
character that does not fit on a line may not start the next one, or the character before it may not end the line,
|
||||
**Push In First** sets it on the line, with the characters up to the next break the Kinsoku Set allows. The line then
|
||||
closes its gaps next to CJK characters to fit its measure, however it is aligned, each gap by the same share of the em
|
||||
of the character before it and by at most a tenth of that em; a line that cannot close up that much, or that holds a
|
||||
tab, pushes out instead. **Push Out First** pushes in
|
||||
the same way only when pushing out would leave the line more than an em, of the character before the break, short of
|
||||
its measure. **Push Out Only** (the default: new type and documents from before the setting) moves the
|
||||
character before a prohibited line start to the next line with it. With Burasagari on, an overflowing comma or full
|
||||
stop may hang outside the margin first, leaving the preceding character on the line. With Kinsoku Set None the setting
|
||||
does nothing. When Push In First or Push Out First is selected and a paragraph contains a prohibited line-start or
|
||||
line-end character, the whole paragraph is broken line by line, as the Single-line Composer breaks it, even if no line
|
||||
ends up pushing a character in. The setting is saved
|
||||
with the paragraph (`kinsoku_type` in `para`) only when it is not `pushOutOnly`, paragraph styles carry it, and Reset
|
||||
Panel sets it back to Push Out Only. One undo step.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"kinsokuType":"pushInFirst"}}}
|
||||
```
|
||||
|
||||
## Hanging punctuation (burasagari)
|
||||
|
||||
`text.setFormat {burasagari: "none"|"standard"|"forced"}` sets the Paragraph panel menu's Burasagari (None / Regular /
|
||||
|
||||
@@ -1084,7 +1084,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The frames of threaded text share one writing direction: Type › Type Orientation (`type.orientation.vertical`, `type.orientation.horizontal`) in one frame sets every frame, threading frames gives the thread its first frame's direction, frames taken out of a thread keep theirs, and the story flows through every frame in the thread's direction.
|
||||
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests/hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.
|
||||
Optical Margin Alignment and Roman Hanging Punctuation hang right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, too. Auto aligns a right-to-left line at its right, or at the end bracket of a path, so it hangs there. A right-to-left line is read from right to left: where it hangs at its left margin, `."` ending it hangs both glyphs.
|
||||
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
|
||||
The Paragraph panel menu's Kinsoku Shori Type (`text.setFormat {kinsokuType}`, with the East Asian options) picks Push In First, Push Out First or Push Out Only for each paragraph: a character the Kinsoku Set keeps off a line's start or end is set on the line, with the line's gaps next to CJK characters closed by at most a tenth of an em each to fit, or moved to the next line with the character before it. Push Out First pushes in only when pushing out would leave the line more than an em short. Push Out Only is the default, the setting is saved only when it is not, and paragraph styles carry it.
|
||||
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, Push Out Only, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
|
||||
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
|
||||
PDF export with type as text writes the hyphen that ends a line broken inside a word as a hyphen character where the line draws it, and the file opens with the hyphen in the line's text; kashidas are drawn as paths.
|
||||
The Paragraph panel's Insert Kashidas (with the Indic options; `text.setFormat {kashida}`) lengthens each Arabic word of a justified horizontal area type line with a kashida, a tatweel between its last pair of joined letters, before the word spaces widen: Short, Medium and Long kashidas are at most a quarter, half and a whole em long, Stylistic ones are whole tatweels up to an em, and None (the default) leaves the line as before; paragraph styles and the native file keep the setting.
|
||||
|
||||
@@ -61,6 +61,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Middle Eastern: Middle Eastern composers, split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. |
|
||||
| CJK: input methods (marked text, candidate window) | done | |
|
||||
| CJK: kinsoku (Hard, Soft, None), Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, the Mojikumi Settings dialog and the other mojikumi sets missing |
|
||||
| CJK: Kinsoku Shori Type (Push In First, Push Out First, Push Out Only) | done | Paragraph panel menu › Kinsoku Shori Type (`text.setFormat {kinsokuType}`), with the East Asian options ([Formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); Push Out Only is the default, and a push-in closes the line's gaps next to CJK characters by at most a tenth of an em each (VectorCraft's own limit). Evidence: `layout::wrapping_tests::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `tests::vertical::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `typecmd::area_tests::kinsoku_shori_type_is_set_per_paragraph_and_saved_only_when_not_push_out_only` |
|
||||
| CJK: Proportional Metrics (`palt` in horizontal type, `vpal` in vertical type) | done | #966 |
|
||||
| CJK: East Asian Type units (Q, Ha) | done | Preferences › Units › East Asian Type (`unitsAsianType`, Points by default) offers every unit and Q and Ha, a quarter of a millimetre each ([Scripting Guide](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/): 0.709 pt per Q; [InDesign's unit notation](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html): `q`, `h`). The public pages do not say which fields follow it, so no field uses it yet and that waits for a source. Evidence: `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` |
|
||||
| CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Tate-chu-yoko Settings… (`ui.tateChuYokoSettings`, `{tcyUpDown, tcyLeftRight}` in points) moves a marked block up or down and right or left as one piece. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `tests_vertical::tate_chu_yoko_settings_move_a_marked_block_up_down_left_and_right`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on`, `typecmd::tate_chu_yoko_settings_are_character_attributes_saved_only_when_set`, `dialogs::tate_chu_yoko::tests` |
|
||||
|
||||
Reference in New Issue
Block a user