Iced

ElmアーキテクチャにインスパイアされたクロスプラットフォームGUIライブラリ。型安全性と関数型プログラミングの原則に基づき、予測可能な状態管理を実現。美しいアニメーションとカスタムウィジェットをサポート。

デスクトップクロスプラットフォームRustGUIエルムアーキテクチャ型安全

GitHub概要

iced-rs/iced

A cross-platform GUI library for Rust, inspired by Elm

ホームページ:https://iced.rs
スター27,098
ウォッチ205
フォーク1,338
作成日:2019年7月15日
言語:Rust
ライセンス:MIT License

トピックス

elmgraphicsguiinterfacerenderer-agnosticrusttoolkituser-interfacewidgetwidgets

スター履歴

iced-rs/iced Star History
データ取得日時: 2025/7/17 02:30

フレームワーク

Iced

概要

IcedはElm(エルム)からインスパイアされたクロスプラットフォームGUIライブラリfor Rustです。エルムアーキテクチャの原則に基づき、型安全で予測可能なGUIアプリケーション開発を可能にし、関数型プログラミングのパラダイムをGUI開発に取り入れた革新的なフレームワークです。

詳細

Iced(アイスド)は、Elm言語のアーキテクチャをRustに移植したGUIフレームワークとして、2025年現在も着実に進化を続けています。Model-View-Update(MVU)パターンを採用することで、状態管理の明確性と型安全性を実現し、従来のコールバック地獄から開発者を解放します。

Icedのアーキテクチャは、アプリケーション状態(Model)、状態を描画する関数(View)、状態を更新する純粋関数(Update)の3つの要素から構成されます。このパターンにより、アプリケーションの動作が予測可能になり、デバッグとテストが大幅に簡素化されます。

技術的には、Icedは複数のレンダリングバックエンドを提供し、wgpu(WebGPU)、glow(OpenGL)、TinySkiaなど、要件に応じて最適なレンダリング手法を選択できます。WebAssemblyサポートにより、同じコードベースでネイティブアプリとWebアプリの両方を構築でき、真のクロスプラットフォーム開発を実現します。

メリット・デメリット

メリット

  • エルムアーキテクチャ: 予測可能で型安全な状態管理
  • 関数型パラダイム: 副作用の排除と明確なデータフロー
  • 真のクロスプラットフォーム: ネイティブとWeb両方で動作
  • 柔軟なレンダリング: 複数のバックエンド対応
  • 優れたテスタビリティ: 純粋関数による単体テスト容易性
  • 豊富なウィジェット: ボタン、テキスト、スライダーなど充実のコンポーネント
  • 非同期サポート: futures/asyncパターンの統合

デメリット

  • 学習コスト: エルムアーキテクチャの理解が必要
  • パフォーマンス制約: immediate modeより重い処理
  • 成熟度: 他のGUIライブラリより機能が限定的
  • コミュニティ: 比較的小さい開発者コミュニティ
  • ドキュメント: 日本語リソースが限定的
  • 複雑な状態管理: 大規模アプリでのボイラープレート増加

主要リンク

書き方の例

Hello World カウンターアプリ

use iced::widget::{button, column, text, Column};
use iced::{Element, Sandbox, Settings};

#[derive(Default)]
struct Counter {
    value: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    Increment,
    Decrement,
}

impl Sandbox for Counter {
    type Message = Message;

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

    fn title(&self) -> String {
        String::from("A cool counter")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::Increment => {
                self.value += 1;
            }
            Message::Decrement => {
                self.value -= 1;
            }
        }
    }

    fn view(&self) -> Element<Message> {
        column![
            button("+").on_press(Message::Increment),
            text(self.value).size(50),
            button("-").on_press(Message::Decrement),
        ]
        .into()
    }
}

fn main() -> iced::Result {
    Counter::run(Settings::default())
}

複雑な状態管理とレイアウト

use iced::widget::{button, column, container, row, text, text_input};
use iced::{Alignment, Element, Length, Sandbox, Settings};

#[derive(Default)]
struct TodoApp {
    todos: Vec<Todo>,
    input: String,
}

#[derive(Clone, Debug)]
struct Todo {
    id: usize,
    text: String,
    completed: bool,
}

#[derive(Debug, Clone)]
enum Message {
    AddTodo,
    InputChanged(String),
    ToggleTodo(usize),
    DeleteTodo(usize),
    ClearCompleted,
}

impl Sandbox for TodoApp {
    type Message = Message;

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

    fn title(&self) -> String {
        String::from("Todo App - Iced")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::AddTodo => {
                if !self.input.trim().is_empty() {
                    let id = self.todos.len();
                    self.todos.push(Todo {
                        id,
                        text: self.input.clone(),
                        completed: false,
                    });
                    self.input.clear();
                }
            }
            Message::InputChanged(value) => {
                self.input = value;
            }
            Message::ToggleTodo(id) => {
                if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
                    todo.completed = !todo.completed;
                }
            }
            Message::DeleteTodo(id) => {
                self.todos.retain(|todo| todo.id != id);
            }
            Message::ClearCompleted => {
                self.todos.retain(|todo| !todo.completed);
            }
        }
    }

    fn view(&self) -> Element<Message> {
        let input_section = row![
            text_input("What needs to be done?", &self.input)
                .on_input(Message::InputChanged)
                .on_submit(Message::AddTodo)
                .padding(10),
            button("Add")
                .on_press(Message::AddTodo)
                .padding(10)
        ]
        .spacing(10)
        .align_items(Alignment::Center);

        let todos_section = column![{
            let mut col = column![];
            for todo in &self.todos {
                let todo_row = row![
                    button(if todo.completed { "✓" } else { "○" })
                        .on_press(Message::ToggleTodo(todo.id)),
                    text(&todo.text).width(Length::Fill),
                    button("×")
                        .on_press(Message::DeleteTodo(todo.id))
                ]
                .spacing(10)
                .align_items(Alignment::Center);
                
                col = col.push(todo_row);
            }
            col
        }]
        .spacing(5);

        let controls = row![
            text(format!("Total: {}", self.todos.len())),
            button("Clear Completed")
                .on_press(Message::ClearCompleted)
        ]
        .spacing(20)
        .align_items(Alignment::Center);

        container(
            column![
                text("Todo List")
                    .size(24)
                    .width(Length::Fill)
                    .horizontal_alignment(iced::alignment::Horizontal::Center),
                input_section,
                todos_section,
                controls
            ]
            .spacing(20)
            .padding(20)
            .max_width(600)
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .center_x()
        .center_y()
        .into()
    }
}

fn main() -> iced::Result {
    TodoApp::run(Settings::default())
}

非同期処理とネットワーク通信

use iced::widget::{button, column, container, text};
use iced::{executor, Application, Command, Element, Settings, Theme};
use std::time::Duration;

#[derive(Default)]
struct AsyncApp {
    status: String,
    is_loading: bool,
}

#[derive(Debug, Clone)]
enum Message {
    FetchData,
    DataFetched(Result<String, String>),
    StartTimer,
    TimerFinished,
}

impl Application for AsyncApp {
    type Executor = executor::Default;
    type Message = Message;
    type Theme = Theme;
    type Flags = ();

    fn new(_flags: ()) -> (Self, Command<Message>) {
        (Self::default(), Command::none())
    }

    fn title(&self) -> String {
        String::from("Async Example")
    }

    fn update(&mut self, message: Message) -> Command<Message> {
        match message {
            Message::FetchData => {
                self.is_loading = true;
                self.status = "Fetching data...".to_string();
                Command::perform(fetch_data(), Message::DataFetched)
            }
            Message::DataFetched(result) => {
                self.is_loading = false;
                match result {
                    Ok(data) => self.status = format!("Success: {}", data),
                    Err(error) => self.status = format!("Error: {}", error),
                }
                Command::none()
            }
            Message::StartTimer => {
                self.status = "Timer started...".to_string();
                Command::perform(
                    async {
                        tokio::time::sleep(Duration::from_secs(3)).await;
                    },
                    |_| Message::TimerFinished,
                )
            }
            Message::TimerFinished => {
                self.status = "Timer finished!".to_string();
                Command::none()
            }
        }
    }

    fn view(&self) -> Element<Message> {
        container(
            column![
                text("Async Operations Example").size(24),
                text(&self.status).size(16),
                button("Fetch Data")
                    .on_press_maybe(if self.is_loading { None } else { Some(Message::FetchData) }),
                button("Start Timer")
                    .on_press(Message::StartTimer),
            ]
            .spacing(20)
            .padding(20)
        )
        .width(iced::Length::Fill)
        .height(iced::Length::Fill)
        .center_x()
        .center_y()
        .into()
    }
}

async fn fetch_data() -> Result<String, String> {
    // 模擬的なHTTPリクエスト
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok("Data from server".to_string())
}

fn main() -> iced::Result {
    AsyncApp::run(Settings::default())
}

カスタムウィジェットの作成

use iced::widget::canvas::{self, Canvas, Cursor, Geometry, Path, Stroke};
use iced::widget::{column, container, slider};
use iced::{
    Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Size, Theme, Vector,
};

#[derive(Default)]
struct CircleApp {
    radius: f32,
    color_hue: f32,
}

#[derive(Debug, Clone)]
enum Message {
    RadiusChanged(f32),
    ColorChanged(f32),
}

impl Sandbox for CircleApp {
    type Message = Message;

    fn new() -> Self {
        Self {
            radius: 50.0,
            color_hue: 0.0,
        }
    }

    fn title(&self) -> String {
        String::from("Custom Canvas Widget")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::RadiusChanged(radius) => {
                self.radius = radius;
            }
            Message::ColorChanged(hue) => {
                self.color_hue = hue;
            }
        }
    }

    fn view(&self) -> Element<Message> {
        container(
            column![
                Canvas::new(CircleCanvas {
                    radius: self.radius,
                    color_hue: self.color_hue,
                })
                .width(Length::Fill)
                .height(Length::Fixed(300.0)),
                slider(10.0..=100.0, self.radius, Message::RadiusChanged)
                    .step(1.0),
                slider(0.0..=360.0, self.color_hue, Message::ColorChanged)
                    .step(1.0),
            ]
            .spacing(20)
            .padding(20)
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .center_x()
        .center_y()
        .into()
    }
}

struct CircleCanvas {
    radius: f32,
    color_hue: f32,
}

impl canvas::Program<Message> for CircleCanvas {
    type State = ();

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: Cursor,
    ) -> Vec<Geometry> {
        let circle = Path::circle(bounds.center(), self.radius);

        // HSVからRGBへの変換(簡略版)
        let hue = self.color_hue / 60.0;
        let c = 1.0;
        let x = 1.0 - (hue % 2.0 - 1.0).abs();
        let (r, g, b) = match hue as i32 {
            0 => (c, x, 0.0),
            1 => (x, c, 0.0),
            2 => (0.0, c, x),
            3 => (0.0, x, c),
            4 => (x, 0.0, c),
            _ => (c, 0.0, x),
        };

        let color = Color::from_rgb(r, g, b);

        vec![canvas::Geometry::from_path(
            circle,
            canvas::Fill::Solid(color),
        )]
    }
}

fn main() -> iced::Result {
    CircleApp::run(Settings::default())
}

Webアプリケーション対応

// Cargo.tomlの設定
// [dependencies]
// iced = { version = "0.10", features = ["canvas", "tokio"] }
//
// [target.'cfg(target_arch = "wasm32")'.dependencies]
// iced = { version = "0.10", features = ["canvas"] }

use iced::widget::{button, column, container, text};
use iced::{Element, Length, Sandbox, Settings};

#[derive(Default)]
struct WebApp {
    counter: i32,
    message: String,
}

#[derive(Debug, Clone)]
enum Message {
    Increment,
    Decrement,
    Reset,
}

impl Sandbox for WebApp {
    type Message = Message;

    fn new() -> Self {
        Self {
            counter: 0,
            message: "Web上で動作するIcedアプリ".to_string(),
        }
    }

    fn title(&self) -> String {
        String::from("Iced Web App")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::Increment => {
                self.counter += 1;
                self.message = format!("カウンター: {}", self.counter);
            }
            Message::Decrement => {
                self.counter -= 1;
                self.message = format!("カウンター: {}", self.counter);
            }
            Message::Reset => {
                self.counter = 0;
                self.message = "リセットしました".to_string();
            }
        }
    }

    fn view(&self) -> Element<Message> {
        container(
            column![
                text("Iced Web Application").size(24),
                text(&self.message).size(16),
                text(self.counter).size(48),
                column![
                    button("+ Increment").on_press(Message::Increment),
                    button("- Decrement").on_press(Message::Decrement),
                    button("Reset").on_press(Message::Reset),
                ]
                .spacing(10)
            ]
            .spacing(20)
            .padding(40)
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .center_x()
        .center_y()
        .into()
    }
}

fn main() -> iced::Result {
    WebApp::run(Settings::default())
}

プロジェクトセットアップとビルド

# 新しいRustプロジェクト作成
cargo new iced-app
cd iced-app

# Cargo.tomlの依存関係設定
# [dependencies]
# iced = "0.10"
# tokio = { version = "1.0", features = ["full"] }

# ネイティブアプリ実行
cargo run

# リリースビルド
cargo build --release

# Webアプリケーション向けセットアップ
rustup target add wasm32-unknown-unknown
cargo install trunk

# index.htmlファイル作成
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Iced Web App</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 100vw;
            height: 100vh;
            overflow: hidden;
        }
        canvas {
            outline: none;
        }
    </style>
</head>
<body></body>
</html>
EOF

# Webアプリビルドと実行
trunk serve --release

# 異なるレンダリングバックエンドでのビルド
# WebGPU(推奨)
cargo run --features wgpu

# OpenGL
cargo run --features glow

# ソフトウェアレンダリング
cargo run --features tiny-skia

# クロスプラットフォームビルド
# Windows向け
cargo build --target x86_64-pc-windows-gnu --release

# macOS向け
cargo build --target x86_64-apple-darwin --release

# Linux向け
cargo build --target x86_64-unknown-linux-gnu --release