From fff333f89ba99f32171641f0e8d78c9cdfe291b4 Mon Sep 17 00:00:00 2001 From: Songtronix Date: Mon, 23 Mar 2020 15:54:23 +0100 Subject: [PATCH 1/6] Add example for download with progress tracking --- Cargo.toml | 1 + examples/download_progress/Cargo.toml | 13 +++ examples/download_progress/README.md | 15 +++ examples/download_progress/src/downloader.rs | 99 ++++++++++++++++ examples/download_progress/src/main.rs | 116 +++++++++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 examples/download_progress/Cargo.toml create mode 100644 examples/download_progress/README.md create mode 100644 examples/download_progress/src/downloader.rs create mode 100644 examples/download_progress/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 12b75aed..d2444486 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "web", "wgpu", "winit", + "examples/download_progress", "examples/bezier_tool", "examples/clock", "examples/counter", diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml new file mode 100644 index 00000000..ce0435fd --- /dev/null +++ b/examples/download_progress/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "download_progress" +version = "0.1.0" +authors = ["Songtronix "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_futures = { path = "../../futures" } +async-std = { version = "1.0", features = ["unstable"] } +isahc = "0.9.1" diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md new file mode 100644 index 00000000..c6311163 --- /dev/null +++ b/examples/download_progress/README.md @@ -0,0 +1,15 @@ +## Download Progress + +Downloading a file asynchronously with a `Subscription` while displaying the progress with a `ProgressBar`. + +
+ + + +
+ +You can run it with `cargo run`: + +``` +cargo run --package download_progress +``` diff --git a/examples/download_progress/src/downloader.rs b/examples/download_progress/src/downloader.rs new file mode 100644 index 00000000..62f943fd --- /dev/null +++ b/examples/download_progress/src/downloader.rs @@ -0,0 +1,99 @@ +use iced_futures::futures; + +// Just a little utility function +pub fn file(url: T) -> iced::Subscription { + iced::Subscription::from_recipe(Downloader { + url: url.to_string(), + }) +} + +pub struct Downloader { + url: String, +} + +// Make sure iced can use our download stream +impl iced_native::subscription::Recipe for Downloader +where + H: std::hash::Hasher, +{ + type Output = DownloadMessage; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + std::any::TypeId::of::().hash(state); + } + + fn stream( + self: Box, + _input: futures::stream::BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use isahc::prelude::*; + + Box::pin(futures::stream::unfold( + DownloadState::Ready(self.url), + |state| async move { + match state { + DownloadState::Ready(url) => { + let resp = Request::get(&url) + .metrics(true) + .body(()) + .unwrap() + .send_async() + .await + .unwrap(); + let metrics = resp.metrics().unwrap().clone(); + // If you actually want to download: + /*let file = async_std::fs::File::create("download.bin") + .await + .unwrap();*/ + + async_std::task::spawn(async_std::io::copy( + resp.into_body(), + async_std::io::sink(), //file + )); + + Some(( + DownloadMessage::DownloadStarted, + DownloadState::Downloading(metrics), + )) + } + DownloadState::Downloading(metrics) => { + async_std::task::sleep( + std::time::Duration::from_millis(100), + ) + .await; + + let percentage = metrics.download_progress().0 * 100 + / metrics.download_progress().1; + + if percentage == 100 { + Some(( + DownloadMessage::Done, + DownloadState::Finished, + )) + } else { + Some(( + DownloadMessage::Downloading(percentage), + DownloadState::Downloading(metrics), + )) + } + } + DownloadState::Finished => None, + } + }, + )) + } +} + +#[derive(Debug)] +pub enum DownloadMessage { + DownloadStarted, + Downloading(u64), + Done, +} + +pub enum DownloadState { + Ready(String), + Downloading(isahc::Metrics), + Finished, +} diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs new file mode 100644 index 00000000..936144d5 --- /dev/null +++ b/examples/download_progress/src/main.rs @@ -0,0 +1,116 @@ +use iced::{ + button, executor, Align, Application, Button, Column, Command, Container, + Element, Length, ProgressBar, Settings, Subscription, Text, +}; + +mod downloader; + +pub fn main() { + Downloader::run(Settings::default()) +} + +#[derive(Debug, Default)] +struct Downloader { + // Whether to start the download or not. + enabled: bool, + // The current percentage of the download + current_progress: u64, + + btn_state: button::State, +} + +#[derive(Debug)] +pub enum Message { + DownloadUpdate(downloader::DownloadMessage), + Interaction(Interaction), +} + +// For explanation of why we use an Interaction enum see here: +// https://github.com/hecrj/iced/pull/155#issuecomment-573523405 +#[derive(Debug, Clone)] +pub enum Interaction { + // User pressed the button to start the download + StartDownload, +} + +impl Application for Downloader { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Downloader, Command) { + (Downloader::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Download Progress - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Interaction(action) => match action { + Interaction::StartDownload => { + self.enabled = true; + } + }, + Message::DownloadUpdate(update) => match update { + downloader::DownloadMessage::Downloading(percentage) => { + self.current_progress = percentage; + } + downloader::DownloadMessage::Done => { + self.current_progress = 100; + self.enabled = false; + } + _ => {} + }, + }; + + Command::none() + } + + fn subscription(&self) -> Subscription { + if self.enabled { + downloader::file("https://speed.hetzner.de/100MB.bin") + .map(Message::DownloadUpdate) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + // Construct widgets + + let toggle_text = match self.enabled { + true => "Downloading...", + false => "Start the download!", + }; + + let toggle: Element = + Button::new(&mut self.btn_state, Text::new(toggle_text)) + .on_press(Interaction::StartDownload) + .into(); + + let progress_bar = + ProgressBar::new(0.0..=100.0, self.current_progress as f32); + + let progress_text = &match self.enabled { + true => format!("Downloading {}%", self.current_progress), + false => "Ready to rock!".into(), + }; + + // Construct layout + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .padding(20) + .push(Text::new(progress_text)) + .push(progress_bar) + .push(toggle.map(Message::Interaction)); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} From 30c7db3f25d12461f2dec493f92c3f3282bd264d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 23 Mar 2020 20:34:16 +0100 Subject: [PATCH 2/6] Improve `download_progress` example - Use `reqwest` with `Response::chunk` to notify progress. - Turn example state into an enum --- examples/download_progress/Cargo.toml | 5 +- examples/download_progress/src/downloader.rs | 111 ++++++++-------- examples/download_progress/src/main.rs | 127 ++++++++++--------- 3 files changed, 123 insertions(+), 120 deletions(-) diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index ce0435fd..34e6a132 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -6,8 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["tokio"] } iced_native = { path = "../../native" } iced_futures = { path = "../../futures" } -async-std = { version = "1.0", features = ["unstable"] } -isahc = "0.9.1" +reqwest = "0.10" diff --git a/examples/download_progress/src/downloader.rs b/examples/download_progress/src/downloader.rs index 62f943fd..3b54341e 100644 --- a/examples/download_progress/src/downloader.rs +++ b/examples/download_progress/src/downloader.rs @@ -1,7 +1,7 @@ use iced_futures::futures; // Just a little utility function -pub fn file(url: T) -> iced::Subscription { +pub fn file(url: T) -> iced::Subscription { iced::Subscription::from_recipe(Downloader { url: url.to_string(), }) @@ -16,7 +16,7 @@ impl iced_native::subscription::Recipe for Downloader where H: std::hash::Hasher, { - type Output = DownloadMessage; + type Output = Progress; fn hash(&self, state: &mut H) { use std::hash::Hash; @@ -27,73 +27,68 @@ where self: Box, _input: futures::stream::BoxStream<'static, I>, ) -> futures::stream::BoxStream<'static, Self::Output> { - use isahc::prelude::*; - Box::pin(futures::stream::unfold( - DownloadState::Ready(self.url), + State::Ready(self.url), |state| async move { match state { - DownloadState::Ready(url) => { - let resp = Request::get(&url) - .metrics(true) - .body(()) - .unwrap() - .send_async() - .await - .unwrap(); - let metrics = resp.metrics().unwrap().clone(); - // If you actually want to download: - /*let file = async_std::fs::File::create("download.bin") - .await - .unwrap();*/ + State::Ready(url) => { + let response = reqwest::get(&url).await; - async_std::task::spawn(async_std::io::copy( - resp.into_body(), - async_std::io::sink(), //file - )); - - Some(( - DownloadMessage::DownloadStarted, - DownloadState::Downloading(metrics), - )) - } - DownloadState::Downloading(metrics) => { - async_std::task::sleep( - std::time::Duration::from_millis(100), - ) - .await; - - let percentage = metrics.download_progress().0 * 100 - / metrics.download_progress().1; - - if percentage == 100 { - Some(( - DownloadMessage::Done, - DownloadState::Finished, - )) - } else { - Some(( - DownloadMessage::Downloading(percentage), - DownloadState::Downloading(metrics), - )) + match response { + Ok(response) => Some(( + Progress::Started, + State::Downloading { + total: response.content_length().unwrap(), + downloaded: 0, + response, + }, + )), + Err(_) => None, } } - DownloadState::Finished => None, + State::Downloading { + mut response, + total, + downloaded, + } => match response.chunk().await { + Ok(Some(chunk)) => { + let downloaded = downloaded + chunk.len() as u64; + + let percentage = + (downloaded as f32 / total as f32) * 100.0; + + Some(( + Progress::Advanced(percentage), + State::Downloading { + response, + total, + downloaded, + }, + )) + } + Ok(None) => Some((Progress::Finished, State::Finished)), + Err(_) => None, + }, + State::Finished => None, } }, )) } } -#[derive(Debug)] -pub enum DownloadMessage { - DownloadStarted, - Downloading(u64), - Done, -} - -pub enum DownloadState { - Ready(String), - Downloading(isahc::Metrics), +#[derive(Debug, Clone)] +pub enum Progress { + Started, + Advanced(f32), + Finished, +} + +pub enum State { + Ready(String), + Downloading { + response: reqwest::Response, + total: u64, + downloaded: u64, + }, Finished, } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 936144d5..75e3bee0 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -6,60 +6,61 @@ use iced::{ mod downloader; pub fn main() { - Downloader::run(Settings::default()) -} - -#[derive(Debug, Default)] -struct Downloader { - // Whether to start the download or not. - enabled: bool, - // The current percentage of the download - current_progress: u64, - - btn_state: button::State, + Example::run(Settings::default()) } #[derive(Debug)] -pub enum Message { - DownloadUpdate(downloader::DownloadMessage), - Interaction(Interaction), +enum Example { + Idle { button: button::State }, + Downloading { progress: f32 }, + Finished { button: button::State }, } -// For explanation of why we use an Interaction enum see here: -// https://github.com/hecrj/iced/pull/155#issuecomment-573523405 #[derive(Debug, Clone)] -pub enum Interaction { - // User pressed the button to start the download - StartDownload, +pub enum Message { + DownloadProgressed(downloader::Progress), + Download, } -impl Application for Downloader { +impl Application for Example { type Executor = executor::Default; type Message = Message; - fn new() -> (Downloader, Command) { - (Downloader::default(), Command::none()) + fn new() -> (Example, Command) { + ( + Example::Idle { + button: button::State::new(), + }, + Command::none(), + ) } fn title(&self) -> String { - String::from("Download Progress - Iced") + String::from("Download progress - Iced") } fn update(&mut self, message: Message) -> Command { match message { - Message::Interaction(action) => match action { - Interaction::StartDownload => { - self.enabled = true; + Message::Download => match self { + Example::Idle { .. } | Example::Finished { .. } => { + *self = Example::Downloading { progress: 0.0 }; } + _ => {} }, - Message::DownloadUpdate(update) => match update { - downloader::DownloadMessage::Downloading(percentage) => { - self.current_progress = percentage; - } - downloader::DownloadMessage::Done => { - self.current_progress = 100; - self.enabled = false; - } + Message::DownloadProgressed(message) => match self { + Example::Downloading { progress } => match message { + downloader::Progress::Started => { + *progress = 0.0; + } + downloader::Progress::Advanced(percentage) => { + *progress = percentage; + } + downloader::Progress::Finished => { + *self = Example::Finished { + button: button::State::new(), + } + } + }, _ => {} }, }; @@ -68,43 +69,51 @@ impl Application for Downloader { } fn subscription(&self) -> Subscription { - if self.enabled { - downloader::file("https://speed.hetzner.de/100MB.bin") - .map(Message::DownloadUpdate) - } else { - Subscription::none() + match self { + Example::Downloading { .. } => { + downloader::file("https://speed.hetzner.de/100MB.bin") + .map(Message::DownloadProgressed) + } + _ => Subscription::none(), } } fn view(&mut self) -> Element { - // Construct widgets - - let toggle_text = match self.enabled { - true => "Downloading...", - false => "Start the download!", + let current_progress = match self { + Example::Idle { .. } => 0.0, + Example::Downloading { progress } => *progress, + Example::Finished { .. } => 100.0, }; - let toggle: Element = - Button::new(&mut self.btn_state, Text::new(toggle_text)) - .on_press(Interaction::StartDownload) - .into(); + let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); - let progress_bar = - ProgressBar::new(0.0..=100.0, self.current_progress as f32); - - let progress_text = &match self.enabled { - true => format!("Downloading {}%", self.current_progress), - false => "Ready to rock!".into(), + let control: Element<_> = match self { + Example::Idle { button } => { + Button::new(button, Text::new("Start the download!")) + .on_press(Message::Download) + .into() + } + Example::Finished { button } => Column::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new("Download finished!")) + .push( + Button::new(button, Text::new("Start again")) + .on_press(Message::Download), + ) + .into(), + Example::Downloading { .. } => { + Text::new(format!("Downloading... {:.2}%", current_progress)) + .into() + } }; - // Construct layout let content = Column::new() + .spacing(10) + .padding(10) .align_items(Align::Center) - .spacing(20) - .padding(20) - .push(Text::new(progress_text)) .push(progress_bar) - .push(toggle.map(Message::Interaction)); + .push(control); Container::new(content) .width(Length::Fill) From b92e1f957408e3254e5fe0da389808474de6c4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 23 Mar 2020 20:37:30 +0100 Subject: [PATCH 3/6] Rename `downloader` module to `download` --- .../src/{downloader.rs => download.rs} | 6 +++--- examples/download_progress/src/main.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) rename examples/download_progress/src/{downloader.rs => download.rs} (95%) diff --git a/examples/download_progress/src/downloader.rs b/examples/download_progress/src/download.rs similarity index 95% rename from examples/download_progress/src/downloader.rs rename to examples/download_progress/src/download.rs index 3b54341e..0562f54d 100644 --- a/examples/download_progress/src/downloader.rs +++ b/examples/download_progress/src/download.rs @@ -2,17 +2,17 @@ use iced_futures::futures; // Just a little utility function pub fn file(url: T) -> iced::Subscription { - iced::Subscription::from_recipe(Downloader { + iced::Subscription::from_recipe(Download { url: url.to_string(), }) } -pub struct Downloader { +pub struct Download { url: String, } // Make sure iced can use our download stream -impl iced_native::subscription::Recipe for Downloader +impl iced_native::subscription::Recipe for Download where H: std::hash::Hasher, { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 75e3bee0..f3da3d7b 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -3,7 +3,7 @@ use iced::{ Element, Length, ProgressBar, Settings, Subscription, Text, }; -mod downloader; +mod download; pub fn main() { Example::run(Settings::default()) @@ -18,7 +18,7 @@ enum Example { #[derive(Debug, Clone)] pub enum Message { - DownloadProgressed(downloader::Progress), + DownloadProgressed(download::Progress), Download, } @@ -49,13 +49,13 @@ impl Application for Example { }, Message::DownloadProgressed(message) => match self { Example::Downloading { progress } => match message { - downloader::Progress::Started => { + download::Progress::Started => { *progress = 0.0; } - downloader::Progress::Advanced(percentage) => { + download::Progress::Advanced(percentage) => { *progress = percentage; } - downloader::Progress::Finished => { + download::Progress::Finished => { *self = Example::Finished { button: button::State::new(), } @@ -71,7 +71,7 @@ impl Application for Example { fn subscription(&self) -> Subscription { match self { Example::Downloading { .. } => { - downloader::file("https://speed.hetzner.de/100MB.bin") + download::file("https://speed.hetzner.de/100MB.bin") .map(Message::DownloadProgressed) } _ => Subscription::none(), From 0d719bbdf336a022c073986e1e5a91cf632a270c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 23 Mar 2020 20:43:55 +0100 Subject: [PATCH 4/6] Handle errors in `download_progress` example --- examples/download_progress/src/download.rs | 38 +++++++++++++++------- examples/download_progress/src/main.rs | 16 +++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index 0562f54d..96e1dc28 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -35,15 +35,23 @@ where let response = reqwest::get(&url).await; match response { - Ok(response) => Some(( - Progress::Started, - State::Downloading { - total: response.content_length().unwrap(), - downloaded: 0, - response, - }, - )), - Err(_) => None, + Ok(response) => { + if let Some(total) = response.content_length() { + Some(( + Progress::Started, + State::Downloading { + response, + total, + downloaded: 0, + }, + )) + } else { + Some((Progress::Errored, State::Finished)) + } + } + Err(_) => { + Some((Progress::Errored, State::Finished)) + } } } State::Downloading { @@ -67,9 +75,16 @@ where )) } Ok(None) => Some((Progress::Finished, State::Finished)), - Err(_) => None, + Err(_) => Some((Progress::Errored, State::Finished)), }, - State::Finished => None, + State::Finished => { + // We do not let the stream die, as it would start a + // new download repeatedly if the user is not careful + // in case of errors. + let _: () = iced::futures::future::pending().await; + + None + } } }, )) @@ -81,6 +96,7 @@ pub enum Progress { Started, Advanced(f32), Finished, + Errored, } pub enum State { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index f3da3d7b..817a45ac 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -14,6 +14,7 @@ enum Example { Idle { button: button::State }, Downloading { progress: f32 }, Finished { button: button::State }, + Errored { button: button::State }, } #[derive(Debug, Clone)] @@ -60,6 +61,11 @@ impl Application for Example { button: button::State::new(), } } + download::Progress::Errored => { + *self = Example::Errored { + button: button::State::new(), + }; + } }, _ => {} }, @@ -83,6 +89,7 @@ impl Application for Example { Example::Idle { .. } => 0.0, Example::Downloading { progress } => *progress, Example::Finished { .. } => 100.0, + Example::Errored { .. } => 0.0, }; let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); @@ -106,6 +113,15 @@ impl Application for Example { Text::new(format!("Downloading... {:.2}%", current_progress)) .into() } + Example::Errored { button } => Column::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new("Something went wrong :(")) + .push( + Button::new(button, Text::new("Try again")) + .on_press(Message::Download), + ) + .into(), }; let content = Column::new() From 8e073d10d71f80077b843c49013a8bdfeb5a7f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 23 Mar 2020 21:04:20 +0100 Subject: [PATCH 5/6] Update `README` of examples --- examples/README.md | 1 + examples/download_progress/README.md | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index a7673705..5aea51eb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -73,6 +73,7 @@ A bunch of simpler examples exist: - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). - [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. +- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. - [`events`](events), a log of native events displayed using a conditional `Subscription`. - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). - [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application. diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md index c6311163..c606c5f9 100644 --- a/examples/download_progress/README.md +++ b/examples/download_progress/README.md @@ -1,10 +1,12 @@ -## Download Progress +## Download progress -Downloading a file asynchronously with a `Subscription` while displaying the progress with a `ProgressBar`. +A basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. + +The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress. From 8e0dcd212d71ff334aa590ee3b565da7b8d24713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 23 Mar 2020 21:08:03 +0100 Subject: [PATCH 6/6] Fix retry button on `download_progress` example --- examples/download_progress/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 817a45ac..6c3094f7 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -19,8 +19,8 @@ enum Example { #[derive(Debug, Clone)] pub enum Message { - DownloadProgressed(download::Progress), Download, + DownloadProgressed(download::Progress), } impl Application for Example { @@ -43,7 +43,9 @@ impl Application for Example { fn update(&mut self, message: Message) -> Command { match message { Message::Download => match self { - Example::Idle { .. } | Example::Finished { .. } => { + Example::Idle { .. } + | Example::Finished { .. } + | Example::Errored { .. } => { *self = Example::Downloading { progress: 0.0 }; } _ => {}