From 78665aab3b15e876f3e3ce7c04690996da8f35c0 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 24 Feb 2026 14:52:04 -0700 Subject: [PATCH] perf: minimize the performance impact of text decoration Boxed the decoration data to go from 40b to 8b. The performance is almost unchanged for text without decoration. --- src/attrs.rs | 17 +++++++++++++++ src/layout.rs | 12 ++++------- src/render.rs | 58 +++++++++++++++++++++++++++------------------------ src/shape.rs | 44 ++++++++++++++++++++++---------------- 4 files changed, 78 insertions(+), 53 deletions(-) diff --git a/src/attrs.rs b/src/attrs.rs index 7301841..d7129d0 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -253,14 +253,31 @@ impl TextDecoration { overline_color_opt: None, } } + + pub const fn has_decoration(&self) -> bool { + !matches!(self.underline, UnderlineStyle::None) || self.strikethrough || self.overline + } } +/// Offset and thickness for a text decoration line, in EM units. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct DecorationMetrics { + /// Offset from baseline in EM units pub offset: f32, + /// Thickness in EM units pub thickness: f32, } +#[derive(Clone, Debug, PartialEq)] +pub struct GlyphDecorationData { + /// The text decoration configuration from the user + pub text_decoration: TextDecoration, + /// Underline offset and thickness from the font + pub underline_metrics: DecorationMetrics, + /// Strikethrough offset and thickness from the font + pub strikethrough_metrics: DecorationMetrics, +} + /// Text attributes #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Attrs<'a> { diff --git a/src/layout.rs b/src/layout.rs index b1ac1ce..ecaf2c8 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -2,9 +2,9 @@ use core::fmt::Display; -use crate::{math, CacheKey, CacheKeyFlags, Color, DecorationMetrics, TextDecoration}; +use crate::{math, CacheKey, CacheKeyFlags, Color, GlyphDecorationData}; #[cfg(not(feature = "std"))] -use alloc::vec::Vec; +use alloc::{boxed::Box, vec::Vec}; #[cfg(not(feature = "std"))] use core_maths::CoreFloat; @@ -58,12 +58,8 @@ pub struct LayoutGlyph { pub metadata: usize, /// [`CacheKeyFlags`] pub cache_key_flags: CacheKeyFlags, - /// Text decoration (underline, strikethough, overline) - pub text_decoration: TextDecoration, - /// Underline offset and thickness extracted from the font - pub underline_metrics: DecorationMetrics, - /// Strikethrough offset and thickness extracted from the font - pub strikethrough_metrics: DecorationMetrics, + /// Decoration data, only allocated when decorations are active + pub decoration_data: Option>, } #[derive(Clone, Debug)] diff --git a/src/render.rs b/src/render.rs index fdb9b63..e8d5a86 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,6 +1,6 @@ //! Helpers for rendering buffers and editors -use crate::{Color, LayoutGlyph, LayoutRun, PhysicalGlyph, TextDecoration, UnderlineStyle}; +use crate::{Color, LayoutGlyph, LayoutRun, PhysicalGlyph, UnderlineStyle}; #[cfg(feature = "swash")] use crate::{FontSystem, SwashCache}; @@ -14,6 +14,7 @@ pub trait Renderer { fn glyph(&mut self, physical_glyph: PhysicalGlyph, color: Color); } +/// 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; @@ -26,7 +27,7 @@ pub fn render_decoration(renderer: &mut R, run: &LayoutRun, default None => true, Some(_) => { let prev = &run.glyphs[i - 1]; - glyph.text_decoration != prev.text_decoration + glyph.decoration_data != prev.decoration_data } }; @@ -34,7 +35,7 @@ pub fn render_decoration(renderer: &mut R, run: &LayoutRun, default if let Some(gs) = group_start { draw_decoration_group(renderer, run, &run.glyphs[gs..i], default_color); } - group_start = if has_any_decoration(&glyph.text_decoration) { + group_start = if glyph.decoration_data.is_some() { Some(i) } else { None @@ -47,10 +48,6 @@ pub fn render_decoration(renderer: &mut R, run: &LayoutRun, default } } -fn has_any_decoration(td: &TextDecoration) -> bool { - td.underline != UnderlineStyle::None || td.overline || td.strikethrough -} - fn draw_decoration_group( renderer: &mut R, run: &LayoutRun, @@ -63,19 +60,26 @@ fn draw_decoration_group( let first = &glyphs[0]; let last = &glyphs[glyphs.len() - 1]; - let td = &glyphs[0].text_decoration; + + // 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 td = &deco.text_decoration; let font_size = first.font_size; + let x_start = first.x; let x_end = last.x + last.w; let width = x_end - x_start; if width <= 0.0 { - // first check to see if it's below 0.0 return; } - let width = width as u32; - if width == 0 { + let w = width as u32; + if w == 0 { return; } + // Underline match td.underline { UnderlineStyle::None => {} @@ -84,27 +88,27 @@ fn draw_decoration_group( .underline_color_opt .or(first.color_opt) .unwrap_or(default_color); - let thickness = (first.underline_metrics.thickness * font_size) + let thickness = (deco.underline_metrics.thickness * font_size) .max(1.0) .ceil(); - let y = run.line_y - first.underline_metrics.offset * font_size; - renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + let y = run.line_y - deco.underline_metrics.offset * font_size; + renderer.rectangle(x_start as i32, y as i32, w, thickness as u32, color); } UnderlineStyle::Double => { let color = td .underline_color_opt .or(first.color_opt) .unwrap_or(default_color); - let thickness = (first.underline_metrics.thickness * font_size) + let thickness = (deco.underline_metrics.thickness * font_size) .max(1.0) .ceil(); let gap = thickness; - let y = run.line_y - first.underline_metrics.offset * font_size; - renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + let y = run.line_y - deco.underline_metrics.offset * font_size; + renderer.rectangle(x_start as i32, y as i32, w, thickness as u32, color); renderer.rectangle( x_start as i32, (y + thickness + gap) as i32, - width, + w, thickness as u32, color, ); @@ -117,11 +121,11 @@ fn draw_decoration_group( .strikethrough_color_opt .or(first.color_opt) .unwrap_or(default_color); - let thickness = (first.strikethrough_metrics.thickness * font_size) + let thickness = (deco.strikethrough_metrics.thickness * font_size) .max(1.0) .ceil(); - let y = run.line_y - first.strikethrough_metrics.offset * font_size; - renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + let y = run.line_y - deco.strikethrough_metrics.offset * font_size; + renderer.rectangle(x_start as i32, y as i32, w, thickness as u32, color); } // Overline @@ -130,14 +134,14 @@ fn draw_decoration_group( .overline_color_opt .or(first.color_opt) .unwrap_or(default_color); - // we're reusing underline thickness for overline - let thickness = (first.underline_metrics.thickness * font_size) + // Reuse underline thickness for overline + let thickness = (deco.underline_metrics.thickness * font_size) .max(1.0) .ceil(); - let y = run.line_top; //TODO: this should be run.line_y - ascent - // but we don't have ascent in GlyphLayout - // using line_top as an approximation for now, which should be good enough for most fonts - renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + //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; + renderer.rectangle(x_start as i32, y as i32, w, thickness as u32, color); } } diff --git a/src/shape.rs b/src/shape.rs index 5787aa3..dac720a 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -5,8 +5,8 @@ use crate::fallback::FontFallbackIter; use crate::{ math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, Ellipsize, - EllipsizeHeightLimit, Font, FontSystem, Hinting, LayoutGlyph, LayoutLine, Metrics, - TextDecoration, Wrap, + EllipsizeHeightLimit, Font, FontSystem, GlyphDecorationData, Hinting, LayoutGlyph, LayoutLine, + Metrics, Wrap, }; #[cfg(not(feature = "std"))] use alloc::{format, vec, vec::Vec}; @@ -16,7 +16,6 @@ use core::cmp::{max, min}; use core::fmt; use core::mem; use core::ops::Range; -use skrifa::metrics::Decoration; #[cfg(not(feature = "std"))] use core_maths::CoreFloat; @@ -132,7 +131,6 @@ fn shape_fallback( let font_scale = font.metrics().units_per_em as f32; let ascent = font.metrics().ascent / font_scale; let descent = -font.metrics().descent / font_scale; - let (underline_metrics, strikethrough_metrics) = decoration_metrics(font); let mut buffer = scratch.harfrust_buffer.take().unwrap_or_default(); buffer.set_direction(if span_rtl { @@ -239,9 +237,16 @@ fn shape_fallback( metadata: attrs.metadata, cache_key_flags: override_fake_italic(attrs.cache_key_flags, font, &attrs), metrics_opt: attrs.metrics_opt.map(Into::into), - text_decoration: attrs.text_decoration, - underline_metrics, - strikethrough_metrics, + decoration_data: if attrs.text_decoration.has_decoration() { + let (ul_metrics, st_metrics) = decoration_metrics(font); + Some(Box::new(GlyphDecorationData { + text_decoration: attrs.text_decoration, + underline_metrics: ul_metrics, + strikethrough_metrics: st_metrics, + })) + } else { + None + }, }); } @@ -509,7 +514,7 @@ fn shape_skip( let metrics = swash_font.metrics(&[]); let glyph_metrics = swash_font.glyph_metrics(&[]).scale(1.0); - let (underline_metrics, strikethrough_metrics) = decoration_metrics(font.as_ref()); + let deco_metrics = decoration_metrics(font.as_ref()); let ascent = metrics.ascent / f32::from(metrics.units_per_em); let descent = metrics.descent / f32::from(metrics.units_per_em); @@ -544,9 +549,15 @@ fn shape_skip( &attrs, ), metrics_opt: attrs.metrics_opt.map(Into::into), - text_decoration: attrs.text_decoration, - underline_metrics, - strikethrough_metrics, + decoration_data: if attrs.text_decoration.has_decoration() { + Some(Box::new(GlyphDecorationData { + text_decoration: attrs.text_decoration, + underline_metrics: deco_metrics.0, + strikethrough_metrics: deco_metrics.1, + })) + } else { + None + }, } }), ); @@ -583,13 +594,12 @@ pub struct ShapeGlyph { pub metadata: usize, pub cache_key_flags: CacheKeyFlags, pub metrics_opt: Option, - pub text_decoration: TextDecoration, - pub underline_metrics: DecorationMetrics, - pub strikethrough_metrics: DecorationMetrics, + /// Decoration data, only allocated when decorations are active + pub decoration_data: Option>, } impl ShapeGlyph { - const fn layout( + fn layout( &self, font_size: f32, line_height_opt: Option, @@ -615,9 +625,7 @@ impl ShapeGlyph { color_opt: self.color_opt, metadata: self.metadata, cache_key_flags: self.cache_key_flags, - text_decoration: self.text_decoration, - underline_metrics: self.underline_metrics, - strikethrough_metrics: self.strikethrough_metrics, + decoration_data: self.decoration_data.clone(), } }