省流: Rust GUI 架构の聖杯はまだ発見されていない。
原文:https://monadical.com/posts/shades-of-rust-gui-library-list.html#The-current-toolkit
译者:GPT-3.5
Rust における GUI の発展速度は前例のない速さで、Rust GUI の分野の 3 ヶ月は凡人の世界の 3 年に相当します。比較のために、Web の GUI は非常に速く進化しており、ブラウザをも凌駕しています。これには多くの理由が考えられ、Rust のクロスプラットフォーム性や WebAssembly のサポートが含まれ、より簡単なソフトウェア配布方法を提供しています。
この記事では、Rust における現在の GUI ツールキットを振り返り、WebAssembly バンドルを構築するためのいくつかのヒントを共有します。Rust GUI の分野は急速に進化しているため、以下に概説する多くのツールは近い将来には時代遅れになる可能性があります。したがって、この記事を読んでいる間に古くならないように、これまでに私が発見した最新の Rust GUI アーキテクチャについて深く掘り下げていきましょう。
ツールキット#
ツールキットを紹介する前に、ここで何度も言及したElm ArchitectureとImmediate Modeについて注意してください。これらの定義はこの記事の範囲を超えています。しかし、Redux の経験がある場合、Elm Architecture は Redux に似ていると考えることができ、Immediate Mode は Elm Architecture よりも命令的です。
現在、多くのライブラリが「信号」反応的セマンティクスを使用し始めており、状態変数を直接変更できるようになっており、これは Angular 1 の時代と同様に UI に自動的に反映されます。
それでは、ツールキットを見ていきましょう。
Dioxus#
Dioxus は React に似たインターフェースアーキテクチャを持っていますが、Rust に基づいています。デスクトップアプリケーションには Tauri を使用し、Web に重点を置いており、実験的な Terminal サポートとサーバーサイドレンダリング(SSR)機能を持っています。Dioxus は、use_state を使用するなど、データを処理するために React に似た内部可変性を使用しています。useContext に似た API を介してグローバル状態管理を行うため、潜在的な Redux クラスツール(「redux/recoil/mobx on top of context」)を統合することができます。以下は Dioxus を使用して構築されたウィジェットの例です:
// main.rs
fn main() {
// ウェブアプリを起動
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)",
}
// 他のキューブの面のCSS定義は省略
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) {
// Tauriコマンドの詳細については 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("tauriアプリケーションの実行中にエラーが発生しました");
}
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
//! この例は、ベジェ曲線を描画するためのインタラクティブな`Canvas`を紹介します。
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 のサンプルビルドを確認できます:
// app.rs
// main.rs / lib.rsのボイラープレートは省略
/// アプリの状態をシャットダウン時に永続化できるように、Deserialize/Serializeを導出します。
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // 新しいフィールドを追加した場合、古い状態をデシリアライズする際にデフォルト値を与えます
pub struct TemplateApp {
// 例の内容:
label: String,
// メンバーのシリアライズをオプトアウトする方法
#[serde(skip)]
value: f32,
}
impl Default for TemplateApp {
fn default() -> Self {
Self {
// 例の内容:
label: "Hello World!".to_owned(),
value: 2.7,
}
}
}
impl TemplateApp {
/// 最初のフレームの前に一度呼び出されます。
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// ここでeguiの外観や感触をカスタマイズできます
// `cc.egui_ctx.set_visuals`や`cc.egui_ctx.set_fonts`を使用して。
// 前のアプリの状態をロードします(あれば)。
// これが機能するには、`persistence`機能を有効にする必要があります。
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
Default::default()
}
}
impl eframe::App for TemplateApp {
/// シャットダウン前に状態を保存するためにフレームワークによって呼び出されます。
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// UIが再描画されるたびに呼び出されます。これは、1秒間に何度も呼び出される可能性があります。
/// ウィジェットを`SidePanel`、`TopPanel`、`CentralPanel`、`Window`または`Area`に配置します。
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self { label, value } = self;
// 異なるパネルやウィンドウを作成する方法の例。
// あなたに合ったものを選んでください。
// ヒント:良いデフォルトの選択は、単に`CentralPanel`を維持することです。
// インスピレーションや他の例については、https://emilk.github.io/eguiを参照してください。
#[cfg(not(target_arch = "wasm32"))] // ウェブページではFile->Quitはありません!
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// トップパネルはメニューバーに適した場所です:
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("サイドパネル");
ui.horizontal(|ui| {
ui.label("何かを書く: ");
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| {
// 中央パネルは、TopPanelやSidePanelの追加後に残された領域です。
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);
});
}
}
注意、公式にはより大きなデモがあります。
Kas#
Kas は「効率的な状態保持ツールキット」で、いくつかの GUI データモデルを組み合わせています。たとえば、メッセージを処理する際には Elm のように動作しますが、コンポーネント内で状態を保持できます。現在、ブラウザ環境では動作しないため、少なくとも私は迅速にサンプルを構築できませんでした。これは、WebGPU を使用するブラウザよりも先進的だからです。逆ではありません。
Slint#
Slint は Web 環境ではなく、ネイティブアプリケーション用に設計されています。デモ目的で WebAssembly にコンパイルすることができます。Slint の目標はネイティブな外観、組み込みシステム、マイクロコントローラー、デスクトップアプリケーションです。独自のスクリプト言語を使用して UI を記述し、コンポーネントの状態と外観を説明します。Slint はある程度のスクリプトプログラミングを許可しますが、最も重いコードは Rust で実行できます。全体的に見て、命令的なツールです。
以下は彼らのチュートリアルのコンパイルコードです:
// main.rs
#[cfg_attr(target_arch = "wasm32",
wasm_bindgen::prelude::wasm_bindgen(start))]
pub fn main() {
use slint::Model;
let main_window = MainWindow::new();
// モデルからタイルを取得
let mut tiles: Vec<TileData> = main_window.get_memory_tiles().iter().collect();
// ペアを確保するためにそれらを複製
tiles.extend(tiles.clone());
// タイルをランダムに混ぜる
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
tiles.shuffle(&mut rng);
// シャッフルされたVecをモデルプロパティに割り当てる
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;
}
// 左のカーテン
Rectangle {
background: #193076;
width: open_curtain ? 0px : (parent.width / 2);
height: parent.height;
animate width { duration: 250ms; easing: ease-in; }
}
// 右のカーテン
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 => {
// この要素のユーザーに委譲
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;
// モデルからタイルに解決済みの状態を伝播
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 {
// Tauriコマンドの詳細については 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 は水合とサーバーサイドレンダリングをサポートしています。
// 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 上でレンダリングされます。
以下はその一例です:
// 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|
// ラベルとアイコン
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)),
// オプションのリスト
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 がありません。このフレームワークには、サーバーとクライアントの両方で使用できるルーターも含まれています。
以下は Leptos ルーターの例です:
// lib.rs
// main.rsのボイラープレートは省略
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>
// 通常の<a>要素はクライアントサイドのナビゲーションに使用できます
// <A>を使用すると2つの効果があります:
// 1) ネストされたルートの相対ルーティングが正しく機能することを保証します
// 2) 現在のリンクに`aria-current`属性を設定し、
// アクセシビリティとスタイリングの目的のために
<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| {
// このデータは頻繁に変わらないので、.map().collect()を使用できます
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(),
get_contact,
);
let contact_display = move || match contact.read() {
None => None,
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
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/>");
let navigate = use_navigate(cx);
view! { cx,
<>
<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> {
// 人工的な遅延でAPI呼び出しを偽装
_ = 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> {
// 人工的な遅延でAPI呼び出しを偽装
_ = 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 に基づいて構築されていますが、その上に状態とテンプレートの概念を追加しています。
例:
// template/index.rs
use crate::global_state::AppStateRx;
use perseus::prelude::*;
use sycamore::prelude::*;
// このテンプレートはこの例では独自の状態を持たないことに注意してくださいが、確かに持つことができます。
fn index_page<G: Html>(cx: Scope) -> View<G> {
// グローバル状態にアクセスします。
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
view! { cx,
// ユーザーはグローバル状態を入力を通じて変更でき、その変更はアプリ全体に反映されます。
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 に似た状態管理を使用しています。
// 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 は GTK4 と libadwaita に互換性がある新しいバージョンの relm です。元の relm はまだメンテナンスされていますが、これらの 2 つのライブラリ(relm と relm4)は異なる人々によってメンテナンスされているようです。
現在はデバイスとデスクトップ端末のみをサポートしています。Web は利用できないかもしれませんが、GTK4 バックエンドを使用しているためです。それにもかかわらず、Relm4 は CSS を利用でき、たとえばhttps://github.com/Relm4/Relm4/blob/53b9a6bf6e514a5cce979c8307a4fefea422bdd7/examples/tracker.rsのように、UI レンダリングにグローバル CSS を使用します。
以下はデスクトップで実行されているスクリーンショットです:
// 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 {
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()
}
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);
}
}
}
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 フレームワークがグラフィック基盤として使用できる低レベルのバインディングの一つです。
以下の画像は、デスクトップで実行されている彼らの一例を示しています:
// 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;