Rust GUI frameworks
2019 Framework 2026: Real users via COSMIC; no AccessKit yet
09

iced: Elm for Rust

A typed, message-driven toolkit that COSMIC is built on.

iced follows the Elm architecture. Your program is a state type, a Message enum, an update function that applies messages, and a view function that builds widgets from state. Widgets never change state; they emit messages. That makes every state change explicit and easy to follow.

iced-counter/src/bin/one.rs ✔ compiled · iced 0.14 · rustc 1.95.0
use book_error::Result;
use iced::widget::{button, column, text, Column};

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

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

impl Counter {
    // The Elm loop: a message comes in, state changes...
    fn update(&mut self, message: Message) {
        match message {
            Message::Increment => self.value += 1,
            Message::Decrement => self.value -= 1,
        }
    }

    // ...and the view is rebuilt from state. Widgets emit messages, never mutate.
    fn view(&self) -> Column<'_, Message> {
        column![
            button("+").on_press(Message::Increment),
            text(self.value).size(40),
            button("-").on_press(Message::Decrement),
        ]
    }
}

fn main() -> Result<()> {
    iced::run(Counter::update, Counter::view)?;
    Ok(())
}

Things that happen outside the UI (timers, sockets, file watchers) arrive as a subscription that turns events into ordinary messages. In iced 0.14 the time::every helper needs an async runtime feature enabled, which is tokio in this project's manifest.

iced-counter/src/bin/two.rs ✔ compiled · iced 0.14 · rustc 1.95.0
use book_error::Result;
use iced::time::{self, Duration};
use iced::widget::text;
use iced::{Element, Subscription};

struct Clock {
    ticks: u64,
}

#[derive(Debug, Clone)]
enum Message {
    Tick,
}

impl Clock {
    fn update(&mut self, _message: Message) {
        self.ticks += 1;
    }

    fn view(&self) -> Element<'_, Message> {
        text(format!("{} seconds", self.ticks)).into()
    }

    // Anything that happens outside the UI (timers, sockets, file watchers)
    // arrives as a Subscription that produces ordinary Messages.
    fn subscription(&self) -> Subscription<Message> {
        time::every(Duration::from_secs(1)).map(|_| Message::Tick)
    }
}

fn main() -> Result<()> {
    iced::application(|| Clock { ticks: 0 }, Clock::update, Clock::view)
        .subscription(Clock::subscription)
        .run()?;
    Ok(())
}

Underneath, the lockfile shows the stack iced assembles: iced_winit for windows, iced_wgpu for GPU drawing with iced_tiny_skia as a software fallback, and cosmic-text for text. System76's COSMIC desktop is built on a fork of iced. Version 0.14 arrived more than a year after 0.13, and iced has no AccessKit integration yet, unlike egui and Slint.

crates.io first publish.