Start on text highlight support

This commit is contained in:
Nathan Sobo 2022-03-09 14:53:31 -07:00
parent 3dc100adfb
commit ac1eb19f83
15 changed files with 198 additions and 91 deletions

View file

@ -5,7 +5,7 @@ use std::{
};
use crate::json::ToJson;
use pathfinder_color::ColorU;
use pathfinder_color::{ColorF, ColorU};
use serde::{
de::{self, Unexpected},
Deserialize, Deserializer,
@ -48,6 +48,29 @@ impl Color {
pub fn from_u32(rgba: u32) -> Self {
Self(ColorU::from_u32(rgba))
}
pub fn blend(source: Color, dest: Color) -> Color {
if dest.a == 255 {
return dest;
}
let source = source.0.to_f32();
let dest = dest.0.to_f32();
let a = source.a() + (dest.a() * (1. - source.a()));
let r = ((source.r() * source.a()) + (dest.r() * dest.a() * (1. - source.a()))) / a;
let g = ((source.g() * source.a()) + (dest.g() * dest.a() * (1. - source.a()))) / a;
let b = ((source.b() * source.a()) + (dest.b() * dest.a() * (1. - source.a()))) / a;
Self(ColorF::new(r, g, b, a).to_u8())
}
pub fn fade_out(&mut self, factor: f32) {
let source_alpha = 1. - factor.clamp(0., 1.);
let dest_alpha = self.0.a as f32 / 255.;
let dest_alpha = source_alpha + (dest_alpha * (1. - source_alpha));
self.0.a = (dest_alpha * (1. / 255.)) as u8;
}
}
impl<'de> Deserialize<'de> for Color {

View file

@ -67,17 +67,20 @@ impl Element for Text {
let mut highlight_ranges = self.highlights.iter().peekable();
let chunks = std::iter::from_fn(|| {
let result;
if let Some((range, highlight)) = highlight_ranges.peek() {
if let Some((range, highlight_style)) = highlight_ranges.peek() {
if offset < range.start {
result = Some((&self.text[offset..range.start], None));
result = Some((
&self.text[offset..range.start],
HighlightStyle::from(&self.style),
));
offset = range.start;
} else {
result = Some((&self.text[range.clone()], Some(*highlight)));
result = Some((&self.text[range.clone()], *highlight_style));
highlight_ranges.next();
offset = range.end;
}
} else if offset < self.text.len() {
result = Some((&self.text[offset..], None));
result = Some((&self.text[offset..], HighlightStyle::from(&self.style)));
offset = self.text.len();
} else {
result = None;
@ -197,24 +200,24 @@ impl Element for Text {
/// Perform text layout on a series of highlighted chunks of text.
pub fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
style: &'a TextStyle,
chunks: impl Iterator<Item = (&'a str, HighlightStyle)>,
text_style: &'a TextStyle,
text_layout_cache: &'a TextLayoutCache,
font_cache: &'a Arc<FontCache>,
max_line_len: usize,
max_line_count: usize,
) -> Vec<Line> {
let mut layouts = Vec::with_capacity(max_line_count);
let mut prev_font_properties = style.font_properties.clone();
let mut prev_font_id = style.font_id;
let mut prev_font_properties = text_style.font_properties.clone();
let mut prev_font_id = text_style.font_id;
let mut line = String::new();
let mut styles = Vec::new();
let mut row = 0;
let mut line_exceeded_max_len = false;
for (chunk, highlight_style) in chunks.chain([("\n", None)]) {
for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
if ix > 0 {
layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
line.clear();
styles.clear();
row += 1;
@ -225,15 +228,13 @@ pub fn layout_highlighted_chunks<'a>(
}
if !line_chunk.is_empty() && !line_exceeded_max_len {
let highlight_style = highlight_style.unwrap_or(style.clone().into());
// Avoid a lookup if the font properties match the previous ones.
let font_id = if highlight_style.font_properties == prev_font_properties {
prev_font_id
} else {
font_cache
.select_font(style.font_family_id, &highlight_style.font_properties)
.unwrap_or(style.font_id)
.select_font(text_style.font_family_id, &highlight_style.font_properties)
.unwrap_or(text_style.font_id)
};
if line.len() + line_chunk.len() > max_line_len {

View file

@ -31,13 +31,16 @@ pub struct TextStyle {
pub underline: Option<Underline>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
pub underline: Option<Underline>,
pub fade_out: Option<f32>,
}
impl Eq for HighlightStyle {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Underline {
pub color: Color,
@ -83,6 +86,8 @@ struct HighlightStyleJson {
italic: bool,
#[serde(default)]
underline: UnderlineStyleJson,
#[serde(default)]
fade_out: Option<f32>,
}
#[derive(Deserialize)]
@ -131,7 +136,10 @@ impl TextStyle {
if self.font_properties != style.font_properties {
self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?;
}
self.color = style.color;
self.color = Color::blend(self.color, style.color);
if let Some(factor) = style.fade_out {
self.color.fade_out(factor);
}
self.underline = style.underline;
Ok(self)
}
@ -199,10 +207,17 @@ impl TextStyle {
impl From<TextStyle> for HighlightStyle {
fn from(other: TextStyle) -> Self {
Self::from(&other)
}
}
impl From<&TextStyle> for HighlightStyle {
fn from(other: &TextStyle) -> Self {
Self {
color: other.color,
font_properties: other.font_properties,
underline: other.underline,
fade_out: None,
}
}
}
@ -246,6 +261,18 @@ impl HighlightStyle {
color: json.color,
font_properties,
underline: underline_from_json(json.underline, json.color),
fade_out: json.fade_out,
}
}
pub fn highlight(&mut self, other: HighlightStyle) {
self.color = Color::blend(other.color, self.color);
if let Some(factor) = other.fade_out {
self.color.fade_out(factor);
}
self.font_properties = other.font_properties;
if other.underline.is_some() {
self.underline = other.underline;
}
}
}
@ -256,6 +283,7 @@ impl From<Color> for HighlightStyle {
color,
font_properties: Default::default(),
underline: None,
fade_out: None,
}
}
}
@ -295,6 +323,7 @@ impl<'de> Deserialize<'de> for HighlightStyle {
color: serde_json::from_value(json).map_err(de::Error::custom)?,
font_properties: Properties::new(),
underline: None,
fade_out: None,
})
}
}