From e8a6b0cc6029a3a3319a43d054bc0d6bbb033520 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 24 Feb 2026 16:18:21 -0700 Subject: [PATCH] chore: porting to decoration span fix: BiDi Text Decoration improv: don't use glyph decorations at all --- src/buffer.rs | 8 ++++-- src/layout.rs | 21 +++++++++++++--- src/render.rs | 70 ++++++++++++++++----------------------------------- src/shape.rs | 32 ++++++++++++++++++++--- 4 files changed, 74 insertions(+), 57 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index a20f8f3..9c137b1 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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, diff --git a/src/layout.rs b/src/layout.rs index ecaf2c8..a2999e5 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -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>, +} + +/// 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, + /// The decoration config and metrics + pub data: GlyphDecorationData, + /// Fallback color from the first glyph's `color_opt` + pub color_opt: Option, + /// 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, /// Glyphs in line pub glyphs: Vec, + /// Text decoration spans covering ranges of glyphs + pub decorations: Vec, } /// Wrapping mode diff --git a/src/render.rs b/src/render.rs index 0ea793f..c5a2004 100644 --- a/src/render.rs +++ b/src/render.rs @@ -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(renderer: &mut R, run: &LayoutRun, default_color: Color) { - if run.glyphs.is_empty() { - return; - } - - let mut group_start: Option = 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( +fn draw_decoration_span( 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( if w == 0 { return; } + let x_start = x_min; // Underline match td.underline { @@ -89,7 +63,7 @@ fn draw_decoration_group( 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( 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( 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( 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) diff --git a/src/shape.rs b/src/shape.rs index 850eb47..5def45c 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -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 = Vec::new(); + let process_range = |range: Range, x: &mut f32, y: &mut f32, glyphs: &mut Vec, + decorations: &mut Vec, 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(), }); }