From fff333f89ba99f32171641f0e8d78c9cdfe291b4 Mon Sep 17 00:00:00 2001 From: Songtronix Date: Mon, 23 Mar 2020 15:54:23 +0100 Subject: Add example for download with progress tracking --- examples/download_progress/src/downloader.rs | 99 +++++++++++++++++++++++ examples/download_progress/src/main.rs | 116 +++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 examples/download_progress/src/downloader.rs create mode 100644 examples/download_progress/src/main.rs (limited to 'examples/download_progress/src') 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() + } +} -- cgit From 30c7db3f25d12461f2dec493f92c3f3282bd264d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 23 Mar 2020 20:34:16 +0100 Subject: Improve `download_progress` example - Use `reqwest` with `Response::chunk` to notify progress. - Turn example state into an enum --- examples/download_progress/src/downloader.rs | 97 ++++++++++---------- examples/download_progress/src/main.rs | 129 ++++++++++++++------------- 2 files changed, 115 insertions(+), 111 deletions(-) (limited to 'examples/download_progress/src') 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();*/ - - async_std::task::spawn(async_std::io::copy( - resp.into_body(), - async_std::io::sink(), //file - )); + State::Ready(url) => { + let response = reqwest::get(&url).await; - Some(( - DownloadMessage::DownloadStarted, - DownloadState::Downloading(metrics), - )) + match response { + Ok(response) => Some(( + Progress::Started, + State::Downloading { + total: response.content_length().unwrap(), + downloaded: 0, + response, + }, + )), + Err(_) => None, + } } - DownloadState::Downloading(metrics) => { - async_std::task::sleep( - std::time::Duration::from_millis(100), - ) - .await; + State::Downloading { + mut response, + total, + downloaded, + } => match response.chunk().await { + Ok(Some(chunk)) => { + let downloaded = downloaded + chunk.len() as u64; - let percentage = metrics.download_progress().0 * 100 - / metrics.download_progress().1; + let percentage = + (downloaded as f32 / total as f32) * 100.0; - if percentage == 100 { Some(( - DownloadMessage::Done, - DownloadState::Finished, - )) - } else { - Some(( - DownloadMessage::Downloading(percentage), - DownloadState::Downloading(metrics), + Progress::Advanced(percentage), + State::Downloading { + response, + total, + downloaded, + }, )) } - } - DownloadState::Finished => None, + Ok(None) => Some((Progress::Finished, State::Finished)), + Err(_) => None, + }, + State::Finished => None, } }, )) } } -#[derive(Debug)] -pub enum DownloadMessage { - DownloadStarted, - Downloading(u64), - Done, +#[derive(Debug, Clone)] +pub enum Progress { + Started, + Advanced(f32), + Finished, } -pub enum DownloadState { +pub enum State { Ready(String), - Downloading(isahc::Metrics), + 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, self.current_progress as f32); - - let progress_text = &match self.enabled { - true => format!("Downloading {}%", self.current_progress), - false => "Ready to rock!".into(), + let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); + + 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) -- cgit From b92e1f957408e3254e5fe0da389808474de6c4a9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 23 Mar 2020 20:37:30 +0100 Subject: Rename `downloader` module to `download` --- examples/download_progress/src/download.rs | 94 ++++++++++++++++++++++++++++ examples/download_progress/src/downloader.rs | 94 ---------------------------- examples/download_progress/src/main.rs | 12 ++-- 3 files changed, 100 insertions(+), 100 deletions(-) create mode 100644 examples/download_progress/src/download.rs delete mode 100644 examples/download_progress/src/downloader.rs (limited to 'examples/download_progress/src') diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs new file mode 100644 index 00000000..0562f54d --- /dev/null +++ b/examples/download_progress/src/download.rs @@ -0,0 +1,94 @@ +use iced_futures::futures; + +// Just a little utility function +pub fn file(url: T) -> iced::Subscription { + iced::Subscription::from_recipe(Download { + url: url.to_string(), + }) +} + +pub struct Download { + url: String, +} + +// Make sure iced can use our download stream +impl iced_native::subscription::Recipe for Download +where + H: std::hash::Hasher, +{ + type Output = Progress; + + 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> { + Box::pin(futures::stream::unfold( + State::Ready(self.url), + |state| async move { + match state { + State::Ready(url) => { + 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, + } + } + 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, 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/downloader.rs b/examples/download_progress/src/downloader.rs deleted file mode 100644 index 3b54341e..00000000 --- a/examples/download_progress/src/downloader.rs +++ /dev/null @@ -1,94 +0,0 @@ -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 = Progress; - - 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> { - Box::pin(futures::stream::unfold( - State::Ready(self.url), - |state| async move { - match state { - State::Ready(url) => { - 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, - } - } - 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, 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 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(), -- cgit From 0d719bbdf336a022c073986e1e5a91cf632a270c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 23 Mar 2020 20:43:55 +0100 Subject: 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(-) (limited to 'examples/download_progress/src') 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() -- cgit From 8e0dcd212d71ff334aa590ee3b565da7b8d24713 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 23 Mar 2020 21:08:03 +0100 Subject: Fix retry button on `download_progress` example --- examples/download_progress/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'examples/download_progress/src') 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 }; } _ => {} -- cgit From fd7d9622e333a0a2cd5c2e8e6cc38cc09d7981e4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 24 Mar 2020 14:07:34 +0100 Subject: Fix `Recipe::hash` in `download_progress` example --- examples/download_progress/src/download.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'examples/download_progress/src') diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index 96e1dc28..f46a01f7 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -20,7 +20,9 @@ where fn hash(&self, state: &mut H) { use std::hash::Hash; + std::any::TypeId::of::().hash(state); + self.url.hash(state); } fn stream( -- cgit