From 2dff73b8b2871afca6c65c861954c196818f960f Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:23:22 +0200 Subject: [PATCH] final impl --- src/widget/mod.rs | 4 +- src/widget/toaster/mod.rs | 127 ++++++++++++++++++++++++++++------- src/widget/toaster/widget.rs | 40 +++++++---- 3 files changed, 129 insertions(+), 42 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 4953b127..7f36f0c4 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -350,6 +350,4 @@ pub mod warning; #[doc(inline)] pub use warning::*; -mod toaster; -#[doc(inline)] -pub use toaster::toaster; \ No newline at end of file +pub mod toaster; \ No newline at end of file diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index 134fcbb8..0c6b85a0 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -1,11 +1,14 @@ // Copyright 2024 wiiznokes // SPDX-License-Identifier: MIT +//! A widget that display toats. + +use std::collections::VecDeque; use std::time::Duration; use crate::widget::container; use crate::widget::Column; -use iced::{Command, Length}; +use iced::Command; use iced_core::Element; use widget::Toaster; @@ -15,81 +18,155 @@ use super::{button, row, text}; mod widget; +/// Create a new Toaster widget. pub fn toaster<'a, Message>( toasts: &'a Toasts, content: impl Into>, ) -> Element<'a, Message, crate::Theme, iced::Renderer> where - Message: Clone + 'a, + Message: From + Clone + 'a, { let make_toast = |toast: &'a Toast| { - let row = row().push(text(&toast.message)).push_maybe( - toast - .action - .as_ref() - .map(|t| button(text(t.0.clone())).on_press(t.1.clone())), - ); + let row = row() + .push(text(&toast.message)) + .push_maybe(toast.action.as_ref().map(|action| { + button(text(action.description.clone())).on_press(action.message.clone()) + })) + .push(button("close").on_press(ToastMessage(toast.id).into())) + .align_items(iced::Alignment::Center); - container(row).into() + container(row) + .padding(10) + .style(crate::style::Container::Card) }; - let col = Column::with_children(toasts.toasts.iter().map(make_toast)); + let col = + Column::with_children(toasts.toasts.iter().rev().map(|t| make_toast(t).into())).spacing(5); Toaster::new(col.into(), content.into(), toasts.toasts.is_empty()).into() } +/// Duration for the [`Toast`] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum ToastDuration { + #[default] Short, - Medium, Long, + Custom(Duration), } impl ToastDuration { fn duration(&self) -> Duration { match self { - ToastDuration::Short => Duration::from_millis(200), - ToastDuration::Medium => Duration::from_millis(1000), - ToastDuration::Long => Duration::from_millis(5000), + ToastDuration::Short => Duration::from_millis(2000), + ToastDuration::Long => Duration::from_millis(3500), + ToastDuration::Custom(duration) => *duration, } } } +impl From for ToastDuration { + fn from(value: Duration) -> Self { + Self::Custom(value) + } +} + +/// Action that can be triggered by the user. +/// +/// Example: `undo` +#[derive(Debug, Clone)] +pub struct ToastAction { + pub description: String, + pub message: Message, +} + +/// Represent the data used to display a [`Toast`] +#[derive(Debug, Clone)] pub struct Toast { message: String, - /// (Description, message) - action: Option<(String, Message)>, - + action: Option>, duration: ToastDuration, - id: u32, } -pub struct Toasts { - id_count: u32, - toasts: Vec>, +impl Toast { + /// Construct a new [`Toast`] with the provided message. + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + action: None, + duration: ToastDuration::default(), + id: 0, + } + } + + /// Set the [`ToastAction`] of this [`Toast`] + #[must_use] + pub fn action(mut self, action: ToastAction) -> Self { + self.action.replace(action); + self + } + + /// Set the [`ToastDuration`] of this [`Toast`] + #[must_use] + pub fn duration(mut self, duration: impl Into) -> Self { + self.duration = duration.into(); + self + } } +#[derive(Debug, Clone)] +pub struct Toasts { + id_count: u32, + toasts: VecDeque>, + limit: usize, +} + +// need custom impl to not require Message: Clone +impl Default for Toasts { + fn default() -> Self { + Self { + id_count: 0, + toasts: VecDeque::new(), + limit: 5, + } + } +} + +#[derive(Debug, Clone)] pub struct ToastMessage(u32); impl Toasts { - pub fn push(&mut self, mut toast: Toast) -> Command { + /// Add a new [`Toast`] + pub fn push( + &mut self, + mut toast: Toast, + ) -> Command> + where + Message: From, + { + while self.toasts.len() >= self.limit { + self.toasts.pop_front(); + } + toast.id = self.id_count; self.id_count += 1; let message = ToastMessage(toast.id); let duration = toast.duration.duration(); - self.toasts.push(toast); + self.toasts.push_back(toast); Command::perform( async move { tokio::time::sleep(duration).await; }, - |_| message, + |_| crate::app::Message::App(message.into()), ) } - pub fn handle_message(&mut self, message: ToastMessage) { + /// Handle the [`ToastMessage`] + pub fn handle_message(&mut self, message: &ToastMessage) { self.toasts .iter() .position(|e| e.id == message.0) diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index da3e898d..4ca6d3a4 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -3,22 +3,18 @@ use iced::{Limits, Size}; use iced_core::layout::Node; -use iced_runtime::core::widget::Id; -use iced_runtime::{keyboard, Command}; use iced_core::event::{self, Event}; -use iced_core::renderer::{self, Quad, Renderer}; -use iced_core::widget::tree::{self, Tree}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer::{self}; +use iced_core::widget::tree::Tree; use iced_core::widget::Operation; use iced_core::Element; -use iced_core::{layout, svg}; -use iced_core::{mouse, Border}; -use iced_core::{overlay, Shadow}; -use iced_core::{touch, Overlay}; -use iced_core::{ - Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, -}; -use iced_renderer::core::widget::{operation, OperationOutputWrapper}; +use iced_core::Overlay; +use iced_core::{Clipboard, Layout, Length, Point, Rectangle, Shell, Vector, Widget}; +use iced_renderer::core::widget::OperationOutputWrapper; pub struct Toaster<'a, Message, Theme, Renderer> { toasts: Element<'a, Message, Theme, Renderer>, @@ -145,7 +141,7 @@ where &'b mut self, state: &'b mut Tree, layout: Layout<'_>, - renderer: &Renderer, + _renderer: &Renderer, ) -> Option> { if self.is_empty { None @@ -189,7 +185,23 @@ where position: Point, _translation: Vector, ) -> Node { - todo!() + let limits = Limits::new(Size::ZERO, bounds); + + let mut node = self + .element + .as_widget() + .layout(self.state, renderer, &limits); + + let offset = 15.; + + let position = Point::new( + (bounds.width / 2.) - (node.size().width / 2.), + bounds.height - (node.size().height + offset), + ); + + node.move_to_mut(position); + + node } fn draw(