Files
wayvr/dash-frontend/src/util/toast_manager.rs

191 lines
4.3 KiB
Rust

use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use glam::{Mat4, Vec3};
use wgui::{
animation::{Animation, AnimationEasing},
components::tooltip::{TOOLTIP_BORDER_COLOR, TOOLTIP_COLOR},
drawing::Color,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
renderer_vk::{
text::{FontWeight, TextStyle},
util::centered_matrix,
},
taffy::{
self,
prelude::{length, percent},
},
widget::{
div::WidgetDiv,
label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams},
util::WLength,
},
};
struct MountedToast {
#[allow(dead_code)]
id_root: WidgetID, // decorations of a toast
layout_tasks: LayoutTasks,
}
struct State {
toast: Option<MountedToast>,
queue: VecDeque<Translation>,
timeout: u32, // in ticks
}
pub struct ToastManager {
state: Rc<RefCell<State>>,
needs_tick: bool,
}
impl Drop for MountedToast {
fn drop(&mut self) {
self.layout_tasks.push(LayoutTask::RemoveWidget(self.id_root));
}
}
const TOAST_DURATION_TICKS: u32 = 90;
impl ToastManager {
pub fn new() -> Self {
Self {
state: Rc::new(RefCell::new(State {
toast: None,
timeout: 0,
queue: VecDeque::new(),
})),
needs_tick: false,
}
}
fn mount_toast(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
state: &mut State,
content: Translation,
) -> anyhow::Result<()> {
let mut globals = globals.get();
let (root, _) = layout.add_topmost_child(
WidgetDiv::create(),
taffy::Style {
position: taffy::Position::Absolute,
size: taffy::Size {
width: percent(1.0),
height: percent(0.8),
},
align_items: Some(taffy::AlignItems::End),
justify_content: Some(taffy::JustifyContent::Center),
..Default::default()
},
)?;
let (rect, _) = layout.add_child(
root.id,
WidgetRectangle::create(WidgetRectangleParams {
color: TOOLTIP_COLOR,
border_color: TOOLTIP_BORDER_COLOR,
border: 2.0,
round: WLength::Percent(1.0),
..Default::default()
}),
taffy::Style {
position: taffy::Position::Relative,
gap: length(4.0),
padding: taffy::Rect {
left: length(16.0),
right: length(16.0),
top: length(8.0),
bottom: length(8.0),
},
..Default::default()
},
)?;
let (label, _) = layout.add_child(
rect.id,
WidgetLabel::create(
&mut globals,
WidgetLabelParams {
content,
style: TextStyle {
weight: Some(FontWeight::Bold),
..Default::default()
},
},
),
taffy::Style { ..Default::default() },
)?;
// show-up animation
layout.animations.add(Animation::new(
rect.id,
160, // does not use anim_mult
AnimationEasing::Linear,
Box::new(move |common, data| {
let pos_showup = AnimationEasing::OutQuint.interpolate((data.pos * 4.0).min(1.0));
let opacity = 1.0 - AnimationEasing::OutQuint.interpolate(((data.pos - 0.75) * 4.0).clamp(0.0, 1.0));
let scale = AnimationEasing::OutBack.interpolate((data.pos * 4.0).min(1.0));
{
let mtx = Mat4::from_translation(Vec3::new(0.0, (1.0 - pos_showup) * 100.0, 0.0))
* Mat4::from_scale(Vec3::new(scale, scale, 1.0));
data.data.transform = centered_matrix(data.widget_boundary.size, &mtx);
}
let rect = data.obj.get_as_mut::<WidgetRectangle>().unwrap();
rect.params.color.a = opacity;
rect.params.border_color.a = opacity;
let mut label = common.state.widgets.get_as::<WidgetLabel>(label.id).unwrap();
label.set_color(common, Color::new(1.0, 1.0, 1.0, opacity), true);
common.alterables.mark_redraw();
}),
));
state.toast = Some(MountedToast {
id_root: root.id,
layout_tasks: layout.tasks.clone(),
});
Ok(())
}
pub fn tick(&mut self, globals: &WguiGlobals, layout: &mut Layout) -> anyhow::Result<()> {
if !self.needs_tick {
return Ok(());
}
let mut state = self.state.borrow_mut();
if state.timeout > 0 {
state.timeout -= 1;
}
if state.timeout == 0 {
state.toast = None;
state.timeout = TOAST_DURATION_TICKS;
// mount next
if let Some(content) = state.queue.pop_front() {
self.mount_toast(globals, layout, &mut state, content)?;
}
}
if state.queue.is_empty() && state.toast.is_none() {
self.needs_tick = false;
}
Ok(())
}
pub fn push(&mut self, content: Translation) {
let mut state = self.state.borrow_mut();
state.queue.push_back(content);
self.needs_tick = true;
}
}