AlexZhang

AlexZhang

A Review of Dozens of Rust GUIs Emerging in the WASM World

Streamlining: The Holy Grail of Rust GUI Architecture Has Yet to Be Discovered.
Original: https://monadical.com/posts/shades-of-rust-gui-library-list.html#The-current-toolkit
Translator: GPT-3.5

The development of GUI in Rust is faster than ever, with three months in the Rust GUI field equating to three years in the mortal world. To put this into perspective, the GUI development in the Web has progressed so rapidly that it even outpaces the browsers. There could be many reasons for this, including Rust's cross-platform nature and WebAssembly support, which provides an easier way to distribute software.

In this article, I will review the current GUI toolkits in Rust and share some tips for building WebAssembly bundles. Due to the rapid development in the Rust GUI field, many of the tools I outline below may become outdated in the near future. Therefore, to avoid them becoming obsolete while you read this article, let’s first dive into the latest Rust GUI architectures I have discovered so far.

Toolkits#

Before introducing the toolkits, please note that I have mentioned the Elm Architecture and Immediate Mode several times here, although their definitions are beyond the scope of this article. However, if you have experience with Redux, you can think of Elm Architecture as somewhat similar to Redux, while Immediate Mode is more imperative than Elm Architecture.

It is worth mentioning that many libraries now begin to use "signal" reactive semantics, allowing you to modify state variables directly, which will automatically reflect in the UI, much like we did in the Angular 1 era.

Now, let’s take a look at the toolkits.

Dioxus#

Dioxus has an interface architecture similar to React, but it is based on Rust. For desktop applications, it uses Tauri, with a focus on the Web, experimental Terminal support, and SSR capabilities for generating server-side markup. Dioxus also uses internal mutability similar to React to handle data, such as using use_state. Since it manages global state through an API similar to useContext, it can integrate potential Redux-like tools (“redux/recoil/mobx on top of context”). Below is an example widget built with 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 is a framework written in Rust for creating desktop applications (mobile platforms are coming soon), similar to Electron. Tauri provides its own window interface for applications and allows you to run TypeScript code that communicates with Rust through a custom Promise-based protocol. Tauri is not intended for building for the Web, although it leverages web technologies for desktop applications. You can use many frontend libraries, such as React (provided in its initialization script), or just plain.

<!-- 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 is an experimental but very promising data-driven UI architecture that aligns well with Rust's language architecture and is the successor to Druid. The library's author, Raph Levien, states, “Xilem aims to be the preferred UI library for Rust, drawing a lot of inspiration from SwiftUI and focusing heavily on performance.” Please check out his complete article on Rust UI architecture. In my opinion, this is the best article on the subject so far.

Based on this article, a natural decision is to choose centralized state, much like the Elm architecture. The library uses its own renderer and is about to start leveraging GPU capabilities for faster rendering. The project is currently in a very early experimental stage. It is currently targeting WebGPU, which has issues running in browsers, but it has great potential.

Iced#

Iced is a “cross-platform GUI library focused on simplicity and type safety.” It uses the Elm architecture and a reactive programming model. By default, the Web build uses the DOM for rendering, but Iced can also be used with other backends such as wgpu or glow. Currently, the Web part of the library seems to be somewhat neglected (https://github.com/iced-rs/iced_web), and it has not been updated for a while. But hopefully, they can speed up updates. Here, the Bezier tool they built uses 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 is "the easiest to use GUI library." It uses an immediate mode GUI architecture https://en.wikipedia.org/wiki/Immediate_mode_GUI. One of its goals is portability and ease of use. The native look is intentionally not a goal. For example, it is suitable for simple GUIs, games, or other applications that need a fast GUI plugin library. You can check out their WebAssembly example builds:

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);
       });
   }
}

Note that there is a larger Demo available here.

Kas#

Kas is an “efficient stateful toolkit” that combines several GUI data models. For example, it handles messages like Elm but can retain state within components. It currently cannot work in a browser environment, at least I have not been able to quickly build an example. This is because it is more advanced than browsers using WebGPU, rather than the other way around.

Slint#

Slint is intended for native applications rather than web environments. It can only be compiled to WebAssembly for demonstration purposes. Slint aims for a native look, embedded systems, microcontrollers, and desktop applications. It uses its own scripting language to describe the UI, providing component state and appearance descriptions. Slint allows some scripting programming, although the heaviest code can run in Rust. Overall, it is an imperative toolkit.

Pasted image 20230427141739

Here is the compiled code from their tutorial:

// 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 is a “reactive library for creating Rust and WebAssembly web applications.” Its main feature is excellent reactive performance. It looks like mobx, using binding/signal semantics (very similar to Angular or early React), providing a good developer experience:

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)
   }
}

The above code generates DOM.

By default, Sycamore is designed for the Web, but it can easily be embedded into other tools like Tauri to create desktop applications: https://github.com/JonasKruckenberg/tauri-sycamore-template. Currently, the Sycamore template has not been included in Tauri's create-tauri-app tool (https://github.com/tauri-apps/create-tauri-app), but Yew has.

// 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 seems to be quite popular now. It is a GUI framework specifically for web development, with some features similar to React but also some differences. For example, components are like state machines with defined messages and reactions to those messages. Components maintain state within themselves. Yewdux can be used for Redux-like data handling. Among other features, Yew also has hydration and server-side rendering.

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 has been renamed to rltk (Roguelike Toolkit). Bracket consists of a console rendering library and several tools for writing Roguelike games, supporting both Web and desktop platforms. Check out this killer tutorial where you can use Rust to write a Roguelike game.

By default, bracket-lib runs in OpenGL mode (it uses WebGL if it detects that you are compiling wasm32-unknown-unknown). It can use wgpu as a backend, which will be very convenient in the future. Here is the web version of the last level from their tutorial:

// 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)
}

Since we are mentioning Bracket, I must also mention Cursive, which is a console library for terminal interfaces, although it currently seems to lack a compilable backend for browsers.
![[Pasted image 20230427141944.png]]
Here is an example running on the desktop:

// 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 is a declarative reactive GUI framework. It can be used across multiple platforms (Windows, Linux, MacOS, Web) and has a data processing approach similar to Elm, rendering on the GPU.

Pasted image 20230427142002

Here is one of the examples:

// 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 is a React-equivalent isomorphic web framework, but it uses Rust closures for finer-grained reactive design. The entire render function does not execute on every signal update but is treated as a “setup” function. Additionally, Leptos does not have a virtual DOM. The framework also includes a router that is available on both the server and client.

Screenshot 2023-04-27 at 14.20.26

Here is an example of the Leptos router:

// 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 is a framework designed to quickly support every rendering strategy while providing an excellent developer experience. Its scope is the Web, and it uses reactive semantics to represent state variables. It is built on top of Sycamore but adds state and templating concepts on top of it.

Screenshot 2023-04-27 at 14.20.52

Example:

// 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#

This is a simple library for building web UIs. It uses a React-like UI declaration and Elm-like state management.

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#

A full-stack framework from a former Seed maintainer. As seen in the examples, its frontend state management uses signal semantics.

I couldn't build this example until this issue is resolved.

The "counter" example code is as follows:

// 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 is a convenient GUI library based on gtk4-rs, inspired by Elm. Relm4 is a rebuilt version of relm that is compatible with GTK4 and libadwaita. While the original relm is still maintained, these two libraries (relm and relm4) seem to be maintained by different people.

Currently, it only supports devices and desktop, with no web availability, likely due to the use of the gtk4 backend. Nevertheless, Relm4 can leverage CSS, as seen in this example, where global CSS is used for UI rendering.
Here is a screenshot of one of their examples running on the desktop:

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 is a binding library for https://www.fltk.org. FLTK currently does not support Wasm or iOS. It has experimental support for Android (varies by person) and focuses on desktop applications. This is one of the low-level bindings that can be used as a graphical foundation for applications or GUI frameworks.

The image below shows one of their examples running on the desktop:

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());
               };
           }
       });
       frm.handle({
           let on = on.clone();
           move |f, ev| match ev {
               Event::Push => {
                   let prev = *on.borrow();
                   *on.borrow_mut() = !prev;
                   f.do_callback();
                   f.redraw();
                   true
               }
               _ => false,
           }
       });
       Self { frm, on }
   }

   pub fn is_on(&self) -> bool {
       *self.on.borrow()
   }
}

impl Deref for PowerButton {
   type Target = Frame;

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

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

pub struct FancyHorSlider {
   s: Slider,
}

impl FancyHorSlider {
   pub fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
       let mut s = Slider::new(x, y, width, height, "");
       s.set_type(SliderType::Horizontal);
       s.set_frame(FrameType::RFlatBox);
       s.set_color(Color::from_u32(0x868db1));
       s.draw(|s| {
           draw::set_draw_color(Color::Blue);
           draw::draw_pie(
               s.x() - 10 + (s.w() as f64 * s.value()) as i32,
               s.y() - 10,
               30,
               30,
               0.,
               360.,
           );
       });
       Self { s }
   }
}

impl Deref for FancyHorSlider {
   type Target = Slider;

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

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

fn main() {
   let app = app::App::default().with_scheme(app::Scheme::Gtk);
   // app::set_visible_focus(false);

   let mut wind = Window::default()
       .with_size(800, 600)
       .with_label("Custom Widgets");
   let mut but = FlatButton::new(350, 350, 160, 80, "Increment");
   let mut power = PowerButton::new(600, 100, 100, 100);
   let mut dial = FillDial::new(100, 100, 200, 200, "0");
   let mut frame = Frame::default()
       .with_size(160, 80)
       .with_label("0")
       .above_of(&*but, 20);
   let mut fancy_slider = Fancy
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.