From 2758919c8040bf4ab77f83a411e40d2c1dee7b56 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 24 Feb 2026 13:18:59 -0700 Subject: [PATCH] feat: add TextDecoration rendering --- src/buffer.rs | 8 ++-- src/edit/editor.rs | 6 ++- src/edit/syntect.rs | 9 +++- src/layout.rs | 4 +- src/render.rs | 113 +++++++++++++++++++++++++++++++++++++++++++- src/shape.rs | 6 ++- 6 files changed, 136 insertions(+), 10 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 4e6b7d4..d021b76 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -10,9 +10,9 @@ use core_maths::CoreFloat; use unicode_segmentation::UnicodeSegmentation; use crate::{ - Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, - Cursor, Ellipsize, FontSystem, Hinting, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, - LineIter, Motion, Renderer, Scroll, ShapeLine, Shaping, Wrap, + 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, }; /// A line of visible text for rendering @@ -1385,6 +1385,8 @@ impl Buffer { pub fn render(&self, renderer: &mut R, color: Color) { for run in self.layout_runs() { + // draw decorations before glyphs so text renders on top + render_decoration(renderer, &run, color); for glyph in run.glyphs { let physical_glyph = glyph.physical((0., run.line_y), 1.0); let glyph_color = glyph.color_opt.map_or(color, |some| some); diff --git a/src/edit/editor.rs b/src/edit/editor.rs index b290ae8..671b1ff 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -10,8 +10,9 @@ use core::cmp; use unicode_segmentation::UnicodeSegmentation; use crate::{ - Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem, - Color, Cursor, Edit, FontSystem, LayoutRun, LineEnding, LineIter, Renderer, Selection, Shaping, + render_decoration, Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, + Change, ChangeItem, Color, Cursor, Edit, FontSystem, LayoutRun, LineEnding, LineIter, Renderer, + Selection, Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -211,6 +212,7 @@ impl<'buffer> Editor<'buffer> { renderer.rectangle(x, y, 1, line_height as u32, cursor_color); } + render_decoration(renderer, &run, text_color); for glyph in run.glyphs { let physical_glyph = glyph.physical((0., line_y), 1.0); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index cf820ee..ce22d3d 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -9,7 +9,7 @@ use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; use crate::{ Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, Editor, - FontSystem, Renderer, Selection, Shaping, Style, Weight, + FontSystem, Renderer, Selection, Shaping, Style, UnderlineStyle, Weight, }; pub use syntect::highlighting::Theme as SyntaxTheme; @@ -366,7 +366,12 @@ impl<'buffer> Edit<'buffer> for SyntaxEditor<'_, 'buffer> { Weight::BOLD } else { Weight::NORMAL - }); //TODO: underline + }) + .underline(if style.font_style.contains(FontStyle::UNDERLINE) { + UnderlineStyle::Single + } else { + UnderlineStyle::None + }); if span_attrs != original_attrs { attrs_list.add_span(range, &span_attrs); } diff --git a/src/layout.rs b/src/layout.rs index 1ac8ac7..6da7160 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -2,7 +2,7 @@ use core::fmt::Display; -use crate::{math, CacheKey, CacheKeyFlags, Color}; +use crate::{math, CacheKey, CacheKeyFlags, Color, TextDecoration}; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -58,6 +58,8 @@ pub struct LayoutGlyph { pub metadata: usize, /// [`CacheKeyFlags`] pub cache_key_flags: CacheKeyFlags, + /// Text decoration (underline, strikethough, overline) + pub text_decoration: TextDecoration, } #[derive(Clone, Debug)] diff --git a/src/render.rs b/src/render.rs index 8db935f..7218d49 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,6 +1,6 @@ //! Helpers for rendering buffers and editors -use crate::{Color, PhysicalGlyph}; +use crate::{Color, LayoutGlyph, LayoutRun, PhysicalGlyph, TextDecoration, UnderlineStyle}; #[cfg(feature = "swash")] use crate::{FontSystem, SwashCache}; @@ -14,6 +14,117 @@ pub trait Renderer { fn glyph(&mut self, physical_glyph: PhysicalGlyph, color: Color); } +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.text_decoration != prev.text_decoration + } + }; + + if start_new_group { + 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) { + Some(i) + } else { + None + }; + } + } + + if let Some(gs) = group_start { + draw_decoration_group(renderer, run, &run.glyphs[gs..], default_color); + } +} + +fn has_any_decoration(td: &TextDecoration) -> bool { + td.underline != UnderlineStyle::None || td.overline || td.strikethrough +} + +fn draw_decoration_group( + renderer: &mut R, + run: &LayoutRun, + glyphs: &[LayoutGlyph], + default_color: Color, +) { + if glyphs.is_empty() { + return; + } + + let first = &glyphs[0]; + let last = &glyphs[glyphs.len() - 1]; + let td = &glyphs[0].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) as u32; + if width <= 0 { + return; + } + // Underline + match td.underline { + UnderlineStyle::None => {} + UnderlineStyle::Single => { + let color = td + .underline_color_opt + .or(first.color_opt) + .unwrap_or(default_color); + let thickness = (font_size * 14.0).max(1.0); + let y = run.line_y + font_size * 0.125; + renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + } + UnderlineStyle::Double => { + let color = td + .underline_color_opt + .or(first.color_opt) + .unwrap_or(default_color); + let thickness = (font_size * 14.0).max(1.0); + let gap = thickness; + let y = run.line_y + font_size * 0.125; + renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + renderer.rectangle( + x_start as i32, + (y + thickness + gap) as i32, + width, + thickness as u32, + color, + ); + } + } + + // Strikethrough + if td.strikethrough { + let color = td + .strikethrough_color_opt + .or(first.color_opt) + .unwrap_or(default_color); + let thickness = (font_size / 14.0).max(1.0); + let y = run.line_y - font_size * 0.3; + renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + } + + // Overline + if td.overline { + let color = td + .overline_color_opt + .or(first.color_opt) + .unwrap_or(default_color); + let thickness = (font_size / 14.0).max(1.0); + let y = run.line_top; + renderer.rectangle(x_start as i32, y as i32, width, thickness as u32, color); + } +} + /// Helper to migrate from old renderer //TODO: remove in future version #[cfg(feature = "swash")] diff --git a/src/shape.rs b/src/shape.rs index 9b72457..1bef992 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -5,7 +5,7 @@ use crate::fallback::FontFallbackIter; use crate::{ math, Align, Attrs, AttrsList, CacheKeyFlags, Color, Ellipsize, EllipsizeHeightLimit, Font, - FontSystem, Hinting, LayoutGlyph, LayoutLine, Metrics, Wrap, + FontSystem, Hinting, LayoutGlyph, LayoutLine, Metrics, TextDecoration, Wrap, }; #[cfg(not(feature = "std"))] use alloc::{format, vec, vec::Vec}; @@ -236,6 +236,7 @@ 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, }); } @@ -536,6 +537,7 @@ fn shape_skip( &attrs, ), metrics_opt: attrs.metrics_opt.map(Into::into), + text_decoration: attrs.text_decoration, } }), ); @@ -572,6 +574,7 @@ pub struct ShapeGlyph { pub metadata: usize, pub cache_key_flags: CacheKeyFlags, pub metrics_opt: Option, + pub text_decoration: TextDecoration, } impl ShapeGlyph { @@ -601,6 +604,7 @@ impl ShapeGlyph { color_opt: self.color_opt, metadata: self.metadata, cache_key_flags: self.cache_key_flags, + text_decoration: self.text_decoration, } }