dash-frontend: toast messages, 📦📎
This commit is contained in:
190
dash-frontend/src/util/toast_manager.rs
Normal file
190
dash-frontend/src/util/toast_manager.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user