summaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/bezier_tool/src/main.rs2
-rw-r--r--examples/changelog/src/main.rs26
-rw-r--r--examples/download_progress/src/download.rs23
-rw-r--r--examples/download_progress/src/main.rs54
-rw-r--r--examples/gallery/Cargo.toml3
-rw-r--r--examples/gallery/src/civitai.rs119
-rw-r--r--examples/gallery/src/main.rs190
-rw-r--r--examples/game_of_life/src/main.rs18
-rw-r--r--examples/loading_spinners/src/circular.rs4
-rw-r--r--examples/loading_spinners/src/linear.rs4
-rw-r--r--examples/markdown/Cargo.toml13
-rw-r--r--examples/markdown/build.rs5
-rw-r--r--examples/markdown/fonts/markdown-icons.toml4
-rw-r--r--examples/markdown/fonts/markdown-icons.ttfbin0 -> 5856 bytes
-rw-r--r--examples/markdown/src/icon.rs15
-rw-r--r--examples/markdown/src/main.rs291
-rw-r--r--examples/multi_window/src/main.rs8
-rw-r--r--examples/multitouch/src/main.rs4
-rw-r--r--examples/scrollable/src/main.rs12
-rw-r--r--examples/sierpinski_triangle/src/main.rs2
-rw-r--r--examples/system_information/src/main.rs18
-rw-r--r--examples/toast/src/main.rs6
-rw-r--r--examples/todos/src/main.rs9
-rw-r--r--examples/tour/src/main.rs14
-rw-r--r--examples/websocket/src/echo.rs96
25 files changed, 659 insertions, 281 deletions
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index 4d438bd9..95ad299d 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -93,7 +93,7 @@ mod bezier {
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Curve>> {
diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs
index f889e757..a1d0d799 100644
--- a/examples/changelog/src/main.rs
+++ b/examples/changelog/src/main.rs
@@ -267,25 +267,21 @@ impl Generator {
} => {
let details = {
let title = rich_text![
- span(&pull_request.title).size(24).link(
- Message::OpenPullRequest(pull_request.id)
- ),
+ span(&pull_request.title)
+ .size(24)
+ .link(pull_request.id),
span(format!(" by {}", pull_request.author))
.font(Font {
style: font::Style::Italic,
..Font::default()
}),
]
+ .on_link_click(Message::OpenPullRequest)
.font(Font::MONOSPACE);
- let description = markdown::view(
- description,
- markdown::Settings::default(),
- markdown::Style::from_palette(
- self.theme().palette(),
- ),
- )
- .map(Message::UrlClicked);
+ let description =
+ markdown(description, self.theme())
+ .map(Message::UrlClicked);
let labels =
row(pull_request.labels.iter().map(|label| {
@@ -348,11 +344,11 @@ impl Generator {
} else {
container(
scrollable(
- markdown::view(
+ markdown(
preview,
- markdown::Settings::with_text_size(12),
- markdown::Style::from_palette(
- self.theme().palette(),
+ markdown::Settings::with_text_size(
+ 12,
+ self.theme(),
),
)
.map(Message::UrlClicked),
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index d63fb906..5b81f7a2 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,16 +1,14 @@
-use iced::futures::{SinkExt, Stream, StreamExt};
-use iced::stream::try_channel;
+use iced::futures::StreamExt;
+use iced::task::{sipper, Straw};
use std::sync::Arc;
-pub fn download(
- url: impl AsRef<str>,
-) -> impl Stream<Item = Result<Progress, Error>> {
- try_channel(1, move |mut output| async move {
+pub fn download(url: impl AsRef<str>) -> impl Straw<(), Progress, Error> {
+ sipper(move |mut progress| async move {
let response = reqwest::get(url.as_ref()).await?;
let total = response.content_length().ok_or(Error::NoContentLength)?;
- let _ = output.send(Progress::Downloading { percent: 0.0 }).await;
+ let _ = progress.send(Progress { percent: 0.0 }).await;
let mut byte_stream = response.bytes_stream();
let mut downloaded = 0;
@@ -19,23 +17,20 @@ pub fn download(
let bytes = next_bytes?;
downloaded += bytes.len();
- let _ = output
- .send(Progress::Downloading {
+ let _ = progress
+ .send(Progress {
percent: 100.0 * downloaded as f32 / total as f32,
})
.await;
}
- let _ = output.send(Progress::Finished).await;
-
Ok(())
})
}
#[derive(Debug, Clone)]
-pub enum Progress {
- Downloading { percent: f32 },
- Finished,
+pub struct Progress {
+ pub percent: f32,
}
#[derive(Debug, Clone)]
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index f4b07203..8082eccd 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -4,7 +4,7 @@ use download::download;
use iced::task;
use iced::widget::{button, center, column, progress_bar, text, Column};
-use iced::{Center, Element, Right, Task};
+use iced::{Center, Element, Function, Right, Task};
pub fn main() -> iced::Result {
iced::application(
@@ -25,7 +25,7 @@ struct Example {
pub enum Message {
Add,
Download(usize),
- DownloadProgressed(usize, Result<download::Progress, download::Error>),
+ DownloadUpdated(usize, Update),
}
impl Example {
@@ -52,15 +52,13 @@ impl Example {
let task = download.start();
- task.map(move |progress| {
- Message::DownloadProgressed(index, progress)
- })
+ task.map(Message::DownloadUpdated.with(index))
}
- Message::DownloadProgressed(id, progress) => {
+ Message::DownloadUpdated(id, update) => {
if let Some(download) =
self.downloads.iter_mut().find(|download| download.id == id)
{
- download.progress(progress);
+ download.update(update);
}
Task::none()
@@ -95,6 +93,12 @@ struct Download {
state: State,
}
+#[derive(Debug, Clone)]
+pub enum Update {
+ Downloading(download::Progress),
+ Finished(Result<(), download::Error>),
+}
+
#[derive(Debug)]
enum State {
Idle,
@@ -111,18 +115,20 @@ impl Download {
}
}
- pub fn start(
- &mut self,
- ) -> Task<Result<download::Progress, download::Error>> {
+ pub fn start(&mut self) -> Task<Update> {
match self.state {
State::Idle { .. }
| State::Finished { .. }
| State::Errored { .. } => {
- let (task, handle) = Task::stream(download(
- "https://huggingface.co/\
+ let (task, handle) = Task::sip(
+ download(
+ "https://huggingface.co/\
mattshumer/Reflection-Llama-3.1-70B/\
resolve/main/model-00001-of-00162.safetensors",
- ))
+ ),
+ Update::Downloading,
+ Update::Finished,
+ )
.abortable();
self.state = State::Downloading {
@@ -136,20 +142,18 @@ impl Download {
}
}
- pub fn progress(
- &mut self,
- new_progress: Result<download::Progress, download::Error>,
- ) {
+ pub fn update(&mut self, update: Update) {
if let State::Downloading { progress, .. } = &mut self.state {
- match new_progress {
- Ok(download::Progress::Downloading { percent }) => {
- *progress = percent;
- }
- Ok(download::Progress::Finished) => {
- self.state = State::Finished;
+ match update {
+ Update::Downloading(new_progress) => {
+ *progress = new_progress.percent;
}
- Err(_error) => {
- self.state = State::Errored;
+ Update::Finished(result) => {
+ self.state = if result.is_ok() {
+ State::Finished
+ } else {
+ State::Errored
+ };
}
}
}
diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml
index 573389b1..6e8aba06 100644
--- a/examples/gallery/Cargo.toml
+++ b/examples/gallery/Cargo.toml
@@ -17,7 +17,10 @@ serde.features = ["derive"]
bytes.workspace = true
image.workspace = true
+sipper.workspace = true
tokio.workspace = true
+blurhash = "0.2.3"
+
[lints]
workspace = true
diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs
index 986b6bf2..04589030 100644
--- a/examples/gallery/src/civitai.rs
+++ b/examples/gallery/src/civitai.rs
@@ -1,5 +1,6 @@
use bytes::Bytes;
use serde::Deserialize;
+use sipper::{sipper, Straw};
use tokio::task;
use std::fmt;
@@ -10,6 +11,7 @@ use std::sync::Arc;
pub struct Image {
pub id: Id,
url: String,
+ hash: String,
}
impl Image {
@@ -40,45 +42,76 @@ impl Image {
Ok(response.items)
}
- pub async fn download(self, size: Size) -> Result<Rgba, Error> {
- let client = reqwest::Client::new();
-
- let bytes = client
- .get(match size {
- Size::Original => self.url,
- Size::Thumbnail => self
- .url
- .split("/")
- .map(|part| {
- if part.starts_with("width=") {
- "width=640"
- } else {
- part
- }
- })
- .collect::<Vec<_>>()
- .join("/"),
+ pub async fn blurhash(
+ self,
+ width: u32,
+ height: u32,
+ ) -> Result<Blurhash, Error> {
+ task::spawn_blocking(move || {
+ let pixels = blurhash::decode(&self.hash, width, height, 1.0)?;
+
+ Ok::<_, Error>(Blurhash {
+ rgba: Rgba {
+ width,
+ height,
+ pixels: Bytes::from(pixels),
+ },
})
- .send()
- .await?
- .error_for_status()?
- .bytes()
- .await?;
-
- let image = task::spawn_blocking(move || {
- Ok::<_, Error>(
- image::ImageReader::new(io::Cursor::new(bytes))
- .with_guessed_format()?
- .decode()?
- .to_rgba8(),
- )
})
- .await??;
+ .await?
+ }
- Ok(Rgba {
- width: image.width(),
- height: image.height(),
- pixels: Bytes::from(image.into_raw()),
+ pub fn download(self, size: Size) -> impl Straw<Rgba, Blurhash, Error> {
+ sipper(move |mut sender| async move {
+ let client = reqwest::Client::new();
+
+ if let Size::Thumbnail { width, height } = size {
+ let image = self.clone();
+
+ drop(task::spawn(async move {
+ if let Ok(blurhash) = image.blurhash(width, height).await {
+ sender.send(blurhash).await;
+ }
+ }));
+ }
+
+ let bytes = client
+ .get(match size {
+ Size::Original => self.url,
+ Size::Thumbnail { width, .. } => self
+ .url
+ .split("/")
+ .map(|part| {
+ if part.starts_with("width=") {
+ format!("width={}", width * 2) // High DPI
+ } else {
+ part.to_owned()
+ }
+ })
+ .collect::<Vec<_>>()
+ .join("/"),
+ })
+ .send()
+ .await?
+ .error_for_status()?
+ .bytes()
+ .await?;
+
+ let image = task::spawn_blocking(move || {
+ Ok::<_, Error>(
+ image::ImageReader::new(io::Cursor::new(bytes))
+ .with_guessed_format()?
+ .decode()?
+ .to_rgba8(),
+ )
+ })
+ .await??;
+
+ Ok(Rgba {
+ width: image.width(),
+ height: image.height(),
+ pixels: Bytes::from(image.into_raw()),
+ })
})
}
}
@@ -88,6 +121,11 @@ impl Image {
)]
pub struct Id(u32);
+#[derive(Debug, Clone)]
+pub struct Blurhash {
+ pub rgba: Rgba,
+}
+
#[derive(Clone)]
pub struct Rgba {
pub width: u32,
@@ -107,7 +145,7 @@ impl fmt::Debug for Rgba {
#[derive(Debug, Clone, Copy)]
pub enum Size {
Original,
- Thumbnail,
+ Thumbnail { width: u32, height: u32 },
}
#[derive(Debug, Clone)]
@@ -117,6 +155,7 @@ pub enum Error {
IOFailed(Arc<io::Error>),
JoinFailed(Arc<task::JoinError>),
ImageDecodingFailed(Arc<image::ImageError>),
+ BlurhashDecodingFailed(Arc<blurhash::Error>),
}
impl From<reqwest::Error> for Error {
@@ -142,3 +181,9 @@ impl From<image::ImageError> for Error {
Self::ImageDecodingFailed(Arc::new(error))
}
}
+
+impl From<blurhash::Error> for Error {
+ fn from(error: blurhash::Error) -> Self {
+ Self::BlurhashDecodingFailed(Arc::new(error))
+ }
+}
diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs
index 290fa6a0..abafaf2d 100644
--- a/examples/gallery/src/main.rs
+++ b/examples/gallery/src/main.rs
@@ -7,14 +7,15 @@ mod civitai;
use crate::civitai::{Error, Id, Image, Rgba, Size};
use iced::animation;
-use iced::time::Instant;
+use iced::time::{milliseconds, Instant};
use iced::widget::{
button, center_x, container, horizontal_space, image, mouse_area, opaque,
pop, row, scrollable, stack,
};
use iced::window;
use iced::{
- color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme,
+ color, Animation, ContentFit, Element, Fill, Function, Subscription, Task,
+ Theme,
};
use std::collections::HashMap;
@@ -28,7 +29,7 @@ fn main() -> iced::Result {
struct Gallery {
images: Vec<Image>,
- thumbnails: HashMap<Id, Thumbnail>,
+ previews: HashMap<Id, Preview>,
viewer: Viewer,
now: Instant,
}
@@ -40,6 +41,7 @@ enum Message {
ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool),
+ BlurhashDecoded(Id, civitai::Blurhash),
Open(Id),
Close,
Animate(Instant),
@@ -50,7 +52,7 @@ impl Gallery {
(
Self {
images: Vec::new(),
- thumbnails: HashMap::new(),
+ previews: HashMap::new(),
viewer: Viewer::new(),
now: Instant::now(),
},
@@ -64,9 +66,9 @@ impl Gallery {
pub fn subscription(&self) -> Subscription<Message> {
let is_animating = self
- .thumbnails
+ .previews
.values()
- .any(|thumbnail| thumbnail.is_animating(self.now))
+ .any(|preview| preview.is_animating(self.now))
|| self.viewer.is_animating(self.now);
if is_animating {
@@ -93,9 +95,14 @@ impl Gallery {
return Task::none();
};
- Task::perform(image.download(Size::Thumbnail), move |result| {
- Message::ThumbnailDownloaded(id, result)
- })
+ Task::sip(
+ image.download(Size::Thumbnail {
+ width: Preview::WIDTH,
+ height: Preview::HEIGHT,
+ }),
+ Message::BlurhashDecoded.with(id),
+ Message::ThumbnailDownloaded.with(id),
+ )
}
Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba);
@@ -103,14 +110,29 @@ impl Gallery {
Task::none()
}
Message::ThumbnailDownloaded(id, Ok(rgba)) => {
- let thumbnail = Thumbnail::new(rgba);
- let _ = self.thumbnails.insert(id, thumbnail);
+ let thumbnail = if let Some(preview) = self.previews.remove(&id)
+ {
+ preview.load(rgba)
+ } else {
+ Preview::ready(rgba)
+ };
+
+ let _ = self.previews.insert(id, thumbnail);
Task::none()
}
Message::ThumbnailHovered(id, is_hovered) => {
- if let Some(thumbnail) = self.thumbnails.get_mut(&id) {
- thumbnail.zoom.go_mut(is_hovered);
+ if let Some(preview) = self.previews.get_mut(&id) {
+ preview.toggle_zoom(is_hovered);
+ }
+
+ Task::none()
+ }
+ Message::BlurhashDecoded(id, blurhash) => {
+ if !self.previews.contains_key(&id) {
+ let _ = self
+ .previews
+ .insert(id, Preview::loading(blurhash.rgba));
}
Task::none()
@@ -157,7 +179,7 @@ impl Gallery {
row((0..=Image::LIMIT).map(|_| placeholder()))
} else {
row(self.images.iter().map(|image| {
- card(image, self.thumbnails.get(&image.id), self.now)
+ card(image, self.previews.get(&image.id), self.now)
}))
}
.spacing(10)
@@ -174,33 +196,52 @@ impl Gallery {
fn card<'a>(
metadata: &'a Image,
- thumbnail: Option<&'a Thumbnail>,
+ preview: Option<&'a Preview>,
now: Instant,
) -> Element<'a, Message> {
- let image: Element<'_, _> = if let Some(thumbnail) = thumbnail {
- image(&thumbnail.handle)
- .width(Fill)
- .height(Fill)
- .content_fit(ContentFit::Cover)
- .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
- .scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
- .into()
+ let image = if let Some(preview) = preview {
+ let thumbnail: Element<'_, _> =
+ if let Preview::Ready { thumbnail, .. } = &preview {
+ image(&thumbnail.handle)
+ .width(Fill)
+ .height(Fill)
+ .content_fit(ContentFit::Cover)
+ .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
+ .scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
+ .into()
+ } else {
+ horizontal_space().into()
+ };
+
+ if let Some(blurhash) = preview.blurhash(now) {
+ let blurhash = image(&blurhash.handle)
+ .width(Fill)
+ .height(Fill)
+ .content_fit(ContentFit::Cover)
+ .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now));
+
+ stack![blurhash, thumbnail].into()
+ } else {
+ thumbnail
+ }
} else {
horizontal_space().into()
};
let card = mouse_area(
container(image)
- .width(Thumbnail::WIDTH)
- .height(Thumbnail::HEIGHT)
+ .width(Preview::WIDTH)
+ .height(Preview::HEIGHT)
.style(container::dark),
)
.on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false));
- if thumbnail.is_some() {
+ if let Some(preview) = preview {
+ let is_thumbnail = matches!(preview, Preview::Ready { .. });
+
button(card)
- .on_press(Message::Open(metadata.id))
+ .on_press_maybe(is_thumbnail.then_some(Message::Open(metadata.id)))
.padding(0)
.style(button::text)
.into()
@@ -213,23 +254,102 @@ fn card<'a>(
fn placeholder<'a>() -> Element<'a, Message> {
container(horizontal_space())
- .width(Thumbnail::WIDTH)
- .height(Thumbnail::HEIGHT)
+ .width(Preview::WIDTH)
+ .height(Preview::HEIGHT)
.style(container::dark)
.into()
}
+enum Preview {
+ Loading {
+ blurhash: Blurhash,
+ },
+ Ready {
+ blurhash: Option<Blurhash>,
+ thumbnail: Thumbnail,
+ },
+}
+
+struct Blurhash {
+ handle: image::Handle,
+ fade_in: Animation<bool>,
+}
+
struct Thumbnail {
handle: image::Handle,
fade_in: Animation<bool>,
zoom: Animation<bool>,
}
-impl Thumbnail {
- const WIDTH: u16 = 320;
- const HEIGHT: u16 = 410;
+impl Preview {
+ const WIDTH: u32 = 320;
+ const HEIGHT: u32 = 410;
+
+ fn loading(rgba: Rgba) -> Self {
+ Self::Loading {
+ blurhash: Blurhash {
+ fade_in: Animation::new(false)
+ .duration(milliseconds(700))
+ .easing(animation::Easing::EaseIn)
+ .go(true),
+ handle: image::Handle::from_rgba(
+ rgba.width,
+ rgba.height,
+ rgba.pixels,
+ ),
+ },
+ }
+ }
- fn new(rgba: Rgba) -> Self {
+ fn ready(rgba: Rgba) -> Self {
+ Self::Ready {
+ blurhash: None,
+ thumbnail: Thumbnail::new(rgba),
+ }
+ }
+
+ fn load(self, rgba: Rgba) -> Self {
+ let Self::Loading { blurhash } = self else {
+ return self;
+ };
+
+ Self::Ready {
+ blurhash: Some(blurhash),
+ thumbnail: Thumbnail::new(rgba),
+ }
+ }
+
+ fn toggle_zoom(&mut self, enabled: bool) {
+ if let Self::Ready { thumbnail, .. } = self {
+ thumbnail.zoom.go_mut(enabled);
+ }
+ }
+
+ fn is_animating(&self, now: Instant) -> bool {
+ match &self {
+ Self::Loading { blurhash } => blurhash.fade_in.is_animating(now),
+ Self::Ready { thumbnail, .. } => {
+ thumbnail.fade_in.is_animating(now)
+ || thumbnail.zoom.is_animating(now)
+ }
+ }
+ }
+
+ fn blurhash(&self, now: Instant) -> Option<&Blurhash> {
+ match self {
+ Self::Loading { blurhash, .. } => Some(blurhash),
+ Self::Ready {
+ blurhash: Some(blurhash),
+ thumbnail,
+ ..
+ } if thumbnail.fade_in.is_animating(now) => Some(blurhash),
+ Self::Ready { .. } => None,
+ }
+ }
+}
+
+impl Thumbnail {
+ pub fn new(rgba: Rgba) -> Self {
Self {
handle: image::Handle::from_rgba(
rgba.width,
@@ -242,10 +362,6 @@ impl Thumbnail {
.easing(animation::Easing::EaseInOut),
}
}
-
- fn is_animating(&self, now: Instant) -> bool {
- self.fade_in.is_animating(now) || self.zoom.is_animating(now)
- }
}
struct Viewer {
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 1008e477..9516f832 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -9,7 +9,7 @@ use iced::time::{self, milliseconds};
use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text,
};
-use iced::{Center, Element, Fill, Subscription, Task, Theme};
+use iced::{Center, Element, Fill, Function, Subscription, Task, Theme};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
@@ -37,7 +37,7 @@ struct GameOfLife {
#[derive(Debug, Clone)]
enum Message {
- Grid(grid::Message, usize),
+ Grid(usize, grid::Message),
Tick,
TogglePlayback,
ToggleGrid(bool),
@@ -61,7 +61,7 @@ impl GameOfLife {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
- Message::Grid(message, version) => {
+ Message::Grid(version, message) => {
if version == self.version {
self.grid.update(message);
}
@@ -78,9 +78,7 @@ impl GameOfLife {
let version = self.version;
- return Task::perform(task, move |message| {
- Message::Grid(message, version)
- });
+ return Task::perform(task, Message::Grid.with(version));
}
}
Message::TogglePlayback => {
@@ -129,9 +127,7 @@ impl GameOfLife {
);
let content = column![
- self.grid
- .view()
- .map(move |message| Message::Grid(message, version)),
+ self.grid.view().map(Message::Grid.with(version)),
controls,
]
.height(Fill);
@@ -380,7 +376,7 @@ mod grid {
fn update(
&self,
interaction: &mut Interaction,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
@@ -471,7 +467,7 @@ mod grid {
_ => action.and_capture(),
})
}
- mouse::Event::WheelScrolled { delta } => match delta {
+ mouse::Event::WheelScrolled { delta } => match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
if y < 0.0 && self.scaling > Self::MIN_SCALING
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
index 33232fac..24293138 100644
--- a/examples/loading_spinners/src/circular.rs
+++ b/examples/loading_spinners/src/circular.rs
@@ -264,7 +264,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -278,7 +278,7 @@ where
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.rotation_duration,
- now,
+ *now,
);
state.cache.clear();
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
index a10b64f0..a6713c7a 100644
--- a/examples/loading_spinners/src/linear.rs
+++ b/examples/loading_spinners/src/linear.rs
@@ -178,7 +178,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -189,7 +189,7 @@ where
let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event {
- *state = state.timed_transition(self.cycle_duration, now);
+ *state = state.timed_transition(self.cycle_duration, *now);
shell.request_redraw();
}
diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml
index fa6ced74..7af1741b 100644
--- a/examples/markdown/Cargo.toml
+++ b/examples/markdown/Cargo.toml
@@ -7,6 +7,17 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["markdown", "highlighter", "tokio", "debug"]
+iced.features = ["markdown", "highlighter", "image", "tokio", "debug"]
+
+reqwest.version = "0.12"
+reqwest.features = ["json"]
+
+image.workspace = true
+tokio.workspace = true
open = "5.3"
+
+# Disabled to keep amount of build dependencies low
+# This can be re-enabled on demand
+# [build-dependencies]
+# iced_fontello = "0.13"
diff --git a/examples/markdown/build.rs b/examples/markdown/build.rs
new file mode 100644
index 00000000..ecbb7666
--- /dev/null
+++ b/examples/markdown/build.rs
@@ -0,0 +1,5 @@
+pub fn main() {
+ // println!("cargo::rerun-if-changed=fonts/markdown-icons.toml");
+ // iced_fontello::build("fonts/markdown-icons.toml")
+ // .expect("Build icons font");
+}
diff --git a/examples/markdown/fonts/markdown-icons.toml b/examples/markdown/fonts/markdown-icons.toml
new file mode 100644
index 00000000..60c91d17
--- /dev/null
+++ b/examples/markdown/fonts/markdown-icons.toml
@@ -0,0 +1,4 @@
+module = "icon"
+
+[glyphs]
+copy = "fontawesome-docs"
diff --git a/examples/markdown/fonts/markdown-icons.ttf b/examples/markdown/fonts/markdown-icons.ttf
new file mode 100644
index 00000000..013f03a5
--- /dev/null
+++ b/examples/markdown/fonts/markdown-icons.ttf
Binary files differ
diff --git a/examples/markdown/src/icon.rs b/examples/markdown/src/icon.rs
new file mode 100644
index 00000000..cfe32541
--- /dev/null
+++ b/examples/markdown/src/icon.rs
@@ -0,0 +1,15 @@
+// Generated automatically by iced_fontello at build time.
+// Do not edit manually. Source: ../fonts/markdown-icons.toml
+// dcd2f0c969d603e2ee9237a4b70fa86b1a6e84d86f4689046d8fdd10440b06b9
+use iced::widget::{text, Text};
+use iced::Font;
+
+pub const FONT: &[u8] = include_bytes!("../fonts/markdown-icons.ttf");
+
+pub fn copy<'a>() -> Text<'a> {
+ icon("\u{F0C5}")
+}
+
+fn icon(codepoint: &str) -> Text<'_> {
+ text(codepoint).font(Font::with_name("markdown-icons"))
+}
diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs
index ba93ee18..512d4b44 100644
--- a/examples/markdown/src/main.rs
+++ b/examples/markdown/src/main.rs
@@ -1,50 +1,79 @@
+mod icon;
+
+use iced::animation;
+use iced::clipboard;
use iced::highlighter;
-use iced::time::{self, milliseconds};
+use iced::task;
+use iced::time::{self, milliseconds, Instant};
use iced::widget::{
- self, hover, markdown, right, row, scrollable, text_editor, toggler,
+ self, button, center_x, container, horizontal_space, hover, image,
+ markdown, pop, right, row, scrollable, text_editor, toggler,
};
-use iced::{Element, Fill, Font, Subscription, Task, Theme};
+use iced::window;
+use iced::{Animation, Element, Fill, Font, Subscription, Task, Theme};
+
+use std::collections::HashMap;
+use std::io;
+use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
+ .font(icon::FONT)
.subscription(Markdown::subscription)
.theme(Markdown::theme)
.run_with(Markdown::new)
}
struct Markdown {
- content: text_editor::Content,
+ content: markdown::Content,
+ raw: text_editor::Content,
+ images: HashMap<markdown::Url, Image>,
mode: Mode,
theme: Theme,
+ now: Instant,
}
enum Mode {
- Preview(Vec<markdown::Item>),
- Stream {
- pending: String,
- parsed: markdown::Content,
+ Preview,
+ Stream { pending: String },
+}
+
+enum Image {
+ Loading {
+ _download: task::Handle,
+ },
+ Ready {
+ handle: image::Handle,
+ fade_in: Animation<bool>,
},
+ #[allow(dead_code)]
+ Errored(Error),
}
#[derive(Debug, Clone)]
enum Message {
Edit(text_editor::Action),
+ Copy(String),
LinkClicked(markdown::Url),
+ ImageShown(markdown::Url),
+ ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
ToggleStream(bool),
NextToken,
+ Animate(Instant),
}
impl Markdown {
fn new() -> (Self, Task<Message>) {
const INITIAL_CONTENT: &str = include_str!("../overview.md");
- let theme = Theme::TokyoNight;
-
(
Self {
- content: text_editor::Content::with_text(INITIAL_CONTENT),
- mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
- theme,
+ content: markdown::Content::parse(INITIAL_CONTENT),
+ raw: text_editor::Content::with_text(INITIAL_CONTENT),
+ images: HashMap::new(),
+ mode: Mode::Preview,
+ theme: Theme::TokyoNight,
+ now: Instant::now(),
},
widget::focus_next(),
)
@@ -55,26 +84,73 @@ impl Markdown {
Message::Edit(action) => {
let is_edit = action.is_edit();
- self.content.perform(action);
+ self.raw.perform(action);
if is_edit {
- self.mode = Mode::Preview(
- markdown::parse(&self.content.text()).collect(),
- );
+ self.content = markdown::Content::parse(&self.raw.text());
+ self.mode = Mode::Preview;
+
+ let images = self.content.images();
+ self.images.retain(|url, _image| images.contains(url));
}
Task::none()
}
+ Message::Copy(content) => clipboard::write(content),
Message::LinkClicked(link) => {
let _ = open::that_in_background(link.to_string());
Task::none()
}
+ Message::ImageShown(url) => {
+ if self.images.contains_key(&url) {
+ return Task::none();
+ }
+
+ let (download_image, handle) = Task::future({
+ let url = url.clone();
+
+ async move {
+ // Wait half a second for further editions before attempting download
+ tokio::time::sleep(milliseconds(500)).await;
+ download_image(url).await
+ }
+ })
+ .abortable();
+
+ let _ = self.images.insert(
+ url.clone(),
+ Image::Loading {
+ _download: handle.abort_on_drop(),
+ },
+ );
+
+ download_image.map(move |result| {
+ Message::ImageDownloaded(url.clone(), result)
+ })
+ }
+ Message::ImageDownloaded(url, result) => {
+ let _ = self.images.insert(
+ url,
+ result
+ .map(|handle| Image::Ready {
+ handle,
+ fade_in: Animation::new(false)
+ .quick()
+ .easing(animation::Easing::EaseInOut)
+ .go(true),
+ })
+ .unwrap_or_else(Image::Errored),
+ );
+
+ Task::none()
+ }
Message::ToggleStream(enable_stream) => {
if enable_stream {
+ self.content = markdown::Content::new();
+
self.mode = Mode::Stream {
- pending: self.content.text(),
- parsed: markdown::Content::new(),
+ pending: self.raw.text(),
};
scrollable::snap_to(
@@ -82,24 +158,22 @@ impl Markdown {
scrollable::RelativeOffset::END,
)
} else {
- self.mode = Mode::Preview(
- markdown::parse(&self.content.text()).collect(),
- );
+ self.mode = Mode::Preview;
Task::none()
}
}
Message::NextToken => {
match &mut self.mode {
- Mode::Preview(_) => {}
- Mode::Stream { pending, parsed } => {
+ Mode::Preview => {}
+ Mode::Stream { pending } => {
if pending.is_empty() {
- self.mode = Mode::Preview(parsed.items().to_vec());
+ self.mode = Mode::Preview;
} else {
let mut tokens = pending.split(' ');
if let Some(token) = tokens.next() {
- parsed.push_str(&format!("{token} "));
+ self.content.push_str(&format!("{token} "));
}
*pending = tokens.collect::<Vec<_>>().join(" ");
@@ -109,11 +183,16 @@ impl Markdown {
Task::none()
}
+ Message::Animate(now) => {
+ self.now = now;
+
+ Task::none()
+ }
}
}
fn view(&self) -> Element<Message> {
- let editor = text_editor(&self.content)
+ let editor = text_editor(&self.raw)
.placeholder("Type your Markdown here...")
.on_action(Message::Edit)
.height(Fill)
@@ -121,17 +200,14 @@ impl Markdown {
.font(Font::MONOSPACE)
.highlight("markdown", highlighter::Theme::Base16Ocean);
- let items = match &self.mode {
- Mode::Preview(items) => items.as_slice(),
- Mode::Stream { parsed, .. } => parsed.items(),
- };
-
- let preview = markdown(
- items,
- markdown::Settings::default(),
- markdown::Style::from_palette(self.theme.palette()),
- )
- .map(Message::LinkClicked);
+ let preview = markdown::view_with(
+ self.content.items(),
+ &self.theme,
+ &CustomViewer {
+ images: &self.images,
+ now: self.now,
+ },
+ );
row![
editor,
@@ -159,11 +235,146 @@ impl Markdown {
}
fn subscription(&self) -> Subscription<Message> {
- match self.mode {
- Mode::Preview(_) => Subscription::none(),
+ let listen_stream = match self.mode {
+ Mode::Preview => Subscription::none(),
Mode::Stream { .. } => {
time::every(milliseconds(10)).map(|_| Message::NextToken)
}
+ };
+
+ let animate = {
+ let is_animating = self.images.values().any(|image| match image {
+ Image::Ready { fade_in, .. } => fade_in.is_animating(self.now),
+ _ => false,
+ });
+
+ if is_animating {
+ window::frames().map(Message::Animate)
+ } else {
+ Subscription::none()
+ }
+ };
+
+ Subscription::batch([listen_stream, animate])
+ }
+}
+
+struct CustomViewer<'a> {
+ images: &'a HashMap<markdown::Url, Image>,
+ now: Instant,
+}
+
+impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> {
+ fn on_link_click(url: markdown::Url) -> Message {
+ Message::LinkClicked(url)
+ }
+
+ fn image(
+ &self,
+ _settings: markdown::Settings,
+ url: &'a markdown::Url,
+ _title: &'a str,
+ _alt: &markdown::Text,
+ ) -> Element<'a, Message> {
+ if let Some(Image::Ready { handle, fade_in }) = self.images.get(url) {
+ center_x(
+ image(handle)
+ .opacity(fade_in.interpolate(0.0, 1.0, self.now))
+ .scale(fade_in.interpolate(1.2, 1.0, self.now)),
+ )
+ .into()
+ } else {
+ pop(horizontal_space())
+ .key(url.as_str())
+ .on_show(|_size| Message::ImageShown(url.clone()))
+ .into()
}
}
+
+ fn code_block(
+ &self,
+ settings: markdown::Settings,
+ _language: Option<&'a str>,
+ code: &'a str,
+ lines: &'a [markdown::Text],
+ ) -> Element<'a, Message> {
+ let code_block =
+ markdown::code_block(settings, lines, Message::LinkClicked);
+
+ let copy = button(icon::copy().size(12))
+ .padding(2)
+ .on_press_with(|| Message::Copy(code.to_owned()))
+ .style(button::text);
+
+ hover(
+ code_block,
+ right(container(copy).style(container::dark))
+ .padding(settings.spacing / 2),
+ )
+ }
+}
+
+async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> {
+ use std::io;
+ use tokio::task;
+
+ println!("Trying to download image: {url}");
+
+ let client = reqwest::Client::new();
+
+ let bytes = client
+ .get(url)
+ .send()
+ .await?
+ .error_for_status()?
+ .bytes()
+ .await?;
+
+ let image = task::spawn_blocking(move || {
+ Ok::<_, Error>(
+ ::image::ImageReader::new(io::Cursor::new(bytes))
+ .with_guessed_format()?
+ .decode()?
+ .to_rgba8(),
+ )
+ })
+ .await??;
+
+ Ok(image::Handle::from_rgba(
+ image.width(),
+ image.height(),
+ image.into_raw(),
+ ))
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ RequestFailed(Arc<reqwest::Error>),
+ IOFailed(Arc<io::Error>),
+ JoinFailed(Arc<tokio::task::JoinError>),
+ ImageDecodingFailed(Arc<::image::ImageError>),
+}
+
+impl From<reqwest::Error> for Error {
+ fn from(error: reqwest::Error) -> Self {
+ Self::RequestFailed(Arc::new(error))
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(error: io::Error) -> Self {
+ Self::IOFailed(Arc::new(error))
+ }
+}
+
+impl From<tokio::task::JoinError> for Error {
+ fn from(error: tokio::task::JoinError) -> Self {
+ Self::JoinFailed(Arc::new(error))
+ }
+}
+
+impl From<::image::ImageError> for Error {
+ fn from(error: ::image::ImageError) -> Self {
+ Self::ImageDecodingFailed(Arc::new(error))
+ }
}
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
index f9021c8d..8cec9d4c 100644
--- a/examples/multi_window/src/main.rs
+++ b/examples/multi_window/src/main.rs
@@ -3,7 +3,9 @@ use iced::widget::{
text_input,
};
use iced::window;
-use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector};
+use iced::{
+ Center, Element, Fill, Function, Subscription, Task, Theme, Vector,
+};
use std::collections::BTreeMap;
@@ -169,7 +171,7 @@ impl Window {
let scale_input = column![
text("Window scale factor:"),
text_input("Window Scale", &self.scale_input)
- .on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
+ .on_input(Message::ScaleInputChanged.with(id))
.on_submit(Message::ScaleChanged(
id,
self.scale_input.to_string()
@@ -179,7 +181,7 @@ impl Window {
let title_input = column![
text("Window title:"),
text_input("Window Title", &self.title)
- .on_input(move |msg| { Message::TitleChanged(id, msg) })
+ .on_input(Message::TitleChanged.with(id))
.id(format!("input-{id}"))
];
diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs
index 5f4a5c90..bda3b8f7 100644
--- a/examples/multitouch/src/main.rs
+++ b/examples/multitouch/src/main.rs
@@ -55,11 +55,11 @@ impl canvas::Program<Message> for Multitouch {
fn update(
&self,
_state: &mut Self::State,
- event: Event,
+ event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
- let message = match event {
+ let message = match event.clone() {
Event::Touch(
touch::Event::FingerPressed { id, position }
| touch::Event::FingerMoved { id, position },
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index 6359fb5a..fec4e1b4 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -21,9 +21,9 @@ pub fn main() -> iced::Result {
struct ScrollableDemo {
scrollable_direction: Direction,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
+ scrollbar_width: u32,
+ scrollbar_margin: u32,
+ scroller_width: u32,
current_scroll_offset: scrollable::RelativeOffset,
anchor: scrollable::Anchor,
}
@@ -39,9 +39,9 @@ enum Direction {
enum Message {
SwitchDirection(Direction),
AlignmentChanged(scrollable::Anchor),
- ScrollbarWidthChanged(u16),
- ScrollbarMarginChanged(u16),
- ScrollerWidthChanged(u16),
+ ScrollbarWidthChanged(u32),
+ ScrollbarMarginChanged(u32),
+ ScrollerWidthChanged(u32),
ScrollToBeginning,
ScrollToEnd,
Scrolled(scrollable::Viewport),
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index d4d483f5..a4a89455 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -76,7 +76,7 @@ impl canvas::Program<Message> for SierpinskiGraph {
fn update(
&self,
_state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs
index afa657d8..56f934b7 100644
--- a/examples/system_information/src/main.rs
+++ b/examples/system_information/src/main.rs
@@ -102,22 +102,22 @@ impl Example {
);
let memory_readable =
- ByteSize::b(information.memory_total).to_string();
+ ByteSize::b(information.memory_total).to_string_as(true);
let memory_total = text!(
"Memory (total): {} bytes ({memory_readable})",
information.memory_total,
);
- let memory_text = if let Some(memory_used) =
- information.memory_used
- {
- let memory_readable = ByteSize::b(memory_used).to_string();
+ let memory_text =
+ if let Some(memory_used) = information.memory_used {
+ let memory_readable =
+ ByteSize::b(memory_used).to_string_as(true);
- format!("{memory_used} bytes ({memory_readable})")
- } else {
- String::from("None")
- };
+ format!("{memory_used} bytes ({memory_readable})")
+ } else {
+ String::from("None")
+ };
let memory_used = text!("Memory (used): {memory_text}");
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 2ae9bfe2..dc314df8 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -361,7 +361,7 @@ mod toast {
fn update(
&mut self,
state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -491,7 +491,7 @@ mod toast {
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -530,7 +530,7 @@ mod toast {
child.as_widget_mut().update(
state,
- event.clone(),
+ event,
layout,
cursor,
renderer,
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 74c4c464..033cb122 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -4,7 +4,9 @@ use iced::widget::{
scrollable, text, text_input, Text,
};
use iced::window;
-use iced::{Center, Element, Fill, Font, Subscription, Task as Command};
+use iced::{
+ Center, Element, Fill, Font, Function, Subscription, Task as Command,
+};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -215,9 +217,8 @@ impl Todos {
.map(|(i, task)| {
(
task.id,
- task.view(i).map(move |message| {
- Message::TaskMessage(i, message)
- }),
+ task.view(i)
+ .map(Message::TaskMessage.with(i)),
)
}),
)
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index 32720c47..2ca1df44 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -24,12 +24,12 @@ pub struct Tour {
screen: Screen,
slider: u8,
layout: Layout,
- spacing: u16,
- text_size: u16,
+ spacing: u32,
+ text_size: u32,
text_color: Color,
language: Option<Language>,
toggler: bool,
- image_width: u16,
+ image_width: u32,
image_filter_method: image::FilterMethod,
input_value: String,
input_is_secure: bool,
@@ -43,11 +43,11 @@ pub enum Message {
NextPressed,
SliderChanged(u8),
LayoutChanged(Layout),
- SpacingChanged(u16),
- TextSizeChanged(u16),
+ SpacingChanged(u32),
+ TextSizeChanged(u32),
TextColorChanged(Color),
LanguageSelected(Language),
- ImageWidthChanged(u16),
+ ImageWidthChanged(u32),
ImageUseNearestToggled(bool),
InputChanged(String),
ToggleSecureInput(bool),
@@ -537,7 +537,7 @@ impl Screen {
}
fn ferris<'a>(
- width: u16,
+ width: u32,
filter_method: image::FilterMethod,
) -> Container<'a, Message> {
center_x(
diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs
index 14652936..149a260c 100644
--- a/examples/websocket/src/echo.rs
+++ b/examples/websocket/src/echo.rs
@@ -1,73 +1,59 @@
pub mod server;
use iced::futures;
-use iced::stream;
+use iced::task::{sipper, Never, Sipper};
use iced::widget::text;
use futures::channel::mpsc;
use futures::sink::SinkExt;
-use futures::stream::{Stream, StreamExt};
+use futures::stream::StreamExt;
use async_tungstenite::tungstenite;
use std::fmt;
-pub fn connect() -> impl Stream<Item = Event> {
- stream::channel(100, |mut output| async move {
- let mut state = State::Disconnected;
-
+pub fn connect() -> impl Sipper<Never, Event> {
+ sipper(|mut output| async move {
loop {
- match &mut state {
- State::Disconnected => {
- const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
+ const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
- match async_tungstenite::tokio::connect_async(ECHO_SERVER)
- .await
- {
- Ok((websocket, _)) => {
- let (sender, receiver) = mpsc::channel(100);
+ let (mut websocket, mut input) =
+ match async_tungstenite::tokio::connect_async(ECHO_SERVER).await
+ {
+ Ok((websocket, _)) => {
+ let (sender, receiver) = mpsc::channel(100);
- let _ = output
- .send(Event::Connected(Connection(sender)))
- .await;
+ output.send(Event::Connected(Connection(sender))).await;
- state = State::Connected(websocket, receiver);
- }
- Err(_) => {
- tokio::time::sleep(
- tokio::time::Duration::from_secs(1),
- )
+ (websocket.fuse(), receiver)
+ }
+ Err(_) => {
+ tokio::time::sleep(tokio::time::Duration::from_secs(1))
.await;
- let _ = output.send(Event::Disconnected).await;
- }
+ output.send(Event::Disconnected).await;
+ continue;
}
- }
- State::Connected(websocket, input) => {
- let mut fused_websocket = websocket.by_ref().fuse();
-
- futures::select! {
- received = fused_websocket.select_next_some() => {
- match received {
- Ok(tungstenite::Message::Text(message)) => {
- let _ = output.send(Event::MessageReceived(Message::User(message))).await;
- }
- Err(_) => {
- let _ = output.send(Event::Disconnected).await;
-
- state = State::Disconnected;
- }
- Ok(_) => continue,
+ };
+
+ loop {
+ futures::select! {
+ received = websocket.select_next_some() => {
+ match received {
+ Ok(tungstenite::Message::Text(message)) => {
+ output.send(Event::MessageReceived(Message::User(message))).await;
+ }
+ Err(_) => {
+ output.send(Event::Disconnected).await;
+ break;
}
+ Ok(_) => {},
}
+ }
+ message = input.select_next_some() => {
+ let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
- message = input.select_next_some() => {
- let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
-
- if result.is_err() {
- let _ = output.send(Event::Disconnected).await;
-
- state = State::Disconnected;
- }
+ if result.is_err() {
+ output.send(Event::Disconnected).await;
}
}
}
@@ -76,18 +62,6 @@ pub fn connect() -> impl Stream<Item = Event> {
})
}
-#[derive(Debug)]
-#[allow(clippy::large_enum_variant)]
-enum State {
- Disconnected,
- Connected(
- async_tungstenite::WebSocketStream<
- async_tungstenite::tokio::ConnectStream,
- >,
- mpsc::Receiver<Message>,
- ),
-}
-
#[derive(Debug, Clone)]
pub enum Event {
Connected(Connection),