From 993838b939e28c16f1986e679049caf2de3976f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Wed, 1 Jul 2026 22:16:06 +0200 Subject: [PATCH] text-props2 --- examples/bode_rlc.rs | 10 +- src/des.rs | 60 ++++++++-- src/des/axis.rs | 17 ++- src/des/colorbar.rs | 8 +- src/des/legend.rs | 8 +- src/des/sd.rs | 120 +++++++++++++++++--- src/des/sd/colorbar.rs | 2 +- src/des/sd/legend.rs | 2 +- src/drawing.rs | 13 +-- src/drawing/annot.rs | 7 +- src/drawing/axis.rs | 51 ++------- src/drawing/colorbar.rs | 15 +-- src/drawing/figure.rs | 5 +- src/drawing/legend.rs | 19 ++-- src/drawing/series.rs | 12 +- src/style/defaults.rs | 2 - text/examples/text_line.rs | 10 +- text/examples/text_rich.rs | 12 +- text/examples/text_rich_parse.rs | 6 +- text/src/lib.rs | 2 +- text/src/line.rs | 38 ++++--- text/src/props.rs | 107 +++++++++--------- text/src/rich.rs | 24 ++-- text/src/rich/builder.rs | 61 +++++----- text/src/rich/parse.rs | 24 ++-- text/src/rich/render.rs | 8 +- text/src/sd.rs | 186 ++++++++++++++++--------------- 27 files changed, 460 insertions(+), 369 deletions(-) diff --git a/examples/bode_rlc.rs b/examples/bode_rlc.rs index 760be278..3b60d2fa 100644 --- a/examples/bode_rlc.rs +++ b/examples/bode_rlc.rs @@ -52,11 +52,11 @@ fn main() { (100.0, "mag3", "phase3", "R = 100 Ω"), ]; - // [&str; 1] converts to Text::Rich - let title = [concat!( - "Bode diagram of RLC circuit\n", - "[size=18;italic;font=serif]L = 0.1 mH / C = 1 µF[/size;italic;font]" - )]; + // &[&str] converts to Text::Rich with one line per element + let title = &[ + "Bode diagram of RLC circuit", + "[size=18;italic;font=serif]L = 0.1 mH / C = 1 µF[/size;italic;font]", + ]; // magnitude X axis scale is taken from the phase X axis // the reference uses the title given to the phase X axis diff --git a/src/des.rs b/src/des.rs index 794db790..ec117bbc 100644 --- a/src/des.rs +++ b/src/des.rs @@ -39,14 +39,14 @@ pub enum Text { /// The format string for the rich text, with optional classes fmt: String, /// The classes that can be used in the format string - classes: Vec<(String, text::TextModifiers)>, + classes: Vec<(String, text::TextProps)>, }, } impl Text { pub(crate) fn to_rich_text( &self, - base: text::props::TextProps, + base: text::props::TextBaseProps, layout: text::rich::Layout, db: &text::fontdb::Database, ) -> std::result::Result, text::Error> { @@ -81,17 +81,31 @@ impl From<&str> for Text { } } -impl From<[String; 1]> for Text { - fn from(arr: [String; 1]) -> Self { - let mut arr = arr; - let fmt = std::mem::take(&mut arr[0]); +impl From<&[String]> for Text { + fn from(arr: &[String]) -> Self { + let fmt = arr.join("\n"); Text::Rich(fmt) } } -impl From<[&str; 1]> for Text { - fn from(arr: [&str; 1]) -> Self { - Text::Rich(arr[0].to_string()) +impl From<&[&str]> for Text { + fn from(arr: &[&str]) -> Self { + let fmt = arr.join("\n"); + Text::Rich(fmt) + } +} + +impl From<&[String; N]> for Text { + fn from(arr: &[String; N]) -> Self { + let fmt = arr.join("\n"); + Text::Rich(fmt) + } +} + +impl From<&[&str; N]> for Text { + fn from(arr: &[&str; N]) -> Self { + let fmt = arr.join("\n"); + Text::Rich(fmt) } } @@ -107,8 +121,32 @@ impl From<(&str,)> for Text { } } -impl From<(String, Vec<(String, text::TextModifiers)>)> for Text { - fn from(tuple: (String, Vec<(String, text::TextModifiers)>)) -> Self { +impl From<(String, String)> for Text { + fn from(tuple: (String, String)) -> Self { + Text::Rich(tuple.0 + "\n" + &tuple.1) + } +} + +impl From<(&str, &str)> for Text { + fn from(tuple: (&str, &str)) -> Self { + Text::Rich(tuple.0.to_string() + "\n" + tuple.1) + } +} + +impl From<(String, String, String)> for Text { + fn from(tuple: (String, String, String)) -> Self { + Text::Rich(tuple.0 + "\n" + &tuple.1 + "\n" + &tuple.2) + } +} + +impl From<(&str, &str, &str)> for Text { + fn from(tuple: (&str, &str, &str)) -> Self { + Text::Rich(tuple.0.to_string() + "\n" + tuple.1 + "\n" + tuple.2) + } +} + +impl From<(String, Vec<(String, text::TextProps)>)> for Text { + fn from(tuple: (String, Vec<(String, text::TextProps)>)) -> Self { Text::RichWithClasses { fmt: tuple.0, classes: tuple.1, diff --git a/src/des/axis.rs b/src/des/axis.rs index b8379cb9..5a2fc623 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -707,7 +707,7 @@ pub mod ticks { pub struct Ticks { locator: Locator, formatter: Option, - txt_modifiers: text::TextModifiers, + txt_props: text::TextProps, color: theme::Color, } @@ -720,7 +720,7 @@ pub mod ticks { Ticks { locator: Locator::default(), formatter: Some(Formatter::default()), - txt_modifiers: text::TextModifiers::default(), + txt_props: text::TextProps::default(), color: theme::Col::Foreground.into(), } } @@ -741,12 +741,9 @@ pub mod ticks { pub fn with_formatter(self, formatter: Option) -> Self { Self { formatter, ..self } } - /// Returns a new ticks with the specified text modifiers - pub fn with_font(self, txt_modifiers: text::TextModifiers) -> Self { - Self { - txt_modifiers, - ..self - } + /// Returns a new ticks with the specified text properties + pub fn with_font(self, txt_props: text::TextProps) -> Self { + Self { txt_props, ..self } } /// Returns a new ticks with the specified color pub fn with_color(self, color: theme::Color) -> Self { @@ -763,8 +760,8 @@ pub mod ticks { self.formatter.as_ref() } /// Text properties for the ticks labels - pub fn font(&self) -> &text::TextModifiers { - &self.txt_modifiers + pub fn font(&self) -> &text::TextProps { + &self.txt_props } /// Color for the ticks. /// Will be used for the labels as well unless a specific color is set in [`font`](Self::font). diff --git a/src/des/colorbar.rs b/src/des/colorbar.rs index 561bbdf3..14690fca 100644 --- a/src/des/colorbar.rs +++ b/src/des/colorbar.rs @@ -24,7 +24,7 @@ pub struct ColorBar { pub(crate) pos: Pos, width: f32, title: Option, - ticks_font: text::TextModifiers, + ticks_font: text::TextProps, border: Option, locator: axis::ticks::Locator, margin: f32, @@ -36,7 +36,7 @@ impl Default for ColorBar { pos: Pos::default(), width: defaults::COLORBAR_WIDTH, title: None, - ticks_font: text::TextModifiers::default(), + ticks_font: text::TextProps::default(), border: Some(theme::Stroke { color: theme::Col::Foreground.into(), width: 1.0, @@ -71,7 +71,7 @@ impl ColorBar { } /// Set the ticks font properties and return self for chaining - pub fn with_ticks_font(mut self, ticks_font: text::TextModifiers) -> Self { + pub fn with_ticks_font(mut self, ticks_font: text::TextProps) -> Self { self.ticks_font = ticks_font; self } @@ -110,7 +110,7 @@ impl ColorBar { } /// Get the ticks font properties - pub fn ticks_font(&self) -> &text::TextModifiers { + pub fn ticks_font(&self) -> &text::TextProps { &self.ticks_font } diff --git a/src/des/legend.rs b/src/des/legend.rs index 577b0847..27960252 100644 --- a/src/des/legend.rs +++ b/src/des/legend.rs @@ -11,7 +11,7 @@ use crate::text; #[derive(Debug, Clone, PartialEq)] pub struct Legend { pos: Pos, - font: text::TextModifiers, + font: text::TextProps, fill: Option, border: Option, columns: Option, @@ -30,7 +30,7 @@ impl Default for Legend { fn default() -> Self { Self { pos: Pos::default(), - font: text::TextModifiers::default(), + font: text::TextProps::default(), fill: defaults::legend_fill(), border: Some(theme::Col::LegendBorder.into()), columns: None, @@ -66,7 +66,7 @@ where impl Legend { /// Get the font configuration for legend entries - pub fn font(&self) -> &text::TextModifiers { + pub fn font(&self) -> &text::TextProps { &self.font } @@ -106,7 +106,7 @@ impl Legend { } /// Set the font configuration for legend entries and return self for chaining - pub fn with_font(self, font: text::TextModifiers) -> Self { + pub fn with_font(self, font: text::TextProps) -> Self { Self { font, ..self } } diff --git a/src/des/sd.rs b/src/des/sd.rs index f9b4ed84..0208fd01 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -30,8 +30,10 @@ impl serde::Serialize for Text { match self { Text::Plain(text) => serializer.serialize_str(text), Text::Rich(fmt) => { - let mut seq = serializer.serialize_seq(Some(1))?; - seq.serialize_element(fmt)?; + let mut seq = serializer.serialize_seq(None)?; + for l in fmt.lines() { + seq.serialize_element(l)?; + } seq.end() } Text::RichWithClasses { fmt, classes } => { @@ -44,18 +46,48 @@ impl serde::Serialize for Text { } } -struct RichPropsMap(Vec<(String, text::TextModifiers)>); +enum TextPropsMapOrString { + Props(Vec<(String, text::TextProps)>), + String(String), +} -impl<'de> serde::Deserialize<'de> for RichPropsMap { +impl<'de> serde::Deserialize<'de> for TextPropsMapOrString { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - let map = - std::collections::HashMap::>::deserialize( - deserializer, - )?; - Ok(RichPropsMap(map.into_iter().collect())) + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = TextPropsMapOrString; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a text properties object") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(TextPropsMapOrString::String(value.to_string())) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut result = Vec::new(); + + while let Some((key, value)) = + map.next_entry::>()? + { + result.push((key, value)); + } + Ok(TextPropsMapOrString::Props(result)) + } + } + + deserializer.deserialize_any(Visitor) } } @@ -87,16 +119,72 @@ impl<'de> serde::Deserialize<'de> for Text { let fmt: String = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - let classes: Option = seq.next_element()?; - if let Some(classes) = classes { - Ok(Text::RichWithClasses { + let next = seq.next_element::()?; + match next { + Some(TextPropsMapOrString::Props(props)) => Ok(Text::RichWithClasses { fmt, - classes: classes.0, - }) - } else { - Ok(Text::Rich(fmt)) + classes: props, + }), + Some(TextPropsMapOrString::String(s2)) => { + let mut fmt = fmt + "\n" + &s2; + while let Some(s) = seq.next_element::()? { + fmt.push('\n'); + fmt.push_str(&s); + } + Ok(Text::Rich(fmt)) + } + None => Ok(Text::Rich(fmt)), } } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut fmt = Option::::None; + let mut classes = Vec::new(); + while let Some((key, value)) = map.next_entry::()? { + match key.as_str() { + "fmt" => { + if fmt.is_some() { + return Err(serde::de::Error::duplicate_field("fmt")); + } + match value { + TextPropsMapOrString::String(s) => fmt = Some(s), + TextPropsMapOrString::Props(_) => { + return Err(serde::de::Error::custom( + "The 'fmt' field must be a string, not an object", + )); + } + } + } + "classes" => { + if !classes.is_empty() { + return Err(serde::de::Error::duplicate_field("classes")); + } + match value { + TextPropsMapOrString::Props(props) => classes = props, + TextPropsMapOrString::String(_) => { + return Err(serde::de::Error::custom( + "The 'classes' field must be an object, not a string", + )); + } + } + } + _ => { + return Err(serde::de::Error::unknown_field( + key.as_str(), + &["fmt", "classes"], + )); + } + } + } + + let Some(fmt) = fmt else { + return Err(serde::de::Error::missing_field("fmt")); + }; + Ok(Text::RichWithClasses { fmt, classes }) + } } deserializer.deserialize_any(TextVisitor) } diff --git a/src/des/sd/colorbar.rs b/src/des/sd/colorbar.rs index f9d847b6..5181f989 100644 --- a/src/des/sd/colorbar.rs +++ b/src/des/sd/colorbar.rs @@ -128,7 +128,7 @@ impl<'de> serde::Deserialize<'de> for colorbar::ColorBar { "title" => title: Option, "border" => border: Option>, "ticks" => ticks: Option, - "ticksFont" => ticks_font: Option>, + "ticksFont" => ticks_font: Option>, "margin" => margin: Option, ); let mut colorbar = if let Some(pos) = pos { diff --git a/src/des/sd/legend.rs b/src/des/sd/legend.rs index 537a4e0a..4773ca4a 100644 --- a/src/des/sd/legend.rs +++ b/src/des/sd/legend.rs @@ -117,7 +117,7 @@ where where S: serde::Serializer, { - let font_default = self.font() == &text::TextModifiers::default(); + let font_default = self.font() == &text::TextProps::default(); let fill_default = self.fill() == defaults::legend_fill().as_ref(); let border_default = self.border() == Some(&theme::Col::LegendBorder.into()); let columns_default = self.columns().is_none(); diff --git a/src/drawing.rs b/src/drawing.rs index 50693f42..6e8b6621 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -241,21 +241,18 @@ struct TextSpan { stroke: Option, } -fn resolve_line_font( - modifiers: &text::TextModifiers, - default: text::Font, -) -> text::Font { +fn resolve_line_font(props: &text::TextProps, default: text::Font) -> text::Font { let mut res = default; - if let Some(families) = modifiers.families.as_ref() { + if let Some(families) = props.family.as_ref() { res = res.with_families(families.clone()); } - if let Some(style) = modifiers.style { + if let Some(style) = props.style { res = res.with_style(style); } - if let Some(weight) = modifiers.weight { + if let Some(weight) = props.weight { res = res.with_weight(weight); } - if let Some(width) = modifiers.width { + if let Some(width) = props.width { res = res.with_width(width); } res diff --git a/src/drawing/annot.rs b/src/drawing/annot.rs index a78ba124..149f51e4 100644 --- a/src/drawing/annot.rs +++ b/src/drawing/annot.rs @@ -6,7 +6,7 @@ use crate::des::{self}; use crate::drawing::axis::{Axis, Orientation}; use crate::drawing::plot::Axes; use crate::drawing::{Text, marker}; -use crate::style::{self, AsPaint, AsStroke, defaults, theme}; +use crate::style::{self, AsPaint, AsStroke, theme}; use crate::{Style, data, geom, render, text}; #[derive(Debug, Clone)] @@ -53,10 +53,7 @@ where Anchor::Center => (text::rich::Align::Center, text::rich::VerAlign::Center), }; let text = label.text().to_rich_text( - text::props::TextProps::::new( - defaults::FONT_FAMILY.parse().unwrap(), - 12.0, - ), + text::props::TextBaseProps::::new(12.0), text::rich::Layout::Horizontal(align, ver_align, Default::default()), self.fontdb(), )?; diff --git a/src/drawing/axis.rs b/src/drawing/axis.rs index 8bced420..524df40a 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -9,7 +9,6 @@ mod side; #[cfg(feature = "time")] pub use bounds::TimeBounds; pub use bounds::{AsBoundRef, Bounds, BoundsRef, NumBounds}; -use plotive_text::props::FontProps; pub use side::Side; use crate::drawing::scale::{self, CoordMap}; @@ -350,10 +349,7 @@ where .title() .map(|title| { title.to_rich_text( - text::props::TextProps::new( - defaults::FONT_FAMILY.parse().unwrap(), - defaults::AXIS_TITLE_FONT_SIZE, - ), + text::props::TextBaseProps::new(defaults::AXIS_TITLE_FONT_SIZE), side.title_layout(), self.fontdb(), ) @@ -520,10 +516,7 @@ where major_ticks: &des::axis::Ticks, ) -> Result<(text::Font, f32, theme::Color), Error> { let font_props = major_ticks.font(); - let font = super::resolve_line_font( - font_props, - text::Font::new(text::parse_font_families(defaults::FONT_FAMILY).unwrap()), - ); + let font = super::resolve_line_font(font_props, text::Font::default()); let font_size = major_ticks .font() .size @@ -559,12 +552,7 @@ where let mut ticks = Vec::new(); for loc in major_locs.into_iter() { let text = lbl_formatter.format_label(loc.into()); - let lbl = text::LineText::new( - text, - ticks_align, - FontProps::new(font.clone(), font_size), - db, - )?; + let lbl = text::LineText::new(text, ticks_align, font_size, font.clone(), db)?; let lbl = Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))?; ticks.push(NumTick { loc, lbl }); } @@ -574,14 +562,7 @@ where } else { lbl_formatter .axis_annotation() - .map(|l| { - text::LineText::new( - l.to_string(), - annot_align, - FontProps::new(font, font_size), - db, - ) - }) + .map(|l| text::LineText::new(l.to_string(), annot_align, font_size, font, db)) .transpose()? .map(|lbl| Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))) .transpose()? @@ -646,12 +627,7 @@ where let mut ticks = Vec::new(); for loc in major_locs.into_iter() { let text = lbl_formatter.format_label(loc.into()); - let lbl = text::LineText::new( - text, - ticks_align, - FontProps::new(font.clone(), font_size), - db, - )?; + let lbl = text::LineText::new(text, ticks_align, font_size, font.clone(), db)?; let lbl = Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))?; ticks.push(NumTick { loc: loc.timestamp(), @@ -661,14 +637,7 @@ where let annot = lbl_formatter .axis_annotation() - .map(|l| { - text::LineText::new( - l.to_string(), - annot_align, - FontProps::new(font, font_size), - db, - ) - }) + .map(|l| text::LineText::new(l.to_string(), annot_align, font_size, font, db)) .transpose()? .map(|lbl| Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))) .transpose()?; @@ -694,12 +663,8 @@ where let mut lbls = Vec::with_capacity(cb.len()); for cat in cb.iter() { - let lbl = text::LineText::new( - cat.to_string(), - ticks_align, - FontProps::new(font.clone(), font_size), - db, - )?; + let lbl = + text::LineText::new(cat.to_string(), ticks_align, font_size, font.clone(), db)?; let lbl = Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))?; lbls.push(lbl); } diff --git a/src/drawing/colorbar.rs b/src/drawing/colorbar.rs index 6f2b2695..0d755917 100644 --- a/src/drawing/colorbar.rs +++ b/src/drawing/colorbar.rs @@ -2,7 +2,6 @@ use std::fmt; use std::sync::Arc; use plotive_base::Rgb8; -use plotive_text::props::FontProps; use crate::des::axis::ticks::Locator; use crate::des::{self, colorbar}; @@ -102,10 +101,7 @@ impl ColorBarBuilder { .title() .map(|title| { title.to_rich_text( - text::props::TextProps::new( - defaults::FONT_FAMILY.parse().unwrap(), - defaults::COLORBAR_TITLE_FONT_SIZE, - ), + text::props::TextBaseProps::new(defaults::COLORBAR_TITLE_FONT_SIZE), side.title_layout(), ctx.fontdb(), ) @@ -116,7 +112,7 @@ impl ColorBarBuilder { let align = side.ticks_labels_align(); let font_props = des.ticks_font().clone(); - let font = super::resolve_line_font(&font_props, defaults::FONT_FAMILY.parse().unwrap()); + let font = super::resolve_line_font(&font_props, Default::default()); let font_size = font_props .size .unwrap_or(defaults::COLORBAR_TICKS_FONT_SIZE); @@ -135,12 +131,7 @@ impl ColorBarBuilder { .filter(|t| view_bounds.contains(*t)) .map(|t| -> Result<_, super::Error> { let text = formatter.format_label(t.into()); - let lt = text::LineText::new( - text, - align, - FontProps::new(font.clone(), font_size), - ctx.fontdb(), - )?; + let lt = text::LineText::new(text, align, font_size, font.clone(), ctx.fontdb())?; let text = Text::from_line_text(<, ctx.fontdb(), color)?; Ok((data::Sample::Num(t), text)) }) diff --git a/src/drawing/figure.rs b/src/drawing/figure.rs index ea518f8e..66adbc1a 100644 --- a/src/drawing/figure.rs +++ b/src/drawing/figure.rs @@ -83,10 +83,7 @@ where text::line::VerAlign::Hanging.into(), Default::default(), ); - let base = text::props::TextProps::new( - defaults::FONT_FAMILY.parse().unwrap(), - defaults::TITLE_FONT_SIZE, - ); + let base = text::props::TextBaseProps::new(defaults::TITLE_FONT_SIZE); let rich = fig_title.to_rich_text(base, layout, self.fontdb())?; let paths = super::Text::from_rich_text(&rich, self.fontdb())?; diff --git a/src/drawing/legend.rs b/src/drawing/legend.rs index 3a9a3948..b58fedae 100644 --- a/src/drawing/legend.rs +++ b/src/drawing/legend.rs @@ -1,5 +1,3 @@ -use plotive_text::props::FontProps; - use crate::drawing::Text; use crate::geom::{Padding, Size}; use crate::style::{AsPaint, AsStroke, defaults, theme}; @@ -56,7 +54,7 @@ impl ShapeRef<'_> { #[derive(Debug, Clone)] pub struct Entry<'a> { pub label: &'a str, - pub txt_modifiers: Option<&'a text::TextModifiers>, + pub txt_props: Option<&'a text::TextProps>, pub shape: ShapeRef<'a>, } @@ -82,7 +80,7 @@ impl LegendEntry { #[derive(Debug)] pub struct LegendBuilder<'a> { - txt_modifiers: text::TextModifiers, + txt_props: text::TextProps, fill: Option, border: Option, columns: Option, @@ -115,7 +113,7 @@ impl<'a> LegendBuilder<'a> { columns.replace(1); } LegendBuilder { - txt_modifiers: legend.font().clone(), + txt_props: legend.font().clone(), fill: legend.fill().cloned(), border: legend.border().cloned(), columns, @@ -130,10 +128,10 @@ impl<'a> LegendBuilder<'a> { pub fn add_entry(&mut self, index: usize, entry: Entry) -> Result<(), drawing::Error> { let shape = entry.shape.to_shape(); - let font_props = entry.txt_modifiers.unwrap_or(&self.txt_modifiers); - let font = super::resolve_line_font(font_props, defaults::FONT_FAMILY.parse().unwrap()); - let font_size = font_props.size.unwrap_or(defaults::LEGEND_LABEL_FONT_SIZE); - let fill = font_props + let txt_props = entry.txt_props.unwrap_or(&self.txt_props); + let font = super::resolve_line_font(txt_props, Default::default()); + let font_size = txt_props.size.unwrap_or(defaults::LEGEND_LABEL_FONT_SIZE); + let fill = txt_props .color .clone() .flatten() @@ -146,7 +144,8 @@ impl<'a> LegendBuilder<'a> { let text = LineText::new( entry.label.to_string(), align, - FontProps::new(font, font_size), + font_size, + font, &self.fontdb, )?; let text = Text::from_line_text(&text, &self.fontdb, fill)?; diff --git a/src/drawing/series.rs b/src/drawing/series.rs index ca52d498..4f64688e 100644 --- a/src/drawing/series.rs +++ b/src/drawing/series.rs @@ -26,7 +26,7 @@ impl SeriesExt for des::series::Line { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::Line(self.stroke()), }) } @@ -36,7 +36,7 @@ impl SeriesExt for des::series::Scatter { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::Marker(self.marker()), }) } @@ -51,7 +51,7 @@ impl SeriesExt for des::series::Area { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::AreaRect { fill: Some(self.fill()), y1_stroke: self.y1_stroke(), @@ -65,7 +65,7 @@ impl SeriesExt for des::series::Histogram { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), }) } @@ -75,7 +75,7 @@ impl SeriesExt for des::series::Bars { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), }) } @@ -85,7 +85,7 @@ impl SeriesExt for des::series::BarSeries { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - txt_modifiers: None, + txt_props: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.outline()), }) } diff --git a/src/style/defaults.rs b/src/style/defaults.rs index c938be9b..99d0c6d0 100644 --- a/src/style/defaults.rs +++ b/src/style/defaults.rs @@ -1,7 +1,5 @@ use crate::{geom, style}; -pub const FONT_FAMILY: &str = "sans-serif"; - pub const FIG_SIZE: geom::Size = geom::Size::new(800.0, 600.0); pub const FIG_PADDING: geom::Padding = geom::Padding::Even(20.0); diff --git a/text/examples/text_line.rs b/text/examples/text_line.rs index 53139eef..1ad185e4 100644 --- a/text/examples/text_line.rs +++ b/text/examples/text_line.rs @@ -1,6 +1,6 @@ use line::LineText; use plotive_base::geom; -use plotive_text::{bundled_font_db, font, line, props}; +use plotive_text::{bundled_font_db, font, line}; fn main() { let mut db = bundled_font_db(); @@ -36,13 +36,7 @@ fn main() { for (text, align, (x, y)) in texts { let (tx, ty) = (*x, *y); - let line = LineText::new( - text.to_string(), - *align, - props::FontProps::new(font.clone(), 32.0), - &db, - ) - .unwrap(); + let line = LineText::new(text.to_string(), *align, 32.0, font.clone(), &db).unwrap(); line::render_line_text( &line, &mut pm_mut, diff --git a/text/examples/text_rich.rs b/text/examples/text_rich.rs index 8f7a0884..8073c172 100644 --- a/text/examples/text_rich.rs +++ b/text/examples/text_rich.rs @@ -42,7 +42,7 @@ fn main() { let start_line2 = line1.len(); let end_line2 = line1.len() + line2.len(); - let root_props = props::TextProps::new(sans_font.clone(), FS_LARGE); + let root_props = props::TextBaseProps::new(FS_LARGE).with_font(sans_font.clone()); let mut builder = RichTextBuilder::new(text, root_props).with_layout(rich::Layout::Horizontal( rich::Align::Center, @@ -52,7 +52,7 @@ fn main() { builder.add_span( start_rlc, end_rlc, - props::TextModifiers { + props::TextProps { weight: Some(font::Weight::BOLD), style: Some(font::Style::Italic), ..Default::default() @@ -61,8 +61,8 @@ fn main() { builder.add_span( start_line2, end_line2, - props::TextModifiers { - families: Some(serif_family), + props::TextProps { + family: Some(serif_family), size: Some(FS_MEDIUM), style: Some(font::Style::Italic), ..Default::default() @@ -90,7 +90,7 @@ fn main() { font::Family::Serif, ]); - let root_props = props::TextProps::new(serif_cjk_font, FS_LARGE); + let root_props = props::TextBaseProps::new(FS_LARGE).with_font(serif_cjk_font); let builder = RichTextBuilder::new(text.to_string(), root_props).with_layout(rich::Layout::Vertical( rich::Align::Start, @@ -114,7 +114,7 @@ fn main() { // Vertical french text let text = "Axe des ordonnées"; - let root_props = props::TextProps::new(sans_font.clone(), FS_SMALL); + let root_props = props::TextBaseProps::new(FS_SMALL).with_font(sans_font.clone()); let builder = RichTextBuilder::new(text.to_string(), root_props).with_layout(rich::Layout::Vertical( rich::Align::End, diff --git a/text/examples/text_rich_parse.rs b/text/examples/text_rich_parse.rs index f995fae1..fa250f3a 100644 --- a/text/examples/text_rich_parse.rs +++ b/text/examples/text_rich_parse.rs @@ -22,7 +22,11 @@ fn main() { ); let rich_text = text::parse_rich_text(fmt) .unwrap() - .into_builder(props::TextProps::new(sans_font, 36.0).with_render(color::BLACK.into())) + .into_builder( + props::TextBaseProps::new(36.0) + .with_font(sans_font) + .with_render(color::BLACK.into()), + ) .with_layout(rich::Layout::Horizontal( rich::Align::Center, rich::VerAlign::Center, diff --git a/text/src/lib.rs b/text/src/lib.rs index 0062d3c8..ece6edc8 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -31,7 +31,7 @@ pub mod sd; pub use font::{Font, ScaledMetrics, parse_font_families}; pub use line::{LineText, render_line_text}; -pub use props::{Foreground, TextModifiers, TextProps}; +pub use props::{Foreground, TextBaseProps, TextProps}; pub use rich::{ ParseRichTextError, ParsedRichText, RichPrimitive, RichText, RichTextBuilder, parse_rich_text, parse_rich_text_with_classes, render_rich_text, render_rich_text_with, diff --git a/text/src/line.rs b/text/src/line.rs index 4a3a87e2..6eb22815 100644 --- a/text/src/line.rs +++ b/text/src/line.rs @@ -5,8 +5,7 @@ use ttf_parser as ttf; use crate::bidi::{self, BidiAlgo}; use crate::font::{self, DatabaseExt}; -use crate::props::{self, FontProps}; -use crate::{Error, Font, ScriptDir, fontdb}; +use crate::{Error, Font, ScriptDir, fontdb, props}; /// Horizontal alignment #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -45,7 +44,8 @@ pub enum VerAlign { pub struct LineText { text: String, align: (Align, VerAlign), - font: FontProps, + font_size: f32, + font: Font, bbox: Option, main_dir: ScriptDir, metrics: font::ScaledMetrics, @@ -61,7 +61,11 @@ impl LineText { self.align } - pub fn font(&self) -> &FontProps { + pub fn font_size(&self) -> f32 { + self.font_size + } + + pub fn font(&self) -> &Font { &self.font } @@ -91,7 +95,8 @@ impl LineText { Self { text: String::new(), align: (Default::default(), Default::default()), - font: FontProps::new(font, 1.0), + font_size: 1.0, + font, bbox: None, main_dir: ScriptDir::LeftToRight, metrics: font::ScaledMetrics::null(), @@ -108,7 +113,8 @@ impl LineText { pub fn new( text: String, align: (Align, VerAlign), - font: FontProps, + font_size: f32, + font: Font, db: &fontdb::Database, ) -> Result { let default_lev = match crate::script_is_rtl(&text) { @@ -119,7 +125,7 @@ impl LineText { let mut bidi = BidiAlgo::Yep { default_lev }; let bidi_runs = bidi.visual_runs(&text, 0); if bidi_runs.is_empty() { - return Ok(LineText::new_empty(font.font.clone())); + return Ok(LineText::new_empty(font.clone())); } let main_dir = match default_lev { Some(lev) if lev.is_ltr() => ScriptDir::LeftToRight, @@ -134,7 +140,7 @@ impl LineText { let mut shapes = Vec::with_capacity(bidi_runs.len()); let mut ctx = Ctx { buffer: None }; for run in &bidi_runs { - let shape = Shape::shape_run(&text, run, &font, db, &mut ctx)?; + let shape = Shape::shape_run(&text, run, font_size, &font, db, &mut ctx)?; shapes.push(shape); } @@ -184,6 +190,7 @@ impl LineText { Ok(LineText { text, align: (align, ver_align), + font_size: font_size, font: font.clone(), bbox: Some(geom::Rect::from_trbl(top, x_cursor, bottom, x_start)), main_dir, @@ -254,14 +261,15 @@ impl Shape { fn shape_run( text: &str, run: &bidi::BidiRun, - font: &FontProps, + font_size: f32, + font: &Font, db: &fontdb::Database, ctx: &mut Ctx, ) -> Result { let face_id = db - .select_face_for_str(&font.font, text) - .or_else(|| db.select_face(&font.font)) - .ok_or_else(|| Error::NoSuchFont(font.font.clone()))?; + .select_face_for_str(&font, text) + .or_else(|| db.select_face(&font)) + .ok_or_else(|| Error::NoSuchFont(font.clone()))?; let mut buffer = ctx .buffer @@ -281,9 +289,9 @@ impl Shape { let (shape, metrics) = db .with_face_data(face_id, |data, index| -> Result<_, Error> { let face = ttf::Face::parse(data, index)?; - let metrics = font::face_metrics(&face).scaled(font.size); + let metrics = font::face_metrics(&face).scaled(font_size); let mut hbface = rustybuzz::Face::from_face(face); - font::apply_hb_variations(&mut hbface, &font.font); + font::apply_hb_variations(&mut hbface, &font); Ok((rustybuzz::shape(&hbface, &[], buffer), metrics)) }) @@ -322,7 +330,7 @@ pub fn render_line_text_with( for shape in line.shapes.iter() { db.with_face_data(shape.face_id, |data, index| { let mut face = ttf::Face::parse(data, index).unwrap(); - font::apply_ttf_variations(&mut face, &line.font.font); + font::apply_ttf_variations(&mut face, &line.font); // the path builder for the entire string let mut shape_builder = geom::PathBuilder::new(); diff --git a/text/src/props.rs b/text/src/props.rs index 52a4a3b9..0b69d78f 100644 --- a/text/src/props.rs +++ b/text/src/props.rs @@ -2,18 +2,6 @@ use plotive_base::{color, style}; use crate::font::{self, Font}; -#[derive(Debug, Clone, PartialEq)] -pub struct FontProps { - pub font: Font, - pub size: f32, -} - -impl FontProps { - pub fn new(font: Font, size: f32) -> Self { - FontProps { font, size } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Decorations { pub underline: bool, @@ -32,7 +20,7 @@ impl Foreground for color::Rgba8 { } } -/// Properties for rendering text, including color and outline. +/// Properties for rendering text, including c font: FontProps::new(font, size),olor and outline. #[derive(Debug, Clone, PartialEq)] pub struct RenderProps { /// The color of the text. This is the fill color for the text glyphs. @@ -63,30 +51,32 @@ impl Default for RenderProps { } #[derive(Debug, Clone, PartialEq)] -pub struct TextProps { - pub font: FontProps, - pub decorations: Decorations, - pub render: RenderProps, +pub struct TextBaseProps { + size: f32, + font: Font, + decorations: Decorations, + render: RenderProps, } -impl TextProps +impl TextBaseProps where C: Foreground, { - pub fn new(font: Font, size: f32) -> TextProps { - TextProps { - font: FontProps::new(font, size), + pub fn new(size: f32) -> TextBaseProps { + TextBaseProps { + size, + font: Font::default(), decorations: Decorations::default(), render: RenderProps::default(), } } } -impl TextProps +impl TextBaseProps where C: Clone, { - pub fn with_font(mut self, font: FontProps) -> Self { + pub fn with_font(mut self, font: Font) -> Self { self.font = font; self } @@ -101,32 +91,48 @@ where self } - pub fn apply_modifiers(&mut self, modifiers: &TextModifiers) { - if let Some(families) = &modifiers.families { - self.font.font.families = families.clone(); + pub fn size(&self) -> f32 { + self.size + } + + pub fn font(&self) -> &Font { + &self.font + } + + pub fn decorations(&self) -> &Decorations { + &self.decorations + } + + pub fn render(&self) -> &RenderProps { + &self.render + } + + pub fn apply_props(&mut self, props: &TextProps) { + if let Some(family) = &props.family { + self.font.families = family.clone(); } - if let Some(weight) = modifiers.weight { - self.font.font.weight = weight; + if let Some(weight) = props.weight { + self.font.weight = weight; } - if let Some(width) = modifiers.width { - self.font.font.width = width; + if let Some(width) = props.width { + self.font.width = width; } - if let Some(style) = modifiers.style { - self.font.font.style = style; + if let Some(style) = props.style { + self.font.style = style; } - if let Some(size) = modifiers.size { - self.font.size = size; + if let Some(size) = props.size { + self.size = size; } - if let Some(fill) = modifiers.color.as_ref() { + if let Some(fill) = props.color.as_ref() { self.render.fill = fill.clone(); } - if let Some(outline) = modifiers.outline.as_ref() { + if let Some(outline) = props.outline.as_ref() { self.render.outline = outline.clone(); } - if let Some(underline) = modifiers.underline { + if let Some(underline) = props.underline { self.decorations.underline = underline; } - if let Some(strikethrough) = modifiers.strikethrough { + if let Some(strikethrough) = props.strikethrough { self.decorations.strikethrough = strikethrough; } } @@ -156,17 +162,18 @@ impl RenderProps { } } -impl TextProps +impl TextBaseProps where C: Clone, { - /// Convert this TextProps to another color type using the provided mapping function - pub fn to_other_color(&self, color_map: M) -> TextProps + /// Convert this TextBaseProps to another color type using the provided mapping function + pub fn to_other_color(&self, color_map: M) -> TextBaseProps where D: Clone, M: Fn(&C) -> D, { - TextProps { + TextBaseProps { + size: self.size, font: self.font.clone(), decorations: self.decorations, render: self.render.to_other_color(color_map), @@ -174,11 +181,11 @@ where } } -/// A set of modifiers that can be applied on top of [`TextProps`] to alter the text appearance. +/// A set of text properties that can be applied on top of [`TextBaseProps`] to alter the text appearance. /// This is especially used for rich text rendering, where different parts of the text can have different styles. #[derive(Debug, Clone, PartialEq)] -pub struct TextModifiers { - pub families: Option>, +pub struct TextProps { + pub family: Option>, pub weight: Option, pub width: Option, pub style: Option, @@ -189,10 +196,10 @@ pub struct TextModifiers { pub strikethrough: Option, } -impl Default for TextModifiers { +impl Default for TextProps { fn default() -> Self { - TextModifiers { - families: None, + TextProps { + family: None, weight: None, width: None, style: None, @@ -205,9 +212,9 @@ impl Default for TextModifiers { } } -impl TextModifiers { +impl TextProps { pub(crate) fn affect_shape(&self) -> bool { - self.families.is_some() + self.family.is_some() || self.weight.is_some() || self.width.is_some() || self.style.is_some() diff --git a/text/src/rich.rs b/text/src/rich.rs index 85d79de7..e27e6a3a 100644 --- a/text/src/rich.rs +++ b/text/src/rich.rs @@ -14,7 +14,7 @@ pub use parse::{ }; pub use render::{RichPrimitive, render_rich_text, render_rich_text_with}; -use crate::props::{TextModifiers, TextProps}; +use crate::props::{TextBaseProps, TextProps}; /// Typographic alignment, possibly depending on the script direction. #[derive(Debug, Clone, Copy, Default)] @@ -160,7 +160,7 @@ where C: Clone + PartialEq, { text: String, - root_props: TextProps, + root_props: TextBaseProps, layout: Layout, spans: Vec>, } @@ -170,7 +170,7 @@ where C: Clone + PartialEq, { /// Create a new RichTextBuilder - pub fn new(text: String, root_props: TextProps) -> RichTextBuilder { + pub fn new(text: String, root_props: TextBaseProps) -> RichTextBuilder { RichTextBuilder { text, root_props, @@ -185,17 +185,13 @@ where } /// Add a new text span - pub fn add_span(&mut self, start: usize, end: usize, modifiers: TextModifiers) { + pub fn add_span(&mut self, start: usize, end: usize, props: TextProps) { assert!(start <= end); assert!( self.text.is_char_boundary(start) && self.text.is_char_boundary(end), "start and end must be on char boundaries" ); - self.spans.push(TextSpan { - start, - end, - modifiers, - }); + self.spans.push(TextSpan { start, end, props }); } /// Create a RichText from this builder @@ -315,7 +311,7 @@ where struct TextSpan { start: usize, end: usize, - modifiers: TextModifiers, + props: TextProps, } /// A line of rich text @@ -520,12 +516,12 @@ where /// The font of this shape pub fn font(&self) -> &font::Font { - &self.spans[0].props.font.font + self.spans[0].props.font() } /// The font of this shape pub fn font_size(&self) -> f32 { - self.spans[0].props.font.size + self.spans[0].props.size() } /// The text spans in this shape @@ -582,7 +578,7 @@ where { start: usize, end: usize, - props: TextProps, + props: TextBaseProps, bbox: Option, } @@ -615,7 +611,7 @@ where } /// The properties of this span - pub fn props(&self) -> &TextProps { + pub fn props(&self) -> &TextBaseProps { &self.props } diff --git a/text/src/rich/builder.rs b/text/src/rich/builder.rs index 914a784a..dffdcf09 100644 --- a/text/src/rich/builder.rs +++ b/text/src/rich/builder.rs @@ -7,7 +7,7 @@ use super::{ }; use crate::bidi::BidiAlgo; use crate::font::{self, DatabaseExt}; -use crate::props::{TextModifiers, TextProps}; +use crate::props::{TextBaseProps, TextProps}; use crate::{fontdb, line}; #[derive(Debug)] @@ -25,36 +25,36 @@ struct PropsResolver where C: Clone, { - init_props: TextProps, - stack: Vec>, + init_props: TextBaseProps, + stack: Vec>, } impl PropsResolver where C: Clone + PartialEq, { - fn new(init_props: TextProps) -> PropsResolver { + fn new(init_props: TextBaseProps) -> PropsResolver { PropsResolver { init_props, stack: Vec::new(), } } - fn resolved(&self) -> TextProps { - let mut props = self.init_props.clone(); - for modifiers in self.stack.iter() { - props.apply_modifiers(modifiers); + fn resolved(&self) -> TextBaseProps { + let mut base_props = self.init_props.clone(); + for props in self.stack.iter() { + base_props.apply_props(props); } - props + base_props } - fn push_modifiers(&mut self, modifiers: TextModifiers) { - self.stack.push(modifiers); + fn push_props(&mut self, props: TextProps) { + self.stack.push(props); } - fn pop_modifiers(&mut self, modifiers: &TextModifiers) { + fn pop_props(&mut self, props: &TextProps) { for i in (0..self.stack.len()).rev() { - if &self.stack[i] == modifiers { + if &self.stack[i] == props { self.stack.remove(i); break; } @@ -272,7 +272,7 @@ where boundaries.check_in(run.start); boundaries.check_in(run.end); } - for span in self.spans.iter().filter(|s| s.modifiers.affect_shape()) { + for span in self.spans.iter().filter(|s| s.props.affect_shape()) { boundaries.check_in(span.start); boundaries.check_in(span.end); } @@ -321,7 +321,7 @@ where for (span_start, span_end) in boundaries { for span in self.spans.iter() { if span.start == span_start { - ctx.resolver.push_modifiers(span.modifiers.clone()); + ctx.resolver.push_props(span.props.clone()); } } props_spans.push(PropsSpan { @@ -332,7 +332,7 @@ where }); for span in self.spans.iter() { if span.end == span_end { - ctx.resolver.pop_modifiers(&span.modifiers); + ctx.resolver.pop_props(&span.props); } } } @@ -341,9 +341,9 @@ where // which are all the same for the subspans within the shape let shape_props = &props_spans.first().unwrap().props; let face_id = fontdb - .select_face_for_str(&shape_props.font.font, txt) - .or_else(|| fontdb.select_face(&shape_props.font.font)) - .ok_or_else(|| Error::NoSuchFont(shape_props.font.font.clone()))?; + .select_face_for_str(&shape_props.font(), txt) + .or_else(|| fontdb.select_face(&shape_props.font())) + .ok_or_else(|| Error::NoSuchFont(shape_props.font().clone()))?; let mut buffer = ctx .buffer @@ -362,9 +362,9 @@ where let (glyphs, metrics, buffer) = fontdb .with_face_data(face_id, |data, index| -> Result<_, Error> { let face = ttf::Face::parse(data, index)?; - let metrics = font::face_metrics(&face).scaled(shape_props.font.size); + let metrics = font::face_metrics(&face).scaled(shape_props.size()); let mut hbface = rustybuzz::Face::from_face(face); - font::apply_hb_variations(&mut hbface, &shape_props.font.font); + font::apply_hb_variations(&mut hbface, &shape_props.font()); let buffer = rustybuzz::shape(&hbface, &[], buffer); @@ -698,12 +698,12 @@ mod tests { let db = bundled_font_db(); let mut builder: RichTextBuilder = RichTextBuilder::new( "Some RICH\ntext string".to_string(), - TextProps::new(Default::default(), 12.0), + TextBaseProps::new(12.0), ); builder.add_span( 5, 9, - TextModifiers { + TextProps { underline: Some(true), ..Default::default() }, @@ -715,15 +715,24 @@ mod tests { assert_eq!(text.lines[0].shapes[0].spans.len(), 2); assert_eq!(text.lines[1].shapes[0].spans.len(), 1); assert_eq!( - text.lines[0].shapes[0].spans[0].props.decorations.underline, + text.lines[0].shapes[0].spans[0] + .props + .decorations() + .underline, false ); assert_eq!( - text.lines[0].shapes[0].spans[1].props.decorations.underline, + text.lines[0].shapes[0].spans[1] + .props + .decorations() + .underline, true ); assert_eq!( - text.lines[1].shapes[0].spans[0].props.decorations.underline, + text.lines[1].shapes[0].spans[0] + .props + .decorations() + .underline, false ); } diff --git a/text/src/rich/parse.rs b/text/src/rich/parse.rs index de9cbb81..d5980cb8 100644 --- a/text/src/rich/parse.rs +++ b/text/src/rich/parse.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use plotive_base::style; -use crate::props::{TextModifiers, TextProps}; +use crate::props::{TextBaseProps, TextProps}; use crate::{RichTextBuilder, font}; /// Position into an input stream @@ -71,14 +71,14 @@ impl std::error::Error for ParseRichTextError {} #[derive(Debug, Clone)] pub struct ParsedRichText { pub text: String, - pub prop_spans: Vec<(Pos, Pos, TextModifiers)>, + pub prop_spans: Vec<(Pos, Pos, TextProps)>, } impl ParsedRichText where C: style::Color + PartialEq, { - pub fn into_builder(self, root_props: TextProps) -> RichTextBuilder { + pub fn into_builder(self, root_props: TextBaseProps) -> RichTextBuilder { let mut builder = RichTextBuilder::new(self.text, root_props); for (start, end, props) in self.prop_spans { builder.add_span(start, end, props); @@ -97,7 +97,7 @@ where pub fn parse_rich_text_with_classes( fmt: &str, - user_classes: &[(String, TextModifiers)], + user_classes: &[(String, TextProps)], ) -> Result, ParseRichTextError> where C: style::Color + FromStr, @@ -109,7 +109,7 @@ where #[derive(Debug, Clone)] struct RichTextParser<'a, C> { fmt: &'a str, - user_classes: &'a [(String, TextModifiers)], + user_classes: &'a [(String, TextProps)], } impl<'a, C> RichTextParser<'a, C> @@ -123,7 +123,7 @@ where } } - pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, TextModifiers)]) -> Self { + pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, TextProps)]) -> Self { Self { fmt, user_classes } } @@ -173,9 +173,9 @@ where Ok(ParsedRichText { text, prop_spans }) } - fn merge_props(base: TextModifiers, overlay: &TextModifiers) -> TextModifiers { - TextModifiers { - families: overlay.families.clone().or_else(|| base.families), + fn merge_props(base: TextProps, overlay: &TextProps) -> TextProps { + TextProps { + family: overlay.family.clone().or_else(|| base.family), weight: overlay.weight.or(base.weight), width: overlay.width.or(base.width), style: overlay.style.or(base.style), @@ -191,8 +191,8 @@ where &self, span: Span, tag: &lex::OpeningTag, - ) -> Result, ParseRichTextError> { - let mut props = TextModifiers::default(); + ) -> Result, ParseRichTextError> { + let mut props = TextProps::default(); for prop in &tag.0 { // if no value, it is a class, or boolean prop. // we first check for user classes, if no match, @@ -205,7 +205,7 @@ where } } "font-family" | "font" | "family" | "ff" => { - props.families = Some(font::parse_font_families(value).map_err(|_| { + props.family = Some(font::parse_font_families(value).map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?); } diff --git a/text/src/rich/render.rs b/text/src/rich/render.rs index 2d3faa06..89d5e43e 100644 --- a/text/src/rich/render.rs +++ b/text/src/rich/render.rs @@ -55,7 +55,7 @@ where } } - if span.props.decorations.underline { + if span.props.decorations().underline { let line = shape.metrics.uline; let path = crate::line_path( span.bbox(), @@ -66,7 +66,7 @@ where span_builder.push_path(&path); glyph_builder = path.clear(); } - if span.props.decorations.strikethrough { + if span.props.decorations().strikethrough { let line = shape.metrics.strikeout; let path = crate::line_path( span.bbox(), @@ -79,11 +79,11 @@ where } if let Some(path) = span_builder.finish() { - if let Some(fill) = span.props.render.fill.as_ref() { + if let Some(fill) = span.props.render().fill.as_ref() { let prim = RichPrimitive::Fill(&path, fill); render_fn(prim); } - if let Some(outline) = span.props.render.outline.as_ref() { + if let Some(outline) = span.props.render().outline.as_ref() { let prim = RichPrimitive::Stroke(&path, outline); render_fn(prim); } diff --git a/text/src/sd.rs b/text/src/sd.rs index 91c31c81..c63e3ff2 100644 --- a/text/src/sd.rs +++ b/text/src/sd.rs @@ -224,7 +224,7 @@ impl<'de> serde::de::Deserialize<'de> for font::Width { } } -impl serde::Serialize for props::TextModifiers +impl serde::Serialize for props::TextProps where C: serde::Serialize + style::DefaultStroke + style::DefaultStrokeWidth + PartialEq, { @@ -233,7 +233,7 @@ where S: serde::Serializer, { let mut state = serializer.serialize_map(None)?; - if let Some(families) = &self.families { + if let Some(families) = &self.family { let family_str = crate::font::font_families_to_string(families); state.serialize_entry("family", &family_str)?; } @@ -265,7 +265,7 @@ where } } -impl<'de, C> serde::de::Deserialize<'de> for props::TextModifiers +impl<'de, C> serde::de::Deserialize<'de> for props::TextProps where C: serde::de::Deserialize<'de> + Copy @@ -281,98 +281,104 @@ where where D: serde::Deserializer<'de>, { - struct Visitor(std::marker::PhantomData); + deserializer.deserialize_map(TextPropsVisitor::::new()) + } +} - impl<'de, C> serde::de::Visitor<'de> for Visitor - where - C: serde::de::Deserialize<'de> - + Copy - + Clone - + style::DefaultColor - + style::DefaultStroke - + style::DefaultStrokeWidth - + FromStr - + From, - ::Err: std::fmt::Display, - { - type Value = props::TextModifiers; +pub struct TextPropsVisitor(std::marker::PhantomData); - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a map representing TextModifiers") - } +impl TextPropsVisitor { + pub fn new() -> Self { + TextPropsVisitor(std::marker::PhantomData) + } +} - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - M::Error: serde::de::Error, - { - let mut props = props::TextModifiers::::default(); - while let Some(key) = map.next_key::()? { - match key.as_str() { - "family" => { - let family_str: String = map.next_value()?; - let value = font::parse_font_families(&family_str).map_err(|err| { - M::Error::custom(format!( - "Invalid font family string: {}. Error: {}", - family_str, err - )) - })?; - props.families = Some(value); - } - "weight" => { - let value: font::Weight = map.next_value()?; - props.weight = Some(value); - } - "width" => { - let value: font::Width = map.next_value()?; - props.width = Some(value); - } - "style" => { - let value: font::Style = map.next_value()?; - props.style = Some(value); - } - "size" => { - let value: f32 = map.next_value()?; - props.size = Some(value); - } - "color" => { - let value: Option> = map.next_value()?; - props.color = Some(value); - } - "outline" => { - let value: Option> = map.next_value()?; - props.outline = Some(value); - } - "underline" => { - let value: bool = map.next_value()?; - props.underline = Some(value); - } - "strikethrough" | "strikeout" => { - let value: bool = map.next_value()?; - props.strikethrough = Some(value); - } - other => { - return Err(M::Error::unknown_field( - other, - &[ - "family", - "weight", - "width", - "style", - "size", - "color", - "outline", - "underline", - "strikethrough", - ], - )); - } - } +impl<'de, C> serde::de::Visitor<'de> for TextPropsVisitor +where + C: serde::de::Deserialize<'de> + + Copy + + Clone + + style::DefaultColor + + style::DefaultStroke + + style::DefaultStrokeWidth + + FromStr + + From, + ::Err: std::fmt::Display, +{ + type Value = props::TextProps; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map representing TextProps") + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + M::Error: serde::de::Error, + { + let mut props = props::TextProps::::default(); + while let Some(key) = map.next_key::()? { + match key.as_str() { + "family" => { + let family_str: String = map.next_value()?; + let value = font::parse_font_families(&family_str).map_err(|err| { + M::Error::custom(format!( + "Invalid font family string: {}. Error: {}", + family_str, err + )) + })?; + props.family = Some(value); + } + "weight" => { + let value: font::Weight = map.next_value()?; + props.weight = Some(value); + } + "width" => { + let value: font::Width = map.next_value()?; + props.width = Some(value); + } + "style" => { + let value: font::Style = map.next_value()?; + props.style = Some(value); + } + "size" => { + let value: f32 = map.next_value()?; + props.size = Some(value); + } + "color" => { + let value: Option> = map.next_value()?; + props.color = Some(value); + } + "outline" => { + let value: Option> = map.next_value()?; + props.outline = Some(value); + } + "underline" => { + let value: bool = map.next_value()?; + props.underline = Some(value); + } + "strikethrough" | "strikeout" => { + let value: bool = map.next_value()?; + props.strikethrough = Some(value); + } + other => { + return Err(M::Error::unknown_field( + other, + &[ + "family", + "weight", + "width", + "style", + "size", + "color", + "outline", + "underline", + "strikethrough", + ], + )); } - Ok(props) } } - - deserializer.deserialize_map(Visitor::(std::marker::PhantomData)) + Ok(props) } }