use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::*;
const DEADLINE: Duration = Duration::from_secs(10);
#[derive(Default, Debug)]
struct Drawn {
texts: Vec<String>,
textures: Vec<[usize; 2]>,
rects: Vec<egui::Rect>,
path: bool,
}
struct Screen {
ctx: egui::Context,
scroll: bool,
}
impl Screen {
fn new() -> Screen {
let ctx = egui::Context::default();
crate::icons::install_loaders(&ctx);
Screen { ctx, scroll: false }
}
fn frame(&self, preview: &mut ImagePreview, path: &Path) -> Drawn {
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(400.0, 300.0),
)),
..Default::default()
};
let mut output = self.ctx.run_ui(input, |ui| {
egui::CentralPanel::default().show(ui, |ui| {
if self.scroll {
egui::ScrollArea::vertical().show(ui, |ui| preview.ui(ui, path));
} else {
preview.ui(ui, path);
}
});
});
output.textures_delta.clear();
let mut drawn = Drawn::default();
let textures = self.ctx.tex_manager();
let textures = textures.read();
for clipped in &output.shapes {
collect(&clipped.shape, &textures, &mut drawn);
}
drawn
}
fn until(
&self,
preview: &mut ImagePreview,
path: &Path,
mut done: impl FnMut(&ImagePreview, &Drawn) -> bool,
) -> Drawn {
let start = Instant::now();
loop {
let drawn = self.frame(preview, path);
if done(preview, &drawn) {
return drawn;
}
assert!(start.elapsed() < DEADLINE, "gave up waiting: {drawn:?}");
std::thread::sleep(Duration::from_millis(5));
}
}
}
fn collect(shape: &egui::Shape, textures: &egui::epaint::TextureManager, drawn: &mut Drawn) {
match shape {
egui::Shape::Vec(shapes) => {
for shape in shapes {
collect(shape, textures, drawn);
}
}
egui::Shape::Text(text) => drawn.texts.push(text.galley.text().to_owned()),
egui::Shape::Mesh(mesh) if mesh.texture_id != egui::TextureId::default() => {
if let Some(meta) = textures.meta(mesh.texture_id) {
drawn.textures.push(meta.size);
drawn.rects.push(mesh.calc_bounds());
}
}
egui::Shape::Path(_) => drawn.path = true,
_ => {}
}
}
fn png(dir: &Path, name: &str, width: u32, height: u32) -> PathBuf {
let path = dir.join(name);
::image::RgbaImage::from_pixel(width, height, ::image::Rgba([200, 40, 40, 255]))
.save(&path)
.expect("write the PNG");
path
}
#[test]
fn a_picture_shows_a_spinner_then_the_image() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "small.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
let first = screen.frame(&mut preview, &path);
assert!(first.path, "no spinner: {first:?}");
assert!(first.textures.is_empty(), "{first:?}");
let drawn = screen.until(&mut preview, &path, |_, d| d.textures == [[64, 32]]);
assert_eq!(drawn.rects[0].size(), egui::vec2(64.0, 32.0));
}
#[test]
fn in_a_scroll_area_it_fits_the_visible_height() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "square.png", 1000, 1000);
let mut screen = Screen::new();
screen.scroll = true;
let mut preview = ImagePreview::new();
let drawn = screen.until(&mut preview, &path, |_, d| !d.textures.is_empty());
let rect = drawn.rects[0];
assert!(rect.top() >= 0.0 && rect.bottom() <= 300.0, "{rect:?}");
assert!((rect.width() - rect.height()).abs() < 0.5, "{rect:?}");
}
#[test]
fn a_newer_selection_wins() {
let dir = tempfile::tempdir().unwrap();
let a = png(dir.path(), "a.png", 3000, 3000);
let b = png(dir.path(), "b.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
screen.frame(&mut preview, &a);
screen.until(&mut preview, &b, |p, d| {
assert!(d.textures.iter().all(|&t| t == [64, 32]), "{d:?}");
p.idle()
});
let drawn = screen.frame(&mut preview, &b);
assert_eq!(drawn.textures, [[64, 32]]);
}
#[test]
fn an_unsupported_file_says_so() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("photo.heic");
let mut bytes = vec![0, 0, 0, 24];
bytes.extend_from_slice(b"ftypheic");
bytes.extend_from_slice(&[0; 12]);
std::fs::write(&path, bytes).unwrap();
let screen = Screen::new();
let mut preview = ImagePreview::new();
screen.until(&mut preview, &path, |_, d| {
d.texts.iter().any(|t| t == "This file can't be previewed.")
});
}
#[test]
fn the_strings_exist_in_both_languages() {
use crate::i18n::{Language, LanguageSettings, check, tr};
assert_eq!(
check::key_difference(&crate::i18n::CORUNDUM, "en", "ja"),
Vec::<String>::new()
);
let ctx = egui::Context::default();
let ids = [
"corundum-preview-unsupported",
"corundum-preview-too-large",
"corundum-preview-failed",
];
let mut english = Vec::new();
for tag in ["en", "ja"] {
crate::i18n::install(
&ctx,
&LanguageSettings {
language: Language::Explicit(tag.into()),
},
);
for id in ids {
let text = tr(&ctx, id);
assert_ne!(text, id, "{tag} lacks {id}");
if tag == "en" {
english.push(text);
} else {
assert!(!english.contains(&text), "{id} is not translated");
}
}
}
assert_eq!(english[0], "This file can't be previewed.");
}
#[test]
fn the_thread_ends_with_the_preview() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "small.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
screen.frame(&mut preview, &path);
let token = preview.thread_token().expect("the thread started");
drop(preview);
let start = Instant::now();
while token.upgrade().is_some() {
assert!(
start.elapsed() < DEADLINE,
"the thread outlived the preview"
);
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn the_target_size_rounds_up_to_a_step() {
use super::image::target;
assert_eq!(target(egui::vec2(100.0, 50.0), 1.0), 256);
assert_eq!(target(egui::vec2(200.0, 300.0), 1.0), 512);
assert_eq!(target(egui::vec2(300.0, 200.0), 2.0), 1024);
assert_eq!(target(egui::vec2(1024.0, 10.0), 1.0), 1024);
assert_eq!(target(egui::vec2(3000.0, 2000.0), 1.0), 2048);
assert_eq!(target(egui::vec2(1500.0, 800.0), 3.0), 2048);
}
#[test]
fn a_file_is_not_looked_at_every_frame() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "small.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
screen.until(&mut preview, &path, |_, d| d.textures == [[64, 32]]);
std::fs::remove_file(&path).unwrap();
for _ in 0..3 {
screen.frame(&mut preview, &path);
}
let drawn = screen.until(&mut preview, &path, |p, _| p.idle());
assert_eq!(drawn.textures, [[64, 32]], "{drawn:?}");
let drawn = screen.frame(&mut preview, &path);
assert_eq!(drawn.textures, [[64, 32]], "{drawn:?}");
}
#[test]
fn a_changed_file_is_shown_anew() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "small.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
preview.set_recheck(Duration::ZERO);
screen.until(&mut preview, &path, |_, d| d.textures == [[64, 32]]);
png(dir.path(), "small.png", 48, 16);
screen.until(&mut preview, &path, |_, d| d.textures == [[48, 16]]);
}
#[test]
fn the_default_limits_suit_a_phone() {
use corundum_framework_image::Limits;
let preview = ImagePreview::new();
assert_eq!(
preview.limits(),
Limits::default().with_max_pixels(64_000_000)
);
}
#[test]
fn a_picture_over_the_limits_says_so() {
use corundum_framework_image::Limits;
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), "small.png", 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new().with_limits(Limits::default().with_max_pixels(100));
screen.until(&mut preview, &path, |_, d| {
assert!(d.textures.is_empty(), "{d:?}");
d.texts
.iter()
.any(|t| t == "This image is too large to preview.")
});
}
#[test]
fn a_panic_while_decoding_fails_once() {
let dir = tempfile::tempdir().unwrap();
let path = png(dir.path(), super::image::PANIC_NAME, 64, 32);
let screen = Screen::new();
let mut preview = ImagePreview::new();
screen.until(&mut preview, &path, |_, d| {
d.texts
.iter()
.any(|t| t.starts_with("Couldn't read this file:") && t.contains("panicked"))
});
for _ in 0..5 {
screen.frame(&mut preview, &path);
std::thread::sleep(Duration::from_millis(5));
}
screen.until(&mut preview, &path, |p, _| p.idle());
assert_eq!(super::image::panics(), 1);
}