fix: find decoration spans in bidi text

This commit is contained in:
Hojjat 2026-02-24 18:39:26 -07:00 committed by Jeremy Soller
parent 0666ba14b1
commit b0884c0ab1
6 changed files with 64 additions and 17 deletions

View file

@ -276,6 +276,9 @@ pub struct GlyphDecorationData {
pub underline_metrics: DecorationMetrics,
/// Strikethrough offset and thickness from the font
pub strikethrough_metrics: DecorationMetrics,
/// Font ascent in EM units (ascent / upem).
/// Used for overline positioning
pub ascent: f32,
}
/// Text attributes

View file

@ -115,9 +115,8 @@ fn draw_decoration_span<R: Renderer>(
let thickness = (deco.underline_metrics.thickness * font_size)
.max(1.0)
.ceil();
//TODO: this should be run.line_y - ascent, but we don't have per-glyph ascent
// in LayoutGlyph. Using line_top as an approximation for now.
let y = run.line_top;
// clamped so it doesn't go above the line top.
let y = (run.line_y - deco.ascent * font_size).max(run.line_top);
renderer.rectangle(x_start as i32, y as i32, w, thickness as u32, color);
}
}

View file

@ -612,11 +612,15 @@ impl ShapeGlyph {
}
}
fn decoration_metrics(font: &Font) -> (DecorationMetrics, DecorationMetrics) {
fn decoration_metrics(font: &Font) -> (DecorationMetrics, DecorationMetrics, f32) {
let metrics = font.metrics();
let upem = metrics.units_per_em as f32;
if upem == 0.0 {
return (DecorationMetrics::default(), DecorationMetrics::default());
return (
DecorationMetrics::default(),
DecorationMetrics::default(),
0.0,
);
}
(
DecorationMetrics {
@ -627,10 +631,11 @@ fn decoration_metrics(font: &Font) -> (DecorationMetrics, DecorationMetrics) {
offset: metrics.strikeout.map_or(0.3, |d| d.offset / upem),
thickness: metrics.strikeout.map_or(1.0 / 14.0, |d| d.thickness / upem),
},
metrics.ascent / upem,
)
}
/// span index used in VlRange to indicate this range is the ellipsis.
/// span index used in `VlRange` to indicate this range is the ellipsis.
const ELLIPSIS_SPAN: usize = usize::MAX;
fn shape_ellipsis(
@ -1046,7 +1051,7 @@ impl ShapeSpan {
.map(|font| decoration_metrics(&font))
});
if let Some((ul_metrics, st_metrics)) = primary_metrics {
if let Some((ul_metrics, st_metrics, ascent)) = primary_metrics {
// Track which sub-ranges of span_range are covered by explicit spans
let mut covered_end = span_range.start;
@ -1068,6 +1073,7 @@ impl ShapeSpan {
text_decoration: default_attrs.text_decoration,
underline_metrics: ul_metrics,
strikethrough_metrics: st_metrics,
ascent,
},
));
}
@ -1082,6 +1088,7 @@ impl ShapeSpan {
text_decoration: attrs.text_decoration,
underline_metrics: ul_metrics,
strikethrough_metrics: st_metrics,
ascent,
},
));
}
@ -1097,6 +1104,7 @@ impl ShapeSpan {
text_decoration: default_attrs.text_decoration,
underline_metrics: ul_metrics,
strikethrough_metrics: st_metrics,
ascent,
},
));
}
@ -2081,7 +2089,7 @@ impl ShapeLine {
.map_or(0.0, |s| s.words.iter().map(|w| w.width(font_size)).sum())
}
/// Creates a VlRange for the ellipsis with the given BiDi level.
/// Creates a `VlRange` for the ellipsis with the give`BiDi`Di level.
fn ellipsis_vlrange(&self, level: unicode_bidi::Level) -> VlRange {
VlRange {
span: ELLIPSIS_SPAN,
@ -2091,7 +2099,7 @@ impl ShapeLine {
}
}
/// Determines the appropriate BiDi level for the ellipsis based on the
/// Determines the appropriate `BiDi` level for the ellipsis based on the
/// adjacent ranges, following UAX#9 N1/N2 rules for neutral characters.
fn ellipsis_level_between(
&self,
@ -2860,11 +2868,8 @@ impl ShapeLine {
}
glyphs.push(layout_glyph);
// Advance (or reset) the decoration cursor to find
// the span covering this glyph's byte position.
// Resets for RTL/BiDi where byte order reverses.
if deco_cursor < deco_spans.len()
&& glyph.start < deco_spans[deco_cursor].0.start
if deco_cursor >= deco_spans.len()
|| glyph.start < deco_spans[deco_cursor].0.start
{
deco_cursor = 0;
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c856de2313bef83d36dd8776dfcba6f6312238d983f97603fc9a76fa53fea2c
size 15386
oid sha256:d83b58ede597fede243ebb042c31668c0847069a3843dc9b279c3624e1387728
size 15146

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e332dc9edc0d8c2fd6dd7d9361dc1441b4c979023e79571ac26b7f4f1f1b274
size 15305

View file

@ -50,7 +50,6 @@ fn test_text_decorations() {
.validate_text_rendering();
}
#[test]
fn test_text_decorations_rtl() {
let base = base_attrs();
@ -125,3 +124,41 @@ fn test_text_decorations_bidi() {
.canvas(600, 50)
.validate_text_rendering();
}
/// Multiline Bidi test
#[test]
fn test_text_decorations_multiline_bidi() {
let base = base_attrs();
let red = Color::rgb(0xFF, 0x00, 0x00);
let cyan = Color::rgb(0x00, 0xFF, 0xFF);
DrawTestCfg::new("text_decoration_multiline_bidi")
.font_size(20., 26.)
.font_attrs(base.clone())
.rich_text(vec![
("زیرخط ", base.clone().underline(UnderlineStyle::Single)),
("Double ", base.clone().underline(UnderlineStyle::Double)),
("خط ", base.clone().strikethrough()),
("Over \n", base.clone().overline()),
(
"Red زیر خط ",
base.clone()
.underline(UnderlineStyle::Single)
.underline_color(red),
),
(
"CyanSt ",
base.clone().strikethrough().strikethrough_color(cyan),
),
(
"All",
base.clone()
.underline(UnderlineStyle::Single)
.strikethrough()
.overline(),
),
(" Plain", base),
])
.canvas(400, 80)
.validate_text_rendering();
}