nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

#[derive(Clone, Default, Serialize, Deserialize)]
pub struct RecentFiles {
    paths: Vec<PathBuf>,
    max_entries: usize,
}

pub enum RecentFilesAction {
    Open(PathBuf),
    Clear,
}

impl RecentFiles {
    pub fn new(max_entries: usize) -> Self {
        Self {
            paths: Vec::new(),
            max_entries,
        }
    }

    pub fn add(&mut self, path: PathBuf) {
        self.paths.retain(|existing| existing != &path);
        self.paths.insert(0, path);
        self.paths.truncate(self.max_entries);
    }

    pub fn remove(&mut self, path: &PathBuf) {
        self.paths.retain(|existing| existing != path);
    }

    pub fn clear(&mut self) {
        self.paths.clear();
    }

    pub fn paths(&self) -> &[PathBuf] {
        &self.paths
    }

    pub fn is_empty(&self) -> bool {
        self.paths.is_empty()
    }

    pub fn len(&self) -> usize {
        self.paths.len()
    }

    pub fn render_menu(
        &self,
        ui: &mut egui::Ui,
        pinned_path: Option<&str>,
        display_name_fn: impl Fn(&std::path::Path) -> String,
    ) -> Option<RecentFilesAction> {
        let mut action = None;

        ui.label(
            egui::RichText::new("Recent Projects")
                .color(egui::Color32::from_rgb(150, 150, 150))
                .small()
                .strong(),
        );
        ui.add_space(4.0);

        if self.paths.is_empty() {
            ui.label(
                egui::RichText::new("  No recent projects")
                    .color(egui::Color32::from_rgb(120, 120, 120))
                    .small()
                    .italics(),
            );
        } else {
            for path in &self.paths {
                let path_string = path.to_string_lossy().to_string();
                let project_name = display_name_fn(path);
                let is_pinned = pinned_path.is_some_and(|pinned| path_string == pinned);
                let display_name = if project_name.trim().is_empty() {
                    if is_pinned {
                        "⭐ Untitled".to_string()
                    } else {
                        "Untitled".to_string()
                    }
                } else if is_pinned {
                    format!("{}", project_name)
                } else {
                    project_name
                };

                let button = ui
                    .add(
                        egui::Button::new(
                            egui::RichText::new(display_name)
                                .color(egui::Color32::from_rgb(100, 200, 255)),
                        )
                        .min_size(egui::vec2(200.0, 0.0)),
                    )
                    .on_hover_text(&path_string);

                if button.clicked() {
                    action = Some(RecentFilesAction::Open(path.clone()));
                    ui.close();
                }
            }

            ui.add_space(4.0);
            if ui
                .button(
                    egui::RichText::new("Clear Recent Projects")
                        .color(egui::Color32::from_rgb(255, 100, 100))
                        .small(),
                )
                .clicked()
            {
                action = Some(RecentFilesAction::Clear);
                ui.close();
            }
        }

        action
    }
}