mirror of
https://github.com/penpot/penpot.git
synced 2026-10-02 16:56:16 +00:00
fix alignment
This commit is contained in:
parent
c512ec189a
commit
a4a0be38d6
@ -355,10 +355,14 @@ impl<'a> SpanCells<'a> {
|
||||
let (ink_top, ink_bottom) = ink.unwrap_or((0.0, extent - letter_spacing));
|
||||
let (mut ink_top, mut ink_bottom) =
|
||||
(ink_top + vpal_flow_shift, ink_bottom + vpal_flow_shift);
|
||||
// Centre cl-05 ink in the em body, unless vpal already centres it.
|
||||
// Centre cl-05 and forced-upright Western ink in the em body,
|
||||
// unless vpal already centres it. Western glyphs hung from a
|
||||
// shared baseline leave uneven gaps: a descender meets the next
|
||||
// cell's ascender (`gf`) while x-height letters float apart.
|
||||
let glyph_flow_shift = if !synthetic_rotation
|
||||
&& vpal_delta.is_none()
|
||||
&& ch.is_some_and(is_centered_punctuation)
|
||||
&& ink.is_some()
|
||||
&& ch.is_some_and(|c| is_centered_punctuation(c) || !is_upright_char(c))
|
||||
{
|
||||
let shift = centered_flow_shift(ink_top, ink_bottom, extent - letter_spacing);
|
||||
ink_top += shift;
|
||||
@ -490,6 +494,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upright_latin_ink_is_centred_in_its_cell() {
|
||||
// A descender followed by an ascender (`gf`) must not touch: each
|
||||
// letter's ink sits in the middle of its em cell.
|
||||
let mut content = make_content(&["gf"], 1000.0);
|
||||
content.paragraphs_mut()[0].children_mut()[0].text_orientation = TextOrientation::Upright;
|
||||
let layout = layout_with(&provider(TEST_FONT), &content);
|
||||
for cell in &layout.cells {
|
||||
let ink_middle = (cell.ink_top + cell.ink_bottom) / 2.0;
|
||||
assert!(
|
||||
(ink_middle - cell.extent / 2.0).abs() < 0.01,
|
||||
"ink middle {ink_middle} in a {} cell",
|
||||
cell.extent
|
||||
);
|
||||
}
|
||||
let (g, f) = (&layout.cells[0], &layout.cells[1]);
|
||||
let gap = f.top + f.ink_top - (g.top + g.ink_bottom);
|
||||
assert!(gap > 0.2 * EM, "g/f ink gap {gap}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warichu_span_composes_two_half_size_sub_lines() {
|
||||
let content = warichu_content("ABCD", 400.0);
|
||||
|
||||
@ -200,33 +200,33 @@ pub(super) fn align_offset_along_column(align: TextAlign, budget: f32, used: f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Japanese inter-script spacing at an upright CJK <-> rotated alphanumeric
|
||||
/// boundary; punctuation and whitespace get none. Scales by the smaller
|
||||
/// adjacent font size so a large neighbour cannot widen the gap.
|
||||
fn inter_script_spacing(
|
||||
previous: FlowScript,
|
||||
previous_font_size: f32,
|
||||
next: FlowScript,
|
||||
next_font_size: f32,
|
||||
) -> f32 {
|
||||
let boundary = matches!(
|
||||
(previous, next),
|
||||
/// Japanese inter-script spacing (JLREQ §3.2.6) between kana or kanji
|
||||
/// (cl-15, cl-16, cl-19) and a rotated alphanumeric run. Punctuation keeps
|
||||
/// its own aki and Latin sets solid against it; whitespace gets none. Scales
|
||||
/// by the smaller adjacent font size so a large neighbour cannot widen the
|
||||
/// gap.
|
||||
fn inter_script_spacing(previous: &FlowCell, next: &FlowCell) -> f32 {
|
||||
let is_japanese_letter =
|
||||
|flow: &FlowCell| flow.ch.is_some_and(|ch| classify(ch).is_japanese_letter());
|
||||
let boundary = match (previous.script, next.script) {
|
||||
(
|
||||
FlowScript::Upright,
|
||||
FlowScript::Rotated {
|
||||
starts_alphanumeric: true,
|
||||
..
|
||||
}
|
||||
) | (
|
||||
},
|
||||
) => is_japanese_letter(previous),
|
||||
(
|
||||
FlowScript::Rotated {
|
||||
ends_alphanumeric: true,
|
||||
..
|
||||
},
|
||||
FlowScript::Upright
|
||||
)
|
||||
);
|
||||
FlowScript::Upright,
|
||||
) => is_japanese_letter(next),
|
||||
_ => false,
|
||||
};
|
||||
if boundary {
|
||||
previous_font_size.min(next_font_size) * INTER_SCRIPT_SPACING_EM
|
||||
previous.cell.font_size.min(next.cell.font_size) * INTER_SCRIPT_SPACING_EM
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
@ -238,12 +238,7 @@ fn inter_script_spacing(
|
||||
/// keeps its trailing letter-spacing.
|
||||
pub(super) fn apply_inter_script_spacing(cells: &mut [FlowCell]) {
|
||||
for i in 1..cells.len() {
|
||||
let target_gap = inter_script_spacing(
|
||||
cells[i - 1].script,
|
||||
cells[i - 1].cell.font_size,
|
||||
cells[i].script,
|
||||
cells[i].cell.font_size,
|
||||
);
|
||||
let target_gap = inter_script_spacing(&cells[i - 1], &cells[i]);
|
||||
if target_gap <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
@ -1237,6 +1232,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latin_sets_against_japanese_punctuation_without_script_spacing() {
|
||||
// JLREQ §3.2.6 (Figure 103): after 、。」 Latin keeps only the
|
||||
// punctuation's half-em aki, and Latin sets solid after 「 and
|
||||
// before 、」.
|
||||
let provider = provider_with_fallback(VPAL_TEST_FONT, TEST_FONT);
|
||||
let fallback = ["fallback".to_string()];
|
||||
let layout = |text: &str| {
|
||||
layout_with_fallback(&provider, &make_content(&[text], 1000.0), 1000.0, &fallback)
|
||||
};
|
||||
let natural = layout("editor").cells[0].extent;
|
||||
for text in ["あ、editor", "あ。editor", "あ」editor", "あ「editor"] {
|
||||
let cells = layout(text).cells;
|
||||
assert!(
|
||||
(cells[2].top - cells[1].top - EM).abs() < 0.01,
|
||||
"{text}: punctuation frame is {} em",
|
||||
(cells[2].top - cells[1].top) / EM
|
||||
);
|
||||
}
|
||||
for text in ["editor、", "editor」"] {
|
||||
let cells = layout(text).cells;
|
||||
assert!(
|
||||
(cells[1].top - natural).abs() < 0.01,
|
||||
"{text}: Latin must set solid, gap {}",
|
||||
cells[1].top - natural
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oikomi_reduces_script_gap_before_oidashi() {
|
||||
let natural = layout_content(&make_content(&["あa"], 1000.0), 1000.0);
|
||||
|
||||
@ -99,7 +99,7 @@ fn cell_draws(layout: &VerticalLayout, cell: &VerticalCell, origin: (f32, f32))
|
||||
CellKind::Rotated { run } => {
|
||||
let run = &layout.runs[run];
|
||||
// After rotation +x runs down the column and +y runs across it;
|
||||
// the shift centres the run's actual ink band on the column axis.
|
||||
// the shift puts the font's central baseline on the column axis.
|
||||
run.blob()
|
||||
.map(|blob| {
|
||||
GlyphDraw::transformed(
|
||||
|
||||
@ -40,7 +40,8 @@ pub struct ShapedRun {
|
||||
pub utf8_range: Range<usize>,
|
||||
/// Source text of `utf8_range`, attached to the run's text blobs.
|
||||
pub text: String,
|
||||
/// Local-y offset that centres the run's ink after 90° rotation.
|
||||
/// Local-y offset that puts the font's central baseline on the column
|
||||
/// axis after 90° rotation.
|
||||
pub rotated_baseline_shift: f32,
|
||||
}
|
||||
|
||||
@ -262,11 +263,7 @@ impl RunHandler for RunCollector {
|
||||
.map(|(i, position)| positions.get(i + 1).map_or(end, |next| next.x) - position.x)
|
||||
.collect();
|
||||
self.runs.push(ShapedRun {
|
||||
rotated_baseline_shift: rotated_run_baseline_shift(
|
||||
info.font,
|
||||
&self.scratch_glyphs,
|
||||
&positions,
|
||||
),
|
||||
rotated_baseline_shift: central_baseline_shift(info.font),
|
||||
font: info.font.clone(),
|
||||
glyphs: self.scratch_glyphs.clone(),
|
||||
positions,
|
||||
@ -453,8 +450,22 @@ pub(super) fn rotated_baseline_shift(top: f32, bottom: f32) -> f32 {
|
||||
-(top + bottom) / 2.0
|
||||
}
|
||||
|
||||
/// Shift that puts the font's central baseline on the column axis: the
|
||||
/// middle of its em box, derived from ascent/descent scaled to 1em (CSS
|
||||
/// `central` for fonts without a BASE table). It depends only on the font,
|
||||
/// so every sideways run of a face shares one baseline whatever its ink.
|
||||
fn central_baseline_shift(font: &Font) -> f32 {
|
||||
let (_, metrics) = font.metrics();
|
||||
let band = metrics.descent - metrics.ascent;
|
||||
if band <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let em_over = -metrics.ascent * font.size() / band;
|
||||
em_over - font.size() / 2.0
|
||||
}
|
||||
|
||||
/// Centres the glyphs' actual ink; ascent/descent metrics are only the
|
||||
/// fallback for runs without visible ink.
|
||||
/// fallback for glyphs without visible ink.
|
||||
fn rotated_run_baseline_shift(font: &Font, glyphs: &[GlyphId], positions: &[SkPoint]) -> f32 {
|
||||
if let Some(ink) = glyph_run_ink_bounds(font, glyphs, positions) {
|
||||
rotated_baseline_shift(ink.top, ink.bottom)
|
||||
@ -477,33 +488,39 @@ mod tests {
|
||||
assert_eq!(families[2], "fallback");
|
||||
}
|
||||
|
||||
fn rotated_shift(text: &str) -> f32 {
|
||||
let layout = layout_content(&make_content(&[text], 1000.0), 1000.0);
|
||||
let CellKind::Rotated { run } = layout.cells[0].kind else {
|
||||
panic!("expected a rotated run");
|
||||
};
|
||||
layout.runs[run].rotated_baseline_shift
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_run_centres_actual_lowercase_ink() {
|
||||
fn rotated_runs_share_a_baseline_whatever_their_ink() {
|
||||
// Ascenders and descenders must not move the baseline across the
|
||||
// column.
|
||||
let plain = rotated_shift("aaa");
|
||||
for text in ["aaaf", "aaag", "aaagf", "AAA"] {
|
||||
assert!(
|
||||
(rotated_shift(text) - plain).abs() < 0.01,
|
||||
"{text} shifted from {plain} to {}",
|
||||
rotated_shift(text)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_lowercase_sits_on_the_column_axis() {
|
||||
// Source Sans: the em-box middle falls within 2% of an em of the
|
||||
// x-height middle, so lowercase looks centred in the column.
|
||||
let layout = layout_content(&make_content(&["a"], 1000.0), 1000.0);
|
||||
let cell = &layout.cells[0];
|
||||
let CellKind::Rotated { run } = cell.kind else {
|
||||
let CellKind::Rotated { run } = layout.cells[0].kind else {
|
||||
panic!("expected a rotated run");
|
||||
};
|
||||
let run = &layout.runs[run];
|
||||
let mut bounds = vec![skia::Rect::default(); run.glyphs.len()];
|
||||
run.font.get_bounds(&run.glyphs, &mut bounds, None);
|
||||
let top = bounds
|
||||
.iter()
|
||||
.zip(&run.positions)
|
||||
.map(|(bound, position)| bound.top + position.y)
|
||||
.fold(f32::MAX, f32::min);
|
||||
let bottom = bounds
|
||||
.iter()
|
||||
.zip(&run.positions)
|
||||
.map(|(bound, position)| bound.bottom + position.y)
|
||||
.fold(f32::MIN, f32::max);
|
||||
let shift = run.rotated_baseline_shift;
|
||||
assert!(((top + bottom) / 2.0 + shift).abs() < 0.01);
|
||||
|
||||
// Lowercase ink does not fill the face's ascent/descent band, so ink
|
||||
// centring differs from font-wide metric centring.
|
||||
let (_, metrics) = run.font.metrics();
|
||||
let metrics_shift = rotated_baseline_shift(metrics.ascent, metrics.descent);
|
||||
assert!((shift - metrics_shift).abs() > 0.1);
|
||||
let ink = run.ink_bounds().expect("visible ink");
|
||||
let ink_middle = (ink.top + ink.bottom) / 2.0 + run.rotated_baseline_shift;
|
||||
assert!(ink_middle.abs() < 0.02 * EM, "ink middle at {ink_middle}");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user