chore: porting to decoration span

fix: BiDi Text Decoration

improv: don't use glyph decorations at all
This commit is contained in:
Hojjat 2026-02-24 16:18:21 -07:00 committed by Jeremy Soller
parent abdbad308f
commit e8a6b0cc60
4 changed files with 74 additions and 57 deletions

View file

@ -11,8 +11,9 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{
render_decoration, Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem,
BufferLine, Color, Cursor, Ellipsize, FontSystem, Hinting, LayoutCursor, LayoutGlyph,
LayoutLine, LineEnding, LineIter, Motion, Renderer, Scroll, ShapeLine, Shaping, Wrap,
BufferLine, Color, Cursor, DecorationSpan, Ellipsize, FontSystem, Hinting, LayoutCursor,
LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Renderer, Scroll, ShapeLine, Shaping,
Wrap,
};
/// A line of visible text for rendering
@ -26,6 +27,8 @@ pub struct LayoutRun<'a> {
pub rtl: bool,
/// The array of layout glyphs to draw
pub glyphs: &'a [LayoutGlyph],
/// Text decoration spans covering ranges of glyphs
pub decorations: &'a [DecorationSpan],
/// Y offset to baseline of line
pub line_y: f32,
/// Y offset to top of line
@ -147,6 +150,7 @@ impl<'b> Iterator for LayoutRunIter<'b> {
text: line.text(),
rtl: shape.rtl,
glyphs: &layout_line.glyphs,
decorations: &layout_line.decorations,
line_y,
line_top,
line_height,

View file

@ -2,9 +2,11 @@
use core::fmt::Display;
use core::ops::Range;
use crate::{math, CacheKey, CacheKeyFlags, Color, GlyphDecorationData};
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, vec::Vec};
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use core_maths::CoreFloat;
@ -58,8 +60,19 @@ pub struct LayoutGlyph {
pub metadata: usize,
/// [`CacheKeyFlags`]
pub cache_key_flags: CacheKeyFlags,
/// Decoration data, only allocated when decorations are active
pub decoration_data: Option<Box<GlyphDecorationData>>,
}
/// A span of consecutive glyphs sharing the same text decoration.
#[derive(Clone, Debug, PartialEq)]
pub struct DecorationSpan {
/// Range of glyph indices in `LayoutLine::glyphs` covered by this span
pub glyph_range: Range<usize>,
/// The decoration config and metrics
pub data: GlyphDecorationData,
/// Fallback color from the first glyph's `color_opt`
pub color_opt: Option<Color>,
/// Font size from the first glyph (used to scale EM-unit metrics)
pub font_size: f32,
}
#[derive(Clone, Debug)]
@ -106,6 +119,8 @@ pub struct LayoutLine {
pub line_height_opt: Option<f32>,
/// Glyphs in line
pub glyphs: Vec<LayoutGlyph>,
/// Text decoration spans covering ranges of glyphs
pub decorations: Vec<DecorationSpan>,
}
/// Wrapping mode

View file

@ -3,7 +3,7 @@
#[cfg(not(feature = "std"))]
use core_maths::CoreFloat;
use crate::{Color, LayoutGlyph, LayoutRun, PhysicalGlyph, UnderlineStyle};
use crate::{Color, DecorationSpan, LayoutRun, PhysicalGlyph, UnderlineStyle};
#[cfg(feature = "swash")]
use crate::{FontSystem, SwashCache};
@ -19,62 +19,35 @@ pub trait Renderer {
/// Draw text decoration lines (underline, strikethrough, overline) for a layout run.
pub fn render_decoration<R: Renderer>(renderer: &mut R, run: &LayoutRun, default_color: Color) {
if run.glyphs.is_empty() {
return;
}
let mut group_start: Option<usize> = None;
for (i, glyph) in run.glyphs.iter().enumerate() {
let start_new_group = match group_start {
None => true,
Some(_) => {
let prev = &run.glyphs[i - 1];
glyph.decoration_data != prev.decoration_data
}
};
if start_new_group {
if let Some(gs) = group_start {
draw_decoration_group(renderer, run, &run.glyphs[gs..i], default_color);
}
group_start = if glyph.decoration_data.is_some() {
Some(i)
} else {
None
};
}
}
if let Some(gs) = group_start {
draw_decoration_group(renderer, run, &run.glyphs[gs..], default_color);
for span in run.decorations {
draw_decoration_span(renderer, run, span, default_color);
}
}
fn draw_decoration_group<R: Renderer>(
fn draw_decoration_span<R: Renderer>(
renderer: &mut R,
run: &LayoutRun,
glyphs: &[LayoutGlyph],
span: &DecorationSpan,
default_color: Color,
) {
let glyphs = &run.glyphs[span.glyph_range.clone()];
if glyphs.is_empty() {
return;
}
let first = &glyphs[0];
let last = &glyphs[glyphs.len() - 1];
// All glyphs in a group have the same decoration_data (guaranteed by grouping logic)
let deco = match &first.decoration_data {
Some(d) => d,
None => return,
};
let deco = &span.data;
let td = &deco.text_decoration;
let font_size = first.font_size;
let font_size = span.font_size;
let x_start = first.x;
let x_end = last.x + last.w;
let width = x_end - x_start;
// Compute x extent as min/max over all glyphs, not first/last,
// because RTL paragraphs store glyphs in right-to-left order.
let mut x_min = f32::INFINITY;
let mut x_max = f32::NEG_INFINITY;
for g in glyphs {
x_min = x_min.min(g.x);
x_max = x_max.max(g.x + g.w);
}
let width = x_max - x_min;
if width <= 0.0 {
return;
}
@ -82,6 +55,7 @@ fn draw_decoration_group<R: Renderer>(
if w == 0 {
return;
}
let x_start = x_min;
// Underline
match td.underline {
@ -89,7 +63,7 @@ fn draw_decoration_group<R: Renderer>(
UnderlineStyle::Single => {
let color = td
.underline_color_opt
.or(first.color_opt)
.or(span.color_opt)
.unwrap_or(default_color);
let thickness = (deco.underline_metrics.thickness * font_size)
.max(1.0)
@ -100,7 +74,7 @@ fn draw_decoration_group<R: Renderer>(
UnderlineStyle::Double => {
let color = td
.underline_color_opt
.or(first.color_opt)
.or(span.color_opt)
.unwrap_or(default_color);
let thickness = (deco.underline_metrics.thickness * font_size)
.max(1.0)
@ -122,7 +96,7 @@ fn draw_decoration_group<R: Renderer>(
if td.strikethrough {
let color = td
.strikethrough_color_opt
.or(first.color_opt)
.or(span.color_opt)
.unwrap_or(default_color);
let thickness = (deco.strikethrough_metrics.thickness * font_size)
.max(1.0)
@ -135,7 +109,7 @@ fn draw_decoration_group<R: Renderer>(
if td.overline {
let color = td
.overline_color_opt
.or(first.color_opt)
.or(span.color_opt)
.unwrap_or(default_color);
// Reuse underline thickness for overline
let thickness = (deco.underline_metrics.thickness * font_size)

View file

@ -4,9 +4,9 @@
use crate::fallback::FontFallbackIter;
use crate::{
math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, Ellipsize,
EllipsizeHeightLimit, Font, FontSystem, GlyphDecorationData, Hinting, LayoutGlyph, LayoutLine,
Metrics, Wrap,
math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, DecorationSpan,
Ellipsize, EllipsizeHeightLimit, Font, FontSystem, GlyphDecorationData, Hinting, LayoutGlyph,
LayoutLine, Metrics, Wrap,
};
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, format, vec, vec::Vec};
@ -625,7 +625,6 @@ impl ShapeGlyph {
color_opt: self.color_opt,
metadata: self.metadata,
cache_key_flags: self.cache_key_flags,
decoration_data: self.decoration_data.clone(),
}
}
@ -2688,10 +2687,13 @@ impl ShapeLine {
None
};
let mut decorations: Vec<DecorationSpan> = Vec::new();
let process_range = |range: Range<usize>,
x: &mut f32,
y: &mut f32,
glyphs: &mut Vec<LayoutGlyph>,
decorations: &mut Vec<DecorationSpan>,
max_ascent: &mut f32,
max_descent: &mut f32| {
for r in visual_line.ranges[range.clone()].iter() {
@ -2781,6 +2783,24 @@ impl ShapeLine {
}
}
glyphs.push(layout_glyph);
// Build decoration spans inline: extend or close+open
let glyph_idx = glyphs.len() - 1;
let cur_deco = glyph.decoration_data.as_deref();
let extends = match (decorations.last(), cur_deco) {
(Some(span), Some(d)) if span.data == *d => true,
_ => false,
};
if extends {
decorations.last_mut().unwrap().glyph_range.end = glyph_idx + 1;
} else if let Some(d) = cur_deco {
decorations.push(DecorationSpan {
glyph_range: glyph_idx..glyph_idx + 1,
data: d.clone(),
color_opt: glyphs[glyph_idx].color_opt,
font_size: glyphs[glyph_idx].font_size,
});
}
if !self.rtl {
*x += x_advance;
}
@ -2799,6 +2819,7 @@ impl ShapeLine {
&mut x,
&mut y,
&mut glyphs,
&mut decorations,
&mut max_ascent,
&mut max_descent,
);
@ -2811,6 +2832,7 @@ impl ShapeLine {
&mut x,
&mut y,
&mut glyphs,
&mut decorations,
&mut max_ascent,
&mut max_descent,
);
@ -2839,6 +2861,7 @@ impl ShapeLine {
max_descent,
line_height_opt,
glyphs,
decorations,
});
}
@ -2850,6 +2873,7 @@ impl ShapeLine {
max_descent: 0.0,
line_height_opt: self.metrics_opt.map(|x| x.line_height),
glyphs: Vec::default(),
decorations: Vec::new(),
});
}