text rendering: support strikethroughs (#7363)

<img width="1269" alt="image"
src="https://github.com/zed-industries/zed/assets/18583882/d4c93033-b2ac-4ae0-8e12-457f256ee869">

Release Notes:

- Added support for styling text with strikethrough.

Related: 
- https://github.com/zed-industries/zed/issues/5364
- https://github.com/zed-industries/zed/pull/7345
This commit is contained in:
Kieran Gill 2024-02-07 09:51:27 -05:00 committed by GitHub
parent 55129d4d6c
commit ad3940c66f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 145 additions and 4 deletions

View File

@ -962,6 +962,7 @@ impl AssistantPanel {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
@ -3166,6 +3167,7 @@ impl InlineAssistant {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(

View File

@ -360,6 +360,7 @@ impl Render for MessageEditor {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};

View File

@ -2068,6 +2068,7 @@ impl CollabPanel {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};

View File

@ -9495,6 +9495,7 @@ impl Render for Editor {
line_height: relative(settings.buffer_line_height.value()),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
},
@ -9508,6 +9509,7 @@ impl Render for Editor {
line_height: relative(settings.buffer_line_height.value()),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
},
};

View File

@ -1073,6 +1073,7 @@ impl EditorElement {
font: self.style.text.font(),
color: self.style.background,
background_color: None,
strikethrough: None,
underline: None,
}],
)
@ -1713,6 +1714,7 @@ impl EditorElement {
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
@ -1849,6 +1851,7 @@ impl EditorElement {
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
@ -1906,6 +1909,7 @@ impl EditorElement {
color: placeholder_color,
background_color: None,
underline: Default::default(),
strikethrough: None,
};
cx.text_system()
.shape_line(line.to_string().into(), font_size, &[run])
@ -2321,6 +2325,7 @@ impl EditorElement {
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
@ -2335,6 +2340,7 @@ impl EditorElement {
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
@ -2868,6 +2874,7 @@ impl LineWithInvisibles {
color: text_style.color,
background_color: text_style.background_color,
underline: text_style.underline,
strikethrough: text_style.strikethrough,
});
if editor_mode == EditorMode::Full {
@ -3281,6 +3288,7 @@ fn layout_line(
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
}],
)
}

View File

@ -197,6 +197,9 @@ pub struct TextStyle {
/// The underline style of the text
pub underline: Option<UnderlineStyle>,
/// The strikethrough style of the text
pub strikethrough: Option<StrikethroughStyle>,
/// How to handle whitespace in the text
pub white_space: WhiteSpace,
}
@ -214,6 +217,7 @@ impl Default for TextStyle {
font_style: FontStyle::default(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
}
}
@ -246,6 +250,10 @@ impl TextStyle {
self.underline = Some(underline);
}
if let Some(strikethrough) = style.strikethrough {
self.strikethrough = Some(strikethrough);
}
self
}
@ -277,6 +285,7 @@ impl TextStyle {
color: self.color,
background_color: self.background_color,
underline: self.underline,
strikethrough: self.strikethrough,
}
}
}
@ -300,6 +309,9 @@ pub struct HighlightStyle {
/// The underline style of the text
pub underline: Option<UnderlineStyle>,
/// The underline style of the text
pub strikethrough: Option<StrikethroughStyle>,
/// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
pub fade_out: Option<f32>,
}
@ -553,6 +565,17 @@ pub struct UnderlineStyle {
pub wavy: bool,
}
/// The properties that can be applied to a strikethrough.
#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)]
#[refineable(Debug)]
pub struct StrikethroughStyle {
/// The thickness of the strikethrough.
pub thickness: Pixels,
/// The color of the strikethrough.
pub color: Option<Hsla>,
}
/// The kinds of fill that can be applied to a shape.
#[derive(Clone, Debug)]
pub enum Fill {
@ -601,6 +624,7 @@ impl From<&TextStyle> for HighlightStyle {
font_style: Some(other.font_style),
background_color: other.background_color,
underline: other.underline,
strikethrough: other.strikethrough,
fade_out: None,
}
}
@ -636,6 +660,10 @@ impl HighlightStyle {
self.underline = other.underline;
}
if other.strikethrough.is_some() {
self.strikethrough = other.strikethrough;
}
match (other.fade_out, self.fade_out) {
(Some(source_fade), None) => self.fade_out = Some(source_fade),
(Some(source_fade), Some(dest_fade)) => {

View File

@ -10,7 +10,7 @@ pub use line_wrapper::*;
use crate::{
px, Bounds, DevicePixels, EntityId, Hsla, Pixels, PlatformTextSystem, Point, Result,
SharedString, Size, UnderlineStyle,
SharedString, Size, StrikethroughStyle, UnderlineStyle,
};
use anyhow::anyhow;
use collections::{BTreeSet, FxHashMap, FxHashSet};
@ -317,6 +317,7 @@ impl WindowTextSystem {
if let Some(last_run) = decoration_runs.last_mut() {
if last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.strikethrough == run.strikethrough
&& last_run.background_color == run.background_color
{
last_run.len += run.len as u32;
@ -328,6 +329,7 @@ impl WindowTextSystem {
color: run.color,
background_color: run.background_color,
underline: run.underline,
strikethrough: run.strikethrough,
});
}
@ -382,6 +384,7 @@ impl WindowTextSystem {
if decoration_runs.last().map_or(false, |last_run| {
last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.strikethrough == run.strikethrough
&& last_run.background_color == run.background_color
}) {
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
@ -391,6 +394,7 @@ impl WindowTextSystem {
color: run.color,
background_color: run.background_color,
underline: run.underline,
strikethrough: run.strikethrough,
});
}
@ -406,6 +410,7 @@ impl WindowTextSystem {
let layout = self
.line_layout_cache
.layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
lines.push(WrappedLine {
layout,
decoration_runs,
@ -599,6 +604,8 @@ pub struct TextRun {
pub background_color: Option<Hsla>,
/// The underline style (if any)
pub underline: Option<UnderlineStyle>,
/// The strikethrough style (if any)
pub strikethrough: Option<StrikethroughStyle>,
}
/// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`].

View File

@ -1,6 +1,6 @@
use crate::{
black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result,
SharedString, UnderlineStyle, WrapBoundary, WrappedLineLayout,
SharedString, StrikethroughStyle, UnderlineStyle, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
use smallvec::SmallVec;
@ -20,6 +20,9 @@ pub struct DecorationRun {
/// The underline style for this run
pub underline: Option<UnderlineStyle>,
/// The strikethrough style for this run
pub strikethrough: Option<StrikethroughStyle>,
}
/// A line of text that has been shaped and decorated.
@ -113,6 +116,7 @@ fn paint_line(
let mut run_end = 0;
let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone();
let mut glyph_origin = origin;
@ -145,6 +149,17 @@ fn paint_line(
underline_origin.x = origin.x;
underline_origin.y += line_height;
}
if let Some((strikethrough_origin, strikethrough_style)) =
current_strikethrough.as_mut()
{
cx.paint_strikethrough(
*strikethrough_origin,
glyph_origin.x - strikethrough_origin.x,
strikethrough_style,
);
strikethrough_origin.x = origin.x;
strikethrough_origin.y += line_height;
}
glyph_origin.x = origin.x;
glyph_origin.y += line_height;
@ -153,6 +168,7 @@ fn paint_line(
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
if glyph.index >= run_end {
if let Some(style_run) = decoration_runs.next() {
if let Some((_, background_color)) = &mut current_background {
@ -183,6 +199,24 @@ fn paint_line(
},
));
}
if let Some((_, strikethrough_style)) = &mut current_strikethrough {
if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
finished_strikethrough = current_strikethrough.take();
}
}
if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
current_strikethrough.get_or_insert((
point(
glyph_origin.x,
glyph_origin.y
+ (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
),
StrikethroughStyle {
color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
thickness: run_strikethrough.thickness,
},
));
}
run_end += style_run.len as usize;
color = style_run.color;
@ -190,6 +224,7 @@ fn paint_line(
run_end = layout.len;
finished_background = current_background.take();
finished_underline = current_underline.take();
finished_strikethrough = current_strikethrough.take();
}
}
@ -211,6 +246,14 @@ fn paint_line(
);
}
if let Some((strikethrough_origin, strikethrough_style)) = finished_strikethrough {
cx.paint_strikethrough(
strikethrough_origin,
glyph_origin.x - strikethrough_origin.x,
&strikethrough_style,
);
}
let max_glyph_bounds = Bounds {
origin: glyph_origin,
size: max_glyph_size,
@ -263,5 +306,13 @@ fn paint_line(
);
}
if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
cx.paint_strikethrough(
strikethrough_start,
last_line_end_x - strikethrough_start.x,
&strikethrough_style,
);
}
Ok(())
}

View File

@ -225,6 +225,7 @@ mod tests {
font: font("Helvetica"),
color: Default::default(),
underline: Default::default(),
strikethrough: None,
background_color: None,
};
let bold = TextRun {
@ -232,6 +233,7 @@ mod tests {
font: font("Helvetica").bold(),
color: Default::default(),
underline: Default::default(),
strikethrough: None,
background_color: None,
};

View File

@ -34,8 +34,8 @@ use crate::{
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, Window,
WindowContext, SUBPIXEL_VARIANTS,
StackingOrder, StrikethroughStyle, Style, Surface, TextStyleRefinement, Underline,
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
};
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
@ -758,6 +758,38 @@ impl<'a> ElementContext<'a> {
);
}
/// Paint a strikethrough into the scene for the next frame at the current z-index.
pub fn paint_strikethrough(
&mut self,
origin: Point<Pixels>,
width: Pixels,
style: &StrikethroughStyle,
) {
let scale_factor = self.scale_factor();
let height = style.thickness;
let bounds = Bounds {
origin,
size: size(width, height),
};
let content_mask = self.content_mask();
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
Underline {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default(),
wavy: false,
},
);
}
/// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
///
/// The y component of the origin is the baseline of the glyph.

View File

@ -282,6 +282,7 @@ impl PickerDelegate for OutlineViewDelegate {
line_height: relative(1.).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};

View File

@ -88,6 +88,7 @@ impl BufferSearchBar {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};

View File

@ -1632,6 +1632,7 @@ impl ProjectSearchBar {
line_height: relative(1.3).into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};

View File

@ -362,6 +362,7 @@ impl TerminalElement {
..text_style.font()
},
underline,
strikethrough: None,
};
if let Some((style, range)) = hyperlink {
@ -414,6 +415,7 @@ impl TerminalElement {
color: Some(theme.colors().link_text_hover),
wavy: false,
}),
strikethrough: None,
fade_out: None,
};
@ -427,6 +429,7 @@ impl TerminalElement {
white_space: WhiteSpace::Normal,
// These are going to be overridden per-cell
underline: None,
strikethrough: None,
color: theme.colors().text,
font_weight: FontWeight::NORMAL,
};
@ -545,6 +548,7 @@ impl TerminalElement {
color: theme.colors().terminal_background,
background_color: None,
underline: Default::default(),
strikethrough: None,
}],
)
.unwrap()