Iced for Desktop Development in Rust
desktop iced rustAs someone who loves Elm and has recently fallen in love with Rust, I was delighted to learn there are not just one but two Rust frameworks modeled on the Elm Architecture. If you're not familiar, the Elm Architecture is a pain-free way of developing fast, responsive, asynchronous-by-default front-ends. Let's see it in action by developing a simple "Hello, world" app.
Note: I'm using iced
version 0.10.0
. Iced is currently under rapid
development with frequent breaking changes. It's possible these directions
won't work as expected in a future release. As always, if something breaks,
open an issue
or submit a pull request.
Setup
Let's start with a fresh application:
cargo init hello-iced
cd hello-iced
cargo add iced
cargo build
If the build succeeds, we're off to a good start.
Writing a State Model for Iced in Rust
The easiest part of the Elm Architecture to understand is the model state, so let's start there. The model state represents the whole state of the application--everything you would need to know in order to draw any given application screen. As you can imagine, state models are often quite large, but our application will need only one piece of information: whom to greet (by default: the world). That's easy.
struct Hello {
addressee: String
}
Writing a Message Enum for Iced in Rust
Next, we need our Message. A message is an enum that represents all the ways that our model can update according to the logic of our application. For example, you might have a counter that can increment or a counter that can either increment and decrement, or a counter that can increment, decrement, or reset, all depending on the logic of your application.
In our case, we only have one field in our model struct
, and it can only be
updated through a text input, so let's write an enum to that effect:
#[derive(Clone, Debug)]
enum HelloMessage {
TextBoxChange(String)
}
We can see that our enum takes a String
. This represents the new string to
which we will update our model. Also note that it derives Clone
and Debug
.
This is required by the framework.
Implementing iced::Application
Now the real work begins: turning our model into an Iced Application
. Let's
begin:
struct Hello {
addressee: String,
}
#[derive(Clone, Debug)]
enum HelloMessage {
TextBoxChange(String)
}
impl iced::Application for Hello {
}
We need four types for our application.
Executor
- The only type provided by the framework isDefault
, which will suffice for our purposes.Flags
- represents any data we want to initialize our application. We have no flags to pass, so the unit type will suffice.Message
- this is theHelloMessage
we defined earlier.Theme
- the theme of the application.
struct Hello {
addressee: String,
}
#[derive(Clone, Debug)]
enum HelloMessage {
TextBoxChange(String)
}
impl iced::Application for Hello {
type Executor = iced::executor::Default;
type Flags = ();
type Message = HelloMessage;
type Theme = iced::Theme;
}
Next we need a function that initializes our model. Our model will default to greeting, well, the world.
Start by adding use iced::Command;
at the top. A command is an asynchronous
action that the framework will take on next. We don't need to start any
commands on startup, so we'll initialize with Command::none()
.
impl iced::Application for Hello {
type Executor = iced::executor::Default;
type Flags = ();
type Message = HelloMessage;
type Theme = iced::Theme;
fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
( Hello { addressee: String::from("world"), }, Command::none() )
}
We also need a function which takes the &self
state model and returns a title
for the title bar at the top of the application.
impl iced::Application for Hello {
type Executor = iced::executor::Default;
type Flags = ();
type Message = HelloMessage;
type Theme = iced::Theme;
fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
( Hello { addressee: String::from("world"), }, Command::none() )
}
fn title(&self) -> String {
String::from("Greet the World")
}
Now we're getting to the real meat of the application: the update
and view
functions.
The update
function takes our state model and mutates it based on the given
message. It also gives us the opportunity to start another Command
after
mutating our state. For example, a common use case for a submit action message
is to change part of the state to represent that the state is "Loading..." and
then to start a command to fetch some data based on the submission.
We only have one kind of message, so our update function will be very
simple.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
HelloMessage::TextBoxChange(string) => {
self.addressee = string;
Command::none()
}
}
}
Finally, we can write our view. Let's start by use
ing some relevant parts.
use iced::{Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};
We build out our view programmatically using the
push
method, and then convert the final result to an Element
using
into()
.
fn view(&self) -> Element<Self::Message> {
let text = Text::new(format!("Hello, {0}.", self.addressee))
.width(Length::Fill)
.horizontal_alignment(iced::alignment::Horizontal::Center);
let row1 = Row::new().push(text);
let text_input: iced::widget::TextInput<'_, HelloMessage, Renderer> =
iced::widget::text_input("world", self.addressee.as_str())
.on_input(HelloMessage::TextBoxChange);
let row2 = Row::new().push(text_input);
Column::new().push(row1).push(row2).into()
}
Writing a main
function to run
an Iced Application
Finally, we need to run
our application from the main
function. Make sure
to use
iced::Application
to bring the run
function into scope.
use iced::{Application, Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};
pub fn main() -> iced::Result {
Hello::run(Settings::default())
}
Wrapping up
In conclusion, I hope this has been a helpful introduction to the Iced framework and the Elm Architecture. The architecture has a learning curve, so I figure the more learning resources out there, the better.
For reference, this is the full source code:
src/main.rs
use iced::{Application, Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};
pub fn main() -> iced::Result {
Hello::run(Settings::default())
}
struct Hello {
addressee: String,
}
#[derive(Clone, Debug)]
enum HelloMessage {
TextBoxChange(String)
}
impl iced::Application for Hello {
type Executor = iced::executor::Default;
type Flags = ();
type Message = HelloMessage;
type Theme = iced::Theme;
fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
( Hello { addressee: String::from("world"), }, Command::none() )
}
fn title(&self) -> String {
String::from("Greet the World")
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
HelloMessage::TextBoxChange(string) => {
self.addressee = string;
Command::none()
}
}
}
fn view(&self) -> Element<Self::Message> {
let text = Text::new(format!("Hello, {0}.", self.addressee))
.width(Length::Fill)
.horizontal_alignment(iced::alignment::Horizontal::Center);
let row1 = Row::new().push(text);
let text_input: iced::widget::TextInput<'_, HelloMessage, Renderer> =
iced::widget::text_input("world", self.addressee.as_str())
.on_input(HelloMessage::TextBoxChange);
let row2 = Row::new().push(text_input);
Column::new().push(row1).push(row2).into()
}
}
I write to learn, so I welcome your constructive criticism. Report issues on GitLab.