#css #style #web #css-in-rust #inline-style

rusty-css

rusty-css 是一种在 Rust 中编写 CSS 的方法,无需离开 Rust 语法或使属性不可访问

2 个版本

0.1.3 2023 年 1 月 12 日
0.1.0 2022 年 11 月 3 日

#2048 in 网页编程

MIT 协议

32KB
209

rusty-css

Rust CI badge

在 Rust 中实现 CSS 的一种锈迹斑斑的方式

rusty-css 提供了一种以熟悉的方式创建和导出 CSS 样式的解决方案,但无需离开 Rust 语法。您可以逐个访问和操作您定义的每个值。

标识符按原样取用并转换为字符串,唯一的例外是下划线令牌 (_),当结构体要转换为相应的 CSS 值时,它会被转换为减号令牌 (-)。

示例

struct A {
    css_property: "css value" 
}

将被转换为

"css-property: css value"

无论属性名称或值的有效性如何。如果您的 CSS 中有错误,它仍然可以编译!

路线图

  • 将 Rust 结构体渲染为内联 CSS 代码
    • 支持深层嵌套
  • 从内联 CSS 设置结构体的值
  • 更可靠地从 String 中提取数值
  • 支持类
    • 支持查询
  • 在编译时验证编写的 CSS 代码
    • 根据 CSS 规范自动实现默认样式结构体
  • 具有严格类型(例如,为给定属性的每个可能单位使用枚举)的第二层系统实现
  • 更少的样板代码,更多抽象

如何使用

目前,此包使用 bevy_reflect crate 将结构体转换为 CSS "属性:值" 字符串,因此您希望转换的所有结构体都必须派生自 Reflect

use bevy_reflect::{Reflect};

#[derive(Reflect)]
struct ExampleStruct {
    width: String,
    height: String,
    background: String,
    transform: String,
}

要将此结构体转换为内联 CSS 字符串,我们首先需要实现该结构体及其初始状态。为此,我们实现此包中的 Style Trait。

use rusty_css::*;

impl Style for ExampleStruct {
    fn create() -> Self {
        // return an instance of Self, so in this case an instance of ExampleStruct
        Self { 
            width: "4em".to_string(),
            height: "2rem".to_string(),
            background: "rgb(69,13,37)".to_string(),
            transform: "skewX(20deg) skewY(30deg)".to_string(),
        }
    }
}

现在我们可以创建一个 ExampleStruct 实例并将其转换为 CSS 内联字符串。


let example_struct = ExampleStruct::create();
let inline_css: String = example_struct.inline();
// "width: 4em; height: 2rem; background: rgb(69,13,37); transform: skewX(20deg) skewY(30deg);"

开发者体验改进

由于可能很难访问可以接受多个值(如 transform)的属性值,我们可以在原始结构体中实现嵌套结构体。通过这样做,第二层结构体的字段不再被视为 CSS 属性,而是作为接受参数的 CSS 函数。

#[allow(non_snake_case)] // so the skewX field doesn't throw a warning for being in snake case, which css uses
#[derive(Reflect)]
struct NestedTransformStruct {
    skewX: String,
    skewY: String,
}

#[derive(Reflect)]
struct ExampleStruct {
    width: String,
    height: String,
    background: String,
    transform: NestedTransformStruct,
}

impl Style for ExampleStruct {
    fn create() -> Self {
        // return an instance of Self, so in this case an instance of ExampleStruct
        Self { 
            width: "4em".to_string(),
            height: "2rem".to_string(),
            background: "rgb(69,13,37)".to_string(),
            transform: NestedTransformStruct {
                skewX: "20deg".to_string(),
                skewY: "30deg".to_string(),
            },
        }
    }
}

let example_struct = ExampleStruct::create();
let inline_css: String = example_struct.inline();
let skewX: String = example_struct.transform.skewX; // can access this field, wuhu!
// "width: 4em; height: 2rem; background: rgb(69,13,37); transform: skewX(20deg) skewY(30deg);"

输出相同,但现在我们可以单独访问 skewX 和 skewY 字段。按照这个逻辑,您应该能够以类似的方式编写背景字段的值,让我们试试

#[derive(Reflect)]
struct Background {
    rgb: String,
}

#[derive(Reflect)]
struct ExampleStruct {
    width: String,
    height: String,
    background: Background,
}

impl Style for ExampleStruct {
    fn create() -> Self {
        // return an instance of Self, so in this case an instance of ExampleStruct
        Self { 
            width: "4em".to_string(),
            height: "2rem".to_string(),
            background: Background {
                rgb: "69,13,37".to_string(),
            }
        }
    }
}

let example_struct = ExampleStruct::create();
let inline_css: String = example_struct.inline();
// "width: 4em; height: 2rem; background: rgb(69,13,37);"

一切正常!

您可能已经注意到我们正在附加很多 .to_string() 调用。在扩展规模时,这可能会变得相当繁琐,因此我创建了 append_to_string crate,它有助于解决这个问题。

完整示例

解决所有这些问题后,下面是您的代码可能的样子

use rusty_css::*;
use append_to_string::*;
use bevy_reflect::{Reflect};

// define all the structs we want to be css-ified 

#[allow(non_snake_case)]
#[derive(Reflect)]
struct NestedTransformStruct {
    skewX: String,
    skewY: String,
}

#[derive(Reflect)]
struct ExampleStruct {
    width: String,
    height: String,
    background: String,
    transform: NestedTransformStruct,
}

impl Style for ExampleStruct {
    fn create() -> Self {
        // return an instance of Self, so in this case an instance of ExampleStruct
        append_to_string!( 
            Self { 
                width: "4em",
                height: "2rem",
                background: "rgb(69,13,37)",
                transform: NestedTransformStruct {
                    skewX: "20deg",
                    skewY: "30deg",
                },
            }
        )
    }
}

let example_struct = ExampleStruct::create();
let inline_css: String = example_struct.inline();
// "width: 4em; height: 2rem; background: rgb(69,13,37); transform: skewX(20deg) skewY(30deg);"

将样式实现为类

使用您的结构体的 .as_class() 函数来将其样式导出到应用中的 <style> 标签。目前,您需要传递您想要导出样式的 web_sys::Document 的引用,如下所示

let style_struct = ExampleStruct::create();

// grab the current document
let window = web_sys::window().expect("No global `window` found");
let document = window.document().expect("couldn't get `document");

// export the style to the <style> tag
let style_struct = ExampleStruct::create();
let class_name = style_struct.as_class(&document).unwrap();

assert_eq!("ExampleStruct", class_name); //true

在一个 yew 组件中,它可能看起来像这样

fn view(&self, ctx: &Context<Self>) -> Html {
    let window = window().expect("No global `window` found");
    let document = window.document().expect("couldn't get `document");

    let class_name = self.style.as_class(&document).unwrap();

    html! {
        <div class={class_name}
        </div>
    }
}

支持类型

以下结构体将用作嵌套结构体的示例

#[derive(Reflect, FromReflect)]
struct EvenFurtherNestedStruct {
    further_vec_field: Vec<String>,
}

#[derive(Reflect, FromReflect)]
struct NestedStruct {
    nested_vec_field: Vec<String>,
    nested_string_field: String,
    nested_struct_field: EvenFurtherNestedStruct,
}

注意,如果您想要使用结构体向量,结构体除了 Reflect 之外还必须派生 FromReflect

#[derive(Reflect)]
struct Struct {
    string_field: String, 
    //string-field: String;

    struct_field: NestedStruct, 
    //struct-field: 
    //    nested-string-field(String)
    //    nested-vec-field(
    //        String1,
    //        String2,
    //        String3,
    //        String...
    //    )
    //    nested-struct-field(
    //        further-vec-field(
    //            String1,
    //            String2,
    //            String...
    //        )
    //    )
    //; 

    vec_field_string: Vec<String>, 
    //vec-field-string: String1, String2, String...;

    vec_field_struct: Vec<NestedStruct>, //
}   //vec_field_struct: 
    //    nested-string-field(String)
    //    nested-vec-field(...)
    //    nested-struct-field(...)
    //    ,
    //     nested-string-field(String)
    //    nested-vec-field(...)
    //    nested-struct-field(...)
    //    ,
    //    ...
    //;

嵌套结构体在实现类似渐变背景等功能时非常有用

background-image: radial-gradient(rgb(0,46,255), rgb(0,255,64), rgb(255,255,0));

在 Rust 中手动实现类似此功能,并且能够直接访问值,可能看起来像这样

#[derive(Reflect, FromReflect)]
struct RGB {
    rgb: Vec<String>
}


#[derive(Reflect)]
struct RG {
    radial_gradient: Vec<RGB>
}


#[derive(Reflect)]
struct BG {
    background_image: RG,
}

impl Style for BG {
    fn create() -> Self {
        Self {
            background_image: RG {
                radial_gradient: vec![
                    RGB { 
                        rgb: vec![
                            "0".to_string(), 
                            "46".to_string(), 
                            "255".to_string()
                        ]
                    }, 
                    RGB { 
                        rgb: vec![
                            "0".to_string(), 
                            "255".to_string(), 
                            "64".to_string()
                        ]
                    }, 
                    RGB { 
                        rgb: vec![
                            "255".to_string(), 
                            "255".to_string(), 
                            "0".to_string()
                        ]
                    }, 
                ]
            }
        }
    }
}

如果您这样做,您可以像这样访问单个值

fn main() {
    let bg = BG::create(); // create struct
    let rgbs = bg.background_image.radial_gradient; // get vec of rgbs
    let first_color = rgbs[0] // get the first color of the gradient
    first_color[0] = 255 // change the value of the red channel of the first color of the gradient
}

从 CSS 中直接获取值

您还可以使用 set_from_inline_string() 方法从 CSS 字符串设置任何上述结构体的值。但是,您必须小心匹配 CSS 字符串与您的结构体结构。例如,您可以在 tests/from_string.rs 文件中查看示例。

包实现

trait ExtractNums {
    // tries to extract the numbers from whatever string is given.
    fn try_extract_nums(&self) -> Option<String>;

    // tries to cast any given string to an f64
    fn try_to_f64(&self) -> Result<f64, ParseFloatError>;
}

trait Style {
    // returns the inline css representaton of a struct
    fn inline(&self) -> String 

    // Sets a structs' fields' values equivalent to the given inline css string
    // Struct { struct_field: "value" }.set_from_inline_string("struct-field: different value");
    // will result in:
    // Struct { struct_field: "different value"}
    // (also works for nested structs)
    fn set_from_inline_string(&self, style: String) -> &Self

    // - returns the class name to put into the class attribute
    // - inserts the style as a class into the style sheet
    fn as_class(&mut self, document: &Document) -> Result<String, &'static str> ;

    // retruns the struct as a css class String like so: .StructIdent { property: value }
    fn as_class_string(&mut self, mut class_name: String) -> Result<String, &'static str>;

    // returns the struct name
    fn get_struct_name(&self) -> Result<String, &'static str>;

    // logs the Reflects of the given objects fields to the browser console with wasm_logger 
    fn debug(self) -> Self;

    //----------internal funcs----------//

    // retruns the String Representation of a fields value
    fn create_value_string(reflect: &dyn Reflect) -> String;

    // sets the value of a String field to the value side of a property in a css string (e.g. prop: >value<)
    fn set_string_reflect(string_reflect: &mut dyn Reflect, value: &str);

    // sets the value of a Struct field to the value side of a property in a css string (e.g. prop: >field(value)<)
    fn set_struct_reflect(struct_reflect: &mut dyn Struct, value: &str);

    // sets the value of a Vec (bevy_reflect Lists in general) field to the value side of a property in a css string (e.g. prop: >1,2,3,4<)
    fn set_list_reflect(list_reflect: &mut dyn List, value: &str)
}

保留字段名称

append: "this will be appended to the class name" // for classes that are supposed to be exported with a psuedo-class (i.e. :before, :active, etc.) 

依赖关系

~14–23MB
~322K SLoC