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