nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use std::collections::VecDeque;

use crate::prelude::*;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ToastKind {
    Info,
    Warning,
    Error,
    Success,
}

impl ToastKind {
    fn accent_color(self) -> egui::Color32 {
        match self {
            ToastKind::Info => egui::Color32::from_rgb(100, 160, 255),
            ToastKind::Warning => egui::Color32::from_rgb(255, 200, 60),
            ToastKind::Error => egui::Color32::from_rgb(255, 80, 80),
            ToastKind::Success => egui::Color32::from_rgb(80, 200, 120),
        }
    }
}

struct ToastEntry {
    message: String,
    kind: ToastKind,
    spawn_time: f32,
    duration: f32,
}

pub struct Toasts {
    entries: VecDeque<ToastEntry>,
    elapsed_time: f32,
}

impl Default for Toasts {
    fn default() -> Self {
        Self::new()
    }
}

impl Toasts {
    pub fn new() -> Self {
        Self {
            entries: VecDeque::new(),
            elapsed_time: 0.0,
        }
    }

    pub fn push(&mut self, kind: ToastKind, message: impl Into<String>, duration: f32) {
        self.entries.push_back(ToastEntry {
            message: message.into(),
            kind,
            spawn_time: self.elapsed_time,
            duration,
        });
    }

    pub fn tick(&mut self, delta_time: f32) {
        self.elapsed_time += delta_time;
        while let Some(front) = self.entries.front() {
            if self.elapsed_time - front.spawn_time >= front.duration {
                self.entries.pop_front();
            } else {
                break;
            }
        }
    }

    pub fn render(&self, ui_context: &egui::Context) {
        if self.entries.is_empty() {
            return;
        }

        egui::Area::new(egui::Id::new("toast_area"))
            .anchor(egui::Align2::RIGHT_BOTTOM, egui::vec2(-16.0, -16.0))
            .order(egui::Order::Foreground)
            .show(ui_context, |ui| {
                ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| {
                    for (index, entry) in self.entries.iter().enumerate() {
                        let age = self.elapsed_time - entry.spawn_time;
                        let fade_start = entry.duration - 0.5;
                        let alpha = if age > fade_start {
                            ((entry.duration - age) / 0.5).clamp(0.0, 1.0)
                        } else {
                            1.0
                        };

                        let bg_alpha = (200.0 * alpha) as u8;
                        let text_alpha = (255.0 * alpha) as u8;
                        let accent = entry.kind.accent_color();

                        egui::Frame::NONE
                            .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 50, bg_alpha))
                            .inner_margin(egui::Margin::symmetric(12, 8))
                            .corner_radius(egui::CornerRadius::same(6))
                            .stroke(egui::Stroke::new(
                                1.0,
                                egui::Color32::from_rgba_unmultiplied(
                                    accent.r(),
                                    accent.g(),
                                    accent.b(),
                                    bg_alpha,
                                ),
                            ))
                            .show(ui, |ui| {
                                ui.horizontal(|ui| {
                                    ui.painter().rect_filled(
                                        egui::Rect::from_min_size(
                                            ui.cursor().left_top() - egui::vec2(8.0, 0.0),
                                            egui::vec2(3.0, 20.0),
                                        ),
                                        egui::CornerRadius::same(1),
                                        egui::Color32::from_rgba_unmultiplied(
                                            accent.r(),
                                            accent.g(),
                                            accent.b(),
                                            text_alpha,
                                        ),
                                    );
                                    ui.label(
                                        egui::RichText::new(&entry.message)
                                            .color(egui::Color32::from_rgba_unmultiplied(
                                                230, 230, 240, text_alpha,
                                            ))
                                            .size(14.0),
                                    );
                                });
                            });

                        if index < self.entries.len() - 1 {
                            ui.add_space(4.0);
                        }
                    }
                });
            });

        ui_context.request_repaint();
    }
}