mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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:
parent
55129d4d6c
commit
ad3940c66f
@ -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(
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -2068,6 +2068,7 @@ impl CollabPanel {
|
||||
line_height: relative(1.3).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
@ -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)) => {
|
||||
|
@ -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`].
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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.
|
||||
|
@ -282,6 +282,7 @@ impl PickerDelegate for OutlineViewDelegate {
|
||||
line_height: relative(1.).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
|
@ -88,6 +88,7 @@ impl BufferSearchBar {
|
||||
line_height: relative(1.3).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
|
@ -1632,6 +1632,7 @@ impl ProjectSearchBar {
|
||||
line_height: relative(1.3).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user