AlexZhang

AlexZhang

【譯】盤點在 WASM 世界中湧現的幾十個 Rust GUI

省流: Rust GUI 架構的聖杯還未被發現。
原文:https://monadical.com/posts/shades-of-rust-gui-library-list.html#The-current-toolkit
译者:GPT-3.5

Rust 中的 GUI 發展速度前所未有地快,Rust GUI 領域的三個月就相當於凡人世界的三年。為了衡量一下,Web 的 GUI 發展得如此之快,以至於它甚至領先於瀏覽器。這可能有很多原因,其中包括 Rust 的跨平台性質以及 WebAssembly 支持,它提供了更容易的軟體分發方式。

在本文中,我將回顧 Rust 中當前的 GUI 工具包,並分享一些構建 WebAssembly bundle 的技巧。由於 Rust GUI 領域的快速發展,下面我所概述的許多工具可能會在不久的將來過時。因此,為了避免您在閱讀本文期間就過時了,讓我們先深入了解到目前為止我所發現的最新的 Rust GUI 架構。

工具包#

在介紹工具包之前,請注意我在此處多次提到了 Elm Architecture 和  Immediate Mode,儘管它們的定義超出了本篇文章的範圍。但是,如果您有 Redux 的經驗,可以將 Elm Architecture 大致看作類似於 Redux,而 Immediate Mode 則比 Elm Architecture 更加命令式。

值得一提的是,現在許多庫都開始使用 “信號” 響應式語義,您可以直接修改狀態變量,這將自動反映在 UI 上,就像我們在 Angular 1 時代一樣。

現在來看看工具包。

Dioxus#

Dioxus 具有類似於 React 的介面架構,但是它是基於 Rust 的。對於桌面應用,它使用 Tauri,在 Web 上有重點,具有實驗性的 Terminal 支持,並具有用於生成伺服器端標記的 SSR 功能。Dioxus 還使用類似於 React 的內部可變性來處理數據,例如使用 use_state。由於它通過類似於 useContext 的 API 進行全局狀態管理,因此可以通過它集成 潛在的類 Redux 工具(“redux/recoil/mobx on top of context”)。下面是使用 Dioxus 構建的示例小部件:

Screenshot 2023-04-27 at 14.15.44

// main.rs

fn main() {
   // launch the web app
   dioxus_web::launch(App);
}

fn echo_to_angle(s: &str) -> u32 {
   let mut angle = 0;
   for c in s.chars() {
       angle += c as u32 % 360;
   }
   angle
}

fn App(cx: Scope) -> Element {
   let echo = use_state(&cx, || "Echo".to_string());
   let angle = use_state(&cx, || echo_to_angle(&echo.get()));
   cx.spawn({
       let mut angle = angle.to_owned();
       async move {
           TimeoutFuture::new(50).await;
           angle.with_mut(|a| {
               *a += 1;
               *a = *a % 360;
           });
       }
   });
   cx.render(rsx! {
       div {
           div {
               "Angle: {angle}"
           }
           div {
               transform: "rotateX({angle}deg) rotateY({angle}deg)",
               width: "100px",
               height: "100px",
               transform_style: "preserve-3d",
               position: "relative",
               div {
                   position: "absolute",
                   width: "100%",
                   height: "100%",
                   background: "red",
                   transform: "translateZ(50px)",
               }
               // more cube sides CSS definitions omitted
               div {
                   position: "absolute",
                   width: "100%",
                   height: "100%",
                   background: "orange",
                   transform: "rotateY(90deg) translateZ(50px)",
               }
           }
       }
   })
}

Tauri#

Tauri 是一個用 Rust 編寫的框架,用於創建桌面應用程序(移動平台即將推出),類似於 Electron。Tauri 為應用程序提供了自己的窗口介面,並通過自定義的基於 Promise 的協議讓您運行與 Rust 通信的 TS 代碼。Tauri 的目的不是為 Web 構建,儘管它利用 Web 技術用於桌面應用。您可以使用許多前端庫,如 React(在其初始化腳本中提供),或者只是純淨的。

<!-- index.html -->

<div class="row">
   <div>
       <input id="greet-input" placeholder="Enter a name..."/>
       <button id="greet-button" type="button">Greet</button>
   </div>
</div>
// main.ts

import { invoke } from "@tauri-apps/api/tauri";

let greetInputEl: HTMLInputElement | null;
let greetMsgEl: HTMLElement | null;

async function greet() {
 if (greetMsgEl && greetInputEl) {
   // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
   greetMsgEl.textContent = await invoke("greet", {
     name: greetInputEl.value,
   });
 }

}

window.addEventListener("DOMContentLoaded", () => {
 greetInputEl = document.querySelector("#greet-input");
 greetMsgEl = document.querySelector("#greet-msg");
 document
   .querySelector("#greet-button")
   ?.addEventListener("click", () => greet());
});
//main.rs

#[tauri::command]
fn greet(name: &str) -> String {
   format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
   tauri::Builder::default()
       .invoke_handler(tauri::generate_handler![greet])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
}

Xilem#

Xilem 是一種實驗性但非常有前途的數據優先的 UI 架構,與 Rust 的語言架構非常契合,是 Druid 的繼任者。庫的作者 Raph Levien 表示,“Xilem 旨在成為 Rust 的首選 UI 庫,從 SwiftUI 中吸取了很多靈感,並且嚴重關注性能。” 請查看他關於 Rust UI 架構 的完整文章。在我看來,這是目前為止關於這個主題的最好的文章。

基於這篇文章,自然的決定是選擇中心化狀態,就像 Elm 架構一樣。該庫使用自己的渲染器,並即將開始大量利用 GPU 功能以實現更快的渲染。該項目目前處於非常早期的實驗階段。它目前針對的是 WebGPU,這在瀏覽器中運行起來還存在問題,但它有很大的潛力。

Iced#

Iced 是一款 “跨平台 GUI 庫,專注於簡單性和類型安全性”。它使用 Elm 架構和響應式編程模型。默認情況下,Web 構建使用 DOM 進行渲染,但 Iced 也可以與其他後端一起使用,例如 wgpu 或 glow。目前,該庫的 Web 部分似乎有些被忽略了(https://github.com/iced-rs/iced_web),而且已經有一段時間沒有更新了。但希望他們能夠加速更新。在這裡,他們構建的貝塞爾工具使用了 Canvas:

// main.rs

//! This example showcases an interactive `Canvas` for drawing Bézier curves.
use iced::widget::{button, column, text};
use iced::{Alignment, Element, Length, Sandbox, Settings};

pub fn main() -> iced::Result {
   Example::run(Settings {
       antialiasing: true,
       ..Settings::default()
   })
}

#[derive(Default)]
struct Example {
   bezier: bezier::State,
   curves: Vec<bezier::Curve>,
}

#[derive(Debug, Clone, Copy)]
enum Message {
   AddCurve(bezier::Curve),
   Clear,
}

impl Sandbox for Example {
   type Message = Message;

   fn new() -> Self {
       Example::default()
   }

   fn title(&self) -> String {
       String::from("Bezier tool - Iced")
   }

   fn update(&mut self, message: Message) {
       match message {
           Message::AddCurve(curve) => {
               self.curves.push(curve);
               self.bezier.request_redraw();
           }
           Message::Clear => {
               self.bezier = bezier::State::default();
               self.curves.clear();
           }
       }
   }

   fn view(&self) -> Element<Message> {
       column![
           text("Bezier tool example").width(Length::Shrink).size(50),
           self.bezier.view(&self.curves).map(Message::AddCurve),
           button("Clear").padding(8).on_press(Message::Clear),
       ]
           .padding(20)
           .spacing(20)
           .align_items(Alignment::Center)
           .into()
   }
}

mod bezier {
   use iced::mouse;
   use iced::widget::canvas::event::{self, Event};
   use iced::widget::canvas::{
       self, Canvas, Cursor, Frame, Geometry, Path, Stroke,
   };
   use iced::{Element, Length, Point, Rectangle, Theme};

   #[derive(Default)]
   pub struct State {
       cache: canvas::Cache,
   }

   impl State {
       pub fn view<'a>(&'a self, curves: &'a [Curve]) -> Element<'a, Curve> {
           Canvas::new(Bezier {
               state: self,
               curves,
           })
               .width(Length::Fill)
               .height(Length::Fill)
               .into()
       }

       pub fn request_redraw(&mut self) {
           self.cache.clear()
       }
   }

   struct Bezier<'a> {
       state: &'a State,
       curves: &'a [Curve],
   }

   impl<'a> canvas::Program<Curve> for Bezier<'a> {
       type State = Option<Pending>;

       fn update(
           &self,
           state: &mut Self::State,
           event: Event,
           bounds: Rectangle,
           cursor: Cursor,
       ) -> (event::Status, Option<Curve>) {
           let cursor_position =
               if let Some(position) = cursor.position_in(&bounds) {
                   position
               } else {
                   return (event::Status::Ignored, None);
               };

           match event {
               Event::Mouse(mouse_event) => {
                   let message = match mouse_event {
                       mouse::Event::ButtonPressed(mouse::Button::Left) => {
                           match *state {
                               None => {
                                   *state = Some(Pending::One {
                                       from: cursor_position,
                                   });

                                   None
                               }
                               Some(Pending::One { from }) => {
                                   *state = Some(Pending::Two {
                                       from,
                                       to: cursor_position,
                                   });

                                   None
                               }
                               Some(Pending::Two { from, to }) => {
                                   *state = None;

                                   Some(Curve {
                                       from,
                                       to,
                                       control: cursor_position,
                                   })
                               }
                           }
                       }
                       _ => None,
                   };

                   (event::Status::Captured, message)
               }
               _ => (event::Status::Ignored, None),
           }
       }

       fn draw(
           &self,
           state: &Self::State,
           _theme: &Theme,
           bounds: Rectangle,
           cursor: Cursor,
       ) -> Vec<Geometry> {
           let content =
               self.state.cache.draw(bounds.size(), |frame: &mut Frame| {
                   Curve::draw_all(self.curves, frame);

                   frame.stroke(
                       &Path::rectangle(Point::ORIGIN, frame.size()),
                       Stroke::default().with_width(2.0),
                   );
               });

           if let Some(pending) = state {
               let pending_curve = pending.draw(bounds, cursor);

               vec![content, pending_curve]
           } else {
               vec![content]
           }
       }

       fn mouse_interaction(
           &self,
           _state: &Self::State,
           bounds: Rectangle,
           cursor: Cursor,
       ) -> mouse::Interaction {
           if cursor.is_over(&bounds) {
               mouse::Interaction::Crosshair
           } else {
               mouse::Interaction::default()
           }
       }
   }

   #[derive(Debug, Clone, Copy)]
   pub struct Curve {
       from: Point,
       to: Point,
       control: Point,
   }

   impl Curve {
       fn draw_all(curves: &[Curve], frame: &mut Frame) {
           let curves = Path::new(|p| {
               for curve in curves {
                   p.move_to(curve.from);
                   p.quadratic_curve_to(curve.control, curve.to);
               }
           });

           frame.stroke(&curves, Stroke::default().with_width(2.0));
       }
   }

   #[derive(Debug, Clone, Copy)]
   enum Pending {
       One { from: Point },
       Two { from: Point, to: Point },
   }

   impl Pending {
       fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry {
           let mut frame = Frame::new(bounds.size());

           if let Some(cursor_position) = cursor.position_in(&bounds) {
               match *self {
                   Pending::One { from } => {
                       let line = Path::line(from, cursor_position);
                       frame.stroke(&line, Stroke::default().with_width(2.0));
                   }
                   Pending::Two { from, to } => {
                       let curve = Curve {
                           from,
                           to,
                           control: cursor_position,
                       };

                       Curve::draw_all(&[curve], &mut frame);
                   }
               };
           }

           frame.into_geometry()
       }
   }
}

Egui#

Egui 是 "最易於使用的 GUI 庫"。它使用即時模式 GUI 架構https://en.wikipedia.org/wiki/Immediate_mode_GUI。其目標之一是具備可移植性和易用性。本地外觀是特意不作為目標的。例如,它適用於需要快速 GUI 插件庫的簡單 GUI、遊戲或其他應用程序。您可以查看它們的 WebAssembly 示例構建:

Screenshot 2023-04-27 at 14.16.30

// app.rs
// main.rs / lib.rs boilerplate omitted

/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct TemplateApp {
   // Example stuff:
   label: String,

   // this how you opt-out of serialization of a member
   #[serde(skip)]
   value: f32,
}

impl Default for TemplateApp {
   fn default() -> Self {
       Self {
           // Example stuff:
           label: "Hello World!".to_owned(),
           value: 2.7,
       }
   }
}

impl TemplateApp {
   /// Called once before the first frame.
   pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
       // This is also where you can customize the look and feel of egui using
       // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.

       // Load previous app state (if any).
       // Note that you must enable the `persistence` feature for this to work.
       if let Some(storage) = cc.storage {
           return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
       }

       Default::default()
   }
}

impl eframe::App for TemplateApp {
   /// Called by the frame work to save state before shutdown.
   fn save(&mut self, storage: &mut dyn eframe::Storage) {
       eframe::set_value(storage, eframe::APP_KEY, self);
   }

   /// Called each time the UI needs repainting, which may be many times per second.
   /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
   fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
       let Self { label, value } = self;

       // Examples of how to create different panels and windows.
       // Pick whichever suits you.
       // Tip: a good default choice is to just keep the `CentralPanel`.
       // For inspiration and more examples, go to https://emilk.github.io/egui

       #[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages!
       egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
           // The top panel is often a good place for a menu bar:
           egui::menu::bar(ui, |ui| {
               ui.menu_button("File", |ui| {
                   if ui.button("Quit").clicked() {
                       _frame.close();
                   }
               });
           });
       });

       egui::SidePanel::left("side_panel").show(ctx, |ui| {
           ui.heading("Side Panel");

           ui.horizontal(|ui| {
               ui.label("Write something: ");
               ui.text_edit_singleline(label);
           });

           ui.add(egui::Slider::new(value, 0.0..=10.0).text("value"));
           if ui.button("Increment").clicked() {
               *value += 1.0;
           }

           ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
               ui.horizontal(|ui| {
                   ui.spacing_mut().item_spacing.x = 0.0;
                   ui.label("powered by ");
                   ui.hyperlink_to("egui", "https://github.com/emilk/egui");
                   ui.label(" and ");
                   ui.hyperlink_to(
                       "eframe",
                       "https://github.com/emilk/egui/tree/master/crates/eframe",
                   );
                   ui.label(".");
               });
           });
       });

       egui::CentralPanel::default().show(ctx, |ui| {
           // The central panel the region left after adding TopPanel's and SidePanel's

           ui.heading("Shades of Rust - egui");
           ui.hyperlink("https://github.com/Firfi/shades-of-rust/tree/master/shades-egui");
           egui::warn_if_debug_build(ui);
       });
   }
}

注意,官方這裡有一個更大的 Demo

Kas#

Kas 是一個 “高效的保留狀態工具包”,它結合了幾種 GUI 數據模型。例如,它處理消息時像 Elm 一樣,但可以在組件中保留狀態。它目前還不能在瀏覽器環境下工作,至少我沒有能夠快速構建示例。這是因為它比使用 WebGPU 的瀏覽器還要先進,而不是相反

Slint#

Slint 旨在用於本地應用程序而非 Web 環境。它僅可編譯成 WebAssembly 用於演示目的。Slint 的目標是本地外觀、嵌入式系統、微控制器和桌面應用程序。它使用自己的腳本語言描述 UI,提供組件狀態和外觀描述。Slint 允許一些腳本編程,儘管可以在 Rust 中運行最重的代碼。總的來說,它是一種命令式的工具。

Pasted image 20230427141739

以下是他們教程的編譯代碼:

// main.rs

#[cfg_attr(target_arch = "wasm32",
wasm_bindgen::prelude::wasm_bindgen(start))]
pub fn main() {
   use slint::Model;

   let main_window = MainWindow::new();

   // Fetch the tiles from the model
   let mut tiles: Vec<TileData> = main_window.get_memory_tiles().iter().collect();
   // Duplicate them to ensure that we have pairs
   tiles.extend(tiles.clone());

   // Randomly mix the tiles
   use rand::seq::SliceRandom;
   let mut rng = rand::thread_rng();
   tiles.shuffle(&mut rng);

   // Assign the shuffled Vec to the model property
   let tiles_model = std::rc::Rc::new(slint::VecModel::from(tiles));
   main_window.set_memory_tiles(tiles_model.clone().into());

   let main_window_weak = main_window.as_weak();
   main_window.on_check_if_pair_solved(move || {
       let mut flipped_tiles =
           tiles_model.iter().enumerate().filter(|(_, tile)| tile.image_visible && !tile.solved);

       if let (Some((t1_idx, mut t1)), Some((t2_idx, mut t2))) =
           (flipped_tiles.next(), flipped_tiles.next())
       {
           let is_pair_solved = t1 == t2;
           if is_pair_solved {
               t1.solved = true;
               tiles_model.set_row_data(t1_idx, t1);
               t2.solved = true;
               tiles_model.set_row_data(t2_idx, t2);
           } else {
               let main_window = main_window_weak.unwrap();
               main_window.set_disable_tiles(true);
               let tiles_model = tiles_model.clone();
               slint::Timer::single_shot(std::time::Duration::from_secs(1), move || {
                   main_window.set_disable_tiles(false);
                   t1.image_visible = false;
                   tiles_model.set_row_data(t1_idx, t1);
                   t2.image_visible = false;
                   tiles_model.set_row_data(t2_idx, t2);
               });
           }
       }
   });

   main_window.run();
}

slint::slint! {
   struct TileData := {
       image: image,
       image_visible: bool,
       solved: bool,
   }
   MemoryTile := Rectangle {
       callback clicked;
       property <bool> open_curtain;
       property <bool> solved;
       property <image> icon;

       height: 64px;
       width: 64px;
       background: solved ? #34CE57 : #3960D5;
       animate background { duration: 800ms; }

       Image {
           source: icon;
           width: parent.width;
           height: parent.height;
       }

       // Left curtain
       Rectangle {
           background: #193076;
           width: open_curtain ? 0px : (parent.width / 2);
           height: parent.height;
           animate width { duration: 250ms; easing: ease-in; }
       }

       // Right curtain
       Rectangle {
           background: #193076;
           x: open_curtain ? parent.width : (parent.width / 2);
           width: open_curtain ? 0px : (parent.width / 2);
           height: parent.height;
           animate width { duration: 250ms; easing: ease-in; }
           animate x { duration: 250ms; easing: ease-in; }
       }

       TouchArea {
           clicked => {
               // Delegate to the user of this element
               root.clicked();
           }
       }
   }
   MainWindow := Window {
       width: 326px;
       height: 326px;

       callback check_if_pair_solved();
       property <bool> disable_tiles;

       property <[TileData]> memory_tiles: [
           { image: @image-url("icons/at.png") },
           { image: @image-url("icons/balance-scale.png") },
           { image: @image-url("icons/bicycle.png") },
           { image: @image-url("icons/bus.png") },
           { image: @image-url("icons/cloud.png") },
           { image: @image-url("icons/cogs.png") },
           { image: @image-url("icons/motorcycle.png") },
           { image: @image-url("icons/video.png") },
       ];
       for tile[i] in memory_tiles : MemoryTile {
           x: mod(i, 4) * 74px;
           y: floor(i / 4) * 74px;
           width: 64px;
           height: 64px;
           icon: tile.image;
           open_curtain: tile.image_visible || tile.solved;
           // propagate the solved status from the model to the tile
           solved: tile.solved;
           clicked => {
               if (!root.disable_tiles) {
                   tile.image_visible = !tile.image_visible;
                   root.check_if_pair_solved();
               }
           }
       }
   }
}

Sycamore#

Sycamore 是一個 “用於創建 Rust 和 WebAssembly Web 應用程序的反應式庫”。它具有很好的反應性能是其中的主要特點。它看起來像是 mobx,使用綁定 / 信號語義(與 Angular 或早期的 React 非常相似),提供了良好的開發者體驗:

fn App<G: Html>(cx: Scope) -> View<G> {
   let a = create_signal(cx, 0_f64);
   view! { cx,
p { "Input value: " (a.get()) }
       input(type="number", bind:valueAsNumber=a)
   }
}

上面的代碼生成 DOM。

默認情況下,Sycamore 是為 Web 而設計的,但它可以輕鬆地嵌入到其他工具中,比如 Tauri,以創建桌面應用程序:https://github.com/JonasKruckenberg/tauri-sycamore-template。目前,Sycamore 模板還沒有被包含在 Tauri 的 create-tauri-app 工具(https://github.com/tauri-apps/create-tauri-app)中,但 Yew 是。

// app.rs

#[derive(Serialize, Deserialize)]
struct GreetArgs<'a> {
   name: &'a str,
}

#[component]
pub fn App<G: Html>(cx: Scope) -> View<G> {
   let name = create_signal(cx, String::new());
   let greet_msg = create_signal(cx, String::new());

   let greet = move |_| {
       spawn_local_scoped(cx, async move {
           // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
           let new_msg =
               invoke("greet", to_value(&GreetArgs { name: &name.get() }).unwrap()).await;

           log(&new_msg.as_string().unwrap());

           greet_msg.set(new_msg.as_string().unwrap());
       })
   };

   view! { cx,
       main(class="container") {             
           div(class="row") {
               input(id="greet-input",bind:value=name,placeholder="Enter a name...")
               button(type="button",on:click=greet) {
                   "Greet"
               }
           }
           p {
               b {
                   (greet_msg.get())
               }
           }
       }
   }
}
// main.rs

mod app;

use app::App;

#[cfg(all(not(debug_assertions), not(feature = "ssg")))]
fn main() {
   sycamore::hydrate(App);
}

#[cfg(all(debug_assertions, not(feature = "ssg")))]
fn main() {
   sycamore::render(App);
}

#[cfg(feature = "ssg")]
fn main() {
   let out_dir = std::env::args().nth(1).unwrap();

   println!("out_dir {}", out_dir);

   let template = std::fs::read_to_string(format!("{}/index.html", out_dir)).unwrap();

   let html = sycamore::render_to_string(App);

   let html = template.replace("<!--app-html-->\n", &html);

   let path = format!("{}/index.html", out_dir);

   println!("Writing html to file \"{}\"", path);
   std::fs::write(path, html).unwrap();
}

Yew#

Yew 現在似乎相當受歡迎。它是專門為 Web 開發的 GUI 框架,具有一些 React 的特點,但也有一些不同之處。例如,組件類似於具有定義消息和對這些消息的反應的狀態機。組件在自身中保持狀態。可以使用 Yewdux 來實現類似於 Redux 的數據處理。除其他功能外,Yew 還具有水合和伺服器端渲染。

Screenshot 2023-04-27 at 14.18.43

// main.rs

use cell::Cellule;
use gloo::timers::callback::Interval;
use rand::Rng;
use yew::html::Scope;
use yew::{classes, html, Component, Context, Html};

mod cell;

pub enum Msg {
   Random,
   Start,
   Step,
   Reset,
   Stop,
   ToggleCellule(usize),
   Tick,
}

pub struct App {
   active: bool,
   cellules: Vec<Cellule>,
   cellules_width: usize,
   cellules_height: usize,
   _interval: Interval,
}

impl App {
   pub fn random_mutate(&mut self) {
       for cellule in self.cellules.iter_mut() {
           if rand::thread_rng().gen() {
               cellule.set_alive();
           } else {
               cellule.set_dead();
           }
       }
   }

   fn reset(&mut self) {
       for cellule in self.cellules.iter_mut() {
           cellule.set_dead();
       }
   }

   fn step(&mut self) {
       let mut to_dead = Vec::new();
       let mut to_live = Vec::new();
       for row in 0..self.cellules_height {
           for col in 0..self.cellules_width {
               let neighbors = self.neighbors(row as isize, col as isize);

               let current_idx = self.row_col_as_idx(row as isize, col as isize);
               if self.cellules[current_idx].is_alive() {
                   if Cellule::alone(&neighbors) || Cellule::overpopulated(&neighbors) {
                       to_dead.push(current_idx);
                   }
               } else if Cellule::can_be_revived(&neighbors) {
                   to_live.push(current_idx);
               }
           }
       }
       to_dead
           .iter()
           .for_each(|idx| self.cellules[*idx].set_dead());
       to_live
           .iter()
           .for_each(|idx| self.cellules[*idx].set_alive());
   }

   fn neighbors(&self, row: isize, col: isize) -> [Cellule; 8] {
       [
           self.cellules[self.row_col_as_idx(row + 1, col)],
           self.cellules[self.row_col_as_idx(row + 1, col + 1)],
           self.cellules[self.row_col_as_idx(row + 1, col - 1)],
           self.cellules[self.row_col_as_idx(row - 1, col)],
           self.cellules[self.row_col_as_idx(row - 1, col + 1)],
           self.cellules[self.row_col_as_idx(row - 1, col - 1)],
           self.cellules[self.row_col_as_idx(row, col - 1)],
           self.cellules[self.row_col_as_idx(row, col + 1)],
       ]
   }

   fn row_col_as_idx(&self, row: isize, col: isize) -> usize {
       let row = wrap(row, self.cellules_height as isize);
       let col = wrap(col, self.cellules_width as isize);

       row * self.cellules_width + col
   }

   fn view_cellule(&self, idx: usize, cellule: &Cellule, link: &Scope<Self>) -> Html {
       let cellule_status = {
           if cellule.is_alive() {
               "cellule-live"
           } else {
               "cellule-dead"
           }
       };
       html! {
           <div key={idx} class={classes!("game-cellule", cellule_status)}
               onclick={link.callback(move |_| Msg::ToggleCellule(idx))}>
           </div>
       }
   }
}

impl Component for App {
   type Message = Msg;
   type Properties = ();

   fn create(ctx: &Context<Self>) -> Self {
       let callback = ctx.link().callback(|_| Msg::Tick);
       let interval = Interval::new(200, move || callback.emit(()));
       let (cellules_width, cellules_height) = (53, 40);

       Self {
           active: false,
           cellules: vec![Cellule::new_dead(); cellules_width * cellules_height],
           cellules_width,
           cellules_height,
           _interval: interval,
       }
   }

   fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
       match msg {
           Msg::Random => {
               self.random_mutate();
               log::info!("Random");
               true
           }
           Msg::Start => {
               self.active = true;
               log::info!("Start");
               false
           }
           Msg::Step => {
               self.step();
               true
           }
           Msg::Reset => {
               self.reset();
               log::info!("Reset");
               true
           }
           Msg::Stop => {
               self.active = false;
               log::info!("Stop");
               false
           }
           Msg::ToggleCellule(idx) => {
               let cellule = self.cellules.get_mut(idx).unwrap();
               cellule.toggle();
               true
           }
           Msg::Tick => {
               if self.active {
                   self.step();
                   true
               } else {
                   false
               }
           }
       }
   }

   fn view(&self, ctx: &Context<Self>) -> Html {
       let cell_rows =
           self.cellules
               .chunks(self.cellules_width)
               .enumerate()
               .map(|(y, cellules)| {
                   let idx_offset = y * self.cellules_width;

                   let cells = cellules
                       .iter()
                       .enumerate()
                       .map(|(x, cell)| self.view_cellule(idx_offset + x, cell, ctx.link()));
                   html! {
                       <div key={y} class="game-row">
                           { for cells }
                       </div>
                   }
               });

       html! {
           <div>
               <section class="game-container">
                   <section class="game-area">
                       <div class="game-of-life">
                           { for cell_rows }
                       </div>
                       <div class="game-buttons">
                           <button class="game-button" onclick={ctx.link().callback(|_| Msg::Random)}>{ "Random" }</button>
                           <button class="game-button" onclick={ctx.link().callback(|_| Msg::Step)}>{ "Step" }</button>
                           <button class="game-button" onclick={ctx.link().callback(|_| Msg::Start)}>{ "Start" }</button>
                           <button class="game-button" onclick={ctx.link().callback(|_| Msg::Stop)}>{ "Stop" }</button>
                           <button class="game-button" onclick={ctx.link().callback(|_| Msg::Reset)}>{ "Reset" }</button>
                       </div>
                   </section>
               </section>
           </div>
       }
   }
}

fn wrap(coord: isize, range: isize) -> usize {
   let result = if coord < 0 {
       coord + range
   } else if coord >= range {
       coord - range
   } else {
       coord
   };
   result as usize
}

fn main() {
   wasm_logger::init(wasm_logger::Config::default());
   log::trace!("Initializing yew...");
   yew::Renderer::<App>::new().render();
}

Bracket#

Bracket 被重新命名為 rltk(Roguelike Toolkit)。Bracket 由控制台渲染庫和幾個編寫 Roguelike 遊戲的輔助工具組成,支持 Web 和桌面平台。請查看這個殺手級別的 教程,你可以使用 Rust 編寫 Roguelike 遊戲。

默認情況下,bracket-lib 在 OpenGL 模式下運行(如果檢測到您正在編譯 wasm32-unknown-unknown 則使用 WebGL)。它可以使用 wgpu 作為後端,在未來將非常方便。這是他們教程中最後一關的 Web 版本:

// main.rs

use bracket_lib::prelude::*;

struct State {}

impl GameState for State {
 fn tick(&mut self, ctx: &mut BTerm) {
   ctx.print(1, 1, "Hello Bracket World");
 }
}

fn main() -> BError {
 let context = BTermBuilder::simple80x50()
   .with_title("Hello Minimal Bracket World")
   .build()?;

 let gs: State = State {};
 main_loop(context, gs)
}

既然提到了 Bracket,我必須也提一下 Cursive,它是一个用于终端界面的控制台库,尽管目前似乎没有可编译的后端用于浏览器。
![[Pasted image 20230427141944.png]]
下面是一个在桌面上运行的例子:

// main.rs

use cursive::views::TextView;

fn main() {
	let mut siv = cursive::default();
	siv.add_global_callback('q', |s| s.quit());
	siv.add_layer(TextView::new("Hello cursive! Press <q> to quit."));
	siv.run();
}

Vizia#

Vizia 是一個聲明式的反應式 GUI 框架。它可以在多個平台上使用(Windows、Linux、MacOS、Web),具有類似 Elm 的數據處理方式,並在 GPU 上呈現。

Pasted image 20230427142002

以下是其中一個示例:

// main.rs

use std::str::FromStr;

use vizia::fonts::icons_names::DOWN;
use vizia::prelude::*;

use chrono::{NaiveDate, ParseError};

const STYLE: &str = r#"
   /*
   * {
       border-width: 1px;
       border-color: red;
   }
   */
   textbox.invalid {
       background-color: #AA0000;
   }
"#;

#[derive(Clone)]
pub struct SimpleDate(NaiveDate);

impl Data for SimpleDate {
   fn same(&self, other: &Self) -> bool {
       self.0 == other.0
   }
}

impl std::fmt::Display for SimpleDate {
   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       write!(f, "{}", self.0.format("%Y:%m:%d"))
   }
}

impl FromStr for SimpleDate {
   type Err = ParseError;
   fn from_str(s: &str) -> Result<Self, Self::Err> {
       NaiveDate::parse_from_str(s, "%Y:%m:%d").map(|date| SimpleDate(date))
   }
}

#[derive(Lens)]
pub struct AppData {
   options: Vec<&'static str>,
   choice: String,
   start_date: SimpleDate,
   end_date: SimpleDate,
}

pub enum AppEvent {
   SetChoice(String),
   SetStartDate(SimpleDate),
   SetEndDate(SimpleDate),
}

impl Model for AppData {
   fn event(&mut self, _: &mut EventContext, event: &mut Event) {
       event.map(|app_event, _| match app_event {
           AppEvent::SetChoice(choice) => {
               self.choice = choice.clone();
           }

           AppEvent::SetStartDate(date) => {
               self.start_date = date.clone();
           }

           AppEvent::SetEndDate(date) => {
               self.end_date = date.clone();
           }
       });
   }
}

impl AppData {
   pub fn new() -> Self {
       Self {
           options: vec!["one-way flight", "return flight"],
           choice: "one-way flight".to_string(),
           start_date: SimpleDate(NaiveDate::from_ymd_opt(2022, 02, 12).unwrap()),
           end_date: SimpleDate(NaiveDate::from_ymd_opt(2022, 02, 26).unwrap()),
       }
   }
}

fn main() {
   Application::new(|cx| {
       cx.add_theme(STYLE);
       AppData::new().build(cx);
       VStack::new(cx, |cx| {
           Dropdown::new(
               cx,
               move |cx|
                   // A Label and an Icon
                   HStack::new(cx, move |cx|{
                       Label::new(cx, AppData::choice)
                           .width(Stretch(1.0))
                           .text_wrap(false);
                       Label::new(cx, DOWN).font("icons").left(Pixels(5.0)).right(Pixels(5.0));
                   }).width(Stretch(1.0)),
               // List of options
               move |cx| {
                   List::new(cx, AppData::options, |cx, _, item| {
                       Label::new(cx, item)
                           .width(Stretch(1.0))
                           .child_top(Stretch(1.0))
                           .child_bottom(Stretch(1.0))
                           .bind(AppData::choice, move |handle, choice| {
                               let selected = item.get(handle.cx) == choice.get(handle.cx);
                               handle.background_color(if selected {
                                   Color::from("#f8ac14")
                               } else {
                                   Color::white()
                               });
                           })
                           .on_press(move |cx| {
                               cx.emit(AppEvent::SetChoice(item.get(cx).to_string().to_owned()));
                               cx.emit(PopupEvent::Close);
                           });
                   });
               },
           )
               .width(Pixels(150.0));

           Textbox::new(cx, AppData::start_date)
               .on_edit(|cx, text| {
                   if let Ok(val) = text.parse::<SimpleDate>() {
                       cx.emit(AppEvent::SetStartDate(val));
                       cx.toggle_class("invalid", false);
                   } else {
                       cx.toggle_class("invalid", true);
                   }
               })
               .width(Pixels(150.0));

           Textbox::new(cx, AppData::end_date)
               .on_edit(|cx, text| {
                   if let Ok(val) = text.parse::<SimpleDate>() {
                       cx.emit(AppEvent::SetEndDate(val));
                       cx.toggle_class("invalid", false);
                   } else {
                       cx.toggle_class("invalid", true);
                   }
               })
               .width(Pixels(150.0))
               .disabled(AppData::choice.map(|choice| choice == "one-way flight"));

           Button::new(cx, |_| {}, |cx| Label::new(cx, "Book").width(Stretch(1.0)))
               .width(Pixels(150.0));
       })
           .row_between(Pixels(10.0))
           .child_space(Stretch(1.0));
   })
       .title("Flight Booker")
       .inner_size((250, 250))
       .run();
}

Leptos#

Leptos 是一個等同於 React 的同構 Web 框架,但它使用 Rust 閉包進行更細粒度的反應式設計。整個渲染函數不會在每次信號更新時執行,而是被視為 “設置” 函數。此外,Leptos 沒有虛擬 DOM。該框架還包含了在伺服器和客戶端上都可用的路由器。

Screenshot 2023-04-27 at 14.20.26

以下是 Leptos 路由器示例:

// lib.rs

// main.rs boilerplate omitted

mod api;

use leptos::*;
use leptos_router::*;

use crate::api::{get_contact, get_contacts};

#[component]
pub fn RouterExample(cx: Scope) -> impl IntoView {
   log::debug!("rendering <RouterExample/>");

   view! { cx,
       <Router>
           <nav>
               // ordinary <a> elements can be used for client-side navigation
               // using <A> has two effects:
               // 1) ensuring that relative routing works properly for nested routes
               // 2) setting the `aria-current` attribute on the current link,
               //    for a11y and styling purposes
               <A exact=true href="/">"Contacts"</A>
               <A href="about">"About"</A>
               <A href="settings">"Settings"</A>
               <A href="redirect-home">"Redirect to Home"</A>
           </nav>
           <main>
               <Routes>
                   <Route
                       path=""
                       view=move |cx| view! { cx,  <ContactList/> }
                   >
                       <Route
                           path=":id"
                           view=move |cx| view! { cx,  <Contact/> }
                       />
                       <Route
                           path="/"
                           view=move |_| view! { cx,  <p>"Select a contact."</p> }
                       />
                   </Route>
                   <Route
                       path="about"
                       view=move |cx| view! { cx,  <About/> }
                   />
                   <Route
                       path="settings"
                       view=move |cx| view! { cx,  <Settings/> }
                   />
                   <Route
                       path="redirect-home"
                       view=move |cx| view! { cx, <Redirect path="/"/> }
                   />
               </Routes>
           </main>
       </Router>
   }
}

#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
   log::debug!("rendering <ContactList/>");

   let location = use_location(cx);
   let contacts = create_resource(cx, move || location.search.get(), get_contacts);
   let contacts = move || {
       contacts.read().map(|contacts| {
           // this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
           contacts
               .into_iter()
               .map(|contact| {
                   view! { cx,
                       <li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
                   }
               })
               .collect::<Vec<_>>()
       })
   };

   view! { cx,
       <div class="contact-list">
           <h1>"Contacts"</h1>
           <Suspense fallback=move || view! { cx,  <p>"Loading contacts..."</p> }>
               {move || view! { cx, <ul>{contacts}</ul>}}
           </Suspense>
           <Outlet/>
       </div>
   }
}

#[derive(Params, PartialEq, Clone, Debug)]
pub struct ContactParams {
   id: usize,
}

#[component]
pub fn Contact(cx: Scope) -> impl IntoView {
   log::debug!("rendering <Contact/>");

   let params = use_params::<ContactParams>(cx);
   let contact = create_resource(
       cx,
       move || params().map(|params| params.id).ok(),
       // any of the following would work (they're identical)
       // move |id| async move { get_contact(id).await }
       // move |id| get_contact(id),
       // get_contact
       get_contact,
   );

   let contact_display = move || match contact.read() {
       // None => loading, but will be caught by Suspense fallback
       // I'm only doing this explicitly for the example
       None => None,
       // Some(None) => has loaded and found no contact
       Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
       // Some(Some) => has loaded and found a contact
       Some(Some(contact)) => Some(
           view! { cx,
               <section class="card">
                   <h1>{contact.first_name} " " {contact.last_name}</h1>
                   <p>{contact.address_1}<br/>{contact.address_2}</p>
               </section>
           }
               .into_any(),
       ),
   };

   view! { cx,
       <div class="contact">
           <Transition fallback=move || view! { cx,  <p>"Loading..."</p> }>
               {contact_display}
           </Transition>
       </div>
   }
}

#[component]
pub fn About(cx: Scope) -> impl IntoView {
   log::debug!("rendering <About/>");
   // use_navigate allows you to navigate programmatically by calling a function
   let navigate = use_navigate(cx);

   view! { cx,
       <>
           // note: this is just an illustration of how to use `use_navigate`
           // <button on:click> to navigate is an *anti-pattern*
           // you should ordinarily use a link instead,
           // both semantically and so your link will work before WASM loads
           <button on:click=move |_| { _ = navigate("/", Default::default()); }>
               "Home"
           </button>
           <h1>"About"</h1>
           <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
       </>
   }
}

#[component]
pub fn Settings(cx: Scope) -> impl IntoView {
   log::debug!("rendering <Settings/>");
   view! { cx,
       <>
           <h1>"Settings"</h1>
           <form>
               <fieldset>
                   <legend>"Name"</legend>
                   <input type="text" name="first_name" placeholder="First"/>
                   <input type="text" name="last_name" placeholder="Last"/>
               </fieldset>
               <pre>"This page is just a placeholder."</pre>
           </form>
       </>
   }
}
// api.rs

use futures::{
   channel::oneshot::{self, Canceled},
   Future,
};
use leptos::set_timeout;
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContactSummary {
   pub id: usize,
   pub first_name: String,
   pub last_name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Contact {
   pub id: usize,
   pub first_name: String,
   pub last_name: String,
   pub address_1: String,
   pub address_2: String,
   pub city: String,
   pub state: String,
   pub zip: String,
   pub email: String,
   pub phone: String,
}

pub async fn get_contacts(_search: String) -> Vec<ContactSummary> {
   // fake an API call with an artificial delay
   _ = delay(Duration::from_millis(300)).await;
   vec![
       ContactSummary {
           id: 0,
           first_name: "Bill".into(),
           last_name: "Smith".into(),
       },
       ContactSummary {
           id: 1,
           first_name: "Tim".into(),
           last_name: "Jones".into(),
       },
       ContactSummary {
           id: 2,
           first_name: "Sally".into(),
           last_name: "Stevens".into(),
       },
   ]
}

pub async fn get_contact(id: Option<usize>) -> Option<Contact> {
   // fake an API call with an artificial delay
   _ = delay(Duration::from_millis(500)).await;
   match id {
       Some(0) => Some(Contact {
           id: 0,
           first_name: "Bill".into(),
           last_name: "Smith".into(),
           address_1: "12 Mulberry Lane".into(),
           address_2: "".into(),
           city: "Boston".into(),
           state: "MA".into(),
           zip: "02129".into(),
           email: "[email protected]".into(),
           phone: "617-121-1221".into(),
       }),
       Some(1) => Some(Contact {
           id: 1,
           first_name: "Tim".into(),
           last_name: "Jones".into(),
           address_1: "56 Main Street".into(),
           address_2: "".into(),
           city: "Chattanooga".into(),
           state: "TN".into(),
           zip: "13371".into(),
           email: "[email protected]".into(),
           phone: "232-123-1337".into(),
       }),
       Some(2) => Some(Contact {
           id: 2,
           first_name: "Sally".into(),
           last_name: "Stevens".into(),
           address_1: "404 E 123rd St".into(),
           address_2: "Apt 7E".into(),
           city: "New York".into(),
           state: "NY".into(),
           zip: "10082".into(),
           email: "[email protected]".into(),
           phone: "242-121-3789".into(),
       }),
       _ => None,
   }
}

fn delay(duration: Duration) -> impl Future<Output = Result<(), Canceled>> {
   let (tx, rx) = oneshot::channel();
   set_timeout(
       move || {
           _ = tx.send(());
       },
       duration,
   );
   rx
}

Perseus#

Perseus 是一個旨在快速支持每種渲染策略的框架,同時提供出色的開發者體驗。它的範圍是 Web,並且使用反應性語義來表示狀態變量。它是基於 Sycamore 構建的,但在其之上增加了狀態和模板概念。

Screenshot 2023-04-27 at 14.20.52

示例:

// template/index.rs

use crate::global_state::AppStateRx;
use perseus::prelude::*;
use sycamore::prelude::*;

// Note that this template takes no state of its own in this example, but it
// certainly could

fn index_page<G: Html>(cx: Scope) -> View<G> {
   // We access the global state through the render context, extracted from
   // Sycamore's context system
   let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);

   view! { cx,
      // The user can change the global state through an input, and the changes they make will be reflected throughout the app
      p { (global_state.test.get()) }
      input(bind:value = global_state.test)
      a(href = "about", id = "about-link") { "About" }
  }
}

#[engine_only_fn]
fn head(cx: Scope) -> View<SsrNode> {
   view! { cx,
      title { "Index Page" }
  }
}

pub fn get_template<G: Html>() -> Template<G> {
   Template::build("index").view(index_page).head(head).build()
}
// main.rs

mod global_state;
mod templates;

use perseus::prelude::*;

#[perseus::main(perseus_axum::dflt_server)]
pub fn main<G: Html>() -> PerseusApp<G> {
   PerseusApp::new()
       .template(crate::templates::index::get_template())
       .template(crate::templates::about::get_template())
       .error_views(ErrorViews::unlocalized_development_default())
       .global_state_creator(crate::global_state::get_global_state_creator())
}

Sauron#

這是一個關於構建 Web UI 的簡單庫。它使用類似 React 的 UI 聲明和類似 ELM 的狀態管理。

Screenshot 2023-04-27 at 14.21.25

// main.rs

use sauron::{jss, prelude::*};

enum Msg {
   Increment,
   Decrement,
   Reset,
}

struct App {
   count: i32,
}

impl App {
   fn new() -> Self {
       App { count: 0 }
   }
}

impl Application<Msg> for App {
   fn view(&self) -> Node<Msg> {
       node! {
           <main>
               <input type="button"
                   value="+"
                   on_click=|_| {
                       Msg::Increment
                   }
               />
               <button class="count" on_click=|_|{Msg::Reset} >{text(self.count)}</button>
               <input type="button"
                   value="-"
                   on_click=|_| {
                       Msg::Decrement
                   }
               />
           </main>
       }
   }

   fn update(&mut self, msg: Msg) -> Cmd<Self, Msg> {
       match msg {
           Msg::Increment => self.count += 1,
           Msg::Decrement => self.count -= 1,
           Msg::Reset => self.count = 0,
       }
       Cmd::none()
   }

   fn style(&self) -> String {
       jss! {
           "body":{
               font_family: "verdana, arial, monospace",
           },

           "main":{
               width:px(30),
               height: px(100),
               margin: "auto",
               text_align: "center",
           },

           "input, .count":{
               font_size: px(40),
               padding: px(30),
               margin: px(30),
           }
       }
   }
}

#[wasm_bindgen(start)]
pub fn start() {
   Program::mount_to_body(App::new());
}

MoonZoon#

一個前 Seed 維護者的全棧框架。從示例中可以看出,它的前端狀態管理使用了 signal 語義。

這個問題 得到解決之前,我無法構建這個示例。

"counter" 示例代碼如下:

// main.rs

use zoon::*;

#[static_ref]
fn counter() -> &'static Mutable<i32> {
   Mutable::new(0)
}

fn increment() {
   counter().update(|counter| counter + 1)
}

fn decrement() {
   counter().update(|counter| counter - 1)
}

fn root() -> impl Element {
   Column::new()
       .item(Button::new().label("-").on_press(decrement))
       .item(Text::with_signal(counter().signal()))
       .item(Button::new().label("+").on_press(increment))
}

// ------ Alternative ------
fn _root() -> impl Element {
   let (counter, counter_signal) = Mutable::new_and_signal(0);
   let on_press = move |step: i32| *counter.lock_mut() += step;
   Column::new()
       .item(
           Button::new()
               .label("-")
               .on_press(clone!((on_press) move || on_press(-1))),
       )
       .item_signal(counter_signal)
       .item(Button::new().label("+").on_press(move || on_press(1)))
}
// ---------- // -----------

fn main() {
   start_app("app", root);
}

Relm4#

Relm4 是一款受 Elm 啟發、基於 gtk4-rs 的慣用 GUI 庫。Relm4 是重新構建的 relm 的一個新版本,與 GTK4 和 libadwaita 兼容。儘管原始的 relm 仍在維護,但這兩個庫(relm 和 relm4)似乎由不同的人維護。

目前僅支持設備和桌面端。沒有 web 可用,可能是因為使用了 gtk4 後端。儘管如此,Relm4 可以利用 CSS,例如在 https://github.com/Relm4/Relm4/blob/53b9a6bf6e514a5cce979c8307a4fefea422bdd7/examples/tracker.rs 中,全局 CSS 用於 UI 渲染。
以下是它們的一個示例在桌面端運行的屏幕截圖:

Pasted image 20230427142210

// main.rs

use gtk::glib::clone;
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt};
use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent};

struct App {
   counter: u8,
}

#[derive(Debug)]
enum Msg {
   Increment,
   Decrement,
}

struct AppWidgets {
   // window: gtk::Window,
   // vbox: gtk::Box,
   // inc_button: gtk::Button,
   // dec_button: gtk::Button,
   label: gtk::Label,
}

impl SimpleComponent for App {
   type Init = u8;
   type Input = Msg;
   type Output = ();
   type Widgets = AppWidgets;
   type Root = gtk::Window;

   fn init_root() -> Self::Root {
       gtk::Window::builder()
           .title("Simple app")
           .default_width(300)
           .default_height(100)
           .build()
   }

   // Initialize the component.
   fn init(
       counter: Self::Init,
       window: &Self::Root,
       sender: ComponentSender<Self>,
   ) -> ComponentParts<Self> {
       let model = App { counter };

       let vbox = gtk::Box::builder()
           .orientation(gtk::Orientation::Vertical)
           .spacing(5)
           .build();

       let inc_button = gtk::Button::with_label("Increment");
       let dec_button = gtk::Button::with_label("Decrement");

       let label = gtk::Label::new(Some(&format!("Counter: {}", model.counter)));
       label.set_margin_all(5);

       window.set_child(Some(&vbox));
       vbox.set_margin_all(5);
       vbox.append(&inc_button);
       vbox.append(&dec_button);
       vbox.append(&label);

       inc_button.connect_clicked(clone!(@strong sender => move |_| {
           sender.input(Msg::Increment);
       }));

       dec_button.connect_clicked(clone!(@strong sender => move |_| {
           sender.input(Msg::Decrement);
       }));

       let widgets = AppWidgets { label };

       ComponentParts { model, widgets }
   }

   fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
       match msg {
           Msg::Increment => {
               self.counter = self.counter.wrapping_add(1);
           }
           Msg::Decrement => {
               self.counter = self.counter.wrapping_sub(1);
           }
       }
   }

   // Update the view to represent the updated model.
   fn update_view(&self, widgets: &mut Self::Widgets, _sender: ComponentSender<Self>) {
       widgets
           .label
           .set_label(&format!("Counter: {}", self.counter));
   }
}

fn main() {
   let app = RelmApp::new("relm4.example.simple_manual");
   app.run::<App>(0);
}

Fltk-rs#

Fltk-rs 是 https://www.fltk.org 的綁定庫。FLTK 目前不支持 Wasm 或 iOS。它對 Android 有實驗性支持(因人而異),並專注於桌面應用程序。這是可以被應用程序或 GUI 框架用作圖形基礎的低級綁定之一。

下面的圖片顯示了他們的一個示例在桌面上運行的情況:

Pasted image 20230427140019

// main.rs

use fltk::{
   app, button, draw, enums::*, frame::Frame, image::SvgImage, prelude::*,
   valuator::*, widget::Widget, window::Window,
};
use std::ops::{Deref, DerefMut};
use std::{cell::RefCell, rc::Rc};

const POWER: &str = r#"<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 315.083 315.083" style="enable-background:new 0 0 315.083 315.083;" xml:space="preserve">
<g id="Layer_1">
	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="157.5417" y1="4.5417" x2="157.5417" y2="310.5417">
		<stop  offset="0" style="stop-color:#939598"/>
		<stop  offset="0.25" style="stop-color:#414042"/>
		<stop  offset="0.5" style="stop-color:#252223"/>
		<stop  offset="1" style="stop-color:#000000"/>
	</linearGradient>
	<circle style="fill:url(#SVGID_1_);" cx="157.542" cy="157.542" r="153"/>
</g>
<g id="Layer_2">
	<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="157.5417" y1="292.5417" x2="157.5417" y2="22.5417">
		<stop  offset="0" style="stop-color:#58595B"/>
		<stop  offset="0.1" style="stop-color:#414042"/>
		<stop  offset="0.2" style="stop-color:#242122"/>
		<stop  offset="1" style="stop-color:#000000"/>
	</linearGradient>
	<circle style="fill:url(#SVGID_2_);stroke:#58595B;stroke-miterlimit:10;" cx="157.542" cy="157.542" r="135"/>
</g>
<g id="Layer_4">
	<radialGradient id="SVGID_3_" cx="157.5417" cy="89.9217" r="62.2727" gradientUnits="userSpaceOnUse">
		<stop  offset="0" style="stop-color:#58595B"/>
		<stop  offset="0.5" style="stop-color:#414042"/>
		<stop  offset="1" style="stop-color:#231F20"/>
	</radialGradient>
	<radialGradient id="SVGID_4_" cx="157.5417" cy="89.9217" r="62.7723" gradientUnits="userSpaceOnUse">
		<stop  offset="0" style="stop-color:#FFFFFF"/>
		<stop  offset="0.6561" style="stop-color:#231F20"/>
		<stop  offset="1" style="stop-color:#000000"/>
	</radialGradient>
	<ellipse style="fill:url(#SVGID_3_);stroke:url(#SVGID_4_);stroke-miterlimit:10;" cx="157.542" cy="89.922" rx="59.833" ry="64.62"/>
</g>
<g id="Layer_6">
	<path style="fill:none;stroke:red;stroke-width:10;stroke-linecap:round;stroke-miterlimit:10;" d="M119.358,119.358
		c-9.772,9.772-15.816,23.272-15.816,38.184c0,14.912,6.044,28.412,15.816,38.184s23.272,15.816,38.184,15.816
		c14.912,0,28.412-6.044,38.184-15.816s15.816-23.272,15.816-38.184c0-14.912-6.044-28.412-15.816-38.184"/>
	<line style="fill:none;stroke:red;stroke-width:10;stroke-linecap:round;stroke-miterlimit:10;" x1="157.542" y1="154.542" x2="157.542" y2="100.542"/>
</g>
</svg>"#;

pub struct FlatButton {
   wid: Widget,
}

impl FlatButton {
   pub fn new(x: i32, y: i32, w: i32, h: i32, label: &str) -> FlatButton {
       let mut x = FlatButton {
           wid: Widget::new(x, y, w, h, None).with_label(label),
       };
       x.draw();
       x.handle();
       x
   }

   // Overrides the draw function
   fn draw(&mut self) {
       self.wid.draw(move |b| {
           draw::draw_box(
               FrameType::FlatBox,
               b.x(),
               b.y(),
               b.width(),
               b.height(),
               Color::from_u32(0x304FFE),
           );
           draw::set_draw_color(Color::White);
           draw::set_font(Font::Courier, 24);
           draw::draw_text2(
               &b.label(),
               b.x(),
               b.y(),
               b.width(),
               b.height(),
               Align::Center,
           );
       });
   }

   // Overrides the handle function.

   // Notice the do_callback which allows the set_callback method to work
   fn handle(&mut self) {
       let mut wid = self.wid.clone();
       self.wid.handle(move |_, ev| match ev {
           Event::Push => {
               wid.do_callback();
               true
           }
           _ => false,
       });
   }
}

impl Deref for FlatButton {
   type Target = Widget;

   fn deref(&self) -> &Self::Target {
       &self.wid
   }
}

impl DerefMut for FlatButton {
   fn deref_mut(&mut self) -> &mut Self::Target {
       &mut self.wid
   }
}

pub struct PowerButton {
   frm: Frame,
   on: Rc<RefCell<bool>>,
}

impl PowerButton {
   pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
       let mut frm = Frame::new(x, y, w, h, "");
       frm.set_frame(FrameType::FlatBox);
       frm.set_color(Color::Black);
       let on = Rc::from(RefCell::from(false));
       frm.draw({
           // storing two almost identical images here, in a real application this could be optimized
           let on = Rc::clone(&on);
           let mut on_svg = SvgImage::from_data(&POWER.to_string().replace("red", "green")).unwrap();
           on_svg.scale(frm.width(), frm.height(), true, true);
           let mut off_svg = SvgImage::from_data(POWER).unwrap();
           off_svg.scale(frm.width(), frm.height(), true, true);
           move |f| {
               if *on.borrow() {
                   on_svg.draw(f.x(), f.y(), f.width(), f.height());
               } else {
                   off_svg.draw(f.x(), f.y(), f.width(), f.height());
               };
           }
      
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。