summaryrefslogtreecommitdiff
path: root/library/src/layout
diff options
context:
space:
mode:
authorPeng Guanwen <pg999w@outlook.com>2023-05-11 21:02:52 +0800
committerGitHub <noreply@github.com>2023-05-11 15:02:52 +0200
commitd19a4124de60b043e36e76dfe20fca193deb6a41 (patch)
treee71f1906acf454a97f3b8083ca03c60b8b32b7cd /library/src/layout
parente472b0347f84f39edf4655d39f8b5484870d0a76 (diff)
Implement sophisticated CJK punctuation adjustment (#954)
Diffstat (limited to 'library/src/layout')
-rw-r--r--library/src/layout/par.rs73
1 files changed, 58 insertions, 15 deletions
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index 2edbd93d..96d45f2f 100644
--- a/library/src/layout/par.rs
+++ b/library/src/layout/par.rs
@@ -8,7 +8,8 @@ use crate::layout::AlignElem;
use crate::math::EquationElem;
use crate::prelude::*;
use crate::text::{
- shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem, SpaceElem, TextElem,
+ is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem,
+ SpaceElem, TextElem,
};
/// Arrange text, spacing and inline-level elements into a paragraph.
@@ -354,6 +355,13 @@ impl<'a> Item<'a> {
}
}
+ fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
+ match self {
+ Self::Text(shaped) => Some(shaped),
+ _ => None,
+ }
+ }
+
/// The text length of the item.
fn len(&self) -> usize {
match self {
@@ -715,9 +723,12 @@ fn shape_range<'a>(
spans: &SpanMapper,
styles: StyleChain<'a>,
) {
+ let lang = TextElem::lang_in(styles);
+ let region = TextElem::region_in(styles);
let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
- let shaped = shape(vt, range.start, &bidi.text[range], spans, styles, dir);
+ let shaped =
+ shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region);
items.push(Item::Text(shaped));
};
@@ -905,15 +916,11 @@ fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<L
// This often happens with monospace fonts and CJK texts.
ratio = 0.0;
}
- if ratio.is_infinite() {
- // The line's not stretchable, we calculate the ratio in another way...
- ratio = delta / (em / 2.0);
- // ...and because it is underfull/overfull, make sure the ratio is at least 1.0.
- if ratio > 0.0 {
- ratio += 1.0;
- } else {
- ratio -= 1.0;
- }
+ if ratio > 1.0 {
+ // We should stretch the line above its stretchability. Now calculate the extra amount.
+ let extra_stretch = (delta - adjust) / attempt.justifiables() as f64;
+ // Normalize the amount by half Em size.
+ ratio = 1.0 + extra_stretch / (em / 2.0);
}
// Determine the cost of the line.
@@ -1124,7 +1131,9 @@ fn line<'a>(
let base = expanded.end - shaped.text.len();
let start = range.start.max(base);
let text = &p.bidi.text[start..range.end];
- let trimmed = text.trim_end();
+ // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity,
+ // we want to trim it too.
+ let trimmed = text.trim_end().trim_end_matches('\u{200B}');
range.end = start + trimmed.len();
// Deal with hyphens, dashes and justification.
@@ -1132,6 +1141,11 @@ fn line<'a>(
dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
justify |= text.ends_with('\u{2028}');
+ // Deal with CJK punctuation at line ends.
+ let gb_style = is_gb_style(shaped.lang, shaped.region);
+ let end_cjk_punct = trimmed
+ .ends_with(['”', '’', ',', '。', '、', ':', ';', '》', ')', '』', '」']);
+
// Usually, we don't want to shape an empty string because:
// - We don't want the height of trimmed whitespace in a different
// font to be considered for the line height.
@@ -1141,12 +1155,21 @@ fn line<'a>(
// need the shaped empty string to make the line the appropriate
// height. That is the case exactly if the string is empty and there
// are no other items in the line.
- if hyphen || start + shaped.text.len() > range.end {
+ if hyphen || start + shaped.text.len() > range.end || end_cjk_punct {
if hyphen || start < range.end || before.is_empty() {
let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end);
if hyphen || shy {
reshaped.push_hyphen(vt);
}
+ let punct = reshaped.glyphs.last();
+ if let Some(punct) = punct {
+ if punct.is_cjk_left_aligned_punctuation(gb_style) {
+ let shrink_amount = punct.shrinkability().1;
+ let punct = reshaped.glyphs.to_mut().last_mut().unwrap();
+ punct.shrink_right(shrink_amount);
+ reshaped.width -= shrink_amount.at(reshaped.size);
+ }
+ }
width += reshaped.width;
last = Some(Item::Text(reshaped));
}
@@ -1155,6 +1178,10 @@ fn line<'a>(
}
}
+ // Deal with CJK punctuation at line starts.
+ let text = &p.bidi.text[range.start..end];
+ let start_cjk_punct = text.starts_with(['“', '‘', '《', '(', '『', '「']);
+
// Reshape the start item if it's split in half.
let mut first = None;
if let Some((Item::Text(shaped), after)) = inner.split_first() {
@@ -1163,8 +1190,8 @@ fn line<'a>(
let end = range.end.min(base + shaped.text.len());
// Reshape if necessary.
- if range.start + shaped.text.len() > end {
- if range.start < end {
+ if range.start + shaped.text.len() > end || start_cjk_punct {
+ if range.start < end || start_cjk_punct {
let reshaped = shaped.reshape(vt, &p.spans, range.start..end);
width += reshaped.width;
first = Some(Item::Text(reshaped));
@@ -1174,6 +1201,22 @@ fn line<'a>(
}
}
+ if start_cjk_punct {
+ let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut);
+ if let Some(reshaped) = reshaped {
+ if let Some(punct) = reshaped.glyphs.first() {
+ if punct.is_cjk_right_aligned_punctuation() {
+ let shrink_amount = punct.shrinkability().0;
+ let punct = reshaped.glyphs.to_mut().first_mut().unwrap();
+ punct.shrink_left(shrink_amount);
+ let amount_abs = shrink_amount.at(reshaped.size);
+ reshaped.width -= amount_abs;
+ width -= amount_abs;
+ }
+ }
+ }
+ }
+
// Measure the inner items.
for item in inner {
width += item.width();