From 8b575032a8004deb3220c5ca2a3726ccf5b69bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Sun, 28 Jun 2026 11:46:02 +0200 Subject: [PATCH 1/6] more consistent support for rich text in des --- Cargo.lock | 17 +- Cargo.toml | 16 +- examples/bode_rlc.rs | 7 +- examples/gauss.rs | 6 +- src/des.rs | 248 ++++++++------------ src/des/axis.rs | 16 +- src/des/colorbar.rs | 16 +- src/des/figure.rs | 15 +- src/des/plot.rs | 10 +- src/des/sd.rs | 81 ++++--- src/des/sd/axis.rs | 54 +---- src/des/sd/colorbar.rs | 52 +---- src/des/sd/plot.rs | 4 +- src/drawing/annot.rs | 4 +- src/drawing/axis.rs | 98 ++++++-- src/drawing/axis/side.rs | 2 +- src/drawing/colorbar.rs | 11 +- src/drawing/figure.rs | 6 +- src/drawing/plot.rs | 155 +++++++++++-- src/drawing/series.rs | 3 +- src/dsl.rs | 5 +- src/lib.rs | 2 + src/style/defaults.rs | 2 +- text/Cargo.toml | 11 +- text/examples/text_rich.rs | 4 +- text/examples/text_rich_parse.rs | 2 +- text/src/lib.rs | 10 + text/src/rich.rs | 62 ++--- text/src/rich/builder.rs | 14 +- text/src/rich/parse.rs | 33 +-- text/src/rich/render.rs | 2 +- text/src/sd.rs | 377 +++++++++++++++++++++++++++++++ 32 files changed, 883 insertions(+), 462 deletions(-) create mode 100644 text/src/sd.rs diff --git a/Cargo.lock b/Cargo.lock index 80e807bd..02b9107e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3029,7 +3029,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotive" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "noyalib", "ode_solvers", @@ -3049,7 +3049,7 @@ dependencies = [ [[package]] name = "plotive-base" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "serde", "serde_json", @@ -3059,14 +3059,14 @@ dependencies = [ [[package]] name = "plotive-dsl" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "miette", ] [[package]] name = "plotive-iced" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "arboard", "bytes", @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "plotive-pxl" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "plotive", "png 0.17.16", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "plotive-svg" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "plotive", "rustybuzz", @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "plotive-tests" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "plotive", "plotive-pxl", @@ -3119,13 +3119,14 @@ dependencies = [ [[package]] name = "plotive-text" -version = "0.5.0" +version = "0.6.0-dev" dependencies = [ "fontconfig-parser", "log", "memmap2", "plotive-base", "rustybuzz", + "serde", "slotmap", "tiny-skia", "tiny-skia-path", diff --git a/Cargo.toml b/Cargo.toml index b50d9103..c0a58377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ noto-sans = ["plotive-text/noto-sans"] noto-sans-italic = ["plotive-text/noto-sans-italic"] noto-serif = ["plotive-text/noto-serif"] noto-serif-italic = ["plotive-text/noto-serif-italic"] -serde = ["plotive-base/serde", "dep:serde", "dep:serde-value"] +serde = ["plotive-base/serde", "plotive-text/serde", "dep:serde", "dep:serde-value"] time = [] utils = [] @@ -114,7 +114,7 @@ members = ["base", "dsl", "iced", "pxl", "svg", "text", "tests"] resolver = "3" [workspace.package] -version = "0.5.0" +version = "0.6.0-dev" authors = ["Rémi THEBAULT"] description = "Simple data plotting library" edition = "2024" @@ -124,12 +124,12 @@ categories = ["science", "graphics"] keywords = ["data", "visualization", "plotting"] [workspace.dependencies] -plotive = { version = "0.5.0", path = "." } -plotive-base = { version = "0.5.0", path = "base" } -plotive-dsl = { version = "0.5.0", path = "dsl" } -plotive-pxl = { version = "0.5.0", path = "pxl" } -plotive-svg = { version = "0.5.0", path = "svg" } -plotive-text = { version = "0.5.0", path = "text" } +plotive = { version = "0.6.0-dev", path = "." } +plotive-base = { version = "0.6.0-dev", path = "base" } +plotive-dsl = { version = "0.6.0-dev", path = "dsl" } +plotive-pxl = { version = "0.6.0-dev", path = "pxl" } +plotive-svg = { version = "0.6.0-dev", path = "svg" } +plotive-text = { version = "0.6.0-dev", path = "text" } arboard = "3.6.1" bytes = "1.6" iced = { version = "0.14.0", features = [ diff --git a/examples/bode_rlc.rs b/examples/bode_rlc.rs index 077cdb87..f66e2b41 100644 --- a/examples/bode_rlc.rs +++ b/examples/bode_rlc.rs @@ -1,6 +1,6 @@ use std::f64::consts::PI; -use plotive::{data, des, style, text, utils}; +use plotive::{data, des, style, utils}; mod common; @@ -52,11 +52,10 @@ fn main() { (100.0, "mag3", "phase3", "R = 100 Ω"), ]; - let title = text::parse_rich_text::(concat!( + let title = [concat!( "Bode diagram of RLC circuit\n", "[size=18;italic;font=serif]L = 0.1 mH / C = 1 µF[/size;italic;font]" - )) - .unwrap(); + )]; // magnitude X axis scale is taken from the phase X axis // the reference uses the title given to the phase X axis diff --git a/examples/gauss.rs b/examples/gauss.rs index b6adc719..f126b3a7 100644 --- a/examples/gauss.rs +++ b/examples/gauss.rs @@ -26,9 +26,7 @@ fn main() { let normal = Normal::new(MU, SIGMA).unwrap(); let pop = (0..N_POP).map(|_| normal.sample(&mut rng)).collect(); - let title: des::figure::Title = - format!("Normal distribution (\u{03bc}={}, \u{03c3}={})", MU, SIGMA).into(); - + let title = format!("Normal distribution (\u{03bc}={}, \u{03c3}={})", MU, SIGMA); let ticks = vec![5.0, 9.0, 11.0, 13.0, 15.0, 17.0, 21.0]; let x_axis = des::Axis::new() @@ -66,7 +64,7 @@ fn main() { .with_y_axis(y_axis) .with_legend(des::plot::LegendPos::OutRight.into()); - let fig = des::Figure::new(plot.into()).with_title(title); + let fig = des::Figure::new(plot.into()).with_title(title.into()); let data_source = data::TableSource::new().with_f64_column("pop".into(), pop); diff --git a/src/des.rs b/src/des.rs index 0458e386..40cfdf1e 100644 --- a/src/des.rs +++ b/src/des.rs @@ -23,6 +23,99 @@ pub use legend::Legend; pub use plot::{Plot, PlotLegend, Subplots}; pub use series::{DataCol, Series, data_inline, data_src_ref}; +use crate::style::theme; +use crate::text; + +/// Text content for titles, labels, legends, etc. +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + /// Plain text + Plain(String), + /// Rich text, the format string is parsed to produce a rich text, using the standard classes + Rich(String), + /// Rich text, the format string is parsed to produce a rich text, + /// and the non-standard classes can be used to define the properties of the spans + RichWithClasses { + /// 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::ClassProps)>, + }, +} + +impl Text { + pub(crate) fn to_rich_text( + &self, + base: text::rich::TextProps, + layout: text::rich::Layout, + db: &text::fontdb::Database, + ) -> std::result::Result, text::Error> { + match self { + Text::Plain(text) => { + let builder = text::RichTextBuilder::new(text.clone(), base).with_layout(layout); + builder.done(db) + } + Text::Rich(fmt) => { + let parsed_text = text::parse_rich_text::(fmt)?; + let builder = parsed_text.into_builder(base).with_layout(layout); + builder.done(db) + } + Text::RichWithClasses { fmt, classes } => { + let parsed_text = text::parse_rich_text_with_classes(fmt, &classes)?; + let builder = parsed_text.into_builder(base).with_layout(layout); + builder.done(db) + } + } + } +} + +impl From for Text { + fn from(s: String) -> Self { + Text::Plain(s) + } +} + +impl From<&str> for Text { + fn from(s: &str) -> Self { + Text::Plain(s.to_string()) + } +} + +impl From<[String; 1]> for Text { + fn from(arr: [String; 1]) -> Self { + let mut arr = arr; + let fmt = std::mem::take(&mut arr[0]); + Text::Rich(fmt) + } +} + +impl From<[&str; 1]> for Text { + fn from(arr: [&str; 1]) -> Self { + Text::Rich(arr[0].to_string()) + } +} + +impl From<(String,)> for Text { + fn from(tuple: (String,)) -> Self { + Text::Rich(tuple.0) + } +} + +impl From<(&str,)> for Text { + fn from(tuple: (&str,)) -> Self { + Text::Rich(tuple.0.to_string()) + } +} + +impl From<(String, Vec<(String, text::ClassProps)>)> for Text { + fn from(tuple: (String, Vec<(String, text::ClassProps)>)) -> Self { + Text::RichWithClasses { + fmt: tuple.0, + classes: tuple.1, + } + } +} + /// Index of a plot in a subplot grid #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PlotIdx { @@ -107,158 +200,3 @@ impl Iterator for PlotIdxIter { impl std::iter::FusedIterator for PlotIdxIter {} -// Structs defined with this macro use theme::Color for the generic color of rich properties -// Caller must impl a specific Default for the $props_struct. -macro_rules! define_rich_text_structs { - ($text_struct:ident, $props_struct:ident, $opt_props_struct:ident) => { - /// Rich text properties that can apply only some properties on a given text span - pub type $opt_props_struct = $crate::text::rich::TextOptProps<$crate::style::theme::Color>; - - /// Rich text base properties with plotive theme colors - #[derive(Debug, Clone, PartialEq)] - pub struct $props_struct($crate::text::rich::TextProps<$crate::style::theme::Color>); - - impl $props_struct { - fn new(font_size: f32) -> Self { - Self( - $crate::text::rich::TextProps::new(font_size) - .with_font($crate::style::defaults::FONT_FAMILY.parse().unwrap()), - ) - } - - /// Set the font properties and return self for chaining - pub fn with_font(self, font: $crate::text::font::Font) -> Self { - Self(self.0.with_font(font)) - } - - /// Set the text fill color and return self for chaining - pub fn with_fill(self, fill: Option<$crate::style::theme::Color>) -> Self { - Self(self.0.with_fill(fill)) - } - - /// Set the outline properties and return self for chaining - pub fn with_outline(self, outline: ($crate::style::theme::Color, f32)) -> Self { - Self(self.0.with_outline(outline)) - } - - /// Set underline to true and return self for chaining - pub fn with_underline(self) -> Self { - Self(self.0.with_underline()) - } - - /// Set strikeout to true and return self for chaining - pub fn with_strikeout(self) -> Self { - Self(self.0.with_strikeout()) - } - - /// Get the font size - pub fn font_size(&self) -> f32 { - self.0.font_size() - } - - /// Get the font - pub fn font(&self) -> &$crate::text::font::Font { - self.0.font() - } - - /// Get the fill color - pub fn fill(&self) -> Option<$crate::style::theme::Color> { - self.0.fill() - } - - /// Get the outline properties - pub fn outline(&self) -> Option<($crate::style::theme::Color, f32)> { - self.0.outline() - } - - /// Check if strikeout is enabled - pub fn underline(&self) -> bool { - self.0.underline() - } - } - - /// Rich text structure with plotive theme colors - #[derive(Debug, Clone, PartialEq)] - pub struct $text_struct { - text: String, - props: $props_struct, - spans: Vec<(usize, usize, $opt_props_struct)>, - } - - impl From for $text_struct { - fn from(text: String) -> Self { - $text_struct { - text, - props: $props_struct::default(), - spans: Vec::new(), - } - } - } - - impl From<&str> for $text_struct { - fn from(text: &str) -> Self { - $text_struct { - text: text.to_string(), - props: $props_struct::default(), - spans: Vec::new(), - } - } - } - - impl From<$crate::text::ParsedRichText<$crate::style::theme::Color>> for $text_struct { - fn from(text: $crate::text::ParsedRichText<$crate::style::theme::Color>) -> Self { - $text_struct { - text: text.text, - props: $props_struct::default(), - spans: text.prop_spans, - } - } - } - - impl $text_struct { - /// Set the base properties and return self for chaining - pub fn with_props(self, props: $props_struct) -> Self { - Self { props, ..self } - } - - /// Set the spans and return self for chaining - pub fn with_spans(self, spans: Vec<(usize, usize, $opt_props_struct)>) -> Self { - Self { spans, ..self } - } - - /// Get the text content - pub fn text(&self) -> &str { - &self.text - } - - /// Get the base properties - pub fn props(&self) -> &$props_struct { - &self.props - } - - /// Get the spans - pub fn spans(&self) -> &[(usize, usize, $opt_props_struct)] { - &self.spans - } - - pub(crate) fn to_rich_text( - &self, - layout: $crate::text::rich::Layout, - db: &$crate::text::fontdb::Database, - ) -> std::result::Result< - $crate::text::RichText<$crate::style::theme::Color>, - $crate::text::Error, - > { - let mut builder = - $crate::text::RichTextBuilder::new(self.text.clone(), self.props.0.clone()) - .with_layout(layout); - for (start, end, props) in &self.spans { - builder.add_span(*start, *end, props.clone()); - } - builder.done(db) - } - } - }; -} - -pub(self) use define_rich_text_structs; diff --git a/src/des/axis.rs b/src/des/axis.rs index 7fb82bdc..1ca63dbe 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -7,15 +7,7 @@ pub use ticks::{Grid, MinorGrid, MinorTicks, Ticks, TicksFont}; -use crate::style::defaults; - -super::define_rich_text_structs!(Title, TitleProps, TitleOptProps); - -impl Default for TitleProps { - fn default() -> Self { - TitleProps::new(defaults::AXIS_LABEL_FONT_SIZE) - } -} +use super::Text; /// Side of the axis in the plot, applies to both X and Y axes. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -73,7 +65,7 @@ pub fn ref_id(id: impl Into) -> Ref { #[derive(Debug, Clone, PartialEq)] pub struct Axis { id: Option, - title: Option, + title: Option<Text>, side: Side, scale: Scale, ticks: Option<Ticks>, @@ -118,7 +110,7 @@ impl Axis { } /// Set the title of this axis and return self for chaining - pub fn with_title(self, title: Title) -> Self { + pub fn with_title(self, title: Text) -> Self { Self { title: Some(title), ..self @@ -182,7 +174,7 @@ impl Axis { } /// Get the title of this axis, if any - pub fn title(&self) -> Option<&Title> { + pub fn title(&self) -> Option<&Text> { self.title.as_ref() } diff --git a/src/des/colorbar.rs b/src/des/colorbar.rs index 12ac64dc..16660846 100644 --- a/src/des/colorbar.rs +++ b/src/des/colorbar.rs @@ -1,16 +1,8 @@ //! Color bar configuration -use crate::des::axis; +use crate::des::{axis, Text}; use crate::style::{defaults, theme}; use crate::text; -super::define_rich_text_structs!(Title, TitleProps, TitleOptProps); - -impl Default for TitleProps { - fn default() -> Self { - TitleProps::new(defaults::COLORBAR_TITLE_FONT_SIZE) - } -} - /// Position of a color bar relatively to the plot #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Pos { @@ -52,7 +44,7 @@ pub struct ColorBar { // pub(crate) for serde implementation pub(crate) pos: Pos, width: f32, - title: Option<Title>, + title: Option<Text>, ticks_font: TicksFont, border: Option<theme::Stroke>, locator: axis::ticks::Locator, @@ -94,7 +86,7 @@ impl ColorBar { } /// Set the title text and return self for chaining - pub fn with_title(mut self, title: Title) -> Self { + pub fn with_title(mut self, title: Text) -> Self { self.title = Some(title); self } @@ -134,7 +126,7 @@ impl ColorBar { } /// Get the title text of the color bar, if it has one - pub fn title(&self) -> Option<&Title> { + pub fn title(&self) -> Option<&Text> { self.title.as_ref() } diff --git a/src/des/figure.rs b/src/des/figure.rs index 2ac8f96d..d52d3c27 100644 --- a/src/des/figure.rs +++ b/src/des/figure.rs @@ -1,18 +1,11 @@ //! Figure design structures use std::iter::FusedIterator; +use super::Text; use crate::des::{Legend, Plot, PlotIdx, Subplots}; use crate::geom; use crate::style::{defaults, theme}; -super::define_rich_text_structs!(Title, TitleProps, TitleOptProps); - -impl Default for TitleProps { - fn default() -> Self { - TitleProps::new(defaults::TITLE_FONT_SIZE) - } -} - /// Position of the legend relatively to the figure #[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum LegendPos { @@ -48,7 +41,7 @@ impl From<LegendPos> for FigLegend { pub struct Figure { plots: Plots, - title: Option<Title>, + title: Option<Text>, size: geom::Size, legend: Option<FigLegend>, fill: Option<theme::Fill>, @@ -70,7 +63,7 @@ impl Figure { } /// Set the title and return self for chaining - pub fn with_title(self, title: Title) -> Self { + pub fn with_title(self, title: Text) -> Self { Figure { title: Some(title), ..self @@ -107,7 +100,7 @@ impl Figure { } /// Get the title of the figure - pub fn title(&self) -> Option<&Title> { + pub fn title(&self) -> Option<&Text> { self.title.as_ref() } diff --git a/src/des/plot.rs b/src/des/plot.rs index 62f0863a..1f8392ac 100644 --- a/src/des/plot.rs +++ b/src/des/plot.rs @@ -1,6 +1,6 @@ //! Plot design structures -use crate::des::{Annotation, Axis, ColorBar, Legend, PlotIdx, Series}; +use crate::des::{Annotation, Axis, ColorBar, Legend, PlotIdx, Series, Text}; use crate::style::{defaults, theme}; /// Border style for the plot area that draws a box all around the plot area @@ -193,7 +193,7 @@ pub struct Plot { y_axes: Vec<Axis>, x_axis_set: bool, y_axis_set: bool, - title: Option<String>, + title: Option<Text>, fill: Option<theme::Fill>, border: Option<Border>, insets: Option<Insets>, @@ -246,7 +246,7 @@ impl Plot { } /// Set the title of the plot and return self for chaining - pub fn with_title(self, title: String) -> Self { + pub fn with_title(self, title: Text) -> Self { Self { title: Some(title), ..self @@ -309,8 +309,8 @@ impl Plot { } /// Get the title of the plot - pub fn title(&self) -> Option<&str> { - self.title.as_deref() + pub fn title(&self) -> Option<&Text> { + self.title.as_ref() } /// Get the fill of the plot area diff --git a/src/des/sd.rs b/src/des/sd.rs index 83664487..448aadc8 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,9 +1,9 @@ //! Serialization and deserialization of figures -use serde::ser::SerializeStruct; +use serde::ser::{SerializeSeq, SerializeStruct}; use super::Figure; -use crate::des::{FigLegend, Plot, Subplots, figure}; +use crate::des::{FigLegend, Plot, Subplots, Text, figure}; use crate::geom; use crate::style::{defaults, theme}; @@ -17,58 +17,85 @@ mod series; mod style; #[cfg(feature = "time")] mod time; -// MARK: figure::Title -impl serde::Serialize for figure::Title { +use crate::text; + +// MARK: Text + +impl serde::Serialize for Text { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { - if self.spans().is_empty() && self.props() == &figure::TitleProps::default() { - self.text().serialize(serializer) - } else { - let mut state = serializer.serialize_struct("Title", 2)?; - state.serialize_field("text", self.text())?; - todo!("Serialize rich props and spans") - //state.end() + match self { + Text::Plain(text) => serializer.serialize_str(text), + Text::Rich(fmt) => { + let mut seq = serializer.serialize_seq(Some(1))?; + seq.serialize_element(fmt)?; + seq.end() + } + Text::RichWithClasses { fmt, classes } => { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(fmt)?; + seq.serialize_element(classes)?; + seq.end() + } } } } -impl<'de> serde::Deserialize<'de> for figure::Title { +struct ClassPropsMap(Vec<(String, text::ClassProps)>); + +impl<'de> serde::Deserialize<'de> for ClassPropsMap { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { - struct TitleVisitor; + let map = std::collections::HashMap::<String, text::ClassProps>::deserialize(deserializer)?; + Ok(ClassPropsMap(map.into_iter().collect())) + } +} - impl<'de> serde::de::Visitor<'de> for TitleVisitor { - type Value = figure::Title; +impl<'de> serde::Deserialize<'de> for Text { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct TextVisitor; + + impl<'de> serde::de::Visitor<'de> for TextVisitor { + type Value = Text; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a figure title string or rich text") + formatter.write_str("a string or a rich text array") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { - Ok(figure::Title::from(value.to_string())) + Ok(Text::Plain(value.to_string())) } - fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where - A: serde::de::MapAccess<'de>, + A: serde::de::SeqAccess<'de>, { - deserialize_map_fields!('de, map, - "text" => text: Option<String>, - ); - Ok(figure::Title::from( - text.ok_or_else(|| serde::de::Error::missing_field("text"))?, - )) + let fmt: String = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + let classes: Option<ClassPropsMap> = seq.next_element()?; + if let Some(classes) = classes { + Ok(Text::RichWithClasses { + fmt, + classes: classes.0, + }) + } else { + Ok(Text::Rich(fmt)) + } } } - deserializer.deserialize_any(TitleVisitor) + deserializer.deserialize_any(TextVisitor) } } @@ -140,7 +167,7 @@ impl<'de> serde::de::Visitor<'de> for FigureVisitor { "plots" => plots: Option<Subplots>, "space" => space: Option<f32>, "size" => size: Option<geom::Size>, - "title" => title: Option<figure::Title>, + "title" => title: Option<Text>, "fill" => fill: Option<Option<theme::Fill>>, "legend" => legend: Option<FigLegend>, "padding" => padding: Option<geom::Padding>, diff --git a/src/des/sd/axis.rs b/src/des/sd/axis.rs index 49a66a69..5ecbcac2 100644 --- a/src/des/sd/axis.rs +++ b/src/des/sd/axis.rs @@ -7,59 +7,9 @@ use serde::{Deserializer, Serializer}; use serde_value::Value; use crate::des::sd::{deserialize_map_fields, deserialize_tagged_map_fields}; -use crate::des::{axis, sd}; +use crate::des::{self, axis, sd}; use crate::style::theme; -// MARK: axis::Title - -impl serde::Serialize for axis::Title { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - if self.spans().is_empty() && self.props() == &axis::TitleProps::default() { - self.text().serialize(serializer) - } else { - let mut state = serializer.serialize_struct("Title", 2)?; - state.serialize_field("text", self.text())?; - todo!("Serialize rich props and spans") - //state.end() - } - } -} - -impl<'de> serde::Deserialize<'de> for axis::Title { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - struct TitleVisitor; - - impl<'de> serde::de::Visitor<'de> for TitleVisitor { - type Value = axis::Title; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an axis title string or rich text") - } - - fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> - where - E: serde::de::Error, - { - Ok(axis::Title::from(value.to_string())) - } - - fn visit_map<A>(self, _map: A) -> Result<Self::Value, A::Error> - where - A: serde::de::MapAccess<'de>, - { - todo!("Deserialize rich title with props and spans") - } - } - deserializer.deserialize_any(TitleVisitor) - } -} - // MARK: axis::Ref impl serde::Serialize for axis::Ref { @@ -1420,7 +1370,7 @@ where deserialize_map_fields!( 'de, map, "id" => id: Option<String>, - "title" => title: Option<axis::Title>, + "title" => title: Option<des::Text>, "side" => side: Option<String>, "scale" => scale: Option<axis::Scale>, "ticks" => ticks: Option<axis::Ticks>, diff --git a/src/des/sd/colorbar.rs b/src/des/sd/colorbar.rs index 01c66f54..d8d8c5dd 100644 --- a/src/des/sd/colorbar.rs +++ b/src/des/sd/colorbar.rs @@ -3,57 +3,9 @@ use serde::ser::SerializeStruct; use serde::{Deserializer, Serializer}; use crate::des::axis::ticks; -use crate::des::colorbar; +use crate::des::{self, colorbar}; use crate::style::theme; -impl serde::Serialize for colorbar::Title { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - if self.spans().is_empty() && self.props() == &colorbar::TitleProps::default() { - self.text().serialize(serializer) - } else { - let mut state = serializer.serialize_struct("Title", 2)?; - state.serialize_field("text", self.text())?; - todo!("Serialize rich props and spans") - //state.end() - } - } -} - -impl<'de> serde::Deserialize<'de> for colorbar::Title { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - struct TitleVisitor; - - impl<'de> serde::de::Visitor<'de> for TitleVisitor { - type Value = colorbar::Title; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an axis title string or rich text") - } - - fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> - where - E: serde::de::Error, - { - Ok(colorbar::Title::from(value.to_string())) - } - - fn visit_map<A>(self, _map: A) -> Result<Self::Value, A::Error> - where - A: serde::de::MapAccess<'de>, - { - todo!("Deserialize rich title with props and spans") - } - } - deserializer.deserialize_any(TitleVisitor) - } -} - impl serde::Serialize for colorbar::Pos { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -169,7 +121,7 @@ impl<'de> serde::Deserialize<'de> for colorbar::ColorBar { 'de, map, "pos" => pos: Option<colorbar::Pos>, "width" => width: Option<f32>, - "title" => title: Option<colorbar::Title>, + "title" => title: Option<des::Text>, "border" => border: Option<Option<theme::Stroke>>, "ticks" => ticks: Option<ticks::Locator>, "margin" => margin: Option<f32>, diff --git a/src/des/sd/plot.rs b/src/des/sd/plot.rs index f44c8c61..5817d4b2 100644 --- a/src/des/sd/plot.rs +++ b/src/des/sd/plot.rs @@ -4,7 +4,7 @@ use serde_value::Value; use crate::des::sd::axis::{DeXAxis, DeYAxis}; use crate::des::sd::{self, deserialize_map_fields, deserialize_tagged_map_fields}; -use crate::des::{Annotation, Plot, PlotLegend, Subplots, axis, colorbar, plot, series}; +use crate::des::{Annotation, Plot, PlotLegend, Subplots, Text, axis, colorbar, plot, series}; use crate::style::theme; // MARK: Plot @@ -195,7 +195,7 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { 'de, map, "subplot" => subplot: Option<(u32, u32)>, "series" => series: Option<DeSeries>, - "title" => title: Option<String>, + "title" => title: Option<Text>, "xAxis" => x_axis: Option<DeXAxis>, "yAxis" => y_axis: Option<DeYAxis>, "xAxes" => x_axes: Option<Vec<DeXAxis>>, diff --git a/src/drawing/annot.rs b/src/drawing/annot.rs index 4ed1b08e..e24b1794 100644 --- a/src/drawing/annot.rs +++ b/src/drawing/annot.rs @@ -3,8 +3,8 @@ use std::f32; use super::Ctx; use crate::des::annot::{Anchor, LineDir, ZPos}; use crate::des::{self}; -use crate::drawing::axis::Axis; -use crate::drawing::plot::{Axes, Orientation}; +use crate::drawing::axis::{Axis, Orientation}; +use crate::drawing::plot::Axes; use crate::drawing::{Text, marker}; use crate::style::{self, theme}; use crate::{Style, data, geom, render, text}; diff --git a/src/drawing/axis.rs b/src/drawing/axis.rs index b2a4ce8c..4d79fb9b 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; @@ -12,10 +13,33 @@ pub use side::Side; use crate::drawing::scale::{self, CoordMap}; use crate::drawing::{Categories, Ctx, Error, Text, ticks}; -use crate::style::theme; +use crate::style::{defaults, theme}; use crate::text::{self, font}; use crate::{Style, data, des, geom, missing_params, render}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Orientation { + X, + Y, +} + +/// Cache for a single axis used during setup +#[derive(Debug, Clone)] +pub struct AxisCache { + pub side: Side, + pub title: Option<Text>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AxisCacheKey { + pub plt_idx: usize, + pub ax_idx: usize, + pub orientation: Orientation, +} + +pub type AxisCacheMap = HashMap<AxisCacheKey, AxisCache>; + #[derive(Debug, Clone)] pub struct Axis { id: Option<String>, @@ -94,6 +118,13 @@ impl Axis { }, } } + + pub fn into_cache(self) -> AxisCache { + AxisCache { + side: self.side, + title: self.draw_opts.title, + } + } } /// Implement the scale for an axis @@ -314,28 +345,61 @@ impl<D> Ctx<'_, D> where D: data::Source + ?Sized, { + pub fn setup_axis_cache(&self, side: Side, des_axis: &des::Axis) -> Result<AxisCache, Error> { + let title = des_axis + .title() + .map(|title| { + title.to_rich_text( + text::rich::TextProps::new(defaults::AXIS_TITLE_FONT_SIZE) + .with_font(defaults::FONT_FAMILY.parse().unwrap()), + side.title_layout(), + self.fontdb(), + ) + }) + .transpose()? + .map(|rt| Text::from_rich_text(&rt, self.fontdb())) + .transpose()?; + + Ok(AxisCache { side, title }) + } + /// Estimate the height taken by a horizontal axis. /// It includes ticks marks, ticks labels and axis title. - /// This is the height without any additional margin - pub fn estimate_x_axes_height(&self, x_axes: &[des::Axis], side: des::axis::Side) -> f32 { + pub fn estimate_x_axes_height( + &self, + x_axes: &[des::Axis], + plt_idx: usize, + axis_cache: &AxisCacheMap, + side: des::axis::Side, + ) -> f32 { let mut height = 0.0; - for (idx, axis) in x_axes.iter().filter(|a| a.side() == side).enumerate() { - if idx != 0 { + for (side_idx, (ax_idx, axis)) in x_axes + .iter() + .enumerate() + .filter(|a| a.1.side() == side) + .enumerate() + { + if side_idx != 0 { height += missing_params::AXIS_MARGIN + missing_params::AXIS_SPINE_WIDTH; } if let Some(ticks) = axis.ticks() { if axis.has_tick_labels() { // ticks is only accounted for when there are labels // this allows to merge ticks of subplots with shared scales and zero inter-space - if idx != 0 { + if side_idx != 0 { height += missing_params::TICK_SIZE; } height += missing_params::TICK_SIZE; height += missing_params::TICK_LABEL_MARGIN + ticks.font().size; } } - if let Some(title) = axis.title() { - height += missing_params::AXIS_TITLE_MARGIN + title.props().font_size(); + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: Orientation::X, + }; + if let Some(title) = axis_cache.get(&key).and_then(|c| c.title.as_ref()) { + height += missing_params::AXIS_TITLE_MARGIN + title.height(); } } height @@ -344,18 +408,20 @@ where pub fn setup_axis( &self, des_axis: &des::Axis, + cache: AxisCache, bounds: &Bounds, - side: Side, size_along: f32, insets: &geom::Padding, shared_scale: Option<Rc<RefCell<AxisScale>>>, spine: Option<des::plot::Border>, ) -> Result<Axis, Error> { - let id = des_axis.id().map(|s| s.to_string()); - let title_text = des_axis.title().map(|t| t.text().to_string()); + let AxisCache { side, title } = cache; + let title_text = title.as_ref().map(|t| t.text.clone()); let uses_shared = shared_scale.is_some(); - let draw_opts = self.setup_axis_draw_opts(des_axis, side, uses_shared, spine)?; + let draw_opts = self.setup_axis_draw_opts(des_axis, title, uses_shared, spine)?; + + let id = des_axis.id().map(|s| s.to_string()); let scale = if let Some(scale) = shared_scale { scale @@ -616,16 +682,10 @@ where fn setup_axis_draw_opts( &self, des_axis: &des::Axis, - side: Side, + title: Option<Text>, uses_shared: bool, spine: Option<des::plot::Border>, ) -> Result<DrawOpts, Error> { - let title = des_axis - .title() - .map(|title| title.to_rich_text(side.title_layout(), &self.fontdb)) - .transpose()? - .map(|rich| Text::from_rich_text(&rich, &self.fontdb)) - .transpose()?; let ticks_labels = !uses_shared; let marks = des_axis.ticks().map(|ticks| TickMark { diff --git a/src/drawing/axis/side.rs b/src/drawing/axis/side.rs index c3f44882..e7ef7906 100644 --- a/src/drawing/axis/side.rs +++ b/src/drawing/axis/side.rs @@ -1,4 +1,4 @@ -use crate::drawing::plot::Orientation; +use crate::drawing::axis::Orientation; use crate::drawing::scale::CoordMap; use crate::{des, geom, missing_params, text}; diff --git a/src/drawing/colorbar.rs b/src/drawing/colorbar.rs index 550ab45e..d220ef7b 100644 --- a/src/drawing/colorbar.rs +++ b/src/drawing/colorbar.rs @@ -8,7 +8,7 @@ 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::theme; +use crate::style::{defaults, theme}; use crate::{Style, data, geom, missing_params, render, text}; /// A colorbar entry, used to populate one colorbar @@ -99,7 +99,14 @@ impl ColorBarBuilder { let title = des .title() - .map(|title| title.to_rich_text(side.title_layout(), ctx.fontdb())) + .map(|title| { + title.to_rich_text( + text::rich::TextProps::new(defaults::COLORBAR_TITLE_FONT_SIZE) + .with_font(defaults::FONT_FAMILY.parse().unwrap()), + side.title_layout(), + ctx.fontdb(), + ) + }) .transpose()? .map(|rt| Text::from_rich_text(&rt, ctx.fontdb())) .transpose()?; diff --git a/src/drawing/figure.rs b/src/drawing/figure.rs index f7d453eb..d6c6dc95 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::theme; +use crate::style::{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,7 +83,9 @@ where text::line::VerAlign::Hanging.into(), Default::default(), ); - let rich = fig_title.to_rich_text(layout, self.fontdb())?; + let base = text::rich::TextProps::new(defaults::TITLE_FONT_SIZE) + .with_font(defaults::FONT_FAMILY.parse().unwrap()); + let rich = fig_title.to_rich_text(base, layout, self.fontdb())?; let paths = super::Text::from_rich_text(&rich, self.fontdb())?; let anchor_x = rect.center_x(); diff --git a/src/drawing/plot.rs b/src/drawing/plot.rs index 18c4b902..9b379059 100644 --- a/src/drawing/plot.rs +++ b/src/drawing/plot.rs @@ -1,10 +1,13 @@ use std::cell::RefCell; +use std::collections::HashMap; use std::f32; use std::rc::Rc; use crate::des::{PlotIdx, annot, colorbar}; use crate::drawing::annot::Annot; -use crate::drawing::axis::{AsBoundRef, Axis, AxisScale, Bounds, Side}; +use crate::drawing::axis::{ + AsBoundRef, Axis, AxisCacheKey, AxisCacheMap, AxisScale, Bounds, Orientation, Side, +}; use crate::drawing::colorbar::{ColorBar, ColorBarBuilder, ColorScale}; use crate::drawing::legend::{Legend, LegendBuilder}; use crate::drawing::scale::CoordMapXy; @@ -82,12 +85,6 @@ impl Plot { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Orientation { - X, - Y, -} - #[derive(Debug, Clone)] pub(super) struct Axes { x: Vec<Axis>, @@ -161,6 +158,8 @@ struct PlotData { insets: geom::Padding, } +pub type AxisTextCacheMap = HashMap<AxisCacheKey, Option<String>>; + trait IrPlotExt { fn x_axes(&self) -> &[des::Axis]; fn y_axes(&self) -> &[des::Axis]; @@ -204,6 +203,7 @@ trait IrPlotsExt { or: Orientation, ax_ref: &des::axis::Ref, plt_idx: usize, + axes_text_cache: &AxisTextCacheMap, ) -> Option<(usize, &des::Axis)> { let mut fig_ax_idx = 0; for (pi, plot) in self.plots().enumerate() { @@ -217,9 +217,20 @@ trait IrPlotsExt { } } des::axis::Ref::Id(id) => { - if axis.id() == Some(id) || axis.title().map(|t| t.text()) == Some(id) { + if axis.id() == Some(id) { return Some((fig_ax_idx, axis)); } + let key = AxisCacheKey { + plt_idx: pi, + ax_idx: ai, + orientation: or, + }; + let cache = axes_text_cache.get(&key)?; + if let Some(title) = cache.as_ref() { + if title == id { + return Some((fig_ax_idx, axis)); + } + } } } fig_ax_idx += 1; @@ -286,13 +297,27 @@ where // PlotData contains all data that is not impacted by the size of axes let plot_data = self.setup_plot_data(des_plots, rect)?; + let mut axes_cache = self.setup_axes_cache(des_plots)?; + let mut axes_text_cache: HashMap<AxisCacheKey, Option<String>> = HashMap::new(); + for (key, cache) in axes_cache.iter() { + axes_text_cache.insert(key.clone(), cache.title.as_ref().map(|t| t.text.clone())); + } + // Estimate the space taken by all horizontal axes // Can be slightly wrong if font metrics height isn't exactly font size. // This will be fixed at end of the setup phase. - let bottom_heights = - self.calc_estimated_x_heights(des_plots, &plot_data, des::axis::Side::Main); - let top_heights = - self.calc_estimated_x_heights(des_plots, &plot_data, des::axis::Side::Opposite); + let bottom_heights = self.calc_estimated_x_heights( + des_plots, + &axes_cache, + &plot_data, + des::axis::Side::Main, + ); + let top_heights = self.calc_estimated_x_heights( + des_plots, + &axes_cache, + &plot_data, + des::axis::Side::Opposite, + ); let hor_space_height = bottom_heights.iter().sum::<f32>() + top_heights.iter().sum::<f32>() + des_plots.space() * (des_plots.rows() - 1) as f32; @@ -303,6 +328,8 @@ where Orientation::Y, des_plots, &plot_data, + &mut axes_cache, + &axes_text_cache, subplot_rect_height, )?; @@ -319,10 +346,31 @@ where if subplot_rect_width <= 0.0 || subplot_rect_height <= 0.0 { return Err(Error::NotEnoughSpace); } - let x_axes = - self.setup_orientation_axes(Orientation::X, des_plots, &plot_data, subplot_rect_width)?; + let x_axes = self.setup_orientation_axes( + Orientation::X, + des_plots, + &plot_data, + &mut axes_cache, + &axes_text_cache, + subplot_rect_width, + )?; // bottom heights were estimated, we can now calculate them accurately and rebuild the y-axes + // let's take back the axes cache as those didn't change + for (plt_idx, y_ax) in y_axes.into_iter().enumerate() { + if let Some(y_ax) = y_ax { + for (ax_idx, ax) in y_ax.0.into_iter().enumerate() { + if let Some(ax) = ax { + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: Orientation::Y, + }; + axes_cache.insert(key, ax.into_cache()); + } + } + } + } let bottom_heights = self.calc_x_heights(des_plots, &plot_data, &x_axes, des::axis::Side::Main); let top_heights = @@ -335,6 +383,8 @@ where Orientation::Y, des_plots, &plot_data, + &mut axes_cache, + &axes_text_cache, subplot_rect_height, )?; @@ -455,6 +505,7 @@ where let legend = self.setup_plot_legend(des_plot, avail_width)?; let colorbars = self.setup_plot_colorbars(des_plot)?; let insets = plot_insets(des_plot); + plot_data[idx] = Some(PlotData { series, legend, @@ -465,6 +516,40 @@ where Ok(plot_data) } + fn setup_axes_cache(&self, des_plots: &des::figure::Plots) -> Result<AxisCacheMap, Error> { + let mut axis_cache = HashMap::new(); + for (plt_idx, des_plot) in des_plots.iter().enumerate() { + let Some(des_plot) = des_plot else { continue }; + + for (ax_idx, des_ax) in des_plot.x_axes().iter().enumerate() { + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: Orientation::X, + }; + let cache = self.setup_axis_cache( + Side::from_or_des_side(Orientation::X, des_ax.side()), + des_ax, + )?; + axis_cache.insert(key, cache); + } + + for (ax_idx, des_ax) in des_plot.y_axes().iter().enumerate() { + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: Orientation::Y, + }; + let cache = self.setup_axis_cache( + Side::from_or_des_side(Orientation::Y, des_ax.side()), + des_ax, + )?; + axis_cache.insert(key, cache); + } + } + Ok(axis_cache) + } + fn setup_plot_series(&self, plot: &des::Plot) -> Result<Vec<Series>, Error> { plot.series() .iter() @@ -547,6 +632,7 @@ where fn calc_estimated_x_heights( &self, des_plots: &des::figure::Plots, + axis_cache: &AxisCacheMap, datas: &[Option<PlotData>], side: des::axis::Side, ) -> Vec<f32> { @@ -558,7 +644,8 @@ where let data = datas[plt_idx].as_ref().unwrap(); let mut height = x_plot_padding(side); - height += self.estimate_x_axes_height(des_plot.x_axes(), side); + height += + self.estimate_x_axes_height(des_plot.x_axes(), plt_idx, axis_cache, side); if let (Some(des_leg), Some(leg)) = (des_plot.legend(), data.legend.as_ref()) { if x_side_matches_out_legend_pos(side, des_leg.pos()) { height += leg.size().height() + des_leg.margin(); @@ -666,6 +753,8 @@ where or: Orientation, des_plots: &des::figure::Plots, datas: &[Option<PlotData>], + axes_cache: &mut AxisCacheMap, + axes_text_cache: &AxisTextCacheMap, size_along: f32, ) -> Result<Vec<Option<PlotAxes>>, Error> { let mut plot_axes = vec![None; des_plots.len()]; @@ -683,6 +772,7 @@ where let Some(des_plot) = des_plot else { continue }; let des_axes = des_plot.orientation_axes(or); + let mut axes = vec![None; des_axes.len()]; // track whether the main and opposite axes are directly attached to the plot area @@ -702,12 +792,20 @@ where // We also have to collect data bounds of series that refer to a shared axis // referring explicitly to `des_ax`. This is done in the inner loop with `des_ax2`. + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: or, + }; + + let title = axes_text_cache.get(&key).and_then(|t| t.as_deref()); let matcher = series::AxisMatcher { plt_idx, ax_idx, id: des_ax.id(), - title: des_ax.title().map(|t| t.text()), + title, }; + let mut bounds = None; for (plt_idx2, des_plot2) in des_plots.iter().enumerate() { @@ -719,11 +817,17 @@ where for (ax_idx2, des_ax2) in des_plot2.orientation_axes(or).iter().enumerate() { if let des::axis::Scale::Shared(ax_ref2) = des_ax2.scale() { if matcher.matches_ref(ax_ref2, plt_idx2)? { + let key = AxisCacheKey { + plt_idx: plt_idx2, + ax_idx: ax_idx2, + orientation: or, + }; + let title = axes_text_cache.get(&key).and_then(|t| t.as_deref()); let matcher = series::AxisMatcher { plt_idx: plt_idx2, ax_idx: ax_idx2, id: des_ax2.id(), - title: des_ax2.title().map(|t| t.text()), + title, }; bounds = Series::unite_bounds(or, series, bounds, &matcher, plt_idx2)?; @@ -753,8 +857,10 @@ where let ax = self.setup_axis( des_ax, + axes_cache + .remove(&key) + .expect("AxisCache should have been built for axis owning its scale"), &bounds, - Side::from_or_des_side(or, des_ax.side()), size_along, &datas[plt_idx].as_ref().unwrap().insets, None, @@ -787,7 +893,7 @@ where continue; }; let (fig_ax_idx, _) = des_plots - .orientation_find_axis(or, ax_ref, plt_idx) + .orientation_find_axis(or, ax_ref, plt_idx, axes_text_cache) .ok_or_else(|| Error::UnknownAxisRef(ax_ref.clone()))?; let info = ax_infos[fig_ax_idx] @@ -811,10 +917,19 @@ where (false, None) => None, }; + let key = AxisCacheKey { + plt_idx, + ax_idx, + orientation: or, + }; + let cache = axes_cache + .remove(&key) + .expect("AxisCache should have been built for all axes"); + let axis = self.setup_axis( des_ax, + cache, &info.0, - Side::from_or_des_side(or, des_ax.side()), size_along, &datas[plt_idx].as_ref().unwrap().insets, Some(info.1.clone()), diff --git a/src/drawing/series.rs b/src/drawing/series.rs index 398c1f60..c510bd65 100644 --- a/src/drawing/series.rs +++ b/src/drawing/series.rs @@ -3,10 +3,9 @@ use plotive_base::Rgb8; use plotive_base::geom::PathSegment; use scale::{CoordMap, CoordMapXy}; -use crate::drawing::axis::Bounds; +use crate::drawing::axis::{Bounds, Orientation}; use crate::drawing::cmap::AsColorMap; use crate::drawing::colorbar::ColorScale; -use crate::drawing::plot::Orientation; use crate::drawing::{ Categories, ColumnExt, Error, F64ColumnExt, axis, colorbar, get_column, legend, marker, plot_to_fig, scale, diff --git a/src/dsl.rs b/src/dsl.rs index 60700f8e..a37baf64 100644 --- a/src/dsl.rs +++ b/src/dsl.rs @@ -342,8 +342,7 @@ fn parse_fig(mut val: ast::Struct) -> Result<des::Figure, Error> { for prop in val.props { match prop.name.name.as_str() { "title" => { - let (span, fmt) = expect_string_val(prop)?; - fig = fig.with_title(parse_rich_text(span, fmt)?.into()); + todo!("delete this module") } "legend" => { fig = fig.with_legend(parse_fig_legend(prop.value)?); @@ -443,7 +442,7 @@ fn parse_plot(mut val: ast::Struct) -> Result<(Option<(u32, u32)>, des::plot::Pl } "x-axis" => plot = plot.with_x_axis(parse_axis(prop, false)?), "y-axis" => plot = plot.with_y_axis(parse_axis(prop, true)?), - "title" => plot = plot.with_title(expect_string_val(prop)?.1.into()), + "title" => todo!("delete this module"), "legend" => plot = plot.with_legend(parse_plot_legend(prop.value)?), _ => { return Err(Error::Parse { diff --git a/src/lib.rs b/src/lib.rs index 8c443f42..80bdf0a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,6 +178,8 @@ 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 ClassProps = plotive_text::rich::ClassProps<crate::style::theme::Color>; } #[cfg(any( feature = "noto-sans", diff --git a/src/style/defaults.rs b/src/style/defaults.rs index 7951a468..c938be9b 100644 --- a/src/style/defaults.rs +++ b/src/style/defaults.rs @@ -6,7 +6,7 @@ pub const FIG_SIZE: geom::Size = geom::Size::new(800.0, 600.0); pub const FIG_PADDING: geom::Padding = geom::Padding::Even(20.0); pub const TITLE_FONT_SIZE: f32 = 20.0; -pub const AXIS_LABEL_FONT_SIZE: f32 = 16.0; +pub const AXIS_TITLE_FONT_SIZE: f32 = 16.0; pub const TICKS_LABEL_FONT_SIZE: f32 = 12.0; pub const SERIES_STROKE_WIDTH: f32 = 1.5; diff --git a/text/Cargo.toml b/text/Cargo.toml index 78a58969..445894d3 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -18,6 +18,7 @@ ttf-parser.workspace = true unicode-bidi = "0.3.18" log.workspace = true memmap2 = { version = "0.9", optional = true } +serde = { workspace = true, optional = true } slotmap = { version = "1.0.6", default-features = false } tinyvec = { version = "1.6.0", features = ["alloc"] } @@ -26,19 +27,21 @@ fontconfig-parser = { version = "0.5", optional = true, default-features = false [features] default = ["std", "fs", "memmap", "fontconfig"] -std = ["ttf-parser/std"] +# Enables minimal fontconfig support on Linux. +# Must be enabled for NixOS, otherwise no fonts will be loaded. +fontconfig = ["fontconfig-parser", "fs"] # Allows local filesystem interactions. fs = ["std"] # Allows font files memory mapping, greatly improves performance. memmap = ["fs", "memmap2"] -# Enables minimal fontconfig support on Linux. -# Must be enabled for NixOS, otherwise no fonts will be loaded. -fontconfig = ["fontconfig-parser", "fs"] noto-sans = [] noto-sans-italic = [] noto-serif = [] noto-serif-italic = [] noto-mono = [] +serde = ["dep:serde"] +std = ["ttf-parser/std"] + [[example]] name="text_line" diff --git a/text/examples/text_rich.rs b/text/examples/text_rich.rs index e35e6ff4..bdbc3c03 100644 --- a/text/examples/text_rich.rs +++ b/text/examples/text_rich.rs @@ -52,7 +52,7 @@ fn main() { builder.add_span( start_rlc, end_rlc, - rich::TextOptProps { + rich::ClassProps { font_weight: Some(font::Weight::BOLD), font_style: Some(font::Style::Italic), ..Default::default() @@ -61,7 +61,7 @@ fn main() { builder.add_span( start_line2, end_line2, - rich::TextOptProps { + rich::ClassProps { font_family: Some(serif_family), font_size: Some(FS_MEDIUM), font_style: Some(font::Style::Italic), diff --git a/text/examples/text_rich_parse.rs b/text/examples/text_rich_parse.rs index a4068fe1..9ba68902 100644 --- a/text/examples/text_rich_parse.rs +++ b/text/examples/text_rich_parse.rs @@ -25,7 +25,7 @@ fn main() { .into_builder( rich::TextProps::new(36.0) .with_font(sans_font) - .with_fill(Some(color::BLACK)), + .with_color(Some(color::BLACK)), ) .with_layout(rich::Layout::Horizontal( rich::Align::Center, diff --git a/text/src/lib.rs b/text/src/lib.rs index 5fd9a104..4b7103b4 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -25,6 +25,8 @@ pub mod font; pub mod fontdb; pub mod line; pub mod rich; +#[cfg(feature = "serde")] +pub mod sd; pub use font::{Font, ScaledMetrics, parse_font_families}; pub use line::{LineText, render_line_text}; @@ -72,6 +74,7 @@ pub enum Error { InvalidSpan(String), NoSuchFont(font::Font), FaceParsingError(ttf::FaceParsingError), + ParseRichText(ParseRichTextError), } impl fmt::Display for Error { @@ -80,6 +83,7 @@ impl fmt::Display for Error { Error::InvalidSpan(s) => write!(f, "Invalid span: {}", s), Error::NoSuchFont(font) => write!(f, "Could not find a face for {:?}", font), Error::FaceParsingError(err) => err.fmt(f), + Error::ParseRichText(err) => err.fmt(f), } } } @@ -90,6 +94,12 @@ impl From<ttf::FaceParsingError> for Error { } } +impl From<ParseRichTextError> for Error { + fn from(err: ParseRichTextError) -> Self { + Error::ParseRichText(err) + } +} + impl std::error::Error for Error {} /// Script direction diff --git a/text/src/rich.rs b/text/src/rich.rs index dc1aac6e..7f0f9589 100644 --- a/text/src/rich.rs +++ b/text/src/rich.rs @@ -183,7 +183,7 @@ where } /// Add a new text span - pub fn add_span(&mut self, start: usize, end: usize, props: TextOptProps<C>) { + pub fn add_span(&mut self, start: usize, end: usize, props: ClassProps<C>) { assert!(start <= end); assert!( self.text.is_char_boundary(start) && self.text.is_char_boundary(end), @@ -307,35 +307,35 @@ 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 TextOptProps<C> { +pub struct ClassProps<C> { pub font_family: Option<Vec<font::Family>>, pub font_weight: Option<font::Weight>, pub font_width: Option<font::Width>, pub font_style: Option<font::Style>, pub font_size: Option<f32>, - pub fill: Option<C>, - pub stroke: Option<(C, f32)>, + pub color: Option<C>, + pub outline: Option<(C, f32)>, pub underline: Option<bool>, pub strikeout: Option<bool>, } -impl<C> Default for TextOptProps<C> { +impl<C> Default for ClassProps<C> { fn default() -> Self { - TextOptProps { + ClassProps { font_family: None, font_weight: None, font_width: None, font_style: None, font_size: None, - fill: None, - stroke: None, + color: None, + outline: None, underline: None, strikeout: None, } } } -impl<C> TextOptProps<C> { +impl<C> ClassProps<C> { fn affect_shape(&self) -> bool { self.font_family.is_some() || self.font_weight.is_some() @@ -353,7 +353,7 @@ where { font_size: f32, font: font::Font, - fill: Option<C>, + color: Option<C>, outline: Option<(C, f32)>, underline: bool, strikeout: bool, @@ -372,7 +372,7 @@ where TextProps { font_size: self.font_size, font: self.font.clone(), - fill: self.fill.as_ref().map(|c| color_map(c)), + 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, @@ -400,7 +400,7 @@ where TextProps { font_size, font: font::Font::default(), - fill: Some(C::foreground()), + color: Some(C::foreground()), outline: None, underline: false, strikeout: false, @@ -417,13 +417,13 @@ where self } - pub fn with_fill(mut self, fill: Option<C>) -> Self { - self.fill = fill; + pub fn with_color(mut self, color: Option<C>) -> Self { + self.color = color; self } - pub fn with_outline(mut self, stroke: (C, f32)) -> Self { - self.outline = Some(stroke); + pub fn with_outline(mut self, outline: (C, f32)) -> Self { + self.outline = Some(outline); self } @@ -445,8 +445,8 @@ where &self.font } - pub fn fill(&self) -> Option<C> { - self.fill.clone() + pub fn color(&self) -> Option<C> { + self.color.clone() } pub fn outline(&self) -> Option<(C, f32)> { @@ -461,32 +461,32 @@ where self.strikeout } - fn apply_opts(&mut self, opts: &TextOptProps<C>) { - if let Some(font_family) = &opts.font_family { + fn apply_opts(&mut self, class_props: &ClassProps<C>) { + if let Some(font_family) = &class_props.font_family { self.font = self.font.clone().with_families(font_family.clone()); } - if let Some(font_weight) = opts.font_weight { + if let Some(font_weight) = class_props.font_weight { self.font = self.font.clone().with_weight(font_weight); } - if let Some(font_width) = opts.font_width { + if let Some(font_width) = class_props.font_width { self.font = self.font.clone().with_width(font_width); } - if let Some(font_style) = opts.font_style { + if let Some(font_style) = class_props.font_style { self.font = self.font.clone().with_style(font_style); } - if let Some(font_size) = opts.font_size { + if let Some(font_size) = class_props.font_size { self.font_size = font_size; } - if let Some(fill) = opts.fill.as_ref() { - self.fill = Some(fill.clone()); + if let Some(color) = class_props.color.as_ref() { + self.color = Some(color.clone()); } - if let Some(stroke) = opts.stroke.as_ref() { - self.outline = Some(stroke.clone()); + if let Some(outline) = class_props.outline.as_ref() { + self.outline = Some(outline.clone()); } - if let Some(underline) = opts.underline { + if let Some(underline) = class_props.underline { self.underline = underline; } - if let Some(strikeout) = opts.strikeout { + if let Some(strikeout) = class_props.strikeout { self.strikeout = strikeout; } } @@ -497,7 +497,7 @@ where struct TextSpan<C> { start: usize, end: usize, - props: TextOptProps<C>, + props: ClassProps<C>, } /// A line of rich text diff --git a/text/src/rich/builder.rs b/text/src/rich/builder.rs index 046897a1..52bd0d7b 100644 --- a/text/src/rich/builder.rs +++ b/text/src/rich/builder.rs @@ -2,8 +2,8 @@ use plotive_base::geom; use ttf_parser as ttf; use super::{ - Align, Boundaries, Direction, Error, Glyph, HorAlign, Layout, LineSpan, PropsSpan, RichText, - RichTextBuilder, ShapeSpan, TextOptProps, TextProps, VerAlign, VerDirection, VerProgression, + Align, Boundaries, ClassProps, Direction, Error, Glyph, HorAlign, Layout, LineSpan, PropsSpan, + RichText, RichTextBuilder, ShapeSpan, TextProps, VerAlign, VerDirection, VerProgression, }; use crate::bidi::BidiAlgo; use crate::font::{self, DatabaseExt}; @@ -25,7 +25,7 @@ where C: Clone, { init_props: TextProps<C>, - stack: Vec<TextOptProps<C>>, + stack: Vec<ClassProps<C>>, } impl<C> PropsResolver<C> @@ -47,18 +47,18 @@ where TextProps { font: props.font, font_size: props.font_size, - fill: props.fill.clone(), + color: props.color.clone(), outline: props.outline.clone(), underline: props.underline, strikeout: props.strikeout, } } - fn push_opts(&mut self, opts: TextOptProps<C>) { + fn push_opts(&mut self, opts: ClassProps<C>) { self.stack.push(opts); } - fn pop_opts(&mut self, opts: &TextOptProps<C>) { + fn pop_opts(&mut self, opts: &ClassProps<C>) { for i in (0..self.stack.len()).rev() { if &self.stack[i] == opts { self.stack.remove(i); @@ -707,7 +707,7 @@ mod tests { builder.add_span( 5, 9, - TextOptProps { + ClassProps { underline: Some(true), ..Default::default() }, diff --git a/text/src/rich/parse.rs b/text/src/rich/parse.rs index 98b50bc2..ed16bc69 100644 --- a/text/src/rich/parse.rs +++ b/text/src/rich/parse.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use plotive_base::Color; -use crate::rich::{TextOptProps, TextProps}; +use crate::rich::{ClassProps, TextProps}; use crate::{RichTextBuilder, font}; /// Position into an input stream @@ -63,10 +63,15 @@ impl fmt::Display for ParseRichTextError { impl std::error::Error for ParseRichTextError {} +/// A rich text string that has been parsed into a plain text string and a list of spans with properties. +/// +/// It is produced by the [`parse_rich_text`] or [`parse_rich_text_with_classes`] function, +/// and can be converted into a [`RichTextBuilder`] +/// using the [`into_builder`](Self::into_builder) method. #[derive(Debug, Clone)] pub struct ParsedRichText<C> { pub text: String, - pub prop_spans: Vec<(Pos, Pos, TextOptProps<C>)>, + pub prop_spans: Vec<(Pos, Pos, ClassProps<C>)>, } impl<C> ParsedRichText<C> @@ -92,7 +97,7 @@ where pub fn parse_rich_text_with_classes<C>( fmt: &str, - user_classes: &[(String, TextOptProps<C>)], + user_classes: &[(String, ClassProps<C>)], ) -> Result<ParsedRichText<C>, ParseRichTextError> where C: Color + FromStr, @@ -104,7 +109,7 @@ where #[derive(Debug, Clone)] struct RichTextParser<'a, C> { fmt: &'a str, - user_classes: &'a [(String, TextOptProps<C>)], + user_classes: &'a [(String, ClassProps<C>)], } impl<'a, C> RichTextParser<'a, C> @@ -118,7 +123,7 @@ where } } - pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, TextOptProps<C>)]) -> Self { + pub fn new_with_classes(fmt: &'a str, user_classes: &'a [(String, ClassProps<C>)]) -> Self { Self { fmt, user_classes } } @@ -168,15 +173,15 @@ where Ok(ParsedRichText { text, prop_spans }) } - fn merge_props(base: TextOptProps<C>, overlay: &TextOptProps<C>) -> TextOptProps<C> { - TextOptProps { + fn merge_props(base: ClassProps<C>, overlay: &ClassProps<C>) -> ClassProps<C> { + 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), - fill: overlay.fill.or(base.fill), - stroke: overlay.stroke.or(base.stroke), + color: overlay.color.or(base.color), + outline: overlay.outline.or(base.outline), underline: overlay.underline.or(base.underline), strikeout: overlay.strikeout.or(base.strikeout), } @@ -186,8 +191,8 @@ where &self, span: Span, tag: &lex::OpeningTag, - ) -> Result<TextOptProps<C>, ParseRichTextError> { - let mut props = TextOptProps::default(); + ) -> Result<ClassProps<C>, ParseRichTextError> { + let mut props = ClassProps::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, @@ -231,13 +236,13 @@ where let color: C = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.fill = Some(color); + props.color = Some(color); } "outline" | "stroke" => { let color: C = value.parse().map_err(|_| { ParseRichTextError::BadPropValue(span, prop.prop.clone(), value.clone()) })?; - props.fill = Some(color); + props.color = Some(color); } _ => { return Err(ParseRichTextError::UnknownClass(span, prop.prop.clone())); @@ -338,7 +343,7 @@ where let color: C = other.parse().map_err(|_| { ParseRichTextError::UnknownClass(span, other.to_string()) })?; - props.fill = Some(color); + props.color = Some(color); } } } diff --git a/text/src/rich/render.rs b/text/src/rich/render.rs index c8604dbf..697d88bc 100644 --- a/text/src/rich/render.rs +++ b/text/src/rich/render.rs @@ -71,7 +71,7 @@ where } if let Some(path) = span_builder.finish() { - if let Some(c) = span.props.fill.as_ref() { + if let Some(c) = span.props.color.as_ref() { let prim = RichPrimitive::Fill(&path, c.clone()); render_fn(prim); } diff --git a/text/src/sd.rs b/text/src/sd.rs new file mode 100644 index 00000000..33b3ae20 --- /dev/null +++ b/text/src/sd.rs @@ -0,0 +1,377 @@ +use std::borrow::Cow; + +use serde::de::Error; +use serde::ser::{SerializeMap, SerializeSeq}; + +use crate::font; + +impl serde::Serialize for font::Family { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let name = match self { + font::Family::SansSerif => "sans-serif", + font::Family::Serif => "serif", + font::Family::Monospace => "monospace", + font::Family::Cursive => "cursive", + font::Family::Fantasy => "fantasy", + font::Family::Named(name) => name.as_str(), + }; + name.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for font::Family { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?; + Ok(match name.as_ref() { + "sans-serif" => font::Family::SansSerif, + "serif" => font::Family::Serif, + "monospace" => font::Family::Monospace, + "cursive" => font::Family::Cursive, + "fantasy" => font::Family::Fantasy, + _ => font::Family::Named(name.into_owned()), + }) + } +} + +impl serde::Serialize for font::Weight { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + match self.0 { + 100 => return serializer.serialize_str("thin"), + 200 => return serializer.serialize_str("extra-light"), + 300 => return serializer.serialize_str("light"), + 400 => return serializer.serialize_str("normal"), + 500 => return serializer.serialize_str("medium"), + 600 => return serializer.serialize_str("semi-bold"), + 700 => return serializer.serialize_str("bold"), + 800 => return serializer.serialize_str("extra-bold"), + 900 => return serializer.serialize_str("black"), + _ => self.0.serialize(serializer), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for font::Weight { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = font::Weight; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or number representing a font weight") + } + + fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + if value <= 0 { + return Err(E::custom(format!( + "Font weight must be a positive integer, got: {}", + value + ))); + } + if value > 1000 { + return Err(E::custom(format!( + "Font weight must be <= 1000, got: {}", + value + ))); + } + Ok(font::Weight(value as u16)) + } + + fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_i64(value as i64) + } + + fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_i64(value as i64) + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + value.parse::<font::Weight>().map_err(|err| { + E::custom(format!( + "Invalid font weight string: {}. Error: {}", + value, err + )) + }) + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl serde::Serialize for font::Style { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + match self { + font::Style::Normal => serializer.serialize_str("normal"), + font::Style::Italic => serializer.serialize_str("italic"), + font::Style::Oblique => serializer.serialize_str("oblique"), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for font::Style { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = font::Style; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing a font style") + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + match value { + "normal" => Ok(font::Style::Normal), + "italic" => Ok(font::Style::Italic), + "oblique" => Ok(font::Style::Oblique), + _ => Err(E::custom(format!("Invalid font style string: {}", value))), + } + } + } + + deserializer.deserialize_str(Visitor) + } +} + +impl serde::Serialize for font::Width { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + match self { + font::Width::UltraCondensed => serializer.serialize_str("ultra-condensed"), + font::Width::ExtraCondensed => serializer.serialize_str("extra-condensed"), + font::Width::Condensed => serializer.serialize_str("condensed"), + font::Width::SemiCondensed => serializer.serialize_str("semi-condensed"), + font::Width::Normal => serializer.serialize_str("normal"), + font::Width::SemiExpanded => serializer.serialize_str("semi-expanded"), + font::Width::Expanded => serializer.serialize_str("expanded"), + font::Width::ExtraExpanded => serializer.serialize_str("extra-expanded"), + font::Width::UltraExpanded => serializer.serialize_str("ultra-expanded"), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for font::Width { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = font::Width; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing a font width") + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + match value { + "ultra-condensed" => Ok(font::Width::UltraCondensed), + "extra-condensed" => Ok(font::Width::ExtraCondensed), + "condensed" => Ok(font::Width::Condensed), + "semi-condensed" => Ok(font::Width::SemiCondensed), + "normal" => Ok(font::Width::Normal), + "semi-expanded" => Ok(font::Width::SemiExpanded), + "expanded" => Ok(font::Width::Expanded), + "extra-expanded" => Ok(font::Width::ExtraExpanded), + "ultra-expanded" => Ok(font::Width::UltraExpanded), + _ => Err(E::custom(format!("Invalid font width string: {}", value))), + } + } + } + + deserializer.deserialize_str(Visitor) + } +} + +struct Outline<C>(C, f32); + +impl<C> serde::Serialize for Outline<C> +where + C: serde::Serialize, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<C> serde::Serialize for crate::rich::ClassProps<C> +where + C: serde::Serialize + Copy + Clone, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut state = serializer.serialize_map(None)?; + if let Some(families) = &self.font_family { + let family_str = crate::font::font_families_to_string(families); + state.serialize_entry("fontFamily", &family_str)?; + } + if let Some(weight) = &self.font_weight { + state.serialize_entry("fontWeight", weight)?; + } + if let Some(width) = &self.font_width { + state.serialize_entry("fontWidth", width)?; + } + if let Some(style) = &self.font_style { + state.serialize_entry("fontStyle", style)?; + } + if let Some(size) = &self.font_size { + state.serialize_entry("fontSize", 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(underline) = &self.underline { + state.serialize_entry("underline", underline)?; + } + if let Some(strikeout) = &self.strikeout { + state.serialize_entry("strikeout", strikeout)?; + } + state.end() + } +} + +impl<'de, C> serde::de::Deserialize<'de> for crate::rich::ClassProps<C> +where + C: serde::de::Deserialize<'de> + Copy + Clone, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Visitor<C>(std::marker::PhantomData<C>); + + impl<'de, C> serde::de::Visitor<'de> for Visitor<C> + where + C: serde::de::Deserialize<'de> + Copy + Clone, + { + type Value = crate::rich::ClassProps<C>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map representing ClassProps") + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: serde::de::MapAccess<'de>, + M::Error: serde::de::Error, + { + let mut props = crate::rich::ClassProps::<C>::default(); + while let Some(key) = map.next_key::<String>()? { + match key.as_str() { + "fontFamily" => { + let family_str: String = map.next_value()?; + let value = font::parse_font_families(&family_str).map_err(|err| { + M::Error::custom(format!( + "Invalid font family string: {}. Error: {}", + family_str, err + )) + })?; + props.font_family = Some(value); + } + "fontWeight" => { + let value: font::Weight = map.next_value()?; + props.font_weight = Some(value); + } + "fontWidth" => { + let value: font::Width = map.next_value()?; + props.font_width = Some(value); + } + "fontStyle" => { + let value: font::Style = map.next_value()?; + props.font_style = Some(value); + } + "fontSize" => { + let value: f32 = map.next_value()?; + props.font_size = Some(value); + } + "color" => { + let value: C = map.next_value()?; + props.color = Some(value); + } + "outline" => { + let (outline, width): (C, f32) = map.next_value()?; + props.outline = Some((outline, width)); + } + "underline" => { + let value: bool = map.next_value()?; + props.underline = Some(value); + } + "strikeout" => { + let value: bool = map.next_value()?; + props.strikeout = Some(value); + } + other => { + return Err(M::Error::unknown_field( + other, + &[ + "fontFamily", + "fontWeight", + "fontWidth", + "fontStyle", + "fontSize", + "color", + "outline", + "underline", + "strikeout", + ], + )); + } + } + } + Ok(props) + } + } + + deserializer.deserialize_map(Visitor::<C>(std::marker::PhantomData)) + } +} From e5d14c436f1e9ac2657c7abc376d55d251003f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= <remi.thebault@gmail.com> Date: Sun, 28 Jun 2026 22:39:45 +0200 Subject: [PATCH 2/6] annot label is using des::Text --- examples/bode_rlc.rs | 5 +++-- src/des/annot.rs | 47 ++++---------------------------------------- src/des/sd/annot.rs | 11 ++--------- src/drawing/annot.rs | 34 ++++++++++++++++---------------- 4 files changed, 26 insertions(+), 71 deletions(-) diff --git a/examples/bode_rlc.rs b/examples/bode_rlc.rs index f66e2b41..1d9badc0 100644 --- a/examples/bode_rlc.rs +++ b/examples/bode_rlc.rs @@ -52,6 +52,7 @@ fn main() { (100.0, "mag3", "phase3", "R = 100 Ω"), ]; + // [&str; 1] converts to Text::Rich let title = [concat!( "Bode diagram of RLC circuit\n", "[size=18;italic;font=serif]L = 0.1 mH / C = 1 µF[/size;italic;font]" @@ -117,11 +118,11 @@ fn main() { let slope_line = des::annot::Line::two_points(cutoff, 0.0, 100.0 * cutoff, mag_2_decades) .with_pattern(style::Dash::default().into()); let cut_off_label = - des::annot::Label::new(format!("{:.2} kHz", cutoff / 1000.0), cutoff, -60.0) + des::annot::Label::new(format!("{:.2} kHz", cutoff / 1000.0).into(), cutoff, -60.0) .with_anchor(des::annot::Anchor::BottomLeft) .with_angle(90.0); let slope_label = des::annot::Label::new( - format!("{:.0} dB/decade", mag_2_decades / 2.0), + format!("{:.0} dB/decade", mag_2_decades / 2.0).into(), cutoff * 10.0, mag_2_decades / 2.0, ) diff --git a/src/des/annot.rs b/src/des/annot.rs index 58b3d446..4aea6253 100644 --- a/src/des/annot.rs +++ b/src/des/annot.rs @@ -1,7 +1,6 @@ //! Annotations to place on the plot area. -use crate::des::axis; +use crate::des::{Text, axis}; use crate::style::{self, theme}; -use crate::text::Font; /// An arbitrary graphical annotation placed on the plot area. /// The placement is made according to the data coordinates. @@ -497,10 +496,7 @@ pub enum Anchor { pub struct Label { x: f64, y: f64, - text: String, - font_size: f32, - font: Font, - color: theme::Color, + text: Text, anchor: Anchor, frame: (Option<theme::Fill>, Option<theme::Stroke>), angle: f32, @@ -512,14 +508,11 @@ pub struct Label { impl Label { /// Create a new label with the given text at data coordinates (x, y) - pub fn new(text: String, x: f64, y: f64) -> Self { + pub fn new(text: Text, x: f64, y: f64) -> Self { Label { x, y, text, - font_size: 12.0, - font: Font::default(), - color: theme::Col::Foreground.into(), anchor: Anchor::default(), frame: (None, None), angle: 0.0, @@ -529,22 +522,6 @@ impl Label { } } - /// Set the font size of the label - pub fn with_font_size(self, font_size: f32) -> Self { - Self { font_size, ..self } - } - - /// Set the font of the label - pub fn with_font(self, font: Font) -> Self { - Self { font, ..self } - } - - /// Set the color of the label. - /// By default, the foreground theme color is used. - pub fn with_color(self, color: theme::Color) -> Self { - Self { color, ..self } - } - /// Set the anchor point of the label. /// By default, the top-left corner is used. pub fn with_anchor(self, anchor: Anchor) -> Self { @@ -595,26 +572,10 @@ impl Label { } /// Get the text of the label. - pub fn text(&self) -> &str { + pub fn text(&self) -> &Text { &self.text } - /// Get the font size of the label - pub fn font_size(&self) -> f32 { - self.font_size - } - - /// Get the font of the label - pub fn font(&self) -> &Font { - &self.font - } - - /// Get the color of the label. - /// By default, the foreground theme color is used. - pub fn color(&self) -> &theme::Color { - &self.color - } - /// Get the anchor point of the label. /// By default, the top-left corner is used. pub fn anchor(&self) -> Anchor { diff --git a/src/des/sd/annot.rs b/src/des/sd/annot.rs index 21b728cd..75db3136 100644 --- a/src/des/sd/annot.rs +++ b/src/des/sd/annot.rs @@ -2,7 +2,7 @@ use serde::de::{Error, MapAccess}; use serde::ser::SerializeStruct; use serde_value::Value; -use crate::des::{Annotation, annot, axis}; +use crate::des::{Annotation, Text, annot, axis}; use crate::style::{self, theme}; impl serde::Serialize for annot::ZPos { @@ -212,9 +212,6 @@ where if let (Some(fill), Some(stroke)) = (fill, stroke) { state.serialize_field("frame", &(fill, stroke))?; } - if label.color() != &theme::Color::from(theme::Col::Foreground) { - state.serialize_field("color", label.color())?; - } if label.angle() != 0.0 { state.serialize_field("angle", &label.angle())?; } @@ -432,10 +429,9 @@ where super::deserialize_tagged_map_fields! { 'de, map, buffered, "xy" => xy: Option<(f64, f64)>, - "text" => text: Option<String>, + "text" => text: Option<Text>, "anchor" => anchor: Option<annot::Anchor>, "frame" => frame: Option<(Option<theme::Fill>, Option<theme::Stroke>)>, - "color" => color: Option<theme::Color>, "angle" => angle: Option<f32>, "xAxis" => x_axis: Option<axis::Ref>, "yAxis" => y_axis: Option<axis::Ref>, @@ -452,9 +448,6 @@ where if let Some((fill, stroke)) = frame { annot = annot.with_frame(fill, stroke); } - if let Some(color) = color { - annot = annot.with_color(color); - } if let Some(angle) = angle { annot = annot.with_angle(angle); } diff --git a/src/drawing/annot.rs b/src/drawing/annot.rs index e24b1794..4a15cf33 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, theme}; +use crate::style::{self, defaults, theme}; use crate::{Style, data, geom, render, text}; #[derive(Debug, Clone)] @@ -40,27 +40,27 @@ where des::Annotation::Marker(marker) => Annot::Marker(marker.clone()), des::Annotation::Label(label) => { let (align, ver_align) = match label.anchor() { - Anchor::TopLeft => (text::line::Align::Left, text::line::VerAlign::Top), - Anchor::TopCenter => (text::line::Align::Center, text::line::VerAlign::Top), - Anchor::TopRight => (text::line::Align::Right, text::line::VerAlign::Top), - Anchor::CenterRight => (text::line::Align::Right, text::line::VerAlign::Middle), - Anchor::BottomRight => (text::line::Align::Right, text::line::VerAlign::Bottom), + Anchor::TopLeft => (text::rich::Align::Left, text::rich::VerAlign::Top), + Anchor::TopCenter => (text::rich::Align::Center, text::rich::VerAlign::Top), + Anchor::TopRight => (text::rich::Align::Right, text::rich::VerAlign::Top), + Anchor::CenterRight => (text::rich::Align::Right, text::rich::VerAlign::Center), + Anchor::BottomRight => (text::rich::Align::Right, text::rich::VerAlign::Bottom), Anchor::BottomCenter => { - (text::line::Align::Center, text::line::VerAlign::Bottom) + (text::rich::Align::Center, text::rich::VerAlign::Bottom) } - Anchor::BottomLeft => (text::line::Align::Left, text::line::VerAlign::Bottom), - Anchor::CenterLeft => (text::line::Align::Left, text::line::VerAlign::Middle), - Anchor::Center => (text::line::Align::Center, text::line::VerAlign::Middle), + Anchor::BottomLeft => (text::rich::Align::Left, text::rich::VerAlign::Bottom), + Anchor::CenterLeft => (text::rich::Align::Left, text::rich::VerAlign::Center), + Anchor::Center => (text::rich::Align::Center, text::rich::VerAlign::Center), }; - let line_text = text::LineText::new( - label.text().to_string(), - (align, ver_align), - label.font_size(), - label.font().clone(), - &self.fontdb, + let text = label.text().to_rich_text( + text::rich::TextProps::<theme::Color>::new(12.0).with_font( + defaults::FONT_FAMILY.parse().unwrap() + ), + text::rich::Layout::Horizontal(align, ver_align, Default::default()), + self.fontdb(), )?; let (x, y) = label.position(); - let text = Text::from_line_text(&line_text, &self.fontdb, *label.color())?; + let text = Text::from_rich_text(&text, self.fontdb())?; let frame = label.frame(); let frame = (frame.0.cloned(), frame.1.cloned()); Annot::Label(Label { From cff3de1e56a600e4b477a4325cdba44e0d66db4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= <remi.thebault@gmail.com> Date: Mon, 29 Jun 2026 00:11:19 +0200 Subject: [PATCH 3/6] text::LineProps --- src/des.rs | 6 +- src/des/axis.rs | 37 +++------- src/des/colorbar.rs | 29 ++------ src/des/legend.rs | 31 ++------- src/des/sd.rs | 146 ++++++++++++++++++++++++++++++++++++---- src/des/sd/axis.rs | 19 ++++-- src/des/sd/colorbar.rs | 14 +++- src/des/sd/legend.rs | 14 ++-- src/drawing.rs | 17 +++++ src/drawing/axis.rs | 51 ++++++++------ src/drawing/colorbar.rs | 12 +++- src/drawing/legend.rs | 18 +++-- src/lib.rs | 22 +++++- 13 files changed, 277 insertions(+), 139 deletions(-) diff --git a/src/des.rs b/src/des.rs index 40cfdf1e..03dbd5d4 100644 --- a/src/des.rs +++ b/src/des.rs @@ -39,7 +39,7 @@ 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::ClassProps)>, + classes: Vec<(String, text::RichProps)>, }, } @@ -107,8 +107,8 @@ impl From<(&str,)> for Text { } } -impl From<(String, Vec<(String, text::ClassProps)>)> for Text { - fn from(tuple: (String, Vec<(String, text::ClassProps)>)) -> Self { +impl From<(String, Vec<(String, text::RichProps)>)> for Text { + fn from(tuple: (String, Vec<(String, text::RichProps)>)) -> Self { Text::RichWithClasses { fmt: tuple.0, classes: tuple.1, diff --git a/src/des/axis.rs b/src/des/axis.rs index 1ca63dbe..6546355e 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -5,7 +5,7 @@ * They are not tied to a specific orientation (X or Y), that is handled at the plot level. */ -pub use ticks::{Grid, MinorGrid, MinorTicks, Ticks, TicksFont}; +pub use ticks::{Grid, MinorGrid, MinorTicks, Ticks}; use super::Text; @@ -382,8 +382,8 @@ impl Scale { /// Describe the ticks of an axis pub mod ticks { - use crate::style::{self, Dash, defaults, theme}; - use crate::text::Font; + use crate::style::{self, Dash, theme}; + use crate::text; /// Describes how to locate the ticks of an axis #[derive(Debug, Default, Clone, PartialEq)] @@ -669,24 +669,6 @@ pub mod ticks { } } - /// Describes the font of the ticks labels - #[derive(Debug, Clone, PartialEq)] - pub struct TicksFont { - /// The font of the ticks labels - pub font: Font, - /// The font size of the ticks labels - pub size: f32, - } - - impl Default for TicksFont { - fn default() -> Self { - TicksFont { - font: defaults::FONT_FAMILY.parse().unwrap(), - size: defaults::TICKS_LABEL_FONT_SIZE, - } - } - } - /// Describes the style of the major grid lines #[derive(Debug, Clone, PartialEq)] pub struct Grid(pub theme::Stroke); @@ -725,7 +707,7 @@ pub mod ticks { pub struct Ticks { locator: Locator, formatter: Option<Formatter>, - font: TicksFont, + font: text::LineProps, color: theme::Color, } @@ -738,7 +720,7 @@ pub mod ticks { Ticks { locator: Locator::default(), formatter: Some(Formatter::default()), - font: TicksFont::default(), + font: text::LineProps::default(), color: theme::Col::Foreground.into(), } } @@ -760,7 +742,7 @@ pub mod ticks { Self { formatter, ..self } } /// Returns a new ticks with the specified font - pub fn with_font(self, font: TicksFont) -> Self { + pub fn with_font(self, font: text::LineProps) -> Self { Self { font, ..self } } /// Returns a new ticks with the specified color @@ -777,11 +759,12 @@ pub mod ticks { pub fn formatter(&self) -> Option<&Formatter> { self.formatter.as_ref() } - /// Font for the ticks labels - pub fn font(&self) -> &TicksFont { + /// Font properties for the ticks labels + pub fn font(&self) -> &text::LineProps { &self.font } - /// Color for the ticks and the labels + /// Color for the ticks. + /// Will be used for the labels as well unless a specific color is set in [`font`](Self::font). pub fn color(&self) -> theme::Color { self.color } diff --git a/src/des/colorbar.rs b/src/des/colorbar.rs index 16660846..57a9b369 100644 --- a/src/des/colorbar.rs +++ b/src/des/colorbar.rs @@ -17,27 +17,6 @@ pub enum Pos { Left, } -/// Font configuration for color bar ticks -#[derive(Debug, Clone, PartialEq)] -pub struct TicksFont { - /// The font size in figure units - pub size: f32, - /// The font - pub font: text::Font, - /// The font color - pub color: theme::Color, -} - -impl Default for TicksFont { - fn default() -> Self { - Self { - size: defaults::COLORBAR_TICKS_FONT_SIZE, - font: text::Font::default(), - color: theme::Col::Foreground.into(), - } - } -} - /// ColorBar configuration for a plot #[derive(Debug, Clone, PartialEq)] pub struct ColorBar { @@ -45,7 +24,7 @@ pub struct ColorBar { pub(crate) pos: Pos, width: f32, title: Option<Text>, - ticks_font: TicksFont, + ticks_font: text::LineProps, border: Option<theme::Stroke>, locator: axis::ticks::Locator, margin: f32, @@ -57,7 +36,7 @@ impl Default for ColorBar { pos: Pos::default(), width: defaults::COLORBAR_WIDTH, title: None, - ticks_font: TicksFont::default(), + ticks_font: text::LineProps::default(), border: Some(theme::Stroke { color: theme::Col::Foreground.into(), width: 1.0, @@ -92,7 +71,7 @@ impl ColorBar { } /// Set the ticks font properties and return self for chaining - pub fn with_ticks_font(mut self, ticks_font: TicksFont) -> Self { + pub fn with_ticks_font(mut self, ticks_font: text::LineProps) -> Self { self.ticks_font = ticks_font; self } @@ -131,7 +110,7 @@ impl ColorBar { } /// Get the ticks font properties - pub fn ticks_font(&self) -> &TicksFont { + pub fn ticks_font(&self) -> &text::LineProps { &self.ticks_font } diff --git a/src/des/legend.rs b/src/des/legend.rs index 965278f9..b3e3e859 100644 --- a/src/des/legend.rs +++ b/src/des/legend.rs @@ -7,32 +7,11 @@ use crate::geom::{Padding, Size}; use crate::style::{defaults, theme}; use crate::text; -/// The font configuration for legend entries -#[derive(Debug, Clone, PartialEq)] -pub struct EntryFont { - /// The font size in figure units - pub size: f32, - /// The font - pub font: text::Font, - /// The font color - pub color: theme::Color, -} - -impl Default for EntryFont { - fn default() -> Self { - Self { - size: defaults::LEGEND_LABEL_FONT_SIZE, - font: text::Font::default(), - color: theme::Col::Foreground.into(), - } - } -} - /// Legend configuration for a plot #[derive(Debug, Clone, PartialEq)] pub struct Legend<Pos> { pos: Pos, - font: EntryFont, + font: text::LineProps, fill: Option<theme::Fill>, border: Option<theme::Stroke>, columns: Option<NonZeroU32>, @@ -45,13 +24,13 @@ impl<Pos: Default> Default for Legend<Pos> { /// Create a default legend configuration /// - Fill color: theme::Col::LegendFill, opacity 0.5 /// - Border: theme::Col::LegendBorder, 1.0 - /// - Font: default EntryFont + /// - Font: default LineProps /// - Default column layout (depdend on the position and number and width of entries) /// - Default padding and spacing fn default() -> Self { Self { pos: Pos::default(), - font: EntryFont::default(), + font: text::LineProps::default(), fill: defaults::legend_fill(), border: Some(theme::Col::LegendBorder.into()), columns: None, @@ -87,7 +66,7 @@ where impl<Pos> Legend<Pos> { /// Get the font configuration for legend entries - pub fn font(&self) -> &EntryFont { + pub fn font(&self) -> &text::LineProps { &self.font } @@ -127,7 +106,7 @@ impl<Pos> Legend<Pos> { } /// Set the font configuration for legend entries and return self for chaining - pub fn with_font(self, font: EntryFont) -> Self { + pub fn with_font(self, font: text::LineProps) -> Self { Self { font, ..self } } diff --git a/src/des/sd.rs b/src/des/sd.rs index 448aadc8..b07d4cf3 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,6 +1,6 @@ //! Serialization and deserialization of figures -use serde::ser::{SerializeSeq, SerializeStruct}; +use serde::ser::{SerializeSeq, SerializeStruct, SerializeMap}; use super::Figure; use crate::des::{FigLegend, Plot, Subplots, Text, figure}; @@ -22,6 +22,136 @@ use crate::text; // MARK: Text +impl serde::Serialize for text::LineProps { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + 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<D>(deserializer: D) -> Result<Self, D::Error> + 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<A>(self, mut map: A) -> Result<Self::Value, A::Error> + 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::<String>()? { + 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<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let map = std::collections::HashMap::<String, text::RichProps>::deserialize(deserializer)?; + Ok(RichPropsMap(map.into_iter().collect())) + } +} + impl serde::Serialize for Text { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -44,18 +174,6 @@ impl serde::Serialize for Text { } } -struct ClassPropsMap(Vec<(String, text::ClassProps)>); - -impl<'de> serde::Deserialize<'de> for ClassPropsMap { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - let map = std::collections::HashMap::<String, text::ClassProps>::deserialize(deserializer)?; - Ok(ClassPropsMap(map.into_iter().collect())) - } -} - impl<'de> serde::Deserialize<'de> for Text { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -84,7 +202,7 @@ impl<'de> serde::Deserialize<'de> for Text { let fmt: String = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - let classes: Option<ClassPropsMap> = seq.next_element()?; + let classes: Option<RichPropsMap> = seq.next_element()?; if let Some(classes) = classes { Ok(Text::RichWithClasses { fmt, diff --git a/src/des/sd/axis.rs b/src/des/sd/axis.rs index 5ecbcac2..32fdc275 100644 --- a/src/des/sd/axis.rs +++ b/src/des/sd/axis.rs @@ -871,17 +871,19 @@ impl serde::Serialize for axis::Ticks { let has_default_locator = self.locator() == default.locator(); let has_default_formatter = self.formatter() == default.formatter(); + let has_default_font = self.font() == default.font(); let has_default_color = self.color() == default.color(); match ( has_default_locator, has_default_formatter, + has_default_font, has_default_color, ) { - (true, true, true) => "auto".serialize(serializer), - (false, true, true) => self.locator().serialize(serializer), - (true, false, true) => self.formatter().serialize(serializer), - (true, true, false) => self.color().serialize(serializer), + (true, true, true, true) => "auto".serialize(serializer), + (false, true, true, true) => self.locator().serialize(serializer), + (true, false, true, true) => self.formatter().serialize(serializer), + (true, true, true, false) => self.color().serialize(serializer), _ => { let len = 3 - has_default_locator as usize @@ -894,6 +896,9 @@ impl serde::Serialize for axis::Ticks { if !has_default_formatter { state.serialize_field("formatter", &self.formatter())?; } + if !has_default_font { + state.serialize_field("font", &self.font())?; + } if !has_default_color { state.serialize_field("color", &self.color())?; } @@ -983,6 +988,10 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { let formatter = map.next_value()?; ticks = ticks.with_formatter(formatter); } + "font" => { + let font = map.next_value()?; + ticks = ticks.with_font(font); + } "color" => { let color = map.next_value()?; ticks = ticks.with_color(color); @@ -1023,7 +1032,7 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { _ => { return Err(serde::de::Error::unknown_field( &key, - &["locator", "formatter", "color", "type"], + &["locator", "formatter", "font", "color", "type"], )); } } diff --git a/src/des/sd/colorbar.rs b/src/des/sd/colorbar.rs index d8d8c5dd..e3121153 100644 --- a/src/des/sd/colorbar.rs +++ b/src/des/sd/colorbar.rs @@ -5,6 +5,7 @@ use serde::{Deserializer, Serializer}; use crate::des::axis::ticks; use crate::des::{self, colorbar}; use crate::style::theme; +use crate::text; impl serde::Serialize for colorbar::Pos { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> @@ -79,6 +80,9 @@ impl serde::Serialize for colorbar::ColorBar { if self.border() != default.border() { map.serialize_field("border", &self.border())?; } + if self.ticks_font() != default.ticks_font() { + map.serialize_field("ticksFont", &self.ticks_font())?; + } if self.ticks_locator() != default.ticks_locator() { map.serialize_field("ticks", &self.ticks_locator())?; } @@ -124,6 +128,7 @@ impl<'de> serde::Deserialize<'de> for colorbar::ColorBar { "title" => title: Option<des::Text>, "border" => border: Option<Option<theme::Stroke>>, "ticks" => ticks: Option<ticks::Locator>, + "ticksFont" => ticks_font: Option<text::LineProps>, "margin" => margin: Option<f32>, ); let mut colorbar = if let Some(pos) = pos { @@ -138,13 +143,16 @@ impl<'de> serde::Deserialize<'de> for colorbar::ColorBar { colorbar = colorbar.with_title(title); } if let Some(border) = border { - colorbar = colorbar.with_border(border) + colorbar = colorbar.with_border(border); } if let Some(ticks) = ticks { - colorbar = colorbar.with_ticks_locator(ticks) + colorbar = colorbar.with_ticks_locator(ticks); + } + if let Some(ticks_font) = ticks_font { + colorbar = colorbar.with_ticks_font(ticks_font); } if let Some(margin) = margin { - colorbar = colorbar.with_margin(margin) + colorbar = colorbar.with_margin(margin); } Ok(colorbar) } diff --git a/src/des/sd/legend.rs b/src/des/sd/legend.rs index 1df0dbb7..1d44c368 100644 --- a/src/des/sd/legend.rs +++ b/src/des/sd/legend.rs @@ -2,8 +2,8 @@ use std::borrow::Cow; use serde::ser::SerializeStruct; -use crate::des::{Legend, figure, legend, plot}; -use crate::geom; +use crate::des::{Legend, figure, plot}; +use crate::{geom, text}; use crate::style::{defaults, theme}; // MARK: figure::LegendPos @@ -117,7 +117,7 @@ where where S: serde::Serializer, { - let font_default = self.font() == &legend::EntryFont::default(); + let font_default = self.font() == &text::LineProps::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(); @@ -139,7 +139,7 @@ where let mut state = serializer.serialize_struct("Legend", 2)?; state.serialize_field("pos", &self.pos())?; if !font_default { - todo!("Serialize legend::EntryFont") + state.serialize_field("font", self.font())?; } if !fill_default { state.serialize_field("fill", &self.fill())?; @@ -151,7 +151,7 @@ where state.serialize_field("columns", &self.columns())?; } if !padding_default { - todo!("Serialize geom::Padding") + state.serialize_field("padding", &self.padding())?; } if !margin_default { state.serialize_field("margin", &self.margin())?; @@ -214,11 +214,11 @@ where while let Some(key) = map.next_key::<Cow<'de, str>>()? { match &*key { "pos" => legend = legend.with_pos(map.next_value()?), - "font" => todo!("Deserialize legend::EntryFont"), + "font" => legend = legend.with_font(map.next_value()?), "fill" => legend = legend.with_fill(map.next_value()?), "border" => legend = legend.with_border(map.next_value()?), "columns" => legend = legend.with_columns(map.next_value()?), - "padding" => todo!("Deserialize geom::Padding"), + "padding" => legend = legend.with_padding(map.next_value()?), "margin" => legend = legend.with_margin(map.next_value()?), "spacing" => legend = legend.with_spacing(map.next_value()?), _ => { diff --git a/src/drawing.rs b/src/drawing.rs index 3cf66535..9fd04c19 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -239,6 +239,23 @@ struct TextSpan { stroke: Option<theme::Stroke>, } +fn resolve_line_font(props: &text::LineProps, default: text::Font) -> text::Font { + let mut res = default; + if let Some(families) = props.family.as_ref() { + res = res.with_families(families.clone()); + } + if let Some(style) = props.style { + res = res.with_style(style); + } + if let Some(weight) = props.weight { + res = res.with_weight(weight); + } + if let Some(width) = props.width { + res = res.with_width(width); + } + res +} + impl Text { fn from_line_text( text: &text::LineText, diff --git a/src/drawing/axis.rs b/src/drawing/axis.rs index 4d79fb9b..81c5f841 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -17,7 +17,6 @@ use crate::style::{defaults, theme}; use crate::text::{self, font}; use crate::{Style, data, des, geom, missing_params, render}; - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Orientation { X, @@ -390,7 +389,11 @@ where height += missing_params::TICK_SIZE; } height += missing_params::TICK_SIZE; - height += missing_params::TICK_LABEL_MARGIN + ticks.font().size; + height += missing_params::TICK_LABEL_MARGIN + + ticks + .font() + .size + .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); } } let key = AxisCacheKey { @@ -512,6 +515,17 @@ where } } + fn axis_major_ticks_font(&self, major_ticks: &des::axis::Ticks) -> Result<(text::Font, f32, theme::Color), Error> { + let font_props = major_ticks.font(); + let font = super::resolve_line_font( + font_props, + text::Font::new(text::parse_font_families(defaults::FONT_FAMILY).unwrap()), + ); + let font_size = major_ticks.font().size.unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); + let color = font_props.color.unwrap_or(major_ticks.color()); + Ok((font, font_size, color)) + } + fn setup_num_ticks( &self, major_ticks: &des::axis::Ticks, @@ -521,7 +535,7 @@ where copy_from: Option<&NumTicks>, ) -> Result<NumTicks, Error> { let db: &font::Database = self.fontdb(); - let font = major_ticks.font(); + let (font, font_size, lbl_color) = self.axis_major_ticks_font(major_ticks)?; let ticks_align = side.ticks_labels_align(); let annot_align = side.annot_align(); @@ -534,8 +548,8 @@ 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.font.clone(), db)?; - let lbl = Text::from_line_text(&lbl, db, major_ticks.color())?; + let lbl = text::LineText::new(text, ticks_align, font_size, font.clone(), db)?; + let lbl = Text::from_line_text(&lbl, db, lbl_color)?; ticks.push(NumTick { loc, lbl }); } @@ -548,13 +562,13 @@ where text::LineText::new( l.to_string(), annot_align, - font.size, - font.font.clone(), + font_size, + font, db, ) }) .transpose()? - .map(|lbl| Text::from_line_text(&lbl, db, major_ticks.color())) + .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) .transpose()? }; @@ -599,7 +613,7 @@ where side: Side, ) -> Result<NumTicks, Error> { let db: &font::Database = self.fontdb(); - let font = major_ticks.font(); + let (font, font_size, lbl_color) = self.axis_major_ticks_font(major_ticks)?; let ticks_align = side.ticks_labels_align(); let annot_align = side.annot_align(); @@ -617,8 +631,8 @@ 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.font.clone(), db)?; - let lbl = Text::from_line_text(&lbl, db, major_ticks.color())?; + let lbl = text::LineText::new(text, ticks_align, font_size, font.clone(), db)?; + let lbl = Text::from_line_text(&lbl, db, lbl_color)?; ticks.push(NumTick { loc: loc.timestamp(), lbl, @@ -628,10 +642,10 @@ where let annot = lbl_formatter .axis_annotation() .map(|l| { - text::LineText::new(l.to_string(), annot_align, font.size, font.font.clone(), db) + text::LineText::new(l.to_string(), annot_align, font_size, font, db) }) .transpose()? - .map(|lbl| Text::from_line_text(&lbl, db, major_ticks.color())) + .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) .transpose()?; Ok(NumTicks { @@ -649,7 +663,7 @@ where side: Side, ) -> Result<CategoryTicks, Error> { let db: &font::Database = self.fontdb(); - let font = des.font(); + let (font, font_size, lbl_color) = self.axis_major_ticks_font(des)?; let ticks_align = side.ticks_labels_align(); @@ -658,11 +672,11 @@ where let lbl = text::LineText::new( cat.to_string(), ticks_align, - font.size, - font.font.clone(), + font_size, + font.clone(), db, )?; - let lbl = Text::from_line_text(&lbl, db, des.color())?; + let lbl = Text::from_line_text(&lbl, db, lbl_color)?; lbls.push(lbl); } @@ -673,7 +687,7 @@ where }); Ok(CategoryTicks { - font_size: font.size, + font_size, lbls, sep, }) @@ -686,7 +700,6 @@ where uses_shared: bool, spine: Option<des::plot::Border>, ) -> Result<DrawOpts, Error> { - let ticks_labels = !uses_shared; let marks = des_axis.ticks().map(|ticks| TickMark { stroke: ticks.color().into(), diff --git a/src/drawing/colorbar.rs b/src/drawing/colorbar.rs index d220ef7b..44b86e37 100644 --- a/src/drawing/colorbar.rs +++ b/src/drawing/colorbar.rs @@ -112,7 +112,13 @@ impl ColorBarBuilder { .transpose()?; let align = side.ticks_labels_align(); - let font = des.ticks_font().clone(); + let font_props = des.ticks_font().clone(); + let font = super::resolve_line_font(&font_props, defaults::FONT_FAMILY.parse().unwrap()); + 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 formatter = des::axis::ticks::Formatter::Auto; let ticks = ticks::locate_num(&self.locator, view_bounds, &self.scale)?; let formatter = @@ -123,8 +129,8 @@ impl ColorBarBuilder { .map(|t| -> Result<_, super::Error> { let text = formatter.format_label(t.into()); let lt = - text::LineText::new(text, align, font.size, font.font.clone(), ctx.fontdb())?; - let text = Text::from_line_text(<, ctx.fontdb(), font.color)?; + text::LineText::new(text, align, font_size, font.clone(), ctx.fontdb())?; + let text = Text::from_line_text(<, ctx.fontdb(), color)?; Ok((data::Sample::Num(t), text)) }) .collect::<Result<Vec<_>, _>>()?; diff --git a/src/drawing/legend.rs b/src/drawing/legend.rs index 410ca85a..425903f9 100644 --- a/src/drawing/legend.rs +++ b/src/drawing/legend.rs @@ -54,7 +54,7 @@ impl ShapeRef<'_> { #[derive(Debug, Clone)] pub struct Entry<'a> { pub label: &'a str, - pub font: Option<&'a des::legend::EntryFont>, + pub font: Option<&'a text::LineProps>, pub shape: ShapeRef<'a>, } @@ -80,7 +80,7 @@ impl LegendEntry { #[derive(Debug)] pub struct LegendBuilder<'a> { - font: des::legend::EntryFont, + font: text::LineProps, fill: Option<theme::Fill>, border: Option<theme::Stroke>, columns: Option<u32>, @@ -128,7 +128,13 @@ 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 = entry.font.unwrap_or(&self.font); + let font_props = entry.font.unwrap_or(&self.font); + 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 align = ( text::line::Align::Start, text::line::VerAlign::Middle.into(), @@ -136,11 +142,11 @@ impl<'a> LegendBuilder<'a> { let text = LineText::new( entry.label.to_string(), align, - font.size, - font.font.clone(), + font_size, + font, &self.fontdb, )?; - let text = Text::from_line_text(&text, &self.fontdb, font.color)?; + let text = Text::from_line_text(&text, &self.fontdb, color)?; self.entries.push(LegendEntry { index, shape, diff --git a/src/lib.rs b/src/lib.rs index 80bdf0a7..2b917d7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,8 +179,28 @@ pub mod geom { pub mod text { pub use plotive_text::*; /// Class properties for rich text, with `plotive` theme colors - pub type ClassProps = plotive_text::rich::ClassProps<crate::style::theme::Color>; + pub type RichProps = plotive_text::rich::ClassProps<crate::style::theme::Color>; + + /// 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<Vec<font::Family>>, + /// The font weight + pub weight: Option<font::Weight>, + /// The font width (or stretch) + pub width: Option<font::Width>, + /// The font style (normal, italic, oblique) + pub style: Option<font::Style>, + /// The font size in points + pub size: Option<f32>, + /// The color of the text + pub color: Option<crate::style::theme::Color>, + } } + #[cfg(any( feature = "noto-sans", feature = "noto-sans-italic", From 62eee5bcc136a098490842f77049947987418af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= <remi.thebault@gmail.com> Date: Mon, 29 Jun 2026 23:03:22 +0200 Subject: [PATCH 4/6] neutralize DSL compile errors --- src/dsl.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/dsl.rs b/src/dsl.rs index a37baf64..3fac4c06 100644 --- a/src/dsl.rs +++ b/src/dsl.rs @@ -47,6 +47,7 @@ //! 17 │ y-axis: "y2", Ticks //! ╰──── //! ``` +#![allow(unused)] use std::{fmt, path}; use plotive_dsl::{self, Span, ast}; @@ -260,12 +261,6 @@ fn check_opt_type(val: &ast::Struct, type_name: &str) -> Result<(), Error> { Ok(()) } -fn parse_rich_text(span: Span, fmt: String) -> Result<ParsedRichText<style::theme::Color>, Error> { - let text = text::parse_rich_text::<style::theme::Color>(&fmt) - .map_err(|err| Error::ParseRichText(span.0, err))?; - Ok(text) -} - fn parse_fig(mut val: ast::Struct) -> Result<des::Figure, Error> { check_opt_type(&val, "Figure")?; @@ -635,9 +630,10 @@ fn parse_axis(prop: ast::Prop, is_y: bool) -> Result<des::Axis, Error> { }; match val { ast::Value::Scalar(ast::Scalar { - span, - kind: ast::ScalarKind::Str(title), - }) => Ok(des::Axis::default().with_title(parse_rich_text(span, title)?.into())), + .. + }) => { + todo!("delete this module") + }, ast::Value::Scalar(ast::Scalar { kind: ast::ScalarKind::Enum(ident), @@ -742,9 +738,10 @@ fn parse_axis_seq(seq: ast::Seq, is_y: bool) -> Result<des::Axis, Error> { for scalar in seq.scalars { match scalar { ast::Scalar { - span, - kind: ast::ScalarKind::Str(title), - } => axis = axis.with_title(parse_rich_text(span, title)?.into()), + .. + } => { + todo!("delete this module") + }, ast::Scalar { kind: ast::ScalarKind::Enum(ident), span, @@ -824,8 +821,7 @@ fn parse_axis_struct(val: ast::Struct, is_y: bool) -> Result<des::Axis, Error> { for prop in val.props { match prop.name.name.as_str() { "title" => { - let (span, title) = expect_string_val(prop)?; - axis = axis.with_title(parse_rich_text(span, title)?.into()); + todo!("delete this module") } "ticks" => { axis = axis.with_ticks(parse_ticks(prop)?); From 8d6a4e9660ba4a6e1e5cf601132c20cd7c25d93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= <remi.thebault@gmail.com> Date: Mon, 29 Jun 2026 23:06:58 +0200 Subject: [PATCH 5/6] cargo fmt --- src/des.rs | 1 - src/des/colorbar.rs | 2 +- src/des/sd.rs | 12 ++++++++---- src/des/sd/legend.rs | 2 +- src/drawing/annot.rs | 5 ++--- src/drawing/axis.rs | 38 +++++++++++++------------------------- src/drawing/colorbar.rs | 3 +-- src/drawing/legend.rs | 4 +--- src/dsl.rs | 12 ++++-------- 9 files changed, 31 insertions(+), 48 deletions(-) diff --git a/src/des.rs b/src/des.rs index 03dbd5d4..5b4f196b 100644 --- a/src/des.rs +++ b/src/des.rs @@ -199,4 +199,3 @@ impl Iterator for PlotIdxIter { } impl std::iter::FusedIterator for PlotIdxIter {} - diff --git a/src/des/colorbar.rs b/src/des/colorbar.rs index 57a9b369..8bdcc63d 100644 --- a/src/des/colorbar.rs +++ b/src/des/colorbar.rs @@ -1,5 +1,5 @@ //! Color bar configuration -use crate::des::{axis, Text}; +use crate::des::{Text, axis}; use crate::style::{defaults, theme}; use crate::text; diff --git a/src/des/sd.rs b/src/des/sd.rs index b07d4cf3..122b14d9 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,6 +1,6 @@ //! Serialization and deserialization of figures -use serde::ser::{SerializeSeq, SerializeStruct, SerializeMap}; +use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct}; use super::Figure; use crate::des::{FigLegend, Plot, Subplots, Text, figure}; @@ -82,9 +82,13 @@ impl<'de> serde::Deserialize<'de> for text::LineProps { 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)) - })?); + 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() { diff --git a/src/des/sd/legend.rs b/src/des/sd/legend.rs index 1d44c368..51da2336 100644 --- a/src/des/sd/legend.rs +++ b/src/des/sd/legend.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use serde::ser::SerializeStruct; use crate::des::{Legend, figure, plot}; -use crate::{geom, text}; use crate::style::{defaults, theme}; +use crate::{geom, text}; // MARK: figure::LegendPos diff --git a/src/drawing/annot.rs b/src/drawing/annot.rs index 4a15cf33..6aa9012f 100644 --- a/src/drawing/annot.rs +++ b/src/drawing/annot.rs @@ -53,9 +53,8 @@ where Anchor::Center => (text::rich::Align::Center, text::rich::VerAlign::Center), }; let text = label.text().to_rich_text( - text::rich::TextProps::<theme::Color>::new(12.0).with_font( - defaults::FONT_FAMILY.parse().unwrap() - ), + text::rich::TextProps::<theme::Color>::new(12.0) + .with_font(defaults::FONT_FAMILY.parse().unwrap()), text::rich::Layout::Horizontal(align, ver_align, Default::default()), self.fontdb(), )?; diff --git a/src/drawing/axis.rs b/src/drawing/axis.rs index 81c5f841..95fc5c6c 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -390,10 +390,7 @@ where } height += missing_params::TICK_SIZE; height += missing_params::TICK_LABEL_MARGIN - + ticks - .font() - .size - .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); + + ticks.font().size.unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); } } let key = AxisCacheKey { @@ -515,13 +512,19 @@ where } } - fn axis_major_ticks_font(&self, major_ticks: &des::axis::Ticks) -> Result<(text::Font, f32, theme::Color), Error> { + fn axis_major_ticks_font( + &self, + major_ticks: &des::axis::Ticks, + ) -> Result<(text::Font, f32, theme::Color), Error> { let font_props = major_ticks.font(); let font = super::resolve_line_font( font_props, text::Font::new(text::parse_font_families(defaults::FONT_FAMILY).unwrap()), ); - let font_size = major_ticks.font().size.unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); + let font_size = major_ticks + .font() + .size + .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); let color = font_props.color.unwrap_or(major_ticks.color()); Ok((font, font_size, color)) } @@ -558,15 +561,7 @@ 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, font_size, font, db)) .transpose()? .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) .transpose()? @@ -641,9 +636,7 @@ 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, font_size, font, db)) .transpose()? .map(|lbl| Text::from_line_text(&lbl, db, lbl_color)) .transpose()?; @@ -669,13 +662,8 @@ where let mut lbls = Vec::with_capacity(cb.len()); for cat in cb.iter() { - let lbl = text::LineText::new( - cat.to_string(), - ticks_align, - font_size, - font.clone(), - db, - )?; + 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)?; lbls.push(lbl); } diff --git a/src/drawing/colorbar.rs b/src/drawing/colorbar.rs index 44b86e37..b681d5dd 100644 --- a/src/drawing/colorbar.rs +++ b/src/drawing/colorbar.rs @@ -128,8 +128,7 @@ impl ColorBarBuilder { .filter(|t| view_bounds.contains(*t)) .map(|t| -> Result<_, super::Error> { let text = formatter.format_label(t.into()); - let lt = - text::LineText::new(text, align, font_size, font.clone(), ctx.fontdb())?; + let lt = text::LineText::new(text, align, font_size, font.clone(), ctx.fontdb())?; let text = Text::from_line_text(<, ctx.fontdb(), color)?; Ok((data::Sample::Num(t), text)) }) diff --git a/src/drawing/legend.rs b/src/drawing/legend.rs index 425903f9..3114c0a9 100644 --- a/src/drawing/legend.rs +++ b/src/drawing/legend.rs @@ -130,9 +130,7 @@ impl<'a> LegendBuilder<'a> { let shape = entry.shape.to_shape(); let font_props = entry.font.unwrap_or(&self.font); 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 font_size = font_props.size.unwrap_or(defaults::LEGEND_LABEL_FONT_SIZE); let color = font_props.color.unwrap_or(theme::Col::Foreground.into()); let align = ( diff --git a/src/dsl.rs b/src/dsl.rs index 3fac4c06..00e342bc 100644 --- a/src/dsl.rs +++ b/src/dsl.rs @@ -629,11 +629,9 @@ fn parse_axis(prop: ast::Prop, is_y: bool) -> Result<des::Axis, Error> { return Ok(Default::default()); }; match val { - ast::Value::Scalar(ast::Scalar { - .. - }) => { + ast::Value::Scalar(ast::Scalar { .. }) => { todo!("delete this module") - }, + } ast::Value::Scalar(ast::Scalar { kind: ast::ScalarKind::Enum(ident), @@ -737,11 +735,9 @@ fn parse_axis_seq(seq: ast::Seq, is_y: bool) -> Result<des::Axis, Error> { let mut axis = des::Axis::default(); for scalar in seq.scalars { match scalar { - ast::Scalar { - .. - } => { + ast::Scalar { .. } => { todo!("delete this module") - }, + } ast::Scalar { kind: ast::ScalarKind::Enum(ident), span, From b021dbc8a190511569a9c4dd80e176b226f07b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= <remi.thebault@gmail.com> Date: Mon, 29 Jun 2026 23:09:21 +0200 Subject: [PATCH 6/6] fix iris title example --- examples/iris.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/iris.rs b/examples/iris.rs index 24526f49..eb672adb 100644 --- a/examples/iris.rs +++ b/examples/iris.rs @@ -95,7 +95,7 @@ fn main() { &virginica_petal_length as &dyn data::Column, ); - let title: des::figure::Title = "Iris dataset".into(); + let title = "Iris dataset"; let x_axis = des::Axis::new() .with_title("Sepal Length [cm]".into()) @@ -133,7 +133,7 @@ fn main() { .with_y_axis(y_axis) .with_legend(des::plot::LegendPos::InBottomRight.into()); - let fig = des::Figure::new(plot.into()).with_title(title); + let fig = des::Figure::new(plot.into()).with_title(title.into()); common::save_figure(&fig, &source, None, "iris"); }