Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OS-Native Dialogs

To create native-looking dialogs, you can use the Rusty File Dialog crate.

Opening Files

Scenario

Imagine that you're creating a simple image viewer, and you want to load an image you've recently downloaded.

You have a simple prototype, but not sure what to do at open_image:

use iced::widget::image;

enum Error {
    InvalidImage,
}

pub enum Message {
    /// Open the file dialog.
    OpenImage,

    /// An image has been loaded.
    ImageLoaded(Result<image::Handle, Error>),

    /// The user changed their mind.
    FileCancelled
}

// Your app's update function.
pub fn update(&mut self, message: Message) -> Task<Message> {
    match self {
        Message::OpenImage => open_image(),

        /* Additional code omitted for simplicity */
        _ => Task::none()
    }
}

pub fn open_image() -> Task<Message> {
    todo!()
}

Solution

Use rfd::AsyncFileDialog's pick_file in combination with iced's Task.

use iced::Alignment::Center;
use iced::widget::{button, center, column, image};
use iced::{Element, Task};

fn main() -> iced::Result {
    iced::application("File Dialog Example", App::update, App::view).run()
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Error {
    InvalidImage
}

#[derive(Debug, Clone)]
enum Message {
    OpenImage,
    ImageLoaded(Result<image::Handle, Error>),
    FileCancelled,
}

#[derive(Default)]
struct App {
    loaded_image: Option<image::Handle>,
}

impl App {
    pub fn view(&self) -> Element<'_, Message> {
        center(
            column![button("Open Image").on_press(Message::OpenImage)]
                .push_maybe(self.loaded_image.as_ref().map(image))
                .align_x(Center),
        )
        .into()
    }

    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::OpenImage => open_image(),
            Message::ImageLoaded(result) => match result {
                Ok(handle) => {
                    self.loaded_image = Some(handle);
                    Task::none()
                }
                Err(_) => Task::none(),
            },
            Message::FileCancelled => Task::none(),
        }
    }
}

fn open_image() -> Task<Message> {
    Task::future(
        rfd::AsyncFileDialog::new()
            .add_filter( // <-- (OPTIONAL) Add a filter to only allow PNG and JPEG formats.
                "Image Formats",
                &["png", "jpg", "jpeg"], 
            )
            .pick_file() // <-- Launch the dialog window.
    )
    .then(|handle| match handle {
        // After obtaining a file handle from the dialog, we load the image.
        //
        // We use Task::perform to run load_image, as this may take a while to load.
        Some(file_handle) => {
            Task::perform(
                load_image(file_handle), 
                Message::ImageLoaded
            )
        },

        // The user has cancelled the operation, so we return a "Cancelled" message.
        None => Task::done(Message::FileCancelled)
    })
}

/// Simplified code to load an image. 
/// 
/// In practice, you may explore other options,
/// but this goes beyond the scope of this tutorial.
async fn load_image(handle: rfd::FileHandle) -> Result<image::Handle, Error> {
    Ok(image::Handle::from_path(handle.path()))
}

When you pick a file/folder, it will return an Option<Filehandle>, as the user might change their mind.

We leverege the Task's monadic api to make this implementation succinct and clean.

Complete Example

main.rs

use iced::Alignment::Center;
use iced::widget::{button, center, column, image};
use iced::{Element, Task};

fn main() -> iced::Result {
    iced::application("File Dialog Example", App::update, App::view).run()
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Error {
    InvalidImage
}

#[derive(Debug, Clone)]
enum Message {
    OpenImage,
    ImageLoaded(Result<image::Handle, Error>),
    FileCancelled,
}

#[derive(Default)]
struct App {
    loaded_image: Option<image::Handle>,
}

impl App {
    pub fn view(&self) -> Element<'_, Message> {
        center(
            column![button("Open Image").on_press(Message::OpenImage)]
                .push_maybe(self.loaded_image.as_ref().map(image))
                .align_x(Center),
        )
        .into()
    }

    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::OpenImage => open_image(),
            Message::ImageLoaded(result) => match result {
                Ok(handle) => {
                    self.loaded_image = Some(handle);
                    Task::none()
                }
                Err(_) => Task::none(),
            },
            Message::FileCancelled => Task::none(),
        }
    }
}

fn open_image() -> Task<Message> {
    Task::future(
        rfd::AsyncFileDialog::new()
            .add_filter( // <-- (OPTIONAL) Add a filter to only allow PNG and JPEG formats.
                "Image Formats",
                &["png", "jpg", "jpeg"], 
            )
            .pick_file() // <-- Launch the dialog window.
    )
    .then(|handle| match handle {
        // After obtaining a file handle from the dialog, we load the image.
        //
        // We use Task::perform to run load_image, as this may take a while to load.
        Some(file_handle) => {
            Task::perform(
                load_image(file_handle), 
                Message::ImageLoaded
            )
        },

        // The user has cancelled the operation, so we return a "Cancelled" message.
        None => Task::done(Message::FileCancelled)
    })
}

/// Simplified code to load an image. 
/// 
/// In practice, you may explore other options,
/// but this goes beyond the scope of this tutorial.
async fn load_image(handle: rfd::FileHandle) -> Result<image::Handle, Error> {
    Ok(image::Handle::from_path(handle.path()))
}

Saving Files

To open a save-file dialog, use rfd::AsyncFileDialog's save_file

use std::path::PathBuf;

use iced::widget::{button, center};
use iced::{Element, Task};

fn main() -> iced::Result {
    iced::application("Save File Dialog Example", App::update, App::view).run()
}

#[derive(Debug, Clone)]
pub enum Message {
    Save,
    Exported(Result<PathBuf, Error>),
    ExportCancelled,
}

#[derive(Debug, Default)]
struct App;

impl App {
    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Save => return export_build_info(),
            Message::Exported(Ok(destination)) => {
                let _ = open::that(destination);
            }
            Message::Exported(Err(_error)) => (),
            Message::ExportCancelled => (),
        }
        Task::none()
    }

    pub fn view(&self) -> Element<'_, Message> {
        center(button("Export Build Information").on_press(Message::Save)).into()
    }
}

pub fn export_build_info() -> Task<Message> {
    Task::future(
        rfd::AsyncFileDialog::new()
            .set_file_name("build info.txt")
            .save_file(),
    )
    .then(|handle| match handle {
        Some(handle) => Task::perform(
            save_build_info(handle), 
            Message::Exported
        ),
        None => Task::done(Message::ExportCancelled),
    })
}

#[derive(Debug, Clone)]
pub enum Error {
    Io { reason: String },
}

pub async fn save_build_info(handle: rfd::FileHandle) -> Result<PathBuf, Error> {
    static BUILD_INFO: &str = "Build Date: 07/07/2025\nVersion: 1.2.3";

    handle
        .write(BUILD_INFO.as_bytes())
        .await
        .map(|_| handle.path().to_path_buf())
        .map_err(|error| Error::Io {
            reason: error.to_string(),
        })
}

Similar to pick_file, save_file will return an an Option<Filehandle>,

Complete Example

main.rs

use std::path::PathBuf;

use iced::widget::{button, center};
use iced::{Element, Task};

fn main() -> iced::Result {
    iced::application("Save File Dialog Example", App::update, App::view).run()
}

#[derive(Debug, Clone)]
pub enum Message {
    Save,
    Exported(Result<PathBuf, Error>),
    ExportCancelled,
}

#[derive(Debug, Default)]
struct App;

impl App {
    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Save => return export_build_info(),
            Message::Exported(Ok(destination)) => {
                let _ = open::that(destination);
            }
            Message::Exported(Err(_error)) => (),
            Message::ExportCancelled => (),
        }
        Task::none()
    }

    pub fn view(&self) -> Element<'_, Message> {
        center(button("Export Build Information").on_press(Message::Save)).into()
    }
}

pub fn export_build_info() -> Task<Message> {
    Task::future(
        rfd::AsyncFileDialog::new()
            .set_file_name("build info.txt")
            .save_file(),
    )
    .then(|handle| match handle {
        Some(handle) => Task::perform(
            save_build_info(handle), 
            Message::Exported
        ),
        None => Task::done(Message::ExportCancelled),
    })
}

#[derive(Debug, Clone)]
pub enum Error {
    Io { reason: String },
}

pub async fn save_build_info(handle: rfd::FileHandle) -> Result<PathBuf, Error> {
    static BUILD_INFO: &str = "Build Date: 07/07/2025\nVersion: 1.2.3";

    handle
        .write(BUILD_INFO.as_bytes())
        .await
        .map(|_| handle.path().to_path_buf())
        .map_err(|error| Error::Io {
            reason: error.to_string(),
        })
}