Iced

Cross-platform GUI library inspired by Elm architecture. Based on type safety and functional programming principles, achieving predictable state management. Supports beautiful animations and custom widgets.

desktopcross-platformRustGUIelm-architecturetype-safe

GitHub Overview

iced-rs/iced

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

Stars27,098
Watchers205
Forks1,338
Created:July 15, 2019
Language:Rust
License:MIT License

Topics

elmgraphicsguiinterfacerenderer-agnosticrusttoolkituser-interfacewidgetwidgets

Star History

iced-rs/iced Star History
Data as of: 7/17/2025, 02:30 AM

Framework

Iced

Overview

Iced is a cross-platform GUI library for Rust, inspired by Elm. Based on the principles of Elm architecture, it enables type-safe and predictable GUI application development, bringing functional programming paradigms to GUI development as an innovative framework.

Details

Iced is a GUI framework that ports the Elm language architecture to Rust, continuing its steady evolution as of 2025. By adopting the Model-View-Update (MVU) pattern, it achieves clarity in state management and type safety, liberating developers from traditional callback hell.

Iced's architecture consists of three elements: application state (Model), a function that renders the state (View), and pure functions that update the state (Update). This pattern makes application behavior predictable, significantly simplifying debugging and testing.

Technically, Iced provides multiple rendering backends, allowing selection of optimal rendering approaches based on requirements, including wgpu (WebGPU), glow (OpenGL), and TinySkia. WebAssembly support enables building both native and web applications from the same codebase, achieving true cross-platform development.

Pros and Cons

Pros

  • Elm Architecture: Predictable and type-safe state management
  • Functional Paradigm: Elimination of side effects and clear data flow
  • True Cross-Platform: Works on both native and web platforms
  • Flexible Rendering: Multiple backend support
  • Excellent Testability: Easy unit testing with pure functions
  • Rich Widgets: Comprehensive components including buttons, text, sliders
  • Async Support: Integration with futures/async patterns

Cons

  • Learning Curve: Understanding of Elm architecture required
  • Performance Constraints: Heavier processing than immediate mode
  • Maturity: More limited features compared to other GUI libraries
  • Community: Relatively small developer community
  • Documentation: Limited Japanese resources
  • Complex State Management: Increased boilerplate in large applications

Key Links

Code Examples

Hello World Counter App

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

Complex State Management and Layout

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

Asynchronous Processing and Network Communication

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> {
    // Simulate HTTP request
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok("Data from server".to_string())
}

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

Creating Custom Widgets

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 to RGB conversion (simplified)
        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 Application Support

// Cargo.toml configuration
// [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: "Iced app running on the web".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!("Counter: {}", self.counter);
            }
            Message::Decrement => {
                self.counter -= 1;
                self.message = format!("Counter: {}", self.counter);
            }
            Message::Reset => {
                self.counter = 0;
                self.message = "Reset completed".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())
}

Project Setup and Build

# Create new Rust project
cargo new iced-app
cd iced-app

# Cargo.toml dependencies
# [dependencies]
# iced = "0.10"
# tokio = { version = "1.0", features = ["full"] }

# Run native app
cargo run

# Release build
cargo build --release

# Web application setup
rustup target add wasm32-unknown-unknown
cargo install trunk

# Create index.html file
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

# Build and run web app
trunk serve --release

# Build with different rendering backends
# WebGPU (recommended)
cargo run --features wgpu

# OpenGL
cargo run --features glow

# Software rendering
cargo run --features tiny-skia

# Cross-platform builds
# For Windows
cargo build --target x86_64-pc-windows-gnu --release

# For macOS
cargo build --target x86_64-apple-darwin --release

# For Linux
cargo build --target x86_64-unknown-linux-gnu --release