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.
This commit is contained in:
Hojjat 2026-02-24 14:52:04 -07:00 committed by Jeremy Soller
parent 6ef1ccbeed
commit 78665aab3b
4 changed files with 78 additions and 53 deletions

View file

@ -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> {

View file

@ -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<Box<GlyphDecorationData>>,
}
#[derive(Clone, Debug)]

View file

@ -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<R: Renderer>(renderer: &mut R, run: &LayoutRun, default_color: Color) {
if run.glyphs.is_empty() {
return;
@ -26,7 +27,7 @@ pub fn render_decoration<R: Renderer>(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<R: Renderer>(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<R: Renderer>(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<R: Renderer>(
renderer: &mut R,
run: &LayoutRun,
@ -63,19 +60,26 @@ fn draw_decoration_group<R: Renderer>(
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<R: Renderer>(
.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<R: Renderer>(
.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<R: Renderer>(
.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);
}
}

View file

@ -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<Metrics>,
pub text_decoration: TextDecoration,
pub underline_metrics: DecorationMetrics,
pub strikethrough_metrics: DecorationMetrics,
/// Decoration data, only allocated when decorations are active
pub decoration_data: Option<Box<GlyphDecorationData>>,
}
impl ShapeGlyph {
const fn layout(
fn layout(
&self,
font_size: f32,
line_height_opt: Option<f32>,
@ -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(),
}
}