diff --git a/base/src/color.rs b/base/src/color.rs index cafa71ab..6889f144 100644 --- a/base/src/color.rs +++ b/base/src/color.rs @@ -7,29 +7,6 @@ pub(crate) mod xkcd; pub use css4::*; -pub trait ResolveColor { - fn resolve_color(&self, color: &Color) -> Rgba8; -} - -pub trait Color: Clone + Copy { - #[inline] - fn resolve(&self, rc: &R) -> Rgba8 - where - R: ResolveColor, - Self: Sized, - { - rc.resolve_color(self) - } -} - -impl Color for Rgba8 {} - -impl ResolveColor for () { - fn resolve_color(&self, color: &Rgba8) -> Rgba8 { - *color - } -} - /// A simple color type with 8-bit RGB components, including an alpha channel. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Rgba8(u8, u8, u8, u8); diff --git a/base/src/lib.rs b/base/src/lib.rs index 4dbda7d5..b91ed8bc 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -1,7 +1,9 @@ pub mod color; -pub use color::{Color, ResolveColor, Rgb8, Rgba8}; +pub use color::{Rgb8, Rgba8}; pub mod geom; #[cfg(feature = "serde")] -mod sd; +pub mod sd; + +pub mod style; diff --git a/base/src/sd.rs b/base/src/sd.rs index 775ce00d..885afea1 100644 --- a/base/src/sd.rs +++ b/base/src/sd.rs @@ -1,11 +1,41 @@ use std::borrow::Cow; use std::collections::HashMap; use std::fmt; +use std::str::FromStr; use std::sync::LazyLock; +use serde::Serialize; +use serde::ser::SerializeStruct; + use crate::color::{css4, xkcd}; use crate::geom::{Padding, Size}; -use crate::{Rgb8, Rgba8, geom}; +use crate::style::{Color, DefaultColor, DefaultStroke, DefaultStrokeWidth, Stroke}; +use crate::{Rgb8, Rgba8, geom, style}; + +macro_rules! deserialize_map_fields { + ($de:lifetime, $map:expr, $($key:expr => $name:ident: Option<$ty:ty>,)+) => { + $( + let mut $name = None::<$ty>; + )+ + + while let Some(key) = $map.next_key::>()? { + match key.as_ref() { + $($key => { + if $name.is_some() { + let _: $ty = $map.next_value()?; + return Err(serde::de::Error::duplicate_field($key)); + } + $name = Some($map.next_value::<$ty>()?); + })+ + _ => { + return Err(serde::de::Error::unknown_field(key.as_ref(), &[$($key),+])); + } + } + } + } +} + +// MARK: Color static INVERSE_COLOR_MAP: LazyLock> = LazyLock::new(|| { let mut map = HashMap::with_capacity(xkcd::COLORS.len() + css4::COLORS.len()); @@ -101,6 +131,492 @@ impl<'de> serde::Deserialize<'de> for Rgba8 { } } +// MARK: style::Fill + +impl serde::Serialize for style::Fill +where + C: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let style::Fill::Solid { color, opacity } = self; + + if let Some(opacity) = opacity { + if *opacity == 1.0 { + color.serialize(serializer) + } else { + let mut state = serializer.serialize_struct("Fill", 2)?; + state.serialize_field("color", color)?; + state.serialize_field("opacity", opacity)?; + state.end() + } + } else { + color.serialize(serializer) + } + } +} + +impl<'de, C> serde::Deserialize<'de> for style::Fill +where + C: serde::Deserialize<'de> + Color + FromStr + DefaultColor, + ::Err: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(FillVisitor { + _phantom: std::marker::PhantomData, + }) + } +} + +struct FillVisitor { + _phantom: std::marker::PhantomData, +} + +impl<'de, C> serde::de::Visitor<'de> for FillVisitor +where + C: serde::de::Deserialize<'de> + Color + FromStr + DefaultColor, + ::Err: std::fmt::Display, +{ + type Value = style::Fill; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a fill color or a map with color and opacity") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + let color = value.parse::().map_err(E::custom)?; + Ok(style::Fill::Solid { + color, + opacity: None, + }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + deserialize_map_fields!( + 'de, map, + "color" => color: Option, + "opacity" => opacity: Option, + ); + + let color = match (color, C::default_color()) { + (Some(color), _) => color, + (None, Some(color)) => color, + (None, None) => return Err(serde::de::Error::missing_field("color")), + }; + + Ok(style::Fill::Solid { color, opacity }) + } +} + +// MARK: LinePattern + +impl serde::Serialize for style::LinePattern { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use style::LinePattern; + match self { + LinePattern::Solid => "solid".serialize(serializer), + LinePattern::Dashed => "dashed".serialize(serializer), + LinePattern::Dot => "dotted".serialize(serializer), + LinePattern::DashDot => "dash-dot".serialize(serializer), + LinePattern::Custom(dash) => dash.serialize(serializer), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for style::LinePattern { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(LinePatternVisitor) + } +} + +struct LinePatternVisitor; + +fn str_to_line_pattern(value: &str) -> Option { + match value { + "solid" => Some(style::LinePattern::Solid), + "dashed" => Some(style::LinePattern::Dashed), + "dotted" => Some(style::LinePattern::Dot), + "dash-dot" => Some(style::LinePattern::DashDot), + _ => None, + } +} + +impl<'de> serde::de::Visitor<'de> for LinePatternVisitor { + type Value = style::LinePattern; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a line pattern string or a dash array") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + str_to_line_pattern(value) + .ok_or_else(|| E::unknown_variant(value, &["solid", "dashed", "dotted", "dash-dot"])) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut dash: Vec = if let Some(sz) = seq.size_hint() { + Vec::with_capacity(sz) + } else { + Vec::new() + }; + + while let Some(value) = seq.next_element()? { + dash.push(value); + } + Ok(style::LinePattern::Custom(dash)) + } +} + +// MARK: Stroke + +impl serde::Serialize for Stroke +where + C: serde::Serialize + DefaultStroke + DefaultStrokeWidth + PartialEq, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_stroke(self, C::default_stroke(), "Stroke", serializer) + } +} + +impl<'de, C> serde::de::Deserialize<'de> for Stroke +where + C: serde::de::Deserialize<'de> + DefaultStroke + DefaultStrokeWidth + FromStr, + ::Err: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(StrokeVisitor::new("Stroke", C::default_stroke())) + } +} + +pub fn serialize_stroke( + stroke: &Stroke, + default_stroke: Option>, + name: &'static str, + serializer: S, +) -> Result +where + C: serde::Serialize + DefaultStrokeWidth + PartialEq, + S: serde::Serializer, +{ + let default_width = C::default_stroke_width(); + + let (has_default_color, has_default_width, has_default_pattern, has_default_opacity) = + if let Some(default) = default_stroke { + ( + default.color == stroke.color, + default.width == stroke.width, + default.pattern == stroke.pattern, + default.opacity == stroke.opacity, + ) + } else { + ( + false, + stroke.width == default_width, + stroke.pattern == Default::default(), + stroke.opacity.unwrap_or(1.0) == 1.0, + ) + }; + + match ( + has_default_color, + has_default_width, + has_default_pattern, + has_default_opacity, + ) { + (true, true, true, true) => "auto".serialize(serializer), + (false, true, true, true) => stroke.color.serialize(serializer), + (true, false, true, true) => stroke.width.serialize(serializer), + (true, true, false, true) => stroke.pattern.serialize(serializer), + _ => { + let fields = (!has_default_color as usize) + + (!has_default_width as usize) + + (!has_default_pattern as usize) + + (!has_default_opacity as usize); + let mut state = serializer.serialize_struct(name, fields)?; + if !has_default_color { + state.serialize_field("color", &stroke.color)?; + } + if !has_default_width { + state.serialize_field("width", &stroke.width)?; + } + if !has_default_pattern { + state.serialize_field("pattern", &stroke.pattern)?; + } + if !has_default_opacity { + state.serialize_field("opacity", &stroke.opacity.unwrap_or(1.0))?; + } + state.end() + } + } +} + +pub struct StrokeVisitor { + name: &'static str, + default_stroke: Option>, +} + +impl StrokeVisitor { + pub fn new(name: &'static str, default_stroke: Option>) -> Self { + Self { + name, + default_stroke, + } + } + + fn accepts_compact_pattern(&self) -> bool { + self.default_stroke.is_some() + } + + fn accepted_string_forms(&self) -> &'static str { + if self.accepts_compact_pattern() { + "'auto', a line pattern, or a color string" + } else { + "a color string" + } + } + + fn expecting_description(&self) -> &'static str { + if self.accepts_compact_pattern() { + "a stroke object, 'auto', a stroke width, a line pattern, a dash array, or a color string" + } else { + "a stroke object or a color string" + } + } + + fn invalid_string_message(&self) -> String { + format!( + "Invalid string value for {}: expected {}", + self.name, + self.accepted_string_forms() + ) + } + + fn no_default_numeric_message(&self) -> String { + format!( + "Numeric value is not valid for {} because there is no default stroke defined", + self.name + ) + } + + fn no_default_dash_array_message(&self) -> String { + format!( + "Dash array is not valid for {} because there is no default stroke defined", + self.name + ) + } +} + +impl<'de, C> serde::de::Visitor<'de> for StrokeVisitor +where + C: serde::de::Deserialize<'de> + DefaultStroke + DefaultStrokeWidth + FromStr, + ::Err: std::fmt::Display, +{ + type Value = Stroke; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str(self.expecting_description()) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + self.visit_f64(value as f64) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + self.visit_f64(value as f64) + } + + fn visit_f64(self, value: f64) -> Result + where + E: serde::de::Error, + { + let Some(default) = self.default_stroke else { + return Err(serde::de::Error::custom(self.no_default_numeric_message())); + }; + + if value <= 0.0 { + return Err(serde::de::Error::custom(format!( + "Invalid stroke width for {}: width cannot be null or negative", + self.name + ))); + } + + let width = value as f32; + Ok(Stroke { + color: default.color, + width, + pattern: default.pattern, + opacity: default.opacity, + }) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + let invalid_string_message = self.invalid_string_message(); + + if value == "auto" { + if let Some(default) = self.default_stroke { + Ok(default) + } else { + Err(serde::de::Error::custom(format!( + "'auto' is not a valid value for {} because there is no default stroke defined", + self.name + ))) + } + } else if let Some(default) = self.default_stroke { + if let Some(pattern) = str_to_line_pattern(value) { + return Ok(Self::Value::from(Stroke { + color: default.color, + width: default.width, + pattern, + opacity: default.opacity, + })); + } + + let color = value + .parse() + .map_err(|_| serde::de::Error::custom(invalid_string_message))?; + + Ok(Self::Value::from(Stroke { + color, + width: default.width, + pattern: default.pattern, + opacity: default.opacity, + })) + } else { + let color = value + .parse() + .map_err(|_| serde::de::Error::custom(invalid_string_message))?; + + Ok(Self::Value::from(Stroke { + color, + width: C::default_stroke_width(), + pattern: Default::default(), + opacity: None, + })) + } + } + + // TODO: check seq definition for stroke: color or dash pattern ?? + + // fn visit_seq(self, mut seq: A) -> Result + // where + // A: serde::de::SeqAccess<'de>, + // { + // let r = seq + // .next_element()? + // .ok_or_else(|| A::Error::custom("Expected red component"))?; + // let g = seq + // .next_element()? + // .ok_or_else(|| A::Error::custom("Expected green component"))?; + // let b = seq + // .next_element()? + // .ok_or_else(|| A::Error::custom("Expected blue component"))?; + // let a = seq.next_element()?.unwrap_or(255u8); + + // let color: C = Rgba8::new(r, g, b, a).into(); + // Ok(props::Outline { + // color, + // width: 1.0, + // pattern: props::LinePattern::Solid, + // }) + // } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let no_default_dash_array_message = self.no_default_dash_array_message(); + + let Some(default) = self.default_stroke else { + return Err(serde::de::Error::custom(no_default_dash_array_message)); + }; + + let mut dash: Vec = if let Some(sz) = seq.size_hint() { + Vec::with_capacity(sz) + } else { + Vec::new() + }; + + while let Some(value) = seq.next_element()? { + dash.push(value); + } + + Ok(Self::Value::from(Stroke { + color: default.color, + width: default.width, + pattern: style::LinePattern::Custom(dash), + opacity: default.opacity, + })) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + deserialize_map_fields!( + 'de, map, + "color" => color: Option, + "width" => width: Option, + "pattern" => pattern: Option, + "opacity" => opacity: Option, + ); + if let Some(default) = self.default_stroke { + Ok(Stroke { + color: color.unwrap_or(default.color), + width: width.unwrap_or(default.width), + pattern: pattern.unwrap_or(default.pattern), + opacity: opacity.or(default.opacity), + }) + } else { + Ok(Stroke { + color: color.ok_or_else(|| serde::de::Error::missing_field("color"))?, + width: width.unwrap_or_else(|| C::default_stroke_width()), + pattern: pattern.unwrap_or_default(), + opacity, + }) + } + } +} + +// MARK: Size and Padding + impl serde::Serialize for geom::Size { fn serialize(&self, serializer: S) -> Result where diff --git a/base/src/style.rs b/base/src/style.rs new file mode 100644 index 00000000..473ee6f2 --- /dev/null +++ b/base/src/style.rs @@ -0,0 +1,258 @@ +use crate::Rgba8; + +/// Trait for color types that have a default value for serialization purposes +pub trait DefaultColor: Color { + fn default_color() -> Option; +} + +/// Trait for types that have a default stroke for serialization purposes +/// The trait is implemented for color types, so that the default stroke width +/// can be associated with the color type used in the stroke. +pub trait DefaultStroke: Sized { + /// Return the default stroke for this color type. + fn default_stroke() -> Option>; +} + +/// Trait for types that have a default stroke width for serialization purposes +/// The trait is implemented for color types, so that the default stroke width +/// can be associated with the color type used in the stroke. +pub trait DefaultStrokeWidth { + /// Return the default stroke width for this color type. + fn default_stroke_width() -> f32; +} + +/// Trait that defines a context for resolving colors. +/// The context can be used to resolve colors based on themes, series, or other factors. +pub trait ResolveColor { + fn resolve_color(&self, color: &C) -> Rgba8; +} + +/// Trait for color types that can be resolved to a concrete color. +pub trait Color: Clone + Copy + From { + #[inline] + fn resolve(&self, rc: &R) -> Rgba8 + where + R: ResolveColor, + Self: Sized, + { + rc.resolve_color(self) + } +} + +impl Color for Rgba8 {} + +impl DefaultStroke for Rgba8 { + fn default_stroke() -> Option> { + None + } +} + +impl DefaultStrokeWidth for Rgba8 { + fn default_stroke_width() -> f32 { + 1.0 + } +} + +impl ResolveColor for () { + fn resolve_color(&self, color: &Rgba8) -> Rgba8 { + *color + } +} + +/// Dash pattern for dashed lines +/// A dash pattern is a sequence of lengths that specify the lengths of +/// alternating dashes and gaps. +/// +/// The lengths of dashes and gaps are relative to the line width. +/// So a pattern will scale with the line width and remain visually consistent. +#[derive(Debug, Clone, PartialEq, Default)] +pub enum LinePattern { + /// Solid line + #[default] + Solid, + /// Dashed line. Equivalent to Custom(vec![5.0, 5.0]) + Dashed, + /// Dotted line. Equivalent to Custom(vec![1.0, 1.0]) + Dot, + /// Dash-dot line. Equivalent to Custom(vec![5.0, 5.0, 1.0, 5.0]) + DashDot, + /// Custom dashed line. + /// The pattern is a sequence of lengths that specify the lengths of alternating dashes and gaps. + Custom(Vec), +} + +impl LinePattern { + const DASHED: &'static [f32] = &[5.0, 5.0]; + const DOTTED: &'static [f32] = &[1.0, 1.0]; + const DASH_DOT: &'static [f32] = &[5.0, 5.0, 1.0, 5.0]; + + pub fn get_dash(&self) -> Option<&[f32]> { + match self { + LinePattern::Solid => None, + LinePattern::Dashed => Some(Self::DASHED), + LinePattern::Dot => Some(Self::DOTTED), + LinePattern::DashDot => Some(Self::DASH_DOT), + LinePattern::Custom(pattern) => Some(pattern), + } + } +} + +/// Stroke style definition. Defines how lines are stroked. +/// +/// The color is a generic parameter to support different color resolution strategies, +/// such as fixed colors, theme-based colors, or series-based colors. +#[derive(Debug, Clone, PartialEq)] +pub struct Stroke { + /// Line color + pub color: C, + /// Line width in figure units + pub width: f32, + /// Line pattern + pub pattern: LinePattern, + /// Line opacity (0.0 to 1.0) + pub opacity: Option, +} + +impl Stroke { + /// Set the line width in figure units, returning self for chaining + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Set the line opacity (0.0 to 1.0), returning self for chaining + pub fn with_opacity(self, opacity: f32) -> Self { + Stroke { + opacity: Some(opacity), + ..self + } + } + + /// Set the line pattern, returning self for chaining + pub fn with_pattern(self, pattern: LinePattern) -> Self { + Stroke { pattern, ..self } + } +} + +impl Stroke { + pub fn solid(color: C) -> Self { + Stroke { + color, + width: C::default_stroke_width(), + pattern: LinePattern::Solid, + opacity: None, + } + } +} + +impl Default for Stroke +where + C: Color + Default + DefaultStrokeWidth, +{ + fn default() -> Self { + Stroke { + color: C::default(), + width: C::default_stroke_width(), + pattern: LinePattern::default(), + opacity: None, + } + } +} + +impl From for Stroke { + fn from(color: C) -> Self { + Stroke { + color, + width: C::default_stroke_width(), + pattern: LinePattern::default(), + opacity: None, + } + } +} + +impl From<(C, f32)> for Stroke { + fn from((color, width): (C, f32)) -> Self { + Stroke { + color, + width, + pattern: LinePattern::default(), + opacity: None, + } + } +} + +impl From<(C, f32, LinePattern)> for Stroke { + fn from((color, width, pattern): (C, f32, LinePattern)) -> Self { + Stroke { + color, + width, + pattern, + opacity: None, + } + } +} + +impl From<(C, f32, Vec)> for Stroke { + fn from((color, width, dash): (C, f32, Vec)) -> Self { + Stroke { + color, + width, + pattern: LinePattern::Custom(dash), + opacity: None, + } + } +} + +/// Fill style definition +/// The color is a generic parameter to support different color resolution strategies, +/// such as fixed colors, theme based colors, or series-based colors. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Fill { + /// Solid fill + Solid { + /// Fill color + color: C, + /// Fill opacity (0.0 to 1.0) + opacity: Option, + }, +} + +impl Default for Fill +where + C: Color + Default, +{ + fn default() -> Self { + Fill::Solid { + color: C::default(), + opacity: None, + } + } +} + +impl From for Fill { + fn from(color: C) -> Self { + Fill::Solid { + color, + opacity: None, + } + } +} + +impl Fill { + /// Create a solid and opaque fill + pub fn solid(color: C) -> Self { + Fill::Solid { + color, + opacity: None, + } + } + + /// Set the fill opacity (0.0 to 1.0), returning self for chaining + pub fn with_opacity(self, opacity: f32) -> Self { + match self { + Fill::Solid { color, .. } => Fill::Solid { + color, + opacity: Some(opacity), + }, + } + } +} diff --git a/examples/bode_rlc.rs b/examples/bode_rlc.rs index 1d9badc0..760be278 100644 --- a/examples/bode_rlc.rs +++ b/examples/bode_rlc.rs @@ -114,9 +114,9 @@ fn main() { let mag_2_decades = rlc_freq_response(cutoff * 100.0, 1.0, L, C).0; let cutoff_line = - des::annot::Line::vertical(cutoff).with_pattern(style::Dash::default().into()); + des::annot::Line::vertical(cutoff).with_pattern(style::LinePattern::Dashed.into()); let slope_line = des::annot::Line::two_points(cutoff, 0.0, 100.0 * cutoff, mag_2_decades) - .with_pattern(style::Dash::default().into()); + .with_pattern(style::LinePattern::Dashed.into()); let cut_off_label = des::annot::Label::new(format!("{:.2} kHz", cutoff / 1000.0).into(), cutoff, -60.0) .with_anchor(des::annot::Anchor::BottomLeft) diff --git a/iced/src/figure.rs b/iced/src/figure.rs index 4b10a200..868c218b 100644 --- a/iced/src/figure.rs +++ b/iced/src/figure.rs @@ -3,7 +3,7 @@ use iced::advanced::widget::tree; use iced::advanced::{Layout, Widget, layout, mouse, renderer, widget}; use iced::{Element, Length, Rectangle, Size}; use plotive::render::Surface; -use plotive::style::theme; +use plotive::style::{AsStroke, theme}; use plotive::{drawing, geom, style}; use crate::surface; @@ -323,7 +323,7 @@ where if let Some((start, end)) = self.zoom_rect { let rect = geom::Rect::from_corners(start, end); let stroke = theme::Stroke::from(theme::Col::Foreground) - .with_pattern(style::Dash::default().into()); + .with_pattern(style::LinePattern::Dashed); let stroke = stroke.as_stroke(&style); let _ = surface.draw_rect(&plotive::render::Rect { rect, diff --git a/src/des.rs b/src/des.rs index 5b4f196b..794db790 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::RichProps)>, + classes: Vec<(String, text::TextModifiers)>, }, } impl Text { pub(crate) fn to_rich_text( &self, - base: text::rich::TextProps, + base: text::props::TextProps, layout: text::rich::Layout, db: &text::fontdb::Database, ) -> std::result::Result, text::Error> { @@ -107,8 +107,8 @@ impl From<(&str,)> for Text { } } -impl From<(String, Vec<(String, text::RichProps)>)> for Text { - fn from(tuple: (String, Vec<(String, text::RichProps)>)) -> Self { +impl From<(String, Vec<(String, text::TextModifiers)>)> for Text { + fn from(tuple: (String, Vec<(String, text::TextModifiers)>)) -> Self { Text::RichWithClasses { fmt: tuple.0, classes: tuple.1, diff --git a/src/des/axis.rs b/src/des/axis.rs index 6546355e..b8379cb9 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -382,7 +382,7 @@ impl Scale { /// Describe the ticks of an axis pub mod ticks { - use crate::style::{self, Dash, theme}; + use crate::style::{self, theme}; use crate::text; /// Describes how to locate the ticks of an axis @@ -707,7 +707,7 @@ pub mod ticks { pub struct Ticks { locator: Locator, formatter: Option, - font: text::LineProps, + txt_modifiers: text::TextModifiers, color: theme::Color, } @@ -720,7 +720,7 @@ pub mod ticks { Ticks { locator: Locator::default(), formatter: Some(Formatter::default()), - font: text::LineProps::default(), + txt_modifiers: text::TextModifiers::default(), color: theme::Col::Foreground.into(), } } @@ -741,9 +741,12 @@ pub mod ticks { pub fn with_formatter(self, formatter: Option) -> Self { Self { formatter, ..self } } - /// Returns a new ticks with the specified font - pub fn with_font(self, font: text::LineProps) -> Self { - Self { font, ..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 color pub fn with_color(self, color: theme::Color) -> Self { @@ -759,9 +762,9 @@ pub mod ticks { pub fn formatter(&self) -> Option<&Formatter> { self.formatter.as_ref() } - /// Font properties for the ticks labels - pub fn font(&self) -> &text::LineProps { - &self.font + /// Text properties for the ticks labels + pub fn font(&self) -> &text::TextModifiers { + &self.txt_modifiers } /// Color for the ticks. /// Will be used for the labels as well unless a specific color is set in [`font`](Self::font). @@ -788,7 +791,7 @@ pub mod ticks { MinorGrid(theme::Stroke { width: 0.5, color: theme::Col::Grid.into(), - pattern: style::LinePattern::Dash(Dash::default()), + pattern: style::LinePattern::Dashed, opacity: Some(0.6), }) } diff --git a/src/des/colorbar.rs b/src/des/colorbar.rs index 8bdcc63d..561bbdf3 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::LineProps, + ticks_font: text::TextModifiers, 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::LineProps::default(), + ticks_font: text::TextModifiers::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::LineProps) -> Self { + pub fn with_ticks_font(mut self, ticks_font: text::TextModifiers) -> Self { self.ticks_font = ticks_font; self } @@ -110,7 +110,7 @@ impl ColorBar { } /// Get the ticks font properties - pub fn ticks_font(&self) -> &text::LineProps { + pub fn ticks_font(&self) -> &text::TextModifiers { &self.ticks_font } diff --git a/src/des/legend.rs b/src/des/legend.rs index b3e3e859..577b0847 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::LineProps, + font: text::TextModifiers, fill: Option, border: Option, columns: Option, @@ -30,7 +30,7 @@ impl Default for Legend { fn default() -> Self { Self { pos: Pos::default(), - font: text::LineProps::default(), + font: text::TextModifiers::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::LineProps { + pub fn font(&self) -> &text::TextModifiers { &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::LineProps) -> Self { + pub fn with_font(self, font: text::TextModifiers) -> Self { Self { font, ..self } } diff --git a/src/des/sd.rs b/src/des/sd.rs index 122b14d9..f9b4ed84 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,6 +1,6 @@ //! Serialization and deserialization of figures -use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct}; +use serde::ser::{SerializeSeq, SerializeStruct}; use super::Figure; use crate::des::{FigLegend, Plot, Subplots, Text, figure}; @@ -22,140 +22,6 @@ use crate::text; // MARK: Text -impl serde::Serialize for text::LineProps { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_map(None)?; - if let Some(families) = self.family.as_ref() { - let family = text::font::font_families_to_string(families); - state.serialize_entry("family", &family)?; - } - if let Some(size) = self.size { - state.serialize_entry("size", &size)?; - } - if let Some(weight) = self.weight.as_ref() { - state.serialize_entry("weight", &weight)?; - } - if let Some(width) = self.width.as_ref() { - state.serialize_entry("width", &width)?; - } - if let Some(style) = self.style.as_ref() { - state.serialize_entry("style", &style)?; - } - if let Some(color) = self.color.as_ref() { - state.serialize_entry("color", &color)?; - } - state.end() - } -} - -impl<'de> serde::Deserialize<'de> for text::LineProps { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = text::LineProps; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a map representing LineProps") - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut family = None; - let mut size = None; - let mut weight = None; - let mut width = None; - let mut style = None; - let mut color = None; - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "family" => { - if family.is_some() { - return Err(serde::de::Error::duplicate_field("family")); - } - let family_str: String = map.next_value()?; - family = - Some(text::parse_font_families(&family_str).map_err(|err| { - serde::de::Error::custom(format!( - "failed to parse font families '{}': {}", - family_str, err - )) - })?); - } - "size" => { - if size.is_some() { - return Err(serde::de::Error::duplicate_field("size")); - } - size = Some(map.next_value()?); - } - "weight" => { - if weight.is_some() { - return Err(serde::de::Error::duplicate_field("weight")); - } - weight = Some(map.next_value()?); - } - "width" => { - if width.is_some() { - return Err(serde::de::Error::duplicate_field("width")); - } - width = Some(map.next_value()?); - } - "style" => { - if style.is_some() { - return Err(serde::de::Error::duplicate_field("style")); - } - style = Some(map.next_value()?); - } - "color" => { - if color.is_some() { - return Err(serde::de::Error::duplicate_field("color")); - } - color = Some(map.next_value()?); - } - _ => { - return Err(serde::de::Error::unknown_field( - &key, - &["family", "size", "weight", "width", "style", "color"], - )); - } - } - } - - Ok(text::LineProps { - family, - size, - weight, - width, - style, - color, - }) - } - } - - deserializer.deserialize_map(Visitor) - } -} - -struct RichPropsMap(Vec<(String, text::RichProps)>); - -impl<'de> serde::Deserialize<'de> for RichPropsMap { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let map = std::collections::HashMap::::deserialize(deserializer)?; - Ok(RichPropsMap(map.into_iter().collect())) - } -} - impl serde::Serialize for Text { fn serialize(&self, serializer: S) -> Result where @@ -178,6 +44,21 @@ impl serde::Serialize for Text { } } +struct RichPropsMap(Vec<(String, text::TextModifiers)>); + +impl<'de> serde::Deserialize<'de> for RichPropsMap { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map = + std::collections::HashMap::>::deserialize( + deserializer, + )?; + Ok(RichPropsMap(map.into_iter().collect())) + } +} + impl<'de> serde::Deserialize<'de> for Text { fn deserialize(deserializer: D) -> Result where diff --git a/src/des/sd/axis.rs b/src/des/sd/axis.rs index 32fdc275..8b38b0b1 100644 --- a/src/des/sd/axis.rs +++ b/src/des/sd/axis.rs @@ -1151,7 +1151,7 @@ impl serde::Serialize for axis::Grid { S: serde::Serializer, { let default_stroke = Some(axis::Grid::default().0); - sd::style::serialize_stroke(&self.0, default_stroke, "Grid", serializer) + plotive_base::sd::serialize_stroke(&self.0, default_stroke, "Grid", serializer) } } @@ -1161,7 +1161,7 @@ impl<'de> serde::Deserialize<'de> for axis::Grid { D: serde::Deserializer<'de>, { let default_stroke = Some(axis::Grid::default().0); - let visitor = sd::style::StrokeVisitor::new("Grid", default_stroke); + let visitor = plotive_base::sd::StrokeVisitor::new("Grid", default_stroke); let stroke = deserializer.deserialize_any(visitor)?; Ok(axis::Grid(stroke)) } @@ -1173,7 +1173,7 @@ impl serde::Serialize for axis::MinorGrid { S: serde::Serializer, { let default_stroke = Some(axis::MinorGrid::default().0); - sd::style::serialize_stroke(&self.0, default_stroke, "MinorGrid", serializer) + plotive_base::sd::serialize_stroke(&self.0, default_stroke, "MinorGrid", serializer) } } @@ -1183,7 +1183,7 @@ impl<'de> serde::Deserialize<'de> for axis::MinorGrid { D: serde::Deserializer<'de>, { let default_stroke = Some(axis::MinorGrid::default().0); - let visitor = sd::style::StrokeVisitor::new("MinorGrid", default_stroke); + let visitor = plotive_base::sd::StrokeVisitor::new("MinorGrid", default_stroke); let stroke = deserializer.deserialize_any(visitor)?; Ok(axis::MinorGrid(stroke)) } diff --git a/src/des/sd/colorbar.rs b/src/des/sd/colorbar.rs index e3121153..f9d847b6 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 51da2336..537a4e0a 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::LineProps::default(); + let font_default = self.font() == &text::TextModifiers::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/des/sd/style.rs b/src/des/sd/style.rs index aaceb1b3..52142dc5 100644 --- a/src/des/sd/style.rs +++ b/src/des/sd/style.rs @@ -5,40 +5,7 @@ use std::str::FromStr; use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize}; -use crate::Color; -use crate::style::{self, series, theme}; - -trait DefaultColor: Color { - fn default_color() -> Option; -} - -trait DefaultStroke: Color { - fn default_stroke() -> Option>; -} - -impl DefaultColor for theme::Color { - fn default_color() -> Option { - None - } -} - -impl DefaultStroke for theme::Color { - fn default_stroke() -> Option> { - None - } -} - -impl DefaultColor for series::Color { - fn default_color() -> Option { - Some(series::Color::Auto) - } -} - -impl DefaultStroke for series::Color { - fn default_stroke() -> Option> { - Some(style::Stroke::default()) - } -} +use crate::style::{self, theme}; // MARK: style::theme::Color @@ -150,484 +117,18 @@ impl<'de> serde::de::Visitor<'de> for SeriesColorVisitor { } } -///////////////////////// -// MARK: style::Fill -///////////////////////// - -impl Serialize for style::Fill -where - C: Serialize + Color, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let style::Fill::Solid { color, opacity } = self; - - if let Some(opacity) = opacity { - if *opacity == 1.0 { - color.serialize(serializer) - } else { - let mut state = serializer.serialize_struct("Fill", 2)?; - state.serialize_field("color", color)?; - state.serialize_field("opacity", opacity)?; - state.end() - } - } else { - color.serialize(serializer) - } - } -} - -impl<'de, C> Deserialize<'de> for style::Fill -where - C: Deserialize<'de> + Color + FromStr + DefaultColor, - ::Err: std::fmt::Display, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(FillVisitor { - _phantom: PhantomData, - }) - } -} - -struct FillVisitor { - _phantom: PhantomData, -} - -impl<'de, C> serde::de::Visitor<'de> for FillVisitor -where - C: Deserialize<'de> + Color + FromStr + DefaultColor, - ::Err: std::fmt::Display, -{ - type Value = style::Fill; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a fill color or a map with color and opacity") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - let color = value.parse::().map_err(E::custom)?; - Ok(style::Fill::Solid { - color, - opacity: None, - }) - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - super::deserialize_map_fields!( - 'de, map, - "color" => color: Option, - "opacity" => opacity: Option, - ); - - let color = match (color, C::default_color()) { - (Some(color), _) => color, - (None, Some(color)) => color, - (None, None) => return Err(serde::de::Error::missing_field("color")), - }; - - Ok(style::Fill::Solid { color, opacity }) - } -} - -//////////////////////////////// -// MARK: style::LinePattern -//////////////////////////////// - -impl Serialize for style::LinePattern { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use style::LinePattern; - match self { - LinePattern::Solid => "solid".serialize(serializer), - LinePattern::Dashed => "dashed".serialize(serializer), - LinePattern::Dot => "dotted".serialize(serializer), - LinePattern::DashDot => "dash-dot".serialize(serializer), - LinePattern::Dash(dash) => dash.0.serialize(serializer), - } - } -} - -impl<'de> Deserialize<'de> for style::LinePattern { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(LinePatternVisitor) - } -} - -struct LinePatternVisitor; - -fn str_to_line_pattern(value: &str) -> Option { - match value { - "solid" => Some(style::LinePattern::Solid), - "dashed" => Some(style::LinePattern::Dashed), - "dotted" => Some(style::LinePattern::Dot), - "dash-dot" => Some(style::LinePattern::DashDot), - _ => None, - } -} - -impl<'de> serde::de::Visitor<'de> for LinePatternVisitor { - type Value = style::LinePattern; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a line pattern string or a dash array") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - str_to_line_pattern(value) - .ok_or_else(|| E::unknown_variant(value, &["solid", "dashed", "dotted", "dash-dot"])) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut dash: Vec = if let Some(sz) = seq.size_hint() { - Vec::with_capacity(sz) - } else { - Vec::new() - }; - - while let Some(value) = seq.next_element()? { - dash.push(value); - } - Ok(style::LinePattern::Dash(style::Dash(dash))) - } -} - -/////////////////////////// -// MARK: style::Stroke -/////////////////////////// - -impl Serialize for style::Stroke -where - C: Serialize + Color + DefaultStroke + style::DefaultStrokeWidth + PartialEq, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serialize_stroke(self, C::default_stroke(), "Stroke", serializer) - } -} - -impl<'de, C> Deserialize<'de> for style::Stroke -where - C: Deserialize<'de> + Color + DefaultStroke + style::DefaultStrokeWidth + FromStr, - ::Err: std::fmt::Display, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(StrokeVisitor::new("Stroke", C::default_stroke())) - } -} - -pub fn serialize_stroke( - stroke: &style::Stroke, - default_stroke: Option>, - name: &'static str, - serializer: S, -) -> Result -where - C: Serialize + Color + style::DefaultStrokeWidth + PartialEq, - S: serde::Serializer, -{ - let default_width = C::default_stroke_width(); - - let (has_default_color, has_default_width, has_default_pattern, has_default_opacity) = - if let Some(default) = default_stroke { - ( - default.color == stroke.color, - default.width == stroke.width, - default.pattern == stroke.pattern, - default.opacity == stroke.opacity, - ) - } else { - ( - false, - stroke.width == default_width, - stroke.pattern == Default::default(), - stroke.opacity.unwrap_or(1.0) == 1.0, - ) - }; - - match ( - has_default_color, - has_default_width, - has_default_pattern, - has_default_opacity, - ) { - (true, true, true, true) => "auto".serialize(serializer), - (false, true, true, true) => stroke.color.serialize(serializer), - (true, false, true, true) => stroke.width.serialize(serializer), - (true, true, false, true) => stroke.pattern.serialize(serializer), - _ => { - let fields = (!has_default_color as usize) - + (!has_default_width as usize) - + (!has_default_pattern as usize) - + (!has_default_opacity as usize); - let mut state = serializer.serialize_struct(name, fields)?; - if !has_default_color { - state.serialize_field("color", &stroke.color)?; - } - if !has_default_width { - state.serialize_field("width", &stroke.width)?; - } - if !has_default_pattern { - state.serialize_field("pattern", &stroke.pattern)?; - } - if !has_default_opacity { - state.serialize_field("opacity", &stroke.opacity.unwrap_or(1.0))?; - } - state.end() - } - } -} - -pub struct StrokeVisitor -where - C: Color, -{ - name: &'static str, - default_stroke: Option>, -} - -impl StrokeVisitor -where - C: Color, -{ - pub fn new(name: &'static str, default_stroke: Option>) -> Self { - Self { - name, - default_stroke, - } - } - - fn accepts_compact_pattern(&self) -> bool { - self.default_stroke.is_some() - } - - fn accepted_string_forms(&self) -> &'static str { - if self.accepts_compact_pattern() { - "'auto', a line pattern, or a color string" - } else { - "a color string" - } - } - - fn expecting_description(&self) -> &'static str { - if self.accepts_compact_pattern() { - "a stroke object, 'auto', a stroke width, a line pattern, a dash array, or a color string" - } else { - "a stroke object or a color string" - } - } - - fn invalid_string_message(&self) -> String { - format!( - "Invalid string value for {}: expected {}", - self.name, - self.accepted_string_forms() - ) - } - - fn no_default_numeric_message(&self) -> String { - format!( - "Numeric value is not valid for {} because there is no default stroke defined", - self.name - ) - } - - fn no_default_dash_array_message(&self) -> String { - format!( - "Dash array is not valid for {} because there is no default stroke defined", - self.name - ) - } -} - -impl<'de, C> serde::de::Visitor<'de> for StrokeVisitor -where - C: Deserialize<'de> + Color + DefaultStroke + style::DefaultStrokeWidth + FromStr, - ::Err: std::fmt::Display, -{ - type Value = style::Stroke; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str(self.expecting_description()) - } - - fn visit_i64(self, value: i64) -> Result - where - E: serde::de::Error, - { - self.visit_f64(value as f64) - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - self.visit_f64(value as f64) - } - - fn visit_f64(self, value: f64) -> Result - where - E: serde::de::Error, - { - let Some(default) = self.default_stroke else { - return Err(serde::de::Error::custom(self.no_default_numeric_message())); - }; - - if value <= 0.0 { - return Err(serde::de::Error::custom(format!( - "Invalid stroke width for {}: width cannot be null or negative", - self.name - ))); - } - - let width = value as f32; - Ok(style::Stroke { - color: default.color, - width, - pattern: default.pattern, - opacity: default.opacity, - }) - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - let invalid_string_message = self.invalid_string_message(); - - if value == "auto" { - if let Some(default) = self.default_stroke { - Ok(default) - } else { - Err(serde::de::Error::custom(format!( - "'auto' is not a valid value for {} because there is no default stroke defined", - self.name - ))) - } - } else if let Some(default) = self.default_stroke { - if let Some(pattern) = str_to_line_pattern(value) { - return Ok(Self::Value::from(style::Stroke { - color: default.color, - width: default.width, - pattern, - opacity: default.opacity, - })); - } - - let color = value - .parse() - .map_err(|_| serde::de::Error::custom(invalid_string_message))?; - - Ok(Self::Value::from(style::Stroke { - color, - width: default.width, - pattern: default.pattern, - opacity: default.opacity, - })) - } else { - let color = value - .parse() - .map_err(|_| serde::de::Error::custom(invalid_string_message))?; - - Ok(Self::Value::from(style::Stroke { - color, - width: C::default_stroke_width(), - pattern: Default::default(), - opacity: None, - })) - } - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let no_default_dash_array_message = self.no_default_dash_array_message(); - - let Some(default) = self.default_stroke else { - return Err(serde::de::Error::custom(no_default_dash_array_message)); - }; - - let mut dash: Vec = if let Some(sz) = seq.size_hint() { - Vec::with_capacity(sz) - } else { - Vec::new() - }; - - while let Some(value) = seq.next_element()? { - dash.push(value); - } - - Ok(Self::Value::from(style::Stroke { - color: default.color, - width: default.width, - pattern: style::LinePattern::Dash(style::Dash(dash)), - opacity: default.opacity, - })) - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - super::deserialize_map_fields!( - 'de, map, - "color" => color: Option, - "width" => width: Option, - "pattern" => pattern: Option, - "opacity" => opacity: Option, - ); - if let Some(default) = self.default_stroke { - Ok(style::Stroke { - color: color.unwrap_or(default.color), - width: width.unwrap_or(default.width), - pattern: pattern.unwrap_or(default.pattern), - opacity: opacity.or(default.opacity), - }) - } else { - Ok(style::Stroke { - color: color.ok_or_else(|| serde::de::Error::missing_field("color"))?, - width: width.unwrap_or_else(|| C::default_stroke_width()), - pattern: pattern.unwrap_or_default(), - opacity, - }) - } - } -} - /////////////////////////// // MARK: style::Marker /////////////////////////// impl Serialize for style::Marker where - C: Serialize + Color + DefaultColor + DefaultStroke + style::DefaultStrokeWidth + PartialEq, + C: Serialize + + style::Color + + style::DefaultColor + + style::DefaultStroke + + style::DefaultStrokeWidth + + PartialEq, { fn serialize(&self, serializer: S) -> Result where @@ -687,9 +188,9 @@ where impl<'de, C> Deserialize<'de> for style::Marker where C: Deserialize<'de> - + Color - + DefaultColor - + DefaultStroke + + style::Color + + style::DefaultColor + + style::DefaultStroke + style::DefaultStrokeWidth + FromStr, ::Err: std::fmt::Display, @@ -711,9 +212,9 @@ struct MarkerVisitor { impl<'de, C> serde::de::Visitor<'de> for MarkerVisitor where C: Deserialize<'de> - + Color - + DefaultColor - + DefaultStroke + + style::Color + + style::DefaultColor + + style::DefaultStroke + style::DefaultStrokeWidth + FromStr, ::Err: std::fmt::Display, @@ -998,7 +499,7 @@ mod tests { assert_eq!( stroke, style::series::Stroke::default() - .with_pattern(style::LinePattern::Dash(style::Dash(vec![2.0, 3.0]))), + .with_pattern(style::LinePattern::Custom(vec![2.0, 3.0])), ); } diff --git a/src/drawing.rs b/src/drawing.rs index 9fd04c19..50693f42 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -27,6 +27,8 @@ pub mod zoom; pub use figure::PreparedFigure; pub use hit_test::PlotHit; +use crate::style::{AsPaint, AsStroke}; + /// Errors that can occur during figure drawing #[derive(Debug)] pub enum Error { @@ -239,18 +241,21 @@ struct TextSpan { stroke: Option, } -fn resolve_line_font(props: &text::LineProps, default: text::Font) -> text::Font { +fn resolve_line_font( + modifiers: &text::TextModifiers, + default: text::Font, +) -> text::Font { let mut res = default; - if let Some(families) = props.family.as_ref() { + if let Some(families) = modifiers.families.as_ref() { res = res.with_families(families.clone()); } - if let Some(style) = props.style { + if let Some(style) = modifiers.style { res = res.with_style(style); } - if let Some(weight) = props.weight { + if let Some(weight) = modifiers.weight { res = res.with_weight(weight); } - if let Some(width) = props.width { + if let Some(width) = modifiers.width { res = res.with_width(width); } res @@ -260,13 +265,13 @@ impl Text { fn from_line_text( text: &text::LineText, fontdb: &fontdb::Database, - color: theme::Color, + fill: theme::Fill, ) -> Result { let mut spans = Vec::new(); - text::line::render_line_text_with(text, fontdb, |path| { + text::line::render_line_text_with(text, fontdb, Default::default(), |path| { spans.push(TextSpan { path: path.clone(), - fill: Some(color.into()), + fill: Some(fill.clone()), stroke: None, }); }); @@ -286,20 +291,15 @@ impl Text { text::RichPrimitive::Fill(path, color) => { spans.push(TextSpan { path: path.clone(), - fill: Some(color.into()), + fill: Some(color.clone()), stroke: None, }); } - text::RichPrimitive::Stroke(path, color, thickness) => { + text::RichPrimitive::Stroke(path, stroke) => { spans.push(TextSpan { path: path.clone(), fill: None, - stroke: Some(theme::Stroke { - color: color.into(), - width: thickness, - opacity: None, - pattern: Default::default(), - }), + stroke: Some(stroke.clone()), }); } })?; diff --git a/src/drawing/annot.rs b/src/drawing/annot.rs index 6aa9012f..a78ba124 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, defaults, theme}; +use crate::style::{self, AsPaint, AsStroke, defaults, theme}; use crate::{Style, data, geom, render, text}; #[derive(Debug, Clone)] @@ -53,8 +53,10 @@ where Anchor::Center => (text::rich::Align::Center, text::rich::VerAlign::Center), }; let text = label.text().to_rich_text( - text::rich::TextProps::::new(12.0) - .with_font(defaults::FONT_FAMILY.parse().unwrap()), + text::props::TextProps::::new( + defaults::FONT_FAMILY.parse().unwrap(), + 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 95fc5c6c..8bced420 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -9,11 +9,12 @@ 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}; use crate::drawing::{Categories, Ctx, Error, Text, ticks}; -use crate::style::{defaults, theme}; +use crate::style::{AsStroke, defaults, theme}; use crate::text::{self, font}; use crate::{Style, data, des, geom, missing_params, render}; @@ -349,8 +350,10 @@ where .title() .map(|title| { title.to_rich_text( - text::rich::TextProps::new(defaults::AXIS_TITLE_FONT_SIZE) - .with_font(defaults::FONT_FAMILY.parse().unwrap()), + text::props::TextProps::new( + defaults::FONT_FAMILY.parse().unwrap(), + defaults::AXIS_TITLE_FONT_SIZE, + ), side.title_layout(), self.fontdb(), ) @@ -525,7 +528,12 @@ where .font() .size .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); - let color = font_props.color.unwrap_or(major_ticks.color()); + let color = font_props + .color + .clone() + .flatten() + .unwrap_or(major_ticks.color().into()); + let theme::Fill::Solid { color, .. } = color; Ok((font, font_size, color)) } @@ -551,8 +559,13 @@ 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, font_size, font.clone(), db)?; - let lbl = Text::from_line_text(&lbl, db, lbl_color)?; + let lbl = text::LineText::new( + text, + ticks_align, + FontProps::new(font.clone(), font_size), + db, + )?; + let lbl = Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))?; ticks.push(NumTick { loc, lbl }); } @@ -561,9 +574,16 @@ where } else { lbl_formatter .axis_annotation() - .map(|l| text::LineText::new(l.to_string(), annot_align, font_size, font, db)) + .map(|l| { + text::LineText::new( + l.to_string(), + annot_align, + FontProps::new(font, font_size), + db, + ) + }) .transpose()? - .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) + .map(|lbl| Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))) .transpose()? }; @@ -626,8 +646,13 @@ 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, font_size, font.clone(), db)?; - let lbl = Text::from_line_text(&lbl, db, lbl_color)?; + let lbl = text::LineText::new( + text, + ticks_align, + FontProps::new(font.clone(), font_size), + db, + )?; + let lbl = Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))?; ticks.push(NumTick { loc: loc.timestamp(), lbl, @@ -636,9 +661,16 @@ where let annot = lbl_formatter .axis_annotation() - .map(|l| text::LineText::new(l.to_string(), annot_align, font_size, font, db)) + .map(|l| { + text::LineText::new( + l.to_string(), + annot_align, + FontProps::new(font, font_size), + db, + ) + }) .transpose()? - .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) + .map(|lbl| Text::from_line_text(&lbl, db, theme::Fill::solid(lbl_color))) .transpose()?; Ok(NumTicks { @@ -662,9 +694,13 @@ where let mut lbls = Vec::with_capacity(cb.len()); for cat in cb.iter() { - let lbl = - text::LineText::new(cat.to_string(), ticks_align, font_size, font.clone(), db)?; - let lbl = Text::from_line_text(&lbl, db, lbl_color)?; + let lbl = text::LineText::new( + cat.to_string(), + ticks_align, + FontProps::new(font.clone(), font_size), + 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 b681d5dd..6f2b2695 100644 --- a/src/drawing/colorbar.rs +++ b/src/drawing/colorbar.rs @@ -2,13 +2,14 @@ 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}; use crate::drawing::cmap::{AsColorMap, ColorMap}; use crate::drawing::scale::CoordMap; use crate::drawing::{Ctx, Text, axis, ticks}; -use crate::style::{defaults, theme}; +use crate::style::{AsStroke, defaults, theme}; use crate::{Style, data, geom, missing_params, render, text}; /// A colorbar entry, used to populate one colorbar @@ -101,8 +102,10 @@ impl ColorBarBuilder { .title() .map(|title| { title.to_rich_text( - text::rich::TextProps::new(defaults::COLORBAR_TITLE_FONT_SIZE) - .with_font(defaults::FONT_FAMILY.parse().unwrap()), + text::props::TextProps::new( + defaults::FONT_FAMILY.parse().unwrap(), + defaults::COLORBAR_TITLE_FONT_SIZE, + ), side.title_layout(), ctx.fontdb(), ) @@ -117,7 +120,11 @@ impl ColorBarBuilder { let font_size = font_props .size .unwrap_or(defaults::COLORBAR_TICKS_FONT_SIZE); - let color = font_props.color.unwrap_or(theme::Col::Foreground.into()); + let color = font_props + .color + .clone() + .flatten() + .unwrap_or(theme::Col::Foreground.into()); let formatter = des::axis::ticks::Formatter::Auto; let ticks = ticks::locate_num(&self.locator, view_bounds, &self.scale)?; @@ -128,7 +135,12 @@ 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, font_size, font.clone(), ctx.fontdb())?; + let lt = text::LineText::new( + text, + align, + FontProps::new(font.clone(), font_size), + 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 d6c6dc95..ea518f8e 100644 --- a/src/drawing/figure.rs +++ b/src/drawing/figure.rs @@ -1,6 +1,6 @@ use crate::drawing::legend::{self, LegendBuilder}; use crate::drawing::{Ctx, Error, plot}; -use crate::style::{defaults, theme}; +use crate::style::{AsPaint, defaults, theme}; use crate::{Style, data, des, geom, missing_params, render, text}; /// A figure that has been prepared for drawing. See the [`Prepare`](crate::drawing::Prepare) trait. @@ -83,8 +83,10 @@ where text::line::VerAlign::Hanging.into(), Default::default(), ); - let base = text::rich::TextProps::new(defaults::TITLE_FONT_SIZE) - .with_font(defaults::FONT_FAMILY.parse().unwrap()); + let base = text::props::TextProps::new( + defaults::FONT_FAMILY.parse().unwrap(), + 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 3114c0a9..3a9a3948 100644 --- a/src/drawing/legend.rs +++ b/src/drawing/legend.rs @@ -1,6 +1,8 @@ +use plotive_text::props::FontProps; + use crate::drawing::Text; use crate::geom::{Padding, Size}; -use crate::style::{defaults, theme}; +use crate::style::{AsPaint, AsStroke, defaults, theme}; use crate::text::{self, LineText, fontdb}; use crate::{Style, des, drawing, geom, render, style}; @@ -54,7 +56,7 @@ impl ShapeRef<'_> { #[derive(Debug, Clone)] pub struct Entry<'a> { pub label: &'a str, - pub font: Option<&'a text::LineProps>, + pub txt_modifiers: Option<&'a text::TextModifiers>, pub shape: ShapeRef<'a>, } @@ -80,7 +82,7 @@ impl LegendEntry { #[derive(Debug)] pub struct LegendBuilder<'a> { - font: text::LineProps, + txt_modifiers: text::TextModifiers, fill: Option, border: Option, columns: Option, @@ -113,7 +115,7 @@ impl<'a> LegendBuilder<'a> { columns.replace(1); } LegendBuilder { - font: legend.font().clone(), + txt_modifiers: legend.font().clone(), fill: legend.fill().cloned(), border: legend.border().cloned(), columns, @@ -128,10 +130,14 @@ 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.font.unwrap_or(&self.font); + 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 color = font_props.color.unwrap_or(theme::Col::Foreground.into()); + let fill = font_props + .color + .clone() + .flatten() + .unwrap_or(theme::Fill::solid(theme::Col::Foreground.into())); let align = ( text::line::Align::Start, @@ -140,11 +146,10 @@ impl<'a> LegendBuilder<'a> { let text = LineText::new( entry.label.to_string(), align, - font_size, - font, + FontProps::new(font, font_size), &self.fontdb, )?; - let text = Text::from_line_text(&text, &self.fontdb, color)?; + let text = Text::from_line_text(&text, &self.fontdb, fill)?; self.entries.push(LegendEntry { index, shape, diff --git a/src/drawing/plot.rs b/src/drawing/plot.rs index 9b379059..069c1c0a 100644 --- a/src/drawing/plot.rs +++ b/src/drawing/plot.rs @@ -13,7 +13,7 @@ use crate::drawing::legend::{Legend, LegendBuilder}; use crate::drawing::scale::CoordMapXy; use crate::drawing::series::{self, Series, SeriesExt}; use crate::drawing::{ColumnExt, Ctx, Error, get_column}; -use crate::style::{defaults, theme}; +use crate::style::{AsPaint, AsStroke, defaults, theme}; use crate::{Style, data, des, geom, missing_params, render}; #[derive(Debug, Clone)] diff --git a/src/drawing/series.rs b/src/drawing/series.rs index c510bd65..ca52d498 100644 --- a/src/drawing/series.rs +++ b/src/drawing/series.rs @@ -10,6 +10,7 @@ use crate::drawing::{ Categories, ColumnExt, Error, F64ColumnExt, axis, colorbar, get_column, legend, marker, plot_to_fig, scale, }; +use crate::style::{AsPaint, AsStroke}; use crate::{Style, data, des, geom, render, style}; /// trait implemented by series, or any other item that @@ -25,7 +26,7 @@ impl SeriesExt for des::series::Line { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::Line(self.stroke()), }) } @@ -35,7 +36,7 @@ impl SeriesExt for des::series::Scatter { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::Marker(self.marker()), }) } @@ -50,7 +51,7 @@ impl SeriesExt for des::series::Area { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::AreaRect { fill: Some(self.fill()), y1_stroke: self.y1_stroke(), @@ -64,7 +65,7 @@ impl SeriesExt for des::series::Histogram { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), }) } @@ -74,7 +75,7 @@ impl SeriesExt for des::series::Bars { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), }) } @@ -84,7 +85,7 @@ impl SeriesExt for des::series::BarSeries { fn legend_entry(&self) -> Option> { self.name().map(|n| legend::Entry { label: n.as_ref(), - font: None, + txt_modifiers: None, shape: legend::ShapeRef::Rect(Some(self.fill()), self.outline()), }) } diff --git a/src/lib.rs b/src/lib.rs index 319bef6a..f54cb288 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,7 +164,7 @@ pub mod color { pub use plotive_base::color::*; } -pub use color::{Color, ResolveColor, Rgb8, Rgba8}; +pub use color::{Rgb8, Rgba8}; /// Rexports of [`plotive_base::geom`]` items pub mod geom { @@ -174,27 +174,6 @@ pub mod geom { /// Rexports of [`plotive_text`]` items pub mod text { pub use plotive_text::*; - /// Class properties for rich text, with `plotive` theme colors - pub type RichProps = plotive_text::rich::ClassProps; - - /// Text properties for line text, with `plotive` theme colors - /// Use this to provide customization of text properties for line text, such as font size, font family and color. - /// All properties are optional, and if not provided, the default values will be used depending on the context. - #[derive(Debug, Clone, PartialEq, Default)] - pub struct LineProps { - /// The font family name - pub family: Option>, - /// The font weight - pub weight: Option, - /// The font width (or stretch) - pub width: Option, - /// The font style (normal, italic, oblique) - pub style: Option, - /// The font size in points - pub size: Option, - /// The color of the text - pub color: Option, - } } #[cfg(any( diff --git a/src/style.rs b/src/style.rs index 1f8ca328..8a7e0d63 100644 --- a/src/style.rs +++ b/src/style.rs @@ -5,9 +5,13 @@ mod dracula; pub mod series; pub mod theme; +pub use plotive_base::style::{ + Color, DefaultColor, DefaultStroke, DefaultStrokeWidth, Fill, LinePattern, ResolveColor, Stroke, +}; + pub use crate::style::series::Palette; pub use crate::style::theme::Theme; -use crate::{Color, ResolveColor, Rgba8, render}; +use crate::{Rgba8, render}; /// Overall style definition for figures /// @@ -140,18 +144,6 @@ impl ResolveColor for Style { } } -impl ResolveColor for Style { - fn resolve_color(&self, col: &series::IndexColor) -> Rgba8 { - self.palette.get(*col) - } -} - -impl ResolveColor for (&Style, usize) { - fn resolve_color(&self, _col: &series::AutoColor) -> Rgba8 { - self.0.palette.get(series::IndexColor(self.1)) - } -} - impl ResolveColor for (&Style, usize) { fn resolve_color(&self, col: &series::Color) -> Rgba8 { match col { @@ -171,103 +163,52 @@ fn add_opacity(c: Rgba8, opacity: Option) -> Rgba8 { } } -/// Dash pattern for dashed lines -/// A dash pattern is a sequence of lengths that specify the lengths of -/// alternating dashes and gaps. -/// -/// The lengths are relative to the line width. -/// So a pattern will scale with the line width and remain visually consistent. -#[derive(Debug, Clone, PartialEq)] -pub struct Dash(pub Vec); - -impl Default for Dash { - fn default() -> Self { - Dash(vec![5.0, 5.0]) - } -} - -/// Line pattern defines how the line is drawn -#[derive(Debug, Clone, PartialEq, Default)] -pub enum LinePattern { - /// Solid line - #[default] - Solid, - /// Dashed line. Equivalent to Dash(vec![5.0, 5.0]) - Dashed, - /// Dotted line. Equivalent to Dash(vec![1.0, 1.0]) - Dot, - /// Dash-dot line. Equivalent to Dash(vec![5.0, 5.0, 1.0, 5.0]) - DashDot, - /// Dashed line. The pattern is relative to the line width. - Dash(Dash), +/// Trait for converting a fill style into a renderable paint, resolving colors using a color resolver +pub trait AsPaint { + /// Convert to a renderable paint, resolving colors using the provided resolver + fn as_paint(&self, rc: &R) -> render::Paint<'_> + where + R: ResolveColor; } -impl From for LinePattern { - fn from(dash: Dash) -> Self { - LinePattern::Dash(dash) +impl AsPaint for Fill +where + C: Color, +{ + fn as_paint(&self, rc: &R) -> render::Paint<'_> + where + R: ResolveColor, + { + match self { + Fill::Solid { color, opacity } => { + render::Paint::Solid(add_opacity(color.resolve(rc), *opacity)) + } + } } } -/// Stroke style definition. Defines how lines are stroked. -/// -/// The color is a generic parameter to support different color resolution strategies, -/// such as fixed colors, theme-based colors, or series-based colors. -#[derive(Debug, Clone, PartialEq)] -pub struct Stroke { - /// Line color - pub color: C, - /// Line width in figure units - pub width: f32, - /// Line pattern - pub pattern: LinePattern, - /// Line opacity (0.0 to 1.0) - pub opacity: Option, -} - -const DASHED_DASH: &[f32] = &[5.0, 5.0]; -const DOT_DASH: &[f32] = &[1.0, 1.0]; -const DASH_DOT_DASH: &[f32] = &[5.0, 5.0, 1.0, 5.0]; - -/// Trait for types that have a default stroke width for serialization purposes -/// The trait is implemented for color types, so that the default stroke width -/// can be associated with the color type used in the stroke. -pub trait DefaultStrokeWidth { - /// Return the default stroke width for this color type. - fn default_stroke_width() -> f32; +/// Trait for converting a stroke style into a renderable stroke, resolving colors using a color resolver +pub trait AsStroke { + /// Convert to a renderable stroke, resolving colors using the provided resolver + fn as_stroke(&self, rc: &R) -> render::Stroke<'_> + where + R: ResolveColor; } -impl Stroke { - /// Set the line width in figure units, returning self for chaining - pub fn with_width(self, width: f32) -> Self { - Stroke { width, ..self } - } - - /// Set the line opacity (0.0 to 1.0), returning self for chaining - pub fn with_opacity(self, opacity: f32) -> Self { - Stroke { - opacity: Some(opacity), - ..self - } - } - - /// Set the line pattern, returning self for chaining - pub fn with_pattern(self, pattern: LinePattern) -> Self { - Stroke { pattern, ..self } - } - +impl AsStroke for Stroke +where + C: Color, +{ /// Convert to a renderable stroke, resolving colors using the provided resolver - pub fn as_stroke<'a, R>(&'a self, rc: &R) -> render::Stroke<'a> + fn as_stroke<'a, R>(&'a self, rc: &R) -> render::Stroke<'a> where R: ResolveColor, { let color = add_opacity(self.color.resolve(rc), self.opacity); - let pattern = match &self.pattern { - LinePattern::Solid => render::LinePattern::Solid, - LinePattern::Dashed => render::LinePattern::Dash(DASHED_DASH), - LinePattern::Dot => render::LinePattern::Dash(DOT_DASH), - LinePattern::DashDot => render::LinePattern::Dash(DASH_DOT_DASH), - LinePattern::Dash(Dash(a)) => render::LinePattern::Dash(a.as_slice()), + let pattern = match self.pattern.get_dash() { + Some(dash) => render::LinePattern::Dash(dash), + None => render::LinePattern::Solid, }; render::Stroke { @@ -278,123 +219,6 @@ impl Stroke { } } -impl Default for Stroke -where - C: Color + Default + DefaultStrokeWidth, -{ - fn default() -> Self { - Stroke { - color: C::default(), - width: C::default_stroke_width(), - pattern: LinePattern::default(), - opacity: None, - } - } -} - -impl From for Stroke { - fn from(color: C) -> Self { - Stroke { - width: 1.0, - color, - pattern: LinePattern::default(), - opacity: None, - } - } -} - -impl From<(C, f32)> for Stroke { - fn from((color, width): (C, f32)) -> Self { - Stroke { - color, - width, - pattern: LinePattern::default(), - opacity: None, - } - } -} - -impl From<(C, f32, LinePattern)> for Stroke { - fn from((color, width, pattern): (C, f32, LinePattern)) -> Self { - Stroke { - color, - width, - pattern, - opacity: None, - } - } -} - -impl From<(C, f32, Dash)> for Stroke { - fn from((color, width, dash): (C, f32, Dash)) -> Self { - Stroke { - color, - width, - pattern: LinePattern::Dash(dash), - opacity: None, - } - } -} - -/// Fill style definition -/// The color is a generic parameter to support different color resolution strategies, -/// such as fixed colors, theme based colors, or series-based colors. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Fill { - /// Solid fill - Solid { - /// Fill color - color: C, - /// Fill opacity (0.0 to 1.0) - opacity: Option, - }, -} - -impl Default for Fill -where - C: Color + Default, -{ - fn default() -> Self { - Fill::Solid { - color: C::default(), - opacity: None, - } - } -} - -impl Fill { - /// Set the fill opacity (0.0 to 1.0), returning self for chaining - pub const fn with_opacity(self, opacity: f32) -> Self { - match self { - Fill::Solid { color, .. } => Fill::Solid { - color, - opacity: Some(opacity), - }, - } - } - - /// Convert to a renderable paint, resolving colors using the provided resolver - pub fn as_paint(&self, rc: &R) -> render::Paint<'_> - where - R: ResolveColor, - { - match self { - Fill::Solid { color, opacity } => { - render::Paint::Solid(add_opacity(color.resolve(rc), *opacity)) - } - } - } -} - -impl From for Fill { - fn from(color: C) -> Self { - Fill::Solid { - color, - opacity: None, - } - } -} - /// Shape of a marker, used in scatter plots #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum MarkerShape { @@ -463,7 +287,7 @@ pub struct Marker { impl Marker where - C: Color + DefaultStrokeWidth, + C: Color + plotive_base::style::DefaultStrokeWidth, { /// Create a new marker with both fill and stroke set to the same color pub fn new_with_color(color: C) -> Self { @@ -570,7 +394,7 @@ where impl Default for Marker where - C: Color + Default + DefaultStrokeWidth, + C: Color + Default + plotive_base::style::DefaultStrokeWidth, { fn default() -> Self { Marker::new_with_color(C::default()) @@ -587,19 +411,22 @@ mod tests { fn test_color_resolve() { let style = Style::light(); - let theme_line: theme::Stroke = (theme::Color::Theme(theme::Col::LegendBorder), 2.0).into(); - let stroke = theme_line.as_stroke(&style); + let theme_stroke: theme::Stroke = + (theme::Color::Theme(theme::Col::LegendBorder), 2.0).into(); + let stroke = theme_stroke.as_stroke(&style); assert_eq!(stroke.color, Rgba8::from_hex(b"#000000")); - let series_line: Stroke = (series::IndexColor(2), 2.0).into(); - let stroke = series_line.as_stroke(&style); + let series_color: series::Color = series::IndexColor(2).into(); + let series_stroke: Stroke = series_color.into(); + let stroke = series_stroke.as_stroke(&(&style, 1)); assert_eq!(stroke.color, Rgba8::from_hex(b"#2ca02c")); - let series_line: Stroke = (series::AutoColor, 2.0).into(); - let stroke = series_line.as_stroke(&(&style, 2)); - assert_eq!(stroke.color, Rgba8::from_hex(b"#2ca02c")); + let series_color: series::Color = series::AutoColor.into(); + let series_stroke: Stroke = series_color.into(); + let stroke = series_stroke.as_stroke(&(&style, 1)); + assert_eq!(stroke.color, Rgba8::from_hex(b"#ff7f0e")); - let fixed_color: Stroke = (Rgba8::from_hex(b"#123456"), 2.0).into(); + let fixed_color: Stroke = Rgba8::from_hex(b"#123456").into(); let stroke = fixed_color.as_stroke(&()); assert_eq!(stroke.color, Rgba8::from_hex(b"#123456")); } diff --git a/src/style/series.rs b/src/style/series.rs index 4cf0603a..bc1b262c 100644 --- a/src/style/series.rs +++ b/src/style/series.rs @@ -3,8 +3,8 @@ */ use plotive_base::color; +use crate::Rgba8; use crate::style::{self, catppuccin, defaults, dracula}; -use crate::{ResolveColor, Rgba8}; /// A palette for data series. /// It provides ordered colors for series in a figure. @@ -73,8 +73,6 @@ impl Palette { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IndexColor(pub usize); -impl style::Color for IndexColor {} - /// An error type for parsing an IndexColor from a string #[derive(Debug, Clone, Copy)] pub enum IndexColorParseError { @@ -123,8 +121,6 @@ impl std::fmt::Display for IndexColor { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AutoColor; -impl style::Color for AutoColor {} - /// A flexible color for data series #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum Color { @@ -155,8 +151,6 @@ impl From for Color { } } -impl style::Color for Color {} - /// an error type for parsing a Color from a string #[derive(Debug)] @@ -206,19 +200,9 @@ impl std::str::FromStr for Color { } } -impl ResolveColor for Palette { - fn resolve_color(&self, col: &IndexColor) -> Rgba8 { - self.get(*col) - } -} +impl super::Color for Color {} -impl ResolveColor for (&Palette, usize) { - fn resolve_color(&self, _col: &AutoColor) -> Rgba8 { - self.0.get(IndexColor(self.1)) - } -} - -impl ResolveColor for (&Palette, usize) { +impl super::ResolveColor for (&Palette, usize) { fn resolve_color(&self, col: &Color) -> Rgba8 { match col { Color::Auto => self.0.get(IndexColor(self.1)), @@ -228,39 +212,30 @@ impl ResolveColor for (&Palette, usize) { } } +impl super::DefaultColor for Color { + fn default_color() -> Option { + Some(Color::Auto) + } +} + +impl super::DefaultStroke for Color { + fn default_stroke() -> Option> { + Some(style::Stroke::default()) + } +} + impl super::DefaultStrokeWidth for Color { fn default_stroke_width() -> f32 { defaults::SERIES_STROKE_WIDTH } } + /// Stroke style for theme elements pub type Stroke = style::Stroke; -impl From for Stroke { - fn from(color: Rgba8) -> Self { - use super::DefaultStrokeWidth; - - Stroke { - color: color.into(), - width: Color::default_stroke_width(), - pattern: style::LinePattern::default(), - opacity: None, - } - } -} - /// Fill style for theme elements pub type Fill = style::Fill; -impl From for Fill { - fn from(color: Rgba8) -> Self { - Fill::Solid { - color: color.into(), - opacity: None, - } - } -} - /// Marker style for theme elements pub type Marker = style::Marker; diff --git a/src/style/theme.rs b/src/style/theme.rs index 4828c65d..85fd49b3 100644 --- a/src/style/theme.rs +++ b/src/style/theme.rs @@ -1,7 +1,9 @@ //! Theme definitions and implementations +use plotive_base::style::DefaultStrokeWidth; + use crate::color::{self, Rgb8, Rgba8}; -use crate::style::{DefaultStrokeWidth, catppuccin, dracula}; +use crate::style::{catppuccin, dracula}; use crate::{style, text}; /// A theme, for styling figures @@ -167,8 +169,6 @@ pub enum Col { LegendBorder, } -impl super::Color for Col {} - impl std::str::FromStr for Col { type Err = (); fn from_str(s: &str) -> Result { @@ -196,18 +196,6 @@ impl std::fmt::Display for Col { } } -impl color::ResolveColor for Theme { - fn resolve_color(&self, col: &Col) -> Rgba8 { - match col { - Col::Background => self.background(), - Col::Foreground => self.foreground(), - Col::Grid => self.grid(), - Col::LegendFill => self.legend_fill(), - Col::LegendBorder => self.legend_border(), - } - } -} - /// A flexible color for theme elements #[derive(Debug, Clone, Copy, PartialEq)] pub enum Color { @@ -229,8 +217,6 @@ impl From for Color { } } -impl super::Color for Color {} - impl std::str::FromStr for Color { type Err = ::Err; @@ -253,16 +239,22 @@ impl std::fmt::Display for Color { } } -impl text::rich::Foreground for Color { +impl text::Foreground for Color { fn foreground() -> Self { Color::Theme(Col::Foreground) } } -impl color::ResolveColor for Theme { +impl super::Color for Color {} + +impl super::ResolveColor for Theme { fn resolve_color(&self, col: &Color) -> Rgba8 { match col { - Color::Theme(col) => self.resolve_color(col), + Color::Theme(Col::Background) => self.background(), + Color::Theme(Col::Foreground) => self.foreground(), + Color::Theme(Col::Grid) => self.grid(), + Color::Theme(Col::LegendFill) => self.legend_fill(), + Color::Theme(Col::LegendBorder) => self.legend_border(), Color::Fixed(c) => *c, } } @@ -273,11 +265,22 @@ impl super::DefaultStrokeWidth for Color { 1.0 } } +impl super::DefaultColor for Color { + fn default_color() -> Option { + None + } +} + +impl super::DefaultStroke for Color { + fn default_stroke() -> Option> { + None + } +} /// Stroke style for theme elements pub type Stroke = style::Stroke; -// From for Stroke is already defined in style.rs, using generics. +// From for Stroke is already defined using generics. // We just add From for Stroke here. impl From for Stroke { fn from(col: Col) -> Self { diff --git a/src/utils.rs b/src/utils.rs index c890a0d6..39df17d8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -144,7 +144,7 @@ impl LineMplStyle { match next { '-' => { chars.next(); - style.set_pattern(style::Dash::default().into())?; + style.set_pattern(style::LinePattern::Dashed)?; } '.' => { chars.next(); diff --git a/tests/src/tests/axes.rs b/tests/src/tests/axes.rs index 334f1604..290e5453 100644 --- a/tests/src/tests/axes.rs +++ b/tests/src/tests/axes.rs @@ -1,4 +1,4 @@ -use plotive::{color, des}; +use plotive::{color, des, style}; use super::{fig_small, line, line2}; use crate::tests::fig_mid; @@ -211,7 +211,7 @@ fn axes_categories() { let x = vec!["a".to_string(), "b".to_string(), "c".to_string()]; let y = vec![1.0, 1.4, 3.0]; let series = des::series::Bars::new(x.into(), y.into()) - .with_fill(color::TRANSPARENT.into()) + .with_fill(style::series::Fill::solid(color::TRANSPARENT.into())) .with_stroke(Default::default()); let plot = des::Plot::new(vec![series.into()]) diff --git a/tests/src/tests/series.rs b/tests/src/tests/series.rs index a4ba174b..b06944b2 100644 --- a/tests/src/tests/series.rs +++ b/tests/src/tests/series.rs @@ -139,8 +139,8 @@ fn series_area_double() { let y1 = vec![10.0, 15.0, 8.0, 6.0, 12.0, 10.0]; let y2 = vec![4.0, 9.0, 2.0, 0.0, 6.0, 4.0]; - let fill = plotive::Rgba8::from_hex(b"#888").into(); - let stroke: style::series::Stroke = plotive::Rgba8::from_hex(b"#000").into(); + let fill = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#888").into()); + let stroke = style::series::Stroke::solid(plotive::Rgba8::from_hex(b"#000").into()); let plot = des::Plot::new(vec![ des::series::Area::new( @@ -173,9 +173,9 @@ fn series_area_double_legend() { let y1 = vec![10.0, 15.0, 8.0, 6.0, 12.0, 10.0]; let y2 = vec![4.0, 9.0, 2.0, 0.0, 6.0, 4.0]; - let fill1 = plotive::Rgba8::from_hex(b"#888").into(); - let fill2 = plotive::Rgba8::from_hex(b"#444").into(); - let stroke: style::series::Stroke = plotive::Rgba8::from_hex(b"#000").into(); + let fill1 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#888").into()); + let fill2 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#444").into()); + let stroke = style::series::Stroke::solid(plotive::Rgba8::from_hex(b"#000").into()); let plot = des::Plot::new(vec![ des::series::Area::new( diff --git a/tests/src/tests/style.rs b/tests/src/tests/style.rs index 20ad5513..c7cff5c8 100644 --- a/tests/src/tests/style.rs +++ b/tests/src/tests/style.rs @@ -139,7 +139,7 @@ fn style_line_markers_triup_color() { plotive::style::series::Marker::new_with_color( plotive::Rgba8::from_hex(b"#000").into(), ) - .with_stroke(Rgba8::from_hex(b"#080").into()) + .with_stroke(plotive::style::series::Color::Fixed(Rgba8::from_hex(b"#080")).into()) .with_shape(plotive::style::MarkerShape::TriangleUp), ) .into_plot(); diff --git a/text/Cargo.toml b/text/Cargo.toml index 445894d3..6bdd20a0 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -26,7 +26,7 @@ tinyvec = { version = "1.6.0", features = ["alloc"] } fontconfig-parser = { version = "0.5", optional = true, default-features = false } [features] -default = ["std", "fs", "memmap", "fontconfig"] +default = ["std", "fs", "memmap", "fontconfig", "noto-sans"] # Enables minimal fontconfig support on Linux. # Must be enabled for NixOS, otherwise no fonts will be loaded. fontconfig = ["fontconfig-parser", "fs"] @@ -39,7 +39,7 @@ noto-sans-italic = [] noto-serif = [] noto-serif-italic = [] noto-mono = [] -serde = ["dep:serde"] +serde = ["plotive-base/serde", "dep:serde"] std = ["ttf-parser/std"] diff --git a/text/examples/text_line.rs b/text/examples/text_line.rs index 6ee3a659..53139eef 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}; +use plotive_text::{bundled_font_db, font, line, props}; fn main() { let mut db = bundled_font_db(); @@ -36,14 +36,22 @@ fn main() { for (text, align, (x, y)) in texts { let (tx, ty) = (*x, *y); - let render_opts = line::RenderOptions { - fill: Some(tiny_skia::Paint::default()), - outline: None, - transform: tiny_skia::Transform::from_translate(tx, ty), - mask: None, - }; - let line = LineText::new(text.to_string(), *align, 32.0, font.clone(), &db).unwrap(); - line::render_line_text(&line, &render_opts, &db, &mut pm_mut); + let line = LineText::new( + text.to_string(), + *align, + props::FontProps::new(font.clone(), 32.0), + &db, + ) + .unwrap(); + line::render_line_text( + &line, + &mut pm_mut, + None, + tiny_skia::Transform::from_translate(tx, ty), + &db, + &Default::default(), + Default::default(), + ); draw_line_bbox(&line, (tx, ty), &mut pm_mut); } diff --git a/text/examples/text_rich.rs b/text/examples/text_rich.rs index bdbc3c03..8f7a0884 100644 --- a/text/examples/text_rich.rs +++ b/text/examples/text_rich.rs @@ -1,4 +1,4 @@ -use plotive_text::{self as text, Font, RichTextBuilder, font, rich}; +use plotive_text::{self as text, Font, RichTextBuilder, font, props, rich}; use tiny_skia::Transform; fn main() { @@ -42,7 +42,7 @@ fn main() { let start_line2 = line1.len(); let end_line2 = line1.len() + line2.len(); - let root_props = rich::TextProps::new(FS_LARGE).with_font(sans_font.clone()); + let root_props = props::TextProps::new(sans_font.clone(), FS_LARGE); let mut builder = RichTextBuilder::new(text, root_props).with_layout(rich::Layout::Horizontal( rich::Align::Center, @@ -52,19 +52,19 @@ fn main() { builder.add_span( start_rlc, end_rlc, - rich::ClassProps { - font_weight: Some(font::Weight::BOLD), - font_style: Some(font::Style::Italic), + props::TextModifiers { + weight: Some(font::Weight::BOLD), + style: Some(font::Style::Italic), ..Default::default() }, ); builder.add_span( start_line2, end_line2, - rich::ClassProps { - font_family: Some(serif_family), - font_size: Some(FS_MEDIUM), - font_style: Some(font::Style::Italic), + props::TextModifiers { + families: 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 = rich::TextProps::new(FS_LARGE).with_font(serif_cjk_font); + let root_props = props::TextProps::new(serif_cjk_font, FS_LARGE); 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 = rich::TextProps::new(FS_SMALL).with_font(sans_font); + let root_props = props::TextProps::new(sans_font.clone(), FS_SMALL); 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 9ba68902..f995fae1 100644 --- a/text/examples/text_rich_parse.rs +++ b/text/examples/text_rich_parse.rs @@ -1,5 +1,5 @@ use plotive_base::color; -use plotive_text::{self as text, Font, bundled_font_db, font, rich}; +use plotive_text::{self as text, Font, bundled_font_db, font, props, rich}; use tiny_skia::Transform; fn main() { let db = bundled_font_db(); @@ -22,11 +22,7 @@ fn main() { ); let rich_text = text::parse_rich_text(fmt) .unwrap() - .into_builder( - rich::TextProps::new(36.0) - .with_font(sans_font) - .with_color(Some(color::BLACK)), - ) + .into_builder(props::TextProps::new(sans_font, 36.0).with_render(color::BLACK.into())) .with_layout(rich::Layout::Horizontal( rich::Align::Center, rich::VerAlign::Center, diff --git a/text/src/font.rs b/text/src/font.rs index bea0759d..7d6221df 100644 --- a/text/src/font.rs +++ b/text/src/font.rs @@ -373,10 +373,10 @@ impl str::FromStr for Width { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Font { - families: Vec, - weight: Weight, - width: Width, - style: Style, + pub(crate) families: Vec, + pub(crate) weight: Weight, + pub(crate) width: Width, + pub(crate) style: Style, } impl str::FromStr for Font { diff --git a/text/src/lib.rs b/text/src/lib.rs index 4b7103b4..0062d3c8 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -24,12 +24,14 @@ mod bidi; pub mod font; pub mod fontdb; pub mod line; +pub mod props; pub mod rich; #[cfg(feature = "serde")] 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 rich::{ ParseRichTextError, ParsedRichText, RichPrimitive, RichText, RichTextBuilder, parse_rich_text, parse_rich_text_with_classes, render_rich_text, render_rich_text_with, @@ -165,3 +167,20 @@ impl ttf::OutlineBuilder for Outliner<'_> { self.0.close(); } } + +// Create a path for a line decoration (underline, strikethrough, etc.) given the rectangle of the text, the baseline y position, and the line metrics. +// Placed here because it is used in both line and rich text rendering. +fn line_path( + rect: geom::Rect, + y_baseline: f32, + line: font::ScaledLineMetrics, + mut builder: geom::PathBuilder, +) -> geom::Path { + // there is no y-flip transform on this one + builder.move_to(rect.left(), y_baseline - line.position); + builder.line_to(rect.right(), y_baseline - line.position); + builder.line_to(rect.right(), y_baseline - line.position + line.thickness); + builder.line_to(rect.left(), y_baseline - line.position + line.thickness); + builder.close(); + builder.finish().unwrap() +} diff --git a/text/src/line.rs b/text/src/line.rs index 1f02de11..4a3a87e2 100644 --- a/text/src/line.rs +++ b/text/src/line.rs @@ -5,6 +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}; /// Horizontal alignment @@ -44,8 +45,7 @@ pub enum VerAlign { pub struct LineText { text: String, align: (Align, VerAlign), - font_size: f32, - font: Font, + font: FontProps, bbox: Option, main_dir: ScriptDir, metrics: font::ScaledMetrics, @@ -61,11 +61,7 @@ impl LineText { self.align } - pub fn font_size(&self) -> f32 { - self.font_size - } - - pub fn font(&self) -> &Font { + pub fn font(&self) -> &FontProps { &self.font } @@ -95,8 +91,7 @@ impl LineText { Self { text: String::new(), align: (Default::default(), Default::default()), - font_size: 1.0, - font, + font: FontProps::new(font, 1.0), bbox: None, main_dir: ScriptDir::LeftToRight, metrics: font::ScaledMetrics::null(), @@ -113,8 +108,7 @@ impl LineText { pub fn new( text: String, align: (Align, VerAlign), - font_size: f32, - font: Font, + font: FontProps, db: &fontdb::Database, ) -> Result { let default_lev = match crate::script_is_rtl(&text) { @@ -125,7 +119,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.clone())); + return Ok(LineText::new_empty(font.font.clone())); } let main_dir = match default_lev { Some(lev) if lev.is_ltr() => ScriptDir::LeftToRight, @@ -140,7 +134,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_size, &font, db, &mut ctx)?; + let shape = Shape::shape_run(&text, run, &font, db, &mut ctx)?; shapes.push(shape); } @@ -190,7 +184,6 @@ impl LineText { Ok(LineText { text, align: (align, ver_align), - font_size, font: font.clone(), bbox: Some(geom::Rect::from_trbl(top, x_cursor, bottom, x_start)), main_dir, @@ -261,15 +254,14 @@ impl Shape { fn shape_run( text: &str, run: &bidi::BidiRun, - font_size: f32, - font: &font::Font, + font: &FontProps, db: &fontdb::Database, ctx: &mut Ctx, ) -> Result { let face_id = db - .select_face_for_str(font, text) - .or_else(|| db.select_face(&font)) - .ok_or_else(|| Error::NoSuchFont(font.clone()))?; + .select_face_for_str(&font.font, text) + .or_else(|| db.select_face(&font.font)) + .ok_or_else(|| Error::NoSuchFont(font.font.clone()))?; let mut buffer = ctx .buffer @@ -289,9 +281,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::apply_hb_variations(&mut hbface, &font.font); Ok((rustybuzz::shape(&hbface, &[], buffer), metrics)) }) @@ -319,70 +311,129 @@ impl Shape { } } -pub fn render_line_text_with(line: &LineText, db: &font::Database, mut render_fn: R) -where +pub fn render_line_text_with( + line: &LineText, + db: &font::Database, + decorations: props::Decorations, + mut render_fn: R, +) where R: FnMut(&geom::Path), { 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::apply_ttf_variations(&mut face, &line.font.font); // the path builder for the entire string - let mut str_pb = geom::PathBuilder::new(); + let mut shape_builder = geom::PathBuilder::new(); // the path builder for each glyph - let mut gl_pb = geom::PathBuilder::new(); + let mut glyph_builder = geom::PathBuilder::new(); for gl in &shape.glyphs { { - let mut builder = crate::Outliner(&mut gl_pb); + let mut builder = crate::Outliner(&mut glyph_builder); face.outline_glyph(gl.id, &mut builder); } - if let Some(path) = gl_pb.finish() { + if let Some(path) = glyph_builder.finish() { let path = path.transform(gl.ts).unwrap(); - str_pb.push_path(&path); + shape_builder.push_path(&path); - gl_pb = path.clear(); + glyph_builder = path.clear(); } else { - gl_pb = geom::PathBuilder::new(); + glyph_builder = geom::PathBuilder::new(); } } - if let Some(path) = str_pb.finish() { + if let Some(path) = shape_builder.finish() { render_fn(&path); } }); } + + if line.bbox.is_some() && (decorations.underline || decorations.strikethrough) { + let bbox = line.bbox.unwrap(); + let mut span_builder = geom::PathBuilder::new(); + + if decorations.underline { + let metrics = line.metrics.uline; + let path = crate::line_path(bbox, 0.0, metrics, geom::PathBuilder::new()); + span_builder.push_path(&path); + } + if decorations.strikethrough { + let metrics = line.metrics.strikeout; + let path = crate::line_path(bbox, 0.0, metrics, geom::PathBuilder::new()); + span_builder.push_path(&path); + } + + if let Some(path) = span_builder.finish() { + render_fn(&path); + } + } } #[derive(Debug, Clone)] pub struct RenderOptions<'a> { - pub fill: Option>, - pub outline: Option<(tiny_skia::Paint<'a>, tiny_skia::Stroke)>, + pub render: props::RenderProps, pub mask: Option<&'a tiny_skia::Mask>, pub transform: geom::Transform, } pub fn render_line_text( line: &LineText, - opts: &RenderOptions<'_>, - db: &font::Database, pixmap: &mut tiny_skia::PixmapMut<'_>, + mask: Option<&tiny_skia::Mask>, + transform: geom::Transform, + db: &font::Database, + render: &props::RenderProps, + decorations: props::Decorations, ) { let render_fn = |path: &geom::Path| { - if let Some(paint) = opts.fill.as_ref() { - pixmap.fill_path( - &path, - &paint, - tiny_skia::FillRule::Winding, - opts.transform, - opts.mask, - ); + if let Some(fill) = render.fill.as_ref() { + let plotive_base::style::Fill::Solid { color, opacity } = fill; + let skia_color = tiny_skia_color(*color, *opacity); + + let paint = tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(skia_color), + anti_alias: true, + ..Default::default() + }; + pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, transform, mask); } - if let Some((paint, stroke)) = opts.outline.as_ref() { - pixmap.stroke_path(&path, &paint, &stroke, opts.transform, opts.mask); + if let Some(outline) = render.outline.as_ref() { + let plotive_base::style::Stroke { + color, + width, + pattern, + opacity, + } = outline; + let skia_color = tiny_skia_color(*color, *opacity); + let paint = tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(skia_color), + anti_alias: true, + ..Default::default() + }; + let dash = pattern + .get_dash() + .map(|d| tiny_skia::StrokeDash::new(d.to_vec(), 0.0)) + .flatten(); + + let stroke = tiny_skia::Stroke { + width: *width, + dash, + ..Default::default() + }; + pixmap.stroke_path(&path, &paint, &stroke, transform, mask); } }; - render_line_text_with(line, db, render_fn); + render_line_text_with(line, db, decorations, render_fn); +} + +fn tiny_skia_color(col: plotive_base::Rgba8, opacity: Option) -> tiny_skia::Color { + let a = if let Some(op) = opacity { + (col.a() as f32 * op).clamp(0.0, 255.0) as u8 + } else { + col.a() + }; + tiny_skia::Color::from_rgba8(col.r(), col.g(), col.b(), a) } diff --git a/text/src/props.rs b/text/src/props.rs new file mode 100644 index 00000000..52a4a3b9 --- /dev/null +++ b/text/src/props.rs @@ -0,0 +1,216 @@ +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, + pub strikethrough: bool, +} + +/// A color that has meaning for the foreground +/// (e.g. a text color) +pub trait Foreground { + fn foreground() -> Self; +} + +impl Foreground for color::Rgba8 { + fn foreground() -> Self { + color::BLACK + } +} + +/// Properties for rendering text, including color and outline. +#[derive(Debug, Clone, PartialEq)] +pub struct RenderProps { + /// The color of the text. This is the fill color for the text glyphs. + pub fill: Option>, + /// The outline of the text. This is the stroke color and width for the text glyphs. + pub outline: Option>, +} + +impl From for RenderProps +where + C: Foreground + style::Color, +{ + fn from(color: C) -> Self { + RenderProps { + fill: Some(style::Fill::solid(color)), + outline: None, + } + } +} + +impl Default for RenderProps { + fn default() -> Self { + RenderProps { + fill: Some(style::Fill::solid(C::foreground())), + outline: None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TextProps { + pub font: FontProps, + pub decorations: Decorations, + pub render: RenderProps, +} + +impl TextProps +where + C: Foreground, +{ + pub fn new(font: Font, size: f32) -> TextProps { + TextProps { + font: FontProps::new(font, size), + decorations: Decorations::default(), + render: RenderProps::default(), + } + } +} + +impl TextProps +where + C: Clone, +{ + pub fn with_font(mut self, font: FontProps) -> Self { + self.font = font; + self + } + + pub fn with_decoration(mut self, decorations: Decorations) -> Self { + self.decorations = decorations; + self + } + + pub fn with_render(mut self, render: RenderProps) -> Self { + self.render = render; + self + } + + pub fn apply_modifiers(&mut self, modifiers: &TextModifiers) { + if let Some(families) = &modifiers.families { + self.font.font.families = families.clone(); + } + if let Some(weight) = modifiers.weight { + self.font.font.weight = weight; + } + if let Some(width) = modifiers.width { + self.font.font.width = width; + } + if let Some(style) = modifiers.style { + self.font.font.style = style; + } + if let Some(size) = modifiers.size { + self.font.size = size; + } + if let Some(fill) = modifiers.color.as_ref() { + self.render.fill = fill.clone(); + } + if let Some(outline) = modifiers.outline.as_ref() { + self.render.outline = outline.clone(); + } + if let Some(underline) = modifiers.underline { + self.decorations.underline = underline; + } + if let Some(strikethrough) = modifiers.strikethrough { + self.decorations.strikethrough = strikethrough; + } + } +} + +impl RenderProps { + /// Convert this RenderProps to another color type using the provided mapping function + pub fn to_other_color(&self, color_map: M) -> RenderProps + where + D: Clone, + M: Fn(&C) -> D, + { + RenderProps { + fill: self.fill.as_ref().map(|fill| match fill { + style::Fill::Solid { color, opacity } => style::Fill::Solid { + color: color_map(color), + opacity: *opacity, + }, + }), + outline: self.outline.as_ref().map(|stroke| style::Stroke { + color: color_map(&stroke.color), + width: stroke.width, + pattern: stroke.pattern.clone(), + opacity: stroke.opacity, + }), + } + } +} + +impl TextProps +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 + where + D: Clone, + M: Fn(&C) -> D, + { + TextProps { + font: self.font.clone(), + decorations: self.decorations, + render: self.render.to_other_color(color_map), + } + } +} + +/// A set of modifiers that can be applied on top of [`TextProps`] 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 weight: Option, + pub width: Option, + pub style: Option, + pub size: Option, + pub color: Option>>, + pub outline: Option>>, + pub underline: Option, + pub strikethrough: Option, +} + +impl Default for TextModifiers { + fn default() -> Self { + TextModifiers { + families: None, + weight: None, + width: None, + style: None, + size: None, + color: None, + outline: None, + underline: None, + strikethrough: None, + } + } +} + +impl TextModifiers { + pub(crate) fn affect_shape(&self) -> bool { + self.families.is_some() + || self.weight.is_some() + || self.width.is_some() + || self.style.is_some() + || self.size.is_some() + } +} diff --git a/text/src/rich.rs b/text/src/rich.rs index 7f0f9589..85d79de7 100644 --- a/text/src/rich.rs +++ b/text/src/rich.rs @@ -1,4 +1,4 @@ -use plotive_base::{Color, Rgba8, color, geom}; +use plotive_base::{Rgba8, geom}; use ttf_parser as ttf; use crate::{Error, font, fontdb, line}; @@ -14,6 +14,8 @@ pub use parse::{ }; pub use render::{RichPrimitive, render_rich_text, render_rich_text_with}; +use crate::props::{TextModifiers, TextProps}; + /// Typographic alignment, possibly depending on the script direction. #[derive(Debug, Clone, Copy, Default)] pub enum Align { @@ -183,13 +185,17 @@ where } /// Add a new text span - pub fn add_span(&mut self, start: usize, end: usize, props: ClassProps) { + pub fn add_span(&mut self, start: usize, end: usize, modifiers: TextModifiers) { 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, props }); + self.spans.push(TextSpan { + start, + end, + modifiers, + }); } /// Create a RichText from this builder @@ -304,200 +310,12 @@ where } } -/// A set of properties to be applied to a text span. -/// If a property is `None`, value is inherited from the parent span. -#[derive(Debug, Clone, PartialEq)] -pub struct ClassProps { - pub font_family: Option>, - pub font_weight: Option, - pub font_width: Option, - pub font_style: Option, - pub font_size: Option, - pub color: Option, - pub outline: Option<(C, f32)>, - pub underline: Option, - pub strikeout: Option, -} - -impl Default for ClassProps { - fn default() -> Self { - ClassProps { - font_family: None, - font_weight: None, - font_width: None, - font_style: None, - font_size: None, - color: None, - outline: None, - underline: None, - strikeout: None, - } - } -} - -impl ClassProps { - fn affect_shape(&self) -> bool { - self.font_family.is_some() - || self.font_weight.is_some() - || self.font_width.is_some() - || self.font_style.is_some() - || self.font_size.is_some() - } -} - -/// A set of resolved properties for a text span -#[derive(Debug, Clone, PartialEq)] -pub struct TextProps -where - C: Clone, -{ - font_size: f32, - font: font::Font, - color: Option, - outline: Option<(C, f32)>, - underline: bool, - strikeout: bool, -} - -impl TextProps -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 - where - D: Clone, - M: Fn(&C) -> D, - { - TextProps { - font_size: self.font_size, - font: self.font.clone(), - color: self.color.as_ref().map(|c| color_map(c)), - outline: self.outline.as_ref().map(|(c, w)| (color_map(c), *w)), - underline: self.underline, - strikeout: self.strikeout, - } - } -} - -/// A color that has meaning for the foreground -/// (e.g. a font color) -pub trait Foreground { - fn foreground() -> Self; -} - -impl Foreground for Rgba8 { - fn foreground() -> Self { - color::BLACK - } -} - -impl TextProps -where - C: Color + Foreground, -{ - pub fn new(font_size: f32) -> TextProps { - TextProps { - font_size, - font: font::Font::default(), - color: Some(C::foreground()), - outline: None, - underline: false, - strikeout: false, - } - } -} - -impl TextProps -where - C: Clone, -{ - pub fn with_font(mut self, font: font::Font) -> Self { - self.font = font; - self - } - - pub fn with_color(mut self, color: Option) -> Self { - self.color = color; - self - } - - pub fn with_outline(mut self, outline: (C, f32)) -> Self { - self.outline = Some(outline); - self - } - - pub fn with_underline(mut self) -> Self { - self.underline = true; - self - } - - pub fn with_strikeout(mut self) -> Self { - self.strikeout = true; - self - } - - pub fn font_size(&self) -> f32 { - self.font_size - } - - pub fn font(&self) -> &font::Font { - &self.font - } - - pub fn color(&self) -> Option { - self.color.clone() - } - - pub fn outline(&self) -> Option<(C, f32)> { - self.outline.clone() - } - - pub fn underline(&self) -> bool { - self.underline - } - - pub fn strikeout(&self) -> bool { - self.strikeout - } - - fn apply_opts(&mut self, class_props: &ClassProps) { - if let Some(font_family) = &class_props.font_family { - self.font = self.font.clone().with_families(font_family.clone()); - } - if let Some(font_weight) = class_props.font_weight { - self.font = self.font.clone().with_weight(font_weight); - } - if let Some(font_width) = class_props.font_width { - self.font = self.font.clone().with_width(font_width); - } - if let Some(font_style) = class_props.font_style { - self.font = self.font.clone().with_style(font_style); - } - if let Some(font_size) = class_props.font_size { - self.font_size = font_size; - } - if let Some(color) = class_props.color.as_ref() { - self.color = Some(color.clone()); - } - if let Some(outline) = class_props.outline.as_ref() { - self.outline = Some(outline.clone()); - } - if let Some(underline) = class_props.underline { - self.underline = underline; - } - if let Some(strikeout) = class_props.strikeout { - self.strikeout = strikeout; - } - } -} - /// A text span #[derive(Debug, Clone)] struct TextSpan { start: usize, end: usize, - props: ClassProps, + modifiers: TextModifiers, } /// A line of rich text @@ -702,12 +520,12 @@ where /// The font of this shape pub fn font(&self) -> &font::Font { - &self.spans[0].props.font + &self.spans[0].props.font.font } /// The font of this shape pub fn font_size(&self) -> f32 { - self.spans[0].props.font_size + self.spans[0].props.font.size } /// The text spans in this shape diff --git a/text/src/rich/builder.rs b/text/src/rich/builder.rs index 52bd0d7b..914a784a 100644 --- a/text/src/rich/builder.rs +++ b/text/src/rich/builder.rs @@ -2,11 +2,12 @@ use plotive_base::geom; use ttf_parser as ttf; use super::{ - Align, Boundaries, ClassProps, Direction, Error, Glyph, HorAlign, Layout, LineSpan, PropsSpan, - RichText, RichTextBuilder, ShapeSpan, TextProps, VerAlign, VerDirection, VerProgression, + Align, Boundaries, Direction, Error, Glyph, HorAlign, Layout, LineSpan, PropsSpan, RichText, + RichTextBuilder, ShapeSpan, VerAlign, VerDirection, VerProgression, }; use crate::bidi::BidiAlgo; use crate::font::{self, DatabaseExt}; +use crate::props::{TextModifiers, TextProps}; use crate::{fontdb, line}; #[derive(Debug)] @@ -25,7 +26,7 @@ where C: Clone, { init_props: TextProps, - stack: Vec>, + stack: Vec>, } impl PropsResolver @@ -41,26 +42,19 @@ where fn resolved(&self) -> TextProps { let mut props = self.init_props.clone(); - for opts in self.stack.iter() { - props.apply_opts(opts); - } - TextProps { - font: props.font, - font_size: props.font_size, - color: props.color.clone(), - outline: props.outline.clone(), - underline: props.underline, - strikeout: props.strikeout, + for modifiers in self.stack.iter() { + props.apply_modifiers(modifiers); } + props } - fn push_opts(&mut self, opts: ClassProps) { - self.stack.push(opts); + fn push_modifiers(&mut self, modifiers: TextModifiers) { + self.stack.push(modifiers); } - fn pop_opts(&mut self, opts: &ClassProps) { + fn pop_modifiers(&mut self, modifiers: &TextModifiers) { for i in (0..self.stack.len()).rev() { - if &self.stack[i] == opts { + if &self.stack[i] == modifiers { self.stack.remove(i); break; } @@ -278,7 +272,7 @@ where boundaries.check_in(run.start); boundaries.check_in(run.end); } - for span in self.spans.iter().filter(|s| s.props.affect_shape()) { + for span in self.spans.iter().filter(|s| s.modifiers.affect_shape()) { boundaries.check_in(span.start); boundaries.check_in(span.end); } @@ -327,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_opts(span.props.clone()); + ctx.resolver.push_modifiers(span.modifiers.clone()); } } props_spans.push(PropsSpan { @@ -338,7 +332,7 @@ where }); for span in self.spans.iter() { if span.end == span_end { - ctx.resolver.pop_opts(&span.props); + ctx.resolver.pop_modifiers(&span.modifiers); } } } @@ -347,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, txt) - .or_else(|| fontdb.select_face(&shape_props.font)) - .ok_or_else(|| Error::NoSuchFont(shape_props.font.clone()))?; + .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()))?; let mut buffer = ctx .buffer @@ -368,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.font.size); let mut hbface = rustybuzz::Face::from_face(face); - font::apply_hb_variations(&mut hbface, &shape_props.font); + font::apply_hb_variations(&mut hbface, &shape_props.font.font); let buffer = rustybuzz::shape(&hbface, &[], buffer); @@ -702,12 +696,14 @@ mod tests { #[test] fn underline_span() { let db = bundled_font_db(); - let mut builder: RichTextBuilder = - RichTextBuilder::new("Some RICH\ntext string".to_string(), TextProps::new(12.0)); + let mut builder: RichTextBuilder = RichTextBuilder::new( + "Some RICH\ntext string".to_string(), + TextProps::new(Default::default(), 12.0), + ); builder.add_span( 5, 9, - ClassProps { + TextModifiers { underline: Some(true), ..Default::default() }, @@ -718,8 +714,17 @@ mod tests { assert_eq!(text.lines[1].shapes.len(), 1); 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.underline, false); - assert_eq!(text.lines[0].shapes[0].spans[1].props.underline, true); - assert_eq!(text.lines[1].shapes[0].spans[0].props.underline, false); + assert_eq!( + text.lines[0].shapes[0].spans[0].props.decorations.underline, + false + ); + assert_eq!( + text.lines[0].shapes[0].spans[1].props.decorations.underline, + true + ); + assert_eq!( + 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 ed16bc69..de9cbb81 100644 --- a/text/src/rich/parse.rs +++ b/text/src/rich/parse.rs @@ -1,9 +1,9 @@ use std::fmt; use std::str::FromStr; -use plotive_base::Color; +use plotive_base::style; -use crate::rich::{ClassProps, TextProps}; +use crate::props::{TextModifiers, TextProps}; use crate::{RichTextBuilder, font}; /// Position into an input stream @@ -71,12 +71,12 @@ impl std::error::Error for ParseRichTextError {} #[derive(Debug, Clone)] pub struct ParsedRichText { pub text: String, - pub prop_spans: Vec<(Pos, Pos, ClassProps)>, + pub prop_spans: Vec<(Pos, Pos, TextModifiers)>, } impl ParsedRichText where - C: Color + PartialEq, + C: style::Color + PartialEq, { pub fn into_builder(self, root_props: TextProps) -> RichTextBuilder { let mut builder = RichTextBuilder::new(self.text, root_props); @@ -89,7 +89,7 @@ where pub fn parse_rich_text(fmt: &str) -> Result, ParseRichTextError> where - C: Color + FromStr, + C: style::Color + FromStr, { let parser = RichTextParser::new(fmt); parser.parse() @@ -97,10 +97,10 @@ where pub fn parse_rich_text_with_classes( fmt: &str, - user_classes: &[(String, ClassProps)], + user_classes: &[(String, TextModifiers)], ) -> Result, ParseRichTextError> where - C: Color + FromStr, + C: style::Color + FromStr, { let parser = RichTextParser::new_with_classes(fmt, user_classes); parser.parse() @@ -109,12 +109,12 @@ where #[derive(Debug, Clone)] struct RichTextParser<'a, C> { fmt: &'a str, - user_classes: &'a [(String, ClassProps)], + user_classes: &'a [(String, TextModifiers)], } impl<'a, C> RichTextParser<'a, C> where - C: Color + FromStr, + C: style::Color + FromStr, { pub fn new(fmt: &'a str) -> Self { Self { @@ -123,7 +123,7 @@ where } } - pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, ClassProps)]) -> Self { + pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, TextModifiers)]) -> Self { Self { fmt, user_classes } } @@ -173,17 +173,17 @@ where Ok(ParsedRichText { text, prop_spans }) } - fn merge_props(base: ClassProps, overlay: &ClassProps) -> ClassProps { - ClassProps { - font_family: overlay.font_family.clone().or_else(|| base.font_family), - font_weight: overlay.font_weight.or(base.font_weight), - font_width: overlay.font_width.or(base.font_width), - font_style: overlay.font_style.or(base.font_style), - font_size: overlay.font_size.or(base.font_size), + fn merge_props(base: TextModifiers, overlay: &TextModifiers) -> TextModifiers { + TextModifiers { + families: overlay.families.clone().or_else(|| base.families), + weight: overlay.weight.or(base.weight), + width: overlay.width.or(base.width), + style: overlay.style.or(base.style), + size: overlay.size.or(base.size), color: overlay.color.or(base.color), - outline: overlay.outline.or(base.outline), + outline: overlay.outline.clone().or(base.outline), underline: overlay.underline.or(base.underline), - strikeout: overlay.strikeout.or(base.strikeout), + strikethrough: overlay.strikethrough.or(base.strikethrough), } } @@ -191,8 +191,8 @@ where &self, span: Span, tag: &lex::OpeningTag, - ) -> Result, ParseRichTextError> { - let mut props = ClassProps::default(); + ) -> Result, ParseRichTextError> { + let mut props = TextModifiers::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, @@ -201,48 +201,48 @@ where match prop.prop.as_str() { "font-size" | "size" | "sz" => { if let Ok(size) = value.parse::() { - props.font_size = Some(size); + props.size = Some(size); } } "font-family" | "font" | "family" | "ff" => { - props.font_family = - Some(font::parse_font_families(value).map_err(|_| { - ParseRichTextError::BadPropValue( - span, - prop.prop.clone(), - value.clone(), - ) - })?); + props.families = Some(font::parse_font_families(value).map_err(|_| { + ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) + })?); } "font-weight" | "weight" | "fw" => { let weight: font::Weight = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.font_weight = Some(weight); + props.weight = Some(weight); } "font-style" | "style" | "fs" => { let style: font::Style = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.font_style = Some(style); + props.style = Some(style); } "font-width" | "width" | "font-stretch" | "stretch" => { let width: font::Width = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.font_width = Some(width); + props.width = Some(width); } "color" | "fill" => { let color: C = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.color = Some(color); + props.color = Some(Some(style::Fill::solid(color))); } "outline" | "stroke" => { let color: C = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.color = Some(color); + props.outline = Some(Some(style::Stroke { + color, + width: 1.0, + pattern: style::LinePattern::Solid, + opacity: None, + })); } _ => { return Err(ParseRichTextError::UnknownClass(span, prop.prop.clone())); @@ -266,76 +266,83 @@ where match prop.prop.as_str() { // font weight "thin" => { - props.font_weight = Some(font::Weight::THIN); + props.weight = Some(font::Weight::THIN); } "extra-light" => { - props.font_weight = Some(font::Weight::EXTRA_LIGHT); + props.weight = Some(font::Weight::EXTRA_LIGHT); } "light" => { - props.font_weight = Some(font::Weight::LIGHT); + props.weight = Some(font::Weight::LIGHT); } "medium" => { - props.font_weight = Some(font::Weight::MEDIUM); + props.weight = Some(font::Weight::MEDIUM); } "semi-bold" => { - props.font_weight = Some(font::Weight::SEMIBOLD); + props.weight = Some(font::Weight::SEMIBOLD); } "bold" => { - props.font_weight = Some(font::Weight::BOLD); + props.weight = Some(font::Weight::BOLD); } "extra-bold" | "extrabold" => { - props.font_weight = Some(font::Weight::EXTRA_BOLD); + props.weight = Some(font::Weight::EXTRA_BOLD); } "black" => { - props.font_weight = Some(font::Weight::BLACK); + props.weight = Some(font::Weight::BLACK); } // font style "italic" => { - props.font_style = Some(font::Style::Italic); + props.style = Some(font::Style::Italic); } "oblique" => { - props.font_style = Some(font::Style::Oblique); + props.style = Some(font::Style::Oblique); } // font width "ultra-condensed" => { - props.font_width = Some(font::Width::UltraCondensed); + props.width = Some(font::Width::UltraCondensed); } "extra-condensed" => { - props.font_width = Some(font::Width::ExtraCondensed); + props.width = Some(font::Width::ExtraCondensed); } "condensed" => { - props.font_width = Some(font::Width::Condensed); + props.width = Some(font::Width::Condensed); } "semi-condensed" => { - props.font_width = Some(font::Width::SemiCondensed); + props.width = Some(font::Width::SemiCondensed); } "semi-expanded" => { - props.font_width = Some(font::Width::SemiExpanded); + props.width = Some(font::Width::SemiExpanded); } "expanded" => { - props.font_width = Some(font::Width::Expanded); + props.width = Some(font::Width::Expanded); } "extra-expanded" => { - props.font_width = Some(font::Width::ExtraExpanded); + props.width = Some(font::Width::ExtraExpanded); } "ultra-expanded" => { - props.font_width = Some(font::Width::UltraExpanded); + props.width = Some(font::Width::UltraExpanded); } // for normal, we set them all "normal" => { - props.font_weight = Some(font::Weight::NORMAL); - props.font_style = Some(font::Style::Normal); - props.font_width = Some(font::Width::Normal); + props.weight = Some(font::Weight::NORMAL); + props.style = Some(font::Style::Normal); + props.width = Some(font::Width::Normal); } "underline" => { props.underline = Some(true); } - "strikeout" => { - props.strikeout = Some(true); + "strikethrough" | "strikeout" => { + props.strikethrough = Some(true); + } + + "nofill" => { + props.color = Some(None); + } + "nostroke" => { + props.outline = Some(None); } other => { @@ -343,7 +350,7 @@ where let color: C = other.parse().map_err(|_| { ParseRichTextError::UnknownClass(span, other.to_string()) })?; - props.color = Some(color); + props.color = Some(Some(style::Fill::solid(color))); } } } diff --git a/text/src/rich/render.rs b/text/src/rich/render.rs index 697d88bc..2d3faa06 100644 --- a/text/src/rich/render.rs +++ b/text/src/rich/render.rs @@ -1,4 +1,4 @@ -use plotive_base::{Rgba8, geom}; +use plotive_base::{Rgba8, geom, style}; use ttf_parser as ttf; use super::RichText; @@ -9,8 +9,8 @@ pub enum RichPrimitive<'a, C = Rgba8> where C: Clone, { - Fill(&'a geom::Path, C), - Stroke(&'a geom::Path, C, f32), + Fill(&'a geom::Path, &'a style::Fill), + Stroke(&'a geom::Path, &'a style::Stroke), } pub fn render_rich_text_with( @@ -55,28 +55,36 @@ where } } - if span.props.underline { + if span.props.decorations.underline { let line = shape.metrics.uline; - let path = - line_path(span.bbox(), shape.y_baseline, line, glyph_builder); + let path = crate::line_path( + span.bbox(), + shape.y_baseline, + line, + glyph_builder, + ); span_builder.push_path(&path); glyph_builder = path.clear(); } - if span.props.strikeout { + if span.props.decorations.strikethrough { let line = shape.metrics.strikeout; - let path = - line_path(span.bbox(), shape.y_baseline, line, glyph_builder); + let path = crate::line_path( + span.bbox(), + shape.y_baseline, + line, + glyph_builder, + ); span_builder.push_path(&path); glyph_builder = path.clear(); } if let Some(path) = span_builder.finish() { - if let Some(c) = span.props.color.as_ref() { - let prim = RichPrimitive::Fill(&path, c.clone()); + if let Some(fill) = span.props.render.fill.as_ref() { + let prim = RichPrimitive::Fill(&path, fill); render_fn(prim); } - if let Some((c, thickness)) = span.props.outline.as_ref() { - let prim = RichPrimitive::Stroke(&path, c.clone(), *thickness); + if let Some(outline) = span.props.render.outline.as_ref() { + let prim = RichPrimitive::Stroke(&path, outline); render_fn(prim); } span_builder = path.clear(); @@ -102,33 +110,36 @@ pub fn render_rich_text( pixmap: &mut tiny_skia::PixmapMut<'_>, ) -> Result<(), crate::Error> { let render_fn = |primitive: RichPrimitive| match primitive { - RichPrimitive::Fill(path, color) => { + RichPrimitive::Fill(path, fill) => { let mut paint = tiny_skia::Paint::default(); - paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a()); + match fill { + style::Fill::Solid { color, opacity } => { + let a = if let Some(opacity) = opacity { + (color.a() as f32 * opacity).round() as u8 + } else { + color.a() + }; + paint.set_color_rgba8(color.r(), color.g(), color.b(), a); + } + } pixmap.fill_path(path, &paint, tiny_skia::FillRule::Winding, transform, mask); } - RichPrimitive::Stroke(path, color, width) => { + RichPrimitive::Stroke(path, outline) => { let mut paint = tiny_skia::Paint::default(); - paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a()); + paint.set_color_rgba8( + outline.color.r(), + outline.color.g(), + outline.color.b(), + outline.color.a(), + ); let mut stroke = tiny_skia::Stroke::default(); - stroke.width = width; + stroke.width = outline.width; + if let Some(pattern) = outline.pattern.get_dash() { + let array = pattern.iter().map(|d| d * stroke.width).collect(); + stroke.dash = tiny_skia::StrokeDash::new(array, 0.0); + } pixmap.stroke_path(path, &paint, &stroke, transform, mask); } }; render_rich_text_with(text, fontdb, render_fn) } - -fn line_path( - rect: geom::Rect, - y_baseline: f32, - line: font::ScaledLineMetrics, - mut builder: geom::PathBuilder, -) -> geom::Path { - // there is no y-flip transform on this one - builder.move_to(rect.left(), y_baseline - line.position); - builder.line_to(rect.right(), y_baseline - line.position); - builder.line_to(rect.right(), y_baseline - line.position + line.thickness); - builder.line_to(rect.left(), y_baseline - line.position + line.thickness); - builder.close(); - builder.finish().unwrap() -} diff --git a/text/src/sd.rs b/text/src/sd.rs index 33b3ae20..91c31c81 100644 --- a/text/src/sd.rs +++ b/text/src/sd.rs @@ -1,9 +1,11 @@ use std::borrow::Cow; +use std::str::FromStr; +use plotive_base::{Rgba8, style}; use serde::de::Error; -use serde::ser::{SerializeMap, SerializeSeq}; +use serde::ser::SerializeMap; -use crate::font; +use crate::{font, props}; impl serde::Serialize for font::Family { fn serialize(&self, serializer: S) -> Result @@ -222,67 +224,58 @@ impl<'de> serde::de::Deserialize<'de> for font::Width { } } -struct Outline(C, f32); - -impl serde::Serialize for Outline -where - C: serde::Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_seq(Some(2))?; - state.serialize_element(&self.0)?; - state.serialize_element(&self.1)?; - state.end() - } -} - -impl serde::Serialize for crate::rich::ClassProps +impl serde::Serialize for props::TextModifiers where - C: serde::Serialize + Copy + Clone, + C: serde::Serialize + style::DefaultStroke + style::DefaultStrokeWidth + PartialEq, { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_map(None)?; - if let Some(families) = &self.font_family { + if let Some(families) = &self.families { let family_str = crate::font::font_families_to_string(families); - state.serialize_entry("fontFamily", &family_str)?; + state.serialize_entry("family", &family_str)?; } - if let Some(weight) = &self.font_weight { - state.serialize_entry("fontWeight", weight)?; + if let Some(weight) = &self.weight { + state.serialize_entry("weight", weight)?; } - if let Some(width) = &self.font_width { - state.serialize_entry("fontWidth", width)?; + if let Some(width) = &self.width { + state.serialize_entry("width", width)?; } - if let Some(style) = &self.font_style { - state.serialize_entry("fontStyle", style)?; + if let Some(style) = &self.style { + state.serialize_entry("style", style)?; } - if let Some(size) = &self.font_size { - state.serialize_entry("fontSize", size)?; + if let Some(size) = &self.size { + state.serialize_entry("size", size)?; } if let Some(color) = &self.color { state.serialize_entry("color", color)?; } - if let Some((outline, width)) = &self.outline { - state.serialize_entry("outline", &Outline(outline, *width))?; + if let Some(outline) = &self.outline { + state.serialize_entry("outline", outline)?; } if let Some(underline) = &self.underline { state.serialize_entry("underline", underline)?; } - if let Some(strikeout) = &self.strikeout { - state.serialize_entry("strikeout", strikeout)?; + if let Some(strikethrough) = &self.strikethrough { + state.serialize_entry("strikethrough", strikethrough)?; } state.end() } } -impl<'de, C> serde::de::Deserialize<'de> for crate::rich::ClassProps +impl<'de, C> serde::de::Deserialize<'de> for props::TextModifiers where - C: serde::de::Deserialize<'de> + Copy + Clone, + C: serde::de::Deserialize<'de> + + Copy + + Clone + + style::DefaultColor + + style::DefaultStroke + + style::DefaultStrokeWidth + + FromStr + + From, + ::Err: std::fmt::Display, { fn deserialize(deserializer: D) -> Result where @@ -292,12 +285,20 @@ where impl<'de, C> serde::de::Visitor<'de> for Visitor where - C: serde::de::Deserialize<'de> + Copy + Clone, + C: serde::de::Deserialize<'de> + + Copy + + Clone + + style::DefaultColor + + style::DefaultStroke + + style::DefaultStrokeWidth + + FromStr + + From, + ::Err: std::fmt::Display, { - type Value = crate::rich::ClassProps; + type Value = props::TextModifiers; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a map representing ClassProps") + formatter.write_str("a map representing TextModifiers") } fn visit_map(self, mut map: M) -> Result @@ -305,10 +306,10 @@ where M: serde::de::MapAccess<'de>, M::Error: serde::de::Error, { - let mut props = crate::rich::ClassProps::::default(); + let mut props = props::TextModifiers::::default(); while let Some(key) = map.next_key::()? { match key.as_str() { - "fontFamily" => { + "family" => { let family_str: String = map.next_value()?; let value = font::parse_font_families(&family_str).map_err(|err| { M::Error::custom(format!( @@ -316,53 +317,53 @@ where family_str, err )) })?; - props.font_family = Some(value); + props.families = Some(value); } - "fontWeight" => { + "weight" => { let value: font::Weight = map.next_value()?; - props.font_weight = Some(value); + props.weight = Some(value); } - "fontWidth" => { + "width" => { let value: font::Width = map.next_value()?; - props.font_width = Some(value); + props.width = Some(value); } - "fontStyle" => { + "style" => { let value: font::Style = map.next_value()?; - props.font_style = Some(value); + props.style = Some(value); } - "fontSize" => { + "size" => { let value: f32 = map.next_value()?; - props.font_size = Some(value); + props.size = Some(value); } "color" => { - let value: C = map.next_value()?; + let value: Option> = map.next_value()?; props.color = Some(value); } "outline" => { - let (outline, width): (C, f32) = map.next_value()?; - props.outline = Some((outline, width)); + let value: Option> = map.next_value()?; + props.outline = Some(value); } "underline" => { let value: bool = map.next_value()?; props.underline = Some(value); } - "strikeout" => { + "strikethrough" | "strikeout" => { let value: bool = map.next_value()?; - props.strikeout = Some(value); + props.strikethrough = Some(value); } other => { return Err(M::Error::unknown_field( other, &[ - "fontFamily", - "fontWeight", - "fontWidth", - "fontStyle", - "fontSize", + "family", + "weight", + "width", + "style", + "size", "color", "outline", "underline", - "strikeout", + "strikethrough", ], )); }