Iced
ElmアーキテクチャにインスパイアされたクロスプラットフォームGUIライブラリ。型安全性と関数型プログラミングの原則に基づき、予測可能な状態管理を実現。美しいアニメーションとカスタムウィジェットをサポート。
GitHub概要
スター27,098
ウォッチ205
フォーク1,338
作成日:2019年7月15日
言語:Rust
ライセンス:MIT License
トピックス
elmgraphicsguiinterfacerenderer-agnosticrusttoolkituser-interfacewidgetwidgets
スター履歴
データ取得日時: 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