Chapter 1

Warnings

  • Iced is not production ready and is not yet 1.0. Some tinkering will be involved.
  • For this tutorial, the web aspect of Iced will be ignored.
  • Iced is not React/HTML/CSS/Swing/etc. and you should not treat it as such.
  • This tutorial targets the latest commits.

Setup

Dependencies:

# The core intefaces for the iced framework.
iced = { git = "https://github.com/iced-rs/iced" }

Hello World.

use iced::widget::Text;
use iced::{executor, Application, Command, Element, Settings, Theme};

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

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

struct HelloWorld {}

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

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

  fn title(&self) -> String {
    String::from("Hello World!")
  }

  fn update(&mut self, _message: Message) -> Command<Self::Message> {
    Command::none()
  }

  fn view(&self) -> Element<Message> {
    Element::new(Text::new("Hello World!"))
  }

  fn theme(&self) -> Theme {
    Theme::default()
  }
}

Applications

All Iced GUIs start with an Application trait implemented on a struct. The Application trait requires a few associated types:

Executor

Specifies which runtime will run async Iced code. This may change depending on your target platform i.e. desktop/web/etc.

Message

Message is a type used by your application when any UI state changes occur. It must implement Debug and Send (indicates a type which can be sent across threads) traits. Because this type will be used for application Messages, it should be fast and ergonomic to pass around. Deriving the Clone and Copy traits is the easiest way to do this. Message will usually be an enum because it has to encode all possible UI events.

Theme

A type used for styling information. This type must implement the Default and StyleSheet traits.

Flags

Flags is a type for transferring information to your Iced Application at startup.


Setting-up an Application:

1. Setup Initial State with new()

#![allow(unused)]
fn main() {
fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
  (HelloWorld {}, Command::none())
}
}

New is like any other "new" function in Rust. Its goal is to provide a valid starting state. The 2nd tuple value is used for async operations that may need to be performed at startup -- like authentication.

2. Display your state with Widgets using view()

#![allow(unused)]
fn main() {
fn view(&self) -> Element<Message> {
  Element::new(Text::new("Hello World!"))
}
}

View takes a reference to self providing access to the current UI state and outputs Widgets. Iced tries to minimize the amount of calls to view. It is typically called for initial renders or in response to UI Messages. Widgets are structs implementing the Widget trait.

#![allow(unused)]
fn main() {
pub trait Widget<Message, Renderer>
where
    Renderer: crate::Renderer
}

Widgets are the link between raw UI data and Iced. Widgets know what data they have, how to render it, and when changes to the data should emit Messages. The Widget trait has a Renderer and a Message type parameter. Luckily, these are usually inferred. Notice that view does not want just any widget. It wants an Element!

To get an element from a built-in widget you can use Element::new() or call .into(). An Element is a generic Widget that holds a Box to another Widget. Elements abstract away implementation details and make Iced code easier to read.

3. Update UI State in response to UI Messages With update()

Update is responsible for handling Messages and modifying UI state.

#![allow(unused)]
fn main() {
fn update(&mut self, _message: Message) -> Command<Self::Message> {
  Command::none()
}
}

Notice update has a mutable reference to the UI data allowing us to centralize modifications to our data. This pattern plays well with the design pattern. Similar to new, update can return a Command for async operations like downloading a file.


Starting Iced

We simply need to tell Iced to take over.

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

Note: run will not return until the Iced window has closed.

State Management

Iced has a simple model for state. The type that implements the Application trait stores the entirety of your application's state.

Cargo.toml

We will need to add a few things to Cargo.toml.

# ...
[dependencies]
# The core intefaces for the iced framework.
iced = { git = "https://github.com/iced-rs/iced", features = ["tokio"]}
# For time
chrono = "0.4.26"

reqwest = { version = "0.11", features = ["json"] }

serde = { version = "1.0.157", features = ["derive"] }
serde_json = "1.0.94"

Handling Basic UI Events

New, view, and update presented in chapter 1 provide the basic blocks to manage state.

View tells Iced how you want your state to be displayed by producing a tree of Widgets. This involves passing any state the Widgets need. For components that generate Messages, view() must tell that component the type of Message to generate. The actual logic to handle those Messages is handled in update(). This makes handling UI events easy. Lets look at an example to see how easy it is to update our state in response to basic UI events.

use iced::alignment::{Horizontal, Vertical};
use iced::widget::{Button, Container, Row, Text};
use iced::{executor, Alignment, Application, Command, Element, Length, Settings, Theme};

extern crate chrono;

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  UpdateTime,
}

struct HelloWorld {
  current_time: chrono::DateTime<chrono::Local>,
}

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

  fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
    (
      HelloWorld {
        current_time: chrono::Local::now(),
      },
      Command::none(),
    )
  }

  fn title(&self) -> String {
    String::from("Basic State 1")
  }

  fn update(&mut self, message: Message) -> Command<Self::Message> {
    match message {
      Message::UpdateTime => {
        self.current_time = chrono::Local::now();
      }
    }

    Command::none()
  }

  fn view(&self) -> Element<Message> {
    // Remember .into() will wrap a built-in Widget inside of an Element.
    let timestamp = Text::new(
      self
        .current_time
        .format("%Y-%m-%d %H:%M:%S%.3f")
        .to_string(),
    )
    .into();

    let update = Button::new("Update").on_press(Message::UpdateTime).into();

    let row = Row::with_children(vec![timestamp, update])
      .spacing(20)
      .align_items(Alignment::Center);

    Container::new(row)
      .align_x(Horizontal::Center)
      .align_y(Vertical::Center)
      .width(Length::Fill)
      .height(Length::Fill)
      .into()
  }

  fn theme(&self) -> Theme {
    Theme::default()
  }
}

We create a current_time field in our Application's state. It is initialized in new(). To render our state we convert it to a string and put it inside a Text Widget. We also create a Button that can update the current time. The Message that our update button generates for on_press is specified by calling .on_press(Message::UpdateTime). Notice how the view() isolates itself from update() logic by passing any necessary context into a Message. In update(), we check the type of Message and update the current_time state if the Message is of the UpdateTime variant.

This scenario is the simplest to handle. The Button Widget does the heavy lifting for handling events, we only need to tell it what type of Message we want. Not all events are as simple to handle.

Consider how you could automatically produce an UpdateTime Message with only new, view, and update.

Subscriptions

To handle more general events we will take a look at subscription() from the Application trait.

#![allow(unused)]
fn main() {
    /// Returns the event [`Subscription`] for the current state of the
    /// application.
    ///
    /// A [`Subscription`] will be kept alive as long as you keep returning it,
    /// and the __messages__ produced will be handled by
    /// [`update`](#tymethod.update).
    ///
    /// By default, this method returns an empty [`Subscription`].
    fn subscription(&self) -> Subscription<Self::Message> {
        Subscription::none()
    }
}

Subscription() returns a Subscription. By default, it will return Subscription::none(). Subscription is called when a Message is produced or a non-empty Subscription is returned. Lets look closer at the type definition of Subscription:

#![allow(unused)]
fn main() {
/// A request to listen to external events.
///
/// Besides performing async actions on demand with [`Command`], most
/// applications also need to listen to external events passively.
///
/// A [`Subscription`] is normally provided to some runtime, like a [`Command`],
/// and it will generate events as long as the user keeps requesting it.
///
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
/// [`Command`]: crate::Command
pub type Subscription<T> =
    iced_futures::Subscription<Hasher, (Event, event::Status), T>;
}

Not super helpful -- lets dig further into the type.

#![allow(unused)]
fn main() {
/// A request to listen to external events.
///
/// Besides performing async actions on demand with [`Command`], most
/// applications also need to listen to external events passively.
///
/// A [`Subscription`] is normally provided to some runtime, like a [`Command`],
/// and it will generate events as long as the user keeps requesting it.
///
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
/// This type is normally aliased by runtimes with a specific `Event` and/or
/// `Hasher`.
///
/// [`Command`]: crate::Command
#[must_use = "`Subscription` must be returned to runtime to take effect"]
pub struct Subscription<Hasher, Event, Output> {
    recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>,
}
}

And into Recipe...

#![allow(unused)]
fn main() {
/// The description of a [`Subscription`].
///
/// A [`Recipe`] is the internal definition of a [`Subscription`]. It is used
/// by runtimes to run and identify subscriptions. You can use it to create your
/// own!
//...
pub trait Recipe<Hasher: std::hash::Hasher, Event> {
    /// The events that will be produced by a [`Subscription`] with this
    /// [`Recipe`].
    type Output;

    /// Hashes the [`Recipe`].
    ///
    /// This is used by runtimes to uniquely identify a [`Subscription`].
    fn hash(&self, state: &mut Hasher);

    /// Executes the [`Recipe`] and produces the stream of events of its
    /// [`Subscription`].
    ///
    /// It receives some stream of generic events, which is normally defined by
    /// shells.
    fn stream(
        self: Box<Self>,
        input: BoxStream<Event>,
    ) -> BoxStream<Self::Output>;
}
}

An Iced Recipe is similar to Tokio's Subscription. A Subscription is a Vec of Recipes. As long as Subscriptions are kept alive, they can keep producing values or in this case Messages. subscription() allows us to run async code to continuously provide UI state changes.

Lets incorporate subscription() into our timer to automatically update the time. We will let the button remain as a pause/unpause mechanism.

use iced::alignment::{Horizontal, Vertical};
use iced::widget::{Button, Container, Row, Text};
use iced::{executor, Alignment, Application, Command, Element, Length, Settings, Subscription, Theme};

extern crate chrono;

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  UpdateTime,
  TogglePauseTimer,
}

struct HelloWorld {
  current_time: chrono::DateTime<chrono::Local>,
  is_timer_paused: bool,
}

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

  fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
    (
      HelloWorld {
        current_time: chrono::Local::now(),
        is_timer_paused: false,
      },
      Command::none(),
    )
  }

  fn title(&self) -> String {
    String::from("Basic State 2")
  }

  fn update(&mut self, message: Message) -> Command<Self::Message> {
    match message {
      Message::UpdateTime => {
        self.current_time = chrono::Local::now();
      }
      Message::TogglePauseTimer => self.is_timer_paused = !self.is_timer_paused,
    }

    Command::none()
  }

  fn view(&self) -> Element<Message> {
    // Remember .into() will wrap a built-in Widget inside of an Element.
    let timestamp = Text::new(
      self
        .current_time
        .format("%Y-%m-%d %H:%M:%S%.3f")
        .to_string(),
    )
    .into();

    let update = Button::new("Toggle")
      .on_press(Message::TogglePauseTimer)
      .into();

    let row = Row::with_children(vec![timestamp, update])
      .spacing(20)
      .align_items(Alignment::Center);

    Container::new(row)
      .align_x(Horizontal::Center)
      .align_y(Vertical::Center)
      .width(Length::Fill)
      .height(Length::Fill)
      .into()
  }

  fn theme(&self) -> Theme {
    Theme::default()
  }

  fn subscription(&self) -> Subscription<Self::Message> {
    if self.is_timer_paused {
      return Subscription::none();
    }

    iced::time::every(std::time::Duration::from_millis(17)).map(|_| Message::UpdateTime)
  }
}

Creating custom Subscriptions will be covered in a later tutorial.

Subscriptions vs Commands

That sounds a lot like Command, what's the difference?

Subscriptions are for listening to outside events and creating messages to represent the external changes. These external events may include changes to a file, receiving network requests, etc.

Commands on the other hand are for responding to user interactions. When the user clicks a button, update() will receive a message. If a button needs to perform a task such as an HTTP request this may take a long time. If we try to keep update() simple by using a synchronous HTTP library we will freeze all other UI events until our request is completed. In short we need to run async code in response to a Message directly triggered by a user. Returning a Command from update() will let us do exactly that.

Commands

Commands are wrappers around futures. Iced can run Commands returned from new() or update().

#![allow(unused)]
fn main() {
/// A set of asynchronous actions to be performed by some runtime.
#[must_use = "`Command` must be returned to runtime to take effect"]
pub struct Command<T>(iced_futures::Command<Action<T>>);
}

We can have commands that do nothing, a single thing, or 1 or more things.

#![allow(unused)]
fn main() {
/// A set of asynchronous actions to be performed by some runtime.
#[must_use = "`Command` must be returned to runtime to take effect"]
#[derive(Debug)]
pub struct Command<T>(Internal<T>);

#[derive(Debug)]
enum Internal<T> {
    None,
    Single(T),
    Batch(Vec<T>),
}
}

To create Commands there are helper functions.

#![allow(unused)]
fn main() {
    /// Creates a [`Command`] that performs the action of the given future.
    pub fn perform<A>(
        future: impl Future<Output = T> + 'static + MaybeSend,
        f: impl FnOnce(T) -> A + 'static + MaybeSend,
    ) -> Command<A> {
        use iced_futures::futures::FutureExt;

        Command::single(Action::Future(Box::pin(future.map(f))))
    }
}

Lets look at an example creating a Command::Single with perform(). It's quite simple. We need a future/(block of async code) and a Message to send when the async code is complete. Note the return type of the async code needs to match the Message contents.

use iced::alignment::{Horizontal, Vertical};
use iced::widget::{Button, Column, Container, Text};
use iced::{executor, Alignment, Application, Command, Element, Length, Settings, Theme};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy)]
pub enum Error {
  APIError,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RandomQuoteApiResponse {
  content: String,
}

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

#[derive(Debug, Clone)]
pub enum Message {
  GetRandomQuote,
  GetRandomQuoteDone(Result<RandomQuoteApiResponse, Error>),
}

struct HelloWorld {
  quote: String,
}

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

  fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
    (
      HelloWorld {
        quote: "???".to_string(),
      },
      Command::none(),
    )
  }

  fn title(&self) -> String {
    String::from("Basic State 3")
  }

  fn update(&mut self, message: Message) -> Command<Self::Message> {
    match message {
      Message::GetRandomQuote => Command::perform(get_random_quote(), Message::GetRandomQuoteDone),
      Message::GetRandomQuoteDone(res) => {
        match res {
          Ok(random_quote_api_response) => {
            self.quote = random_quote_api_response.content;
          }
          Err(_) => {}
        }

        Command::none()
      }
    }
  }

  fn view(&self) -> Element<Message> {
    // Remember .into() will wrap a built-in Widget inside of an Element.
    let quote = Text::new(format!("{}", self.quote)).into();

    let update = Button::new("Random Quote")
      .on_press(Message::GetRandomQuote)
      .into();

    let column = Column::with_children(vec![quote, update])
      .spacing(20)
      .align_items(Alignment::Center);

    Container::new(column)
      .align_x(Horizontal::Center)
      .align_y(Vertical::Center)
      .width(Length::Fill)
      .height(Length::Fill)
      .into()
  }

  fn theme(&self) -> Theme {
    Theme::default()
  }
}

async fn get_random_quote() -> Result<RandomQuoteApiResponse, Error> {
  Ok(
    reqwest::get("https://api.quotable.io/random")
      .await?
      .json::<RandomQuoteApiResponse>()
      .await?,
  )
}

impl From<reqwest::Error> for Error {
  fn from(_error: reqwest::Error) -> Error {
    Error::APIError
  }
}

Styling

Styling changes the appearance of the Iced Application and its Widgets. Styling does not affect the layout. To style a Widget call the Widget's .style() function and pass a Stylesheet.

For this example we are focusing on a Button. It is similar for other Widgets.

#![allow(unused)]
fn main() {
/// Sets the style variant of this [`Button`].
pub fn style(
    mut self,
    style: <Renderer::Theme as StyleSheet>::Style,
) -> Self {
    self.style = style;
    self
}
}

When we try to call it:

use iced::widget::Button;
use iced::{executor, Application, Command, Element, Settings};

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  Pressed,
}

struct HelloWorld {}

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

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

  fn title(&self) -> String {
    String::from("Styling 1")
  }

  fn update(&mut self, _message: Message) -> Command<Self::Message> {
    Command::none()
  }

  fn view(&self) -> Element<Message> {
    Element::new(Button::new("Test").on_press(Message::Pressed).style(
      // ??????????
    ))
  }

  fn theme(&self) -> Self::Theme {
    Self::Theme::default()
  }
}

Now when we try to call style() our IDE will tell us we need a ::<::Theme as StyleSheet>::Style. The types are getting confusing again. Lets see if we can expand them to see what is going on.

use iced::{executor, Application, Command, Element, Settings};

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  Pressed,
}

struct HelloWorld {}

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

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

  fn title(&self) -> String {
    String::from("Styling 2")
  }

  fn update(&mut self, _message: Message) -> Command<Self::Message> {
    Command::none()
  }

  fn view(&self) -> Element<Message, iced::Renderer<Self::Theme>> {
    Element::<Message, iced::Renderer<Self::Theme>>::new(
      iced::widget::Button::<Message, iced::Renderer<Self::Theme>>::new("Test").on_press(Message::Pressed).style(
        // style: <Theme as iced::widget::button::StyleSheet>::Style
      )
    )
  }

  fn theme(&self) -> Self::Theme {
    Self::Theme::default()
  }
}

The first thing you should notice is that Button is aliased to a iced::widget::Button. We also see that Rust uses our Application's Theme type as a parameter to the Renderer. Widgets take in a Renderer type as a parameter. This seems fairly obvious when you think about it. However, Renderer requiring a Theme type seems quite strange. It seems like all we should need to do is store some style properties like color in the Button itself and let the Button give style information to the Renderer. In that scenario, Button doesn't really do much other than store the Styles. Instead, Iced delegates the task of styling Widgets to Stylesheets. Stylesheets are simply traits that have functions to return a struct with a generic set of style properties for a Widget. Lets look at the button's stylesheet(iced::widget::button::StyleSheet).

#![allow(unused)]
fn main() {
//! Change the appearance of a button.
use iced_core::{Background, Color, Vector};

/// The appearance of a button.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
    /// The amount of offset to apply to the shadow of the button.
    pub shadow_offset: Vector,
    /// The [`Background`] of the button.
    pub background: Option<Background>,
    /// The border radius of the button.
    pub border_radius: BorderRadius,
    /// The border width of the button.
    pub border_width: f32,
    /// The border [`Color`] of the button.
    pub border_color: Color,
    /// The text [`Color`] of the button.
    pub text_color: Color,
}

impl std::default::Default for Appearance {
    fn default() -> Self {
        Self {
            shadow_offset: Vector::default(),
            background: None,
            border_radius: 0.0.into(),
            border_width: 0.0,
            border_color: Color::TRANSPARENT,
            text_color: Color::BLACK,
        }
    }
}

/// A set of rules that dictate the style of a button.
pub trait StyleSheet {
    /// The supported style of the [`StyleSheet`].
    type Style: Default;

    /// Produces the active [`Appearance`] of a button.
    fn active(&self, style: &Self::Style) -> Appearance;

    /// Produces the hovered [`Appearance`] of a button.
    fn hovered(&self, style: &Self::Style) -> Appearance {
        let active = self.active(style);

        Appearance {
            shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0),
            ..active
        }
    }

    /// Produces the pressed [`Appearance`] of a button.
    fn pressed(&self, style: &Self::Style) -> Appearance {
        Appearance {
            shadow_offset: Vector::default(),
            ..self.active(style)
        }
    }

    /// Produces the disabled [`Appearance`] of a button.
    fn disabled(&self, style: &Self::Style) -> Appearance {
        let active = self.active(style);

        Appearance {
            shadow_offset: Vector::default(),
            background: active.background.map(|background| match background {
                Background::Color(color) => Background::Color(Color {
                    a: color.a * 0.5,
                    ..color
                }),
            }),
            text_color: Color {
                a: active.text_color.a * 0.5,
                ..active.text_color
            },
            ..active
        }
    }
}

}

There are functions for the different states of a Button, but they all return an Appearance struct. We also see there is an associated type: type Style: Default. This is the type that the .style() function requires. It is used to store additional information needed in the different stylesheet functions. Since it is an associated type we have to look at the actual value in iced::Theme to see what the button.style() function requires. A "theme" is simply a type with Stylsheet traits implemented on it.

#![allow(unused)]
fn main() {
/// A built-in theme.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Theme {
    /// The built-in light variant.
    #[default]
    Light,
    /// The built-in dark variant.
    Dark,
    /// A [`Theme`] that uses a [`Custom`] palette.
    Custom(Box<Custom>),
}
}
#![allow(unused)]
fn main() {
impl button::StyleSheet for Theme {
    type Style = Button;
    // ...
}
#![allow(unused)]
fn main() {
/// The style of a button.
#[derive(Default)]
pub enum Button {
    /// The primary style.
    #[default]
    Primary,
    /// The secondary style.
    Secondary,
    /// The positive style.
    Positive,
    /// The destructive style.
    Destructive,
    /// The text style.
    ///
    /// Useful for links!
    Text,
    /// A custom style.
    Custom(Box<dyn button::StyleSheet<Style = Theme>>),
}
}

So all style() really needs is a Button enum. There are some generic options in the enum or a Custom variant that simply needs a Box containing a type that implements the Button Stylesheet trait. Lets see how that is done.

use iced::widget::button::{Appearance, StyleSheet};
use iced::widget::Button;
use iced::{executor, Application, Color, Command, Element, Settings, BorderRadius};

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  Pressed,
}

struct HelloWorld {}

struct CustomButtonStylesheet {}

impl StyleSheet for CustomButtonStylesheet {
  type Style = iced::Theme;

  fn active(&self, style: &Self::Style) -> Appearance {
    Appearance {
      shadow_offset: Default::default(),
      background: None,
      border_radius: BorderRadius::from(0.0),
      border_width: 0.0,
      border_color: Default::default(),
      text_color: Color::new(1.0, 0.0, 0.0, 1.0),
    }
  }
}

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

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

  fn title(&self) -> String {
    String::from("Styling 3")
  }

  fn update(&mut self, _message: Message) -> Command<Self::Message> {
    Command::none()
  }

  fn view(&self) -> Element<Message> {
    Element::new(
      Button::new("Test")
        .on_press(Message::Pressed)
        .style(iced::theme::Button::Custom(Box::new(
          CustomButtonStylesheet {},
        ))),
    )
  }

  fn theme(&self) -> Self::Theme {
    Self::Theme::default()
  }
}

Typically you will choose a theme so that you application looks similar to other applications on your OS. Unless you are using PopOS's Cosmic you will have to either create your own theme or use the default Iced theme.

To create your own theme you simply need to:

  1. Implement required Stylesheet traits + Default on your theme type
  2. Provide your theme type to Application.
  3. Provide your theme type to view's return type.
use iced::widget::button::Appearance;
use iced::widget::{button, Button};
use iced::{executor, Application, Background, BorderRadius, Color, Command, Element, Settings};

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

#[derive(Debug, Clone, Copy)]
pub enum Message {
  Pressed,
}

struct HelloWorld {}

struct CustomTheme {}

impl iced::application::StyleSheet for CustomTheme {
  type Style = ();

  fn appearance(&self, _style: &Self::Style) -> iced::application::Appearance {
    iced::application::Appearance {
      background_color: Default::default(),
      text_color: Default::default(),
    }
  }
}

impl iced::widget::text::StyleSheet for CustomTheme {
  type Style = ();

  fn appearance(&self, _style: Self::Style) -> iced::widget::text::Appearance {
    iced::widget::text::Appearance {
      color: Some(Color::new(0.0, 0.0, 0.0, 1.0)),
    }
  }
}

impl button::StyleSheet for CustomTheme {
  type Style = ();

  fn active(&self, _style: &Self::Style) -> Appearance {
    Appearance {
      shadow_offset: Default::default(),
      background: Some(Background::Color(Color::new(0.3, 0.4, 0.94, 1.0))),
      border_radius: BorderRadius::from(0.0),
      border_width: 0.0,
      border_color: Default::default(),
      text_color: Color::new(0.0, 0.0, 0.0, 1.0),
    }
  }
}

impl Default for CustomTheme {
  fn default() -> Self {
    CustomTheme {}
  }
}

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

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

  fn title(&self) -> String {
    String::from("Styling 4")
  }

  fn update(&mut self, _message: Message) -> Command<Self::Message> {
    Command::none()
  }

  fn view(&self) -> Element<Message, iced::Renderer<Self::Theme>> {
    Element::new(Button::new("Test").on_press(Message::Pressed).style(()))
  }

  fn theme(&self) -> Self::Theme {
    Self::Theme {}
  }
}

This would work but the easier solution is to copy the default Iced theme and modify as needed.