From ad3940c66f49ab6d6f62500b11a7751097006dbf Mon Sep 17 00:00:00 2001 From: Kieran Gill Date: Wed, 7 Feb 2024 09:51:27 -0500 Subject: [PATCH] text rendering: support strikethroughs (#7363) image 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 --- crates/assistant/src/assistant_panel.rs | 2 + .../src/chat_panel/message_editor.rs | 1 + crates/collab_ui/src/collab_panel.rs | 1 + crates/editor/src/editor.rs | 2 + crates/editor/src/element.rs | 8 +++ crates/gpui/src/style.rs | 28 ++++++++++ crates/gpui/src/text_system.rs | 9 +++- crates/gpui/src/text_system/line.rs | 53 ++++++++++++++++++- crates/gpui/src/text_system/line_wrapper.rs | 2 + crates/gpui/src/window/element_cx.rs | 36 ++++++++++++- crates/outline/src/outline.rs | 1 + crates/search/src/buffer_search.rs | 1 + crates/search/src/project_search.rs | 1 + crates/terminal_view/src/terminal_element.rs | 4 ++ 14 files changed, 145 insertions(+), 4 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 42e4180adde2cb255199d634f4d9a21e196ddf36..3bd928961daa1530671b53291f53def9a70c06ce 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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( diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index d29929e1a21beab665f60b92dcbc86af896b14cb..ed47c7a54a702a7a120600a815bb37ec0a7c4b12 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -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, }; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9460be3e954aa6fef86b5ca9bea6b1164d92ec68..a010abdbcb08cafdd8d44ec73b4a91022bf5e391 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2068,6 +2068,7 @@ impl CollabPanel { line_height: relative(1.3).into(), background_color: None, underline: None, + strikethrough: None, white_space: WhiteSpace::Normal, }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d37ba5f3c72b662f7d0d294d97b3fdc971f26db2..b30c9dd8489e40666471aa810e905d4a88608e17 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, }, }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 46c85ddd104b70d850ade75ea4a222b0f492ebcc..24b0a06bcb53cd850dbb0decf45b06f914eb0ecb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, }], ) } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 6118a0ae963621d22944f54e59b2b1b4ce361194..c7054b98c5553eaee4cb8f1a3689061cdb6174d0 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -197,6 +197,9 @@ pub struct TextStyle { /// The underline style of the text pub underline: Option, + /// The strikethrough style of the text + pub strikethrough: Option, + /// 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, + /// The underline style of the text + pub strikethrough: Option, + /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant. pub fade_out: Option, } @@ -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, +} + /// 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)) => { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 12242e26c245be91167c3f34242e9e7a8b3d01e5..43d7b2bb1bfe78f96c4d81e96ab6a53c30f464d9 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -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, /// The underline style (if any) pub underline: Option, + /// The strikethrough style (if any) + pub strikethrough: Option, } /// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`]. diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index e4fe5aad043344da965d26414fada05c571dd4a0..fbf34d39b2413e535252771a7f5c0f82d01691ed 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -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, + + /// The strikethrough style for this run + pub strikethrough: Option, } /// 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, UnderlineStyle)> = None; + let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; let mut current_background: Option<(Point, 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, Hsla)> = None; let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + let mut finished_strikethrough: Option<(Point, 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(()) } diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 0abe31352d58753b09030303f6a0d9de94ffab0b..56f18ea48e21e908652d9fd0cf66ea11fda59bd8 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -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, }; diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 8ba3fc5c4fc94c27629d18efee3c45e023b4a8a1..2f05e716731c029781e980c97658e31f3790c83f 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -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; @@ -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, + 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. diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 53f78b8fbd45bbf208aa7cc537b4d5d3638ded40..e670aa550ac503b8115679b6daafd808ee4d7017 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -282,6 +282,7 @@ impl PickerDelegate for OutlineViewDelegate { line_height: relative(1.).into(), background_color: None, underline: None, + strikethrough: None, white_space: WhiteSpace::Normal, }; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index bf01347ce80c588f0e137984f46704ef06662ab5..e09080e6b2ebcef0d8a0dbf79d1974094e2e9c4f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -88,6 +88,7 @@ impl BufferSearchBar { line_height: relative(1.3).into(), background_color: None, underline: None, + strikethrough: None, white_space: WhiteSpace::Normal, }; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 89f94ad6c431598f2e3b3fdeb8b6a044b839529c..f79e2556a38edcb6132fcef3b4f27c7f564c27cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1632,6 +1632,7 @@ impl ProjectSearchBar { line_height: relative(1.3).into(), background_color: None, underline: None, + strikethrough: None, white_space: WhiteSpace::Normal, }; diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 7ddb5ad988c6847dc91d0f75bfff29e692ab6033..eaac6069c21a7f8592669e91ba4fb7e9a4248dc9 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -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()