2 个不稳定版本

0.6.0 2021 年 3 月 6 日
0.5.1 2020 年 5 月 9 日

#605 in GUI

MIT/Apache

230KB
6K SLoC

Savory

Rust / Wasm 前端库,用于构建用户界面。

master docs · crate info · pipeline · rustc version · unsafe forbidden

Savory 是一个基于 Seed 的用户界面构建库

文档

特性

  • 设计系统:元素完全使用 DesignSystem 样式化。
  • 可复用性:元素高度可复用/可组合。
  • 解耦开发:设计系统可以在不接触元素代码的情况下独立开发,同样,元素开发也可以独立于设计系统进行,这要归功于 DesignSystemImpl 特性。
  • 干净的视图:以干净和声明性的方式构建您的视图,不再需要任何宏。
  • 基于特性的:拥抱 Rust 特性系统,所有 Savory 元素都实现了 Element 和/或 View 特性。
  • 类型化 HTML:使用类型化的 CSS 和 HTML 属性,Savory 尽量不依赖于字符串来创建 CSS 和 HTML 属性,因为这些可能导致难以调试的错误。
  • UI 元素集合:Savory 随带了一系列可复用和可主题化的 UI 元素。
  • 增强 Seed API:对 Seed API 的增强,使得使用 NodeOrders 更加有趣。

Savory 努力使编写 UI 元素变得有趣且无需样板代码。

截图

Screenshot

入门

快速入门的最快方法是遵循 Savory 入门 仓库说明。

示例

在这里,我们将创建一个与Elm教程中相同的计数器应用程序,然后我们将编写同一个应用程序,但使用具有样式和可重用元素的。

简单计数器

use savory::prelude::*;

// app element (the model)
pub struct Counter(i32);

// app message
pub enum Msg {
    Increment,
    Decrement,
}

impl Element for Counter {
    type Message = Msg;
    type Config = Url;

    // initialize the app in this function
    fn init(_: Url, _: &mut impl Orders<Msg>) -> Self {
        Self(0)
    }

    // handle app messages
    fn update(&mut self, msg: Msg, _: &mut impl Orders<Msg>) {
        match msg {
            Msg::Increment => self.0 += 1,
            Msg::Decrement => self.0 -= 1,
        }
    }
}

impl View<Node<Msg>> for Counter {
    // view the app
    fn view(&self) -> Node<Msg> {
        let inc_btn = html::button().push("Increment").on_click(|_| Msg::Increment);
        let dec_btn = html::button().push("Decrement").on_click(|_| Msg::Decrement);

        html::div()
            .push(inc_btn)
            .push(self.0.to_string())
            .push(dec_btn)
    }
}

#[wasm_bindgen(start)]
pub fn view() {
    // mount and start the app at `app` element
    Counter::start();
}

预览:截图

源代码

作为元素的计数器

现在我们将创建计数器元素和应用程序元素,这展示了如何创建父元素和子元素,以及如何创建可重用和可定制的元素。

use savory::prelude::*;
use savory_elements::prelude::*;
use savory_style::{
    css::{unit::px, values as val, Color, St},
    prelude::*,
};

#[derive(Element)]
pub struct Counter {
    #[element(config(default = "10"))]
    value: i32,
}

pub enum Msg {
    Increment,
    Decrement,
}

impl Element for Counter {
    type Message = Msg;
    type Config = Config;

    fn init(config: Self::Config, _: &mut impl Orders<Msg>) -> Self {
        Self {
            value: config.value,
        }
    }

    fn update(&mut self, msg: Msg, _: &mut impl Orders<Msg>) {
        match msg {
            Msg::Increment => self.value += 1,
            Msg::Decrement => self.value -= 1,
        }
    }
}

impl View<Node<Msg>> for Counter {
    fn view(&self) -> Node<Msg> {
        // sharde style for buttons
        let style_btns = |conf: css::Style| {
            conf.push(St::Appearance, val::None)
                .background(Color::SlateBlue)
                .text(Color::White)
                .and_border(|conf| conf.none().radius(px(4)))
                .margin(px(4))
                .padding(px(4))
        };

        // increment button node
        let inc_btn = html::button()
            .class("inc-btn")
            .and_style(style_btns)
            .on_click(|_| Msg::Increment)
            .push("Increment");

        // decrement button node
        let dec_btn = html::button()
            .class("dec-btn")
            .and_style(style_btns)
            .on_click(|_| Msg::Decrement)
            .push("Decrement");

        // contianer node
        html::div()
            .push(dec_btn)
            .push(self.value.to_string())
            .push(inc_btn)
    }
}

// convenient way to convert Config into Counter
impl Config {
    pub fn init(self, orders: &mut impl Orders<Msg>) -> Counter {
        Counter::init(self, orders)
    }
}

// App Element ---

pub enum AppMsg {
    Counter(Msg),
}

pub struct MyApp {
    counter_element: Counter,
}

impl Element for MyApp {
    type Message = AppMsg;
    type Config = Url;

    fn init(_: Url, orders: &mut impl Orders<AppMsg>) -> Self {
        Self {
            counter_element: Counter::config()
                // give it starting value. 10 will be used as default value if
                // we didn't pass value
                .value(100)
                .init(&mut orders.proxy(AppMsg::Counter)),
        }
    }

    fn update(&mut self, msg: AppMsg, orders: &mut impl Orders<AppMsg>) {
        match msg {
            AppMsg::Counter(msg) => self
                .counter_element
                .update(msg, &mut orders.proxy(AppMsg::Counter)),
        }
    }
}

impl View<Node<AppMsg>> for MyApp {
    fn view(&self) -> Node<AppMsg> {
        self.counter_element.view().map_msg(AppMsg::Counter)
    }
}

#[wasm_bindgen(start)]
pub fn view() {
    // mount and start the app at `app` element
    MyApp::start();
}

预览:截图

源代码

在这个示例中有许多事情发生,首先我们创建了一个元素结构Counter,并定义了其属性、事件和样式类型,这一切都是通过Element宏完成的,我们将在稍后解释它是如何工作的,然后我们定义了一个包含计数器元素的应用程序元素,并在init函数中初始化它。最后,我们只需调用start方法来挂载和启动应用程序。

使用Savory Elements的计数器!

Savory附带了一系列元素,我们将使用它们来构建计数器应用程序并查看Savory元素为我们提供了哪些功能。

待办事项:添加示例

生态系统

许可证

根据您选择,受Apache许可证版本2.0MIT许可证的许可。

除非您明确声明,否则您提交给Savory的任何贡献,根据Apache-2.0许可证的定义,将如上双许可,不附加任何额外条款或条件。

依赖关系

~19MB
~343K SLoC