Merge pull request #2805 from iced-rs/feature/sipper-support

`sipper` support and some QoL
This commit is contained in:
Héctor 2025-02-12 01:51:20 +01:00 committed by GitHub
commit 89a412695a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 272 additions and 190 deletions

12
Cargo.lock generated
View file

@ -1875,6 +1875,7 @@ dependencies = [
"image", "image",
"reqwest", "reqwest",
"serde", "serde",
"sipper",
"tokio", "tokio",
] ]
@ -2592,6 +2593,7 @@ dependencies = [
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"raw-window-handle 0.6.2", "raw-window-handle 0.6.2",
"sipper",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@ -5227,6 +5229,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "sipper"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95"
dependencies = [
"futures",
"pin-project-lite",
]
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.22.3" version = "0.22.3"

View file

@ -172,6 +172,7 @@ raw-window-handle = "0.6"
resvg = "0.42" resvg = "0.42"
rustc-hash = "2.0" rustc-hash = "2.0"
sha2 = "0.10" sha2 = "0.10"
sipper = "0.1"
smol = "1.0" smol = "1.0"
smol_str = "0.2" smol_str = "0.2"
softbuffer = "0.4" softbuffer = "0.4"

View file

@ -93,6 +93,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// ///
/// ```no_run /// ```no_run
/// # mod iced { /// # mod iced {
/// # pub use iced_core::Function;
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// # /// #
/// # pub mod widget { /// # pub mod widget {
@ -119,7 +120,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// use counter::Counter; /// use counter::Counter;
/// ///
/// use iced::widget::row; /// use iced::widget::row;
/// use iced::Element; /// use iced::{Element, Function};
/// ///
/// struct ManyCounters { /// struct ManyCounters {
/// counters: Vec<Counter>, /// counters: Vec<Counter>,
@ -142,7 +143,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// // Here we turn our `Element<counter::Message>` into /// // Here we turn our `Element<counter::Message>` into
/// // an `Element<Message>` by combining the `index` and the /// // an `Element<Message>` by combining the `index` and the
/// // message of the `element`. /// // message of the `element`.
/// counter.map(move |message| Message::Counter(index, message)) /// counter.map(Message::Counter.with(index))
/// }), /// }),
/// ) /// )
/// .into() /// .into()

View file

@ -93,3 +93,60 @@ pub use smol_str::SmolStr;
pub fn never<T>(never: std::convert::Infallible) -> T { pub fn never<T>(never: std::convert::Infallible) -> T {
match never {} match never {}
} }
/// A trait extension for binary functions (`Fn(A, B) -> O`).
///
/// It enables you to use a bunch of nifty functional programming paradigms
/// that work well with iced.
pub trait Function<A, B, O> {
/// Applies the given first argument to a binary function and returns
/// a new function that takes the other argument.
///
/// This lets you partially "apply" a function—equivalent to currying,
/// but it only works with binary functions. If you want to apply an
/// arbitrary number of arguments, create a little struct for them.
///
/// # When is this useful?
/// Sometimes you will want to identify the source or target
/// of some message in your user interface. This can be achieved through
/// normal means by defining a closure and moving the identifier
/// inside:
///
/// ```rust
/// # let element: Option<()> = Some(());
/// # enum Message { ButtonPressed(u32, ()) }
/// let id = 123;
///
/// # let _ = {
/// element.map(move |result| Message::ButtonPressed(id, result))
/// # };
/// ```
///
/// That's quite a mouthful. [`with`](Self::with) lets you write:
///
/// ```rust
/// # use iced_core::Function;
/// # let element: Option<()> = Some(());
/// # enum Message { ButtonPressed(u32, ()) }
/// let id = 123;
///
/// # let _ = {
/// element.map(Message::ButtonPressed.with(id))
/// # };
/// ```
///
/// Effectively creating the same closure that partially applies
/// the `id` to the message—but much more concise!
fn with(self, prefix: A) -> impl Fn(B) -> O;
}
impl<F, A, B, O> Function<A, B, O> for F
where
F: Fn(A, B) -> O,
Self: Sized,
A: Copy,
{
fn with(self, prefix: A) -> impl Fn(B) -> O {
move |result| self(prefix, result)
}
}

View file

@ -1,16 +1,14 @@
use iced::futures::{SinkExt, Stream, StreamExt}; use iced::futures::StreamExt;
use iced::stream::try_channel; use iced::task::{sipper, Straw};
use std::sync::Arc; use std::sync::Arc;
pub fn download( pub fn download(url: impl AsRef<str>) -> impl Straw<(), Progress, Error> {
url: impl AsRef<str>, sipper(move |mut progress| async move {
) -> impl Stream<Item = Result<Progress, Error>> {
try_channel(1, move |mut output| async move {
let response = reqwest::get(url.as_ref()).await?; let response = reqwest::get(url.as_ref()).await?;
let total = response.content_length().ok_or(Error::NoContentLength)?; 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 byte_stream = response.bytes_stream();
let mut downloaded = 0; let mut downloaded = 0;
@ -19,23 +17,20 @@ pub fn download(
let bytes = next_bytes?; let bytes = next_bytes?;
downloaded += bytes.len(); downloaded += bytes.len();
let _ = output let _ = progress
.send(Progress::Downloading { .send(Progress {
percent: 100.0 * downloaded as f32 / total as f32, percent: 100.0 * downloaded as f32 / total as f32,
}) })
.await; .await;
} }
let _ = output.send(Progress::Finished).await;
Ok(()) Ok(())
}) })
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Progress { pub struct Progress {
Downloading { percent: f32 }, pub percent: f32,
Finished,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -4,7 +4,7 @@ use download::download;
use iced::task; use iced::task;
use iced::widget::{button, center, column, progress_bar, text, Column}; 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 { pub fn main() -> iced::Result {
iced::application( iced::application(
@ -25,7 +25,7 @@ struct Example {
pub enum Message { pub enum Message {
Add, Add,
Download(usize), Download(usize),
DownloadProgressed(usize, Result<download::Progress, download::Error>), DownloadUpdated(usize, Update),
} }
impl Example { impl Example {
@ -52,15 +52,13 @@ impl Example {
let task = download.start(); let task = download.start();
task.map(move |progress| { task.map(Message::DownloadUpdated.with(index))
Message::DownloadProgressed(index, progress)
})
} }
Message::DownloadProgressed(id, progress) => { Message::DownloadUpdated(id, update) => {
if let Some(download) = if let Some(download) =
self.downloads.iter_mut().find(|download| download.id == id) self.downloads.iter_mut().find(|download| download.id == id)
{ {
download.progress(progress); download.update(update);
} }
Task::none() Task::none()
@ -95,6 +93,12 @@ struct Download {
state: State, state: State,
} }
#[derive(Debug, Clone)]
pub enum Update {
Downloading(download::Progress),
Finished(Result<(), download::Error>),
}
#[derive(Debug)] #[derive(Debug)]
enum State { enum State {
Idle, Idle,
@ -111,18 +115,20 @@ impl Download {
} }
} }
pub fn start( pub fn start(&mut self) -> Task<Update> {
&mut self,
) -> Task<Result<download::Progress, download::Error>> {
match self.state { match self.state {
State::Idle { .. } State::Idle { .. }
| State::Finished { .. } | State::Finished { .. }
| State::Errored { .. } => { | State::Errored { .. } => {
let (task, handle) = Task::stream(download( let (task, handle) = Task::sip(
"https://huggingface.co/\ download(
"https://huggingface.co/\
mattshumer/Reflection-Llama-3.1-70B/\ mattshumer/Reflection-Llama-3.1-70B/\
resolve/main/model-00001-of-00162.safetensors", resolve/main/model-00001-of-00162.safetensors",
)) ),
Update::Downloading,
Update::Finished,
)
.abortable(); .abortable();
self.state = State::Downloading { self.state = State::Downloading {
@ -136,20 +142,18 @@ impl Download {
} }
} }
pub fn progress( pub fn update(&mut self, update: Update) {
&mut self,
new_progress: Result<download::Progress, download::Error>,
) {
if let State::Downloading { progress, .. } = &mut self.state { if let State::Downloading { progress, .. } = &mut self.state {
match new_progress { match update {
Ok(download::Progress::Downloading { percent }) => { Update::Downloading(new_progress) => {
*progress = percent; *progress = new_progress.percent;
} }
Ok(download::Progress::Finished) => { Update::Finished(result) => {
self.state = State::Finished; self.state = if result.is_ok() {
} State::Finished
Err(_error) => { } else {
self.state = State::Errored; State::Errored
};
} }
} }
} }

View file

@ -17,6 +17,7 @@ serde.features = ["derive"]
bytes.workspace = true bytes.workspace = true
image.workspace = true image.workspace = true
sipper.workspace = true
tokio.workspace = true tokio.workspace = true
blurhash = "0.2.3" blurhash = "0.2.3"

View file

@ -1,5 +1,6 @@
use bytes::Bytes; use bytes::Bytes;
use serde::Deserialize; use serde::Deserialize;
use sipper::{sipper, Straw};
use tokio::task; use tokio::task;
use std::fmt; use std::fmt;
@ -45,58 +46,72 @@ impl Image {
self, self,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<Rgba, Error> { ) -> Result<Blurhash, Error> {
task::spawn_blocking(move || { task::spawn_blocking(move || {
let pixels = blurhash::decode(&self.hash, width, height, 1.0)?; let pixels = blurhash::decode(&self.hash, width, height, 1.0)?;
Ok::<_, Error>(Rgba { Ok::<_, Error>(Blurhash {
width, rgba: Rgba {
height, width,
pixels: Bytes::from(pixels), height,
pixels: Bytes::from(pixels),
},
}) })
}) })
.await? .await?
} }
pub async fn download(self, size: Size) -> Result<Rgba, Error> { pub fn download(self, size: Size) -> impl Straw<Rgba, Blurhash, Error> {
let client = reqwest::Client::new(); sipper(move |mut sender| async move {
let client = reqwest::Client::new();
let bytes = client if let Size::Thumbnail { width, height } = size {
.get(match size { let image = self.clone();
Size::Original => self.url,
Size::Thumbnail { width } => self drop(task::spawn(async move {
.url if let Ok(blurhash) = image.blurhash(width, height).await {
.split("/") sender.send(blurhash).await;
.map(|part| { }
if part.starts_with("width=") { }));
format!("width={}", width * 2) // High DPI }
} else {
part.to_owned() let bytes = client
} .get(match size {
}) Size::Original => self.url,
.collect::<Vec<_>>() Size::Thumbnail { width, .. } => self
.join("/"), .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(),
)
}) })
.send() .await??;
.await?
.error_for_status()?
.bytes()
.await?;
let image = task::spawn_blocking(move || { Ok(Rgba {
Ok::<_, Error>( width: image.width(),
image::ImageReader::new(io::Cursor::new(bytes)) height: image.height(),
.with_guessed_format()? pixels: Bytes::from(image.into_raw()),
.decode()? })
.to_rgba8(),
)
})
.await??;
Ok(Rgba {
width: image.width(),
height: image.height(),
pixels: Bytes::from(image.into_raw()),
}) })
} }
} }
@ -106,6 +121,11 @@ impl Image {
)] )]
pub struct Id(u32); pub struct Id(u32);
#[derive(Debug, Clone)]
pub struct Blurhash {
pub rgba: Rgba,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Rgba { pub struct Rgba {
pub width: u32, pub width: u32,
@ -125,7 +145,7 @@ impl fmt::Debug for Rgba {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Size { pub enum Size {
Original, Original,
Thumbnail { width: u32 }, Thumbnail { width: u32, height: u32 },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -14,7 +14,8 @@ use iced::widget::{
}; };
use iced::window; use iced::window;
use iced::{ use iced::{
color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme, color, Animation, ContentFit, Element, Fill, Function, Subscription, Task,
Theme,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -40,7 +41,7 @@ enum Message {
ImageDownloaded(Result<Rgba, Error>), ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>), ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool), ThumbnailHovered(Id, bool),
BlurhashDecoded(Id, Result<Rgba, Error>), BlurhashDecoded(Id, civitai::Blurhash),
Open(Id), Open(Id),
Close, Close,
Animate(Instant), Animate(Instant),
@ -94,18 +95,14 @@ impl Gallery {
return Task::none(); return Task::none();
}; };
Task::batch(vec![ Task::sip(
Task::perform( image.download(Size::Thumbnail {
image.clone().blurhash(Preview::WIDTH, Preview::HEIGHT), width: Preview::WIDTH,
move |result| Message::BlurhashDecoded(id, result), height: Preview::HEIGHT,
), }),
Task::perform( Message::BlurhashDecoded.with(id),
image.download(Size::Thumbnail { Message::ThumbnailDownloaded.with(id),
width: Preview::WIDTH, )
}),
move |result| Message::ThumbnailDownloaded(id, result),
),
])
} }
Message::ImageDownloaded(Ok(rgba)) => { Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba); self.viewer.show(rgba);
@ -131,9 +128,11 @@ impl Gallery {
Task::none() Task::none()
} }
Message::BlurhashDecoded(id, Ok(rgba)) => { Message::BlurhashDecoded(id, blurhash) => {
if !self.previews.contains_key(&id) { if !self.previews.contains_key(&id) {
let _ = self.previews.insert(id, Preview::loading(rgba)); let _ = self
.previews
.insert(id, Preview::loading(blurhash.rgba));
} }
Task::none() Task::none()
@ -167,8 +166,7 @@ impl Gallery {
} }
Message::ImagesListed(Err(error)) Message::ImagesListed(Err(error))
| Message::ImageDownloaded(Err(error)) | Message::ImageDownloaded(Err(error))
| Message::ThumbnailDownloaded(_, Err(error)) | Message::ThumbnailDownloaded(_, Err(error)) => {
| Message::BlurhashDecoded(_, Err(error)) => {
dbg!(error); dbg!(error);
Task::none() Task::none()

View file

@ -9,7 +9,7 @@ use iced::time::{self, milliseconds};
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text, 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 { pub fn main() -> iced::Result {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@ -37,7 +37,7 @@ struct GameOfLife {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Grid(grid::Message, usize), Grid(usize, grid::Message),
Tick, Tick,
TogglePlayback, TogglePlayback,
ToggleGrid(bool), ToggleGrid(bool),
@ -61,7 +61,7 @@ impl GameOfLife {
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::Grid(message, version) => { Message::Grid(version, message) => {
if version == self.version { if version == self.version {
self.grid.update(message); self.grid.update(message);
} }
@ -78,9 +78,7 @@ impl GameOfLife {
let version = self.version; let version = self.version;
return Task::perform(task, move |message| { return Task::perform(task, Message::Grid.with(version));
Message::Grid(message, version)
});
} }
} }
Message::TogglePlayback => { Message::TogglePlayback => {
@ -129,9 +127,7 @@ impl GameOfLife {
); );
let content = column![ let content = column![
self.grid self.grid.view().map(Message::Grid.with(version)),
.view()
.map(move |message| Message::Grid(message, version)),
controls, controls,
] ]
.height(Fill); .height(Fill);

View file

@ -3,7 +3,9 @@ use iced::widget::{
text_input, text_input,
}; };
use iced::window; 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; use std::collections::BTreeMap;
@ -169,7 +171,7 @@ impl Window {
let scale_input = column![ let scale_input = column![
text("Window scale factor:"), text("Window scale factor:"),
text_input("Window Scale", &self.scale_input) 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( .on_submit(Message::ScaleChanged(
id, id,
self.scale_input.to_string() self.scale_input.to_string()
@ -179,7 +181,7 @@ impl Window {
let title_input = column![ let title_input = column![
text("Window title:"), text("Window title:"),
text_input("Window Title", &self.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}")) .id(format!("input-{id}"))
]; ];

View file

@ -4,7 +4,9 @@ use iced::widget::{
scrollable, text, text_input, Text, scrollable, text, text_input, Text,
}; };
use iced::window; 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 serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -215,9 +217,8 @@ impl Todos {
.map(|(i, task)| { .map(|(i, task)| {
( (
task.id, task.id,
task.view(i).map(move |message| { task.view(i)
Message::TaskMessage(i, message) .map(Message::TaskMessage.with(i)),
}),
) )
}), }),
) )

View file

@ -1,73 +1,59 @@
pub mod server; pub mod server;
use iced::futures; use iced::futures;
use iced::stream; use iced::task::{sipper, Never, Sipper};
use iced::widget::text; use iced::widget::text;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::sink::SinkExt; use futures::sink::SinkExt;
use futures::stream::{Stream, StreamExt}; use futures::stream::StreamExt;
use async_tungstenite::tungstenite; use async_tungstenite::tungstenite;
use std::fmt; use std::fmt;
pub fn connect() -> impl Stream<Item = Event> { pub fn connect() -> impl Sipper<Never, Event> {
stream::channel(100, |mut output| async move { sipper(|mut output| async move {
let mut state = State::Disconnected;
loop { loop {
match &mut state { const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
State::Disconnected => {
const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
match async_tungstenite::tokio::connect_async(ECHO_SERVER) let (mut websocket, mut input) =
.await match async_tungstenite::tokio::connect_async(ECHO_SERVER).await
{ {
Ok((websocket, _)) => { Ok((websocket, _)) => {
let (sender, receiver) = mpsc::channel(100); let (sender, receiver) = mpsc::channel(100);
let _ = output output.send(Event::Connected(Connection(sender))).await;
.send(Event::Connected(Connection(sender)))
.await;
state = State::Connected(websocket, receiver); (websocket.fuse(), receiver)
} }
Err(_) => { Err(_) => {
tokio::time::sleep( tokio::time::sleep(tokio::time::Duration::from_secs(1))
tokio::time::Duration::from_secs(1),
)
.await; .await;
let _ = output.send(Event::Disconnected).await; output.send(Event::Disconnected).await;
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() => {
State::Connected(websocket, input) => { let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
let mut fused_websocket = websocket.by_ref().fuse();
futures::select! { if result.is_err() {
received = fused_websocket.select_next_some() => { output.send(Event::Disconnected).await;
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,
}
}
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;
}
} }
} }
} }
@ -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)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {
Connected(Connection), Connected(Connection),

View file

@ -23,11 +23,10 @@ impl crate::Executor for Executor {
pub mod time { pub mod time {
//! Listen and react to time. //! Listen and react to time.
use crate::core::time::{Duration, Instant}; use crate::core::time::{Duration, Instant};
use crate::stream;
use crate::subscription::Subscription; use crate::subscription::Subscription;
use crate::MaybeSend; use crate::MaybeSend;
use futures::SinkExt; use futures::stream;
use std::future::Future; use std::future::Future;
/// Returns a [`Subscription`] that produces messages at a set interval. /// Returns a [`Subscription`] that produces messages at a set interval.
@ -66,12 +65,12 @@ pub mod time {
let f = *f; let f = *f;
let interval = *interval; let interval = *interval;
stream::channel(1, move |mut output| async move { stream::unfold(0, move |i| async move {
loop { if i > 0 {
let _ = output.send(f().await).await;
tokio::time::sleep(interval).await; tokio::time::sleep(interval).await;
} }
Some((f().await, i + 1))
}) })
}) })
} }

View file

@ -23,5 +23,6 @@ iced_core.workspace = true
iced_futures.workspace = true iced_futures.workspace = true
iced_futures.features = ["thread-pool"] iced_futures.features = ["thread-pool"]
thiserror.workspace = true
raw-window-handle.workspace = true raw-window-handle.workspace = true
sipper.workspace = true
thiserror.workspace = true

View file

@ -3,7 +3,6 @@ use crate::core::widget;
use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot; use crate::futures::futures::channel::oneshot;
use crate::futures::futures::future::{self, FutureExt}; use crate::futures::futures::future::{self, FutureExt};
use crate::futures::futures::never::Never;
use crate::futures::futures::stream::{self, Stream, StreamExt}; use crate::futures::futures::stream::{self, Stream, StreamExt};
use crate::futures::{boxed_stream, BoxStream, MaybeSend}; use crate::futures::{boxed_stream, BoxStream, MaybeSend};
use crate::Action; use crate::Action;
@ -11,6 +10,9 @@ use crate::Action;
use std::future::Future; use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
#[doc(no_inline)]
pub use sipper::{sipper, stream, Never, Sender, Sipper, Straw};
/// A set of concurrent actions to be performed by the iced runtime. /// A set of concurrent actions to be performed by the iced runtime.
/// ///
/// A [`Task`] _may_ produce a bunch of values of type `T`. /// A [`Task`] _may_ produce a bunch of values of type `T`.
@ -57,6 +59,22 @@ impl<T> Task<T> {
Self::stream(stream.map(f)) Self::stream(stream.map(f))
} }
/// Creates a [`Task`] that runs the given [`Sipper`] to completion, mapping
/// progress with the first closure and the output with the second one.
pub fn sip<S>(
sipper: S,
on_progress: impl FnMut(S::Progress) -> T + MaybeSend + 'static,
on_output: impl FnOnce(<S as Future>::Output) -> T + MaybeSend + 'static,
) -> Self
where
S: sipper::Core + MaybeSend + 'static,
T: MaybeSend + 'static,
{
Self::stream(stream(sipper::sipper(move |sender| async move {
on_output(sipper.with(on_progress).run(sender).await)
})))
}
/// Combines the given tasks and produces a single [`Task`] that will run all of them /// Combines the given tasks and produces a single [`Task`] that will run all of them
/// in parallel. /// in parallel.
pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self

View file

@ -506,8 +506,8 @@ pub use crate::core::padding;
pub use crate::core::theme; pub use crate::core::theme;
pub use crate::core::{ pub use crate::core::{
never, Alignment, Animation, Background, Border, Color, ContentFit, never, Alignment, Animation, Background, Border, Color, ContentFit,
Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Degrees, Function, Gradient, Length, Padding, Pixels, Point, Radians,
Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector,
}; };
pub use crate::runtime::exit; pub use crate::runtime::exit;
pub use iced_futures::Subscription; pub use iced_futures::Subscription;
@ -519,7 +519,9 @@ pub use Length::{Fill, FillPortion, Shrink};
pub mod task { pub mod task {
//! Create runtime tasks. //! Create runtime tasks.
pub use crate::runtime::task::{Handle, Task}; pub use crate::runtime::task::{
sipper, stream, Handle, Never, Sipper, Straw, Task,
};
} }
pub mod clipboard { pub mod clipboard {