From 080c25c3620356b85f9174a588a505a4f6070adb Mon Sep 17 00:00:00 2001 From: mkorje Date: Thu, 17 Apr 2025 19:50:16 +1000 Subject: [PATCH] Fix bottom accent positioning in math --- crates/typst-layout/src/math/accent.rs | 59 +++++++++++++-------- crates/typst-layout/src/math/attach.rs | 6 ++- crates/typst-layout/src/math/fragment.rs | 33 ++++++++---- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-library/src/math/accent.rs | 13 +++++ tests/ref/math-accent-bottom-high-base.png | Bin 0 -> 572 bytes tests/ref/math-accent-bottom-sized.png | Bin 0 -> 382 bytes tests/ref/math-accent-bottom-subscript.png | Bin 0 -> 417 bytes tests/ref/math-accent-bottom-wide-base.png | Bin 0 -> 359 bytes tests/ref/math-accent-bottom.png | Bin 0 -> 622 bytes tests/ref/math-accent-nested.png | Bin 0 -> 537 bytes tests/suite/math/accent.typ | 28 ++++++++++ 12 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 tests/ref/math-accent-bottom-high-base.png create mode 100644 tests/ref/math-accent-bottom-sized.png create mode 100644 tests/ref/math-accent-bottom-subscript.png create mode 100644 tests/ref/math-accent-bottom-wide-base.png create mode 100644 tests/ref/math-accent-bottom.png create mode 100644 tests/ref/math-accent-nested.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 73d821019..53dfdf055 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -1,7 +1,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Em, Frame, Point, Size}; -use typst_library::math::{Accent, AccentElem}; +use typst_library::math::AccentElem; use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; @@ -18,8 +18,11 @@ pub fn layout_accent( let cramped = style_cramped(); let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; - // Try to replace a glyph with its dotless variant. - if elem.dotless(styles) { + let accent = elem.accent; + let top_accent = !accent.is_bottom(); + + // Try to replace base glyph with its dotless variant. + if top_accent && elem.dotless(styles) { if let MathFragment::Glyph(glyph) = &mut base { glyph.make_dotless_form(ctx); } @@ -29,41 +32,54 @@ pub fn layout_accent( let base_class = base.class(); let base_attach = base.accent_attach(); - let width = elem.size(styles).relative_to(base.width()); + let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span()); - let Accent(c) = elem.accent; - let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); - - // Try to replace accent glyph with flattened variant. - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.ascent() > flattened_base_height { - glyph.make_flattened_accent_form(ctx); + // Try to replace accent glyph with its flattened variant. + if top_accent { + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + if base.ascent() > flattened_base_height { + glyph.make_flattened_accent_form(ctx); + } } // Forcing the accent to be at least as large as the base makes it too // wide in many case. + let width = elem.size(styles).relative_to(base.width()); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); let variant = glyph.stretch_horizontal(ctx, width, short_fall); let accent = variant.frame; - let accent_attach = variant.accent_attach; + let accent_attach = variant.accent_attach.0; + + let (gap, accent_pos, base_pos) = if top_accent { + // Descent is negative because the accent's ink bottom is above the + // baseline. Therefore, the default gap is the accent's negated descent + // minus the accent base height. Only if the base is very small, we + // need a larger gap so that the accent doesn't move too low. + let accent_base_height = scaled!(ctx, styles, accent_base_height); + let gap = -accent.descent() - base.ascent().min(accent_base_height); + let accent_pos = Point::with_x(base_attach.0 - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + (gap, accent_pos, base_pos) + } else { + let gap = -accent.ascent(); + let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap); + let base_pos = Point::zero(); + (gap, accent_pos, base_pos) + }; - // Descent is negative because the accent's ink bottom is above the - // baseline. Therefore, the default gap is the accent's negated descent - // minus the accent base height. Only if the base is very small, we need - // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, styles, accent_base_height); - let gap = -accent.descent() - base.ascent().min(accent_base_height); let size = Size::new(base.width(), accent.height() + gap + base.height()); - let accent_pos = Point::with_x(base_attach - accent_attach); - let base_pos = Point::with_y(accent.height() + gap); let baseline = base_pos.y + base.ascent(); + let base_italics_correction = base.italics_correction(); let base_text_like = base.is_text_like(); - let base_ascent = match &base { MathFragment::Frame(frame) => frame.base_ascent, _ => base.ascent(), }; + let base_descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; let mut frame = Frame::soft(size); frame.set_baseline(baseline); @@ -73,6 +89,7 @@ pub fn layout_accent( FrameFragment::new(styles, frame) .with_class(base_class) .with_base_ascent(base_ascent) + .with_base_descent(base_descent) .with_italics_correction(base_italics_correction) .with_accent_attach(base_attach) .with_text_like(base_text_like), diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index e1d7d7c9d..90aad941e 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -434,9 +434,13 @@ fn compute_script_shifts( } if bl.is_some() || br.is_some() { + let descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; shift_down = shift_down .max(sub_shift_down) - .max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min }) + .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min }) .max(measure!(bl, ascent) - sub_top_max) .max(measure!(br, ascent) - sub_top_max); } diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 1b508a349..81e1772bc 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -164,12 +164,12 @@ impl MathFragment { } } - pub fn accent_attach(&self) -> Abs { + pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, - _ => self.width() / 2.0, + _ => (self.width() / 2.0, self.width() / 2.0), } } @@ -240,7 +240,7 @@ pub struct GlyphFragment { pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub font_size: Abs, pub class: MathClass, pub math_size: MathSize, @@ -294,7 +294,7 @@ impl GlyphFragment { descent: Abs::zero(), limits: Limits::for_char(c), italics_correction: Abs::zero(), - accent_attach: Abs::zero(), + accent_attach: (Abs::zero(), Abs::zero()), class, span, modifiers: FrameModifiers::get_in(styles), @@ -326,8 +326,14 @@ impl GlyphFragment { }); let mut width = advance.scaled(ctx, self.font_size); - let accent_attach = + + // The fallback for accents is half the width plus or minus the italics + // correction. This is similar to how top and bottom attachments are + // shifted. For bottom accents we do not use the accent attach of the + // base as it is meant for top acccents. + let top_accent_attach = accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); + let bottom_accent_attach = (width - italics) / 2.0; let extended_shape = is_extended_shape(ctx, id); if !extended_shape { @@ -339,7 +345,7 @@ impl GlyphFragment { self.ascent = bbox.y_max.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.italics_correction = italics; - self.accent_attach = accent_attach; + self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } @@ -457,7 +463,7 @@ impl Debug for GlyphFragment { pub struct VariantFragment { pub c: char, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub frame: Frame, pub font_size: Abs, pub class: MathClass, @@ -499,8 +505,9 @@ pub struct FrameFragment { pub limits: Limits, pub spaced: bool, pub base_ascent: Abs, + pub base_descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub text_like: bool, pub ignorant: bool, } @@ -508,6 +515,7 @@ pub struct FrameFragment { impl FrameFragment { pub fn new(styles: StyleChain, frame: Frame) -> Self { let base_ascent = frame.ascent(); + let base_descent = frame.descent(); let accent_attach = frame.width() / 2.0; Self { frame: frame.modified(&FrameModifiers::get_in(styles)), @@ -517,8 +525,9 @@ impl FrameFragment { limits: Limits::Never, spaced: false, base_ascent, + base_descent, italics_correction: Abs::zero(), - accent_attach, + accent_attach: (accent_attach, accent_attach), text_like: false, ignorant: false, } @@ -540,11 +549,15 @@ impl FrameFragment { Self { base_ascent, ..self } } + pub fn with_base_descent(self, base_descent: Abs) -> Self { + Self { base_descent, ..self } + } + pub fn with_italics_correction(self, italics_correction: Abs) -> Self { Self { italics_correction, ..self } } - pub fn with_accent_attach(self, accent_attach: Abs) -> Self { + pub fn with_accent_attach(self, accent_attach: (Abs, Abs)) -> Self { Self { accent_attach, ..self } } diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index f45035e27..6157d0c50 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -278,7 +278,7 @@ fn assemble( } let accent_attach = match axis { - Axis::X => frame.width() / 2.0, + Axis::X => (frame.width() / 2.0, frame.width() / 2.0), Axis::Y => base.accent_attach, }; diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index e62b63872..f2c9168c2 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -80,6 +80,19 @@ impl Accent { pub fn new(c: char) -> Self { Self(Self::combine(c).unwrap_or(c)) } + + /// List of bottom accents. Currently just a list of ones included in the + /// Unicode math class document. + const BOTTOM: &[char] = &[ + '\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}', + '\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}', + '\u{20ED}', '\u{20EE}', '\u{20EF}', + ]; + + /// Whether this accent is a bottom accent or not. + pub fn is_bottom(&self) -> bool { + Self::BOTTOM.contains(&self.0) + } } /// This macro generates accent-related functions. diff --git a/tests/ref/math-accent-bottom-high-base.png b/tests/ref/math-accent-bottom-high-base.png new file mode 100644 index 0000000000000000000000000000000000000000..23b14467280d93fba150194ded116d6b15fea4da GIT binary patch literal 572 zcmV-C0>k}@P)v*+FZC^6au}I+YU#J7le%9ciTqo1 z?0@j>I;vZ|cvTD%?|V$|$+DltUy+2SZAku9`=WvB7TbSE=B?=e|9|(g#mGWdz)+sq z^orURzqUr<#-2o1aNnIasd`-@3Rm?Px`G9Dw6VBw5i;+A5r%@~^|Z0r_yS5wOTi-UTRc?ab% z6u8W$jm1iPk$Dq+F%*cNqKU;HnBRb?KTvLBErdo0Jz@P$6N?Y3fT;>-8ELcyp4zn_ z^cEx9)W;K{Y3nqY{LKOKKeEXGmTcNsoID*&o&5r)E~z7A4nU~TCA6{F45i#!idk-H z+@*=d|D;eF78P^Q6?~VaO`Geg9}3s)9=d`PF|@IG-eeU28)GKE1+=j^@(D8USQ&-_ z|97;oIPh@^a>`m!{1Vt8*>M~p6eS`jE?aOks3{>``;4F15zPWt)@};Hll(iDbEx)$y`0Dql%5ELnaGLr){`eY& zdwXzn+U{;yvvTF~m21~5U$Js&+nE9CEnXV!ACFo*YVoMWv;_b(vVqN&POabo0000< KMNUMnLSTaLKoLv; literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-sized.png b/tests/ref/math-accent-bottom-sized.png new file mode 100644 index 0000000000000000000000000000000000000000..5455b2f5b260a860704717196db8b988e54b12f6 GIT binary patch literal 382 zcmV-^0fGLBP)^*)K^trgXzr>?sZh||E-Dt|M$1iB#vkLpvcYkLgAJ!`u{)R7;O?)@B|cY zXdg1qw14hwq2IKz_^lNR_njpY=cCI1|L5Ill%4u-K|Nmz}sk#rsE&2cd3?zfmC?CJ~ z+PZ!F)`j&_)BfN1@c;h@2)99M>i?S`{@+?ulo%VG-n{!G^(}t71`0l+a)FN6I&J-} zzu**heD3TAG)P>J9)f}xS>)k&dQ=h%dJ3bb7e0aWccrDI(WQ0*)($_9BYE-OXnj0t c@ldoF0CeBVg-HF}4gdfE07*qoM6N<$f{N|D(*OVf literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-subscript.png b/tests/ref/math-accent-bottom-subscript.png new file mode 100644 index 0000000000000000000000000000000000000000..818544445587a485c7152e543f2ab8a8536a89eb GIT binary patch literal 417 zcmV;S0bc%zP)_^QKU;L<91?f&9SD7A;fbqA z!u=m1^o4~dA5h2QFTuYnw?ocO(txH$&){ga6&A6x7v+e!2Ze_}Hx(;^TySZ-W0LeC*N+ z@v-~&%Q;lC_}ZM#lK)3mY~2Cj)EFQAPIJ$!K0Gkh;;a*+4S`XM2dBjVM)yn!G36@500000 LNkvXXu0mjfK$6xw literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-wide-base.png b/tests/ref/math-accent-bottom-wide-base.png new file mode 100644 index 0000000000000000000000000000000000000000..0475b4856bd49b9a4f7663564747ca9635654028 GIT binary patch literal 359 zcmV-t0hs=YP)YZomn-m~=V zl5b$v%y(ew?VOkY4?^UAnQW(79ACM#xNh-NWQ&*1U;8l#DtlA;1+6Up_rLfoh`!X; z7Es&P0gTkYq3^f$!=$_B(#qnSKdt{B`;YA7|33dq&i#k@^nE4Gg1U2N#fqb-7I$t_ z{rBJc4UqM>E7q-Fxq8i-)hk!7Uo+d~H&puT z)UEU^tN(0-&~K-;pWV{3{2$$GCrE1hHd-H#T08_S1^~|x+^P)&N!S1Y002ovPDHLk FV1n_Aw{-vj literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom.png b/tests/ref/math-accent-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..bd1b921460ca17d139552bd63f73144da916bc91 GIT binary patch literal 622 zcmV-!0+IcRP)zM(@7Ur)P)swAt_M<6Ut1fP)c+<8ciD}Qd5Z3L@6vA ziem*X(`{^sdt+OBld~7U<#W!t5GI`3MM3*;E`IQE_})EF8GlJxDO?Jd!v8}!59;!8 zu*7=tVPmV|(BlV9O08oS#pSwfgwA8b1N9^9Goe$L{Y-uM&%#4QkAPY1RIP0VS+G$; z_N)zj)9f?fjH>B9zZKrL%y{Jjlb0YQwy6t6x~*;#+4(n;HIo9!kJ$OJ3-Cpc>aYYr zwBIqlt|6Q_mC?UO)?!AnR^1Xrc7F~<`P15u2-c;WREy>BMX^zZFgARLdJW;eJ{ls( zD!eEjrBLa@$UK#N&LZUREd%b`B^5r~<+{ zz;pwEogY9=er!;jr#(a^a5r+=0r50&lBwGo!pC!Vs8xn$ehJ{x>MkEpy$wUxEP;`N| zRA|MsNYUxc-?Z8?t?g+&=43MSpZmYb@1B#Jdu{^a6bTAg0RwiHWgq`l#@1!t@JtK7 zS6jZD!y^2MdpL$w3mpmcV!;o<8BYVc-uSUwGy!djb64Q^#Z$_@ADa^_O0 z!KzS}wz{GPO;C$HLJ~1pNEU!Azp~7>L*@Dy?Q8B&^wNU2bAYwIfUMf%Q^bI}n*`Dv zq~K>wwQWX)gGNcGTRRNUZ3B*i8$4u8lY;F_QrO@b1nvrrhrr1qVCGyn;yB{X^@1xW zd>qhG;s4#e^}l(Z7l}95R#|b$E4%`*yYsOQDa>kv!9)zc_@XP%LrRQocXEXV!!R51 zXp1hrUT=y^KNdw%x$G(6Uki4)aeFDZ_W?6KSk!6SBJ2Zg&D!5B^<~!yyC`?L`IS*8 zIe5H5hdk+PWdiX8c;?g2)|<(}LjI#SOYT~W(LF{F&cdozmNAA#pr$XiqXkVyx!ZJs z%$OV?xt>V>xI8A{$|!%%NDmgGT|s*2!RFS9`iN5Z4EIjPpw#XSU!-k0RobEgR=^7Q b$ARAgn*4*