use std::time::{Duration, Instant};
use iced::{
advanced::{
graphics::text::Paragraph,
layout,
layout::{Limits, Node},
renderer::{Quad, Style},
text::{LineHeight, Shaping},
widget::{tree::Tag, Tree},
Clipboard, Layout, Renderer as RendererTrait, Shell, Widget,
},
alignment::{Horizontal, Vertical},
event::Status,
font::Weight,
mouse::Cursor,
widget::{text, text::Appearance},
window,
window::RedrawRequest,
Background, Color, Element, Event, Font, Length, Rectangle, Renderer, Size, Theme,
};
use keyframe::{functions::EaseOutQuint, keyframes, AnimationSequence};
use crate::theme::colours::SYSTEM_GRAY6;
pub struct Toast {
pub text: String,
pub start: Instant,
pub ttl: Duration,
}
#[allow(clippy::module_name_repetitions)]
pub struct ToastElement<'a, M> {
toast: &'a Toast,
on_expiry: Option<M>,
}
impl<'a, M: Clone> ToastElement<'a, M> {
pub fn new(toast: &'a Toast) -> Self {
Self {
toast,
on_expiry: None,
}
}
pub fn on_expiry(mut self, msg: M) -> Self {
self.on_expiry = Some(msg);
self
}
fn advance_closing_state(&self, shell: &mut Shell<'_, M>, state: &mut State) {
match &mut state.state {
TickerState::Closing(last_tick, v) => {
if v.finished() {
if let Some(msg) = self.on_expiry.clone() {
shell.publish(msg);
}
state.state = TickerState::Closed;
} else {
v.advance_by(last_tick.elapsed().as_secs_f64());
*last_tick = Instant::now();
shell.request_redraw(RedrawRequest::NextFrame);
}
}
TickerState::Ticking => {
state.state = TickerState::Closing(
Instant::now(),
keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
);
shell.request_redraw(RedrawRequest::NextFrame);
}
TickerState::Closed => {}
}
}
}
impl<'a, M: Clone> Widget<M, Renderer> for ToastElement<'a, M> {
fn on_event(
&mut self,
state: &mut Tree,
event: Event,
_layout: Layout<'_>,
_cursor: Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, M>,
_viewport: &Rectangle,
) -> Status {
if let Event::Window(_, window::Event::RedrawRequested(_)) = event {
if self.toast.start.elapsed() <= self.toast.ttl {
shell.request_redraw(RedrawRequest::NextFrame);
} else {
let state = state.state.downcast_mut::<State>();
self.advance_closing_state(shell, state);
}
Status::Captured
} else {
Status::Ignored
}
}
fn size(&self) -> Size<Length> {
Size::new(Length::Shrink, Length::Shrink)
}
fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
let local_state = tree.state.downcast_mut::<State>();
layout::padded(
limits,
self.size().width,
self.size().height,
[20, 20, 20, 20],
|limits| {
text::layout(
&mut local_state.content,
renderer,
limits,
Length::Shrink,
Length::Shrink,
&self.toast.text,
LineHeight::default(),
None,
Some(Font {
weight: Weight::Normal,
..Font::with_name("Helvetica Neue")
}),
Horizontal::Center,
Vertical::Center,
Shaping::Basic,
)
},
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
style: &Style,
layout: Layout<'_>,
_cursor: Cursor,
viewport: &Rectangle,
) {
let local_state = tree.state.downcast_ref::<State>();
renderer.fill_quad(
Quad {
bounds: layout.bounds(),
border_radius: 20.0.into(),
border_width: 0.0,
border_color: Color::default(),
},
Background::Color(Color {
a: 0.7 * local_state.state.alpha_mut(),
..SYSTEM_GRAY6
}),
);
let remaining_pct = (1.0
- self.toast.start.elapsed().as_secs_f32() / self.toast.ttl.as_secs_f32())
.max(0.0);
if remaining_pct > 0.0 {
let base = layout.bounds();
let timeout_bounds = Rectangle {
x: base.x + 20.0,
y: base.y + base.height - 2.0,
width: (base.width - 20.0) * remaining_pct,
height: 2.0,
};
renderer.fill_quad(
Quad {
bounds: timeout_bounds,
border_radius: 20.0.into(),
border_width: 0.0,
border_color: Color::default(),
},
Background::Color(Color {
a: 0.7,
..Color::WHITE
}),
);
}
let mut children = layout.children();
text::draw(
renderer,
style,
children.next().unwrap(),
&local_state.content,
Appearance {
color: Some(Color {
a: 1.0 * local_state.state.alpha_mut(),
..Color::WHITE
}),
},
viewport,
);
}
fn state(&self) -> iced::advanced::widget::tree::State {
iced::advanced::widget::tree::State::new(State::default())
}
fn tag(&self) -> Tag {
Tag::of::<State>()
}
}
#[derive(Default)]
pub struct State {
content: text::State<Paragraph>,
state: TickerState,
}
#[derive(Default)]
pub enum TickerState {
#[default]
Ticking,
Closing(Instant, AnimationSequence<f32>),
Closed,
}
impl TickerState {
pub fn alpha_mut(&self) -> f32 {
match self {
Self::Ticking => 1.0,
Self::Closing(_, v) => v.now(),
Self::Closed => 0.0,
}
}
}
impl<'a, M> From<ToastElement<'a, M>> for Element<'a, M>
where
M: 'a + Clone,
{
fn from(modal: ToastElement<'a, M>) -> Self {
Element::new(modal)
}
}