Skip to main content

launcher/utils/
texture_loader.rs

1use eframe::egui;
2use std::path::{Path, PathBuf};
3
4/// Texture loader with multiple fallbacks
5///
6/// # Errors
7///
8/// Returns an error if the texture file cannot be found, read, or decoded.
9pub fn load_texture_robust(
10    ctx: &egui::Context,
11    path: &Path,
12    name: &str,
13) -> Result<egui::TextureHandle, Box<dyn std::error::Error>> {
14    // 1. Resolve actual file path (recursive search for highest res)
15    let actual_path = find_texture_file(path).ok_or_else(|| {
16        std::io::Error::new(
17            std::io::ErrorKind::NotFound,
18            format!("Texture not found: {}", path.display()),
19        )
20    })?;
21
22    // 2. Read file data
23    let file_data = std::fs::read(&actual_path).map_err(|e| {
24        eprintln!("Failed to read texture file {}: {e}", actual_path.display());
25        e
26    })?;
27
28    // 3. Try decoding.
29    let image_result = image::load_from_memory(&file_data).or_else(|_| {
30        let format = if actual_path
31            .extension()
32            .is_some_and(|e| e.eq_ignore_ascii_case("tga"))
33        {
34            image::ImageFormat::Tga
35        } else {
36            image::ImageFormat::from_path(&actual_path).unwrap_or(image::ImageFormat::Tga)
37        };
38        image::load_from_memory_with_format(&file_data, format)
39    });
40
41    let color_image = image_result
42        .map(|img| {
43            let size = [img.width() as usize, img.height() as usize];
44            let pixels = img.to_rgba8();
45            egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_flat_samples().as_slice())
46        })
47        .map_err(|e| {
48            eprintln!("Failed to decode texture {}: {e}", actual_path.display());
49            e
50        })?;
51
52    Ok(ctx.load_texture(name, color_image, egui::TextureOptions::LINEAR))
53}
54
55/// Find texture file, resolving case sensitivity and searching subdirectories
56#[must_use]
57pub fn find_texture_file(target_path: &Path) -> Option<PathBuf> {
58    let target_filename = target_path
59        .file_name()
60        .and_then(|n| n.to_str())?
61        .to_lowercase();
62
63    // Start search from the parent directory of the requested texture path
64    if let Some(parent) = target_path.parent()
65        && parent.exists()
66    {
67        let mut candidates = Vec::new();
68        collect_candidates_recursive(parent, &target_filename, &mut candidates);
69
70        if !candidates.is_empty() {
71            // Sort descending by size to pick the best "high-res" variant
72            candidates.sort_by_key(|b| std::cmp::Reverse(b.file_size));
73            return Some(candidates[0].path.clone());
74        }
75    }
76
77    None
78}
79
80struct FileCandidate {
81    path: PathBuf,
82    file_size: u64,
83}
84
85fn collect_candidates_recursive(
86    dir: &Path,
87    target_filename: &str,
88    candidates: &mut Vec<FileCandidate>,
89) {
90    if let Ok(entries) = std::fs::read_dir(dir) {
91        for entry in entries.flatten() {
92            let path = entry.path();
93            if path.is_dir() {
94                collect_candidates_recursive(&path, target_filename, candidates);
95            } else if path.is_file()
96                && let Some(filename) = path.file_name().and_then(|n| n.to_str())
97                && filename.to_lowercase() == target_filename
98                && let Ok(metadata) = path.metadata()
99            {
100                candidates.push(FileCandidate {
101                    path,
102                    file_size: metadata.len(),
103                });
104            }
105        }
106    }
107}