Files
figcraft/crates/engine/src/menu_gaps_tests.rs
T
bcedfeba10 M7: recent files as thumbnail cards on the home screen (#249)
The Recents section shows recent files as cards: the picture each file
carries (thumbnail.png in .figcraft and .fig, an image file itself) fitted
on its first page's colour, a divider, and a footer with the file's icon,
name and "Edited ... ago" (or "File not found"); the full path on hover.
Pictures are read off the frame's thread, only for cards in view, cached
by path and modification time, capped in bytes and pixels; missing,
damaged or oversized ones give a placeholder. They fade in, and cards
show a skeleton until their file has been looked at.

A sort menu (Alphabetical, Date created, Last modified; either order) and
a grid/list toggle sit above them. The list is a Name, Last modified and
Created table whose headers sort. View (per section) and sort are kept in
the preferences and set with ui.set {homeRecentsView, homeRecentsSort,
homeRecentsOrder}. A click selects, a double-click or Enter opens, arrow
keys move the selection; a right-click offers Open, Show in folder and
Remove from recents (file.openRecent {remove}).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 12:00:20 +07:00

87 lines
4.2 KiB
Rust

//! File ▸ Open recent (kept in the preferences, capped, missing files dropped), outlines for the
//! selection and mask outlines.
use serde_json::json;
use crate::Session;
use crate::cmd::file::MAX_RECENT;
use crate::params::id_of;
#[test]
fn open_recent_lists_opens_prunes_and_clears() {
let dir = std::env::temp_dir().join(format!("figcraft-recent-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let a = dir.join("a.figcraft").to_string_lossy().to_string();
let b = dir.join("b.figcraft").to_string_lossy().to_string();
let mut s = Session::new();
s.execute("file.save", &json!({"path": a})).unwrap();
s.execute("file.saveAs", &json!({"path": b})).unwrap();
let list = s.execute("file.openRecent", &json!({})).unwrap();
assert_eq!(list["files"][0]["path"], b.as_str(), "newest first");
assert_eq!(list["files"][1]["name"], "a.figcraft");
assert_eq!(list["files"][1]["exists"], true);
// Opening one moves it to the top (an open file is switched to).
s.execute("file.openRecent", &json!({"index": 1})).unwrap();
assert_eq!(s.prefs.recent_files, vec![a.clone(), b.clone()]);
assert!(s.execute("file.openRecent", &json!({"index": 7})).is_err());
// A file that's gone is dropped when it's picked.
std::fs::remove_file(&b).unwrap();
assert!(s.execute("file.openRecent", &json!({"path": b})).is_err());
assert_eq!(s.prefs.recent_files, vec![a.clone()]);
s.execute("file.openRecent", &json!({"clear": true})).unwrap();
assert!(s.prefs.recent_files.is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn one_recent_file_is_forgotten_and_nothing_opens() {
let mut s = Session::empty();
s.prefs.recent_files = vec!["/tmp/a.figcraft".into(), "/tmp/b.fig".into(), "/tmp/c.svg".into()];
let list = s.execute("file.openRecent", &json!({"remove": "/tmp/b.fig"})).unwrap();
assert_eq!(s.prefs.recent_files, vec!["/tmp/a.figcraft".to_string(), "/tmp/c.svg".to_string()]);
assert_eq!(list["files"].as_array().map(Vec::len), Some(2), "the list as it is now");
assert!(s.docs.is_empty(), "nothing opens");
// A path that isn't in the list changes nothing.
s.execute("file.openRecent", &json!({"remove": "/tmp/zzz.fig"})).unwrap();
assert_eq!(s.prefs.recent_files.len(), 2);
}
#[test]
fn recent_files_are_capped() {
let dir = std::env::temp_dir().join(format!("figcraft-recent-cap-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut s = Session::new();
for i in 0..MAX_RECENT + 3 {
let p = dir.join(format!("{i}.figcraft")).to_string_lossy().to_string();
s.execute("file.saveAs", &json!({"path": p})).unwrap();
}
assert_eq!(s.prefs.recent_files.len(), MAX_RECENT);
assert!(s.prefs.recent_files[0].ends_with(&format!("{}.figcraft", MAX_RECENT + 2)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn view_toggles_flip_their_flags() {
let mut s = Session::new();
assert_eq!(s.execute("view.outlinesSelection", &json!({})).unwrap()["on"], true);
assert!(s.ds().unwrap().view.outlines_selection);
assert_eq!(s.execute("view.maskOutlines", &json!({"on": true})).unwrap()["on"], true);
assert_eq!(s.execute("view.maskOutlines", &json!({})).unwrap()["on"], false);
}
#[test]
fn outlined_layers_draw_as_outlines_and_the_rest_normally() {
let mut s = Session::new();
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap()["id"]).unwrap();
s.execute("shape.rectangle", &json!({"x": 60, "y": 0, "width": 40, "height": 40})).unwrap();
let ds = s.ds().unwrap();
let view = figcraft_render::View { origin: (0.0, 0.0), scale: 1.0, width: 120, height: 60 };
let render =
|outlined| figcraft_render::render_view(&ds.doc, ds.page, &view, &figcraft_render::Options { outlined, ..Default::default() }).unwrap();
let (plain, outlined) = (render(vec![]), render(vec![a]));
let empty = plain.pixel(110, 50).unwrap();
assert_ne!(plain.pixel(20, 20).unwrap(), empty, "a filled rectangle");
assert_eq!(outlined.pixel(20, 20).unwrap(), empty, "only its outline");
assert_eq!(outlined.pixel(80, 20).unwrap(), plain.pixel(80, 20).unwrap(), "the other layer draws normally");
}