]> git.proxmox.com Git - ui/proxmox-yew-widget-toolkit.git/commitdiff
props: add WidgetStyleBuilder trait
authorDominik Csapak <d.csapak@proxmox.com>
Thu, 20 Jun 2024 07:18:28 +0000 (09:18 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Thu, 20 Jun 2024 07:40:24 +0000 (09:40 +0200)
for having 'style' and 'set_style' functions on components that have
a WidgetBuilder already, makes it easier to set the style of
components, so instead of writing:

```
Component::new()
    .attribute("style", "style1: value1; style2: value2")
```

one can now do

```
Component::new()
    .style("style1", "value1")
    .style("style2", "value2")
```

making it much more obvious what is happening and easier if setting
multiple styles.

This also adds a shorthand for width and height, with a custom
CssLength enum that represents different css length units
and defaults to px if only a number is given

The CssStyles struct is there to hold the styles and generate the final
attribute string to use in components.

It implements a From trait for AsRef<str>, but that implmenetation is
not fully html/css spec conform as it leaves out some unused syntax.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
src/props/css_styles.rs [new file with mode: 0644]
src/props/mod.rs
src/props/widget_std_props.rs
src/props/widget_style_builder.rs [new file with mode: 0644]

diff --git a/src/props/css_styles.rs b/src/props/css_styles.rs
new file mode 100644 (file)
index 0000000..4b2ba1f
--- /dev/null
@@ -0,0 +1,79 @@
+use indexmap::IndexMap;
+use yew::{html::IntoPropValue, AttrValue};
+
+/// Holds the CSS styles to set on elements
+#[derive(Clone, Default, Debug, PartialEq)]
+pub struct CssStyles {
+    styles: IndexMap<AttrValue, AttrValue>,
+}
+
+impl CssStyles {
+    /// Method to set style attributes
+    ///
+    /// Note: Value 'None' removes the attribute.
+    /// Note: In debug mode, panics on invalid characters (';' and ':')
+    pub fn set_style(
+        &mut self,
+        key: impl Into<AttrValue>,
+        value: impl IntoPropValue<Option<AttrValue>>,
+    ) {
+        let key = key.into();
+        #[cfg(debug_assertions)]
+        if key.contains(|x| x == ';' || x == ':') {
+            panic!("invalid character in style key: '{key}'");
+        }
+        if let Some(value) = value.into_prop_value() {
+            #[cfg(debug_assertions)]
+            if value.contains(|x| x == ';' || x == ':') {
+                panic!("invalid character in style value '{value}' for '{key}'");
+            }
+            self.styles
+                .insert(AttrValue::from(key), AttrValue::from(value));
+        } else {
+            self.styles.swap_remove(&AttrValue::from(key));
+        }
+    }
+
+    /// Method to compile the finished style attribute to use
+    ///
+    /// Optionally takes an additional [yew::AttrValue] to append
+    pub fn compile_style_attribute(&self, additional_style: Option<AttrValue>) -> AttrValue {
+        let mut style = String::new();
+
+        for (key, value) in self.styles.iter() {
+            style += &format!("{key}: {value};");
+        }
+
+        if let Some(additional_style) = additional_style {
+            style += &additional_style;
+        }
+
+        AttrValue::from(style)
+    }
+}
+
+// not completely spec compliant, since we ignore 'at-rules', but there are
+// no valid ones currently, so this should not be an issue
+// https://w3c.github.io/csswg-drafts/css-style-attr/
+impl<T: AsRef<str>> From<T> for CssStyles {
+    fn from(value: T) -> Self {
+        let mut this: CssStyles = Default::default();
+        for rule in value.as_ref().split(';') {
+            if let Some((key, val)) = rule.split_once(':') {
+                this.set_style(key.to_owned(), val.to_owned());
+            }
+        }
+        this
+    }
+}
+
+/// Trait which provides mutable access to the style property.
+pub trait AsCssStylesMut {
+    fn as_css_styles_mut(&mut self) -> &mut CssStyles;
+}
+
+impl AsCssStylesMut for CssStyles {
+    fn as_css_styles_mut(&mut self) -> &mut CssStyles {
+        self
+    }
+}
index 2083a4067f40c372b1698c8f957ecdc1bdeb05b2..3c5cc9aa5b2c06e87d7ea7c9528f3b5db898c03a 100644 (file)
@@ -126,6 +126,12 @@ pub use widget_std_props::WidgetStdProps;
 mod widget_builder;
 pub use widget_builder::WidgetBuilder;
 
+mod css_styles;
+pub use css_styles::{AsCssStylesMut, CssStyles};
+
+mod widget_style_builder;
+pub use widget_style_builder::{CssLength, WidgetStyleBuilder};
+
 mod container_builder;
 pub use container_builder::ContainerBuilder;
 
index 9c00149b70e5ebeb2fdd04b6f268f0bafca52cfb..902693709c6b71d74ee5e280504bd8f19ec0df98 100644 (file)
@@ -4,7 +4,7 @@ use yew::html::IntoPropValue;
 use yew::prelude::*;
 use yew::virtual_dom::{ApplyAttributeAs, Attributes, Key, Listeners, VList, VNode, VTag};
 
-use crate::props::ListenersWrapper;
+use crate::props::{AsCssStylesMut, CssStyles, ListenersWrapper};
 
 /// Standard widget properties.
 #[derive(PartialEq, Debug, Default, Clone)]
@@ -20,6 +20,9 @@ pub struct WidgetStdProps {
 
     /// Additional Html attributes.
     pub attributes: Attributes,
+
+    /// Additional CSS styles
+    pub styles: CssStyles,
 }
 
 impl WidgetStdProps {
@@ -56,6 +59,15 @@ impl WidgetStdProps {
             (class.into_prop_value(), ApplyAttributeAs::Attribute),
         );
 
+        let style = self
+            .styles
+            .compile_style_attribute(attr_map.get("style").map(|a| a.0.clone()));
+
+        attr_map.insert(
+            AttrValue::Static("style"),
+            (style, ApplyAttributeAs::Attribute),
+        );
+
         attributes
     }
 
@@ -90,3 +102,9 @@ impl WidgetStdProps {
         )
     }
 }
+
+impl AsCssStylesMut for WidgetStdProps {
+    fn as_css_styles_mut(&mut self) -> &mut CssStyles {
+        &mut self.styles
+    }
+}
diff --git a/src/props/widget_style_builder.rs b/src/props/widget_style_builder.rs
new file mode 100644 (file)
index 0000000..691ba56
--- /dev/null
@@ -0,0 +1,134 @@
+use std::fmt::Display;
+
+use yew::{html::IntoPropValue, AttrValue};
+
+use crate::props::{AsCssStylesMut, CssStyles};
+
+/// CSS length in pixel, em or percentage.
+#[derive(Copy, Clone, PartialEq)]
+pub enum CssLength {
+    Px(f64),
+    Em(f64),
+    Fraction(f32),
+    None,
+}
+
+impl Default for CssLength {
+    fn default() -> Self {
+        CssLength::Px(0.0)
+    }
+}
+
+impl Display for CssLength {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        match self {
+            CssLength::Px(v) => write!(f, "{v}px"),
+            CssLength::Em(v) => write!(f, "{v}em"),
+            CssLength::Fraction(v) => write!(f, "{}%", v * 100.0),
+            CssLength::None => Ok(()),
+        }
+    }
+}
+
+impl From<f32> for CssLength {
+    fn from(v: f32) -> CssLength {
+        CssLength::Px(v as f64)
+    }
+}
+
+impl From<f64> for CssLength {
+    fn from(v: f64) -> CssLength {
+        CssLength::Px(v)
+    }
+}
+
+impl From<usize> for CssLength {
+    fn from(v: usize) -> CssLength {
+        CssLength::Px(v as f64)
+    }
+}
+
+impl From<i32> for CssLength {
+    fn from(v: i32) -> CssLength {
+        CssLength::Px(v as f64)
+    }
+}
+
+impl From<CssLength> for AttrValue {
+    fn from(v: CssLength) -> Self {
+        v.to_string().into()
+    }
+}
+
+impl IntoPropValue<Option<AttrValue>> for CssLength {
+    fn into_prop_value(self) -> Option<AttrValue> {
+        match self {
+            CssLength::None => None,
+            other => Some(other.into()),
+        }
+    }
+}
+
+// macro to generate the trait functions
+
+macro_rules! generate_style_trait_fn {
+    ($func:ident, $builder:ident, $name:literal) => {
+        /// Builder style method to set the $name of the element style.
+        ///
+        /// Note: Value [CssLength::None] removes it.
+        fn $builder(mut self, value: impl Into<CssLength>) -> Self {
+            self.$func(value);
+            self
+        }
+
+        /// Sets the $name of the element style.
+        ///
+        /// Note: Value [CssLength::None] removes it.
+        fn $func(&mut self, value: impl Into<CssLength>) {
+            self.as_css_styles_mut().set_style($name, value.into());
+        }
+    };
+}
+
+pub trait WidgetStyleBuilder: AsCssStylesMut + Sized {
+    /// Builder style method to override all styles for the element with the given ones
+    fn styles(mut self, styles: CssStyles) -> Self {
+        self.set_styles(styles);
+        self
+    }
+
+    /// Overrides all styles for the element with the given ones
+    fn set_styles(&mut self, styles: CssStyles) {
+        *self.as_css_styles_mut() = styles;
+    }
+
+    /// Builder style method to set additional css styles via the 'style' attribute
+    ///
+    /// Note: Value 'None' removes the style.
+    fn style(
+        mut self,
+        key: impl Into<AttrValue>,
+        value: impl IntoPropValue<Option<AttrValue>>,
+    ) -> Self {
+        self.set_style(key, value);
+        self
+    }
+
+    /// Method to set additional css styles via the 'style' attribute
+    ///
+    /// Note: Value 'None' removes the style.
+    fn set_style(
+        &mut self,
+        key: impl Into<AttrValue>,
+        value: impl IntoPropValue<Option<AttrValue>>,
+    ) {
+        self.as_css_styles_mut().set_style(key, value)
+    }
+
+    generate_style_trait_fn!(set_width, width, "width");
+    generate_style_trait_fn!(set_min_width, min_width, "min-width");
+    generate_style_trait_fn!(set_max_width, max_width, "max-width");
+    generate_style_trait_fn!(set_height, height, "height");
+    generate_style_trait_fn!(set_min_height, min_height, "min-height");
+    generate_style_trait_fn!(set_max_height, max_height, "max-height");
+}