From 662df66f1c08bdb1b1f1d0c1c638c4cef936d0b0 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Mon, 26 Oct 2026 20:50:41 -0600 Subject: [PATCH] Preserve queued file drops across dialogs and restore Flatpak sources (#2975) --- .github/workflows/sonarcloud.yml | 8 +- apps/lightcraft-cli/tests/it/cli.rs | 86 +- apps/lightcraft-cli/tests/it/main.rs | 5 + apps/lightcraft-cli/tests/it/mcp_shutdown.rs | 8 +- apps/lightcraft-web/Cargo.toml | 3 + apps/lightcraft-web/src/drop_gate.rs | 391 + apps/lightcraft-web/src/lib.rs | 4 + apps/lightcraft-web/src/web.rs | 47 +- crates/engine/src/cmd/activity.rs | 2 +- crates/engine/src/cmd/previews.rs | 143 +- crates/engine/src/tests.rs | 47 +- crates/engine/src/tests/face_recognize.rs | 127 +- crates/mcp/tests/it/e2e.rs | 56 +- crates/raw/src/preview.rs | 50 +- crates/raw/src/preview/tests.rs | 92 +- .../ui-egui/examples/thumb_request_bench.rs | 2 +- crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/app/frame.rs | 12 +- crates/ui-egui/src/app/input.rs | 48 +- crates/ui-egui/src/app/screenshot.rs | 45 +- .../ui-egui/src/dialogs/slideshow_export.rs | 26 +- crates/ui-egui/src/drop_overlay.rs | 18 +- crates/ui-egui/src/headless/tests.rs | 38 +- crates/ui-egui/src/lib.rs | 6 + crates/ui-egui/src/panels/detail.rs | 9 +- crates/ui-egui/src/panels/grid.rs | 13 +- crates/ui-egui/src/panels/library_problem.rs | 24 + crates/ui-egui/src/render.rs | 179 +- crates/ui-egui/src/render/thumbnail_tests.rs | 420 +- crates/ui-egui/src/sync.rs | 22 + crates/ui-egui/src/tests.rs | 3 + crates/ui-egui/src/tests/activity.rs | 21 +- crates/ui-egui/src/tests/before_after.rs | 29 +- crates/ui-egui/src/tests/capture_time.rs | 14 +- crates/ui-egui/src/tests/crop_rotate.rs | 23 +- crates/ui-egui/src/tests/dialog_settings.rs | 32 +- .../ui-egui/src/tests/docking_acceptance.rs | 17 +- crates/ui-egui/src/tests/drop_import.rs | 755 +- crates/ui-egui/src/tests/filmstrip.rs | 15 +- crates/ui-egui/src/tests/fixtures.rs | 86 + crates/ui-egui/src/tests/keyword_files.rs | 34 +- crates/ui-egui/src/tests/library_problem.rs | 38 +- crates/ui-egui/src/tests/lifecycle_037.rs | 113 +- crates/ui-egui/src/tests/panels.rs | 8 +- crates/ui-egui/src/tests/preset_import.rs | 11 +- crates/ui-egui/src/tests/preview_limit.rs | 398 +- crates/ui-egui/src/tests/print.rs | 9 +- crates/ui-egui/src/tests/render_fixtures.rs | 179 + crates/ui-egui/src/tests/slideshow.rs | 131 +- crates/ui-egui/src/tests/zoom.rs | 67 + crates/ui-egui/tests/it/docking_workflows.rs | 15 +- packaging/linux/flatpak/README.md | 120 + .../flatpak/ai.storyteller.lightcraft.yml | 7 +- packaging/linux/flatpak/cargo-sources.json | 8593 +++++++++++++++++ .../linux/flatpak/generate-cargo-sources.py | 398 + .../flatpak/test_generate_cargo_sources.py | 483 + sonar-project.properties | 6 + 57 files changed, 13099 insertions(+), 438 deletions(-) create mode 100644 apps/lightcraft-web/src/drop_gate.rs create mode 100644 crates/ui-egui/src/tests/fixtures.rs create mode 100644 crates/ui-egui/src/tests/render_fixtures.rs create mode 100644 packaging/linux/flatpak/README.md create mode 100644 packaging/linux/flatpak/cargo-sources.json create mode 100755 packaging/linux/flatpak/generate-cargo-sources.py create mode 100644 packaging/linux/flatpak/test_generate_cargo_sources.py diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e78fe1ea3b..d13c3cb4a7 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -16,6 +16,12 @@ concurrency: jobs: sonarcloud: - # Tracked at @main so org-wide CI improvements propagate without PR churn. + # Track shared CI improvements on main. uses: storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main + with: + shards: 8 + python-coverage-source: packaging/linux/flatpak + python-coverage-script: packaging/linux/flatpak/test_generate_cargo_sources.py + timeout-minutes: 120 + fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 secrets: inherit diff --git a/apps/lightcraft-cli/tests/it/cli.rs b/apps/lightcraft-cli/tests/it/cli.rs index 1c8910d9d8..8a11f5c9ab 100644 --- a/apps/lightcraft-cli/tests/it/cli.rs +++ b/apps/lightcraft-cli/tests/it/cli.rs @@ -5,7 +5,7 @@ use std::process::{Command, Stdio}; use serde_json::{Value, json}; -const BIN: &str = env!("CARGO_BIN_EXE_lightcraft-cli"); +use super::binary; #[test] fn contact_sheet_exports_pdf_and_protects_originals() { @@ -106,7 +106,7 @@ fn calibrate_reads_raf_inputs_and_reports_bad_files() { let input = tmp("invalid.RAF"); std::fs::write(&input, b"FUJIFILMCCD-RAW").unwrap(); let out = tmp("raf-profiles"); - let result = Command::new(BIN).args(["calibrate", "--out"]).arg(&out).arg(&input).output().unwrap(); + let result = Command::new(binary()).args(["calibrate", "--out"]).arg(&out).arg(&input).output().unwrap(); assert!(!result.status.success()); let stderr = String::from_utf8_lossy(&result.stderr); assert!(stderr.contains("1 of 1 raw files"), "RAF was silently ignored: {stderr}"); @@ -124,8 +124,13 @@ fn mean(path: &std::path::Path) -> f64 { fn mcp_over_stdio_sets_exposure_and_renders() { let input = tmp("in.png"); gradient_png(&input); - let mut child = - Command::new(BIN).args(["mcp", input.to_str().unwrap()]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); + let mut child = Command::new(binary()) + .args(["mcp", input.to_str().unwrap()]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); let mut stdin = child.stdin.take().unwrap(); let mut stdout = BufReader::new(child.stdout.take().unwrap()); let mut rpc = |id: u64, method: &str, params: Value| -> Value { @@ -163,9 +168,9 @@ fn render_subcommand() { gradient_png(&input); let plain = tmp("r-plain.png"); let out = tmp("r-out.jpg"); - let st = Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", plain.to_str().unwrap()]).status().unwrap(); + let st = Command::new(binary()).args(["render", input.to_str().unwrap(), "-o", plain.to_str().unwrap()]).status().unwrap(); assert!(st.success()); - let st = Command::new(BIN) + let st = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "light.exposure=1", "--size", "60"]) .status() .unwrap(); @@ -176,7 +181,7 @@ fn render_subcommand() { assert_eq!((d.width, d.height), (120, 80)); assert!(mean(&out) > mean(&plain) + 10.0); // Bad control id fails cleanly. - let o = Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "nope=1"]).output().unwrap(); + let o = Command::new(binary()).args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "nope=1"]).output().unwrap(); assert!(!o.status.success()); assert!(String::from_utf8_lossy(&o.stderr).contains("unknown control")); } @@ -204,10 +209,10 @@ fn render_settings_applies_an_lcpreset_and_refuses_unknown_files() { ); assert!(ok, "{err}: {lines:?}"); let plain = tmp("lcpreset-plain.png"); - let st = Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", plain.to_str().unwrap()]).status().unwrap(); + let st = Command::new(binary()).args(["render", input.to_str().unwrap(), "-o", plain.to_str().unwrap()]).status().unwrap(); assert!(st.success()); let bright = tmp("lcpreset-bright.png"); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", bright.to_str().unwrap(), "--settings", preset.to_str().unwrap()]) .output() .unwrap(); @@ -217,7 +222,7 @@ fn render_settings_applies_an_lcpreset_and_refuses_unknown_files() { let other = tmp("other.lcpreset"); std::fs::write(&other, r#"{"format": "other", "version": 1, "presets": []}"#).unwrap(); let out = tmp("lcpreset-other.png"); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--settings", other.to_str().unwrap()]) .output() .unwrap(); @@ -236,8 +241,10 @@ fn render_refuses_to_overwrite_its_input() { std::fs::create_dir_all(dir.join("sub")).unwrap(); let other_spelling = dir.join("sub/../self-in.png"); for out in [&input, &other_spelling] { - let o = - Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "light.exposure=1"]).output().unwrap(); + let o = Command::new(binary()) + .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "light.exposure=1"]) + .output() + .unwrap(); assert!(!o.status.success()); assert!(String::from_utf8_lossy(&o.stderr).contains("never writes over an original"), "{}", String::from_utf8_lossy(&o.stderr)); } @@ -249,7 +256,7 @@ fn render_export_options() { let input = tmp("o-in.png"); gradient_png(&input); let out = tmp("o-out.tif"); - let st = Command::new(BIN) + let st = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap()]) .args(["--opt", "shortEdge=40", "--opt", "colorSpace=displayP3", "--opt", "bitDepth=16"]) .status() @@ -260,7 +267,7 @@ fn render_export_options() { assert_eq!(d.space.named, Some(lightcraft_codecs::NamedSpace::DisplayP3)); // original + sidecar named after the output let out = tmp("o-copy.png"); - let st = Command::new(BIN) + let st = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap(), "--set", "light.exposure=1", "--opt", "format=original"]) .status() .unwrap(); @@ -269,26 +276,26 @@ fn render_export_options() { let xmp = std::fs::read_to_string(out.with_extension("xmp")).unwrap(); assert!(xmp.contains("exposure"), "{xmp}"); // unknown extension fails with a hint - let o = Command::new(BIN).args(["render", input.to_str().unwrap(), "-o", tmp("x.bmp").to_str().unwrap()]).output().unwrap(); + let o = Command::new(binary()).args(["render", input.to_str().unwrap(), "-o", tmp("x.bmp").to_str().unwrap()]).output().unwrap(); assert!(!o.status.success() && String::from_utf8_lossy(&o.stderr).contains("unknown extension")); } #[test] fn commands_subcommand_lists_registry() { - let o = Command::new(BIN).args(["commands", "--json"]).output().unwrap(); + let o = Command::new(binary()).args(["commands", "--json"]).output().unwrap(); assert!(o.status.success()); let v: Value = serde_json::from_slice(&o.stdout).unwrap(); let ids: Vec<&str> = v.as_array().unwrap().iter().filter_map(|c| c["id"].as_str()).collect(); for id in ["develop.set", "photo.rate", "library.import", "edit.undo", "app.export"] { assert!(ids.contains(&id), "{id}"); } - let o = Command::new(BIN).arg("controls").output().unwrap(); + let o = Command::new(binary()).arg("controls").output().unwrap(); assert!(String::from_utf8_lossy(&o.stdout).contains("light.exposure")); } #[test] fn commands_subcommand_lists_commands_sorted_by_id() { - let o = Command::new(BIN).args(["commands", "--json"]).output().unwrap(); + let o = Command::new(binary()).args(["commands", "--json"]).output().unwrap(); assert!(o.status.success()); let v: Value = serde_json::from_slice(&o.stdout).unwrap(); let cmds = v.as_array().unwrap(); @@ -302,7 +309,7 @@ fn commands_subcommand_lists_commands_sorted_by_id() { let unsorted = ids.windows(2).find(|w| w[0] >= w[1]); assert!(unsorted.is_none(), "--json should list commands sorted by id, but {unsorted:?} are out of order"); - let o = Command::new(BIN).arg("commands").output().unwrap(); + let o = Command::new(binary()).arg("commands").output().unwrap(); assert!(o.status.success()); let text = String::from_utf8_lossy(&o.stdout); let lines: Vec<&str> = text.lines().collect(); @@ -338,7 +345,7 @@ fn snapshot_subcommand_renders_the_ui_headlessly() { ), ) .unwrap(); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["snapshot", "--demo", "--script", script.to_str().unwrap(), "-o", a.to_str().unwrap(), "--size", "640x400"]) .output() .unwrap(); @@ -353,7 +360,7 @@ fn snapshot_subcommand_renders_the_ui_headlessly() { assert_eq!(mean(p) < mean(&a) - 2.0, dimmed, "{}", p.display()); } // no script: one settled screenshot, at 2× scale - let o = Command::new(BIN).args(["snapshot", "-o", a.to_str().unwrap(), "--size", "320x240", "--scale", "2"]).output().unwrap(); + let o = Command::new(binary()).args(["snapshot", "-o", a.to_str().unwrap(), "--size", "320x240", "--scale", "2"]).output().unwrap(); assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); let d = lightcraft_codecs::decode(&std::fs::read(&a).unwrap(), Default::default()).unwrap(); assert_eq!((d.width, d.height), (640, 480)); @@ -375,7 +382,7 @@ fn snapshot_script_failure_exits_non_zero() { ) .unwrap(); let _ = std::fs::remove_file(&out); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["snapshot", "--demo", "--script", script.to_str().unwrap(), "-o", out.to_str().unwrap(), "--size", "320x200"]) .output() .unwrap(); @@ -400,7 +407,7 @@ fn snapshot_script_with_non_object_params_is_an_error() { let script = tmp("snap-bad-params.jsonl"); let out = tmp("snap-bad-params.png"); std::fs::write(&script, format!("{}\n", json!({"method": "ui.screenshot", "params": [1]}))).unwrap(); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["snapshot", "--demo", "--script", script.to_str().unwrap(), "-o", out.to_str().unwrap(), "--size", "320x200"]) .output() .unwrap(); @@ -417,7 +424,7 @@ fn snapshot_ui_zoom_keeps_requested_pixel_dimensions() { .unwrap(); for (size, scale, expected) in [("400x240", "1", (400, 240)), ("345x200", "0.9", (311, 180))] { let out = tmp(&format!("snap-ui-zoom-{scale}.png")); - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["snapshot", "--script", script.to_str().unwrap(), "-o", out.to_str().unwrap(), "--size", size, "--scale", scale]) .output() .unwrap(); @@ -440,7 +447,7 @@ fn snapshot_defaults_to_cpu_without_gpu_environment_overrides() { ) .unwrap(); let image = tmp("default-cpu.png"); - let output = Command::new(BIN) + let output = Command::new(binary()) .env_remove("LIGHTCRAFT_GPU") .env_remove("LIGHTCRAFT_GPU_BACKEND") .env_remove("WGPU_BACKEND") @@ -476,7 +483,7 @@ fn snapshot_starts_without_a_gpu() { .unwrap(); for (var, value, reason) in [("LIGHTCRAFT_GPU", "0", "LIGHTCRAFT_GPU=0"), ("LIGHTCRAFT_GPU_BACKEND", "off", "LIGHTCRAFT_GPU_BACKEND=off")] { let out = tmp(&format!("nogpu-{var}.png")); - let o = Command::new(BIN) + let o = Command::new(binary()) .env_remove("LIGHTCRAFT_GPU") .env_remove("LIGHTCRAFT_GPU_BACKEND") .env(var, value) @@ -496,7 +503,7 @@ fn snapshot_starts_without_a_gpu() { } fn run_cli(args: &[&str], stdin: Option<&str>) -> (bool, Vec, String) { - let mut child = Command::new(BIN).arg("run").args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); + let mut child = Command::new(binary()).arg("run").args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); if let Some(text) = stdin { child.stdin.take().unwrap().write_all(text.as_bytes()).unwrap(); } @@ -561,7 +568,7 @@ fn devices_are_listed_and_imported_from() { let _ = std::fs::remove_dir_all(&base); std::fs::create_dir_all(base.join("CARD/DCIM/100TEST")).unwrap(); gradient_png(&base.join("CARD/DCIM/100TEST/IMG_0001.png")); - let o = Command::new(BIN).env("LIGHTCRAFT_DEVICE_ROOTS", &base).args(["run", "--demo", "library.devices"]).output().unwrap(); + let o = Command::new(binary()).env("LIGHTCRAFT_DEVICE_ROOTS", &base).args(["run", "--demo", "library.devices"]).output().unwrap(); assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); let line: Value = serde_json::from_slice(o.stdout.split(|b| *b == b'\n').next().unwrap()).unwrap(); let dev = &line["result"][0]; @@ -570,7 +577,7 @@ fn devices_are_listed_and_imported_from() { let dcim = dev["path"].as_str().unwrap().to_string(); // JSON-escaped: a Windows path has backslashes let paths = serde_json::to_string(&[&dcim]).unwrap(); - let o = Command::new(BIN).args(["run", "library.importPreview", &format!("paths={paths}")]).output().unwrap(); + let o = Command::new(binary()).args(["run", "library.importPreview", &format!("paths={paths}")]).output().unwrap(); let line: Value = serde_json::from_slice(o.stdout.split(|b| *b == b'\n').next().unwrap()).unwrap(); assert_eq!(line["result"]["candidates"].as_array().map(Vec::len), Some(1), "{line}"); } @@ -583,7 +590,7 @@ fn a_library_open_in_another_process_is_refused() { let _ = std::fs::remove_dir_all(&lib); let lib_s = lib.to_str().unwrap(); let mut holder = - Command::new(BIN).args(["mcp", "--library", lib_s]).stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::piped()).spawn().unwrap(); + Command::new(binary()).args(["mcp", "--library", lib_s]).stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::piped()).spawn().unwrap(); let mut err = BufReader::new(holder.stderr.take().unwrap()); let mut line = String::new(); while !line.contains("opened library") { @@ -643,7 +650,7 @@ fn warnings_are_logged_on_stderr() { gradient_png(&input); let out = tmp("log-out.jpg"); let run = |level: &str| { - let o = Command::new(BIN) + let o = Command::new(binary()) .args(["render", input.to_str().unwrap(), "-o", out.to_str().unwrap()]) .env("LIGHTCRAFT_GPU_BACKEND", "bogus") .env("RUST_LOG", level) @@ -667,7 +674,7 @@ fn snapshot_rejects_invalid_dimensions_before_creating_an_image() { [("NaNx100", "1"), ("100x100", "inf"), ("9000x9000", "1"), ("9000x100", "2"), ("100x100", "0.001")].into_iter().enumerate() { let image = tmp(&format!("invalid-viewport-{index}.png")); - let output = Command::new(BIN).args(["snapshot", "--size", size, "--scale", scale, "-o"]).arg(&image).output().unwrap(); + let output = Command::new(binary()).args(["snapshot", "--size", size, "--scale", scale, "-o"]).arg(&image).output().unwrap(); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("snapshot: bad --size/--scale"), "{}", String::from_utf8_lossy(&output.stderr)); assert!(!image.exists()); @@ -677,8 +684,12 @@ fn snapshot_rejects_invalid_dimensions_before_creating_an_image() { #[test] fn snapshot_preserves_fractional_scale_rounding() { let image = tmp("fractional-scale-rounding.png"); - let output = - Command::new(BIN).env("LIGHTCRAFT_GPU", "0").args(["snapshot", "--size", "345x200", "--scale", "0.9", "-o"]).arg(&image).output().unwrap(); + let output = Command::new(binary()) + .env("LIGHTCRAFT_GPU", "0") + .args(["snapshot", "--size", "345x200", "--scale", "0.9", "-o"]) + .arg(&image) + .output() + .unwrap(); assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); let decoded = lightcraft_codecs::decode(&std::fs::read(image).unwrap(), Default::default()).unwrap(); assert_eq!((decoded.width, decoded.height), (311, 180)); @@ -709,7 +720,7 @@ fn snapshot_leaves_desktop_gpu_preferences_untouched() { .unwrap(); for gpu_environment in [None, Some("1")] { let image = dir.join(format!("{}.png", gpu_environment.unwrap_or("default"))); - let mut cmd = Command::new(BIN); + let mut cmd = Command::new(binary()); cmd.env("XDG_CONFIG_HOME", &config).env_remove("LIGHTCRAFT_GPU").env_remove("LIGHTCRAFT_GPU_BACKEND").env_remove("WGPU_BACKEND"); if let Some(value) = gpu_environment { cmd.env("LIGHTCRAFT_GPU", value); @@ -770,13 +781,14 @@ fn denoise_serve_runs_tiles_for_another_computer() { let sha = lightcraft_denoise::hash::sha256_file(&onnx).unwrap(); // without its token the server doesn't start - let o = Command::new(BIN).args(["denoise-serve", "--models", models.to_str().unwrap(), "--token-env", "LC_TEST_NO_SUCH_VAR"]).output().unwrap(); + let o = + Command::new(binary()).args(["denoise-serve", "--models", models.to_str().unwrap(), "--token-env", "LC_TEST_NO_SUCH_VAR"]).output().unwrap(); assert!(!o.status.success() && String::from_utf8_lossy(&o.stderr).contains("LC_TEST_NO_SUCH_VAR")); let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); let addr = format!("127.0.0.1:{port}"); let token = "cli-test-token-0123456789"; - let mut child = Command::new(BIN) + let mut child = Command::new(binary()) .args(["denoise-serve", "--models", models.to_str().unwrap(), "--listen", &addr, "--run-on", "cpu"]) .env("LIGHTCRAFT_DENOISE_TOKEN", token) .stderr(Stdio::piped()) diff --git a/apps/lightcraft-cli/tests/it/main.rs b/apps/lightcraft-cli/tests/it/main.rs index 0c07c64385..596ffb21d9 100644 --- a/apps/lightcraft-cli/tests/it/main.rs +++ b/apps/lightcraft-cli/tests/it/main.rs @@ -2,3 +2,8 @@ mod cli; mod mcp_shutdown; + +/// Nextest archives relocate the executable; plain Cargo uses its compile-time path. +fn binary() -> std::ffi::OsString { + std::env::var_os("NEXTEST_BIN_EXE_lightcraft_cli").unwrap_or_else(|| env!("CARGO_BIN_EXE_lightcraft-cli").into()) +} diff --git a/apps/lightcraft-cli/tests/it/mcp_shutdown.rs b/apps/lightcraft-cli/tests/it/mcp_shutdown.rs index 29d1ace937..0237b6b37d 100644 --- a/apps/lightcraft-cli/tests/it/mcp_shutdown.rs +++ b/apps/lightcraft-cli/tests/it/mcp_shutdown.rs @@ -6,13 +6,7 @@ use std::time::{Duration, Instant}; #[test] fn broken_stdout_exits_with_stdin_still_open() { - let mut child = Command::new(env!("CARGO_BIN_EXE_lightcraft-cli")) - .arg("mcp") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); + let mut child = Command::new(super::binary()).arg("mcp").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); let mut input = child.stdin.take().unwrap(); drop(child.stdout.take()); input.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}\n{\"jsonrpc\":").unwrap(); diff --git a/apps/lightcraft-web/Cargo.toml b/apps/lightcraft-web/Cargo.toml index aaa14a648b..d4ae0f6674 100644 --- a/apps/lightcraft-web/Cargo.toml +++ b/apps/lightcraft-web/Cargo.toml @@ -77,3 +77,6 @@ web-sys = { workspace = true, features = [ # it (HEVC patents); build with `--features heif` to include it. On wasm32 panics abort, so # heic-rs's `catch_unwind` guard does not apply there. heif = ["lightcraft-codecs/heif"] + +[dev-dependencies] +egui = { workspace = true } diff --git a/apps/lightcraft-web/src/drop_gate.rs b/apps/lightcraft-web/src/drop_gate.rs new file mode 100644 index 0000000000..f21efced49 --- /dev/null +++ b/apps/lightcraft-web/src/drop_gate.rs @@ -0,0 +1,391 @@ +//! When the browser host may consume queued drops. Kept out of the wasm-only `web` module so the +//! rule that keeps stored drops out of a library behind a foreground window is testable natively. + +use lightcraft_ui_egui::LightcraftApp; + +/// The browser host's two drop queues: file handles not yet read, and originals whose asynchronous +/// storage has finished and that wait to be imported. Taking either only through this trait keeps +/// the gate and the drain together. +pub trait QueuedDrops { + type Dropped; + type Pending; + /// Whether a prompt or the blocking library window owns input right now. + fn drops_wait(&self) -> bool; + /// Record that all currently unread handles are held by this foreground barrier. + fn defer_dropped(&mut self); + fn take_dropped(&mut self) -> Vec; + /// Take fresh arrivals only, retaining handles protected by an earlier foreground barrier. + fn take_unprotected_dropped(&mut self) -> Vec; + fn take_pending(&mut self) -> Vec; +} + +/// A prompt or the blocking library window (over an already open library, or its own New Library +/// name prompt or asynchronous folder picker) owns input: leaves the dropped-file queue and the +/// originals still being stored for a later UI pass, instead of importing them behind the window. +/// With no library open and no dialog in front, the existing drop path is unchanged. +pub fn drops_wait(app: &LightcraftApp) -> bool { + lightcraft_ui_egui::panels::notices::blocks_input(app) || lightcraft_ui_egui::panels::library_problem::blocks_file_drops(app) +} + +/// One UI pass's queues, drained by [`drain_ready`]. +pub struct Drained { + pub dropped: Vec, + /// `None` when new file handles were refused behind a dialog: the originals already stored + /// stay queued for a later pass (as before), rather than importing behind that dialog. + pub pending: Option>, +} + +/// The actual queue drain `WebApp` runs each UI pass: while [`drops_wait`] holds, neither queue is +/// touched. With an ordinary dialog open, only fresh handles are taken for refusal; earlier +/// barrier-held handles remain queued. If fresh handles are refused, stored originals are left +/// queued as well. Otherwise the existing stored-original drain policy is unchanged. +pub fn drain_ready(queues: &mut Q, dialog_open: bool) -> Option> { + if queues.drops_wait() { + queues.defer_dropped(); + return None; + } + let dropped = if dialog_open { queues.take_unprotected_dropped() } else { queues.take_dropped() }; + let pending = if !dropped.is_empty() && dialog_open { None } else { Some(queues.take_pending()) }; + Some(Drained { dropped, pending }) +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::{QueuedDrops, drain_ready, drops_wait}; + use lightcraft_engine::Session; + use lightcraft_ui_egui::panels::library_problem::LibraryProblem; + use lightcraft_ui_egui::state::Dialog; + use lightcraft_ui_egui::{LightcraftApp, Services}; + + struct Scratch(std::path::PathBuf); + + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + type Asked = std::sync::Arc>>>>; + + fn app_with_library(tag: &str) -> Result<(Scratch, LightcraftApp), String> { + let dir = std::env::temp_dir().join(format!("lc-web-drop-gate-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).map_err(|error| error.to_string())?; + let scratch = Scratch(dir); + let mut session = Session::new().with_fs(); + session.open_library(&scratch.0, false).map_err(|error| error.to_string())?; + Ok((scratch, LightcraftApp::new(session, Services::default()))) + } + + /// Stands in for `WebApp`: the same shared orchestration over two real queues, so a test sees + /// that a held window leaves both queues untouched, that the ready path drains both, and that + /// a refused new drop keeps the already stored originals queued. + struct FakeQueues { + app: LightcraftApp, + dropped: Vec, + protected: usize, + pending: Vec, + } + + impl FakeQueues { + fn new(app: LightcraftApp) -> Self { + Self { app, dropped: vec!["dropped-photo".into()], protected: 0, pending: vec!["web/hash/photo.png".into()] } + } + } + + impl QueuedDrops for FakeQueues { + type Dropped = String; + type Pending = String; + + fn drops_wait(&self) -> bool { + drops_wait(&self.app) + } + + fn defer_dropped(&mut self) { + self.protected = self.dropped.len(); + } + + fn take_dropped(&mut self) -> Vec { + self.protected = 0; + std::mem::take(&mut self.dropped) + } + + fn take_unprotected_dropped(&mut self) -> Vec { + self.dropped.split_off(self.protected.min(self.dropped.len())) + } + + fn take_pending(&mut self) -> Vec { + std::mem::take(&mut self.pending) + } + } + + #[test] + fn stored_drops_wait_through_the_choosers_new_library_prompt_and_drain_after_it() { + let (_dir, mut app) = app_with_library("chooser").expect("create chooser test library"); + let mut problem = LibraryProblem::choose_later(&app.ui.settings); + if let Some(c) = problem.chooser.as_mut() { + c.over = Some(app.session.library_generation()); + } + app.library_problem = Some(problem); + let mut queues = FakeQueues::new(app); + + // The list's own New Library name prompt hides the list but still owns the library. + queues.app.ui.dialog = Some(Dialog::TextPrompt { + title: "New Library".into(), + hint: "Library name".into(), + value: String::new(), + command: "app.newLibrary".into(), + params: serde_json::json!({}), + key: "name".into(), + }); + assert!(drain_ready(&mut queues, false).is_none(), "the name prompt holds both queues"); + assert_eq!((queues.dropped.len(), queues.pending.len()), (1, 1), "the queued drop survived the prompt"); + + // Cancelling the prompt brings the list back; it still owns the drop until it goes. + queues.app.ui.dialog = None; + assert!(drain_ready(&mut queues, false).is_none(), "the list is back and still holds the queue"); + queues.app.library_problem = None; + let got = drain_ready(&mut queues, false).expect("the queues should be ready"); + assert_eq!(got.dropped, vec!["dropped-photo".to_string()]); + assert_eq!(got.pending, Some(vec!["web/hash/photo.png".to_string()])); + assert!(queues.dropped.is_empty() && queues.pending.is_empty(), "both queues drained once the window was gone"); + } + + #[test] + fn a_refused_new_drop_behind_a_dialog_keeps_stored_originals_queued() { + let (_dir, app) = app_with_library("refused").expect("create refused-drop test library"); + // not held by the gate: the dialog alone refuses the newly dropped handles + let mut queues = FakeQueues::new(app); + let got = drain_ready(&mut queues, true).expect("the queues should be ready"); + assert_eq!(got.dropped, vec!["dropped-photo".to_string()]); + assert_eq!(got.pending, None, "the refused drop must not drain originals behind the dialog"); + assert_eq!(queues.pending.len(), 1, "the earlier originals stayed queued"); + + // Nothing new was dropped: originals read from an earlier drop still finish, as before. + let got = drain_ready(&mut queues, true).expect("the queues should be ready"); + assert!(got.dropped.is_empty()); + assert_eq!(got.pending, Some(vec!["web/hash/photo.png".to_string()])); + assert!(queues.pending.is_empty()); + } + + #[test] + fn a_no_library_library_picker_holds_both_queues() { + let mut app = LightcraftApp::new(Session::new().with_fs(), Services::default()); + let asked: Asked = Default::default(); + app.services.picker = Some(Box::new(move |_| { + let (tx, rx) = std::sync::mpsc::channel(); + asked.lock().unwrap().push(tx); + Ok(rx) + })); + // New Library's name prompt was confirmed; the asynchronous place picker is still open. + assert_eq!(app.run("app.newLibrary", serde_json::json!({"name": "Queued"})).unwrap(), serde_json::Value::Null); + assert!(app.session.library.is_none()); + app.library_problem = Some(LibraryProblem::choose_later(&app.ui.settings)); + let mut queues = FakeQueues::new(app); + assert!(drain_ready(&mut queues, false).is_none(), "a library picker that will open one holds both queues"); + assert_eq!((queues.dropped.len(), queues.pending.len()), (1, 1), "nothing was drained while the picker is up"); + } + + #[test] + fn a_no_library_window_lets_the_existing_drop_path_consume_the_queues() { + let mut app = LightcraftApp::new(Session::with_demo(), Services::default()); + app.library_problem = Some(LibraryProblem::new("/no/such/library", "missing")); + let mut queues = FakeQueues::new(app); + assert!(!drops_wait(&queues.app), "with no library open the existing drop path is unchanged"); + let got = drain_ready(&mut queues, false).expect("the queues should be ready"); + assert_eq!(got.dropped, vec!["dropped-photo".to_string()]); + assert_eq!(got.pending, Some(vec!["web/hash/photo.png".to_string()])); + assert!(queues.dropped.is_empty() && queues.pending.is_empty(), "the host consumed both queues"); + + queues.app.notices.push("The settings file could not be read and was reset.".into()); + queues.dropped.push("late-drop".into()); + queues.pending.push("web/hash/late.png".into()); + assert!(drain_ready(&mut queues, false).is_none(), "a prompt still owns input"); + assert_eq!((queues.dropped.len(), queues.pending.len()), (1, 1), "the prompt left both queues alone"); + } + + /// Uses the product's actual raw-handle queue and provenance APIs. Only the stored-original + /// vector is host-owned; there is no test copy of protected-prefix bookkeeping or policy. + struct RealAppQueues { + h: lightcraft_ui_egui::headless::Headless, + pending: Vec, + pending_takes: usize, + } + + impl QueuedDrops for RealAppQueues { + type Dropped = egui::DroppedFileHandle; + type Pending = String; + + fn drops_wait(&self) -> bool { + drops_wait(&self.h.app) + } + + fn defer_dropped(&mut self) { + self.h.app.defer_dropped_files(); + } + + fn take_dropped(&mut self) -> Vec { + self.h.app.take_dropped_files() + } + + fn take_unprotected_dropped(&mut self) -> Vec { + self.h.app.take_unprotected_dropped_files() + } + + fn take_pending(&mut self) -> Vec { + self.pending_takes += 1; + std::mem::take(&mut self.pending) + } + } + + #[derive(Debug)] + struct UnreadDrop { + path: std::path::PathBuf, + reads: std::sync::Arc, + } + + impl egui::DroppedFile for UnreadDrop { + fn path(&self) -> &std::path::Path { + &self.path + } + + fn bytes(&self) -> Result, String> { + self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Err("the queue gate must not read dropped bytes".into()) + } + } + + fn unread(path: &str) -> (egui::DroppedFileHandle, std::sync::Arc) { + let reads = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let handle = std::sync::Arc::new(UnreadDrop { path: path.into(), reads: reads.clone() }); + (handle, reads) + } + + fn queue_raw(queues: &mut RealAppQueues, handles: &[egui::DroppedFileHandle]) { + let mut raw = egui::RawInput { dropped_files: handles.to_vec(), ..Default::default() }; + queues.h.app.raw_input_hook(&queues.h.view.ctx, &mut raw); + assert!(raw.dropped_files.is_empty(), "the product takes raw ownership once"); + } + + fn hold_library(queues: &mut RealAppQueues) -> Result<(), String> { + let mut problem = LibraryProblem::choose_later(&queues.h.app.ui.settings); + problem.chooser.as_mut().ok_or("the launch list has no chooser")?.over = Some(queues.h.app.session.library_generation()); + queues.h.app.library_problem = Some(problem); + Ok(()) + } + + fn real_queues(tag: &str) -> Result<(Scratch, RealAppQueues), String> { + let (dir, app) = app_with_library(tag)?; + let h = lightcraft_ui_egui::headless::Headless::new(app, [1300.0, 900.0], 1.0); + Ok((dir, RealAppQueues { h, pending: vec!["web/hash/stored.png".into()], pending_takes: 0 })) + } + + fn protect(queues: &mut RealAppQueues, handle: &egui::DroppedFileHandle) -> Result<(), String> { + hold_library(queues)?; + queue_raw(queues, std::slice::from_ref(handle)); + // WebApp's gate runs before App.ui, including the cold first frame. The gate + // itself must establish provenance; a warmed native UI pass cannot stand in for it. + assert_eq!(queues.h.frames(), 0, "this starts before the first native UI pass"); + assert!(drops_wait(&queues.h.app)); + assert!(drain_ready(queues, false).is_none()); + assert_eq!(queues.h.frames(), 0, "the host gate protects ownership without drawing"); + assert_eq!(queues.pending_takes, 0); + assert!(queues.h.app.scan.is_none() && queues.h.app.import.is_none()); + assert_eq!(queues.h.app.session.catalog.len(), 0); + Ok(()) + } + + #[test] + fn real_unread_barrier_drop_survives_a_dialog_while_stored_originals_finish() { + let (_dir, mut queues) = real_queues("real-held-dialog").expect("create real-queue library"); + let (a, reads) = unread("A.png"); + protect(&mut queues, &a).expect("hold the library barrier"); + queues.h.app.library_problem = None; + queues.h.app.ui.dialog = Some(Dialog::WhatsNew); + // WebApp drains BEFORE App.ui. A native UI pass here would conceal the host-order bug. + let got = drain_ready(&mut queues, true).expect("only the ordinary dialog remains"); + assert!(got.dropped.is_empty(), "the earlier unread A must not be drained for dialog refusal"); + assert_eq!( + got.pending, + Some(vec!["web/hash/stored.png".into()]), + "with no fresh refusal, earlier stored originals retain their existing completion policy" + ); + assert_eq!(queues.pending_takes, 1); + let again = drain_ready(&mut queues, true).unwrap(); + assert!(again.dropped.is_empty(), "A stays protected across another host pass"); + queues.h.app.ui.dialog = None; + let released = drain_ready(&mut queues, false).unwrap(); + assert_eq!(released.dropped.len(), 1); + assert!(std::sync::Arc::ptr_eq(&released.dropped[0], &a), "the original A handle is released"); + assert!(drain_ready(&mut queues, false).unwrap().dropped.is_empty(), "A transfers exactly once"); + assert!(queues.h.app.take_dropped_files().is_empty()); + assert_eq!(reads.load(std::sync::atomic::Ordering::SeqCst), 0); + } + + #[test] + fn real_fresh_dialog_drop_is_refused_without_consuming_protected_or_stored_files() { + let (_dir, mut queues) = real_queues("real-mixed-dialog").expect("create real-queue library"); + let (a, reads_a) = unread("A.png"); + let (b, reads_b) = unread("B.png"); + protect(&mut queues, &a).expect("hold the library barrier"); + queues.h.app.library_problem = None; + queues.h.app.ui.dialog = Some(Dialog::WhatsNew); + queue_raw(&mut queues, std::slice::from_ref(&b)); + let got = drain_ready(&mut queues, true).unwrap(); + assert_eq!(got.dropped.len(), 1, "only fresh B is offered to the browser's existing refusal branch"); + assert!(std::sync::Arc::ptr_eq(&got.dropped[0], &b)); + assert_eq!(got.pending, None, "refusing B must not consume stored originals"); + assert_eq!(queues.pending_takes, 0); + assert_eq!(queues.pending, vec!["web/hash/stored.png".to_string()]); + assert!(matches!(queues.h.app.ui.dialog, Some(Dialog::WhatsNew))); + queues.h.app.ui.dialog = None; + let released = drain_ready(&mut queues, false).unwrap(); + assert_eq!(released.dropped.len(), 1, "protected A survived B's refusal"); + assert!(std::sync::Arc::ptr_eq(&released.dropped[0], &a)); + assert_eq!(released.pending, Some(vec!["web/hash/stored.png".into()])); + let empty = drain_ready(&mut queues, false).unwrap(); + assert!(empty.dropped.is_empty()); + assert_eq!(empty.pending, Some(Vec::::new())); + assert_eq!(reads_a.load(std::sync::atomic::Ordering::SeqCst), 0); + assert_eq!(reads_b.load(std::sync::atomic::Ordering::SeqCst), 0); + // Consuming A clears its protection: a later ordinary drop is still fresh. + let (c, reads_c) = unread("C.png"); + queues.h.app.ui.dialog = Some(Dialog::WhatsNew); + queue_raw(&mut queues, std::slice::from_ref(&c)); + let fresh = drain_ready(&mut queues, true).unwrap(); + assert_eq!(fresh.dropped.len(), 1); + assert!(std::sync::Arc::ptr_eq(&fresh.dropped[0], &c)); + assert_eq!(fresh.pending, None); + queues.h.app.ui.dialog = None; + assert!(drain_ready(&mut queues, false).unwrap().dropped.is_empty()); + assert_eq!(reads_c.load(std::sync::atomic::Ordering::SeqCst), 0); + } + + #[test] + fn real_protected_drops_keep_order_through_barrier_reentry_and_transfer_once() { + let (_dir, mut queues) = real_queues("real-reentered").expect("create real-queue library"); + let (a, reads_a) = unread("A.png"); + let (b, reads_b) = unread("B.png"); + protect(&mut queues, &a).expect("hold the library barrier"); + queues.h.app.library_problem = None; + queues.h.app.ui.dialog = Some(Dialog::WhatsNew); + assert!(drain_ready(&mut queues, true).unwrap().dropped.is_empty()); + queues.h.app.ui.dialog = None; + hold_library(&mut queues).expect("reenter the library barrier"); // Before a pass can consume A. + queue_raw(&mut queues, std::slice::from_ref(&b)); + assert!(drain_ready(&mut queues, false).is_none(), "the reentered host gate protects B before native UI"); + assert_eq!(queues.h.frames(), 0, "neither protected arrival depends on font readiness or a completed UI pass"); + queues.h.app.library_problem = None; + queues.h.app.ui.dialog = Some(Dialog::WhatsNew); + assert!(drain_ready(&mut queues, true).unwrap().dropped.is_empty(), "both real barrier arrivals remain protected"); + queues.h.app.ui.dialog = None; + let released = drain_ready(&mut queues, false).unwrap(); + assert_eq!(released.dropped.len(), 2); + assert!(std::sync::Arc::ptr_eq(&released.dropped[0], &a)); + assert!(std::sync::Arc::ptr_eq(&released.dropped[1], &b)); + assert!(drain_ready(&mut queues, false).unwrap().dropped.is_empty()); + assert!(queues.h.app.take_dropped_files().is_empty()); + assert_eq!(reads_a.load(std::sync::atomic::Ordering::SeqCst), 0); + assert_eq!(reads_b.load(std::sync::atomic::Ordering::SeqCst), 0); + } +} diff --git a/apps/lightcraft-web/src/lib.rs b/apps/lightcraft-web/src/lib.rs index f984cb34c3..5d3c011589 100644 --- a/apps/lightcraft-web/src/lib.rs +++ b/apps/lightcraft-web/src/lib.rs @@ -19,6 +19,10 @@ pub mod automation; pub mod backup; pub mod bench; +// The web host's drop gate, also compiled natively so its queue handling has tests (the module is +// unused in a native, non-test library build and would otherwise be dead code there). +#[cfg(any(test, target_arch = "wasm32"))] +mod drop_gate; pub mod files; pub mod store; pub mod wire; diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 31fe2cb0aa..6e9e855960 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -422,6 +422,33 @@ struct WebApp { ui_written: Vec, } +/// The browser host's drop queues, taken only through the shared gate: a prompt or blocking +/// library window leaves both untouched for a later UI pass. +impl crate::drop_gate::QueuedDrops for WebApp { + type Dropped = egui::DroppedFileHandle; + type Pending = String; + + fn drops_wait(&self) -> bool { + crate::drop_gate::drops_wait(&self.app) + } + + fn defer_dropped(&mut self) { + self.app.defer_dropped_files(); + } + + fn take_dropped(&mut self) -> Vec { + self.app.take_dropped_files() + } + + fn take_unprotected_dropped(&mut self) -> Vec { + self.app.take_unprotected_dropped_files() + } + + fn take_pending(&mut self) -> Vec { + self.originals.take_pending() + } +} + impl WebApp { /// Construct the web app from the boot options: install originals and open the library from /// browser storage (a read failure starts an empty session and a blocking `LibraryProblem`), @@ -534,14 +561,18 @@ impl WebApp { /// Consume the drops the raw-input hook queued once (never re-read `raw.dropped_files` in a /// hidden logic tick), and add originals whose bytes have arrived. fn import_dropped(&mut self, ctx: &egui::Context) { - let dropped = self.app.take_dropped_files(); + // A prompt or the blocking library window (including the chooser's own New Library name + // prompt or asynchronous folder picker) holds both queues for a later UI pass: nothing is + // read or imported behind it. `drain_ready` is the same orchestration the native tests run. + let dialog_open = self.app.ui.dialog.is_some(); + let Some(drained) = crate::drop_gate::drain_ready(self, dialog_open) else { return }; // a dialog is on screen (the card said so while the files hovered): nothing is read or imported // (files read from an earlier drop still finish below) - if !dropped.is_empty() && self.app.ui.dialog.is_some() { + if !drained.dropped.is_empty() && dialog_open { self.app.toast(ctx, lightcraft_ui_egui::i18n::tr("Close the open dialog, then drop the files again")); return; } - for f in dropped { + for f in drained.dropped { let (originals, backend, ctx) = (self.originals.clone(), self.backend.clone(), ctx.clone()); wasm_bindgen_futures::spawn_local(async move { let name = egui::DroppedFile::path(f.as_ref()).to_string_lossy().to_string(); @@ -551,7 +582,7 @@ impl WebApp { } }); } - let paths = self.originals.take_pending(); + let Some(paths) = drained.pending else { return }; if !paths.is_empty() { match self.app.run("library.import", json!({"paths": paths})) { // what the import did, not what was submitted: duplicates and unsupported files add nothing, @@ -723,12 +754,10 @@ impl eframe::App for WebApp { /// page title, and advance the benchmark once per root pass. fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { // Drops are the UI phase's: a hidden `logic`/`run_logic` tick must not import them twice. - // A prompt owns the library too: keep taking the queue only after it is gone, so a drop - // during a notice cannot add photos behind it. + // `import_dropped` itself takes both queues only through the gate, so a prompt or the + // blocking library window (over an already open library) keeps a stored drop queued. let ctx = ui.ctx().clone(); - if !lightcraft_ui_egui::panels::notices::blocks_input(&self.app) { - self.import_dropped(&ctx); - } + self.import_dropped(&ctx); self.app.ui(ui); // eframe's web runner ignores `ViewportCommand::Title`: name the open photo in the page title let title = self.app.page_title(self.files.is_dirty() || self.flushing.get()); diff --git a/crates/engine/src/cmd/activity.rs b/crates/engine/src/cmd/activity.rs index af86677287..ed4e8d35c7 100644 --- a/crates/engine/src/cmd/activity.rs +++ b/crates/engine/src/cmd/activity.rs @@ -20,7 +20,7 @@ pub fn specs() -> Vec { "Cancel Background Task", [], None, - "{id} | {all: true} → {cancelled: n} — ask a task (or every cancellable task) to stop; it stops at its next file or photo. An unknown id (it may have just finished) or a task that can't be cancelled is an error", + "{id} | {all: true} → {cancelled: n} — ask a task (or every cancellable task) to stop; it stops at its next file or photo (a preview build also stops between a photo's own previews). An unknown id (it may have just finished) or a task that can't be cancelled is an error", always, |s, p| { const C: &str = "activity.cancel"; diff --git a/crates/engine/src/cmd/previews.rs b/crates/engine/src/cmd/previews.rs index bef292f8f7..e1b33cc714 100644 --- a/crates/engine/src/cmd/previews.rs +++ b/crates/engine/src/cmd/previews.rs @@ -31,6 +31,10 @@ pub struct PreviewBuild { pub what: &'static str, /// Why the whole build failed (e.g. the smart previews folder isn't there). pub error: std::sync::Mutex>, + /// Test-only: the gate this build's lanes wait at before a photo's first render (see + /// [`test_gate`]). Captured by `build` on the calling thread; production builds never have one. + #[cfg(test)] + pub(crate) gate: Option>, } impl PreviewBuild { @@ -79,10 +83,15 @@ fn task_for(s: &Session, state: &PreviewBuild) -> TaskGuard { /// Render one photo's jobs in order. The source the first job decodes serves the ones after it /// (the grid thumbnail is made from the view's source instead of decoding the file again). -pub(crate) fn run_photo(jobs: Vec) -> bool { +/// `None` when `cancel` was asked for before a job started: that photo is not counted as done. +fn run_photo_cancellable(jobs: Vec, cancel: Option<&AtomicBool>) -> Option { let mut decoded: Option = None; let mut ok = true; for mut j in jobs { + // between a photo's own previews too (a cancelled 1:1 view skips its grid thumbnail) + if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) { + return None; + } if let Some(src) = &decoded && !matches!(j.source, crate::media::SourceRef::Loaded(_)) { @@ -94,7 +103,13 @@ pub(crate) fn run_photo(jobs: Vec) -> bool { decoded = r.loaded; } } - ok + Some(ok) +} + +/// [`run_photo_cancellable`] for callers that render a photo without a cancel flag. +#[cfg(test)] +pub(crate) fn run_photo(jobs: Vec) -> bool { + run_photo_cancellable(jobs, None) == Some(true) } /// How many photos render side by side: as many as an export batch for standard previews (each @@ -105,6 +120,112 @@ fn lanes(n: usize, full: bool) -> usize { if full { l.min(2) } else { l } } +/// A test-only rendezvous at the start of a photo's jobs, so a cancel can be delivered at a known +/// point instead of racing a render. One gate belongs to the build `build` starts on the test +/// thread that installed it ([`install`]); it is never global and production builds have none. +#[cfg(test)] +pub(crate) mod test_gate { + use std::sync::{Arc, Condvar, Mutex, PoisonError}; + use std::time::{Duration, Instant}; + + /// The longest a lane waits for a test to open the gate: if a test ends without its + /// [`ReleaseOnDrop`] running, the build still finishes instead of stranding its worker. + const MAX_HOLD: Duration = Duration::from_secs(30); + /// The longest a test waits for a lane to reach the gate. + const MAX_ENTER_WAIT: Duration = Duration::from_secs(10); + + #[derive(Default)] + struct State { + entered: usize, + released: bool, + } + + /// The gate a test's preview build waits at before rendering a photo (see `install`). + #[derive(Default)] + pub(crate) struct PreviewGate { + state: Mutex, + opened: Condvar, + } + + impl std::fmt::Debug for PreviewGate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PreviewGate").finish_non_exhaustive() + } + } + + impl PreviewGate { + pub(crate) fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Called by a lane about to render a photo's first job; returns once the test opens the + /// gate, or after `MAX_HOLD` so a lost test cannot strand the worker. + pub(crate) fn enter(&self) { + let mut st = self.state.lock().unwrap_or_else(PoisonError::into_inner); + st.entered = st.entered.saturating_add(1); + self.opened.notify_all(); + let deadline = Instant::now() + MAX_HOLD; + while !st.released { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + break; + } + let (next, _) = self.opened.wait_timeout(st, remaining).unwrap_or_else(|e| e.into_inner()); + st = next; + } + } + + /// Wait (at most `MAX_ENTER_WAIT`) until at least `n` lanes have reached the gate. + pub(crate) fn wait_entered(&self, n: usize) -> bool { + let mut st = self.state.lock().unwrap_or_else(PoisonError::into_inner); + let deadline = Instant::now() + MAX_ENTER_WAIT; + loop { + if st.entered >= n { + return true; + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return false; + } + let (next, _) = self.opened.wait_timeout(st, remaining).unwrap_or_else(|e| e.into_inner()); + st = next; + } + } + + /// Open the gate: every lane waiting, or arriving later, passes without waiting. + pub(crate) fn release(&self) { + let mut st = self.state.lock().unwrap_or_else(PoisonError::into_inner); + st.released = true; + self.opened.notify_all(); + } + } + + /// Opens the gate when dropped, wherever a test ends (assertion failure, panic, early return). + pub(crate) struct ReleaseOnDrop(pub(crate) Arc); + + impl Drop for ReleaseOnDrop { + fn drop(&mut self) { + self.0.release(); + } + } + + thread_local! { + /// The gate for the next preview build started on this thread, taken by `build` so it is + /// per invocation (never shared by two builds or two tests). + static NEXT_GATE: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; + } + + /// Have the next preview build started on this thread wait at `gate` before its first photo. + pub(crate) fn install(gate: Arc) { + NEXT_GATE.with(|g| *g.borrow_mut() = Some(gate)); + } + + /// Take the gate installed for this build, leaving none for later builds. + pub(crate) fn take() -> Option> { + NEXT_GATE.with(|g| g.borrow_mut().take()) + } +} + fn run_all(jobs: Vec>, state: &PreviewBuild, task: &TaskGuard, lanes: usize) { let total = jobs.len(); let queue = std::sync::Mutex::new(jobs.into_iter()); @@ -115,7 +236,17 @@ fn run_all(jobs: Vec>, state: &PreviewBuild, task: &TaskGuard, la } let next = queue.lock().unwrap_or_else(std::sync::PoisonError::into_inner).next(); let Some(photo_jobs) = next else { break }; - let ok = crate::guard::catch("building previews", || run_photo(photo_jobs)).unwrap_or(false); + // A cancel accepted while this lane waited for the queue (or for another lane's + // photo) stops before this photo's first render; a cancelled photo is not counted. + if state.cancel.load(Ordering::Relaxed) { + break; + } + #[cfg(test)] + if let Some(gate) = &state.gate { + gate.enter(); + } + let outcome = crate::guard::catch("building previews", || run_photo_cancellable(photo_jobs, Some(&state.cancel))).unwrap_or(Some(false)); + let Some(ok) = outcome else { break }; if !ok { state.failed.fetch_add(1, Ordering::Relaxed); } @@ -143,6 +274,9 @@ fn run_all(jobs: Vec>, state: &PreviewBuild, task: &TaskGuard, la fn build(s: &mut Session, p: &Value) -> Result { const C: &str = "library.buildPreviews"; + // tests: this invocation's gate, consumed here so an early error cannot attach it to a later build + #[cfg(test)] + let gate = test_gate::take(); let edge = match str_param(p, "size").unwrap_or("standard") { "standard" => Some(p.get("edge").and_then(Value::as_u64).map_or(STANDARD_EDGE, |e| e.clamp(256, 8192) as usize)), "full" | "1:1" => None, @@ -154,6 +288,9 @@ fn build(s: &mut Session, p: &Value) -> Result { // explicit ids, else the selection, else everything in view let ids = if p.get("ids").is_some() || p.get("id").is_some() || !s.selection.ids.is_empty() { s.targets(p)? } else { s.visible_cloned() }; let jobs: Vec> = ids.iter().map(|id| jobs_for(s, *id, edge)).filter(|j| !j.is_empty()).collect(); + #[cfg(test)] + let state = Arc::new(PreviewBuild { total: jobs.len(), gate, ..Default::default() }); + #[cfg(not(test))] let state = Arc::new(PreviewBuild { total: jobs.len(), ..Default::default() }); s.preview_build = Some(state.clone()); let task = task_for(s, &state); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 185c94f63b..ae03859d21 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -601,6 +601,12 @@ fn degenerate_crops_render_with_spots_and_masks() { s.execute("develop.set", &json!({"control": "effects.clarity", "value": 80})).unwrap(); s.execute("develop.set", &json!({"control": "effects.dehaze", "value": 50})).unwrap(); set_crop_unchecked(&mut s, rect, 30.0); + // Measure the degenerate render, not procedural demo generation. Load exactly the + // source level this crop requests; keep the original photo, effects and output size. + let level = s.render_job(id, 700, 450, false, true).unwrap().level; + let source = s.source_now(id, level).unwrap(); + assert!(source.width > 0 && source.height > 0); + assert!(s.media.has_source(id, level), "the timed render must reuse the prepared source"); let t = std::time::Instant::now(); let r = s.render_now(id, 700, 450).unwrap(); assert!(r.image.width >= 1 && r.image.height >= 1, "{rect:?}"); @@ -1194,18 +1200,51 @@ fn preview_build_shows_in_activity_until_it_finishes() { assert_eq!(s.execute("library.previewProgress", &json!({})).unwrap()["running"], false); } -/// Issue #345: `activity.cancel` stops a preview build like `library.cancelPreviews`. +/// Issue #345: `activity.cancel` stops a preview build like `library.cancelPreviews`. A test-only +/// gate holds the lane at its first photo, so the cancel lands before any render instead of racing +/// a 24 MP one; the accepted cancel must not count that photo. #[test] fn activity_cancel_stops_a_preview_build() { + use crate::cmd::previews::test_gate::{self, PreviewGate, ReleaseOnDrop}; let mut s = demo(); - let ids: Vec = s.catalog.photos().map(|p| p.id.0).collect(); - s.execute("library.buildPreviews", &json!({"ids": ids, "size": "full"})).unwrap(); + let ids: Vec = s.catalog.photos().take(2).map(|p| p.id.0).collect(); + let gate = PreviewGate::new(); + // also opens the gate if an assertion below panics, so the worker is never stranded + let release = ReleaseOnDrop(gate.clone()); + test_gate::install(gate.clone()); + s.execute("library.buildPreviews", &json!({"ids": ids, "size": "standard", "edge": 256})).unwrap(); let id = s.activity.list().first().map(|t| t.id).expect("a previews row"); + assert!(gate.wait_entered(1), "a lane reached its first photo"); s.execute("activity.cancel", &json!({"id": id})).unwrap(); + let row = s.activity.list().into_iter().find(|t| t.id == id).expect("the row stays while the lane is held"); + assert!(row.cancelling, "the row shows the accepted cancel: {row:?}"); + drop(release); wait_activity_empty(&s); let p = s.execute("library.previewProgress", &json!({})).unwrap(); assert_eq!(p["cancelled"], true, "{p}"); - assert!(p["done"].as_u64() < p["total"].as_u64(), "stopped part-way: {p}"); + assert_eq!(p["done"], json!(0), "no photo rendered after the accepted cancel: {p}"); + assert_eq!(p["running"], false, "{p}"); +} + +/// The gated fixture is real work: without a cancel, both photos render and are counted. +#[test] +fn preview_build_at_the_test_gate_completes_without_cancel() { + use crate::cmd::previews::test_gate::{self, PreviewGate, ReleaseOnDrop}; + let mut s = demo(); + let ids: Vec = s.catalog.photos().take(2).map(|p| p.id.0).collect(); + let gate = PreviewGate::new(); + let release = ReleaseOnDrop(gate.clone()); + test_gate::install(gate.clone()); + let r = s.execute("library.buildPreviews", &json!({"ids": ids, "size": "standard", "edge": 256})).unwrap(); + assert_eq!(r["total"], json!(2), "{r}"); + assert!(gate.wait_entered(1), "a lane reached its first photo"); + drop(release); + wait_activity_empty(&s); + let p = s.execute("library.previewProgress", &json!({})).unwrap(); + assert_eq!(p["running"], false, "{p}"); + assert_eq!(p["cancelled"], false, "{p}"); + assert_eq!(p["done"], json!(2), "both photos rendered: {p}"); + assert_eq!(p["failed"], json!(0), "{p}"); } /// Colour range: a click samples the colour under it, so the mask selects that colour (white diff --git a/crates/engine/src/tests/face_recognize.rs b/crates/engine/src/tests/face_recognize.rs index b24a7bbb92..f6ca874940 100644 --- a/crates/engine/src/tests/face_recognize.rs +++ b/crates/engine/src/tests/face_recognize.rs @@ -18,10 +18,24 @@ pub(crate) fn temp(name: &str) -> PathBuf { d } +/// Seed CPU-only settings before installing models in a fresh test folder. These tests cover catalog and +/// model logic; GPU kernels have separate tests. Calling the settings mutation command here would also +/// detach the process-wide detector accelerator, interfering with other tests in the same process. +fn keep_on_cpu(s: &mut Session) { + let dir = s.face_models_dir.as_ref().unwrap(); + let settings = dir.join("settings.json"); + assert!(!settings.exists(), "only seed a fresh fixture's settings"); + std::fs::create_dir_all(dir).unwrap(); + std::fs::write(settings, br#"{"cpuOnly":true}"#).unwrap(); + let r = s.execute("faces.gpu", &json!({})).unwrap(); + assert_eq!(r["cpuOnly"], true, "the test session must run face models on the CPU: {r}"); +} + /// A demo session with a tiny model installed and chosen. pub(crate) fn setup(d: &std::path::Path, dim: u64) -> (Session, String) { let mut s = Session::with_demo(); s.face_models_dir = Some(d.join("models")); + keep_on_cpu(&mut s); let model = d.join(format!("tiny{dim}.onnx")); std::fs::write(&model, lightcraft_faces::synthetic::tiny_embedder_model(dim)).unwrap(); let r = s.execute("faces.models.install", &json!({"path": model.to_string_lossy(), "acknowledged": true})).unwrap(); @@ -30,6 +44,47 @@ pub(crate) fn setup(d: &std::path::Path, dim: u64) -> (Session, String) { (s, id) } +/// A fresh empty library with the tiny model installed and chosen. The tests below only need a decodable +/// picture (and enough pixels for an optional real detector), not the demo's 12 procedural 24 MP scenes: +/// each demo photo costs a full develop render before the 112 px model input. +fn fresh_library(d: &std::path::Path, dim: u64) -> (Session, PathBuf) { + let lib = d.join("lib"); + let mut s = Session::new().with_fs(); + s.face_models_dir = Some(d.join("models")); + keep_on_cpu(&mut s); + s.open_library(&lib, false).unwrap(); + let model = d.join(format!("tiny{dim}.onnx")); + std::fs::write(&model, lightcraft_faces::synthetic::tiny_embedder_model(dim)).unwrap(); + s.execute("faces.models.install", &json!({"path": model.to_string_lossy(), "acknowledged": true})).unwrap(); + (s, lib) +} + +/// A real, distinct 1024×768 PNG (seed picks its colours). +fn write_photo(path: &std::path::Path, seed: u8) { + let (w, h) = (1024usize, 768usize); + let data: Vec<[u8; 4]> = (0..w * h) + .map(|i| [((i % w * 3 + seed as usize * 17) % 256) as u8, ((i / w * 5 + seed as usize * 29) % 256) as u8, seed.wrapping_mul(41), 255]) + .collect(); + let img = lightcraft_raster::Rgba8 { width: w, height: h, data }; + let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap(); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, bytes).unwrap(); +} + +/// Import three distinct real file-backed photos: two for faces and one with none for the optional detector. +fn import_three_photos(s: &mut Session, d: &std::path::Path) -> (PhotoId, PhotoId, PhotoId) { + let mut paths = Vec::new(); + for (i, name) in ["ann.png", "bea.png", "empty.png"].iter().enumerate() { + let path = d.join("photos").join(name); + write_photo(&path, i as u8 + 1); + paths.push(path.to_string_lossy().into_owned()); + } + let r = s.execute("library.import", &json!({"paths": paths})).unwrap(); + let ids: Vec = r["imported"].as_array().unwrap().iter().map(|v| PhotoId(v.as_u64().unwrap())).collect(); + assert_eq!(ids.len(), 3, "three real photos imported: {r}"); + (ids[0], ids[1], ids[2]) +} + /// Installs the real YuNet from `LC_YUNET_MODEL=` (what Settings > Faces downloads; nothing is /// committed) and says whether it did: the parts of a test that need to find faces run only then. pub(crate) fn install_yunet(s: &mut Session) -> bool { @@ -61,35 +116,52 @@ pub(crate) fn two_photos(s: &Session) -> (PhotoId, PhotoId) { #[test] fn indexing_embeds_each_face_once_and_reports_progress() { let d = temp("index"); - let (mut s, _) = setup(&d, 64); - let (a, b) = two_photos(&s); + let (mut s, _) = fresh_library(&d, 64); + let (a, b, c) = import_three_photos(&mut s, &d); set_regions(&mut s, a, vec![region(0.1, Some("Ann")), region(0.5, None)]); set_regions(&mut s, b, vec![region(0.3, None)]); // only a report let r = s.execute("faces.index", &json!({"budgetMs": 0})).unwrap(); assert_eq!((r["embedded"].clone(), r["photosDone"].clone(), r["pendingPhotos"].clone()), (json!(0), json!(0), json!(2)), "{r}"); - // a generous budget does it all, the named photo first - let r = s.execute("faces.index", &json!({"budgetMs": 60_000})).unwrap(); - assert_eq!((r["embedded"].clone(), r["pendingPhotos"].clone(), r["indexedFaces"].clone()), (json!(3), json!(0), json!(3)), "{r}"); + // `faces.index` stops between whole photos once the budget is spent, so follow its documented contract: + // call again until `pendingPhotos` is 0. Two photos need at most two productive calls; the named photo is + // first in the queue, and every face is embedded exactly once. + let mut embedded = 0u64; + for call in 0..2 { + let r = s.execute("faces.index", &json!({"budgetMs": 60_000})).unwrap(); + embedded += r["embedded"].as_u64().unwrap(); + if call == 0 { + assert!(s.faces.index.contains(a.0, ®ion(0.1, Some("Ann")).rect), "the named photo is done first: {r}"); + assert!(s.faces.index.contains(a.0, ®ion(0.5, None).rect), "both of its faces are embedded: {r}"); + } + if r["pendingPhotos"] == 0 { + break; + } + } + let r = s.execute("faces.index", &json!({"budgetMs": 0})).unwrap(); + assert_eq!((r["pendingPhotos"].clone(), r["indexedFaces"].clone()), (json!(0), json!(3)), "{r}"); + assert_eq!(embedded, 3, "each of the three faces was embedded exactly once across calls"); // nothing is embedded twice let r = s.execute("faces.index", &json!({"budgetMs": 60_000})).unwrap(); assert_eq!((r["embedded"].clone(), r["photosDone"].clone()), (json!(0), json!(0))); // a small budget still makes progress (at least one photo per call), and `ids` go first - let c = s.catalog.photos().map(|p| p.id).find(|id| *id != a && *id != b).unwrap(); set_regions(&mut s, c, vec![region(0.2, None)]); set_regions(&mut s, b, vec![region(0.3, None), region(0.6, None)]); let r = s.execute("faces.index", &json!({"budgetMs": 1, "ids": [c.0]})).unwrap(); assert_eq!(r["photosDone"], 1, "{r}"); assert_eq!(r["pendingPhotos"], 1, "{r}"); assert!(s.faces.index.contains(c.0, ®ion(0.2, None).rect), "the photo asked for first was done first"); + // keep the imported files until the session that references them is gone + drop(s); let _ = std::fs::remove_dir_all(&d); } #[test] fn suggestions_come_from_the_named_faces_and_are_never_applied() { let d = temp("suggest"); - let (mut s, _) = setup(&d, 64); - let (a, b) = two_photos(&s); + // Gallery and candidate semantics need real embeddings, not full 24 MP demo renders. + let (mut s, _) = fresh_library(&d, 64); + let (a, b, _) = import_three_photos(&mut s, &d); set_regions(&mut s, a, vec![region(0.1, Some("Ann"))]); set_regions(&mut s, b, vec![region(0.1, None), region(0.5, Some("Pet?")), region(0.7, None)]); // thresholds that accept anything: Ann is the only person with a named face except "Pet?" @@ -173,6 +245,7 @@ fn embeddings_are_cached_beside_the_library_and_reset_for_another_model() { }; let mut s = Session::new().with_fs(); s.face_models_dir = Some(models.clone()); + keep_on_cpu(&mut s); s.open_library(&lib, true).unwrap(); let (a, _) = two_photos(&s); set_regions(&mut s, a, vec![region(0.1, Some("Ann")), region(0.5, None)]); @@ -214,8 +287,9 @@ fn without_a_chosen_model_recognition_says_so() { #[test] fn the_background_pump_indexes_by_itself_once_recognition_is_on() { let d = temp("pump"); - let (mut s, _) = setup(&d, 64); - let (a, b) = two_photos(&s); + // Exercise real background embedding without unrelated full 24 MP demo renders. + let (mut s, _) = fresh_library(&d, 64); + let (a, b, _) = import_three_photos(&mut s, &d); set_regions(&mut s, a, vec![region(0.1, Some("Ann")), region(0.5, None)]); set_regions(&mut s, b, vec![region(0.3, None)]); // installing the model switched recognition on; switched off, the pump does nothing, and says so @@ -251,31 +325,33 @@ fn the_background_pump_indexes_by_itself_once_recognition_is_on() { #[test] fn the_background_pump_saves_what_it_embeds_for_the_next_launch() { let d = temp("pumpsave"); - let (lib, models) = (d.join("lib"), d.join("models")); - let mut s = Session::new().with_fs(); - s.face_models_dir = Some(models); - s.open_library(&lib, true).unwrap(); - let model = d.join("tiny64.onnx"); - std::fs::write(&model, lightcraft_faces::synthetic::tiny_embedder_model(64)).unwrap(); - // installing switches recognition on: nothing else is asked for - s.execute("faces.models.install", &json!({"path": model.to_string_lossy(), "acknowledged": true})).unwrap(); + // Installing the tiny model switches recognition on: nothing else is asked for. + let (mut s, lib) = fresh_library(&d, 64); // without the detector (not bundled) photos without faces are left unsearched let searching = install_yunet(&mut s); - let (a, b) = two_photos(&s); + let (a, b, c) = import_three_photos(&mut s, &d); set_regions(&mut s, a, vec![region(0.1, Some("Ann")), region(0.5, None)]); set_regions(&mut s, b, vec![region(0.3, None)]); + assert!(s.catalog.photo(c).unwrap().meta.regions.is_empty(), "the third photo has no face to find"); let photos = s.catalog.photos().count(); - let searched_photos = if searching { photos - 2 } else { 0 }; + assert_eq!(photos, 3, "a fresh library holds only the three imported photos"); + // only the one photo without a face region is searched when the optional detector is installed + let searched_photos = usize::from(searching); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120); - let mut last = json!(null); + let (mut last, mut embedded) = (json!(null), 0u64); while std::time::Instant::now() < deadline { last = s.execute("faces.pump", &json!({})).unwrap(); + embedded += last["embedded"].as_u64().unwrap_or(0); if last["pendingPhotos"] == 0 && s.faces.index.len() == 3 && s.faces.scanned.len() == searched_photos { break; } std::thread::sleep(std::time::Duration::from_millis(50)); } assert_eq!((s.faces.index.len(), s.faces.scanned.len()), (3, searched_photos), "{last}"); + assert_eq!(embedded, 3, "the background worker embedded each face exactly once: {last}"); + // once everything is done another pump starts no work and reports no new embedding + let idle = s.execute("faces.pump", &json!({})).unwrap(); + assert_eq!((idle["embedded"].clone(), idle["inFlight"].clone(), idle["pendingPhotos"].clone()), (json!(0), json!(0), json!(0)), "{idle}"); let (tag, path) = (s.faces.index.tag.clone(), s.faces.index.path.clone().unwrap()); assert!(path.file_name().unwrap().to_string_lossy().starts_with("face-embeddings-custom-"), "one cache file per model: {}", path.display()); // the first batch is written at once; whatever is left is written when the session ends, so a launch never redoes @@ -491,12 +567,9 @@ fn every_scanned_face_is_shown_by_the_same_kind_of_box() { #[test] fn face_scan_row_follows_pending_photos() { let d = temp("activity"); - let (mut s, _) = setup(&d, 64); - // the last two demo photos, not the first two the other scan tests use: two sessions scanning the same demo scene - // at once can deadlock (issue #630), and this test is about the row, not that - let mut ids = s.catalog.photos().map(|p| p.id).collect::>(); - ids.sort(); - let (a, b) = (ids[ids.len() - 2], ids[ids.len() - 1]); + // The activity row needs real background embedding, not full 24 MP demo renders. + let (mut s, _) = fresh_library(&d, 64); + let (a, b, _) = import_three_photos(&mut s, &d); set_regions(&mut s, a, vec![region(0.1, None), region(0.5, None)]); set_regions(&mut s, b, vec![region(0.3, None)]); let faces_rows = |s: &Session| s.activity.list().into_iter().filter(|t| t.kind == "faces").collect::>(); diff --git a/crates/mcp/tests/it/e2e.rs b/crates/mcp/tests/it/e2e.rs index 66cb934605..03d77c7e26 100644 --- a/crates/mcp/tests/it/e2e.rs +++ b/crates/mcp/tests/it/e2e.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -use lightcraft_mcp::{Backend, Headless, ProgressHook, Remote, Server, base64_decode}; +use lightcraft_mcp::{Backend, Headless, ProgressHook, Remote, Server, base64_decode, call_tool}; use serde_json::{Value, json}; /// Feed a whole session through `Server::serve` and return the replies by id. @@ -184,6 +184,28 @@ fn remote_unreachable_is_a_tool_error() { assert!(r[0]["result"]["content"][0]["text"].as_str().unwrap().contains("not reachable")); } +/// Eight distinct 1200×800 PNGs on disk, imported into a filesystem session with their real +/// metadata: a library of actual files for the batch export to read and develop. Generating the +/// procedural 6000×4000 demo library (and rendering from it) costs far more than this test's +/// contract — progress, cancellation and atomic 1200-edge PNG writes — needs. +fn file_backed_photos(dir: &std::path::Path) -> Headless { + let originals = dir.join("originals"); + std::fs::create_dir_all(&originals).unwrap(); + let mut paths = Vec::new(); + for i in 0..8usize { + let path = originals.join(format!("frame-{i:02}.png")); + let img = lightcraft_raster::Rgba8::from_fn(1200, 800, |x, y| { + [((x + i * 37) % 256) as u8, ((y * 3 + i * 11) % 256) as u8, ((x / 4 + y / 2 + i * 23) % 256) as u8, 255] + }); + lightcraft_mcp::write_image(&path, &img, 90).unwrap(); + paths.push(path.to_string_lossy().to_string()); + } + let mut headless = Headless::new(lightcraft_engine::Session::new().with_fs()); + let imported = call_tool(&mut headless, "import", &json!({"paths": paths})); + assert!(!imported.is_error, "{imported:?}"); + headless +} + /// [`Headless`] whose batch export, while `hold` is set, waits once its first photo is written: /// it keeps asking the server's progress hook at that photo boundary (which answers pings and /// reads cancels meanwhile) until the request is cancelled or the test clears `hold`. A ping or @@ -236,7 +258,7 @@ fn export_progress_and_cancel() { let (input, mut send) = std::io::pipe().unwrap(); let (output, write) = std::io::pipe().unwrap(); let hold = Arc::new(AtomicBool::new(true)); - let backend = HoldAfterFirst { inner: Headless::demo(), hold: hold.clone() }; + let backend = HoldAfterFirst { inner: file_backed_photos(&dir), hold: hold.clone() }; let server = std::thread::spawn(move || Server::new(Box::new(backend)).serve(BufReader::new(input), write).unwrap()); let (tx, rx) = std::sync::mpsc::channel(); let reader = std::thread::spawn(move || { @@ -250,8 +272,16 @@ fn export_progress_and_cancel() { let next = || rx.recv_timeout(Duration::from_secs(30)).expect("MCP reply/progress"); writeln!(send, "{}", call(1, "query_photos", json!({"limit":8}))).unwrap(); let query = next(); - let ids: Vec<_> = query["result"]["structuredContent"]["photos"].as_array().unwrap().iter().map(|p| p["id"].clone()).collect(); - assert_eq!(ids.len(), 8); + let photos = query["result"]["structuredContent"]["photos"].as_array().unwrap(); + assert_eq!(photos.len(), 8); + // Real files, real metadata: eight distinct 1200×800 PNGs probed from disk, not synthesized. + let names: std::collections::HashSet<&str> = photos.iter().map(|p| p["fileName"].as_str().unwrap()).collect(); + assert_eq!(names.len(), 8, "{names:?}"); + for p in photos { + assert_eq!((p["width"].as_u64(), p["height"].as_u64()), (Some(1200), Some(800)), "{p}"); + assert_eq!(p["format"], "PNG", "{p}"); + } + let ids: Vec<_> = photos.iter().map(|p| p["id"].clone()).collect(); let mut request = call(2, "export", json!({"ids":ids,"dir":dir.join("complete"),"longEdge":1200,"format":"png"})); request["params"]["_meta"] = json!({"progressToken":"photos","io.modelcontextprotocol/protocolVersion":"2026-07-28"}); writeln!(send, "{request}").unwrap(); @@ -314,10 +344,24 @@ fn export_progress_and_cancel() { let photos: Vec<_> = std::fs::read_dir(&out).unwrap().map(|e| e.unwrap().path()).filter(|p| *p != decoy).collect(); assert_eq!(photos.len(), 1, "only the photo completed before the cancel remains: {photos:?}"); for path in &photos { - lightcraft_codecs::decode(&std::fs::read(path).unwrap(), Default::default()).expect("complete image, no partial/temp file"); + let d = lightcraft_codecs::decode(&std::fs::read(path).unwrap(), Default::default()).expect("complete image, no partial/temp file"); + assert_eq!((d.width, d.height), (1200, 800), "{}", path.display()); } - assert_eq!(std::fs::read_dir(dir.join("complete")).unwrap().count(), 8, "earlier export retained"); + // The finished export's eight outputs are real 1200×800 PNGs, one per distinct original. + let complete: Vec<_> = std::fs::read_dir(dir.join("complete")).unwrap().map(|e| e.unwrap().path()).collect(); + assert_eq!(complete.len(), 8, "earlier export retained: {complete:?}"); + let mut exported = std::collections::HashSet::new(); + for path in &complete { + let bytes = std::fs::read(path).unwrap(); + assert_eq!(&bytes[1..4], b"PNG", "{}", path.display()); + let d = lightcraft_codecs::decode(&bytes, Default::default()).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + assert_eq!((d.width, d.height), (1200, 800), "{}", path.display()); + exported.insert(bytes); + } + assert_eq!(exported.len(), 8, "eight distinct outputs, not copies of one export"); drop(send); + // The fixture's files stay in place until the server (which owns the session) and the reader + // have finished: the session reads the originals; the output is what the checks above read. server.join().unwrap(); reader.join().unwrap(); std::fs::remove_dir_all(dir).unwrap(); diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 245b24f750..2047174da2 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -324,7 +324,13 @@ fn walk_jpeg(b: &[u8], i: &mut usize) -> Option<(usize, u8)> { if m == 0xda { // entropy-coded data up to the next marker that is not a stuffed 0xff or a restart loop { - *i += b.get(*i..)?.iter().position(|&x| x == 0xff)?; + let Some(p) = b.get(*i..)?.iter().position(|&x| x == 0xff) else { + // the terminal search examined the rest of the slice: charge it before failing, + // or a crafted file can repeat full-file searches while the budget stays positive + *i = b.len(); + return None; + }; + *i += p; match *b.get(*i + 1)? { 0x00 | 0xd0..=0xd7 => *i += 2, 0xff => *i += 1, @@ -348,9 +354,29 @@ fn walk_jpeg(b: &[u8], i: &mut usize) -> Option<(usize, u8)> { /// share a budget of eight times the file's size, so the scan stays linear (a real file's candidates /// fail within a few bytes, or are the preview itself). fn scan_for_jpeg(data: &[u8]) -> Option> { + scan_with_budget(data, scan_budget(data.len())).0 +} + +/// The walks' shared budget: eight times the file's size, at least 1 MiB (a tiny file still gets a +/// useful allowance), saturating so a hostile length cannot wrap it. +fn scan_budget(len: usize) -> usize { + len.saturating_mul(8).max(1 << 20) +} + +/// [`scan_for_jpeg`] with the walks' shared budget passed in, returning the walks' charged extent: +/// `(preview, bytes charged)`. The counter records the work each walk did whether or not budget +/// remains, so it stays a measure of work rather than of `budget - left`; a last walk may overshoot +/// the remaining budget by its own reach. It is a conservative charged-work measure, not literally +/// the bytes read: marker payloads skipped by `*i += len` are charged without being read, and a +/// failed terminal search is charged to the end of the slice. That over-count is safe for the +/// budget. It is a `u64` with saturating addition because the unbudgeted worst case is quadratic in +/// the file size (32-bit `usize` could otherwise overflow), and it costs one addition per +/// candidate, no allocation. +fn scan_with_budget(data: &[u8], budget: usize) -> (Option>, u64) { let mut best: Option<(usize, usize)> = None; let mut at = 1; - let mut budget = data.len().saturating_mul(8).max(1 << 20); + let mut left = budget; + let mut walked = 0u64; while let Some(off) = data.get(at..).and_then(|s| s.iter().position(|&b| b == 0xd8)) { let d8 = at + off; at = d8 + 1; @@ -360,8 +386,10 @@ fn scan_for_jpeg(data: &[u8]) -> Option> { let start = d8 - 1; // `at` starts at 1, so d8 >= 1 let mut reach = start; let found = jpeg_extent(data, start, &mut reach); - budget = budget.saturating_sub(reach.saturating_sub(start)); - if budget == 0 { + let spent = reach.saturating_sub(start); + left = left.saturating_sub(spent); + walked = walked.saturating_add(spent as u64); + if left == 0 { break; } if let Some((end, 3)) = found { @@ -371,12 +399,14 @@ fn scan_for_jpeg(data: &[u8]) -> Option> { at = end; } } - let (start, end) = best?; - let mut jpeg = data.get(start..end)?.to_vec(); - if let Some(first) = jpeg.first_mut() { - *first = 0xff; - } - Some(jpeg) + let jpeg = best.and_then(|(start, end)| { + let mut jpeg = data.get(start..end)?.to_vec(); + if let Some(first) = jpeg.first_mut() { + *first = 0xff; + } + Some(jpeg) + }); + (jpeg, walked) } /// Canon CR3: the full-size JPEG track (see [`lightcraft_meta::cr3`]), else the `PRVW` / `THMB` boxes. diff --git a/crates/raw/src/preview/tests.rs b/crates/raw/src/preview/tests.rs index 286ad1d777..2ee098414f 100644 --- a/crates/raw/src/preview/tests.rs +++ b/crates/raw/src/preview/tests.rs @@ -300,19 +300,97 @@ fn scan_jpeg(nc: u8, n: usize) -> Vec { j } -/// A crafted container full of JPEG-like starts (each walk running on through the ones after it) -/// is scanned in linear time: the walks share a budget (without it this 40 MB file takes about a minute). -#[test] -fn a_scan_through_many_fake_jpegs_stays_linear() { +/// `units` copies of a 200-byte record whose `d8 ff db` start runs each walk on through the +/// starts after it (a crafted Minolta-like container). +fn many_fake_jpegs(units: usize) -> Vec { let mut unit = vec![0u8, 0xd8, 0xff, 0xdb, 0x00, 0x02, 0xff, 0xda, 0x00, 0x02]; unit.extend(std::iter::repeat_n(0x55u8, 190)); let mut f = b"\0MRM\0\x01\0\0".to_vec(); - for _ in 0..200_000 { + f.reserve(units * unit.len()); + for _ in 0..units { f.extend_from_slice(&unit); } - let t = std::time::Instant::now(); + f +} + +/// The production scan's budget policy, restated here so the tests assert an expected bound +/// independently instead of calling the helper under test: eight times the file's size, at +/// least 1 MiB. +fn policy_budget(len: usize) -> u64 { + (len as u64 * 8).max(1 << 20) +} + +/// A crafted 40 MB container full of JPEG-like starts (each walk running on through the starts +/// after it) is scanned in bounded work: the walks share a budget (without it this file would +/// walk about 8·10^10 bytes), so the charged work stays within the policy budget plus one walk's +/// reach. No wall clock: this cannot fail on a slow runner, and removing the budget shows up as +/// the work exploding. +#[test] +fn a_scan_through_many_fake_jpegs_stays_linear() { + let f = many_fake_jpegs(200_000); + assert_eq!(f.len(), 40_000_008, "the adversarial fixture the budget was measured on"); + let (found, walked) = scan_with_budget(&f, policy_budget(f.len()) as usize); + assert_eq!(found, None); + let bound = policy_budget(f.len()) + f.len() as u64; + assert!(walked <= bound, "walked {walked} bytes of a {} byte file (bound {bound})", f.len()); +} + +/// A walk whose final entropy search finds no `ff` has examined the rest of the file, so that +/// search is charged to the budget before the walk fails. Without the charge the 40 MB tail is +/// scanned for free and only the few KB before it are reported, while `left` stays positive and +/// the budget no longer bounds the work. +#[test] +fn a_failed_terminal_search_is_charged_to_the_budget() { + let mut f = many_fake_jpegs(20); + f.extend(std::iter::repeat_n(0x55u8, 40_000_000)); + let (found, walked) = scan_with_budget(&f, 1); + assert_eq!(found, None); + assert!(walked >= 40_000_000, "terminal search was not charged: {walked} of {} bytes", f.len()); +} + +/// The production wrapper stops at the same shared budget. Eighteen fake candidates each declare +/// a segment length that makes their walk jump 0xFFF0 bytes, exhausting the tiny file's 1 MiB +/// allowance before the valid JPEG after them: the wrapper finds nothing. A `usize::MAX` budget +/// (or no budget at all) would walk on and return that JPEG. +#[test] +fn the_production_wrapper_stops_at_the_shared_budget() { + let mut f = b"\0MRM\0\x01\0\0".to_vec(); + for _ in 0..18 { + f.extend_from_slice(&[0x00, 0xd8, 0xff, 0xe1, 0xff, 0xf0]); + } + f.extend_from_slice(&scan_jpeg(3, 100)); assert_eq!(scan_for_jpeg(&f), None); - assert!(t.elapsed() < std::time::Duration::from_secs(10), "{:?}", t.elapsed()); +} + +/// The shared budget is what bounds the work, and the counter reports the work itself. The same +/// generator at 200 starts is a 40 KB file: without the budget (never decremented, or restarted +/// for each candidate) the scan walks all 200 walks, 4 MB, instead of about 1 MB — so a negative +/// run costs megabytes, not the 8·10^10 bytes of the 40 MB fixture. +#[test] +fn the_scan_budget_bounds_the_work_a_crafted_file_can_cause() { + let f = many_fake_jpegs(200); + let budget = policy_budget(f.len()); + let (found, walked) = scan_with_budget(&f, budget as usize); + assert_eq!(found, None); + assert!(walked >= (f.len() as u64) / 2, "the walks ran: {walked} bytes of {}", f.len()); + assert!(walked <= budget + (f.len() as u64), "walked {walked} with a {budget} byte budget"); + // The counter records the work, not the unspent budget: a budget of one file size is spent + // by the first walk, and the second walk is still counted in full (it overshoots). + let (_, spent) = scan_with_budget(&f, f.len()); + let len = f.len() as u64; + assert!(spent > len && spent <= 2 * len, "spent {spent} with a {len}-byte budget"); + // No candidate, no work counted. + let none = b"no jpeg here"; + assert_eq!(scan_with_budget(none, policy_budget(none.len()) as usize).1, 0); + // One real candidate: the counter counts exactly the reach of its walk. + let mut one = vec![0u8]; + let jpeg = scan_jpeg(3, 100); + one.extend_from_slice(&jpeg); + let (found, walked) = scan_with_budget(&one, policy_budget(one.len()) as usize); + assert!(found.is_some()); + assert_eq!(walked, jpeg.len() as u64, "one walk of the JPEG's {} bytes", jpeg.len()); + // The production wrapper takes the same path and returns the counter's preview. + assert_eq!(scan_for_jpeg(&one).as_deref(), found.as_deref()); } /// Containers whose structure we do not walk give up their largest colour JPEG, found by scanning. diff --git a/crates/ui-egui/examples/thumb_request_bench.rs b/crates/ui-egui/examples/thumb_request_bench.rs index 166c0e9b62..34fe5fc883 100644 --- a/crates/ui-egui/examples/thumb_request_bench.rs +++ b/crates/ui-egui/examples/thumb_request_bench.rs @@ -113,7 +113,7 @@ fn micro(points: usize, pending: bool) { let key = app.session.thumb_job(id, 384).unwrap().key; app.renderer.textures.insert( Slot::Thumb(id), - Tex { key, photo: id, tex: texture.clone(), size: [1, 1], histogram: None, ms: 0.0, quick: None, pixels: None }, + Tex { look: None, key, photo: id, tex: texture.clone(), size: [1, 1], histogram: None, ms: 0.0, quick: None, pixels: None }, ); } request_thumb(&mut app, id, 384, 10); diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 30a672c52c..98f8458c9d 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -53,6 +53,7 @@ impl LightcraftApp { slideshow_hidden_at: None, slideshow_auto_paused: false, dropped_files: vec![], + deferred_dropped_file_count: 0, synthetic_in_flight: false, ui_passes: 0, #[cfg(test)] diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 5d9ed2f37c..efae98a9b0 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -215,9 +215,15 @@ impl LightcraftApp { self.last_time = t; panels::denoise::pump_ui_input(self, ctx, now); panels::faces::pump_ui_input(self, ctx, now); - // A prompt owns the library too: files dropped while it is up wait in the queue and are - // imported on the first UI pass after it goes away, never behind it. - if !panels::notices::blocks_input(self) { + // A prompt or the blocking library window owns the library too: files dropped while either + // is up wait in the queue and are imported on the first UI pass after it goes away, never + // behind it. With no library open the window's `hold_dropped` path still takes the photos + // once its own dialog and asynchronous library picker are gone, so `blocks_file_drops` + // only holds them back until then. Mark those drops: once the barrier opens they may find + // a dialog, Lightroom import or import that the library's own open started still running. + if panels::notices::blocks_input(self) || panels::library_problem::blocks_file_drops(self) { + self.defer_dropped_files(); + } else { self.import_dropped_files(ctx); } self.check_external_editor(ctx); diff --git a/crates/ui-egui/src/app/input.rs b/crates/ui-egui/src/app/input.rs index 0757ecbe09..f94108ce01 100644 --- a/crates/ui-egui/src/app/input.rs +++ b/crates/ui-egui/src/app/input.rs @@ -1,15 +1,50 @@ use super::*; impl LightcraftApp { + /// Preserve the currently queued handles while a prompt or blocking library window owns + /// drops. Hosts call this in their UI phase even if the app is not ready to draw yet. + pub fn defer_dropped_files(&mut self) { + self.deferred_dropped_file_count = self.dropped_files.len(); + } + /// Take the host's dropped files, exactly once. The queue is filled by `raw_input_hook`, so a /// hidden logic tick can never replay the last pass's drop. Web hosts call this in their UI /// phase (they need `bytes_async`, which is not available on a native file path). pub fn take_dropped_files(&mut self) -> Vec { + self.deferred_dropped_file_count = 0; std::mem::take(&mut self.dropped_files) } + /// Take only drops that did not wait behind a prompt or blocking library window. + /// Hosts call this in the UI phase to refuse fresh arrivals behind an ordinary dialog + /// while preserving the earlier protected prefix for a later pass. + pub fn take_unprotected_dropped_files(&mut self) -> Vec { + if self.deferred_dropped_file_count == 0 { + return self.take_dropped_files(); + } + self.dropped_files.split_off(self.deferred_dropped_file_count.min(self.dropped_files.len())) + } + /// Import native drops as presets, face models or photos. pub(super) fn import_dropped_files(&mut self, ctx: &egui::Context) { + // A drop that waited behind a prompt or the library window is not dropped again just + // because a dialog, Lightroom catalog import, a running Synchronize Folder or the import + // a library open started is still running when the barrier goes. Keep it queued and try + // the next pass. A drop made outside the barrier keeps the deliberate "close the + // dialog"/"An import is running" refusal below. + if self.deferred_dropped_file_count > 0 + && (self.ui.dialog.is_some() || self.lightroom.is_some() || self.sync_run.is_some() || import::running(self)) + { + // Only the earlier barrier-held prefix waits. Fresh drops must still be handled + // under the current dialog/import policy instead of inheriting that protection. + #[cfg(not(target_arch = "wasm32"))] + { + let fresh = self.take_unprotected_dropped_files(); + self.handle_dropped_files(ctx, fresh); + } + ctx.request_repaint_after(std::time::Duration::from_millis(100)); + return; + } let dropped = self.take_dropped_files(); if dropped.is_empty() { return; @@ -35,10 +70,15 @@ impl LightcraftApp { return; } let drop = drop_overlay::classify(dropped); - if let Some(model) = drop.models.first() - && let Err(e) = panels::faces::open_dialog(self, model) - { - self.toast(ctx, e); + if let Some(model) = drop.models.first() { + // no library open yet (the launch list, a library that failed to open): its licence + // dialog would open behind the window that owns input. The drop is refused like a + // preset; photos below still wait for the library chosen. + if panels::library_problem::holds_drops(self) { + self.toast(ctx, crate::i18n::tr("Open a library first, then drop the model again")); + } else if let Err(e) = panels::faces::open_dialog(self, model) { + self.toast(ctx, e); + } } if !drop.presets.is_empty() { // presets live in a library: with none open they would go with the session it replaces diff --git a/crates/ui-egui/src/app/screenshot.rs b/crates/ui-egui/src/app/screenshot.rs index e97d257eac..c2de3aae61 100644 --- a/crates/ui-egui/src/app/screenshot.rs +++ b/crates/ui-egui/src/app/screenshot.rs @@ -131,6 +131,25 @@ impl LightcraftApp { Ok(()) } + /// Finish a rendered headless capture, growing its reservation before writing if needed. + /// Every path replies once and releases the request's reserved bytes. + pub(crate) fn finish_headless_shot(&mut self, shot: &mut PendingShot, img: &egui::ColorImage) { + let bytes = img.size[0].checked_mul(img.size[1]).and_then(|p| p.checked_mul(std::mem::size_of::())); + let result = match bytes { + Some(bytes) if bytes > shot.reserved => self.grow_shot_reservation(shot.reserved, bytes).map(|()| { + shot.reserved = bytes; + }), + Some(_) => Ok(()), + None => Err("ui.screenshot: the rendered image has invalid dimensions".to_owned()), + }; + let reply = match result { + Ok(()) => control::save_screenshot(self, img, shot.target.as_ref()), + Err(error) => serde_json::json!({"ok": false, "error": error}), + }; + let _ = shot.reply.send(reply); + self.release_shot_bytes(shot.reserved); + } + /// Advance pending screenshots: wait (≥ 3 completed UI frames, ≥ 120 ms) until no renders are /// in flight — or until the forced-capture instant, so sustained rendering cannot turn a /// request into a timeout — then capture through the compositor's one-shot callback, or @@ -183,31 +202,7 @@ impl LightcraftApp { let img = if shadow_ticked { None } else { self.headless_screenshot(ctx, true) }; shadow_ticked = true; if let Some(img) = img { - let bytes = img.size[0].checked_mul(img.size[1]).and_then(|p| p.checked_mul(std::mem::size_of::())); - match bytes { - Some(bytes) if bytes > shot.reserved => { - // A stale/undersized reservation is not a user error: grow it to the - // actual image and encode it when the per-image and aggregate caps allow. - if let Err(error) = self.grow_shot_reservation(shot.reserved, bytes) { - let _ = shot.reply.send(serde_json::json!({"ok": false, "error": error})); - self.release_shot_bytes(shot.reserved); - continue; - } - shot.reserved = bytes; - } - Some(_) => {} - None => { - let reply = serde_json::json!({ - "ok": false, - "error": "ui.screenshot: the rendered image has invalid dimensions", - }); - let _ = shot.reply.send(reply); - self.release_shot_bytes(shot.reserved); - continue; - } - } - let _ = shot.reply.send(control::save_screenshot(self, &img, shot.target.as_ref())); - self.release_shot_bytes(shot.reserved); + self.finish_headless_shot(&mut shot, &img); continue; } keep.push(shot); diff --git a/crates/ui-egui/src/dialogs/slideshow_export.rs b/crates/ui-egui/src/dialogs/slideshow_export.rs index ed33d0341e..7a898b5db2 100644 --- a/crates/ui-egui/src/dialogs/slideshow_export.rs +++ b/crates/ui-egui/src/dialogs/slideshow_export.rs @@ -132,11 +132,14 @@ pub(super) fn slideshow_export_body(app: &mut LightcraftApp, ui: &mut egui::Ui, mod tests { use super::*; use crate::state::Dialog; + use crate::tests_fixtures::SyntheticLibrary; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; - /// The app with a picker that answers `answer` and remembers what it was asked. - fn app_answering(answer: String) -> (LightcraftApp, Arc>>) { + /// The app, with two real file-backed photos and a picker that answers `answer` and remembers + /// what it was asked. Keep the returned fixture alive until the app is dropped. + fn app_answering(tag: &str, answer: String) -> (SyntheticLibrary, LightcraftApp, Arc>>) { + let files = SyntheticLibrary::with_photos(tag, 2); let asked = Arc::new(Mutex::new(Vec::::new())); let requests = asked.clone(); let services = crate::Services { @@ -148,9 +151,10 @@ mod tests { })), ..Default::default() }; - let mut s = lightcraft_engine::Session::with_demo(); - s.execute("slideshow.playback", &json!({"slideDuration": 1, "fadeDuration": 0.5})).unwrap(); - (LightcraftApp::new(s, services), asked) + let mut app = files.app(); + app.services = services; + app.session.execute("slideshow.playback", &json!({"slideDuration": 1, "fadeDuration": 0.5})).unwrap(); + (files, app, asked) } fn run_dialog(h: &mut crate::headless::Headless) -> Value { @@ -164,7 +168,11 @@ mod tests { if h.app.slideshow_export_reported && h.app.session.slideshow_export.is_some() { break; } - assert!(Instant::now() < deadline, "export did not finish"); + assert!( + Instant::now() < deadline, + "export did not finish: {}", + h.app.session.slideshow_export.as_ref().map_or_else(|| "no export run".into(), |r| r.json().to_string()) + ); std::thread::sleep(Duration::from_millis(5)); } let run = h.app.session.slideshow_export.clone().unwrap(); @@ -179,7 +187,7 @@ mod tests { let dir = std::env::temp_dir().join(format!("lc-ui-ssexp-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let path = dir.join("Show.pdf").to_string_lossy().to_string(); - let (mut app, asked) = app_answering(path.clone()); + let (_files, mut app, asked) = app_answering("ssexp", path.clone()); assert!(crate::menus::ui_enabled(&app, "dialog.slideshowExport")); app.run("dialog.slideshowExport", json!({"format": "pdf"})).unwrap(); let Some(Dialog::SlideshowExport { settings }) = app.ui.dialog.as_mut() else { panic!("no slideshow export dialog") }; @@ -202,7 +210,7 @@ mod tests { let dir = std::env::temp_dir().join(format!("lc-ui-ssexp-v-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let path = dir.join("Show.mp4").to_string_lossy().to_string(); - let (mut app, asked) = app_answering(path.clone()); + let (_files, mut app, asked) = app_answering("ssexp-v", path.clone()); app.run("dialog.slideshowExport", json!({"format": "video"})).unwrap(); let Some(Dialog::SlideshowExport { settings }) = app.ui.dialog.as_mut() else { panic!("no dialog") }; assert_eq!((settings.size, settings.fps, settings.container.as_str()), (720, 30, "mp4")); @@ -215,7 +223,7 @@ mod tests { assert!(r["sawRow"].as_bool().unwrap(), "the export showed in the activity stack"); assert!(std::path::Path::new(&path).is_file()); // JPEG: a save dialog names the folder (no file type) - let (mut app, asked) = app_answering(dir.join("Frames").to_string_lossy().to_string()); + let (_files_jpeg, mut app, asked) = app_answering("ssexp-jpeg", dir.join("Frames").to_string_lossy().to_string()); app.run("app.slideshowExport", json!({"format": "jpeg", "size": 64})).unwrap(); assert!(asked.lock().unwrap()[0].filter.is_none()); let _ = std::fs::remove_dir_all(&dir); diff --git a/crates/ui-egui/src/drop_overlay.rs b/crates/ui-egui/src/drop_overlay.rs index 8be75ee888..dfe8262c8c 100644 --- a/crates/ui-egui/src/drop_overlay.rs +++ b/crates/ui-egui/src/drop_overlay.rs @@ -32,13 +32,15 @@ fn lines(app: &LightcraftApp, paths: Vec, unknown: bool) -> Vec } let drop = classify(paths); let mut out = Vec::new(); - if !drop.models.is_empty() { - out.push(crate::i18n::tr("Drop to open the face model").to_string()); - } let plural = |n: usize| if n == 1 { "" } else { "s" }; // no library open yet (desktop: the web build's drops import into its session as before): - // photos wait for the library chosen, presets are refused (the drop handler does the same) + // photos wait for the library chosen, presets and models are refused (the drop handler does the same) let waiting = crate::panels::library_problem::holds_drops(app) && !cfg!(target_arch = "wasm32"); + if !drop.models.is_empty() && waiting { + out.push(crate::i18n::tr("Open a library first, then drop the model again").to_string()); + } else if !drop.models.is_empty() { + out.push(crate::i18n::tr("Drop to open the face model").to_string()); + } if !drop.presets.is_empty() && waiting { out.push(crate::i18n::tr("Open a library first, then drop the presets again").to_string()); } else if !drop.presets.is_empty() { @@ -69,9 +71,11 @@ fn lines(app: &LightcraftApp, paths: Vec, unknown: bool) -> Vec /// Veil and card while files are dragged over the window. pub fn show(app: &LightcraftApp, ctx: &Context) { - // This card is an `Order::Tooltip` layer, above the prompt's Foreground window. While a prompt - // owns input the drag can only be background noise: do not paint over or register it. - if crate::panels::notices::blocks_input(app) { + // This card is an `Order::Tooltip` layer, above the prompt's or blocking library window's + // Foreground layer (and any dialog that holds a no-library window's drops). While input is + // owned the drag can only be background noise: do not paint over or register it, or the card + // would promise a drop the barrier is holding back. + if crate::panels::notices::blocks_input(app) || crate::panels::library_problem::blocks_file_drops(app) { return; } let count = ctx.input(|i| i.raw.hovered_files.len()); diff --git a/crates/ui-egui/src/headless/tests.rs b/crates/ui-egui/src/headless/tests.rs index 80d559efc8..f38ed636ae 100644 --- a/crates/ui-egui/src/headless/tests.rs +++ b/crates/ui-egui/src/headless/tests.rs @@ -690,6 +690,14 @@ fn adding_a_face_model_shows_its_terms_and_installs_only_once_accepted() { assert_eq!(r["ok"], true, "{r}"); h.step(); assert!(matches!(&h.app.ui.dialog, Some(Dialog::FaceModel { accepted: true, .. }))); + // Consent is accepted, but no model is installed until confirmation. Keep this UI + // fixture on the CPU so it does not start an unrelated background GPU probe. + let models = dir.join("models"); + std::fs::create_dir_all(&models).unwrap(); + std::fs::write(models.join("settings.json"), br#"{"cpuOnly":true}"#).unwrap(); + assert_eq!(h.app.session.execute("faces.gpu", &json!({})).unwrap()["cpuOnly"], true); + let before_confirm = h.app.session.execute("faces.models.list", &json!({})).unwrap(); + assert!(before_confirm["models"].as_array().unwrap().iter().all(|m| m["installed"] != true), "{before_confirm}"); let r = h.request("ui.dialog.confirm", json!({}), t); assert_eq!(r["ok"], true, "{r}"); let listed = h.app.run("faces.models.list", json!({})).unwrap(); @@ -798,7 +806,13 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() { let dir = std::env::temp_dir().join(format!("lc-ui-facesetup-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - h.app.session.face_models_dir = Some(dir.join("models")); + // The setup offer tests recognition controls, not GPU attachment. Seed only this + // fixture's settings before any model can load, without detaching a shared detector. + let models = dir.join("models"); + std::fs::create_dir_all(&models).unwrap(); + std::fs::write(models.join("settings.json"), br#"{"cpuOnly":true}"#).unwrap(); + h.app.session.face_models_dir = Some(models); + assert_eq!(h.app.session.execute("faces.gpu", &json!({})).unwrap()["cpuOnly"], true); let runtime = h.app.session.execute("faces.models.list", &json!({})).unwrap()["runtime"] == true; let id = h.app.session.active().unwrap(); let mut meta = h.app.session.catalog.photo(id).unwrap().meta.clone(); @@ -1244,14 +1258,29 @@ fn a_screenful_of_small_faces_is_not_evicted_and_left_blank() { } // the smallest faces, so a lot of them fit on the screen h.request("ui.set", json!({"thumbSize": 90.0, "view": "people"}), t); - h.settle(SETTLE); + // Lay out the faces before waiting for their workers; a global settle also waits for + // unrelated work and hides a timed-out initial wait. + for _ in 0..4 { + h.step(); + } h.step(); let drawn = h.request("ui.widgets", json!({"filter": "unnamed-face:"}), t)["result"].as_array().map_or(0, Vec::len); assert!(drawn > 96, "the test needs more faces on screen than the usual budget, got {drawn}"); // the pictures are made and kept, well beyond the old budget (a few demo photos share a scene, so two faces can share a // picture: the count is a little under the number of tiles) let started = Instant::now(); - while h.app.renderer.variant_textures() < drawn - 20 && started.elapsed() < Duration::from_secs(60) { + let timeout = Duration::from_secs(300); + while h.app.renderer.variant_textures() < drawn - 20 { + let left = timeout.saturating_sub(started.elapsed()); + assert!( + !left.is_zero(), + "variant renders did not finish: {} pictures for {drawn} faces, pending {:?}", + h.app.renderer.variant_textures(), + h.app.renderer.pending_slots() + ); + // Coverage instrumentation makes the procedural scenes expensive. Wait for actual + // worker progress instead of continuously rebuilding a full People-view frame. + h.app.renderer.wait_for_worker(left.min(Duration::from_millis(250))); h.step(); std::thread::sleep(Duration::from_millis(20)); } @@ -2356,8 +2385,7 @@ fn click_field_in_view(h: &mut Headless, key: &str, timeout: Duration) { /// accessibility and place fields reach the photo. #[test] fn info_fields_keep_typing_and_save() { - // (tall enough for the place fields to show without scrolling) - let mut h = demo([1300.0, 1200.0]); + let mut h = demo([1300.0, 1000.0]); let t = Duration::from_secs(10); h.request("ui.set", json!({"view": "detail", "right": "info"}), t); h.settle(SETTLE); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8da88f9aea..9db7035c1d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -43,6 +43,8 @@ pub mod window_place; mod tests; #[cfg(test)] use tests::panels as tests_panels; +#[cfg(test)] +use tests::{fixtures as tests_fixtures, render_fixtures as tests_render_fixtures}; use std::sync::mpsc::{Receiver, Sender}; @@ -191,6 +193,10 @@ pub struct LightcraftApp { slideshow_auto_paused: bool, /// Files dropped since the last UI pass, consumed exactly once by the UI phase. dropped_files: Vec, + /// Prefix of `dropped_files` that arrived behind a prompt, library window or its picker. + /// Preserve only this prefix through subsequent dialogs/imports; later arrivals still take + /// the ordinary refusal path unless they too arrive while the original barrier is up. + deferred_dropped_file_count: usize, /// A synthetic batch was injected into a pass and has not been consumed by a completed /// (nondiscarded) UI pass yet: a hidden logic tick must not inject another one. synthetic_in_flight: bool, diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 25f3baa240..eee9d7d0c2 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -184,13 +184,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { _ => vec![area], }; let main_area = *areas.last().unwrap_or(&area); + // A previous crop/rotation can stay visible while its replacement renders. Its pixels + // must not override the current frame geometry, even when it is the same photo. + let current_texture = |t: &&crate::render::Tex| t.photo == id && t.look == Some((crate::render::geometry_look(&d), !crop_tool)); let visible_texture_aspect = app .renderer .textures .get(&Slot::Main) - .filter(|t| t.photo == id) - .or_else(|| app.renderer.textures.get(&Slot::Preview).filter(|t| t.photo == id)) - .or_else(|| app.renderer.thumb(id)) + .filter(current_texture) + .or_else(|| app.renderer.textures.get(&Slot::Preview).filter(current_texture)) + .or_else(|| app.renderer.thumb(id).filter(current_texture)) .map(|tex| tex.size[0].max(1) as f32 / tex.size[1].max(1) as f32); // Follow the texture only where it really differs (an unsupported raw's embedded JPEG with // another crop): a texture's whole-pixel size is a little off the exact aspect, and the crop diff --git a/crates/ui-egui/src/panels/grid.rs b/crates/ui-egui/src/panels/grid.rs index c36feadf84..262979cc5f 100644 --- a/crates/ui-egui/src/panels/grid.rs +++ b/crates/ui-egui/src/panels/grid.rs @@ -710,6 +710,15 @@ pub fn request_thumb(app: &mut LightcraftApp, id: PhotoId, size: usize, priority { return; } + // Several cells can show one photo at different sizes (a print page's cells). Before any + // thumbnail arrives, a caller at another bucket would replace the pending request every frame + // and restart it, so none of them ever gets a picture: keep the request in flight. A higher + // priority caller may take it over once; the caller it replaced then coalesces behind it. + if let Some(photo) = app.session.catalog.photo(id) + && app.renderer.thumb_pending_other_bucket(photo, bucket, priority) + { + return; + } let Some(job) = app.session.thumb_job(id, size) else { return }; #[cfg(test)] { @@ -722,9 +731,9 @@ pub fn request_thumb(app: &mut LightcraftApp, id: PhotoId, size: usize, priority quick.is_some() && app.session.catalog.photo(id).is_some_and(|p| skips_the_real_render(app.session.embedded_thumb_is_final(p), true)); if let Some(photo) = app.session.catalog.photo(id) { if embedded_is_final { - app.renderer.remember_embedded_thumb(photo, bucket, job.key); + app.renderer.remember_embedded_thumb(photo, bucket, job.key, priority); } else { - app.renderer.remember_thumb(photo, bucket, job.key, quick.is_some()); + app.renderer.remember_thumb(photo, bucket, job.key, quick.is_some(), priority); } } match quick { diff --git a/crates/ui-egui/src/panels/library_problem.rs b/crates/ui-egui/src/panels/library_problem.rs index 31128e9a9f..544e29449a 100644 --- a/crates/ui-egui/src/panels/library_problem.rs +++ b/crates/ui-egui/src/panels/library_problem.rs @@ -733,6 +733,30 @@ pub fn holds_drops(app: &LightcraftApp) -> bool { app.session.library.is_none() && app.library_problem.as_ref().is_some_and(|p| !p.dismissed) } +/// Whether the blocking library window must hold file drops back. While a library is already +/// open, that window (the launch list over it, or the error of a failed switch) owns input: +/// importing a drop now would add photos, presets or a model dialog behind it. With no library +/// open, the desktop drop handler's [`hold_dropped`] path takes the photos for the library the +/// window opens instead, so the drop still has to be consumed. +/// +/// The launch list hides itself behind its own New Library name prompt (`chooser_owns_dialog`). +/// It still owns the library then: a queued drop must wait for the prompt to close, not be taken +/// and refused by the drop handler's generic open-dialog check. After the prompt, New Library and +/// Open Another… ask for a folder on the host's asynchronous picker; the library opens only when +/// that answer arrives, so the queued drop waits too. With no library open a dialog (or a library +/// picker that will open one) in front of the window would otherwise refuse the drop or consume it +/// before the photos could reach [`hold_dropped`]. +pub fn blocks_file_drops(app: &LightcraftApp) -> bool { + if app.library_problem.as_ref().is_none_or(|p| p.dismissed) { + return false; + } + if app.session.library.is_none() { + let choosing = app.pending_picks.iter().any(|p| p.command == "app.newLibrary" || p.command == "app.openLibrary"); + return app.ui.dialog.is_some() || choosing; + } + true +} + /// Photos dropped on the window while no library is open (the launch list, or the window of a /// library that failed to open): kept with the command line's files and imported once a library /// opens, instead of going into the session the next library replaces. Gives the photos back when diff --git a/crates/ui-egui/src/render.rs b/crates/ui-egui/src/render.rs index 31117b22ef..98356afb7d 100644 --- a/crates/ui-egui/src/render.rs +++ b/crates/ui-egui/src/render.rs @@ -66,7 +66,23 @@ pub enum Slot { HdrHistogram, } +/// Settings that can change the rendered frame. Colour-only edits, including temporarily +/// hidden grading wheels, must not discard a decoded RAW's actual aspect ratio. +pub(crate) fn geometry_look(settings: &lightcraft_develop::DevelopSettings) -> u64 { + let geometry = lightcraft_develop::DevelopSettings { + crop: settings.crop, + orientation: settings.orientation, + geometry: settings.geometry.clone(), + optics: settings.optics, + disabled_sections: settings.disabled_sections.iter().filter(|s| matches!(s.as_str(), "geometry" | "optics")).cloned().collect(), + ..Default::default() + }; + geometry.hash64() +} + pub struct Tex { + /// Frame geometry and crop mode captured when these pixels were requested. + pub look: Option<(u64, bool)>, pub key: u64, pub photo: PhotoId, pub tex: egui::TextureHandle, @@ -93,12 +109,14 @@ pub trait RenderOffload { /// worker (`None` for prefetches and failures). A 10 MP zoom window's conversion took a few ms of /// the UI thread on every slider step. struct Finished { + look: Option<(u64, bool)>, result: RenderResult, color: Option>, } /// Immutable identity captured when a request is submitted, not when its result is collected. struct Requested { + look: Option<(u64, bool)>, id: u64, source: Option, } @@ -190,6 +208,8 @@ pub struct Renderer { #[cfg(test)] pub(crate) thumb_jobs_built: usize, requests: HashMap, + /// Only jobs actually accepted by an offload: queued replacements retain identity in the job. + offload_looks: HashMap, #[cfg(test)] failure_messages: Vec, /// Request id of the pixels each interactive view (loupe, before, hover) shows: a draft that @@ -200,8 +220,9 @@ pub struct Renderer { epoch: u64, preview_generation: Option<(std::sync::Weak, u64)>, /// Last thumbnail inputs; weak identities do not retain photo histories or force deep clones. - /// (photo, size bucket, job key, whether the request went through an embedded stand-in) - thumb_inputs: HashMap, usize, u64, bool)>, + /// (photo, size bucket, job key, whether the request went through an embedded stand-in, and the + /// priority of the caller that queued it — a stand-in's background render runs below that) + thumb_inputs: HashMap, usize, u64, bool, u32)>, /// Photos whose grid thumbnail is the camera's embedded preview itself (no render behind it). embedded_thumbs: HashMap, usize, u64)>, pool: JobPool, @@ -283,6 +304,7 @@ impl Renderer { #[cfg(test)] thumb_jobs_built: 0, requests: HashMap::new(), + offload_looks: HashMap::new(), #[cfg(test)] failure_messages: Vec::new(), shown_ids: HashMap::new(), @@ -323,11 +345,27 @@ impl Renderer { impl Renderer { /// Would asking for this thumbnail again (at `priority`) change nothing? pub fn thumb_current(&self, photo: &Arc, bucket: usize, priority: u32) -> bool { - let Some(&(ref old, size, key, quick)) = self.thumb_inputs.get(&photo.id) else { return false }; - if old.as_ptr() != Arc::as_ptr(photo) || size != bucket { + let Some(&(ref old, size, key, quick, _)) = self.thumb_inputs.get(&photo.id) else { return false }; + if old.as_ptr() != Arc::as_ptr(photo) { return false; } let slot = Slot::Thumb(photo.id); + if size > bucket { + // A larger thumbnail that has already arrived for this photo and settings serves a + // smaller cell too (cells at different sizes share one photo). Asking for the smaller + // bucket would replace the larger picture, and the larger cell would then ask for its + // bucket back on the next frame: two print cells would alternate renders forever. + // The photo identity above is the renderer's `Weak`: `Catalog` edits through + // `Arc::make_mut`, which (per std) moves the value to a new allocation whenever a + // `Weak` exists, even with one strong reference. An edit therefore breaks this match, + // and the key keeps an older picture of the same allocation from being reused. A + // larger request still renders once (below), and while it is in flight + // [`Self::thumb_pending_other_bucket`] holds the smaller callers. + return self.textures.get(&slot).is_some_and(|t| t.key == key); + } + if size != bucket { + return false; + } if quick && !self.textures.contains_key(&slot) { // Stand-in path (`grid::request_thumb`): the embedded preview is still on its way, or // the real thumbnail is requested at the background priority whatever the caller's. @@ -346,16 +384,30 @@ impl Renderer { /// Whether the camera's embedded preview *is* this photo's thumbnail: its quick render arrived /// (so `Slot::ThumbQuick` holds the picture) and no edit has moved the photo on to a settings /// hash of its own. The grid stops here instead of decoding the raw behind a thumbnail the - /// embedded JPEG already covers — see `grid::skips_the_real_render`. + /// embedded JPEG already covers — see `grid::skips_the_real_render`. A preview that arrived at + /// a larger bucket and is still held is final for a smaller cell too, so print cells at + /// different sizes do not replace it every frame. pub fn thumb_quick_is_final(&self, photo: &Arc, bucket: usize, priority: u32) -> bool { let Some((old, size, key)) = self.embedded_thumbs.get(&photo.id) else { return false }; - old.as_ptr() == Arc::as_ptr(photo) && *size == bucket && !self.request_needed(Slot::ThumbQuick(photo.id), *key, priority) + if old.as_ptr() != Arc::as_ptr(photo) || *size < bucket { + return false; + } + let slot = Slot::ThumbQuick(photo.id); + if *size > bucket { + // A larger preview serves a smaller cell only when its pixels are actually here for + // the recorded key. The same-bucket failure memoization below says "no" after a failed + // job, but a missing larger preview is not a final picture: the smaller cell must ask + // for its own bucket rather than stay blank until the catalog revision changes. + return self.textures.get(&slot).is_some_and(|t| t.key == *key); + } + // The requested bucket itself: keep the failure memoization (`failed`) unchanged. + !self.request_needed(slot, *key, priority) } /// Whether this photo's embedded-preview thumbnail is pending at the requested priority, or /// already running. A queued priority change must reach the grid's stand-in request. pub fn thumb_quick_pending(&self, photo: &Arc, bucket: usize, priority: u32) -> bool { - let Some((old, size, key, quick)) = self.thumb_inputs.get(&photo.id) else { return false }; + let Some((old, size, key, quick, _)) = self.thumb_inputs.get(&photo.id) else { return false }; let slot = Slot::ThumbQuick(photo.id); old.as_ptr() == Arc::as_ptr(photo) && *size == bucket @@ -364,10 +416,44 @@ impl Renderer { && !self.request_needed(slot, *key, priority.saturating_add(1)) } + /// Whether a request for this photo's thumbnail is already in flight at another bucket and a + /// caller asking at `bucket` should leave it there. Several cells can show one photo at + /// different sizes at once (a print page's cells), and replacing `Slot::Thumb(id)` from each of + /// them every frame restarts the render, so none of them ever gets a picture. + /// + /// A caller with a higher priority than the one that queued the request takes it over once; + /// from then on the original caller coalesces behind that higher priority instead of replacing + /// it back. Equal priorities keep the request that is in flight (the print cells' case). This + /// only chooses which pending bucket survives; after it completes, a larger bucket is reused by + /// smaller callers ([`Self::thumb_current`]) rather than being replaced every frame. + /// + /// A full render queued behind a stand-in is submitted at [`crate::panels::grid::BACKGROUND_THUMB_PRIORITY`], + /// deliberately below an on-screen caller. That is not the caller's priority, so the comparison + /// uses the priority recorded with the inputs: comparing the background slot's own priority + /// would let two buckets keep replacing each other's background render every frame. + /// + /// A photo that changed (another `Arc`) or a texture for the recorded job key is not + /// coalesced: its callers ask for their own bucket as before. A stale texture from before an + /// edit does not count — it is still displayed while the replacement renders, and comparing + /// the key (not presence) keeps coalescing across buckets in the meantime. + pub fn thumb_pending_other_bucket(&self, photo: &Arc, bucket: usize, priority: u32) -> bool { + let Some(&(ref old, size, key, _, requested_priority)) = self.thumb_inputs.get(&photo.id) else { return false }; + if old.as_ptr() != Arc::as_ptr(photo) || size == bucket { + return false; + } + if self.textures.get(&Slot::Thumb(photo.id)).is_some_and(|t| t.key == key) { + return false; + } + // (the inputs must belong to the request still in flight, not to one that already ended) + let in_flight = self.pending.get(&Slot::ThumbQuick(photo.id)).is_some_and(|p| p.0 == key) + || self.pending.get(&Slot::Thumb(photo.id)).is_some_and(|p| p.0 == key); + in_flight && requested_priority >= priority + } + /// Remember that the camera's embedded preview is this photo's final thumbnail, so the grid's /// per-frame requests stop at it (the quick render's key, which [`Self::thumb_quick_is_final`] /// compares against). - pub fn remember_embedded_thumb(&mut self, photo: &Arc, bucket: usize, key: u64) { + pub fn remember_embedded_thumb(&mut self, photo: &Arc, bucket: usize, key: u64, priority: u32) { let slot = Slot::Thumb(photo.id); if self.pending.get(&slot).is_some_and(|(pending_key, _)| *pending_key != key) { // An edit can still be rendering when Undo restores this embedded-final thumbnail. @@ -377,20 +463,20 @@ impl Renderer { self.pool.reprioritize(|s, p| if *s == slot { None } else { Some(p) }); self.queue.retain(|q| q.slot != slot); } - self.remember_thumb(photo, bucket, key, true); + self.remember_thumb(photo, bucket, key, true, priority); self.embedded_thumbs.insert(photo.id, (Arc::downgrade(photo), bucket, key)); } - pub fn remember_thumb(&mut self, photo: &Arc, bucket: usize, key: u64, quick: bool) { + pub fn remember_thumb(&mut self, photo: &Arc, bucket: usize, key: u64, quick: bool, priority: u32) { if self.thumb_inputs.len() >= 1024 && !self.thumb_inputs.contains_key(&photo.id) { - self.thumb_inputs.retain(|_, (p, _, _, _)| p.strong_count() > 0); + self.thumb_inputs.retain(|_, (p, _, _, _, _)| p.strong_count() > 0); if self.thumb_inputs.len() >= 1024 && let Some(id) = self.thumb_inputs.keys().next().copied() { self.thumb_inputs.remove(&id); } } - self.thumb_inputs.insert(photo.id, (Arc::downgrade(photo), bucket, key, quick)); + self.thumb_inputs.insert(photo.id, (Arc::downgrade(photo), bucket, key, quick, priority)); } /// Views that render straight into the display profile's primaries (wide gamut); everything @@ -442,7 +528,7 @@ impl Renderer { lightcraft_engine::display::present(&mut r, d); } let color = r.rendered.as_ref().ok().map(|x| Arc::new(color_image(&x.image))); - Finished { result: r, color } + Finished { look: None, result: r, color } } fn request_needed(&self, slot: Slot, key: u64, priority: u32) -> bool { @@ -491,7 +577,10 @@ impl Renderer { } self.pending.insert(slot, (job.key, priority)); job.request_id = lightcraft_preview::next_tick(); - self.requests.insert(slot, Requested { id: job.request_id, source: Some(job.origin.clone()) }); + self.requests.insert( + slot, + Requested { look: Some((geometry_look(&job.settings), job.request.apply_crop)), id: job.request_id, source: Some(job.origin.clone()) }, + ); // (an offload keeps its own per-view stage caches; the flag tells it to) let job = if slot.is_view() { job.with_stages(self.stages.entry(slot).or_default().clone()) } else { job }; if self.offload.is_some() { @@ -509,8 +598,9 @@ impl Renderer { key, priority, Box::new(move || { + let look = Some((geometry_look(&job.settings), job.request.apply_crop)); let run = || if background { lightcraft_engine::memory::in_background(|| job.run()) } else { job.run() }; - Self::run_for(run, &display) + Finished { look, ..Self::run_for(run, &display) } }), ); } @@ -544,7 +634,10 @@ impl Renderer { .as_ref() .map(|job| job.origin.clone()) .or_else(|| job.embedded.as_ref().map(|(path, _, _)| lightcraft_catalog::Source::File { path: path.clone() })); - self.requests.insert(slot, Requested { id: job.request_id, source }); + self.requests.insert( + slot, + Requested { look: job.small.as_ref().map(|j| (geometry_look(&j.settings), j.request.apply_crop)), id: job.request_id, source }, + ); let key = job.key; let display = self.display.clone(); self.pool.submit(slot, key, priority, Box::new(move || Self::run_for(|| job.run(), &display))); @@ -563,7 +656,7 @@ impl Renderer { } self.pending.insert(slot, (key, priority)); let request_id = lightcraft_preview::next_tick(); - self.requests.insert(slot, Requested { id: request_id, source: None }); + self.requests.insert(slot, Requested { look: None, id: request_id, source: None }); let display = self.display.clone(); self.pool.submit(slot, key, priority, Box::new(move || Self::run_for(|| compose.result(request_id), &display))); } @@ -577,13 +670,16 @@ impl Renderer { self.prefetched.insert(slot, job.key); self.pending.insert(slot, (job.key, priority)); job.request_id = lightcraft_preview::next_tick(); - self.requests.insert(slot, Requested { id: job.request_id, source: Some(job.origin.clone()) }); + self.requests.insert( + slot, + Requested { look: Some((geometry_look(&job.settings), job.request.apply_crop)), id: job.request_id, source: Some(job.origin.clone()) }, + ); let key = job.key; self.pool.submit( slot, key, priority, - Box::new(move || Finished { result: lightcraft_engine::memory::in_background(|| job.run()), color: None }), + Box::new(move || Finished { look: None, result: lightcraft_engine::memory::in_background(|| job.run()), color: None }), ); } @@ -608,6 +704,7 @@ impl Renderer { /// Drop every texture and cached stage (another library was opened: photo ids changed meaning). pub fn forget_all(&mut self) { self.requests.clear(); + self.offload_looks.clear(); self.shown_ids.clear(); self.epoch = lightcraft_preview::next_tick(); self.preview_generation = None; @@ -653,10 +750,12 @@ impl Renderer { let Some(off) = self.offload.as_mut() else { return }; while let Some(i) = self.queue.iter().enumerate().max_by_key(|(_, q)| (q.priority, q.seq)).map(|(i, _)| i) { let q = self.queue.swap_remove(i); + let identity = (q.job.request_id, (geometry_look(&q.job.settings), q.job.request.apply_crop)); if let Some(job) = off.try_start(q.slot, q.job) { self.queue.push(Queued { job, ..q }); break; } + self.offload_looks.insert(identity.0, identity.1); } } @@ -665,6 +764,22 @@ impl Renderer { self.pending.len() } + /// Requests queued or running, excluding background preparation of neighbouring photos. + pub fn in_flight_without_prefetch(&self) -> usize { + self.pending.keys().filter(|slot| !matches!(slot, Slot::Prefetch(_))).count() + } + + /// Yield to native render workers until a result is ready or the timeout expires. + /// The result remains queued for the next poll. Browser and offloaded renderers do not wait. + /// Any native pending job, including prefetch, can satisfy the wait. A zero-worker test pool + /// must run its jobs inline first, or this call can only time out. + pub fn wait_for_worker(&mut self, timeout: std::time::Duration) -> bool { + if cfg!(target_arch = "wasm32") || self.offload.is_some() || self.pending.is_empty() { + return false; + } + self.pool.wait(timeout) + } + /// Test seam: make the renderer look busy without starting a job (capture deadlines). #[cfg(test)] pub(crate) fn force_in_flight_for_test(&mut self, count: usize) { @@ -832,14 +947,17 @@ impl Renderer { fn collect(&mut self, ctx: &egui::Context, session: &mut Session, inline_ms: f64) -> bool { let mut finished = Vec::new(); while let Some(done) = self.pool.try_recv() { - finished.push((done.slot, done.result.result, done.result.color, if done.ms > 0.0 { done.ms } else { inline_ms })); + finished.push((done.slot, done.result.result, done.result.color, done.result.look, if done.ms > 0.0 { done.ms } else { inline_ms })); } if let Some(off) = self.offload.as_mut() { - finished.extend(off.finished().into_iter().map(|(slot, r, ms)| (slot, r, None, ms))); + finished.extend(off.finished().into_iter().map(|(slot, r, ms)| { + let look = self.offload_looks.remove(&r.request_id); + (slot, r, None, look, ms) + })); } self.dispatch(); let mut changed = false; - for (mut slot, mut r, mut color, ms) in finished { + for (mut slot, mut r, mut color, completed_look, ms) in finished { self.completed += 1; // made for another display profile than the one in use: obsolete if r.display.is_some_and(|id| Some(id) != self.display_id()) { @@ -849,7 +967,7 @@ impl Renderer { // an interactive view still takes a superseded draft that is newer than what it // shows, and the source it decoded: a drag supersedes every request before it // finishes, and a source thrown away with them is decoded again for every tick. - let source = if self.requests.get(&slot).map(|request| request.id) != Some(r.request_id) { + let (source, look) = if self.requests.get(&slot).map(|request| request.id) != Some(r.request_id) { let newer_draft = slot.is_view() && self.requests.contains_key(&slot) && r.request_id >= self.epoch @@ -860,11 +978,13 @@ impl Renderer { continue; } session.accept(&r); - None // superseded successful drafts have no failure diagnostic + (None, completed_look) // retain the geometry of these pixels, never borrow the newer request’s identity } else { - let source = self.requests.remove(&slot).and_then(|request| request.source); + let request = self.requests.remove(&slot); + let look = request.as_ref().and_then(|request| request.look); + let source = request.and_then(|request| request.source); session.accept(&r); - source + (source, look) }; if slot.is_view() { self.shown_ids.insert(slot, r.request_id); @@ -926,6 +1046,7 @@ impl Renderer { Some(t) => { t.tex.set(color, egui::TextureOptions::LINEAR); t.pixels = pixels; + t.look = look; t.key = r.key; t.photo = r.photo; t.size = [img.width, img.height]; @@ -938,6 +1059,7 @@ impl Renderer { self.textures.insert( slot, Tex { + look, key: r.key, photo: r.photo, tex, @@ -1089,7 +1211,10 @@ mod stage_budget_tests { let ctx = egui::Context::default(); let mut r = Renderer::default(); let tex = ctx.load_texture("window", egui::ColorImage::new([1, 1], vec![egui::Color32::BLACK]), Default::default()); - r.textures.insert(Slot::Region, Tex { key: 1, photo: PhotoId(1), tex, size: [1, 1], histogram: None, ms: 0.0, quick: None, pixels: None }); + r.textures.insert( + Slot::Region, + Tex { look: None, key: 1, photo: PhotoId(1), tex, size: [1, 1], histogram: None, ms: 0.0, quick: None, pixels: None }, + ); let long_ago = std::time::Instant::now().checked_sub(IDLE_TRIM * 2).unwrap(); r.idle = Some((long_ago, false)); r.trim_when_idle(&ctx); diff --git a/crates/ui-egui/src/render/thumbnail_tests.rs b/crates/ui-egui/src/render/thumbnail_tests.rs index 984ce0462d..eaf4e8f2ff 100644 --- a/crates/ui-egui/src/render/thumbnail_tests.rs +++ b/crates/ui-egui/src/render/thumbnail_tests.rs @@ -35,13 +35,37 @@ impl RenderOffload for Blocked { fn photo() -> Arc { Arc::new(Photo::new(PhotoId(1), Source::File { path: "audit.jpg".into() }, "audit.jpg", "JPEG", 6000, 4000, "2026-01-01")) } + +#[test] +fn capture_readiness_excludes_prefetch_but_keeps_photo_and_thumbnail_work() { + let p = photo(); + let mut session = Session::new(); + session.catalog.apply(Op::AddPhoto { photo: Box::new((*p).clone()) }).unwrap(); + // Keep real requests queued without starting native workers. + let mut renderer = Renderer { pool: JobPool::new(0), ..Default::default() }; + renderer.prefetch(Slot::Prefetch(0), session.thumb_job(p.id, 384).unwrap(), 1); + renderer.prefetch(Slot::Prefetch(1), session.thumb_job(p.id, 384).unwrap(), 1); + assert_eq!(renderer.in_flight(), 2); + assert_eq!(renderer.in_flight_without_prefetch(), 0); + for (index, slot) in [Slot::Main, Slot::Thumb(p.id), Slot::Variant(7), Slot::Preview, Slot::ThumbQuick(p.id)].into_iter().enumerate() { + renderer.request(slot, session.thumb_job(p.id, 384).unwrap(), 10); + assert_eq!(renderer.in_flight_without_prefetch(), index + 1); + assert_eq!(renderer.in_flight(), index + 3); + } + assert_eq!(renderer.pool.run_inline(7), 7); + // Waiting observes a completed job without swallowing the result needed by poll. + assert!(renderer.wait_for_worker(std::time::Duration::ZERO)); + renderer.poll(&egui::Context::default(), &mut session); + assert_eq!(renderer.in_flight_without_prefetch(), 0); + assert_eq!(renderer.in_flight(), 0); +} #[test] fn snapshot_tracks_cow_edits_undo_reload_and_replacement() { let mut catalog = Catalog::default(); catalog.apply(Op::AddPhoto { photo: Box::new((*photo()).clone()) }).unwrap(); let old = catalog.photo(PhotoId(1)).unwrap().clone(); let mut r = Renderer::default(); - r.remember_thumb(&old, 384, 77, false); + r.remember_thumb(&old, 384, 77, false, 10); r.pending.insert(Slot::Thumb(PhotoId(1)), (77, 10)); assert!(r.thumb_current(&old, 384, 10)); assert!(!r.thumb_current(&old, 512, 10)); @@ -77,7 +101,7 @@ fn queue_promotions_running_work_and_cancellation() { let key = job.key; let mut r = Renderer::default(); r.set_offload(Box::new(Blocked)); - r.remember_thumb(&p, 384, key, false); + r.remember_thumb(&p, 384, key, false, 5); r.request(slot, job, 5); assert!(r.thumb_current(&p, 384, 5)); assert!(!r.thumb_current(&p, 384, 10)); @@ -110,7 +134,7 @@ fn queued_standin_promotes_from_prefetch_to_visible_without_restarting() { let slot = Slot::ThumbQuick(p.id); // Deterministic queue: no native worker starts. let mut r = Renderer { pool: JobPool::new(0), ..Default::default() }; - r.remember_thumb(&p, 384, quick.key, true); + r.remember_thumb(&p, 384, quick.key, true, 5); r.request_quick(slot, quick.clone(), 6); // grid prefetch: 5 + 1 r.request_quick(Slot::ThumbQuick(PhotoId(2)), other, 9); // filmstrip: 8 + 1 let request_id = r.requests[&slot].id; @@ -138,7 +162,7 @@ fn queued_standin_demotes_when_it_leaves_the_visible_grid() { let (_, other) = queued_quick_thumbnail(PhotoId(2)); let slot = Slot::ThumbQuick(p.id); let mut r = Renderer { pool: JobPool::new(0), ..Default::default() }; - r.remember_thumb(&p, 384, quick.key, true); + r.remember_thumb(&p, 384, quick.key, true, 10); r.request_quick(slot, quick.clone(), 11); r.request_quick(Slot::ThumbQuick(PhotoId(2)), other, 9); assert!(!r.thumb_current(&p, 384, 5)); @@ -153,7 +177,7 @@ fn failures_retry_after_revision_and_quick_only_is_not_final() { let p = photo(); let slot = Slot::Thumb(p.id); let mut r = Renderer::default(); - r.remember_thumb(&p, 384, 77, false); + r.remember_thumb(&p, 384, 77, false, 10); r.pending.insert(Slot::ThumbQuick(p.id), (77, 11)); assert!(!r.thumb_current(&p, 384, 10)); r.failed.insert(slot, (77, "no decoder".into(), 0, p.id)); @@ -164,6 +188,36 @@ fn failures_retry_after_revision_and_quick_only_is_not_final() { assert!(!r.thumb_current(&p, 384, 10)); } +/// A larger texture only serves a smaller bucket when it is the recorded request's picture. +/// This isolates the key guard: under a `contains_key`-only check the first assertion below +/// would return `true`. +#[test] +fn larger_texture_for_another_key_does_not_count_as_current() { + let p = photo(); + let slot = Slot::Thumb(p.id); + let ctx = egui::Context::default(); + let tex = |key| Tex { + look: None, + key, + photo: p.id, + tex: ctx.load_texture(format!("thumb-{key}"), egui::ColorImage::new([1, 1], vec![egui::Color32::BLACK]), Default::default()), + size: [1, 1], + histogram: None, + ms: 0.0, + quick: None, + pixels: None, + }; + let mut r = Renderer::default(); + // A completed larger bucket: the remembered request is bucket 512, key 77. + r.remember_thumb(&p, 512, 77, false, 10); + // Another key's pixels (for example the picture from before an edit) may still be + // displayed, but they are not the remembered request's picture. + r.textures.insert(slot, tex(66)); + assert!(!r.thumb_current(&p, 256, 10), "a mismatched texture key must not serve a smaller bucket"); + r.textures.insert(slot, tex(77)); + assert!(r.thumb_current(&p, 256, 10), "the recorded key's larger texture serves the smaller bucket"); +} + #[test] fn a_fast_draft_is_drawn_in_the_frame_that_asked_for_it() { let mut s = Session::new(); @@ -204,6 +258,15 @@ impl RenderOffload for Manual { fn fixture() -> (crate::LightcraftApp, std::rc::Rc>, egui::Context) { fixture_with(Session::new(), (*photo()).clone()) } +/// An unedited raw: its embedded camera preview is its final thumbnail (the grid's quick path). +fn raw_quick_fixture() -> (crate::LightcraftApp, std::rc::Rc>, egui::Context) { + let mut s = Session::new(); + s.media.preview_loader = Some(Arc::new(|_, _| Some(lightcraft_raster::Rgba8 { width: 4, height: 4, data: vec![[200, 100, 50, 255]; 16] }))); + let mut raw = (*photo()).clone(); + raw.kind = lightcraft_catalog::MediaKind::Raw; + raw.develop = Arc::new(raw.camera_defaults()); + fixture_with(s, raw) +} fn fixture_with(mut s: Session, photo: Photo) -> (crate::LightcraftApp, std::rc::Rc>, egui::Context) { s.media.file_loader = Some(Arc::new(|_, _| { let mut image = lightcraft_raster::Rgb32f::new(8, 8); @@ -538,16 +601,65 @@ fn loupe_shows_superseded_drafts_newer_than_its_pixels_during_a_drag() { let keys: Vec = jobs.iter().map(|j| j.key).collect(); let mut jobs = jobs.into_iter(); let (first, second, last) = (jobs.next().unwrap(), jobs.next().unwrap(), jobs.next().unwrap()); - // the first draft lands while the newest is still rendering: the loupe moves on - finish(&mut app, &work, &ctx, Slot::Main, first.run()); + // A decoded embedded preview can differ from the catalog's square metadata. Its + // superseded colour-only draft must retain the identity that enables aspect correction. + let mut first_result = first.run(); + first_result.rendered.as_mut().unwrap().image = lightcraft_raster::Rgba8::new(8, 4); + finish(&mut app, &work, &ctx, Slot::Main, first_result); + assert_eq!(app.renderer.textures[&Slot::Main].size, [8, 4]); assert_eq!(app.renderer.textures.get(&Slot::Main).map(|t| t.key), Some(keys[0])); assert!(app.renderer.is_pending(Slot::Main)); + assert_eq!( + app.renderer.textures[&Slot::Main].look, + Some((geometry_look(&lightcraft_develop::DevelopSettings::default()), true)), + "colour-only superseded pixels retain their geometry" + ); + let current_look = geometry_look(&last.settings); finish(&mut app, &work, &ctx, Slot::Main, last.run()); + assert_eq!(app.renderer.textures[&Slot::Main].look, Some((current_look, true))); assert_eq!(app.renderer.textures.get(&Slot::Main).map(|t| t.key), Some(keys[2])); // a draft older than the pixels shown never replaces them finish(&mut app, &work, &ctx, Slot::Main, second.run()); assert_eq!(app.renderer.textures.get(&Slot::Main).map(|t| t.key), Some(keys[2])); assert!(!app.renderer.is_pending(Slot::Main)); + assert!(app.renderer.offload_looks.is_empty(), "completed offload identities are released"); +} + +#[test] +fn superseded_crop_keeps_its_own_geometry_instead_of_the_latest_requests() { + let (mut app, work, ctx) = fixture(); + let old = app.session.loupe_job(PhotoId(1), 8, 8, true).unwrap().draft(); + let old_look = geometry_look(&old.settings); + app.renderer.request(Slot::Main, old, 100); + let (_, old) = take_job(&work); + let mut settings = lightcraft_develop::DevelopSettings::default(); + settings.crop.geometry.rect = lightcraft_geom::Rect::new(0.0, 0.0, 0.5, 1.0); + let new_look = geometry_look(&settings); + app.session.catalog.apply(Op::SetDevelop { id: PhotoId(1), settings: Arc::new(settings), label: "crop".into(), edited: None }).unwrap(); + let current = app.session.loupe_job(PhotoId(1), 8, 8, true).unwrap().draft(); + app.renderer.request(Slot::Main, current, 100); + let (_, current) = take_job(&work); + finish(&mut app, &work, &ctx, Slot::Main, old.run()); + assert_eq!(app.renderer.textures[&Slot::Main].look, Some((old_look, true))); + assert_ne!(old_look, new_look); + finish(&mut app, &work, &ctx, Slot::Main, current.run()); + assert_eq!(app.renderer.textures[&Slot::Main].look, Some((new_look, true))); +} + +#[test] +fn hidden_grading_keeps_the_completed_textures_geometry_identity() { + let (mut app, work, ctx) = fixture(); + let mut settings = lightcraft_develop::DevelopSettings::default(); + settings.grading.shadows.sat = 40.0; + settings.crop.geometry.rect = lightcraft_geom::Rect::new(0.1, 0.2, 0.8, 0.7); + app.session.catalog.apply(Op::SetDevelop { id: PhotoId(1), settings: Arc::new(settings.clone()), label: "grade".into(), edited: None }).unwrap(); + let expected = geometry_look(&settings); + let job = app.session.loupe_job(PhotoId(1), 8, 8, true).unwrap().with_hidden_grading([true, false, false]); + assert_ne!(job.settings.hash64(), settings.hash64(), "the view really hides a nonzero wheel"); + app.renderer.request(Slot::Main, job, 100); + let (slot, job) = take_job(&work); + finish(&mut app, &work, &ctx, slot, job.run()); + assert_eq!(app.renderer.textures[&Slot::Main].look, Some((expected, true))); } #[test] @@ -662,6 +774,300 @@ fn undo_to_existing_texture_supersedes_pending_edit() { assert_eq!(app.renderer.thumb(PhotoId(1)).unwrap().pixels.as_ref().unwrap().pixels, original.pixels); } +#[test] +fn cells_at_different_thumbnail_buckets_keep_one_pending_request() { + let (mut app, work, ctx) = fixture(); + let id = PhotoId(1); + let slot = Slot::Thumb(id); + // A print page's cells show the same photo at different sizes. Before any texture arrives, + // each frame's other bucket used to replace (and restart) the pending job. + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + let built = app.renderer.thumb_jobs_built; + assert_eq!(built, 1); + let (submitted, pending) = take_job(&work); + assert_eq!(submitted, slot); + for _ in 0..12 { + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + crate::panels::grid::request_thumb(&mut app, id, 384, 8); + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + } + assert_eq!(app.renderer.thumb_jobs_built, built, "another bucket must not replace the pending thumbnail"); + assert!(work.borrow().jobs.is_empty(), "the pending render is not restarted"); + assert_eq!(app.renderer.pending[&slot].0, pending.key); + let pending_key = pending.key; + finish(&mut app, &work, &ctx, slot, pending.run()); + assert_eq!(app.renderer.textures[&slot].key, pending_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 256, "the completed request's bucket is remembered"); + assert!(!app.renderer.is_pending(slot)); + + // With a completed 256 thumbnail, the larger cell asks for its bucket once; the smaller + // cells then wait behind it instead of replacing it back every frame. + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "a larger bucket still upgrades"); + let (submitted, larger) = take_job(&work); + assert_eq!(submitted, slot); + assert_ne!(larger.key, pending_key); + let larger_key = app.renderer.pending[&slot].0; + assert_eq!(larger_key, larger.key); + for _ in 0..12 { + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + crate::panels::grid::request_thumb(&mut app, id, 384, 10); + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + } + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "smaller cells keep the larger render in flight"); + assert!(work.borrow().jobs.is_empty(), "the larger render is not restarted"); + assert_eq!(app.renderer.pending[&slot].0, larger_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 512, "the in-flight bucket is still the larger one"); + finish(&mut app, &work, &ctx, slot, larger.run()); + assert_eq!(app.renderer.textures[&slot].key, larger_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 512); + assert!(!app.renderer.is_pending(slot)); + + // Once the largest visible bucket is complete, every smaller cell reuses those pixels. + // The 8x8 fixture texture cannot show its requested edge, so the remembered bucket, key + // and photo identity are what prove no render was built. Repeated frames at all sizes + // (the old bug alternated 256/512 forever) must settle with nothing further requested. + let built = app.renderer.thumb_jobs_built; + for _ in 0..24 { + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + crate::panels::grid::request_thumb(&mut app, id, 384, 10); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + crate::panels::grid::request_thumb(&mut app, id, 384, 8); + } + assert_eq!(app.renderer.thumb_jobs_built, built, "completed larger thumbnails serve smaller cells"); + assert!(work.borrow().jobs.is_empty()); + assert!(!app.renderer.is_pending(slot)); + assert_eq!(app.renderer.textures[&slot].key, larger_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 512); + assert_eq!(app.renderer.thumb_inputs[&id].2, larger_key); + + // The renderer remembers the photo through a `Weak` only. That is enough for + // `Catalog`'s `Arc::make_mut` to copy the photo on edit (std: an existing weak pointer + // forces a new allocation even with a single strong reference), so the identity below is + // invalidated without this test holding an extra `Arc`. + let remembered = app.renderer.thumb_inputs[&id].0.clone(); + let remembered_ptr = remembered.as_ptr(); + assert!(remembered.upgrade().is_some(), "the remembered Weak points at the live catalog photo"); + assert_eq!(Arc::strong_count(app.session.catalog.photo(id).unwrap()), 1, "the catalog is the only strong owner; the renderer keeps just a Weak"); + + // An edit is a different photo/content key: the completed larger picture must not be + // reused for a smaller cell, and the replacement is requested as before. + let mut edit = lightcraft_develop::DevelopSettings::default(); + edit.light.exposure = 3.0; + app.session.catalog.apply(Op::SetDevelop { id, settings: Arc::new(edit), label: "edit".into(), edited: None }).unwrap(); + let current = app.session.catalog.photo(id).unwrap(); + assert_ne!(Arc::as_ptr(current), remembered_ptr, "make_mut moved the edited photo to a new allocation"); + assert_eq!(remembered.strong_count(), 0, "the renderer's Weak now points at the old, unowned allocation"); + assert_eq!(Arc::strong_count(current), 1, "the edit left one strong owner; no extra Arc was needed"); + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "an edited photo is requested again"); + let (submitted, edited) = take_job(&work); + assert_eq!(submitted, slot); + assert_ne!(edited.key, larger_key); + let edited_key = edited.key; + finish(&mut app, &work, &ctx, slot, edited.run()); + assert_eq!(app.renderer.textures[&slot].key, edited_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 256); + assert!(!app.renderer.is_pending(slot)); +} + +/// The remembered larger bucket is not a picture by itself: once its texture is evicted, a +/// smaller cell must render again rather than trust the stale record. +#[test] +fn an_evicted_larger_thumbnail_is_rebuilt_for_a_smaller_cell() { + let (mut app, work, ctx) = fixture(); + let id = PhotoId(1); + let slot = Slot::Thumb(id); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + let (submitted, larger) = take_job(&work); + assert_eq!(submitted, slot); + let larger_key = larger.key; + finish(&mut app, &work, &ctx, slot, larger.run()); + assert_eq!(app.renderer.textures[&slot].key, larger_key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 512); + assert!(!app.renderer.is_pending(slot)); + + // An LRU trim drops the pixels; the remembered bucket/key must not stand in for them. + app.renderer.textures.remove(&slot); + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "an evicted larger thumbnail is rebuilt"); + let (submitted, rebuilt) = take_job(&work); + assert_eq!(submitted, slot); + assert_ne!(rebuilt.key, larger_key); + assert_eq!(app.renderer.pending[&slot].0, rebuilt.key); + assert_eq!(app.renderer.thumb_inputs[&id].1, 256); + let rebuilt_key = rebuilt.key; + finish(&mut app, &work, &ctx, slot, rebuilt.run()); + assert_eq!(app.renderer.textures[&slot].key, rebuilt_key); +} + +#[test] +fn a_coalesced_thumbnail_promotes_once_for_a_higher_priority_caller() { + let mut s = Session::new(); + s.catalog.apply(Op::AddPhoto { photo: Box::new((*photo()).clone()) }).unwrap(); + let mut app = crate::LightcraftApp::new(s, crate::Services::default()); + app.renderer.pool = JobPool::new(0); // deterministic: nothing starts + let slot = Slot::Thumb(PhotoId(1)); + crate::panels::grid::request_thumb(&mut app, PhotoId(1), 256, 5); + assert_eq!(app.renderer.pending[&slot].1, 5); + let first = app.renderer.pending[&slot].0; + // An on-screen caller at another bucket takes over the off-screen request once... + crate::panels::grid::request_thumb(&mut app, PhotoId(1), 512, 10); + let promoted = app.renderer.pending[&slot]; + assert_eq!(promoted.1, 10); + assert_ne!(promoted.0, first, "the promoted bucket is the one rendered"); + assert_eq!(app.renderer.thumb_jobs_built, 2, "one promotion"); + // ...and the caller it replaced then waits behind that priority instead of replacing it + // back. Equal priorities also keep the request in flight (the print cells' case). + for _ in 0..6 { + crate::panels::grid::request_thumb(&mut app, PhotoId(1), 256, 5); + crate::panels::grid::request_thumb(&mut app, PhotoId(1), 384, 8); + crate::panels::grid::request_thumb(&mut app, PhotoId(1), 512, 10); + } + assert_eq!(app.renderer.pending[&slot], promoted); + assert_eq!(app.renderer.pool.queued(), 1); + assert_eq!(app.renderer.thumb_jobs_built, 2, "a promoted request is not replaced in turn"); +} + +#[test] +fn a_stale_texture_does_not_disable_cross_bucket_coalescing() { + let (mut app, work, ctx) = fixture(); + let id = PhotoId(1); + let slot = Slot::Thumb(id); + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + let (_, first) = take_job(&work); + finish(&mut app, &work, &ctx, slot, first.run()); + let old_key = app.renderer.textures[&slot].key; + + // The edit leaves the old texture displayed until the replacement renders. + let mut edit = lightcraft_develop::DevelopSettings::default(); + edit.light.exposure = 2.0; + app.session.catalog.apply(Op::SetDevelop { id, settings: Arc::new(edit), label: "edit".into(), edited: None }).unwrap(); + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 384, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1); + let (_, edited) = take_job(&work); + let new_key = app.renderer.pending[&slot].0; + assert_ne!(new_key, old_key); + assert_eq!(app.renderer.textures[&slot].key, old_key, "the old picture stays until the replacement arrives"); + + // The stale texture is not the recorded request's picture: other buckets must still keep + // the replacement in flight rather than restarting it every frame. + for _ in 0..12 { + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + } + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "a stale texture must not disable coalescing"); + assert_eq!(app.renderer.pending[&slot].0, new_key); + assert!(work.borrow().jobs.is_empty()); + + // Once the recorded key has arrived, smaller cells reuse it; a larger cell upgrades. + finish(&mut app, &work, &ctx, slot, edited.run()); + assert_eq!(app.renderer.textures[&slot].key, new_key); + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1); + assert!(!app.renderer.is_pending(slot)); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 2); + assert_ne!(app.renderer.pending[&slot].0, new_key, "the larger bucket is requested once"); +} + +#[test] +fn raw_quick_preview_coalesces_across_buckets_without_priority_ping_pong() { + let mut s = Session::new(); + s.media.preview_loader = Some(Arc::new(|_, _| Some(lightcraft_raster::Rgba8 { width: 4, height: 4, data: vec![[200, 100, 50, 255]; 16] }))); + let mut raw = (*photo()).clone(); + raw.kind = lightcraft_catalog::MediaKind::Raw; + raw.develop = Arc::new(raw.camera_defaults()); + let (mut app, work, ctx) = fixture_with(s, raw); + app.renderer.pool = JobPool::new(0); // deterministic quick renders via run_inline + let id = PhotoId(1); + let quick = Slot::ThumbQuick(id); + let full = Slot::Thumb(id); + + crate::panels::grid::request_thumb(&mut app, id, 384, 5); + assert_eq!(app.renderer.pending[&quick].1, 6); + let first = app.renderer.pending[&quick].0; + assert!(work.borrow().jobs.is_empty(), "the stand-in renders before the real thumbnail"); + + // The on-screen caller at another bucket promotes the queued stand-in once. + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + let promoted = app.renderer.pending[&quick]; + assert_eq!(promoted.1, 11); + assert_ne!(promoted.0, first); + assert_eq!(app.renderer.thumb_jobs_built, 2); + + // The replaced caller waits for it; neither bucket rebuilds the queued stand-in. + for _ in 0..6 { + crate::panels::grid::request_thumb(&mut app, id, 384, 5); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + } + assert_eq!(app.renderer.pending[&quick], promoted); + assert_eq!(app.renderer.pool.queued(), 1); + assert_eq!(app.renderer.thumb_jobs_built, 2); + + assert_eq!(app.renderer.pool.run_inline(1), 1); + app.renderer.poll(&ctx, &mut app.session); + assert_eq!(app.renderer.textures[&quick].key, promoted.0); + assert!(!app.renderer.is_pending(quick)); + + // An unedited raw's embedded preview is final. Repeating its bucket must not queue + // a full sensor render behind it, and a smaller cell reuses a preview that arrived + // at a larger bucket instead of asking for another one every frame. + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + assert!(!app.renderer.is_pending(full)); + assert!(work.borrow().jobs.is_empty()); + assert_eq!(app.renderer.thumb_jobs_built, built); + for _ in 0..6 { + crate::panels::grid::request_thumb(&mut app, id, 384, 10); + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + } + assert!(!app.renderer.is_pending(full)); + assert!(!app.renderer.is_pending(quick)); + assert!(work.borrow().jobs.is_empty(), "the embedded preview is not rebuilt for a smaller cell"); + assert_eq!(app.renderer.thumb_jobs_built, built); + assert_eq!(app.renderer.textures[&quick].key, promoted.0); + assert_eq!(app.renderer.embedded_thumbs[&id].1, 512, "the embedded preview's bucket is remembered"); +} + +/// The embedded preview is final only at the bucket whose pixels actually arrived. A larger +/// preview that was evicted, or whose job failed at that key, must not make a smaller cell +/// believe it is covered: that cell asks for its own quick render. +#[test] +fn missing_or_failed_larger_embedded_preview_does_not_cover_a_smaller_cell() { + for failed_larger in [false, true] { + let (mut app, _work, ctx) = raw_quick_fixture(); + app.renderer.pool = JobPool::new(0); // deterministic quick renders via run_inline + let id = PhotoId(1); + let quick = Slot::ThumbQuick(id); + crate::panels::grid::request_thumb(&mut app, id, 512, 10); + assert_eq!(app.renderer.pool.run_inline(1), 1); + app.renderer.poll(&ctx, &mut app.session); + assert!(app.renderer.textures.contains_key(&quick)); + assert_eq!(app.renderer.embedded_thumbs[&id].1, 512); + let key = app.renderer.embedded_thumbs[&id].2; + assert_eq!(app.renderer.textures[&quick].key, key); + + // The larger picture is gone: evicted, or its job failed at that key. + app.renderer.textures.remove(&quick); + if failed_larger { + app.renderer.failed.insert(quick, (key, "no embedded preview".into(), app.renderer.catalog_rev, id)); + } + let built = app.renderer.thumb_jobs_built; + crate::panels::grid::request_thumb(&mut app, id, 256, 10); + assert_eq!(app.renderer.thumb_jobs_built, built + 1, "a smaller cell must request its own quick render"); + let smaller_key = app.renderer.pending[&quick].0; + assert_ne!(smaller_key, key); + assert_eq!(app.renderer.pool.run_inline(1), 1); + app.renderer.poll(&ctx, &mut app.session); + assert_eq!(app.renderer.textures[&quick].key, smaller_key); + } +} + #[test] fn explicit_clear_repairs_same_key_bad_pixels_and_rejects_old_same_key_job() { let (mut app, work, ctx) = fixture(); diff --git a/crates/ui-egui/src/sync.rs b/crates/ui-egui/src/sync.rs index 3ebf9ce3e1..25d7cd84c5 100644 --- a/crates/ui-egui/src/sync.rs +++ b/crates/ui-egui/src/sync.rs @@ -315,6 +315,28 @@ impl Drop for SyncRun { } } +/// Tests: a synchronize that stays running while the returned sender lives, so a drop that is +/// released when another barrier goes meets a running Synchronize Folder deterministically +/// (without waiting for a real folder's walk). The caller clears `sync_run` when done. +#[cfg(test)] +pub(crate) fn stall_run(app: &mut LightcraftApp, name: &str) -> Box { + let progress = Arc::new(SyncProgress::default()); + let (tx, rx) = std::sync::mpsc::channel(); + let guard = app.session.activity.start("sync", "Synchronizing folder", Cancel::Flag(progress.files.cancel.clone())); + guard.detail(name); + app.sync_run = Some(SyncRun { + name: name.to_string(), + progress, + crashed: Arc::new(std::sync::atomic::AtomicBool::new(false)), + rx, + commit: None, + stopping: false, + not_saved: Vec::new(), + guard, + }); + Box::new(tx) +} + /// Commit what the worker readied (called every frame); when it is done, one undo step and a /// word on what happened. pub fn poll_run(app: &mut LightcraftApp, ctx: &egui::Context) { diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 49174ff0d4..3a102a29a9 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -82,3 +82,6 @@ mod view_cost; mod wb_selector; mod zoom; mod zoom_keys; + +pub(super) mod fixtures; +pub(super) mod render_fixtures; diff --git a/crates/ui-egui/src/tests/activity.rs b/crates/ui-egui/src/tests/activity.rs index 4ed2b31783..58873a6e2e 100644 --- a/crates/ui-egui/src/tests/activity.rs +++ b/crates/ui-egui/src/tests/activity.rs @@ -422,13 +422,15 @@ fn batch_merge_runs_each_set_in_turn() { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let mut paths = Vec::new(); - // different exposures, or the import would take set B for duplicates of set A - for (set, evs) in [("A", [-2.0, 0.0, 2.0]), ("B", [-1.5, 0.5, 2.5]), ("C", [-1.0, 1.0, 3.0])] { - for (i, bytes) in lightcraft_merge::synth::bracket_dngs(1600, 1200, &evs).unwrap().into_iter().enumerate() { - let p = dir.join(format!("{set}_{i}.dng")); - std::fs::write(&p, bytes).unwrap(); - paths.push(p.to_string_lossy().to_string()); - } + // one `bracket_dngs` call renders the procedural scene once for all nine inputs (three calls + // would render it three times); the exposures stay different, or the import would take one + // set for duplicates of another + let evs = [-2.0, 0.0, 2.0, -1.5, 0.5, 2.5, -1.0, 1.0, 3.0]; + let sets = ["A", "A", "A", "B", "B", "B", "C", "C", "C"]; + for (i, (set, bytes)) in sets.into_iter().zip(lightcraft_merge::synth::bracket_dngs(1600, 1200, &evs).unwrap()).enumerate() { + let p = dir.join(format!("{set}_{}.dng", i % 3)); + std::fs::write(&p, bytes).unwrap(); + paths.push(p.to_string_lossy().to_string()); } let mut session = lightcraft_engine::Session::new().with_fs(); let r = session.execute("library.import", &json!({"paths": paths})).unwrap(); @@ -440,7 +442,10 @@ fn batch_merge_runs_each_set_in_turn() { session.execute("library.select", &json!({"ids": ids})).unwrap(); let app = LightcraftApp::new(session, Services { png: None, ..Default::default() }); let mut h = Headless::new(app, [1200.0, 800.0], 1.0); - h.settle(T); + // the merge ordering does not need the grid's nine thumbnails; one Detail render is enough + let r = h.request("ui.set", json!({"view": "detail", "filmstrip": false}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.settle(T), "the imported photos and the active preview settle"); let r = crate::menus::run_ui_command(&mut h.app, "merge.hdrBatch", &json!({})).unwrap().unwrap(); assert_eq!(r["sets"], 3); assert_eq!(h.app.merge.batch.len(), 2, "one running, two queued"); diff --git a/crates/ui-egui/src/tests/before_after.rs b/crates/ui-egui/src/tests/before_after.rs index 02ee879a1c..ffc3767748 100644 --- a/crates/ui-egui/src/tests/before_after.rs +++ b/crates/ui-egui/src/tests/before_after.rs @@ -7,6 +7,7 @@ use serde_json::json; use crate::headless::{Headless, RenderGate}; use crate::render::Slot; use crate::state::{BeforeAfter, ViewMode, Zoom}; +use crate::tests_render_fixtures::{HEIGHT, RenderFixture, WIDTH}; use crate::{LightcraftApp, Services}; const T: Duration = Duration::from_secs(20); @@ -133,18 +134,30 @@ fn assert_rect(actual: egui::Rect, expected: egui::Rect) { ); } +type AnimationView = (RenderFixture, Headless, [egui::Rect; 2], std::rc::Rc>); + /// Start with both textures ready, then hold replacement renders so worker timing cannot hide -/// a frame of the transition or replace a cached tile under the assertions. -fn animation_view(mode: BeforeAfter, zoom: Zoom) -> (Headless, [egui::Rect; 2], std::rc::Rc>) { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); +/// a frame of the transition or replace a cached tile under the assertions. One real 6000×4000 +/// file-backed photo keeps the render work the animation needs, without the demo's neighbours. +fn animation_view(mode: BeforeAfter, zoom: Zoom) -> AnimationView { + let fixture = RenderFixture::new("before-after-animation", 1); + let app = LightcraftApp::new(fixture.session(), Services { png: None, ..Default::default() }); let mut h = Headless::new(app, [1400.0, 900.0], 1.0); + assert_eq!(h.app.session.catalog.len(), 1, "one real 24 MP photo"); let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T); assert_eq!(r["ok"], true, "{r}"); - h.app.ui.before_after = mode; + let id = h.app.session.active().expect("a photo is open"); + let photo = h.app.session.catalog.photo(id).expect("it is in the catalog"); + assert_eq!((photo.width, photo.height), (WIDTH, HEIGHT), "the render fixture is a real 24 MP photo"); + // settle the single photo's source and Main render before the second pane exists: both pane + // jobs then reuse the decoded 24 MP source instead of decoding it twice + h.app.ui.before_after = BeforeAfter::Off; h.app.ui.zoom = zoom; h.app.ui.pan = (0.5, 0.5); h.app.ui.click_zoom = 300; - assert!(h.settle(SETTLE), "baseline renders settled"); + assert!(h.settle(SETTLE), "the single photo's source and Main render settle"); + h.app.ui.before_after = mode; + assert!(h.settle(SETTLE), "both pane renders settled"); let before_id = h.app.renderer.textures[&Slot::Before].tex.id(); let before = h .view @@ -162,7 +175,7 @@ fn animation_view(mode: BeforeAfter, zoom: Zoom) -> (Headless, [egui::Rect; 2], assert!(!before.intersects(after), "pane clips do not overlap"); let open = std::rc::Rc::new(std::cell::Cell::new(false)); h.app.renderer.set_offload(Box::new(RenderGate::new(open.clone()))); - (h, [before, after], open) + (fixture, h, [before, after], open) } fn queue_click(h: &mut Headless, pos: egui::Pos2) { @@ -178,7 +191,7 @@ fn queue_click(h: &mut Headless, pos: egui::Pos2) { } fn check_click_animation(mode: BeforeAfter) { - let (mut h, [before, after], open) = animation_view(mode, Zoom::Fit); + let (_fx, mut h, [before, after], open) = animation_view(mode, Zoom::Fit); let shift = before.center() - after.center(); let fit = h.app.image_rect.expect("Fit image"); let id = h.app.session.active().expect("photo"); @@ -234,7 +247,7 @@ fn both_before_after_panes_use_the_animated_rect_top_bottom() { } fn check_region_animation(mode: BeforeAfter) { - let (mut h, [before, after], open) = animation_view(mode, Zoom::Percent(200.0)); + let (_fx, mut h, [before, after], open) = animation_view(mode, Zoom::Percent(200.0)); let shift = before.center() - after.center(); let start = h.app.image_rect.expect("200 percent image"); let tiles = [(Slot::RegionBefore, before, true), (Slot::Region, after, false)].map(|(slot, pane, is_before)| { diff --git a/crates/ui-egui/src/tests/capture_time.rs b/crates/ui-egui/src/tests/capture_time.rs index b5903e0535..c604ecad1a 100644 --- a/crates/ui-egui/src/tests/capture_time.rs +++ b/crates/ui-egui/src/tests/capture_time.rs @@ -59,7 +59,7 @@ fn shift_preview_says_the_photos_capture_time_cant_be_read() { let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + // Inspect the notification before settling advances the simulated clock past its lifetime. assert!(h.app.ui.dialog.is_none(), "OK closes the dialog"); let toast = toast(&h); assert!(toast.contains("can't be read"), "OK says the photo was left as it was: {toast:?}"); @@ -92,7 +92,7 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() { open_set(&mut h, &[bad, other]); let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + // The click is consumed by request; unrelated rendering must not expire its notification. let toast = toast(&h); assert!(toast.contains("can't be read"), "OK says why nothing changed: {toast:?}"); assert!(matches!(h.app.ui.dialog, Some(Dialog::CaptureTime { .. })), "the dialog stays open after the refusal"); @@ -116,7 +116,8 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() { /// button and from the control channel alike; they used to close it without a word. #[test] fn errors_are_shown_and_the_dialog_stays_open() { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + let files = crate::tests_fixtures::SyntheticLibrary::new("capture-time-errors"); + let app = files.app(); let mut h = Headless::new(app, [1300.0, 900.0], 1.0); let id = h.app.session.visible_cloned()[0]; set_captured(&mut h, id, "9999-12-31T12:00:00"); @@ -125,9 +126,10 @@ fn errors_are_shown_and_the_dialog_stays_open() { Some(Dialog::CaptureTime { time, .. }) => *time = "yesterday".into(), other => panic!("Edit Capture Time is not open: {other:?}"), } - h.settle(SETTLE); + assert!(h.settle(SETTLE), "capture-time dialog renders settled"); assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T)["ok"], true); - h.settle(SETTLE); + // The request consumes the click. Inspect its short-lived notification before waiting + // for unrelated rendering: settling advances the simulated clock and can expire the toast. assert!(toast(&h).contains("not a date"), "OK says the typed time isn't a date: {:?}", toast(&h)); assert!(matches!(&h.app.ui.dialog, Some(Dialog::CaptureTime { time, .. }) if time == "yesterday"), "the dialog stays open with what was typed"); @@ -139,7 +141,7 @@ fn errors_are_shown_and_the_dialog_stays_open() { } other => panic!("Edit Capture Time is not open: {other:?}"), } - h.settle(SETTLE); + assert!(h.settle(SETTLE), "capture-time dialog renders settled"); let r = h.request("ui.dialog.confirm", json!({}), T); assert_ne!(r["ok"], true, "the shift is refused: {r}"); assert!(r.to_string().contains("9999"), "{r}"); diff --git a/crates/ui-egui/src/tests/crop_rotate.rs b/crates/ui-egui/src/tests/crop_rotate.rs index 71368fba21..7cab2f0853 100644 --- a/crates/ui-egui/src/tests/crop_rotate.rs +++ b/crates/ui-egui/src/tests/crop_rotate.rs @@ -6,6 +6,7 @@ use std::time::Duration; use serde_json::json; use crate::headless::Headless; +use crate::tests_fixtures::SyntheticLibrary; use crate::{LightcraftApp, Services}; const T: Duration = Duration::from_secs(20); @@ -13,12 +14,26 @@ const SETTLE: Duration = Duration::from_secs(120); fn crop_tool() -> Headless { let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + crop_view(app, false) +} + +/// The crop tool on `app`, asserting the setup settle: a pixel comparison must not run on a view +/// whose first render never finished. +fn crop_tool_with(app: LightcraftApp) -> Headless { + crop_view(app, true) +} + +fn crop_view(app: LightcraftApp, assert_settle: bool) -> Headless { let mut h = Headless::new(app, [1200.0, 800.0], 1.0); for (method, params) in [("ui.set", json!({"view": "detail"})), ("engine.execute", json!({"command": "panel.crop"}))] { let r = h.request(method, params, T); assert_eq!(r["ok"], true, "{r}"); } - h.settle(SETTLE); + if assert_settle { + assert!(h.settle(SETTLE), "the crop view settles before the snapshot"); + } else { + h.settle(SETTLE); + } h } @@ -385,11 +400,13 @@ fn the_angle_field_writes_the_angle_like_the_readout() { /// box once rotated: dimmed wedges inside it, doubly dark ones outside, growing with the angle.) #[test] fn rotating_the_crop_dims_nothing_inside_the_box() { - let mut h = crop_tool(); + // a real file-backed photo: the regression is a screen-space overlay, not native-resolution detail + let files = SyntheticLibrary::new("crop-dimming"); + let mut h = crop_tool_with(files.app()); let plain = h.paint(); let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "crop.angle", "value": -45.0}}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + assert!(h.settle(SETTLE), "the rotated crop view settles before the snapshot"); assert_eq!(angle(&h), -45.0); let rotated = h.paint(); let img = h.app.image_rect.expect("the photo on screen"); diff --git a/crates/ui-egui/src/tests/dialog_settings.rs b/crates/ui-egui/src/tests/dialog_settings.rs index c62bc2f594..950cae6e66 100644 --- a/crates/ui-egui/src/tests/dialog_settings.rs +++ b/crates/ui-egui/src/tests/dialog_settings.rs @@ -67,6 +67,11 @@ fn controls(h: &Headless) -> Vec<(String, egui::Rect)> { h.app.widgets.iter().filter(|(id, _)| named(id)).cloned().collect() } +/// Whether egui has a visible tooltip area, independent of unrelated window text. +fn tooltip_shown(h: &Headless) -> bool { + h.view.ctx.memory(|m| m.areas().visible_layer_ids().into_iter().any(|layer| layer.order == egui::Order::Tooltip)) +} + fn same(a: egui::Rect, b: egui::Rect) -> bool { (a.min - b.min).abs().max_elem() < 0.5 && (a.max - b.max).abs().max_elem() < 0.5 } @@ -184,7 +189,10 @@ fn a_long_library_folder_is_cut_short_and_its_tooltip_has_all_of_it() { ); // And resting the pointer on it shows the whole path assert_eq!(h.request("ui.hoverWidget", json!({"id": "text:settingsLocation"}), T)["ok"], true); - assert!(h.step_until(T, |h| h.painted_text().iter().any(|text| text.contains(&path))), "the tooltip has the whole path"); + assert!( + h.step_until(T, |h| tooltip_shown(h) && h.painted_text().iter().any(|text| text.contains(&path))), + "the tooltip layer shows the whole path" + ); // And the same holds for the folder Auto Import watches, which stays on one line let watched = h.app.run("library.autoImport", json!({"folder": path})); @@ -256,13 +264,23 @@ fn a_short_path_is_shown_whole_and_has_no_tooltip() { // Given the in-memory library, whose Location is a short sentence let mut h = demo([1600.0, 1000.0]); show(&mut h, "general"); - assert!(h.painted_text().iter().any(|text| text == "In memory — nothing is saved"), "it is all there"); - let before = h.painted_text().len(); - // When the pointer rests on it + let location = "In memory — nothing is saved"; + assert!(h.painted_text().iter().any(|text| text == location), "it is all there"); + assert!(!tooltip_shown(&h), "no tooltip before hovering the location"); assert_eq!(h.request("ui.hoverWidget", json!({"id": "text:settingsLocation"}), T)["ok"], true); - let _ = h.step_until(Duration::from_secs(2), |h| h.painted_text().len() > before); - // Then nothing more is drawn: there is nothing a tooltip could add - assert_eq!(h.painted_text().len(), before, "no tooltip for a path that is all there"); + // Observe every frame for two simulated seconds beyond the configured tooltip delay. + // A render completing or a toast disappearing may change other text in the window. + let rest_until = h.time() + f64::from(h.view.ctx.global_style().interaction.tooltip_delay) + 2.0; + assert!(!tooltip_shown(&h), "no tooltip when hovering the complete location"); + while h.time() < rest_until { + h.step(); + assert!(!tooltip_shown(&h), "no tooltip for a path that is all there at {}", h.time()); + assert_eq!( + h.painted_text().iter().filter(|text| text.as_str() == location).count(), + 1, + "the full location is drawn once throughout the hover" + ); + } } /// How many pixels of `rect` are not `colour` (one pixel per point here). diff --git a/crates/ui-egui/src/tests/docking_acceptance.rs b/crates/ui-egui/src/tests/docking_acceptance.rs index 207ad5fae8..2056103f48 100644 --- a/crates/ui-egui/src/tests/docking_acceptance.rs +++ b/crates/ui-egui/src/tests/docking_acceptance.rs @@ -96,10 +96,23 @@ fn inactive_same_mode_commands_reveal_without_closing_or_editing() { #[test] fn crop_canvas_double_click_reveals_edit_in_actual_custom_workspace() { - let mut h = app(); + // This exercises Canvas input and dock routing, not native-resolution rendering. + // Keep the real file alive until after the headless renderer is dropped. + let photo_files = crate::tests_fixtures::SyntheticLibrary::new("dock-crop-double-click"); + let mut fixture_app = photo_files.app(); + fixture_app.ui.left_panel = true; + let mut h = Headless::new(fixture_app, [1400.0, 1000.0], 1.0); + for _ in 0..5 { + h.step(); + } h.app.ui.view = crate::state::ViewMode::Detail; h.app.run("panel.crop", json!({})).unwrap(); - assert!(h.settle(std::time::Duration::from_secs(120)), "the demo photo settles before pointer input"); + assert!(h.settle(std::time::Duration::from_secs(120)), "the file-backed photo settles before pointer input"); + let active = h.app.session.active().expect("the fixture photo is active"); + assert!( + h.app.renderer.textures.get(&crate::render::Slot::Main).is_some_and(|tex| tex.photo == active && tex.size[0] > 0 && tex.size[1] > 0), + "the actual detail render is present before Canvas input" + ); h.app.ui.presets = true; h.step(); dock(&mut h, json!({"operation":"move","panel":"tools","target":"presets","zone":"center"})); diff --git a/crates/ui-egui/src/tests/drop_import.rs b/crates/ui-egui/src/tests/drop_import.rs index 8b6bf3dfdc..3d8031a69b 100644 --- a/crates/ui-egui/src/tests/drop_import.rs +++ b/crates/ui-egui/src/tests/drop_import.rs @@ -2,11 +2,14 @@ //! dropping will do; dropping photos or folders opens the import review (unless Settings ▸ Import //! says to import dropped files directly); a drop while a dialog is open is refused. +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; use std::time::Duration; use serde_json::json; use crate::headless::Headless; +use crate::pick::PickRequest; use crate::state::Dialog; use crate::{LightcraftApp, Services}; @@ -43,18 +46,68 @@ fn write_png(path: &str, seed: u8) { std::fs::write(path, bytes).unwrap(); } +/// A folder dialog that answers "cancelled": enough to make the launch list's New Library button +/// and its name prompt available. +fn services() -> Services { + Services { png: None, pick_folder: Some(Box::new(|| -> Option { None })), ..Default::default() } +} + /// An empty library, a folder `trip` with two photos, a window big enough for the overlay. fn app(dir: &Scratch) -> Headless { write_png(&dir.path("trip/a.png"), 1); write_png(&dir.path("trip/b.png"), 2); let session = lightcraft_engine::Session::new().with_fs(); - let mut h = Headless::new(LightcraftApp::new(session, Services { png: None, ..Default::default() }), [1300.0, 900.0], 1.0); + let mut h = Headless::new(LightcraftApp::new(session, services()), [1300.0, 900.0], 1.0); for _ in 0..4 { h.step(); } h } +/// Folder dialogs the host shows "elsewhere": each request is recorded with the sender that +/// answers it, so a test can cancel or choose after the app has moved on to its next UI pass. +type Asked = Arc>)>>>; + +/// A no-library window whose New Library and Open Another… folder dialogs are asynchronous, as on +/// the desktop hosts that run pickers off the UI thread (and in the browser). +fn app_with_picker(dir: &Scratch) -> (Headless, Asked) { + write_png(&dir.path("trip/a.png"), 1); + write_png(&dir.path("trip/b.png"), 2); + let session = lightcraft_engine::Session::new().with_fs(); + let mut app = LightcraftApp::new(session, services()); + let asked: Asked = Default::default(); + let a = asked.clone(); + app.services.picker = Some(Box::new(move |req| { + let (tx, rx) = std::sync::mpsc::channel(); + a.lock().unwrap().push((req, tx)); + Ok(rx) + })); + let mut h = Headless::new(app, [1300.0, 900.0], 1.0); + for _ in 0..4 { + h.step(); + } + (h, asked) +} + +fn asked_dialogs(asked: &Asked) -> usize { + asked.lock().unwrap().len() +} + +/// Answer the oldest folder dialog: `paths` empty cancels it. +fn answer_picker(asked: &Asked, paths: Vec) { + let sender = asked.lock().unwrap().remove(0).1; + sender.send(paths).unwrap(); +} + +/// A library folder that can be chosen by Open Another…, made outside the no-library app. +fn library_at(dir: &Scratch, rel: &str) -> String { + let path = dir.path(rel); + let mut session = lightcraft_engine::Session::new().with_fs(); + session.open_library(&path, false).unwrap(); + session.close_library().unwrap(); + path +} + fn drop_files(h: &mut Headless, paths: &[String], hover: bool) { let r = h.request("ui.dropFiles", json!({"paths": paths, "hover": hover}), T); assert_eq!(r["ok"], true, "{r}"); @@ -63,6 +116,47 @@ fn drop_files(h: &mut Headless, paths: &[String], hover: bool) { } } +/// The same window with a library already open, so a launch list over it is the real "over" +/// case rather than a list with no library behind it. +fn app_with_library(dir: &Scratch) -> Headless { + write_png(&dir.path("trip/a.png"), 1); + write_png(&dir.path("trip/b.png"), 2); + let mut session = lightcraft_engine::Session::new().with_fs(); + session.open_library(dir.path("library"), false).unwrap(); + let mut h = Headless::new(LightcraftApp::new(session, services()), [1300.0, 900.0], 1.0); + for _ in 0..4 { + h.step(); + } + h +} + +/// Draw the launch list (`over_open_library`: Alt-at-launch over the library that opened, else the +/// list with no library behind it). +fn draw_launch_list(h: &mut Headless, over_open_library: bool) { + let mut problem = crate::panels::library_problem::LibraryProblem::choose_later(&h.app.ui.settings); + if over_open_library && let Some(c) = problem.chooser.as_mut() { + c.over = Some(h.app.session.library_generation()); + } + h.app.library_problem = Some(problem); + assert!(h.step_until(T, |h| h.app.widgets.iter().any(|(w, _)| w == "button:libraryOpen")), "the launch list is drawn"); +} + +/// The launch list as Alt-at-launch shows it over the library that opened. +fn show_launch_list(h: &mut Headless) { + draw_launch_list(h, true); + assert!(crate::panels::library_problem::blocks_file_drops(&h.app), "the launch list owns the drop barrier"); +} + +/// The same list with no library open: photos still go through the deliberate hold path. +fn show_no_library_launch_list(h: &mut Headless) { + draw_launch_list(h, false); + assert!(!crate::panels::library_problem::blocks_file_drops(&h.app), "with no library open the drop path runs"); +} + +fn user_presets(h: &Headless) -> usize { + h.app.session.presets.iter().filter(|p| !p.builtin).count() +} + fn overlay(h: &Headless) -> bool { h.app.widgets.iter().any(|(w, _)| w == "dropOverlay") } @@ -172,6 +266,497 @@ fn a_drop_and_its_hover_card_wait_behind_a_notice_prompt() { assert_eq!(photos(&h), 0, "the review still imports nothing before it is confirmed"); } +/// A photo dropped while the launch list is over an already open library must wait in the queue: +/// importing it behind the list would add it to the library the list is asking to replace or keep. +#[test] +fn a_photo_drop_waits_behind_the_launch_list_over_an_open_library() { + let dir = Scratch::new("chooser-photo"); + let mut h = app_with_library(&dir); + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], true); + assert!(!overlay(&h), "the drop card must not promise an import above the launch list"); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert!(h.app.ui.dialog.is_none(), "no import review opens behind the launch list"); + assert!(h.app.scan.is_none() && h.app.import.is_none(), "no scan or import starts behind the launch list"); + assert_eq!(photos(&h), 0, "nothing is imported behind the launch list"); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the launch list to go"); + // Esc keeps the library that is open (the launch list's own Keep), then the queued drop runs + let r = h.request("ui.key", json!({"key": "Escape"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "the launch list closes"); + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the queued drop opens its review"); + assert_eq!(photos(&h), 0, "the review still imports nothing before it is confirmed"); + assert!(h.app.dropped_files.is_empty(), "the queued drop was consumed once the list was gone"); +} + +/// Preset drops run a command directly (no review), so the barrier must hold those too: a preset +/// imported behind the list would change the open library the person has not chosen to keep. +#[test] +fn a_preset_drop_waits_behind_the_launch_list_over_an_open_library() { + let dir = Scratch::new("chooser-preset"); + let mut h = app_with_library(&dir); + std::fs::write(dir.path("warm.lcpreset"), r#"{"id": "user.warm", "name": "Warm", "group": "Test", "settings": {"light": {"exposure": 0.5}}}"#) + .unwrap(); + let before = user_presets(&h); + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("warm.lcpreset")], false); + for _ in 0..10 { + h.step(); + } + assert_eq!(user_presets(&h), before, "no preset is imported behind the launch list"); + assert_eq!(h.app.dropped_files.len(), 1, "the preset waits for the launch list to go"); + let r = h.request("ui.key", json!({"key": "Escape"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "Keep closes the launch list"); + assert!(h.step_until(SETTLE, |h| user_presets(h) == before + 1), "the queued preset imports once the list is gone"); + assert!(h.app.dropped_files.is_empty()); +} + +/// A queued drop must survive the launch list hiding itself behind its own New Library name prompt: +/// that prompt does not make the list stop owning the library. Cancelling the prompt brings the +/// list back; only after Keep (Esc) does the drop import. +#[test] +fn a_queued_photo_drop_survives_the_choosers_new_library_prompt_and_cancel() { + let dir = Scratch::new("chooser-new-cancel"); + let mut h = app_with_library(&dir); + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the launch list"); + let r = h.request("ui.clickWidget", json!({"id": "button:libraryNew"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| matches!(h.app.ui.dialog, Some(Dialog::TextPrompt { .. }))), "New Library asks for a name"); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits behind the name prompt too"); + assert!(matches!(h.app.ui.dialog, Some(Dialog::TextPrompt { .. })), "the prompt stays open"); + assert!(h.app.scan.is_none() && h.app.import.is_none(), "nothing starts behind the prompt"); + assert_eq!(photos(&h), 0); + let r = h.request("ui.clickWidget", json!({"id": "button:dialogCancel"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.ui.dialog.is_none()), "Cancel closes the name prompt"); + for _ in 0..5 { + h.step(); + } + assert!(h.app.widgets.iter().any(|(w, _)| w == "button:libraryOpen"), "the launch list is back"); + assert_eq!(h.app.dropped_files.len(), 1, "cancel kept the queued drop"); + assert!(h.app.ui.dialog.is_none(), "no review opens behind the returning list"); + assert_eq!(photos(&h), 0); + let r = h.request("ui.key", json!({"key": "Escape"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "Keep closes the launch list"); + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the queued drop opens its review"); + assert!(h.app.dropped_files.is_empty(), "the queued drop was consumed once the list was gone"); +} + +/// With no library open a drop during the list's New Library name prompt would once have hit the +/// generic "close the open dialog" refusal and been lost. It now waits for the prompt to close, +/// then goes through the deliberate paths: photos are held for the library chosen, presets refused. +#[test] +fn a_drop_during_the_no_library_name_prompt_is_held_or_refused_after_cancel() { + let dir = Scratch::new("newlib-prompt-drop"); + std::fs::write(dir.path("warm.lcpreset"), r#"{"id": "user.warm", "name": "Warm", "group": "Test", "settings": {"light": {"exposure": 0.5}}}"#) + .unwrap(); + let mut h = app(&dir); + show_no_library_launch_list(&mut h); + let r = h.request("ui.clickWidget", json!({"id": "button:libraryNew"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| matches!(h.app.ui.dialog, Some(Dialog::TextPrompt { .. }))), "New Library asks for a name"); + drop_files(&mut h, &[dir.path("trip/a.png"), dir.path("warm.lcpreset")], false); + assert_eq!(h.app.dropped_files.len(), 2, "the drop waits behind the name prompt"); + assert_eq!(h.app.library_problem.as_ref().map(|p| p.pending_import.len()), Some(0), "nothing is held behind the prompt"); + let r = h.request("ui.clickWidget", json!({"id": "button:dialogCancel"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.ui.dialog.is_none()), "Cancel closes the name prompt"); + assert!(h.step_until(SETTLE, |h| h.app.dropped_files.is_empty()), "the drop is consumed once the prompt is gone"); + assert_eq!( + h.app.library_problem.as_ref().map(|p| p.pending_import.clone()), + Some(vec![dir.path("trip/a.png")]), + "the photo waits for the library chosen" + ); + assert_eq!(user_presets(&h), 0, "the preset is refused, not imported into the temporary session"); + assert_eq!(photos(&h), 0, "nothing is imported into the temporary session"); +} + +/// The no-library New Library flow asks for a name, then for a place on the host's asynchronous +/// folder picker. A drop queued during either step waits: it must not be consumed before the new +/// library exists. Confirming the picker opens the library and the queued photo imports into it. +#[test] +fn a_queued_photo_drop_survives_the_async_new_library_picker_and_imports_on_confirmation() { + let dir = Scratch::new("newlib-async-confirm"); + let (mut h, asked) = app_with_picker(&dir); + show_no_library_launch_list(&mut h); + h.app.ui.settings.import_dropped_directly = true; + let r = h.request("ui.clickWidget", json!({"id": "button:libraryNew"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| matches!(h.app.ui.dialog, Some(Dialog::TextPrompt { .. }))), "New Library asks for a name"); + let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.pending_picks.len() == 1), "the folder picker is up"); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(asked_dialogs(&asked), 1, "one asynchronous folder dialog was started"); + assert!(h.app.session.library.is_none(), "no library exists until the picker answers"); + assert!(crate::panels::library_problem::blocks_file_drops(&h.app), "the folder picker still owns the drop barrier"); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits through the picker"); + assert!(h.app.ui.dialog.is_none(), "the name prompt is gone; only the picker is up"); + assert_eq!(h.app.library_problem.as_ref().map(|p| p.pending_import.len()), Some(0), "nothing is held while the picker is still open"); + assert!(h.app.scan.is_none() && h.app.import.is_none() && photos(&h) == 0, "nothing starts behind the picker"); + + let parent = dir.path("new-library-parent"); + std::fs::create_dir_all(&parent).unwrap(); + answer_picker(&asked, vec![parent]); + assert!(h.step_until(SETTLE, |h| h.app.pending_picks.is_empty()), "the picker answered"); + assert!(h.step_until(SETTLE, |h| h.app.session.library.is_some()), "the new library opens"); + assert!( + h.step_until(SETTLE, |h| h.app.dropped_files.is_empty() && photos(h) == 1 && h.app.import.is_none()), + "the queued photo imports into the new library" + ); + assert_eq!(user_presets(&h), 0); +} + +/// Cancelling the asynchronous place picker leaves the launch list up. The queued photo then takes +/// the no-library hold path (kept for the library chosen) instead of being lost behind the picker. +#[test] +fn a_queued_drop_survives_the_async_new_library_picker_cancel_then_takes_the_no_library_path() { + let dir = Scratch::new("newlib-async-cancel"); + std::fs::write(dir.path("warm.lcpreset"), r#"{"id": "user.warm", "name": "Warm", "group": "Test", "settings": {"light": {"exposure": 0.5}}}"#) + .unwrap(); + let (mut h, asked) = app_with_picker(&dir); + show_no_library_launch_list(&mut h); + let r = h.request("ui.clickWidget", json!({"id": "button:libraryNew"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| matches!(h.app.ui.dialog, Some(Dialog::TextPrompt { .. }))), "New Library asks for a name"); + let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.pending_picks.len() == 1), "the folder picker is up"); + drop_files(&mut h, &[dir.path("trip/a.png"), dir.path("warm.lcpreset")], false); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 2, "both files wait through the picker"); + assert!(h.app.session.library.is_none()); + + answer_picker(&asked, vec![]); + assert!(h.step_until(SETTLE, |h| h.app.pending_picks.is_empty()), "the picker was cancelled"); + assert!(h.step_until(SETTLE, |h| h.app.dropped_files.is_empty()), "the drop takes the no-library path now"); + assert!(h.app.session.library.is_none(), "cancel created no library"); + assert!(h.app.widgets.iter().any(|(w, _)| w == "button:libraryOpen"), "the launch list is still up"); + assert_eq!( + h.app.library_problem.as_ref().map(|p| p.pending_import.clone()), + Some(vec![dir.path("trip/a.png")]), + "the photo waits for the library chosen" + ); + assert_eq!(user_presets(&h), 0, "the preset is refused with no library"); + assert_eq!(photos(&h), 0, "nothing is imported into the temporary session"); +} + +/// Open Another… has the same asynchronous folder picker with no library open. A queued drop must +/// wait for it, then import into the library the person opens. +#[test] +fn a_queued_photo_drop_survives_the_async_open_library_picker_and_imports_on_confirmation() { + let dir = Scratch::new("open-async-confirm"); + let target = library_at(&dir, "chosen-library"); + let (mut h, asked) = app_with_picker(&dir); + show_no_library_launch_list(&mut h); + h.app.ui.settings.import_dropped_directly = true; + let r = h.request("ui.clickWidget", json!({"id": "button:libraryOpenAnother"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.pending_picks.len() == 1), "the folder picker is up"); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(asked_dialogs(&asked), 1); + assert!(h.app.session.library.is_none()); + assert!(crate::panels::library_problem::blocks_file_drops(&h.app), "the folder picker still owns the drop barrier"); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits through the picker"); + + answer_picker(&asked, vec![target.clone()]); + assert!(h.step_until(SETTLE, |h| h.app.pending_picks.is_empty()), "the picker answered"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "opening the library ends the launch list"); + assert!( + h.step_until(SETTLE, |h| h.app.dropped_files.is_empty() && photos(h) == 1 && h.app.import.is_none()), + "the queued photo imports into the opened library" + ); + assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.to_string_lossy().into_owned()), Some(target)); +} + +/// A library opened from the asynchronous picker can start files that were already waiting in +/// `pending_import` (the command line, an earlier no-library drop). A drop queued while the picker +/// was up must not be handed to the ordinary drop path while that import runs: it waits for the +/// pending import to end and is imported too, instead of being refused and lost. +#[test] +fn a_queued_drop_waits_for_the_pending_import_started_when_the_picker_opens_the_library() { + let dir = Scratch::new("pending-import-picker"); + let target = library_at(&dir, "chosen-library"); + let (mut h, asked) = app_with_picker(&dir); + show_no_library_launch_list(&mut h); + h.app.ui.settings.import_dropped_directly = true; + let pending = dir.path("trip/a.png"); + h.app.library_problem.as_mut().unwrap().pending_import.push(pending); + + let r = h.request("ui.clickWidget", json!({"id": "button:libraryOpenAnother"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.pending_picks.len() == 1), "the folder picker is up"); + drop_files(&mut h, &[dir.path("trip/b.png")], false); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits through the picker"); + assert!(h.app.session.library.is_none()); + + answer_picker(&asked, vec![target.clone()]); + assert!(h.step_until(SETTLE, |h| h.app.pending_picks.is_empty()), "the picker answered"); + assert!(h.step_until(SETTLE, |h| photos(h) == 1), "the pending import arrives first"); + assert!(h.app.import.is_some(), "the pending import still runs when the drop is released"); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the pending import instead of being refused"); + assert!( + h.step_until(SETTLE, |h| photos(h) == 2 && h.app.dropped_files.is_empty() && h.app.import.is_none()), + "both the pending file and the queued drop are imported (photos: {}, queued: {:?})", + photos(&h), + h.app.dropped_files.len() + ); + assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.to_string_lossy().into_owned()), Some(target)); +} + +/// A drop queued behind the launch list may find another dialog open when the list goes (the +/// person opened one while the barrier was up). It waits for that dialog instead of taking the +/// ordinary "close the open dialog" refusal, so it is not lost. +#[test] +fn a_queued_drop_waits_for_a_dialog_opened_before_the_launch_list_closes() { + let dir = Scratch::new("deferred-dialog"); + let mut h = app_with_library(&dir); + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the launch list"); + h.app.library_problem = None; + h.app.ui.dialog = Some(Dialog::WhatsNew); + for _ in 0..10 { + h.step(); + } + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew)), "the dialog stays"); + assert_eq!(h.app.dropped_files.len(), 1, "the deferred drop is not refused while the dialog is open"); + assert!(h.app.scan.is_none() && h.app.import.is_none(), "nothing starts behind the dialog"); + h.app.ui.dialog = None; + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the drop opens its review once the dialog is closed"); + assert!(h.app.dropped_files.is_empty(), "the deferred drop was consumed only when safe"); +} + +/// A drop queued behind the launch list may be released while a Synchronize Folder runs. It +/// waits for that run to finish instead of taking `import::start_paths`'s "A folder is being +/// synchronized" refusal and being lost. A drop made while no barrier was up still gets that +/// refusal (see `tests_sync::a_synchronize_and_an_import_never_run_at_once`). +#[test] +fn a_queued_drop_waits_for_a_running_synchronize_instead_of_being_refused() { + let dir = Scratch::new("deferred-sync"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = true; + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the launch list"); + let _run = crate::sync::stall_run(&mut h.app, "trip"); + assert!(h.app.sync_run.is_some(), "the synchronize is running"); + h.app.library_problem = None; + for _ in 0..10 { + h.step(); + } + assert!(h.app.sync_run.is_some(), "the synchronize still runs"); + assert_eq!(h.app.dropped_files.len(), 1, "the deferred drop waits for the running synchronize instead of being refused"); + assert!(!said(&h, "A folder is being synchronized"), "the deferred drop is not refused: {:?}", h.painted_text()); + assert_eq!(photos(&h), 0, "nothing is imported during the synchronize"); + h.app.sync_run = None; + assert!( + h.step_until(SETTLE, |h| h.app.dropped_files.is_empty() && photos(h) == 1 && h.app.import.is_none()), + "the queued drop imports once the synchronize is done (queued: {:?}, photos: {})", + h.app.dropped_files, + photos(&h) + ); +} + +/// The same for a Lightroom catalog import: a drop queued behind the launch list waits while the +/// task owns the catalog, instead of being refused with "Finish Lightroom catalog import…". +#[test] +fn a_queued_drop_waits_for_a_running_lightroom_import_instead_of_being_refused() { + let dir = Scratch::new("deferred-lightroom"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = true; + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the launch list"); + let task = crate::lightroom_import::stall_import(&mut h.app); + h.app.library_problem = None; + for _ in 0..10 { + h.step(); + } + assert!(h.app.lightroom.is_some(), "the Lightroom import still runs"); + assert_eq!(h.app.dropped_files.len(), 1, "the deferred drop waits for the Lightroom import instead of being refused"); + assert!(!said(&h, "Finish Lightroom catalog import"), "the deferred drop is not refused: {:?}", h.painted_text()); + assert_eq!(photos(&h), 0, "nothing is imported during the Lightroom import"); + drop(task); + assert!(h.step_until(SETTLE, |h| h.app.lightroom.is_none()), "the Lightroom task ends"); + // the stub's disconnect reports a stopped task and opens its result window; close it so the + // queued drop is released (a real import's result window is another dialog the guard waits for) + h.app.ui.dialog = None; + assert!( + h.step_until(SETTLE, |h| h.app.dropped_files.is_empty() && photos(h) == 1 && h.app.import.is_none()), + "the queued drop imports once the Lightroom task is done (queued: {:?}, photos: {})", + h.app.dropped_files, + photos(&h) + ); +} + +/// Cancelling Open Another…'s asynchronous picker keeps the launch list up; the queued photo is +/// then held for the library chosen rather than dropped behind the cancelling picker. +#[test] +fn a_queued_photo_drop_survives_the_async_open_library_picker_cancel() { + let dir = Scratch::new("open-async-cancel"); + let (mut h, asked) = app_with_picker(&dir); + show_no_library_launch_list(&mut h); + let r = h.request("ui.clickWidget", json!({"id": "button:libraryOpenAnother"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(T, |h| h.app.pending_picks.len() == 1), "the folder picker is up"); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + for _ in 0..10 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits through the picker"); + + answer_picker(&asked, vec![]); + assert!(h.step_until(SETTLE, |h| h.app.pending_picks.is_empty()), "the picker was cancelled"); + assert!(h.step_until(SETTLE, |h| h.app.dropped_files.is_empty()), "the drop takes the no-library path now"); + assert!(h.app.session.library.is_none()); + assert!(h.app.widgets.iter().any(|(w, _)| w == "button:libraryOpen"), "the launch list is still up"); + assert_eq!( + h.app.library_problem.as_ref().map(|p| p.pending_import.clone()), + Some(vec![dir.path("trip/a.png")]), + "the photo waits for the library chosen" + ); + assert_eq!(photos(&h), 0); +} + +/// A valid face-model drop on a no-library launch list or error window must not open its licence +/// dialog behind that window; it is refused like a preset, while photos still take the hold path. +#[test] +fn a_model_drop_on_a_no_library_window_does_not_open_its_dialog_behind_it() { + let dir = Scratch::new("model-window"); + std::fs::write(dir.path("face.onnx"), lightcraft_faces::synthetic::tiny_embedder_model(512)).unwrap(); + for (tag, problem) in [ + ("error", crate::panels::library_problem::LibraryProblem::new(dir.path("Gone"), "not found")), + ("chooser", crate::panels::library_problem::LibraryProblem::choose_later(&crate::state::AppSettings::default())), + ] { + let mut h = app(&dir); + h.app.library_problem = Some(problem); + for _ in 0..4 { + h.step(); + } + drop_files(&mut h, &[dir.path("face.onnx")], true); + assert!(said(&h, "drop the model again"), "{tag}: the card says the model waits: {:?}", h.painted_text()); + drop_files(&mut h, &[dir.path("face.onnx")], false); + for _ in 0..10 { + h.step(); + } + assert!(h.app.ui.dialog.is_none(), "{tag}: no face model dialog behind the window"); + let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default(); + assert!(toast.contains("drop the model again"), "{tag}: {toast}"); + } +} + +/// A model dropped over the launch list is held like a photo/preset, then opens its licence dialog +/// once the list is kept away. +#[test] +fn a_model_drop_waits_behind_the_launch_list_and_opens_after_keep() { + let dir = Scratch::new("chooser-model"); + std::fs::write(dir.path("face.onnx"), lightcraft_faces::synthetic::tiny_embedder_model(512)).unwrap(); + let mut h = app_with_library(&dir); + h.app.session.face_models_dir = Some(std::path::PathBuf::from(dir.path("models"))); + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("face.onnx")], false); + for _ in 0..10 { + h.step(); + } + assert!(h.app.ui.dialog.is_none(), "no licence dialog opens behind the launch list"); + assert_eq!(h.app.dropped_files.len(), 1, "the model waits for the launch list to go"); + let r = h.request("ui.key", json!({"key": "Escape"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "Keep closes the launch list"); + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::FaceModel { .. }))), "the queued model opens its licence dialog"); + assert!(h.app.dropped_files.is_empty()); +} + +/// A failed switch leaves the launch list showing its error over the library that stayed open +/// (`Chooser { over: Some(generation), .. }`, not a separate error window). A queued drop must wait +/// there too: importing it would change the library the person has not chosen to keep. +#[test] +fn a_photo_drop_waits_behind_the_choosers_failed_switch_error_until_keep() { + let dir = Scratch::new("failed-switch"); + let mut h = app_with_library(&dir); + let mut problem = crate::panels::library_problem::LibraryProblem::choose_later(&h.app.ui.settings); + let generation = h.app.session.library_generation(); + let Some(c) = problem.chooser.as_mut() else { panic!("the launch list has its chooser") }; + c.over = Some(generation); + problem.path = dir.path("Gone"); + problem.error = format!("{}: the library is no longer there", dir.path("Gone")); + h.app.library_problem = Some(problem); + assert!( + h.step_until(T, |h| h.app.widgets.iter().any(|(w, _)| w == "button:libraryOpen")), + "the launch list is drawn over the library that stayed open" + ); + assert!(said(&h, "no longer there"), "the failed switch's error is shown: {:?}", h.painted_text()); + assert!(crate::panels::library_problem::blocks_file_drops(&h.app), "the failed-switch list owns the drop barrier"); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1, "the drop waits for the failed-switch list"); + assert!(h.app.ui.dialog.is_none(), "no import review opens behind the list"); + assert!(h.app.scan.is_none() && h.app.import.is_none(), "nothing starts behind the list"); + assert_eq!(photos(&h), 0); + // Esc is the list's Keep: it closes the list and leaves the library that stayed open. + let r = h.request("ui.key", json!({"key": "Escape"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.step_until(SETTLE, |h| h.app.library_problem.is_none()), "Keep closes the launch list"); + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the queued drop opens after Keep"); + assert!(h.app.dropped_files.is_empty()); + assert_eq!(photos(&h), 0, "the review still imports nothing before it is confirmed"); +} + +/// A drop queued while the launch list is up belongs to the library the person then opens with +/// its Open button: the switch must not drop it, and it imports into the new library. +#[test] +fn a_queued_photo_drop_imports_into_the_library_opened_from_the_list() { + let dir = Scratch::new("switch-queue"); + // Made (and flushed) by a session that is dropped before the app opens it: `close_library` + // saves but keeps the library and its OS lock until the session is dropped, and the launch + // list's Open would then be refused with "already open" by this same process's other descriptor. + let second = library_at(&dir, "second"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = true; + h.app.ui.settings.library_path = dir.path("library"); + h.app.ui.settings.recent_libraries = vec![dir.path("library"), second.clone()]; + show_launch_list(&mut h); + drop_files(&mut h, &[dir.path("trip/a.png")], false); + assert_eq!(h.app.dropped_files.len(), 1); + let r = h.request("ui.clickWidget", json!({"id": "libraryRecent:1"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(h.app.library_problem.as_ref().and_then(|p| p.chooser.as_ref()).and_then(|c| c.selected), Some(1), "the second library is selected"); + let r = h.request("ui.clickWidget", json!({"id": "button:libraryOpen"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!( + h.step_until(SETTLE, |h| h.app.library_problem.is_none()), + "the switch ends the launch list; problem: {:?}", + h.app.library_problem.as_ref().map(|p| (&p.path, &p.error)) + ); + assert!( + h.step_until(SETTLE, |h| h.app.import.is_none() && photos(h) == 1), + "the queued drop imports into the opened library (queued: {:?}, photos: {}, import running: {})", + h.app.dropped_files, + photos(&h), + h.app.import.is_some() + ); + assert_eq!(h.app.session.library.as_ref().map(|l| l.dir.to_string_lossy().into_owned()), Some(second)); + assert!(h.app.dropped_files.is_empty()); +} + /// The scan of a dropped folder finishing while another dialog is open (the face model's licence, Settings…) /// keeps that dialog; the review opens, with the scan's photos, once it is closed. #[test] @@ -354,3 +939,171 @@ fn photos_dropped_with_no_library_open_wait_for_one() { let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default(); assert!(toast.contains("drop the presets again"), "{toast}"); } + +/// Finish the protected batch through the ordinary review/direct path, checking exact provenance. +fn finish_mixed_drop_batch(h: &mut Headless, expected: &[String], direct: bool) { + if !direct { + assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(Dialog::Import { .. }))), "the protected batch reaches review"); + let Some(Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") }; + let mut actual = opts.candidates.iter().map(|c| c.path.clone()).collect::>(); + actual.sort(); + let mut wanted = expected.to_vec(); + wanted.sort(); + assert_eq!(actual, wanted, "review contains exactly the protected arrivals"); + assert_eq!(photos(h), 0, "review mode still requires confirmation"); + let reply = h.request("ui.dialog.confirm", json!({}), T); + assert_eq!(reply["ok"], true, "{reply}"); + } + assert!(h.step_until(SETTLE, |h| photos(h) == expected.len() && !crate::import::running(&h.app)), "the protected batch imports"); + let mut paths = h + .app + .session + .catalog + .photos() + .map(|p| match &p.source { + lightcraft_catalog::Source::File { path } => path.clone(), + other => panic!("unexpected imported source: {other:?}"), + }) + .collect::>(); + paths.sort(); + let mut wanted = expected.to_vec(); + wanted.sort(); + assert_eq!(paths, wanted, "only protected files entered the catalog"); + assert!(h.app.dropped_files.is_empty(), "the protected arrivals were consumed"); + let revision = h.app.session.catalog.revision; + // Inspect every later pass, not only the final catalog count: duplicate import suppression + // must not disguise replay of a consumed raw-input drop. + for _ in 0..60 { + h.step(); + assert!(!crate::import::running(&h.app), "no replay starts another scan/import"); + assert!(h.app.ui.dialog.is_none(), "no replay opens another review"); + assert!(h.app.dropped_files.is_empty()); + assert_eq!(photos(h), expected.len()); + assert_eq!(h.app.session.catalog.revision, revision, "no later import mutates the catalog"); + } +} + +#[test] +fn a_new_drop_outside_the_barrier_is_refused_while_an_older_drop_waits_for_a_dialog() { + for direct in [false, true] { + let dir = Scratch::new("mixed-provenance-dialog"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = direct; + let a = dir.path("trip/a.png"); + let b = dir.path("trip/b.png"); + show_launch_list(&mut h); + drop_files(&mut h, std::slice::from_ref(&a), false); + assert_eq!(h.app.dropped_files.len(), 1); + h.app.library_problem = None; + h.app.ui.dialog = Some(Dialog::WhatsNew); + h.step(); + assert!(!crate::panels::library_problem::blocks_file_drops(&h.app)); + assert!(!crate::panels::notices::blocks_input(&h.app)); + drop_files(&mut h, &[b], false); + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew)), "the ordinary dialog survives B"); + assert!(h.app.scan.is_none() && h.app.import.is_none()); + assert_eq!(photos(&h), 0); + assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0.contains("Close the open dialog")), "B is refused immediately: {:?}", h.app.ui.toast); + assert_eq!(h.app.dropped_files.len(), 1, "B cannot inherit A's protected provenance"); + h.app.ui.dialog = None; + finish_mixed_drop_batch(&mut h, &[a], direct); + } +} + +#[test] +fn a_new_drop_outside_the_barrier_is_refused_while_an_older_drop_waits_for_an_import() { + for direct in [false, true] { + let dir = Scratch::new("mixed-provenance-import"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = direct; + let a = dir.path("trip/a.png"); + let b = dir.path("trip/b.png"); + show_launch_list(&mut h); + drop_files(&mut h, std::slice::from_ref(&a), false); + let stalled = crate::import::stall_scan(&mut h.app); + h.app.library_problem = None; + h.step(); + assert!(!crate::panels::library_problem::blocks_file_drops(&h.app)); + assert!(!crate::panels::notices::blocks_input(&h.app)); + drop_files(&mut h, &[b], false); + assert!(h.app.scan.is_some() && h.app.import.is_none(), "the original stalled import scan survives B"); + assert!(h.app.ui.dialog.is_none()); + assert_eq!(photos(&h), 0); + assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0.contains("An import is running")), "B is refused immediately: {:?}", h.app.ui.toast); + assert_eq!(h.app.dropped_files.len(), 1, "B cannot inherit A's protected provenance"); + drop(stalled); // Channel-only fixture: no worker/process to stop. + assert!(h.step_until(T, |h| h.app.scan.is_none()), "the original stalled scan ends"); + finish_mixed_drop_batch(&mut h, &[a], direct); + } +} + +#[test] +fn both_drops_really_made_behind_the_barrier_remain_protected() { + for direct in [false, true] { + let dir = Scratch::new("mixed-provenance-both-protected"); + let mut h = app_with_library(&dir); + h.app.ui.settings.import_dropped_directly = direct; + let a = dir.path("trip/a.png"); + let b = dir.path("trip/b.png"); + show_launch_list(&mut h); + drop_files(&mut h, std::slice::from_ref(&a), false); + drop_files(&mut h, std::slice::from_ref(&b), false); + assert_eq!(h.app.dropped_files.len(), 2, "both arrivals really occurred behind the barrier"); + h.app.library_problem = None; + h.app.ui.dialog = Some(Dialog::WhatsNew); + for _ in 0..10 { + h.step(); + } + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew))); + assert_eq!(h.app.dropped_files.len(), 2, "both protected arrivals survive the later dialog"); + assert!(h.app.scan.is_none() && h.app.import.is_none()); + h.app.ui.dialog = None; + finish_mixed_drop_batch(&mut h, &[a, b], direct); + } +} + +/// Public take also serves web hosts: barrier reentry preserves order and consuming clears +/// protection, so a later ordinary drop cannot inherit the consumed batch's provenance. +#[test] +fn public_drop_take_preserves_reentered_barrier_order_and_clears_protection() { + let dir = Scratch::new("mixed-provenance-public-take"); + let mut h = app_with_library(&dir); + let a = dir.path("trip/a.png"); + let b = dir.path("trip/b.png"); + show_launch_list(&mut h); + drop_files(&mut h, std::slice::from_ref(&a), false); + assert_eq!(h.app.dropped_files.len(), 1); + h.app.library_problem = None; + h.app.ui.dialog = Some(Dialog::WhatsNew); + for _ in 0..3 { + h.step(); + } + assert_eq!(h.app.dropped_files.len(), 1, "A remains held behind the ordinary dialog"); + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew))); + // Reenter before any pass without a blocker can consume A. + h.app.ui.dialog = None; + show_launch_list(&mut h); + drop_files(&mut h, std::slice::from_ref(&b), false); + assert_eq!(h.app.dropped_files.len(), 2); + assert_eq!(photos(&h), 0); + assert!(h.app.scan.is_none() && h.app.import.is_none()); + // Match the web ownership boundary without stepping a native consuming pass. + h.app.library_problem = None; + let taken = h.app.take_dropped_files(); + let paths = taken.iter().map(|f| egui::DroppedFile::path(f.as_ref()).to_string_lossy().into_owned()).collect::>(); + assert_eq!(paths, vec![a.clone(), b], "public take preserves arrival order through barrier reentry"); + assert!(h.app.take_dropped_files().is_empty(), "ownership transfers only once"); + h.app.ui.dialog = Some(Dialog::WhatsNew); + drop_files(&mut h, &[a], false); + assert!(matches!(h.app.ui.dialog, Some(Dialog::WhatsNew))); + assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0.contains("Close the open dialog")), "consumed protection cannot shelter a later drop"); + assert!(h.app.dropped_files.is_empty()); + h.app.ui.dialog = None; + for _ in 0..60 { + h.step(); + assert!(h.app.dropped_files.is_empty()); + assert!(!crate::import::running(&h.app)); + assert!(h.app.ui.dialog.is_none()); + assert_eq!(photos(&h), 0, "neither taken handles nor refused arrivals replay"); + } +} diff --git a/crates/ui-egui/src/tests/filmstrip.rs b/crates/ui-egui/src/tests/filmstrip.rs index bf572c1d68..1529bcb41d 100644 --- a/crates/ui-egui/src/tests/filmstrip.rs +++ b/crates/ui-egui/src/tests/filmstrip.rs @@ -31,30 +31,31 @@ fn filmstrip_shows_a_cjk_file_name() { #[test] fn filmstrip_stack_badge_expands_and_collapses_the_stack() { - let mut session = lightcraft_engine::Session::with_demo(); - let vis = session.visible_cloned(); + // Badge routing needs three real photos, not full 24 MP demo renders. + let library = crate::tests_fixtures::SyntheticLibrary::with_photos("filmstrip-stack-badge", 3); + let mut app = library.app(); + let vis = app.session.visible_cloned(); let (loose, top, member) = (vis[0], vis[1], vis[2]); let stack = Stack { id: StackId(1), photos: vec![top, member], collapsed: true }; - session.catalog.apply(Op::AddStack { stack }).unwrap(); - let app = LightcraftApp::new(session, Services { png: None, ..Default::default() }); + app.session.catalog.apply(Op::AddStack { stack }).unwrap(); let mut h = Headless::new(app, [1200.0, 800.0], 1.0); // the loose photo is the one in the loupe: the stack isn't selected let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [loose.0]}}), T); assert_eq!(r["ok"], true, "{r}"); let r = h.request("ui.set", json!({"view": "detail"}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + assert!(h.settle(SETTLE), "the filmstrip settles before clicking its stack badge"); assert!(!h.app.session.visible_cloned().contains(&member), "collapsed: only the top shows"); let badge = format!("filmstack:{}", top.0); let r = h.request("ui.clickWidget", json!({"id": badge}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + assert!(h.settle(SETTLE), "the expanded stack settles"); assert!(!h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a click on the badge expands the stack"); assert!(h.app.session.visible_cloned().contains(&member)); assert_eq!(h.app.session.selection.active, Some(loose), "the loupe keeps its photo"); let r = h.request("ui.clickWidget", json!({"id": badge}), T); assert_eq!(r["ok"], true, "{r}"); - h.settle(SETTLE); + assert!(h.settle(SETTLE), "the collapsed stack settles"); assert!(h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a second click collapses it"); assert!(!h.app.session.visible_cloned().contains(&member)); } diff --git a/crates/ui-egui/src/tests/fixtures.rs b/crates/ui-egui/src/tests/fixtures.rs new file mode 100644 index 0000000000..0b07b543db --- /dev/null +++ b/crates/ui-egui/src/tests/fixtures.rs @@ -0,0 +1,86 @@ +//! Test-only fixtures: a small, real, file-backed photo library. +//! +//! The demo library generates 24 MP procedural scenes, which Nextest regenerates in every +//! process. The library/lifecycle tests only need an active detail photo whose file really +//! decodes, so this fixture writes 64×48 gradient PNGs and imports them as the only photos. + +use std::path::PathBuf; + +use lightcraft_catalog::{Op, Photo, Source}; + +use crate::{LightcraftApp, Services}; + +/// A temporary folder holding small PNGs; removes only its own folder when dropped. +/// +/// Keep this guard alive until after the [`Headless`](crate::headless::Headless) built from +/// [`SyntheticLibrary::app`] is dropped: the renderer reads the photo file from disk. +pub(crate) struct SyntheticLibrary { + dir: PathBuf, + photos: Vec, +} + +impl SyntheticLibrary { + /// A fresh folder for `tag`, with a deterministic 64×48 gradient `a.png`. + pub(crate) fn new(tag: &str) -> SyntheticLibrary { + SyntheticLibrary::with_photos(tag, 1) + } + + /// A fresh folder for `tag`, with `n` distinct 64×48 gradient PNGs (`a.png`, `b.png`, …). + pub(crate) fn with_photos(tag: &str, n: usize) -> SyntheticLibrary { + assert!((1..=26).contains(&n), "fixture photo names use a through z"); + let dir = std::env::temp_dir().join(format!("lc-ui-fixture-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (w, h) = (64usize, 48usize); + let mut photos = Vec::new(); + for i in 0..n { + let name = format!("{}.png", char::from(b'a' + i as u8)); + let data: Vec<[u8; 4]> = (0..w * h) + .map(|p| { + let (x, y) = (p % w, p / w); + [((x * 3 + i * 17) % 256) as u8, ((y * 4 + i * 9) % 256) as u8, ((x + y + i) % 256) as u8, 255] + }) + .collect(); + let img = lightcraft_raster::Rgba8 { width: w, height: h, data }; + let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap(); + let path = dir.join(name); + std::fs::write(&path, png).unwrap(); + photos.push(path); + } + SyntheticLibrary { dir, photos } + } + + /// A session whose library holds only the fixture photos, the first active and selected, with + /// the file system loaders installed so Detail really decodes the PNGs. + pub(crate) fn app(&self) -> LightcraftApp { + let mut session = lightcraft_engine::Session::new().with_fs(); + let mut ids = Vec::new(); + for (i, path) in self.photos.iter().enumerate() { + let id = session.catalog.alloc_photo_id(); + // distinct capture times keep the default (newest first) view order deterministic + let captured = format!("2026-01-01T10:{:02}:00", i % 60); + let mut photo = Photo::new( + id, + Source::File { path: path.to_string_lossy().into_owned() }, + path.file_name().map_or("photo.png", |n| n.to_str().unwrap_or("photo.png")), + "PNG", + 64, + 48, + &captured, + ); + photo.captured = Some(captured); + session.catalog.apply(Op::AddPhoto { photo: Box::new(photo) }).unwrap(); + ids.push(id); + } + if let Some(first) = ids.first() { + session.selection = lightcraft_engine::Selection::single(*first); + } + LightcraftApp::new(session, Services { png: None, ..Default::default() }) + } +} + +impl Drop for SyntheticLibrary { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } +} diff --git a/crates/ui-egui/src/tests/keyword_files.rs b/crates/ui-egui/src/tests/keyword_files.rs index f7180c3175..1d2f66340f 100644 --- a/crates/ui-egui/src/tests/keyword_files.rs +++ b/crates/ui-egui/src/tests/keyword_files.rs @@ -6,15 +6,14 @@ use std::time::Duration; use serde_json::json; use crate::headless::Headless; -use crate::{LightcraftApp, Services}; +use crate::tests_fixtures::SyntheticLibrary; const T: Duration = Duration::from_secs(20); const SETTLE: Duration = Duration::from_secs(120); -fn app() -> Headless { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); - let mut h = Headless::new(app, [1200.0, 800.0], 1.0); - h.settle(SETTLE); +fn app(library: &SyntheticLibrary) -> Headless { + let mut h = Headless::new(library.app(), [1200.0, 800.0], 1.0); + assert!(h.settle(SETTLE), "keyword fixture did not settle"); h } @@ -26,9 +25,9 @@ fn temp_dir(tag: &str) -> std::path::PathBuf { } fn run(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value { - let r = h.request("engine.execute", json!({"command": command, "params": params}), T); - h.settle(SETTLE); - r + // These commands finish synchronously, including their toast, before replying. Settling + // unrelated photo renders afterwards advances the simulated clock and can expire it. + h.request("engine.execute", json!({"command": command, "params": params}), T) } fn toast(h: &Headless) -> String { @@ -38,7 +37,8 @@ fn toast(h: &Headless) -> String { /// The File menu offers Import Keywords… and Export Keywords…. #[test] fn the_file_menu_offers_keyword_files() { - let mut h = app(); + let library = SyntheticLibrary::new("keyword-files-menu"); + let mut h = app(&library); let r = h.request("ui.menu.list", json!({}), T); let items = r["result"].to_string(); for id in ["file.importKeywords", "file.exportKeywords"] { @@ -50,7 +50,8 @@ fn the_file_menu_offers_keyword_files() { #[test] fn importing_keywords_says_what_it_added() { let dir = temp_dir("import"); - let mut h = app(); + let library = SyntheticLibrary::new("keyword-files-import"); + let mut h = app(&library); let path = dir.join("vocabulary.utf8"); std::fs::write(&path, "[Places]\r\n\tPortugal\r\n\t\tLisbon\r\n").unwrap(); let r = run(&mut h, "file.importKeywords", json!({"path": path.to_string_lossy()})); @@ -64,7 +65,8 @@ fn importing_keywords_says_what_it_added() { #[test] fn exporting_keywords_warns_about_capture_one() { let dir = temp_dir("export"); - let mut h = app(); + let library = SyntheticLibrary::new("keyword-files-export"); + let mut h = app(&library); let id = h.app.session.visible_cloned()[0].0; run(&mut h, "photo.setMeta", json!({"ids": [id], "addKeywords": ["fish, chips"]})); let path = dir.join("keywords.txt"); @@ -79,7 +81,8 @@ fn exporting_keywords_warns_about_capture_one() { #[test] fn exporting_keywords_names_what_it_left_out() { let dir = temp_dir("export-left-out"); - let mut h = app(); + let library = SyntheticLibrary::new("keyword-files-export-left-out"); + let mut h = app(&library); let id = h.app.session.visible_cloned()[0].0; run(&mut h, "photo.setMeta", json!({"ids": [id], "addKeywords": ["Travel|[draft]"]})); let path = dir.join("keywords.txt"); @@ -96,9 +99,12 @@ fn exporting_keywords_names_what_it_left_out() { /// A list that can't be written (a folder that isn't there) says why, as importing one does. #[test] fn exporting_keywords_says_why_it_failed() { - let mut h = app(); - let path = std::env::temp_dir().join("lc-no-such-folder-for-keywords").join("keywords.txt"); + let library = SyntheticLibrary::new("keyword-files-export-failure"); + let mut h = app(&library); + let dir = temp_dir("export-failure"); + let path = dir.join("missing-folder").join("keywords.txt"); let r = run(&mut h, "file.exportKeywords", json!({"path": path.to_string_lossy()})); assert_eq!(r["ok"], false, "{r}"); assert!(toast(&h).contains("keywords.txt"), "{}", toast(&h)); + let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/ui-egui/src/tests/library_problem.rs b/crates/ui-egui/src/tests/library_problem.rs index dc7a037a47..1a2e5a2d9b 100644 --- a/crates/ui-egui/src/tests/library_problem.rs +++ b/crates/ui-egui/src/tests/library_problem.rs @@ -8,6 +8,7 @@ use serde_json::json; use crate::headless::Headless; use crate::panels::library_problem::LibraryProblem; +use crate::tests_fixtures::SyntheticLibrary; use crate::{LightcraftApp, Services}; const T: Duration = Duration::from_secs(20); @@ -163,10 +164,10 @@ fn unopenable_library_asks_instead_of_running_a_demo() { /// own buttons are covered: the click then reports a no-op success and the state never changes. #[test] fn a_backdrop_press_keeps_the_library_window_above_its_dim() { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); - let mut h = Headless::new(app, [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("library-backdrop-press"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error")); - assert!(h.settle(Duration::from_secs(20)), "the library window settles"); + assert!(h.settle(Duration::from_secs(20)), "the library window settles (pending slots: {:?})", h.app.renderer.pending_slots()); assert!(has(&h, "button:libraryTemporary"), "the window is laid out"); // Outside the centred window: on the full-screen dim, which egui raises when pressed. let at = egui::pos2(10.0, 10.0); @@ -189,17 +190,14 @@ fn a_backdrop_press_keeps_the_library_window_above_its_dim() { /// handlers must stay still while it is on screen, then the dialog keeps its ordinary behaviour. #[test] fn the_library_window_blocks_settings_escape_and_the_backdrop() { - let mut h = Headless::new( - LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), - [1200.0, 900.0], - 1.0, - ); + let files = SyntheticLibrary::new("library-blocks-settings"); + let mut h = Headless::new(files.app(), [1200.0, 900.0], 1.0); h.step(); assert!(h.app.run("app.settings", json!({})).is_ok(), "Settings opens"); - assert!(h.settle(Duration::from_secs(20)), "Settings settles"); + assert!(h.settle(Duration::from_secs(20)), "Settings settles (pending slots: {:?})", h.app.renderer.pending_slots()); h.app.ui.settings.close_dialogs_outside = true; h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error")); - assert!(h.settle(Duration::from_secs(20)), "the library window settles"); + assert!(h.settle(Duration::from_secs(20)), "the library window settles (pending slots: {:?})", h.app.renderer.pending_slots()); assert!(has(&h, "button:libraryRetry"), "the library window is laid out"); // Escape belongs to the window, not to Settings behind it (the window has no Escape action // for this error; the launch list handles its own keys separately). @@ -386,14 +384,11 @@ fn alt_launch_creation_after_raw_input_cancels_an_injected_gesture() { /// become a background click. #[test] fn a_held_background_press_is_released_when_a_library_window_appears_after_raw_input() { - let mut h = Headless::new( - LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), - [1200.0, 900.0], - 1.0, - ); + let files = SyntheticLibrary::new("library-after-raw-press"); + let mut h = Headless::new(files.app(), [1200.0, 900.0], 1.0); h.step(); assert!(h.app.run("app.settings", json!({})).is_ok(), "Settings opens"); - assert!(h.settle(T), "Settings settles"); + assert!(h.settle(T), "Settings settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "check:settings.confirmDelete"; let at = h.app.widgets.iter().rev().find(|(widget, _)| widget == id).map(|(_, rect)| rect.center()).expect("the checkbox is on screen"); let before = h.app.ui.settings.confirm_delete; @@ -738,11 +733,8 @@ fn clipboard_pastes_into_the_launch_lists_own_text_prompt() { /// same field must accept typing again once the window is gone. #[test] fn a_settled_library_window_keeps_a_background_dialogs_text_draft_unchanged() { - let mut h = Headless::new( - LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), - [1200.0, 760.0], - 1.0, - ); + let files = SyntheticLibrary::new("library-text-draft"); + let mut h = Headless::new(files.app(), [1200.0, 760.0], 1.0); h.settle(T); h.app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: "Trip".into(), folder: false, parent: None, include_selected: false }); for _ in 0..4 { @@ -750,7 +742,7 @@ fn a_settled_library_window_keeps_a_background_dialogs_text_draft_unchanged() { } assert!(h.view.ctx.text_edit_focused(), "the dialog's field takes the focus while it is the top modal"); h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error")); - assert!(h.settle(T), "the library window settles over the dialog"); + assert!(h.settle(T), "the library window settles over the dialog (pending slots: {:?})", h.app.renderer.pending_slots()); assert!(has(&h, "button:libraryRetry"), "the library window is laid out"); assert!(crate::panels::library_problem::blocks_dialog_input(&h.app), "the library window owns input"); assert!(!h.view.ctx.text_edit_focused(), "the background dialog's field must not regain the focus behind the library window"); @@ -766,7 +758,7 @@ fn a_settled_library_window_keeps_a_background_dialogs_text_draft_unchanged() { assert!(!h.view.ctx.text_edit_focused(), "several settled barrier frames still leave the field unfocused"); // Positive control: with the library window gone, the same field takes focus and typing again. h.app.library_problem = None; - assert!(h.settle(T), "the dialog settles once the library window is gone"); + assert!(h.settle(T), "the dialog settles once the library window is gone (pending slots: {:?})", h.app.renderer.pending_slots()); assert!(!crate::panels::library_problem::blocks_dialog_input(&h.app), "there is no barrier now"); assert!(h.view.ctx.text_edit_focused(), "the dialog's field is usable again"); h.events.push(egui::Event::Text("Holiday".into())); diff --git a/crates/ui-egui/src/tests/lifecycle_037.rs b/crates/ui-egui/src/tests/lifecycle_037.rs index bfb08f1b49..3c4e8009b3 100644 --- a/crates/ui-egui/src/tests/lifecycle_037.rs +++ b/crates/ui-egui/src/tests/lifecycle_037.rs @@ -7,6 +7,7 @@ use std::time::Duration; use crate::headless::Headless; use crate::render::{Slot, Tex}; +use crate::tests_fixtures::SyntheticLibrary; use crate::{ControlResponse, LightcraftApp, MAX_PENDING_SHOTS, MAX_SHOT_BYTES, PendingShot, Services, ShotGeometry}; use lightcraft_engine::slideshow::Item; @@ -302,6 +303,42 @@ fn a_capture_completed_after_a_catalog_edit_is_still_written() { let _ = std::fs::remove_file(&path); } +#[test] +fn headless_capture_finishes_once_and_releases_only_its_reservation() { + for case in ["success", "overflow", "image-cap", "aggregate-cap"] { + let (mut app, seen) = app_with_screenshot_files(); + let ctx = egui::Context::default(); + let path = temp_png(&format!("headless-finish-{case}")); + let (tx, rx) = reply_channel(); + app.last_shot_geometry = Some(geometry(100.0, 100.0)); + let target = app.session.pin_write_target(&path.to_string_lossy()).expect("pin path"); + app.admit_shot(&ctx, Some(target), true, tx).expect("admit"); + let mut shot = app.pending_screenshots.pop().expect("pending shot"); + let mut image = egui::ColorImage::filled([200, 200], egui::Color32::WHITE); + let other_reservations = if case == "aggregate-cap" { crate::MAX_SHOT_TOTAL_BYTES - shot.reserved } else { 0 }; + app.shot_bytes += other_reservations; + if case == "overflow" { + image.size = [usize::MAX, 2]; + } else if case == "image-cap" { + image.size = [MAX_SHOT_BYTES, 1]; + } + app.finish_headless_shot(&mut shot, &image); + let reply = rx.try_recv().expect("one reply"); + assert_eq!(reply["ok"], case == "success", "{case}: {reply}"); + match case { + "overflow" => assert_eq!(reply["error"], "ui.screenshot: the rendered image has invalid dimensions"), + "image-cap" => assert!(reply["error"].as_str().expect("error message").contains("per-image limit")), + "aggregate-cap" => assert!(reply["error"].as_str().expect("error message").contains("bytes reserved is over")), + _ => {} + } + assert!(rx.try_recv().is_err(), "no duplicate reply"); + assert_eq!(app.shot_bytes, other_reservations, "{case}"); + assert_eq!(path.exists(), case == "success", "{case}"); + assert_eq!(seen.lock().unwrap().is_some(), case == "success", "{case}"); + let _ = std::fs::remove_file(path); + } +} + #[test] fn an_image_larger_than_its_reservation_is_grown_and_written() { // Wayland has no `inner_rect`, so on a hidden tick the reservation can be based on the old @@ -559,9 +596,10 @@ fn hidden_slideshow_pauses_and_keeps_its_remaining_time() { // Every slide is "ready": without the hidden gate, 10 hidden seconds would advance the show. for id in app.session.visible_cloned() { let tex = ctx.load_texture(format!("slide-{}", id.0), egui::ColorImage::filled([2, 2], egui::Color32::WHITE), Default::default()); - app.renderer - .textures - .insert(Slot::Slide(Item::Photo(id)), Tex { key: 0, photo: id, tex, size: [2, 2], histogram: None, ms: 0.0, quick: None, pixels: None }); + app.renderer.textures.insert( + Slot::Slide(Item::Photo(id)), + Tex { look: None, key: 0, photo: id, tex, size: [2, 2], histogram: None, ms: 0.0, quick: None, pixels: None }, + ); } let first = app.ui.slideshow.as_ref().unwrap().play.current(); let selection = app.session.active(); @@ -990,13 +1028,14 @@ fn a_maximum_step_drag_is_delivered_past_the_five_second_stall_limit() { #[test] fn a_click_on_a_control_under_a_nonmodal_overlay_is_refused_without_activating_the_overlay() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("control-under-overlay"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); h.app.ui.open_sections = vec!["light".to_string()]; - assert!(h.settle(Duration::from_secs(60)), "the edit panel settles"); + assert!(h.settle(Duration::from_secs(60)), "the edit panel settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "slider:light.exposure"; assert!(h.app.widgets.iter().any(|(widget, _)| widget == id), "{id} is on screen"); let before = h.app.session.develop_of(photo).expect("its develop settings").light.exposure; @@ -1028,12 +1067,13 @@ fn a_click_on_a_control_under_a_nonmodal_overlay_is_refused_without_activating_t /// interactive centre). This is the positive control the hover-only probe lacked. #[test] fn an_unobstructed_click_on_a_subrect_reaches_the_widget() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("unobstructed-subrect"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); - assert!(h.settle(Duration::from_secs(60)), "the detail view settles"); + assert!(h.settle(Duration::from_secs(60)), "the detail view settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "star:bottom:2"; assert!(h.app.widgets.iter().any(|(widget, _)| widget == id), "{id} is on screen"); assert!(h.app.widget_hits.iter().any(|(widget, _)| widget == id), "{id} has a probe"); @@ -1047,11 +1087,12 @@ fn an_unobstructed_click_on_a_subrect_reaches_the_widget() { /// must not turn the release into an overlay click; the pointer is released away from the target. #[test] fn an_overlay_that_appears_after_the_press_cancels_the_release() { - let mut h = Headless::new(app(), [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("overlay-after-press"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); h.step(); let t = Duration::from_secs(20); assert_eq!(h.request("ui.key", serde_json::json!({"key": ",", "cmd": true}), t)["ok"], true); - assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles"); + assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "check:settings.confirmDelete"; let target = h.app.widgets.iter().rev().find(|(widget, _)| widget == id).map(|(_, rect)| *rect).expect("the checkbox is on screen"); let before = h.app.ui.settings.confirm_delete; @@ -1096,13 +1137,14 @@ fn an_overlay_that_appears_after_the_press_cancels_the_release() { /// cleanup moved 8 px to suppress the click, which could start a drag and change a slider/curve. #[test] fn canceling_a_pressed_slider_releases_it_without_dragging_it() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("canceled-pressed-slider"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); h.app.ui.open_sections = vec!["light".to_string()]; - assert!(h.settle(Duration::from_secs(60)), "the edit panel settles"); + assert!(h.settle(Duration::from_secs(60)), "the edit panel settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "slider:light.exposure"; let target = h.app.widgets.iter().rev().find(|(widget, _)| widget == id).map(|(_, rect)| *rect).expect("the slider is on screen"); let before = h.app.session.develop_of(photo).expect("its develop settings").light.exposure; @@ -1136,11 +1178,12 @@ fn canceling_a_pressed_slider_releases_it_without_dragging_it() { /// makes the target invisible too: the release is refused rather than clicking through it. #[test] fn a_nonprompt_overlay_that_appears_after_the_press_cancels_the_release() { - let mut h = Headless::new(app(), [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("nonprompt-overlay-after-press"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); h.step(); let t = Duration::from_secs(20); assert_eq!(h.request("ui.key", serde_json::json!({"key": ",", "cmd": true}), t)["ok"], true); - assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles"); + assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "check:settings.confirmDelete"; let target = h.app.widgets.iter().rev().find(|(widget, _)| widget == id).map(|(_, rect)| *rect).expect("the checkbox is on screen"); let before = h.app.ui.settings.confirm_delete; @@ -1184,11 +1227,12 @@ fn a_nonprompt_overlay_that_appears_after_the_press_cancels_the_release() { /// `ui.clickWidget` counterpart of the press/release cancellation above. #[test] fn a_library_problem_overlay_blocks_a_control_click_in_settings() { - let mut h = Headless::new(app(), [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("library-overlay-settings-click"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); let t = Duration::from_secs(20); h.step(); assert!(h.app.run("app.settings", serde_json::json!({})).is_ok(), "Settings opens"); - assert!(h.settle(Duration::from_secs(60)), "Settings settles"); + assert!(h.settle(Duration::from_secs(60)), "Settings settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "check:settings.confirmDelete"; let before = h.app.ui.settings.confirm_delete; h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("/no/such/library", "test error")); @@ -1476,10 +1520,15 @@ fn a_prompt_created_after_raw_input_hook_shields_a_held_background_press() { /// success after the prompt filters it. #[test] fn a_synthetic_press_already_delivered_is_canceled_by_a_prompt_created_after_raw_input_hook() { - let mut h = Headless::new(app(), [1200.0, 900.0], 1.0); + let files = SyntheticLibrary::new("press-after-raw-hook"); + let mut h = Headless::new(files.app(), [1200.0, 900.0], 1.0); h.step(); assert!(h.app.run("app.settings", serde_json::json!({})).is_ok(), "Settings opens"); - assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles before its target rect is captured"); + assert!( + h.settle(Duration::from_secs(60)), + "the Settings dialog settles before its target rect is captured (pending slots: {:?})", + h.app.renderer.pending_slots() + ); let id = "check:settings.confirmDelete"; let target = h.app.widgets.iter().rev().find(|(widget, _)| widget == id).map(|(_, rect)| *rect).expect("the checkbox is on screen"); let before = h.app.ui.settings.confirm_delete; @@ -1512,13 +1561,14 @@ fn a_synthetic_press_already_delivered_is_canceled_by_a_prompt_created_after_raw /// stay unchanged. #[test] fn raw_background_gestures_at_a_settled_prompt_do_not_reach_the_covered_view() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("raw-gestures-settled-prompt"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); assert!(h.app.run("app.settings", serde_json::json!({})).is_ok(), "Settings opens"); - assert!(h.settle(Duration::from_secs(60)), "the detail view and Settings settle"); + assert!(h.settle(Duration::from_secs(60)), "the detail view and Settings settle (pending slots: {:?})", h.app.renderer.pending_slots()); let image = h.app.image_rect.expect("the detail image is on screen"); let at = image.min + egui::vec2(12.0, 12.0); let before = h.app.ui.settings.confirm_delete; @@ -1579,13 +1629,14 @@ fn a_prompt_keeps_key_releases_and_clears_held_keys() { /// moves/release are discarded, the pointer is released without a click, and the slider is intact. #[test] fn a_prompt_aborts_an_in_progress_synthetic_drag() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("prompt-aborts-drag"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); h.app.ui.open_sections = vec!["light".to_string()]; - assert!(h.settle(Duration::from_secs(60)), "the edit panel settles"); + assert!(h.settle(Duration::from_secs(60)), "the edit panel settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "slider:light.exposure"; let before = h.app.session.develop_of(photo).expect("its develop settings").light.exposure; let (tx, rx) = reply_channel(); @@ -1736,12 +1787,13 @@ fn a_settings_notice_blocks_background_input_and_its_ok_button_works() { #[test] fn clipping_changed_between_the_move_and_press_is_seen_in_the_latest_registry() { - let mut h = Headless::new(app(), [1400.0, 900.0], 1.0); + let files = SyntheticLibrary::new("clipping-changed"); + let mut h = Headless::new(files.app(), [1400.0, 900.0], 1.0); let photo = h.app.session.visible_cloned()[0]; let t = Duration::from_secs(20); assert_eq!(h.request("engine.execute", serde_json::json!({"command": "library.select", "params": {"ids": [photo.0]}}), t)["ok"], true); assert_eq!(h.request("ui.set", serde_json::json!({"view": "detail", "right": "edit"}), t)["ok"], true); - assert!(h.settle(Duration::from_secs(60)), "the detail view settles"); + assert!(h.settle(Duration::from_secs(60)), "the detail view settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "star:bottom:2"; let (tx, rx) = reply_channel(); h.app.admit_gesture(&h.view.ctx.clone(), "ui.clickWidget".into(), serde_json::json!({"id": id}), tx).expect("the click is admitted"); @@ -1811,12 +1863,13 @@ fn queued_pointer_cleanup_requests_a_repaint_without_a_synthetic_event() { #[test] fn a_moved_widget_cancels_before_its_first_event_and_leaves_a_later_gesture_queued() { - let mut h = Headless::new(app(), [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("moved-widget-queued"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); h.step(); let t = Duration::from_secs(20); let r = h.request("ui.key", serde_json::json!({"key": ",", "cmd": true}), t); assert_eq!(r["ok"], true, "{r}"); - assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles"); + assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles (pending slots: {:?})", h.app.renderer.pending_slots()); assert!(h.app.widgets.iter().any(|(id, _)| id == "check:settings.confirmDelete")); assert!(h.app.widgets.iter().any(|(id, _)| id == "button:settingsStartup-2")); @@ -1862,12 +1915,13 @@ fn a_moved_widget_cancels_before_its_first_event_and_leaves_a_later_gesture_queu #[test] fn a_click_whose_target_vanishes_after_the_press_fails_and_releases_the_pointer() { - let mut h = Headless::new(app(), [1000.0, 760.0], 1.0); + let files = SyntheticLibrary::new("target-vanishes"); + let mut h = Headless::new(files.app(), [1000.0, 760.0], 1.0); h.step(); let t = Duration::from_secs(20); let r = h.request("ui.key", serde_json::json!({"key": ",", "cmd": true}), t); assert_eq!(r["ok"], true, "{r}"); - assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles"); + assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles (pending slots: {:?})", h.app.renderer.pending_slots()); let before = h.app.ui.settings.confirm_delete; let (tx, rx) = reply_channel(); @@ -2053,11 +2107,12 @@ fn disclosure_rows_publish_expanded_not_toggled() { /// widget was clipped before delivery is refused, as is one under the non-modal library window. #[test] fn hover_widget_refuses_clipped_and_covered_targets() { - let mut h = Headless::new(app(), [1200.0, 900.0], 1.0); + let files = SyntheticLibrary::new("hover-refuses-clipped"); + let mut h = Headless::new(files.app(), [1200.0, 900.0], 1.0); let t = Duration::from_secs(20); h.step(); assert!(h.app.run("app.settings", serde_json::json!({})).is_ok(), "Settings opens"); - assert!(h.settle(Duration::from_secs(60)), "Settings settles"); + assert!(h.settle(Duration::from_secs(60)), "Settings settles (pending slots: {:?})", h.app.renderer.pending_slots()); let id = "check:settings.confirmDelete"; assert!(h.app.widget_hits.iter().any(|(widget, _)| widget == id), "{id} has a probe"); diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs index 96b0a1a9d6..569a12910c 100644 --- a/crates/ui-egui/src/tests/panels.rs +++ b/crates/ui-egui/src/tests/panels.rs @@ -2206,7 +2206,13 @@ fn recognise_faces_stays_off_until_a_model_is_chosen() { let dir = std::env::temp_dir().join(format!("lc-ui-faces-toggle-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - h.app.session.face_models_dir = Some(dir.join("models")); + // This tests the recognition toggle, not GPU attachment. Seed the fresh fixture before + // loading a model; the mutation command also detaches the process-wide detector. + let models = dir.join("models"); + std::fs::create_dir_all(&models).unwrap(); + std::fs::write(models.join("settings.json"), br#"{"cpuOnly":true}"#).unwrap(); + h.app.session.face_models_dir = Some(models); + assert_eq!(h.app.session.execute("faces.gpu", &json!({})).unwrap()["cpuOnly"], true); let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "faces"}}), T); assert_eq!(r["ok"], true, "{r}"); h.settle(SETTLE); diff --git a/crates/ui-egui/src/tests/preset_import.rs b/crates/ui-egui/src/tests/preset_import.rs index 7a1abf6ee9..0ed804a589 100644 --- a/crates/ui-egui/src/tests/preset_import.rs +++ b/crates/ui-egui/src/tests/preset_import.rs @@ -12,9 +12,10 @@ const T: Duration = Duration::from_secs(20); const SETTLE: Duration = Duration::from_secs(120); fn app() -> Headless { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + // Profile import needs no photos. Avoid unrelated demo renders while testing its notice. + let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() }); let mut h = Headless::new(app, [1200.0, 800.0], 1.0); - h.settle(SETTLE); + assert!(h.settle(SETTLE), "the empty library settles before importing profiles"); h } @@ -27,9 +28,9 @@ fn temp_dir(tag: &str) -> std::path::PathBuf { fn import(h: &mut Headless, paths: &[&std::path::Path]) -> serde_json::Value { let paths: Vec = paths.iter().map(|p| p.to_string_lossy().to_string()).collect(); - let r = h.request("engine.execute", json!({"command": "file.importPresets", "params": {"paths": paths}}), T); - h.settle(SETTLE); - r + // Import and its toast complete synchronously before the control reply. Settling + // unrelated rendering afterwards advances simulated time and can expire the notice. + h.request("engine.execute", json!({"command": "file.importPresets", "params": {"paths": paths}}), T) } fn toast(h: &Headless) -> String { diff --git a/crates/ui-egui/src/tests/preview_limit.rs b/crates/ui-egui/src/tests/preview_limit.rs index e87e523551..07a30c8a58 100644 --- a/crates/ui-egui/src/tests/preview_limit.rs +++ b/crates/ui-egui/src/tests/preview_limit.rs @@ -137,23 +137,26 @@ fn saved_limits_round_trip_and_unknown_ones_are_reset() { // ---- Behaviour in the running UI ------------------------------------------------------------- mod in_the_loupe { + use std::cell::RefCell; + use std::rc::Rc; use std::time::Duration; use serde_json::json; use crate::headless::Headless; - use crate::render::Slot; + use crate::render::{RenderOffload, Renderer, Slot}; + use crate::tests_render_fixtures::{HEIGHT, LONG_EDGE, RenderFixture, WIDTH}; use crate::{LightcraftApp, Services}; + use lightcraft_engine::SourceLevel; + use lightcraft_engine::media::{RenderJob, RenderResult, SourceRef}; const T: Duration = Duration::from_secs(20); const SETTLE: Duration = Duration::from_secs(120); - /// The demo library in a 1400×900 window, the first photo open in Detail. - fn detail() -> (Headless, usize) { - detail_in([1400.0, 900.0]) - } - - fn detail_in(size: [f32; 2]) -> (Headless, usize) { + /// The original 24 MP procedural demo library in a 1400×900 window, the first photo open + /// in Detail. Kept for `profile_slider_drag`, whose meaning depends on the demo's render cost + /// and neighbour prefetch. + fn demo_detail_in(size: [f32; 2]) -> (Headless, usize) { let services = Services { png: None, ..Default::default() }; let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services); let mut h = Headless::new(app, size, 1.0); @@ -168,6 +171,34 @@ mod in_the_loupe { (h, native) } + /// Two real 6000×4000 file-backed photos in a 1400×900 window, the first open in Detail. + /// Destructure the returned tuple immediately, binding the fixture before `Headless`, so the + /// app drops first. Keeping the tuple intact would drop its fixture field first. + fn detail() -> (RenderFixture, Headless, usize) { + detail_in([1400.0, 900.0]) + } + + fn detail_in(size: [f32; 2]) -> (RenderFixture, Headless, usize) { + detail_with(RenderFixture::new("preview", 2), size) + } + + fn detail_with(fixture: RenderFixture, size: [f32; 2]) -> (RenderFixture, Headless, usize) { + let services = Services { png: None, ..Default::default() }; + let app = LightcraftApp::new(fixture.session(), services); + let mut h = Headless::new(app, size, 1.0); + let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + let (native, dims) = { + let id = h.app.session.active().expect("a photo is open"); + let p = h.app.session.catalog.photo(id).expect("it is in the catalog"); + (p.width.max(p.height) as usize, (p.width, p.height)) + }; + assert_eq!(dims, (WIDTH, HEIGHT), "the render fixture is a real 24 MP landscape 3:2 photo"); + assert_eq!(native, LONG_EDGE, "the render fixture is a real 24 MP photo"); + (fixture, h, native) + } + fn rendered_long_edge(h: &Headless) -> usize { let t = h.app.renderer.textures.get(&Slot::Main).expect("the loupe rendered"); t.size[0].max(t.size[1]) @@ -178,7 +209,7 @@ mod in_the_loupe { // cut from the photo's own pixels #[test] fn one_to_one_is_a_canvas_sized_frame_plus_a_native_window() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); let main = rendered_long_edge(&h); @@ -195,7 +226,7 @@ mod in_the_loupe { // overlay swaps the window for one with the other look #[test] fn a_mask_overlay_at_one_to_one_is_a_window_and_keeps_zoom_and_pan() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); for (command, params) in [("panel.masking", json!({})), ("mask.add", json!({"kind": "radial", "center": [0.4, 0.5], "rx": 0.2, "ry": 0.2}))] { let r = h.request("engine.execute", json!({"command": command, "params": params}), T); assert_eq!(r["ok"], true, "{command}: {r}"); @@ -233,7 +264,7 @@ mod in_the_loupe { // size (slower, and its detail stops at the frame render's resolution) #[test] fn an_all_masks_overlay_at_one_to_one_is_a_window() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); for (command, params) in [ ("panel.masking", json!({})), ("mask.add", json!({"kind": "radial", "center": [0.4, 0.5], "rx": 0.2, "ry": 0.2})), @@ -266,7 +297,7 @@ mod in_the_loupe { // photo's full size; switching the proof off swaps the window back to the plain look #[test] fn soft_proofing_at_one_to_one_is_a_window() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); let plain = h.app.region_view.expect("a window render without the proof"); @@ -288,7 +319,7 @@ mod in_the_loupe { // window render carrying the overlay, over a canvas-sized whole frame, as without it #[test] fn visualize_spots_at_one_to_one_is_a_window() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); let plain = h.app.region_view.expect("a window render without the overlay"); @@ -306,7 +337,7 @@ mod in_the_loupe { // Given the user capped the preview at 1600 px, then 1:1 is rendered no larger than that #[test] fn an_explicit_limit_still_caps_one_to_one() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); assert!(native > 1600, "the demo photo must be larger than the cap for this to mean anything (is {native})"); h.app.ui.settings.preview_limit = 1600; h.request("engine.execute", json!({"command": "view.zoom100"}), T); @@ -321,7 +352,7 @@ mod in_the_loupe { // Given a fit view, the whole-frame render is all there is: no second render, no original decoded #[test] fn a_fit_view_makes_no_window_render() { - let (h, _) = detail(); + let (_fx, h, _) = detail(); assert_eq!(region_tile(&h), None); assert_eq!(h.app.region_view, None); } @@ -330,7 +361,7 @@ mod in_the_loupe { // rendered on its own at the zoom scale and drawn over the whole-frame render #[test] fn a_capped_one_to_one_view_gets_a_sharp_window() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.app.ui.settings.preview_limit = 1600; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -345,7 +376,7 @@ mod in_the_loupe { // Given 800 % zoom (a frame of 48000 px), the window is rendered and bounded #[test] fn eight_to_one_renders_only_what_is_visible() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.app.ui.zoom = crate::state::Zoom::Percent(800.0); h.settle(SETTLE); let (w, hh) = region_tile(&h).expect("a window render"); @@ -361,7 +392,7 @@ mod in_the_loupe { // same place of the frame (a shift, a flip or a wrong scale would not) #[test] fn the_window_lines_up_with_the_whole_frame_render() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.renderer.keep_pixels = true; h.app.ui.settings.preview_limit = 1600; h.request("engine.execute", json!({"command": "view.zoom100"}), T); @@ -369,26 +400,31 @@ mod in_the_loupe { let view = h.app.region_view.expect("a window render"); let tile = h.app.renderer.textures.get(&Slot::Region).and_then(|t| t.pixels.clone()).expect("tile pixels"); let main = h.app.renderer.textures.get(&Slot::Main).and_then(|t| t.pixels.clone()).expect("main pixels"); - let (mut sum, mut n) = (0.0f32, 0.0f32); - for j in (8..tile.size[1] - 8).step_by(37) { - for i in (8..tile.size[0] - 8).step_by(37) { - // the same point of the frame in the whole-frame render - let u = (view.window.x + i) as f32 / view.full.0 as f32; - let v = (view.window.y + j) as f32 / view.full.1 as f32; - let (mx, my) = - (((u * main.size[0] as f32) as usize).min(main.size[0] - 1), ((v * main.size[1] as f32) as usize).min(main.size[1] - 1)); - let (a, b) = (tile.pixels[j * tile.size[0] + i], main.pixels[my * main.size[0] + mx]); - sum += (0..3).map(|k| (a.to_array()[k] as f32 - b.to_array()[k] as f32).abs()).sum::() / 3.0; - n += 1.0; + let mean_difference = |offset: usize| { + let (mut sum, mut n) = (0.0f32, 0.0f32); + for j in (8..tile.size[1] - 8).step_by(37) { + for i in (8..tile.size[0] - 8).step_by(37) { + // the same point of the frame in the whole-frame render + let u = (view.window.x + i + offset) as f32 / view.full.0 as f32; + let v = (view.window.y + j) as f32 / view.full.1 as f32; + let (mx, my) = + (((u * main.size[0] as f32) as usize).min(main.size[0] - 1), ((v * main.size[1] as f32) as usize).min(main.size[1] - 1)); + let (a, b) = (tile.pixels[j * tile.size[0] + i], main.pixels[my * main.size[0] + mx]); + sum += (0..3).map(|k| (a.to_array()[k] as f32 - b.to_array()[k] as f32).abs()).sum::() / 3.0; + n += 1.0; + } } - } - assert!(sum / n < 6.0, "mean difference {:.2} / 255 between the window and the frame render", sum / n); + sum / n + }; + let difference = mean_difference(0); + assert!(difference < 6.0, "mean difference {:.2} / 255 between the window and the frame render", difference); + assert!(mean_difference(16) > 6.0, "the fixture must expose a 16-pixel horizontal misalignment"); } // Given a Fit view, a slider drag (whose Main draft is at 0.6 scale) does not start window renders #[test] fn a_slider_drag_at_fit_makes_no_window_render() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.session.begin_interaction("Exposure").unwrap(); for _ in 0..5 { h.step(); @@ -400,7 +436,7 @@ mod in_the_loupe { // Given a zoomed view with a window render, when I zoom back to Fit, then the window's texture is freed #[test] fn the_window_texture_is_freed_when_it_is_not_needed() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.settings.preview_limit = 1600; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -415,7 +451,7 @@ mod in_the_loupe { // too (while one is held the app never counts as idle and keeps the GPU copy of the original) #[test] fn leaving_a_zoomed_detail_view_frees_its_windows() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -430,7 +466,7 @@ mod in_the_loupe { // is no longer current and is replaced by one for the new look #[test] fn an_edit_replaces_the_window() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.settings.preview_limit = 1600; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -446,7 +482,7 @@ mod in_the_loupe { // Given a photo with spot removal, a zoomed view still gets its sharp window #[test] fn a_photo_with_spots_gets_a_window_too() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); let id = h.app.session.active().unwrap(); let mut s = (*h.app.session.develop_of(id).unwrap()).clone(); s.spots.push(lightcraft_develop::Spot { @@ -466,7 +502,7 @@ mod in_the_loupe { // I zoom to 1:1 and 8:1, then no texture the loupe holds is larger than that #[test] fn no_loupe_texture_exceeds_the_gpu_limit() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.max_texture_side = 2048; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -486,7 +522,7 @@ mod in_the_loupe { // the whole canvas: no strip of the magnified whole-frame render beside it #[test] fn a_window_covers_a_canvas_wider_than_2048_px() { - let (mut h, native) = detail_in([2600.0, 1200.0]); + let (_fx, mut h, native) = detail_in([2600.0, 1200.0]); assert!(native > 2600, "the photo is wider than the canvas at 1:1: {native}"); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -507,7 +543,7 @@ mod in_the_loupe { // canvas, not the zoom: a draft of the whole frame at canvas scale and a draft of the window #[test] fn a_slider_drag_when_zoomed_in_renders_canvas_sized_drafts() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.app.ui.zoom = crate::state::Zoom::Percent(400.0); h.settle(SETTLE); h.app.session.begin_interaction("Exposure").unwrap(); @@ -542,7 +578,7 @@ mod in_the_loupe { // the GPU device up front (the loupe never waits for it): with one, it is the earlier // drag's time that makes this drag small let _ = lightcraft_engine::gpu::available(); - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); let id = h.app.session.active().expect("a photo is open"); h.app.drag_scale.remember_slow(id, "Exposure"); let (main, small) = drag_exposure(&mut h); @@ -556,7 +592,7 @@ mod in_the_loupe { // the GPU device up front (the loupe never waits for it): with one, this is the case where // the GPU is there and the render still goes to the CPU let _ = lightcraft_engine::gpu::available(); - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); let lut = lightcraft_pipeline::ProofLut::new(2, vec![[0.5, 0.5, 0.5, 0.0]; 8]).expect("a valid table"); h.app.ui.proof.profile = lightcraft_pipeline::register_proof_profile(0x5eed_9f01, lut); h.app.ui.soft_proof = true; @@ -573,7 +609,7 @@ mod in_the_loupe { // Before is not a 2560 px stand-in beside a full resolution After #[test] fn side_by_side_at_one_to_one_compares_like_with_like() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -592,7 +628,7 @@ mod in_the_loupe { // beside the After's); both windows still arrive #[test] fn side_by_side_at_one_to_one_decodes_the_original_once() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); let id = h.app.session.active().expect("a photo is open"); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.app.ui.zoom = crate::state::Zoom::Percent(100.0); @@ -612,7 +648,7 @@ mod in_the_loupe { // Given the Before view alone (\), only the Before gets a window #[test] fn the_original_view_windows_only_the_before() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::Original; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -624,7 +660,7 @@ mod in_the_loupe { // Given a wipe (Split), both sides are windows, drawn over the same rect #[test] fn a_wipe_windows_both_sides() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::Split; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -635,7 +671,7 @@ mod in_the_loupe { // Given the Before view turned off, its window is freed #[test] fn leaving_before_after_frees_the_before_window() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -651,7 +687,7 @@ mod in_the_loupe { // no stream of renders of every size in between, and the sharp window arrives once it stops #[test] fn a_pinch_holds_the_render_sizes_until_it_stops() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); let main_at_fit = rendered_long_edge(&h); let at = h.app.canvas_rect.unwrap().center(); h.request("ui.move", json!({"x": at.x, "y": at.y}), T); @@ -674,7 +710,7 @@ mod in_the_loupe { // differently, #280), no window is rendered: its coordinates are the frame's #[test] fn no_window_over_a_picture_that_is_not_the_frame() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); assert!(h.app.region_view.is_some()); @@ -699,7 +735,7 @@ mod in_the_loupe { ([2800.0, 1700.0], 400.0, false), ([1400.0, 900.0], 100.0, true), ] { - let (mut h, native) = detail_in(size); + let (mut h, native) = demo_detail_in(size); h.app.ui.soft_proof = proof; h.app.ui.zoom = if percent == 0.0 { crate::state::Zoom::Fit } else { crate::state::Zoom::Percent(percent) }; h.settle(SETTLE); @@ -739,25 +775,188 @@ mod in_the_loupe { } } - // Given a slider drag that outpaces the renders (each tick supersedes the last), the original - // the window was cut from is kept once it is decoded: not decoded again for every tick - #[test] - fn a_drag_keeps_the_decoded_original_even_when_every_result_is_superseded() { - let (mut h, _) = detail(); - assert_eq!(h.app.session.memory_report().full_source.bytes, 0, "fit needed no original"); - h.app.ui.zoom = crate::state::Zoom::Percent(400.0); - h.app.session.begin_interaction("Exposure").unwrap(); - let mut ticks = 0; - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60); - while !h.app.renderer.textures.contains_key(&Slot::Region) && std::time::Instant::now() < deadline { - let v = (ticks % 40) as f64 * 0.02; - h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": v}}), T); + /// Work held by a test offload. [`RegionGate`] keeps `Slot::Region` jobs here; [`Held`] + /// keeps every job. `finished` is what the renderer's next `poll` collects. + #[derive(Default)] + struct OffloadWork { + jobs: Vec, + finished: Vec<(Slot, RenderResult, f64)>, + } + + fn take_job(work: &Rc>) -> RenderJob { + let mut w = work.borrow_mut(); + assert!(!w.jobs.is_empty(), "a render job was handed to the offload"); + w.jobs.remove(0) + } + + /// The next window job the UI asked for, stepping a few frames if the request is still a frame + /// away (deterministic frames, no wall-clock wait). + fn next_region_job(h: &mut Headless, work: &Rc>) -> RenderJob { + for _ in 0..3 { + if !work.borrow().jobs.is_empty() { + break; + } h.step(); - ticks += 1; - std::thread::sleep(std::time::Duration::from_millis(5)); } - assert!(h.app.renderer.textures.contains_key(&Slot::Region), "a window arrived"); - assert!(h.app.session.memory_report().full_source.bytes > 0, "the original stayed in the cache while the drag went on"); + take_job(work) + } + + fn deliver(work: &Rc>, slot: Slot, result: RenderResult) { + work.borrow_mut().finished.push((slot, result, 0.0)); + } + + /// Holds the window jobs and runs every other job synchronously, returning it through + /// `finished`: the UI keeps its real `request_window` / `defer_window` / draft path, but the + /// window's original has not arrived until the test says so. + struct RegionGate(Rc>); + + impl RenderOffload for RegionGate { + fn try_start(&mut self, slot: Slot, job: RenderJob) -> Option { + if slot == Slot::Region { + self.0.borrow_mut().jobs.push(job); + } else { + let result = job.run(); + self.0.borrow_mut().finished.push((slot, result, 0.0)); + } + None + } + + fn finished(&mut self) -> Vec<(Slot, RenderResult, f64)> { + std::mem::take(&mut self.0.borrow_mut().finished) + } + } + + /// Holds every job for the test to run and deliver explicitly (the standalone collector test). + struct Held(Rc>); + + impl RenderOffload for Held { + fn try_start(&mut self, _slot: Slot, job: RenderJob) -> Option { + self.0.borrow_mut().jobs.push(job); + None + } + + fn finished(&mut self) -> Vec<(Slot, RenderResult, f64)> { + std::mem::take(&mut self.0.borrow_mut().finished) + } + } + + // Given a 400 % view of a real 24 MP photo, when the first window is decoded and accepted and + // later drag ticks supersede each other, then the original stays in the cache and every later + // window is cut from it. The first accepted result is the actual Detail wiring's job: the + // window `request_window` asked for (respecting `defer_window`), drafted while the drag runs. + #[test] + fn a_drag_keeps_the_decoded_original_after_the_window_is_accepted() { + let (_fx, mut h, native) = detail(); + let id = h.app.session.active().expect("a photo"); + assert_eq!(native, WIDTH as usize); + assert_eq!(h.app.session.memory_report().full_source.bytes, 0, "fit needed no original"); + h.app.session.media.set_on_screen(Some(id)); + let work = Rc::new(RefCell::new(OffloadWork::default())); + h.app.renderer.set_offload(Box::new(RegionGate(work.clone()))); + let discarded = h.app.renderer.discarded; + h.app.ui.zoom = crate::state::Zoom::Percent(400.0); + h.app.session.begin_interaction("Exposure").expect("a drag"); + + let first = next_region_job(&mut h, &work); + let (first_key, first_request) = (first.key, first.request_id); + let view = h.app.region_view.expect("Detail asked for a window"); + assert_eq!(view.photo, id); + assert_eq!(view.full, (WIDTH as usize, HEIGHT as usize), "at 400 % the window's frame is the photo's own pixels"); + assert!(view.window.w >= 1300 && view.window.h >= 700, "the window covers the canvas: {:?}", view.window); + assert!(view.window.x + view.window.w <= view.full.0 && view.window.y + view.window.h <= view.full.1, "inside the frame: {:?}", view.window); + assert_eq!(first.request.window, Some(view.window), "the job is the window on screen"); + assert_eq!((first.request.max_w, first.request.max_h), view.full, "the job renders the frame the view uses"); + assert_eq!(first.request.quality, lightcraft_pipeline::Quality::Draft, "a drag drafts the window"); + assert_eq!(first.level, SourceLevel::Full); + assert!(matches!(&first.source, SourceRef::File { .. }), "the first window decodes the real file"); + assert_ne!(first_request, 0, "the renderer gave the request its own identity"); + + // An edit changes the desired window key while the first original is still held. + // Detail must defer a second decode until this first result has populated the cache. + let reply = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T); + assert_eq!(reply["ok"], true); + h.step(); + assert!(work.borrow().jobs.is_empty(), "a changed window waits for the first original"); + assert_eq!(h.app.renderer.wanted(Slot::Region), Some(first_key)); + assert_eq!(h.app.session.memory_report().full_source.bytes, 0); + + deliver(&work, Slot::Region, first.run()); + h.step(); + assert!(h.app.region_view.is_some()); + assert_eq!(h.app.renderer.textures.get(&Slot::Region).map(|t| t.key), Some(first_key), "the first window arrived"); + assert!(h.app.session.memory_report().full_source.bytes >= WIDTH as usize * HEIGHT as usize * 3 * 4); + assert!(h.app.session.media.has_source(id, SourceLevel::Full), "the original was accepted before any supersession"); + + let second = next_region_job(&mut h, &work); + let (second_key, second_request) = (second.key, second.request_id); + assert_ne!(second_key, first_key); + assert_ne!(second_request, first_request); + assert_eq!(h.app.renderer.wanted(Slot::Region), Some(second_key), "the newer window is pending"); + assert!(matches!(&second.source, SourceRef::Loaded(_)), "the newer window reuses the accepted original"); + + let reply = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 1.0}}), T); + assert_eq!(reply["ok"], true); + let third = next_region_job(&mut h, &work); + assert_ne!(third.key, second_key); + assert_eq!(h.app.renderer.wanted(Slot::Region), Some(third.key)); + + deliver(&work, Slot::Region, second.run()); + h.step(); + assert_eq!(h.app.renderer.textures.get(&Slot::Region).map(|t| t.key), Some(second_key), "the superseded draft still reached the screen"); + assert_eq!(h.app.renderer.wanted(Slot::Region), Some(third.key), "the newest request is still pending"); + assert_eq!(h.app.renderer.discarded, discarded, "the superseded draft was accepted, not discarded"); + assert!(h.app.session.memory_report().full_source.bytes >= WIDTH as usize * HEIGHT as usize * 3 * 4); + assert!(matches!(&third.source, SourceRef::Loaded(_)), "the pending window still reuses the original"); + } + + // The collector's accept branch on its own: a superseded window result still carries the + // original it decoded, and `Renderer::poll` must insert it (`session.accept(&r)` at + // render.rs:911) although a newer request for the same slot is pending. The Detail wiring + // above can only reach this once the first original is held; this standalone test proves the + // cache reuse without depending on UI timing. + #[test] + fn a_superseded_window_result_keeps_the_original_it_decoded() { + let fixture = RenderFixture::new("preview-limit-superseded", 1); + let mut session = fixture.session(); + let id = session.active().expect("a photo"); + session.media.set_on_screen(Some(id)); + let (full_w, full_h) = (WIDTH as usize, HEIGHT as usize); + let window = lightcraft_pipeline::PixelWindow { x: 1000, y: 800, w: 1300, h: 700 }; + let next_window = lightcraft_pipeline::PixelWindow { x: 1200, y: 800, w: 1300, h: 700 }; + let mut renderer = Renderer::with_worker_threads(1); + let work = Rc::new(RefCell::new(OffloadWork::default())); + renderer.set_offload(Box::new(Held(work.clone()))); + let ctx = egui::Context::default(); + + let first = session.region_job(id, full_w, full_h, window, true).expect("the first window"); + renderer.request(Slot::Region, first, 100); + let first = take_job(&work); + let old_key = first.key; + let old_request = first.request_id; + + let second = session.region_job(id, full_w, full_h, next_window, true).expect("the newer window"); + renderer.request(Slot::Region, second, 100); + let second = take_job(&work); + let new_key = second.key; + assert_ne!(new_key, old_key, "the newer window has its own identity"); + assert_eq!(renderer.wanted(Slot::Region), Some(new_key)); + + let old = first.run(); + assert!(old.rendered.is_ok(), "{:?}", old.rendered.as_ref().err()); + assert_eq!(old.request_id, old_request); + assert_eq!(old.level, SourceLevel::Full); + let loaded = old.loaded.as_ref().expect("the first window decoded the original"); + assert_eq!((loaded.image.width, loaded.image.height), (WIDTH as usize, HEIGHT as usize)); + deliver(&work, Slot::Region, old); + renderer.poll(&ctx, &mut session); + + assert_eq!(renderer.textures.get(&Slot::Region).map(|t| t.key), Some(old_key)); + assert_eq!(renderer.wanted(Slot::Region), Some(new_key)); + assert_eq!(renderer.discarded, 0, "the superseded result was accepted, not discarded"); + assert!(session.memory_report().full_source.bytes >= full_w * full_h * 3 * 4); + assert!(session.media.has_source(id, SourceLevel::Full)); + let reused = session.region_job(id, full_w, full_h, window, true).expect("another window"); + assert!(matches!(&reused.source, SourceRef::Loaded(_)), "the accepted original is reused"); } // Given an original bigger than the cache budget (a 64 MP raw's 797 MB exceeds the default @@ -765,7 +964,7 @@ mod in_the_loupe { // grid it is let go #[test] fn a_zoomed_original_over_the_budget_stays_until_the_view_changes() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.session.media.set_budget(16 << 20); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -794,7 +993,7 @@ mod in_the_loupe { #[test] fn a_wipe_paints_the_before_window_over_its_stand_in() { for mode in [crate::state::BeforeAfter::Split, crate::state::BeforeAfter::SplitTopBottom] { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = mode; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -821,7 +1020,7 @@ mod in_the_loupe { // everywhere (not only left of the line) #[test] fn holding_backslash_in_a_wipe_paints_the_before_window_across_the_canvas() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::Split; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -848,7 +1047,7 @@ mod in_the_loupe { // margin (a window is wider than what its pane shows) does not spill into the neighbour #[test] fn side_by_side_windows_stay_in_their_panes() { - let (mut h, _) = detail_in([1800.0, 900.0]); + let (_fx, mut h, _) = detail_in([1800.0, 900.0]); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -864,7 +1063,7 @@ mod in_the_loupe { // apart: the job key is the same, and one tile must not replace the other #[test] fn identical_before_and_after_keep_a_tile_each() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.before_after = crate::state::BeforeAfter::SideBySide; h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -875,7 +1074,7 @@ mod in_the_loupe { // to the whole frame at the drawn size, and tries windows again when the look changes #[test] fn a_refused_window_falls_back_to_a_full_size_render() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); assert!(h.app.region_view.is_some()); @@ -894,7 +1093,7 @@ mod in_the_loupe { // pinch stops: a new, ever wider window every frame is not asked for #[test] fn pinching_out_asks_for_no_new_windows() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.zoom = crate::state::Zoom::Percent(400.0); h.settle(SETTLE); let key = h.app.region_view.expect("a window").key; @@ -910,7 +1109,7 @@ mod in_the_loupe { // slider drag still reuses them: they are not cleared every frame #[test] fn a_drag_over_the_stage_budget_does_not_clear_the_open_views() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.ui.zoom = crate::state::Zoom::Percent(100.0); h.settle(SETTLE); let held = |h: &Headless| { @@ -947,7 +1146,7 @@ mod in_the_loupe { // whole-frame render, magnified and blurred, was shown) #[test] fn a_display_profile_keeps_the_sharp_window_at_one_to_one() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); use_display_profile(&mut h); h.request("engine.execute", json!({"command": "view.zoom100"}), T); h.settle(SETTLE); @@ -963,7 +1162,7 @@ mod in_the_loupe { // …and in Compare, each pane's window too #[test] fn a_display_profile_keeps_the_compare_windows_at_one_to_one() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); use_display_profile(&mut h); let r = h.request("engine.execute", json!({"command": "view.compare"}), T); assert_eq!(r["ok"], true, "{r}"); @@ -982,7 +1181,7 @@ mod in_the_loupe { // a display #[test] fn with_the_screen_profile_the_window_is_drawn() { - let (mut h, native) = detail(); + let (_fx, mut h, native) = detail(); h.app.services.screen_profile = Some(Box::new(|| Some(lightcraft_codecs::icc::write_named(lightcraft_codecs::NamedSpace::DisplayP3)))); h.step(); assert!(h.app.ui.settings.display_profile.is_empty(), "no profile chosen"); @@ -1000,20 +1199,20 @@ mod in_the_loupe { /// RAM or more: room for both panes' 24 MP originals (288 MB each as linear float). const COMPARE_CACHE: usize = 768 << 20; - /// Compare view of the demo library at `zoom`, settled. + /// Compare view of the render fixture at `zoom`, settled. /// /// The session's cache budget is set here rather than taken from the process-wide memory /// budget, which other tests change while they run (Settings ▸ Performance ▸ 1 GB leaves a /// 512 MiB share, too small for two originals, so one pane's was evicted). - fn compare_at(zoom: crate::state::Zoom) -> Headless { - let (mut h, _) = detail(); + fn compare_at(zoom: crate::state::Zoom) -> (RenderFixture, Headless) { + let (fixture, mut h, _) = detail(); h.app.session.media.set_budget(COMPARE_CACHE); let r = h.request("engine.execute", json!({"command": "view.compare"}), T); assert_eq!(r["ok"], true, "{r}"); assert_eq!(h.app.ui.view, crate::state::ViewMode::Compare); h.app.ui.zoom = zoom; h.settle(SETTLE); - h + (fixture, h) } fn native_of(h: &Headless, id: lightcraft_catalog::PhotoId) -> usize { @@ -1025,7 +1224,7 @@ mod in_the_loupe { // cut from its own photo's pixels, not the 2560 px whole-frame render magnified #[test] fn compare_at_one_to_one_gets_a_sharp_window_per_pane() { - let h = compare_at(crate::state::Zoom::Percent(100.0)); + let (_fx, h) = compare_at(crate::state::Zoom::Percent(100.0)); let (sel, cand) = h.app.ui.compare.map(|(a, b)| (lightcraft_catalog::PhotoId(a), lightcraft_catalog::PhotoId(b))).expect("a pair"); for (i, id) in [(0u8, sel), (1u8, cand)] { let whole = h.app.renderer.textures.get(&Slot::Compare(i)).expect("the whole-frame render"); @@ -1043,7 +1242,7 @@ mod in_the_loupe { // Given Compare at fit, nothing more is rendered than before windows #[test] fn compare_at_fit_makes_no_window() { - let h = compare_at(crate::state::Zoom::Fit); + let (_fx, h) = compare_at(crate::state::Zoom::Fit); assert_eq!(h.app.compare_windows, [None, None]); assert!(!h.app.renderer.textures.contains_key(&Slot::CompareRegion(0)) && !h.app.renderer.textures.contains_key(&Slot::CompareRegion(1))); } @@ -1052,7 +1251,7 @@ mod in_the_loupe { // are freed, and back in Detail only one original is kept decoded #[test] fn leaving_a_zoomed_compare_frees_its_windows() { - let mut h = compare_at(crate::state::Zoom::Percent(100.0)); + let (_fx, mut h) = compare_at(crate::state::Zoom::Percent(100.0)); assert!(h.app.renderer.textures.contains_key(&Slot::CompareRegion(1))); h.app.ui.zoom = crate::state::Zoom::Fit; h.settle(SETTLE); @@ -1072,7 +1271,7 @@ mod in_the_loupe { // at the same place of the frame, and it is painted over that render, inside the pane #[test] fn a_compare_window_lines_up_with_its_pane() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); h.app.renderer.keep_pixels = true; h.request("engine.execute", json!({"command": "view.compare"}), T); h.app.ui.zoom = crate::state::Zoom::Percent(100.0); @@ -1082,19 +1281,24 @@ mod in_the_loupe { let view = h.app.compare_windows[i as usize].expect("a window"); let tile = h.app.renderer.textures.get(&Slot::CompareRegion(i)).and_then(|t| t.pixels.clone()).expect("tile pixels"); let main = h.app.renderer.textures.get(&Slot::Compare(i)).and_then(|t| t.pixels.clone()).expect("pane pixels"); - let (mut sum, mut n) = (0.0f32, 0.0f32); - for j in (8..tile.size[1] - 8).step_by(37) { - for x in (8..tile.size[0] - 8).step_by(37) { - let u = (view.window.x + x) as f32 / view.full.0 as f32; - let v = (view.window.y + j) as f32 / view.full.1 as f32; - let (mx, my) = - (((u * main.size[0] as f32) as usize).min(main.size[0] - 1), ((v * main.size[1] as f32) as usize).min(main.size[1] - 1)); - let (a, b) = (tile.pixels[j * tile.size[0] + x], main.pixels[my * main.size[0] + mx]); - sum += (0..3).map(|k| (a.to_array()[k] as f32 - b.to_array()[k] as f32).abs()).sum::() / 3.0; - n += 1.0; + let mean_difference = |offset: usize| { + let (mut sum, mut n) = (0.0f32, 0.0f32); + for j in (8..tile.size[1] - 8).step_by(37) { + for x in (8..tile.size[0] - 8).step_by(37) { + let u = (view.window.x + x + offset) as f32 / view.full.0 as f32; + let v = (view.window.y + j) as f32 / view.full.1 as f32; + let (mx, my) = + (((u * main.size[0] as f32) as usize).min(main.size[0] - 1), ((v * main.size[1] as f32) as usize).min(main.size[1] - 1)); + let (a, b) = (tile.pixels[j * tile.size[0] + x], main.pixels[my * main.size[0] + mx]); + sum += (0..3).map(|k| (a.to_array()[k] as f32 - b.to_array()[k] as f32).abs()).sum::() / 3.0; + n += 1.0; + } } - } - assert!(sum / n < 6.0, "pane {i}: mean difference {:.2} / 255 between the window and the pane's render", sum / n); + sum / n + }; + let difference = mean_difference(0); + assert!(difference < 6.0, "pane {i}: mean difference {:.2} / 255 between the window and the pane's render", difference); + assert!(mean_difference(16) > 6.0, "the fixture must expose a 16-pixel horizontal misalignment"); let (whole, window) = (drawn(&h, Slot::Compare(i)), drawn(&h, Slot::CompareRegion(i))); assert!(!whole.is_empty() && !window.is_empty(), "pane {i}: both are painted"); assert!(window[0].0 > whole[0].0, "pane {i}: the window is painted over the whole-frame render"); @@ -1109,7 +1313,7 @@ mod in_the_loupe { // drag on the active photo asks for no new window until it ends #[test] fn reference_view_at_one_to_one_gets_windows_and_a_drag_keeps_them() { - let (mut h, _) = detail(); + let (_fx, mut h, _) = detail(); let r = h.request("engine.execute", json!({"command": "photo.setReference"}), T); assert_eq!(r["ok"], true, "{r}"); let r = h.request("ui.key", json!({"key": "r", "shift": true}), T); diff --git a/crates/ui-egui/src/tests/print.rs b/crates/ui-egui/src/tests/print.rs index 25f7014ee1..68f3d328d0 100644 --- a/crates/ui-egui/src/tests/print.rs +++ b/crates/ui-egui/src/tests/print.rs @@ -8,6 +8,7 @@ use serde_json::{Value, json}; use crate::headless::Headless; use crate::state::Dialog; +use crate::tests_fixtures::SyntheticLibrary; use crate::{LightcraftApp, Services}; const T: Duration = Duration::from_secs(30); @@ -402,8 +403,14 @@ fn print_job_colour_controls_reach_the_job_and_the_pdf() { #[test] fn the_print_preview_does_not_rebuild_thumbnails_every_frame() { - let mut h = demo(Services { png: None, ..Default::default() }); + // This counts thumbnail requests across print-cell sizes, not native-image throughput. + // Keep the demo's twelve-photo contact sheet with real files that decode cheaply. + let library = SyntheticLibrary::with_photos("print-thumbnail-reuse", 12); + let mut h = Headless::new(library.app(), [1400.0, 900.0], 1.0); + assert!(h.settle(T), "the print thumbnail fixture settles"); + assert_eq!(h.app.session.catalog.len(), 12, "the contact sheet retains twelve photos"); let before = h.app.renderer.thumb_jobs_built; + assert!(before > 0, "the fixture exercises actual thumbnail jobs"); cmd(&mut h, "dialog.print", json!({})); for style in ["contactSheet", "picturePackage", "singleImage"] { cmd(&mut h, "print.set", json!({"layout": {"style": style}})); diff --git a/crates/ui-egui/src/tests/render_fixtures.rs b/crates/ui-egui/src/tests/render_fixtures.rs new file mode 100644 index 0000000000..b79bbad366 --- /dev/null +++ b/crates/ui-egui/src/tests/render_fixtures.rs @@ -0,0 +1,179 @@ +//! Test-only, real, file-backed photo fixtures for the render tests. +//! +//! The input/modal tests use a tiny `SyntheticLibrary` (64×48) that cannot exercise native-size +//! decoding, a 1300×700 region window, a >16 MiB decoded original or Compare's two originals. +//! These fixtures write real landscape 3:2 PNGs at the demo library's 24 MP size and register +//! them as ordinary `Source::File` photos, so preview, Before/After and slideshow tests decode +//! and render through the same path as a user's files. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +use lightcraft_catalog::{Op, Photo, Source}; +use lightcraft_codecs::{EncodeImage, EncodeMeta, Samples, encode_png}; +use lightcraft_engine::{Selection, Session}; + +/// The demo library's photo size: landscape 3:2, 24 MP. +pub(crate) const WIDTH: u32 = 6000; +pub(crate) const HEIGHT: u32 = 4000; +/// The long edge the native-resolution tests look for. +pub(crate) const LONG_EDGE: usize = WIDTH as usize; +/// The pixel count of every fixture photo. +pub(crate) const MEGAPIXELS: u64 = WIDTH as u64 * HEIGHT as u64; + +static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0); +type FixtureCache = Mutex>>; +/// One generated set per (tag, count) in this test process: parallel tests share the same +/// read-only PNGs instead of each encoding 2–3 24 MP images (and spiking memory). +static FIXTURES: OnceLock = OnceLock::new(); + +struct FixtureFiles { + dir: PathBuf, + paths: Vec, + sizes: Vec, +} + +impl Drop for FixtureFiles { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } +} + +/// Owns (with other users) a temporary directory of real PNGs. Keep it alive until after the +/// [`crate::headless::Headless`] built from [`RenderFixture::session`] has dropped: the renderer +/// reads these paths lazily. +pub(crate) struct RenderFixture { + files: Arc, +} + +impl RenderFixture { + /// Write (or reuse) `count` deterministic, smooth, non-neutral landscape gradient PNGs. + pub(crate) fn new(tag: &str, count: usize) -> Self { + assert!(count > 0, "a render fixture needs at least one photo"); + let key = (tag.to_string(), count); + let cache = FIXTURES.get_or_init(|| Mutex::new(HashMap::new())); + let mut cache = cache.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(files) = cache.get(&key).and_then(Weak::upgrade) { + return RenderFixture { files }; + } + let files = Arc::new(build_files(tag, count)); + cache.insert(key, Arc::downgrade(&files)); + RenderFixture { files } + } + + /// A session with `with_fs` loaders, one default-edited `Source::File` photo per fixture, + /// distinct ids and captured times in a stable order, with the first visible photo selected. + pub(crate) fn session(&self) -> Session { + let mut session = Session::new().with_fs(); + for (i, path) in self.files.paths.iter().enumerate() { + let id = session.catalog.alloc_photo_id(); + let name = format!("RENDER_{i:04}.PNG"); + let mut photo = Photo::new(id, Source::File { path: path.clone() }, &name, "PNG", WIDTH, HEIGHT, "2026-01-01T10:00:00"); + // Distinct captured times keep `visible_cloned()` deterministic (the default sort is + // capture date, newest first) and make every PhotoId its own file. + photo.captured = Some(format!("2026-01-01T10:{:02}:{:02}", i / 60, i % 60)); + photo.file_size = self.files.sizes.get(i).copied().unwrap_or(0); + session.catalog.apply(Op::AddPhoto { photo: Box::new(photo) }).expect("add a render fixture photo"); + } + session.keep_active_in_view(); + let first = session.visible_cloned().first().copied().expect("the fixture has a visible photo"); + session.selection = Selection::single(first); + session + } + + pub(crate) fn paths(&self) -> &[String] { + &self.files.paths + } +} + +fn build_files(tag: &str, count: usize) -> FixtureFiles { + let n = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!("lightcraft-render-fixtures-{tag}-{}-{n}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create the render fixture directory"); + + let mut paths = Vec::with_capacity(count); + let mut sizes = Vec::with_capacity(count); + for i in 0..count { + let path = dir.join(format!("render-{i:02}.png")); + let size = write_gradient(&path, i); + paths.push(path.to_string_lossy().into_owned()); + sizes.push(size); + } + FixtureFiles { dir, paths, sizes } +} + +/// A smooth, non-wrapping, non-neutral gradient: R rises with x, G with y, B falls with x. The +/// superimposed spatial waves make shifted pixels distinguishable in alignment regressions. The +/// centre stays non-neutral, so the white-balance picker sees a real colour and an +/// exposure lift of 1.5 EV is unmistakably brighter. `variant` shifts it slightly so no two +/// fixture files have the same bytes. +fn write_gradient(path: &Path, variant: usize) -> u64 { + let w = WIDTH as usize; + let h = HEIGHT as usize; + let shift_r = (variant % 7) as f32 * 4.0; + let shift_b = (variant % 5) as f32 * 3.0; + let r_base: Vec = (0..w) + .map(|x| { + let u = x as f32 / (w - 1) as f32; + clamp8(80.0 + 120.0 * u + shift_r + 44.0 * (std::f32::consts::TAU * x as f32 / 160.0).sin()) + }) + .collect(); + let b_base: Vec = (0..w) + .map(|x| { + let u = x as f32 / (w - 1) as f32; + clamp8(40.0 + 100.0 * (1.0 - u) + shift_b + 22.0 * (std::f32::consts::TAU * x as f32 / 160.0).sin()) + }) + .collect(); + + let mut bytes = vec![0u8; w * h * 4]; + for (y, row) in bytes.chunks_exact_mut(w * 4).enumerate() { + let v = y as f32 / (h - 1) as f32; + let wave = (std::f32::consts::TAU * y as f32 / 192.0).sin(); + let g = clamp8(60.0 + 100.0 * v + 44.0 * wave); + for (x, pixel) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() { + pixel[0] = r_base[x]; + pixel[1] = g; + pixel[2] = clamp8(f32::from(b_base[x]) + 22.0 * wave); + pixel[3] = 255; + } + } + + let image = EncodeImage::new(WIDTH, HEIGHT, 4, Samples::U8(&bytes)); + let encoded = encode_png(&image, &EncodeMeta::default()).expect("encode a render fixture PNG"); + std::fs::write(path, &encoded).expect("write a render fixture PNG"); + encoded.len() as u64 +} + +fn clamp8(v: f32) -> u8 { + v.clamp(0.0, 255.0).round() as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The fixture must stay a real 24 MP landscape 3:2 PNG: a fast little image would silently + /// weaken every render assertion that depends on native pixels and a >16 MiB decoded source. + #[test] + fn render_fixtures_are_real_24mp_landscape_pngs() { + assert_eq!((WIDTH, HEIGHT), (6000, 4000)); + assert_eq!(MEGAPIXELS, 24_000_000, "the demo photos these replace are 24 MP"); + let fixture = RenderFixture::new("preview", 2); + assert_eq!(fixture.paths().len(), 2); + for path in fixture.paths() { + let bytes = std::fs::read(path).expect("read the fixture PNG"); + let header = lightcraft_codecs::read_header(&bytes).expect("read the fixture PNG header"); + assert_eq!((header.width, header.height), (WIDTH, HEIGHT), "{path}"); + assert_eq!(header.format, lightcraft_codecs::Format::Png, "{path}"); + // A full-resolution decode is width × height linear RGB f32 samples, well over the + // 16 MiB budget `a_zoomed_original_over_the_budget...` checks. + let decoded_bytes = u64::from(header.width) * u64::from(header.height) * 3 * 4; + assert!(decoded_bytes > (16 << 20), "{path}: a decoded source of {decoded_bytes} bytes"); + } + // The files are distinct so each PhotoId is its own original. + assert_ne!(fixture.paths()[0], fixture.paths()[1]); + } +} diff --git a/crates/ui-egui/src/tests/slideshow.rs b/crates/ui-egui/src/tests/slideshow.rs index 7dc8ae4005..01a468ed89 100644 --- a/crates/ui-egui/src/tests/slideshow.rs +++ b/crates/ui-egui/src/tests/slideshow.rs @@ -2,14 +2,18 @@ //! prepared ahead off the UI thread, transitions only between ready slides, keys and controls. //! Simulated time: every headless frame is 1/60 s, so nothing here sleeps. +use std::cell::Cell; +use std::rc::Rc; use std::time::Duration; use lightcraft_catalog::PhotoId; use lightcraft_engine::slideshow::Item; use serde_json::json; -use crate::headless::Headless; +use crate::headless::{Headless, RenderGate}; use crate::render::Slot; +use crate::tests_fixtures::SyntheticLibrary; +use crate::tests_render_fixtures::{HEIGHT, RenderFixture, WIDTH}; use crate::{LightcraftApp, Services}; const T: Duration = Duration::from_secs(20); @@ -27,6 +31,31 @@ fn grid_sized(size: [f32; 2]) -> Headless { h } +/// The slideshow's 1000×700 window over `count` real 6000×4000 file-backed photos. Used only by +/// `next_slides_are_prepared_before_they_are_due`, where the 24 MP decode/render is the point: +/// destructure the tuple immediately with the fixture binding first, so the renderer drops first. +fn fixture_grid(count: usize, open: Rc>) -> (RenderFixture, Headless) { + let fixture = RenderFixture::new("slideshow", count); + assert_eq!(fixture.paths().len(), count); + for (index, path) in fixture.paths().iter().enumerate() { + assert!(std::path::Path::new(path).is_file(), "the fixture photo exists"); + assert!(!fixture.paths()[..index].contains(path), "each fixture photo has its own path"); + } + let services = Services { png: None, ..Default::default() }; + let app = LightcraftApp::new(fixture.session(), services); + let mut h = Headless::new(app, [1000.0, 700.0], 1.0); + // Control native grid work before its first frame too: asynchronous setup must not + // consume the wall-clock budget before the slideshow scheduling assertions begin. + assert!(open.get(), "the native grid setup gate starts open"); + h.app.renderer.set_offload(Box::new(RenderGate::new(open))); + assert!(h.settle(SETTLE), "the grid renders settle before controlling slide renders"); + assert_eq!(h.app.session.catalog.len(), count, "the slideshow fixture photos"); + let id = h.app.session.active().expect("a photo is open"); + let photo = h.app.session.catalog.photo(id).expect("it is in the catalog"); + assert_eq!((photo.width, photo.height), (WIDTH, HEIGHT), "the slideshow fixture is a real 24 MP photo"); + (fixture, h) +} + fn run(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value { let r = h.request("engine.execute", json!({"command": command, "params": params}), T); assert_eq!(r["ok"], true, "{command}: {r}"); @@ -61,12 +90,24 @@ fn slides_held(h: &Headless) -> usize { #[test] fn slideshow_advances_pauses_and_ends() { - let mut h = grid(); + // This tests the automatic playback clock and controls; native 24 MP rendering + // is exercised separately by next_slides_are_prepared_before_they_are_due. + let library = SyntheticLibrary::with_photos("slideshow-automatic-controls", 3); + let mut h = Headless::new(library.app(), [1000.0, 700.0], 1.0); + assert!(h.settle(SETTLE), "the automatic playback fixture settles"); let first = h.app.session.visible_cloned()[0]; - run(&mut h, "view.slideshow", json!({"interval": 0.5, "fadeDuration": 0})); + // The fixture selects its first imported photo; newest-first view order differs. + // Establish the same first-in-view starting selection as the original demo test. + run(&mut h, "library.select", json!({"ids": [first.0]})); + assert_eq!(h.app.session.active(), Some(first), "the first visible photo is selected before playback"); + run(&mut h, "view.slideshow", json!({"interval": 0.5, "fadeDuration": 0, "manual": false})); + assert!(!play(&h).settings().manual, "playback advances automatically"); assert!(h.app.ui.fullscreen && h.app.ui.slideshow.is_some()); assert_eq!(h.app.session.active(), Some(first), "starts at the first photo in view"); assert!(h.step_until(SETTLE, |h| h.app.session.active() != Some(first)), "the next photo comes up and is selected"); + let advanced = photo_up(&h); + assert_eq!(h.app.session.active(), Some(advanced), "selection follows automatic playback"); + assert!(has_slide(&h, advanced), "automatic playback shows a rendered next photo"); // Space pauses: nothing moves let r = h.request("ui.key", json!({"key": "space"}), T); assert_eq!(r["ok"], true, "{r}"); @@ -98,33 +139,54 @@ fn turning_off_the_full_screen_preview_frees_the_slides() { #[test] fn next_slides_are_prepared_before_they_are_due() { - let mut h = grid(); - // a long slide: anything rendered for the next ones is rendered ahead of time - run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 1, "preparePreviews": true, "seed": 7})); + let open = Rc::new(Cell::new(true)); + let (fx, mut h) = fixture_grid(8, open.clone()); + // Grid setup is complete. Hold photo slide jobs until their ahead requests are + // observable; opening the gate runs the actual native decode/render synchronously. + open.set(false); + let reply = run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 1, "preparePreviews": true, "seed": 7})); + assert_eq!(reply["settings"]["preparePreviews"], true); + assert_eq!(reply["settings"]["slideDuration"], 60.0); let ahead = play(&h).upcoming(crate::panels::slideshow::AHEAD); - assert_eq!(ahead.len(), 2); + assert_eq!(ahead.len(), crate::panels::slideshow::AHEAD); let start = h.time(); - // the slide up and the next two; their renders can finish in any order + // The current photo and next two are queued before any slide render completes. let current = photo_up(&h); + for item in std::iter::once(Item::Photo(current)).chain(ahead.iter().copied()) { + assert!(h.app.renderer.is_pending(Slot::Slide(item)), "requested before it is due: {item:?}"); + assert!(!has_slide(&h, item), "not rendered before the gate opens: {item:?}"); + } + assert!(play(&h).remaining(h.time()) > 59.0, "the first slide is not due before rendering"); + // The gate executes the actual decode/render work synchronously when polled. + open.set(true); assert!( h.step_until(SETTLE, |h| has_slide(h, current) && ahead.iter().all(|id| has_slide(h, *id))), "the slide up and the next two slides are rendered" ); assert!(h.time() - start < 60.0, "before the first is due"); assert_eq!(photo_up(&h), current, "still on the first slide"); - h.settle(SETTLE); + assert!(play(&h).remaining(h.time()) > 59.0, "renders are accepted before the first slide is due"); + assert!(h.settle(SETTLE), "enabled-prefetch renders settle"); + drop(h); + drop(fx); // off: only the slide up is rendered until the next one is due - let mut h = grid(); - run(&mut h, "view.slideshow", json!({"interval": 60, "preparePreviews": false})); + let open = Rc::new(Cell::new(true)); + let (_fx2, mut h) = fixture_grid(8, open.clone()); + open.set(false); + let reply = run(&mut h, "view.slideshow", json!({"interval": 60, "preparePreviews": false})); + assert_eq!(reply["settings"]["preparePreviews"], false); let next = play(&h).upcoming(1)[0]; let current = photo_up(&h); - assert!(h.step_until(SETTLE, |h| has_slide(h, current))); for _ in 0..30 { h.step(); } + assert!(h.app.renderer.is_pending(Slot::Slide(Item::Photo(current))), "the current slide is requested"); assert!(!has_slide(&h, next) && !h.app.renderer.is_pending(Slot::Slide(next)), "not prepared"); - h.settle(SETTLE); + open.set(true); + assert!(h.step_until(SETTLE, |h| has_slide(h, current)), "the current slide renders once allowed"); + assert!(!has_slide(&h, next) && !h.app.renderer.is_pending(Slot::Slide(next)), "still not prepared"); + assert!(h.settle(SETTLE), "disabled-prefetch renders settle"); } #[test] @@ -201,10 +263,40 @@ fn a_manual_slideshow_waits_for_the_arrow_keys() { // the Settings checkbox turns it on run(&mut h, "view.slideshowOptions", json!({})); h.step(); - reveal(&mut h, "check:settings.slideshowManual"); + let target = reveal(&mut h, "check:settings.slideshowManual"); + let diagnose = |phase: &str, h: &Headless| { + let id = "check:settings.slideshowManual"; + let rect = latest_widget_rect(h, id); + let hit = crate::control::widget_hit(&h.app, id); + let visible = rect.is_some_and(|r| crate::control::widget_target_visible(&h.view.ctx, hit, r.center())); + eprintln!( + "manual-checkbox {phase}: manual={} time={} frames={} target={target:?} rect={rect:?} room={:?} hit={hit:?} visible={visible} input={:?} dialog={:?} notices={:?} quit_prompt={} notice_barrier={} library_problem={} library_barrier={} in_flight={} pending={:?} toast={:?}", + h.app.session.slideshow.manual, + h.time(), + h.frames(), + latest_widget_rect(h, "settings:pageRoom"), + h.view.ctx.input(|i| (i.is_scrolling(), i.modifiers, i.keys_down.clone(), i.pointer.any_down())), + h.app.ui.dialog, + h.app.notices, + h.app.quit_prompt.is_some(), + crate::panels::notices::blocks_input(&h.app), + h.app.library_problem.is_some(), + crate::panels::library_problem::blocks_file_drops(&h.app), + h.app.renderer.in_flight(), + h.app.renderer.pending_slots(), + h.app.ui.toast, + ); + }; + diagnose("before-click", &h); + assert!(!h.app.session.slideshow.manual, "the checkbox starts unchecked before the real click"); + let journal_before = h.app.session.journal.len(); let r = h.request("ui.clickWidget", json!({"id": "check:settings.slideshowManual"}), T); + diagnose("after-request", &h); + eprintln!("manual-checkbox reply={r} journal_since_click={:?}", h.app.session.journal.get(journal_before..)); assert_eq!(r["ok"], true, "{r}"); h.step(); + diagnose("after-existing-step", &h); + eprintln!("manual-checkbox journal_after_existing_step={:?}", h.app.session.journal.get(journal_before..)); assert!(h.app.session.slideshow.manual, "the checkbox goes through slideshow.playback"); h.settle(SETTLE); } @@ -440,7 +532,9 @@ fn a_composed_slide_shows_during_playback() { let mut h = grid(); let r = run(&mut h, "slideshow.layout", json!({"margins": 0.1, "backdropColor": "#ff0000"})); assert_eq!(r["marginTop"], 0.1); - run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 0})); + // This checks composed pixels and explicit navigation. Rendering can take enough + // headless frames to pass any automatic interval before the next slide is ready. + run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 0, "manual": true})); let current = photo_up(&h); assert!(h.step_until(SETTLE, |h| has_slide(h, current)), "the slide is composed and ready"); h.step(); @@ -457,8 +551,10 @@ fn a_composed_slide_shows_during_playback() { // the next slide is composed too, and stepping shows it let next = play(&h).upcoming(1)[0]; assert!(h.step_until(SETTLE, |h| has_slide(h, next))); + assert_eq!(photo_up(&h), current, "rendering ahead keeps the inspected slide up"); run(&mut h, "slideshow.next", json!({})); h.step(); + assert_eq!(play(&h).current(), next, "navigation shows the prepared slide"); assert_eq!(painted(&h.paint(), 5, 5), [255, 0, 0]); h.settle(SETTLE); } @@ -467,10 +563,13 @@ fn a_composed_slide_shows_during_playback() { fn the_default_layout_plays_the_photo_as_before() { let mut h = grid(); assert!(h.app.session.slide_layout.is_plain()); - run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 0})); + // Inspect one photo's aspect and backdrop without automatic playback advancing + // the simulated clock past it while the asynchronous renderer catches up. + run(&mut h, "view.slideshow", json!({"interval": 60, "fadeDuration": 0, "manual": true})); let current = photo_up(&h); assert!(h.step_until(SETTLE, |h| has_slide(h, current))); h.step(); + assert_eq!(photo_up(&h), current, "the inspected photo stays up while rendering"); // the photo's own render (its aspect, not the screen's), fitted on black, as before let tex = h.app.renderer.textures.get(&Slot::Slide(current.into())).unwrap(); let aspect = h.app.session.slide_photo_aspect(current).unwrap(); diff --git a/crates/ui-egui/src/tests/zoom.rs b/crates/ui-egui/src/tests/zoom.rs index 580b70615a..ebe3ecd22f 100644 --- a/crates/ui-egui/src/tests/zoom.rs +++ b/crates/ui-egui/src/tests/zoom.rs @@ -41,6 +41,73 @@ fn size_of(h: &Headless) -> (f32, f32) { (p.width as f32, p.height as f32) } +/// Keep an old same-photo texture on screen while the replacement is deliberately held. +/// Geometry must follow the edit immediately, independently of renderer throughput. +#[test] +fn a_pending_crop_or_rotation_does_not_reuse_the_old_textures_aspect() { + use crate::render::{RenderOffload, Slot, Tex, geometry_look}; + use crate::state::{RightPanel, Zoom}; + + struct Held; + impl RenderOffload for Held { + fn try_start(&mut self, _: Slot, job: lightcraft_engine::RenderJob) -> Option { + Some(job) + } + fn finished(&mut self) -> Vec<(Slot, lightcraft_engine::media::RenderResult, f64)> { + Vec::new() + } + } + + let fixture = crate::tests_fixtures::SyntheticLibrary::new("zoom-stale-geometry"); + let mut h = Headless::new(fixture.app(), [1400.0, 900.0], 1.0); + h.app.renderer.set_offload(Box::new(Held)); + let response = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T); + assert_eq!(response["ok"], true, "{response}"); + h.step(); + h.app.ui.zoom = Zoom::Percent(100.0); + let id = h.app.session.active().expect("fixture photo"); + let old_look = geometry_look(&h.app.session.develop_of(id).expect("settings")); + let tex = h.view.ctx.load_texture("old loupe", egui::ColorImage::filled([4, 3], egui::Color32::BLACK), Default::default()); + h.app.renderer.textures.insert( + Slot::Main, + Tex { look: Some((old_look, true)), key: 0, photo: id, tex, size: [64, 48], histogram: None, ms: 0.0, quick: None, pixels: None }, + ); + edit(&mut h, |d| d.crop.geometry.rect = lightcraft_geom::Rect::new(0.25, 0.2, 0.75, 0.2 + 1.0 / 3.0)); + for (rotate, expected) in [(false, egui::vec2(32.0, 16.0)), (true, egui::vec2(16.0, 32.0))] { + if rotate { + edit(&mut h, |d| { + d.orientation = lightcraft_geom::Orientation::Rotate90; + // Crop coordinates live in the oriented frame, as in library.rotateRight. + d.crop.geometry.rect = lightcraft_geom::Orientation::Rotate90.map_norm_rect(d.crop.geometry.rect); + }); + } + h.app.ui.zoom_anim = false; + h.step(); + let shown = h.app.image_rect.expect("loupe geometry"); + assert!((shown.size() - expected).abs().max_elem() < 0.01, "pending edit: {shown:?}, expected {expected:?}"); + assert_eq!(h.app.renderer.textures[&Slot::Main].look, Some((old_look, true)), "old pixels remain visible"); + assert!(h.app.renderer.is_pending(Slot::Main), "the edited render is still pending"); + } + // Crop mode shows the full oriented image; leaving it must immediately restore the crop. + h.app.ui.right = RightPanel::Crop; + h.app.ui.zoom_anim = false; + h.step(); + assert!((h.app.image_rect.expect("crop tool geometry").size() - egui::vec2(48.0, 64.0)).abs().max_elem() < 0.01); + h.app.ui.right = RightPanel::None; + h.app.ui.zoom_anim = false; + h.step(); + assert!((h.app.image_rect.expect("restored crop geometry").size() - egui::vec2(16.0, 32.0)).abs().max_elem() < 0.01); + // An actual current-settings render can still correct metadata that differs from the + // decoded image (unsupported RAWs may provide only a differently cropped embedded JPEG). + let current = geometry_look(&h.app.session.develop_of(id).expect("settings")); + let tex = h.app.renderer.textures.get_mut(&Slot::Main).expect("old texture"); + tex.look = Some((current, true)); + tex.size = [16, 48]; + h.step(); + let shown = h.app.image_rect.expect("current render geometry"); + assert!((shown.size() - egui::vec2(16.0, 48.0)).abs().max_elem() < 0.01, "current pixels correct metadata: {shown:?}"); +} + // Given an uncropped photo, 100 % is its pixel size #[test] fn an_uncropped_photo_at_100_is_its_pixel_size() { diff --git a/crates/ui-egui/tests/it/docking_workflows.rs b/crates/ui-egui/tests/it/docking_workflows.rs index e13b0ff0ed..54ad8f186f 100644 --- a/crates/ui-egui/tests/it/docking_workflows.rs +++ b/crates/ui-egui/tests/it/docking_workflows.rs @@ -42,13 +42,22 @@ fn settle_photo(h: &mut Harness<'_, LightcraftApp>) { let started = Instant::now(); let mut quiet = 0; while quiet < 4 { - h.run_steps(2); - if h.state().renderer.in_flight() == 0 && h.state().loupe_shown.is_some_and(|(_, source)| source == "render") { + if h.state().renderer.in_flight_without_prefetch() > 0 { + h.state_mut().renderer.wait_for_worker(Duration::from_millis(250)); + } + h.run_steps(1); + // Neighbouring-photo prefetch does not contribute pixels to this capture. + if h.state().renderer.in_flight_without_prefetch() == 0 && h.state().loupe_shown.is_some_and(|(_, source)| source == "render") { quiet += 1; } else { quiet = 0; } - assert!(started.elapsed() < Duration::from_secs(30), "the synthetic photo and panel thumbnails must finish rendering before capture"); + assert!( + started.elapsed() < Duration::from_secs(120), + "the synthetic photo and panel thumbnails must finish rendering before capture: loupe {:?}, pending {:?}", + h.state().loupe_shown, + h.state().renderer.pending_slots() + ); std::thread::sleep(Duration::from_millis(5)); } } diff --git a/packaging/linux/flatpak/README.md b/packaging/linux/flatpak/README.md new file mode 100644 index 0000000000..7f6f7f1a1b --- /dev/null +++ b/packaging/linux/flatpak/README.md @@ -0,0 +1,120 @@ +# From-source Flatpak packaging + +`ai.storyteller.lightcraft.yml` builds LightCraft and `lightcraft-cli` from this +repository inside the Flatpak SDK. Flatpak builders have no network access, so +`cargo-sources.json` lists every crate and git source with a fixed checksum or +commit. Regenerate it after changing `Cargo.lock`: + +```sh +./packaging/linux/flatpak/generate-cargo-sources.py +flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.lightcraft.yml +flatpak run ai.storyteller.lightcraft +``` + +## Regeneration wrapper + +`generate-cargo-sources.py` uses the upstream +[`flatpak-cargo-generator.py`](https://github.com/flatpak/flatpak-builder-tools/blob/9d86d336a39b9af38b86f114b6ce5820fe260f91/cargo/flatpak-cargo-generator.py) +pinned to commit `9d86d336a39b9af38b86f114b6ce5820fe260f91` with SHA-256 +`0a2db6be87d75910facef28ab46d4d6460802e8419ab850d0caa6a364d26b380`. It downloads +the script, verifies that hash, applies the two reviewed patches below, and +invokes it with the current Python interpreter. The upstream script is not +vendored; it is MIT-licensed +([upstream licence](https://github.com/flatpak/flatpak-builder-tools/blob/9d86d336a39b9af38b86f114b6ce5820fe260f91/LICENSE)). +The patched script's hash is also checked before execution. + +Requirements: Python 3.9+ with `aiohttp` and `tomlkit` (`PyYAML` is optional and +only needed for upstream's YAML output), plus `git` and network access to fetch +the generator and git dependencies. Crate archive URLs and checksums are read +from Cargo.lock; Flatpak downloads those archives when materializing the sources. The generator clones git +repositories into `$XDG_CACHE_HOME/lightcraft-flatpak-cargo` by default, or the +directory passed with `--cache-dir`; the wrapper refuses a cache inside the +checkout or `CARGO_TARGET_DIR`. `cargo-sources.json` is written to a temporary +file and atomically renamed only after the generator succeeds, so a failed run +leaves the previous inventory untouched. + +The regenerated inventory keeps the temporary file’s owner-only permissions (0600 on POSIX). +Run the generator and Flatpak builder as the same user; a cross-user packaging workflow +should explicitly install the inventory with its required ownership and permissions. + +Generation is capped at 30 minutes and the generator's stdin is `/dev/null`, so +an interactive credential prompt fails instead of hanging. On POSIX systems +(Linux is the supported Flatpak packaging platform) the generator starts in its +own session; at the cap, the wrapper kills that generator's process group, +including git clone/fetch descendants, and reaps its leader, so no orphan keeps +writing into the cache. On Windows only the direct generator process is +terminated; its git descendants are not tree-killed, and the timeout is +otherwise the same. The timeout path runs before the atomic rename, so the +existing inventory and the temporary-file cleanup behave exactly like any other +generation failure. + +Because the generator no longer shares the wrapper's terminal session, the +wrapper also handles Ctrl-C (`SIGINT`) and `SIGTERM` while it runs: the signal +is recorded, the generator's process group is killed and reaped like the timeout +path, the temporary file and script directory are removed, and only then is the +signal replayed with the handler that was installed before generation started +(`KeyboardInterrupt` for `SIGINT`, the previous or default action for +`SIGTERM`). A cancelled `flatpak-builder` run or an interactive Ctrl-C therefore +leaves neither a detached generator or git clone nor a modified inventory. The +temporary handlers are installed before the generator is spawned, so a signal +that arrives while the child is being created cannot strand the new session +either. A cancellation delivered while those handlers are installed is recorded +once (the first signal wins) and replayed after cleanup with the caller's +restored handler. Signals that cannot be handled by this process at all +(`SIGKILL`, an OOM kill, or a wrapper crash) cannot be cleaned up: the generator +runs in its own session, so it may survive the wrapper, and any temporary output +or script directory it had created remains. A hard termination between creating +the temporary directory or output file and installing the handlers, or after +`run_generator` returns while the output is being validated and renamed, can +likewise leave those temporary paths behind (the complete temporary output in +the latter case), because the default signal action performs no cleanup. + +## Why two patches + +1. **Keep pinned revisions distinct.** Upstream keys `[source]` replacement + entries by the canonical repository URL and then adds `rev`/`tag`/`branch`. + Two different revisions of the same repository therefore overwrite each + other. The patch keys the entry by the full source URL without its `#` + fragment, so `https://github.com/storytold/craft-libs?rev=...` remains a + separate source for each craft-libs revision. +2. **Include `[patch.unused]`.** `Cargo.lock` records patched packages that are + not in the resolved graph under `patch.unused`. Upstream iterates only + `package`, which omits crates such as `egui_demo_lib`; `--locked` offline + dependency resolution then fails because Cargo still loads those patched manifests. The + patch also walks `patch.unused` entries. + +No third patch is needed: upstream's `git_repo_name()` already appends the +first seven characters of the commit, so the two craft-libs checkouts get +different destinations. + +## Offline validation performed + +The committed inventory was generated from the current `Cargo.lock` and then +validated without contacting the network: every git/archive/shell/inline entry +was materialized as Flatpak would, crate archives were checked against their +recorded SHA-256 values, and the generated Cargo config was pointed at the +materialized vendor directory. With an isolated `CARGO_HOME` and +`CARGO_NET_OFFLINE=true`, `cargo --config metadata --offline +--locked --format-version 1` exited 0. The two craft-libs revisions +(`82a24b006eefa036b43c09a67fb9a3fbd17ffe63` and +`25da1314d443171b4adeaad2cd9454b273f35c6f`) are present as separate git sources +and `[source]` entries. The checked-in `cargo-sources.json` has SHA-256 +`c7f515d1178bb79b2cabdfd419ad375a04ff5d84ba9d08b1b34970a460cd0f2c`. + +This validates the source inventory and offline metadata resolution only. A +full `flatpak-builder` build against the `org.freedesktop.Sdk` 25.08 runtime and +Rust extension is still required before release; it was not performed here. + +Run the wrapper failure-path and cancellation regression tests (the real +process-group cases, including Ctrl-C/`SIGTERM` cancellation of a live +generator session, run on Linux only and are skipped elsewhere) with: + +```sh +python3 -B packaging/linux/flatpak/test_generate_cargo_sources.py +``` + +The cancellation and timeout tests check process liveness and a frozen +heartbeat before their teardown kills anything. With +`LIGHTCRAFT_FLATPAK_TEST_BREAK_KILL=1` the group kill is a deliberate no-op and +those tests must fail; run that variant to confirm they do not silently pass on +a broken kill path. diff --git a/packaging/linux/flatpak/ai.storyteller.lightcraft.yml b/packaging/linux/flatpak/ai.storyteller.lightcraft.yml index 4e03df0055..e125475c57 100644 --- a/packaging/linux/flatpak/ai.storyteller.lightcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.lightcraft.yml @@ -2,11 +2,14 @@ # # Flatpak builds run offline, so the crate sources must be vendored first. From the repo root: # -# curl -fsSLO https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py -# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/linux/flatpak/cargo-sources.json +# ./packaging/linux/flatpak/generate-cargo-sources.py # flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.lightcraft.yml # flatpak run ai.storyteller.lightcraft # +# The wrapper downloads the pinned upstream flatpak-cargo-generator, verifies its checksum, +# applies the two reviewed patches described in packaging/linux/flatpak/README.md, and writes +# cargo-sources.json atomically. That README also covers dependencies and offline validation. +# # For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. # # ai.storyteller.lightcraft.bundle.yml (same directory) builds the .flatpak attached to every diff --git a/packaging/linux/flatpak/cargo-sources.json b/packaging/linux/flatpak/cargo-sources.json new file mode 100644 index 0000000000..8d723f2f41 --- /dev/null +++ b/packaging/linux/flatpak/cargo-sources.json @@ -0,0 +1,8593 @@ +[ + { + "type": "git", + "url": "https://github.com/storytold/craft-libs", + "commit": "82a24b006eefa036b43c09a67fb9a3fbd17ffe63", + "dest": "flatpak-cargo/git/craft-libs-82a24b0" + }, + { + "type": "git", + "url": "https://github.com/storytold/craft-libs", + "commit": "25da1314d443171b4adeaad2cd9454b273f35c6f", + "dest": "flatpak-cargo/git/craft-libs-25da131" + }, + { + "type": "git", + "url": "https://github.com/storytold/egui", + "commit": "6d518fc799681571963842f95b55a6b68431b15a", + "dest": "flatpak-cargo/git/egui-6d518fc" + }, + { + "type": "git", + "url": "https://github.com/storytold/winit", + "commit": "a517b3846ee8457d84ee4975110bfda6797c44dc", + "dest": "flatpak-cargo/git/winit-a517b38" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ab_glyph/ab_glyph-0.2.32.crate", + "sha256": "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2", + "dest": "cargo/vendor/ab_glyph-0.2.32" + }, + { + "type": "inline", + "contents": "{\"package\": \"01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2\", \"files\": {}}", + "dest": "cargo/vendor/ab_glyph-0.2.32", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ab_glyph_rasterizer/ab_glyph_rasterizer-0.1.10.crate", + "sha256": "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618", + "dest": "cargo/vendor/ab_glyph_rasterizer-0.1.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618\", \"files\": {}}", + "dest": "cargo/vendor/ab_glyph_rasterizer-0.1.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit/accesskit-0.25.1.crate", + "sha256": "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932", + "dest": "cargo/vendor/accesskit-0.25.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932\", \"files\": {}}", + "dest": "cargo/vendor/accesskit-0.25.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_atspi_common/accesskit_atspi_common-0.21.0.crate", + "sha256": "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4", + "dest": "cargo/vendor/accesskit_atspi_common-0.21.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_atspi_common-0.21.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_consumer/accesskit_consumer-0.39.1.crate", + "sha256": "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344", + "dest": "cargo/vendor/accesskit_consumer-0.39.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_consumer-0.39.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_ios/accesskit_ios-0.2.1.crate", + "sha256": "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937", + "dest": "cargo/vendor/accesskit_ios-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_ios-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_macos/accesskit_macos-0.27.1.crate", + "sha256": "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4", + "dest": "cargo/vendor/accesskit_macos-0.27.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_macos-0.27.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_unix/accesskit_unix-0.24.0.crate", + "sha256": "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d", + "dest": "cargo/vendor/accesskit_unix-0.24.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_unix-0.24.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_windows/accesskit_windows-0.35.1.crate", + "sha256": "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb", + "dest": "cargo/vendor/accesskit_windows-0.35.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_windows-0.35.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/accesskit_winit/accesskit_winit-0.34.1.crate", + "sha256": "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77", + "dest": "cargo/vendor/accesskit_winit-0.34.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77\", \"files\": {}}", + "dest": "cargo/vendor/accesskit_winit-0.34.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/addr2line/addr2line-0.25.1.crate", + "sha256": "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b", + "dest": "cargo/vendor/addr2line-0.25.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b\", \"files\": {}}", + "dest": "cargo/vendor/addr2line-0.25.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/adler2/adler2-2.0.1.crate", + "sha256": "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa", + "dest": "cargo/vendor/adler2-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa\", \"files\": {}}", + "dest": "cargo/vendor/adler2-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aead/aead-0.5.2.crate", + "sha256": "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0", + "dest": "cargo/vendor/aead-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0\", \"files\": {}}", + "dest": "cargo/vendor/aead-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aes/aes-0.8.4.crate", + "sha256": "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0", + "dest": "cargo/vendor/aes-0.8.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0\", \"files\": {}}", + "dest": "cargo/vendor/aes-0.8.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aes-gcm/aes-gcm-0.10.3.crate", + "sha256": "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1", + "dest": "cargo/vendor/aes-gcm-0.10.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1\", \"files\": {}}", + "dest": "cargo/vendor/aes-gcm-0.10.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ahash/ahash-0.8.12.crate", + "sha256": "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75", + "dest": "cargo/vendor/ahash-0.8.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75\", \"files\": {}}", + "dest": "cargo/vendor/ahash-0.8.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aligned/aligned-0.4.3.crate", + "sha256": "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685", + "dest": "cargo/vendor/aligned-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685\", \"files\": {}}", + "dest": "cargo/vendor/aligned-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aligned-vec/aligned-vec-0.6.4.crate", + "sha256": "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b", + "dest": "cargo/vendor/aligned-vec-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b\", \"files\": {}}", + "dest": "cargo/vendor/aligned-vec-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/alloc-no-stdlib/alloc-no-stdlib-2.0.4.crate", + "sha256": "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3", + "dest": "cargo/vendor/alloc-no-stdlib-2.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3\", \"files\": {}}", + "dest": "cargo/vendor/alloc-no-stdlib-2.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/alloc-stdlib/alloc-stdlib-0.2.4.crate", + "sha256": "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195", + "dest": "cargo/vendor/alloc-stdlib-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195\", \"files\": {}}", + "dest": "cargo/vendor/alloc-stdlib-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/allocator-api2/allocator-api2-0.2.21.crate", + "sha256": "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923", + "dest": "cargo/vendor/allocator-api2-0.2.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923\", \"files\": {}}", + "dest": "cargo/vendor/allocator-api2-0.2.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/android-activity/android-activity-0.6.1.crate", + "sha256": "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd", + "dest": "cargo/vendor/android-activity-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd\", \"files\": {}}", + "dest": "cargo/vendor/android-activity-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/android-properties/android-properties-0.2.2.crate", + "sha256": "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04", + "dest": "cargo/vendor/android-properties-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04\", \"files\": {}}", + "dest": "cargo/vendor/android-properties-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/android_system_properties/android_system_properties-0.1.6.crate", + "sha256": "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc", + "dest": "cargo/vendor/android_system_properties-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc\", \"files\": {}}", + "dest": "cargo/vendor/android_system_properties-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anyhow/anyhow-1.0.104.crate", + "sha256": "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470", + "dest": "cargo/vendor/anyhow-1.0.104" + }, + { + "type": "inline", + "contents": "{\"package\": \"330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470\", \"files\": {}}", + "dest": "cargo/vendor/anyhow-1.0.104", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/arbitrary/arbitrary-1.4.2.crate", + "sha256": "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1", + "dest": "cargo/vendor/arbitrary-1.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1\", \"files\": {}}", + "dest": "cargo/vendor/arbitrary-1.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/arboard/arboard-3.6.1.crate", + "sha256": "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf", + "dest": "cargo/vendor/arboard-3.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf\", \"files\": {}}", + "dest": "cargo/vendor/arboard-3.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/arg_enum_proc_macro/arg_enum_proc_macro-0.3.4.crate", + "sha256": "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea", + "dest": "cargo/vendor/arg_enum_proc_macro-0.3.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea\", \"files\": {}}", + "dest": "cargo/vendor/arg_enum_proc_macro-0.3.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/arrayvec/arrayvec-0.7.8.crate", + "sha256": "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56", + "dest": "cargo/vendor/arrayvec-0.7.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56\", \"files\": {}}", + "dest": "cargo/vendor/arrayvec-0.7.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/as-raw-xcb-connection/as-raw-xcb-connection-1.0.1.crate", + "sha256": "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b", + "dest": "cargo/vendor/as-raw-xcb-connection-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b\", \"files\": {}}", + "dest": "cargo/vendor/as-raw-xcb-connection-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/as-slice/as-slice-0.2.1.crate", + "sha256": "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516", + "dest": "cargo/vendor/as-slice-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516\", \"files\": {}}", + "dest": "cargo/vendor/as-slice-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ash/ash-0.38.0+1.3.281.crate", + "sha256": "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f", + "dest": "cargo/vendor/ash-0.38.0+1.3.281" + }, + { + "type": "inline", + "contents": "{\"package\": \"0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f\", \"files\": {}}", + "dest": "cargo/vendor/ash-0.38.0+1.3.281", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-broadcast/async-broadcast-0.7.2.crate", + "sha256": "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532", + "dest": "cargo/vendor/async-broadcast-0.7.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532\", \"files\": {}}", + "dest": "cargo/vendor/async-broadcast-0.7.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-channel/async-channel-2.5.0.crate", + "sha256": "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2", + "dest": "cargo/vendor/async-channel-2.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2\", \"files\": {}}", + "dest": "cargo/vendor/async-channel-2.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-executor/async-executor-1.14.0.crate", + "sha256": "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a", + "dest": "cargo/vendor/async-executor-1.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a\", \"files\": {}}", + "dest": "cargo/vendor/async-executor-1.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-io/async-io-2.6.0.crate", + "sha256": "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc", + "dest": "cargo/vendor/async-io-2.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc\", \"files\": {}}", + "dest": "cargo/vendor/async-io-2.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-lock/async-lock-3.4.2.crate", + "sha256": "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311", + "dest": "cargo/vendor/async-lock-3.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311\", \"files\": {}}", + "dest": "cargo/vendor/async-lock-3.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-process/async-process-2.5.0.crate", + "sha256": "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75", + "dest": "cargo/vendor/async-process-2.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75\", \"files\": {}}", + "dest": "cargo/vendor/async-process-2.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-recursion/async-recursion-1.1.1.crate", + "sha256": "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11", + "dest": "cargo/vendor/async-recursion-1.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11\", \"files\": {}}", + "dest": "cargo/vendor/async-recursion-1.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-signal/async-signal-0.2.14.crate", + "sha256": "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485", + "dest": "cargo/vendor/async-signal-0.2.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485\", \"files\": {}}", + "dest": "cargo/vendor/async-signal-0.2.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-task/async-task-4.7.1.crate", + "sha256": "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de", + "dest": "cargo/vendor/async-task-4.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de\", \"files\": {}}", + "dest": "cargo/vendor/async-task-4.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-trait/async-trait-0.1.92.crate", + "sha256": "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667", + "dest": "cargo/vendor/async-trait-0.1.92" + }, + { + "type": "inline", + "contents": "{\"package\": \"82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667\", \"files\": {}}", + "dest": "cargo/vendor/async-trait-0.1.92", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atomic-waker/atomic-waker-1.1.2.crate", + "sha256": "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0", + "dest": "cargo/vendor/atomic-waker-1.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0\", \"files\": {}}", + "dest": "cargo/vendor/atomic-waker-1.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atspi/atspi-0.29.0.crate", + "sha256": "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d", + "dest": "cargo/vendor/atspi-0.29.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d\", \"files\": {}}", + "dest": "cargo/vendor/atspi-0.29.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atspi-common/atspi-common-0.13.0.crate", + "sha256": "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d", + "dest": "cargo/vendor/atspi-common-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d\", \"files\": {}}", + "dest": "cargo/vendor/atspi-common-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atspi-proxies/atspi-proxies-0.13.0.crate", + "sha256": "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc", + "dest": "cargo/vendor/atspi-proxies-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc\", \"files\": {}}", + "dest": "cargo/vendor/atspi-proxies-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/autocfg/autocfg-1.5.1.crate", + "sha256": "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53", + "dest": "cargo/vendor/autocfg-1.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53\", \"files\": {}}", + "dest": "cargo/vendor/autocfg-1.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/av-scenechange/av-scenechange-0.14.1.crate", + "sha256": "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394", + "dest": "cargo/vendor/av-scenechange-0.14.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394\", \"files\": {}}", + "dest": "cargo/vendor/av-scenechange-0.14.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/av1-grain/av1-grain-0.2.5.crate", + "sha256": "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8", + "dest": "cargo/vendor/av1-grain-0.2.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8\", \"files\": {}}", + "dest": "cargo/vendor/av1-grain-0.2.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/avif-serialize/avif-serialize-0.8.9.crate", + "sha256": "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38", + "dest": "cargo/vendor/avif-serialize-0.8.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38\", \"files\": {}}", + "dest": "cargo/vendor/avif-serialize-0.8.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/backtrace/backtrace-0.3.76.crate", + "sha256": "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6", + "dest": "cargo/vendor/backtrace-0.3.76" + }, + { + "type": "inline", + "contents": "{\"package\": \"bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6\", \"files\": {}}", + "dest": "cargo/vendor/backtrace-0.3.76", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base16ct/base16ct-0.2.0.crate", + "sha256": "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf", + "dest": "cargo/vendor/base16ct-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf\", \"files\": {}}", + "dest": "cargo/vendor/base16ct-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base64ct/base64ct-1.8.3.crate", + "sha256": "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06", + "dest": "cargo/vendor/base64ct-1.8.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06\", \"files\": {}}", + "dest": "cargo/vendor/base64ct-1.8.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bit-set/bit-set-0.8.0.crate", + "sha256": "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3", + "dest": "cargo/vendor/bit-set-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3\", \"files\": {}}", + "dest": "cargo/vendor/bit-set-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bit-set/bit-set-0.10.0.crate", + "sha256": "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d", + "dest": "cargo/vendor/bit-set-0.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d\", \"files\": {}}", + "dest": "cargo/vendor/bit-set-0.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bit-vec/bit-vec-0.8.0.crate", + "sha256": "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7", + "dest": "cargo/vendor/bit-vec-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7\", \"files\": {}}", + "dest": "cargo/vendor/bit-vec-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bit-vec/bit-vec-0.9.1.crate", + "sha256": "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51", + "dest": "cargo/vendor/bit-vec-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51\", \"files\": {}}", + "dest": "cargo/vendor/bit-vec-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate", + "sha256": "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a", + "dest": "cargo/vendor/bitflags-1.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\", \"files\": {}}", + "dest": "cargo/vendor/bitflags-1.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bitflags/bitflags-2.13.2.crate", + "sha256": "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06", + "dest": "cargo/vendor/bitflags-2.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06\", \"files\": {}}", + "dest": "cargo/vendor/bitflags-2.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bitstream-io/bitstream-io-4.10.0.crate", + "sha256": "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f", + "dest": "cargo/vendor/bitstream-io-4.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f\", \"files\": {}}", + "dest": "cargo/vendor/bitstream-io-4.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block/block-0.1.6.crate", + "sha256": "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a", + "dest": "cargo/vendor/block-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a\", \"files\": {}}", + "dest": "cargo/vendor/block-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block-buffer/block-buffer-0.10.4.crate", + "sha256": "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71", + "dest": "cargo/vendor/block-buffer-0.10.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71\", \"files\": {}}", + "dest": "cargo/vendor/block-buffer-0.10.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block2/block2-0.5.1.crate", + "sha256": "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f", + "dest": "cargo/vendor/block2-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f\", \"files\": {}}", + "dest": "cargo/vendor/block2-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block2/block2-0.6.2.crate", + "sha256": "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5", + "dest": "cargo/vendor/block2-0.6.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5\", \"files\": {}}", + "dest": "cargo/vendor/block2-0.6.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/blocking/blocking-1.7.0.crate", + "sha256": "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa", + "dest": "cargo/vendor/blocking-1.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa\", \"files\": {}}", + "dest": "cargo/vendor/blocking-1.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/brotli/brotli-8.0.4.crate", + "sha256": "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3", + "dest": "cargo/vendor/brotli-8.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3\", \"files\": {}}", + "dest": "cargo/vendor/brotli-8.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/brotli-decompressor/brotli-decompressor-5.0.3.crate", + "sha256": "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583", + "dest": "cargo/vendor/brotli-decompressor-5.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583\", \"files\": {}}", + "dest": "cargo/vendor/brotli-decompressor-5.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/built/built-0.8.1.crate", + "sha256": "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9", + "dest": "cargo/vendor/built-0.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9\", \"files\": {}}", + "dest": "cargo/vendor/built-0.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bumpalo/bumpalo-3.20.3.crate", + "sha256": "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649", + "dest": "cargo/vendor/bumpalo-3.20.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649\", \"files\": {}}", + "dest": "cargo/vendor/bumpalo-3.20.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bytemuck/bytemuck-1.25.2.crate", + "sha256": "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797", + "dest": "cargo/vendor/bytemuck-1.25.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797\", \"files\": {}}", + "dest": "cargo/vendor/bytemuck-1.25.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bytemuck_derive/bytemuck_derive-1.12.1.crate", + "sha256": "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c", + "dest": "cargo/vendor/bytemuck_derive-1.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c\", \"files\": {}}", + "dest": "cargo/vendor/bytemuck_derive-1.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/byteorder/byteorder-1.5.0.crate", + "sha256": "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b", + "dest": "cargo/vendor/byteorder-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b\", \"files\": {}}", + "dest": "cargo/vendor/byteorder-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/byteorder-lite/byteorder-lite-0.1.0.crate", + "sha256": "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495", + "dest": "cargo/vendor/byteorder-lite-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495\", \"files\": {}}", + "dest": "cargo/vendor/byteorder-lite-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bytes/bytes-1.12.1.crate", + "sha256": "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04", + "dest": "cargo/vendor/bytes-1.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04\", \"files\": {}}", + "dest": "cargo/vendor/bytes-1.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/calloop/calloop-0.13.0.crate", + "sha256": "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec", + "dest": "cargo/vendor/calloop-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec\", \"files\": {}}", + "dest": "cargo/vendor/calloop-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/calloop/calloop-0.14.4.crate", + "sha256": "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7", + "dest": "cargo/vendor/calloop-0.14.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7\", \"files\": {}}", + "dest": "cargo/vendor/calloop-0.14.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/calloop-wayland-source/calloop-wayland-source-0.3.0.crate", + "sha256": "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20", + "dest": "cargo/vendor/calloop-wayland-source-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20\", \"files\": {}}", + "dest": "cargo/vendor/calloop-wayland-source-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/calloop-wayland-source/calloop-wayland-source-0.4.1.crate", + "sha256": "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa", + "dest": "cargo/vendor/calloop-wayland-source-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa\", \"files\": {}}", + "dest": "cargo/vendor/calloop-wayland-source-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/candle-core/candle-core-0.9.2.crate", + "sha256": "c15b675b80d994b2eadb20a4bbe434eabeb454eac3ee5e2b4cf6f147ee9be091", + "dest": "cargo/vendor/candle-core-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c15b675b80d994b2eadb20a4bbe434eabeb454eac3ee5e2b4cf6f147ee9be091\", \"files\": {}}", + "dest": "cargo/vendor/candle-core-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/candle-metal-kernels/candle-metal-kernels-0.9.2.crate", + "sha256": "2fdfe9d06de16ce49961e49084e5b79a75a9bdf157246e7c7b6328e87a7aa25d", + "dest": "cargo/vendor/candle-metal-kernels-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"2fdfe9d06de16ce49961e49084e5b79a75a9bdf157246e7c7b6328e87a7aa25d\", \"files\": {}}", + "dest": "cargo/vendor/candle-metal-kernels-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/candle-nn/candle-nn-0.9.2.crate", + "sha256": "3045fa9e7aef8567d209a27d56b692f60b96f4d0569f4c3011f8ca6715c65e03", + "dest": "cargo/vendor/candle-nn-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"3045fa9e7aef8567d209a27d56b692f60b96f4d0569f4c3011f8ca6715c65e03\", \"files\": {}}", + "dest": "cargo/vendor/candle-nn-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/candle-ug/candle-ug-0.9.2.crate", + "sha256": "c22d62be69068bf58987a45f690612739d8d2ea1bf508c1b87dc6815a019575d", + "dest": "cargo/vendor/candle-ug-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c22d62be69068bf58987a45f690612739d8d2ea1bf508c1b87dc6815a019575d\", \"files\": {}}", + "dest": "cargo/vendor/candle-ug-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cc/cc-1.5.1.crate", + "sha256": "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040", + "dest": "cargo/vendor/cc-1.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040\", \"files\": {}}", + "dest": "cargo/vendor/cc-1.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cfg-if/cfg-if-1.0.5.crate", + "sha256": "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600", + "dest": "cargo/vendor/cfg-if-1.0.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600\", \"files\": {}}", + "dest": "cargo/vendor/cfg-if-1.0.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cfg_aliases/cfg_aliases-0.2.2.crate", + "sha256": "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527", + "dest": "cargo/vendor/cfg_aliases-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527\", \"files\": {}}", + "dest": "cargo/vendor/cfg_aliases-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cgl/cgl-0.3.2.crate", + "sha256": "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff", + "dest": "cargo/vendor/cgl-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff\", \"files\": {}}", + "dest": "cargo/vendor/cgl-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/chacha20/chacha20-0.9.1.crate", + "sha256": "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818", + "dest": "cargo/vendor/chacha20-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818\", \"files\": {}}", + "dest": "cargo/vendor/chacha20-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/chacha20poly1305/chacha20poly1305-0.10.1.crate", + "sha256": "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35", + "dest": "cargo/vendor/chacha20poly1305-0.10.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35\", \"files\": {}}", + "dest": "cargo/vendor/chacha20poly1305-0.10.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/chrono/chrono-0.4.45.crate", + "sha256": "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327", + "dest": "cargo/vendor/chrono-0.4.45" + }, + { + "type": "inline", + "contents": "{\"package\": \"1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327\", \"files\": {}}", + "dest": "cargo/vendor/chrono-0.4.45", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cipher/cipher-0.4.4.crate", + "sha256": "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad", + "dest": "cargo/vendor/cipher-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad\", \"files\": {}}", + "dest": "cargo/vendor/cipher-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/clipboard-win/clipboard-win-5.4.1.crate", + "sha256": "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4", + "dest": "cargo/vendor/clipboard-win-5.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4\", \"files\": {}}", + "dest": "cargo/vendor/clipboard-win-5.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/codespan-reporting/codespan-reporting-0.13.1.crate", + "sha256": "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681", + "dest": "cargo/vendor/codespan-reporting-0.13.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681\", \"files\": {}}", + "dest": "cargo/vendor/codespan-reporting-0.13.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/coe-rs/coe-rs-0.1.2.crate", + "sha256": "7e8f1e641542c07631228b1e0dc04b69ae3c1d58ef65d5691a439711d805c698", + "dest": "cargo/vendor/coe-rs-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"7e8f1e641542c07631228b1e0dc04b69ae3c1d58ef65d5691a439711d805c698\", \"files\": {}}", + "dest": "cargo/vendor/coe-rs-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/color/color-0.3.3.crate", + "sha256": "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf", + "dest": "cargo/vendor/color-0.3.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf\", \"files\": {}}", + "dest": "cargo/vendor/color-0.3.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/color_quant/color_quant-1.1.0.crate", + "sha256": "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b", + "dest": "cargo/vendor/color_quant-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b\", \"files\": {}}", + "dest": "cargo/vendor/color_quant-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/colored/colored-2.2.0.crate", + "sha256": "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c", + "dest": "cargo/vendor/colored-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c\", \"files\": {}}", + "dest": "cargo/vendor/colored-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/combine/combine-4.6.8.crate", + "sha256": "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e", + "dest": "cargo/vendor/combine-4.6.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e\", \"files\": {}}", + "dest": "cargo/vendor/combine-4.6.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/concurrent-queue/concurrent-queue-2.5.0.crate", + "sha256": "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973", + "dest": "cargo/vendor/concurrent-queue-2.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973\", \"files\": {}}", + "dest": "cargo/vendor/concurrent-queue-2.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/const-oid/const-oid-0.9.6.crate", + "sha256": "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8", + "dest": "cargo/vendor/const-oid-0.9.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8\", \"files\": {}}", + "dest": "cargo/vendor/const-oid-0.9.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-foundation/core-foundation-0.9.4.crate", + "sha256": "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f", + "dest": "cargo/vendor/core-foundation-0.9.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f\", \"files\": {}}", + "dest": "cargo/vendor/core-foundation-0.9.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-foundation-sys/core-foundation-sys-0.8.7.crate", + "sha256": "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b", + "dest": "cargo/vendor/core-foundation-sys-0.8.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b\", \"files\": {}}", + "dest": "cargo/vendor/core-foundation-sys-0.8.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-graphics/core-graphics-0.23.2.crate", + "sha256": "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081", + "dest": "cargo/vendor/core-graphics-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081\", \"files\": {}}", + "dest": "cargo/vendor/core-graphics-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-graphics-types/core-graphics-types-0.1.3.crate", + "sha256": "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf", + "dest": "cargo/vendor/core-graphics-types-0.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf\", \"files\": {}}", + "dest": "cargo/vendor/core-graphics-types-0.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cpufeatures/cpufeatures-0.2.17.crate", + "sha256": "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280", + "dest": "cargo/vendor/cpufeatures-0.2.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280\", \"files\": {}}", + "dest": "cargo/vendor/cpufeatures-0.2.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/craft-libs-82a24b0/crates/docpath\" \"cargo/vendor/craft-docpath\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"craft-docpath\"\ndescription = \"Decide whether a file path or file URL stored in a document refers to a local file.\"\nversion = \"0.1.0\"\nedition = \"2024\"\nlicense = \"MIT OR Apache-2.0\"\nrust-version = \"1.95\"\nrepository = \"https://github.com/storytold/craft-libs\"\n\n[lints.rust]\nunsafe_code = \"forbid\"\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(craft_pdf_unicode_fixtures)\"]\n\n[lints.clippy]\ndbg_macro = \"warn\"\ntodo = \"warn\"\n", + "dest": "cargo/vendor/craft-docpath", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/craft-docpath", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/craft-libs-25da131/crates/ui\" \"cargo/vendor/craft-ui\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"craft-ui\"\nversion = \"0.1.0\"\nedition = \"2024\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/storytold/craft-libs\"\nrust-version = \"1.95\"\ndescription = \"Reusable egui controls and icon catalogs for Craft frontends\"\n\n[lints.rust]\nunsafe_code = \"forbid\"\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(craft_pdf_unicode_fixtures)\"]\n\n[lints.clippy]\ndbg_macro = \"warn\"\ntodo = \"warn\"\n\n[dependencies.egui]\nversion = \"0.37\"\ndefault-features = false\n\n[dependencies.serde]\nversion = \"1\"\nfeatures = [\"derive\"]\noptional = true\n\n[features]\nserde = [\"dep:serde\"]\n\n[dev-dependencies]\nserde_json = \"1\"\n\n[dev-dependencies.egui]\nfeatures = [\"default_fonts\"]\nversion = \"0.37\"\ndefault-features = false\n\n[dev-dependencies.egui_extras]\nversion = \"0.37\"\ndefault-features = false\nfeatures = [\"svg\"]\n\n[dev-dependencies.egui_kittest]\nversion = \"0.37\"\nfeatures = [\"wgpu\"]\n\n[dev-dependencies.image]\nversion = \"0.25\"\ndefault-features = false\nfeatures = [\"png\"]\n", + "dest": "cargo/vendor/craft-ui", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/craft-ui", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crc32fast/crc32fast-1.5.2.crate", + "sha256": "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78", + "dest": "cargo/vendor/crc32fast-1.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78\", \"files\": {}}", + "dest": "cargo/vendor/crc32fast-1.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crossbeam-channel/crossbeam-channel-0.5.17.crate", + "sha256": "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1", + "dest": "cargo/vendor/crossbeam-channel-0.5.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1\", \"files\": {}}", + "dest": "cargo/vendor/crossbeam-channel-0.5.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crossbeam-deque/crossbeam-deque-0.8.8.crate", + "sha256": "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a", + "dest": "cargo/vendor/crossbeam-deque-0.8.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a\", \"files\": {}}", + "dest": "cargo/vendor/crossbeam-deque-0.8.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crossbeam-epoch/crossbeam-epoch-0.9.21.crate", + "sha256": "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d", + "dest": "cargo/vendor/crossbeam-epoch-0.9.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d\", \"files\": {}}", + "dest": "cargo/vendor/crossbeam-epoch-0.9.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crossbeam-utils/crossbeam-utils-0.8.23.crate", + "sha256": "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6", + "dest": "cargo/vendor/crossbeam-utils-0.8.23" + }, + { + "type": "inline", + "contents": "{\"package\": \"a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6\", \"files\": {}}", + "dest": "cargo/vendor/crossbeam-utils-0.8.23", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crunchy/crunchy-0.2.4.crate", + "sha256": "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5", + "dest": "cargo/vendor/crunchy-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5\", \"files\": {}}", + "dest": "cargo/vendor/crunchy-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crypto-bigint/crypto-bigint-0.5.5.crate", + "sha256": "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76", + "dest": "cargo/vendor/crypto-bigint-0.5.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76\", \"files\": {}}", + "dest": "cargo/vendor/crypto-bigint-0.5.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crypto-common/crypto-common-0.1.7.crate", + "sha256": "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a", + "dest": "cargo/vendor/crypto-common-0.1.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a\", \"files\": {}}", + "dest": "cargo/vendor/crypto-common-0.1.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ctr/ctr-0.9.2.crate", + "sha256": "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835", + "dest": "cargo/vendor/ctr-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835\", \"files\": {}}", + "dest": "cargo/vendor/ctr-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cursor-icon/cursor-icon-1.2.0.crate", + "sha256": "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f", + "dest": "cargo/vendor/cursor-icon-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f\", \"files\": {}}", + "dest": "cargo/vendor/cursor-icon-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/curve25519-dalek/curve25519-dalek-4.1.3.crate", + "sha256": "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be", + "dest": "cargo/vendor/curve25519-dalek-4.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be\", \"files\": {}}", + "dest": "cargo/vendor/curve25519-dalek-4.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/curve25519-dalek-derive/curve25519-dalek-derive-0.1.1.crate", + "sha256": "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3", + "dest": "cargo/vendor/curve25519-dalek-derive-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3\", \"files\": {}}", + "dest": "cargo/vendor/curve25519-dalek-derive-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/der/der-0.7.10.crate", + "sha256": "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb", + "dest": "cargo/vendor/der-0.7.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb\", \"files\": {}}", + "dest": "cargo/vendor/der-0.7.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dhat/dhat-0.3.3.crate", + "sha256": "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827", + "dest": "cargo/vendor/dhat-0.3.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827\", \"files\": {}}", + "dest": "cargo/vendor/dhat-0.3.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dify/dify-0.8.0.crate", + "sha256": "90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc", + "dest": "cargo/vendor/dify-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc\", \"files\": {}}", + "dest": "cargo/vendor/dify-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/digest/digest-0.10.7.crate", + "sha256": "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292", + "dest": "cargo/vendor/digest-0.10.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292\", \"files\": {}}", + "dest": "cargo/vendor/digest-0.10.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dispatch/dispatch-0.2.0.crate", + "sha256": "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b", + "dest": "cargo/vendor/dispatch-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b\", \"files\": {}}", + "dest": "cargo/vendor/dispatch-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dispatch2/dispatch2-0.3.1.crate", + "sha256": "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38", + "dest": "cargo/vendor/dispatch2-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38\", \"files\": {}}", + "dest": "cargo/vendor/dispatch2-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dlib/dlib-0.5.3.crate", + "sha256": "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a", + "dest": "cargo/vendor/dlib-0.5.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a\", \"files\": {}}", + "dest": "cargo/vendor/dlib-0.5.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/document-features/document-features-0.2.12.crate", + "sha256": "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61", + "dest": "cargo/vendor/document-features-0.2.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61\", \"files\": {}}", + "dest": "cargo/vendor/document-features-0.2.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/downcast-rs/downcast-rs-1.2.1.crate", + "sha256": "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2", + "dest": "cargo/vendor/downcast-rs-1.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2\", \"files\": {}}", + "dest": "cargo/vendor/downcast-rs-1.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dpi/dpi-0.1.2.crate", + "sha256": "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76", + "dest": "cargo/vendor/dpi-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76\", \"files\": {}}", + "dest": "cargo/vendor/dpi-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dyn-stack/dyn-stack-0.10.0.crate", + "sha256": "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b", + "dest": "cargo/vendor/dyn-stack-0.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b\", \"files\": {}}", + "dest": "cargo/vendor/dyn-stack-0.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dyn-stack/dyn-stack-0.13.2.crate", + "sha256": "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8", + "dest": "cargo/vendor/dyn-stack-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8\", \"files\": {}}", + "dest": "cargo/vendor/dyn-stack-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dyn-stack-macros/dyn-stack-macros-0.1.3.crate", + "sha256": "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9", + "dest": "cargo/vendor/dyn-stack-macros-0.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9\", \"files\": {}}", + "dest": "cargo/vendor/dyn-stack-macros-0.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ecdsa/ecdsa-0.16.9.crate", + "sha256": "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca", + "dest": "cargo/vendor/ecdsa-0.16.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca\", \"files\": {}}", + "dest": "cargo/vendor/ecdsa-0.16.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/ecolor\" \"cargo/vendor/ecolor\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"ecolor\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \", \"Andreas Reich \"]\ndescription = \"Color structs and color conversion utilities\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui\"\ncategories = [\"mathematics\", \"encoding\"]\nkeywords = [\"gui\", \"color\", \"conversion\", \"gamedev\", \"images\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = []\n\n[dependencies.emath]\nversion = \"0.37.0\"\npath = \"crates/emath\"\ndefault-features = false\n\n[dependencies.bytemuck]\noptional = true\nfeatures = [\"derive\"]\nversion = \"1.25\"\n\n[dependencies.cint]\noptional = true\nversion = \"0.3.1\"\n\n[dependencies.color-hex]\noptional = true\nversion = \"0.2.0\"\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n", + "dest": "cargo/vendor/ecolor", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/ecolor", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ed25519/ed25519-2.2.3.crate", + "sha256": "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53", + "dest": "cargo/vendor/ed25519-2.2.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53\", \"files\": {}}", + "dest": "cargo/vendor/ed25519-2.2.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ed25519-dalek/ed25519-dalek-2.2.0.crate", + "sha256": "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9", + "dest": "cargo/vendor/ed25519-dalek-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9\", \"files\": {}}", + "dest": "cargo/vendor/ed25519-dalek-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/eframe\" \"cargo/vendor/eframe\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"eframe\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"egui framework - write GUI apps that compiles to web and/or natively\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/eframe\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/eframe\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"egui\", \"gui\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\", \"data/icon.png\"]\n\n[package.metadata.docs.rs]\nall-features = true\ntargets = [\"x86_64-unknown-linux-gnu\", \"wasm32-unknown-unknown\"]\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = [\"accesskit\", \"clipboard\", \"default_fonts\", \"links\", \"system_fonts\", \"wayland\", \"web_screen_reader\", \"wgpu\", \"winit/default\", \"x11\"]\naccesskit = [\"egui-winit/accesskit\"]\nandroid-game-activity = [\"egui-winit/android-game-activity\"]\nandroid-native-activity = [\"egui-winit/android-native-activity\"]\ndefault_fonts = [\"egui/default_fonts\"]\nexperimental = [\"egui/experimental\"]\nglow = [\"dep:egui_glow\", \"dep:glow\", \"dep:glutin-winit\", \"dep:glutin\"]\npersistence = [\"egui-winit/serde\", \"egui/persistence\", \"ron\", \"serde\"]\nwayland = [\"egui-winit/wayland\", \"egui-wgpu?/wayland\", \"egui_glow?/wayland\", \"glutin?/wayland\", \"glutin-winit?/wayland\"]\nsystem_fonts = [\"dep:egui_system_font_provider\"]\nweb_screen_reader = [\"web-sys/SpeechSynthesis\", \"web-sys/SpeechSynthesisUtterance\"]\nwgpu = [\"wgpu_no_default_features\", \"egui-wgpu/default\"]\nwgpu_no_default_features = [\"dep:wgpu\", \"dep:egui-wgpu\", \"dep:pollster\"]\nx11 = [\"egui-winit/x11\", \"egui-wgpu?/x11\", \"egui_glow?/x11\", \"glutin?/x11\", \"glutin?/glx\", \"glutin-winit?/x11\", \"glutin-winit?/glx\"]\n__screenshot = []\ninspection = [\"dep:egui_inspection\", \"accesskit\"]\nlinks = [\"egui-winit/links\"]\nclipboard = [\"egui-winit/clipboard\", \"egui-winit/clipboard-image\"]\n\n[dependencies]\ndocument-features = \"0.2.12\"\nparking_lot = \"0.12.5\"\nraw-window-handle = \"0.6.2\"\nstatic_assertions = \"1.1\"\nweb-time = \"1.1\"\n\n[dependencies.egui]\ndefault-features = false\nfeatures = [\"bytemuck\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.ahash]\nversion = \"0.8.12\"\ndefault-features = false\nfeatures = [\"no-rng\", \"std\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.egui_glow]\noptional = true\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui_glow\"\n\n[dependencies.egui_inspection]\noptional = true\nfeatures = [\"plugin\"]\nversion = \"0.37.0\"\npath = \"crates/egui_inspection\"\ndefault-features = false\n\n[dependencies.glow]\noptional = true\nversion = \"0.17.0\"\n\n[dependencies.ron]\noptional = true\nfeatures = [\"integer128\"]\nversion = \"0.12.2\"\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.egui-winit]\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui-winit\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.image]\nfeatures = [\"png\"]\nversion = \"0.25.10\"\ndefault-features = false\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.winit]\ndefault-features = false\nfeatures = [\"rwh_06\"]\nversion = \"0.30.13\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.egui_system_font_provider]\noptional = true\nversion = \"0.37.0\"\npath = \"crates/egui_system_font_provider\"\ndefault-features = false\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.egui-wgpu]\noptional = true\nfeatures = [\"winit\", \"capture\"]\nversion = \"0.37.0\"\npath = \"crates/egui-wgpu\"\ndefault-features = false\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.pollster]\noptional = true\nversion = \"1.0\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.glutin]\noptional = true\ndefault-features = false\nfeatures = [\"egl\", \"wgl\"]\nversion = \"0.32.3\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.glutin-winit]\noptional = true\ndefault-features = false\nfeatures = [\"egl\", \"wgl\"]\nversion = \"0.5.0\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dev-dependencies]\ndirectories = \"6.0\"\n\n[target.\"cfg(any(target_os = \\\"macos\\\"))\".dependencies]\nobjc2 = \"0.6.4\"\n\n[target.\"cfg(any(target_os = \\\"macos\\\"))\".dependencies.objc2-foundation]\ndefault-features = false\nfeatures = [\"std\", \"block2\", \"NSData\", \"NSString\"]\nversion = \"0.3.2\"\n\n[target.\"cfg(any(target_os = \\\"macos\\\"))\".dependencies.objc2-app-kit]\ndefault-features = false\nfeatures = [\"std\", \"NSApplication\", \"NSBitmapImageRep\", \"NSButton\", \"NSControl\", \"NSGraphics\", \"NSImage\", \"NSImageRep\", \"NSMenu\", \"NSMenuItem\", \"NSResponder\", \"NSView\", \"NSWindow\"]\nversion = \"0.3.2\"\n\n[target.\"cfg(any(target_os = \\\"windows\\\"))\".dependencies.windows-sys]\nfeatures = [\"Win32_Foundation\", \"Win32_System_Com\", \"Win32_UI_Input_KeyboardAndMouse\", \"Win32_UI_Shell\", \"Win32_UI_WindowsAndMessaging\"]\nversion = \"0.61.2\"\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies]\nbytemuck = \"1.25\"\njs-sys = \"0.3.103\"\npercent-encoding = \"2.3\"\nsend_wrapper = \"0.6\"\nwasm-bindgen = \"0.2.126\"\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.egui]\nfeatures = [\"web_file\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.image]\nfeatures = [\"png\"]\nversion = \"0.25.10\"\ndefault-features = false\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.web-sys]\nfeatures = [\"AddEventListenerOptions\", \"BinaryType\", \"Blob\", \"BlobPropertyBag\", \"Clipboard\", \"ClipboardEvent\", \"ClipboardItem\", \"CanvasRenderingContext2d\", \"CompositionEvent\", \"console\", \"CssStyleDeclaration\", \"DataTransfer\", \"DataTransferItem\", \"DataTransferItemList\", \"Document\", \"DomRect\", \"DragEvent\", \"Element\", \"Event\", \"EventListener\", \"EventTarget\", \"ExtSRgb\", \"File\", \"FileList\", \"FocusEvent\", \"FocusOptions\", \"HtmlCanvasElement\", \"HtmlElement\", \"HtmlInputElement\", \"InputEvent\", \"ImageData\", \"KeyboardEvent\", \"Location\", \"MediaQueryList\", \"MediaQueryListEvent\", \"MouseEvent\", \"Navigator\", \"Node\", \"NodeList\", \"Performance\", \"ResizeObserver\", \"ResizeObserverBoxOptions\", \"ResizeObserverEntry\", \"ResizeObserverOptions\", \"ResizeObserverSize\", \"ShadowRoot\", \"Storage\", \"TextMetrics\", \"Touch\", \"TouchEvent\", \"PointerEvent\", \"TouchList\", \"WebGl2RenderingContext\", \"WebglDebugRendererInfo\", \"WebGlRenderingContext\", \"WheelEvent\", \"Window\"]\nversion = \"0.3.103\"\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.egui-wgpu]\noptional = true\nfeatures = [\"capture\"]\nversion = \"0.37.0\"\npath = \"crates/egui-wgpu\"\ndefault-features = false\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.wgpu]\noptional = true\nversion = \"30.0\"\ndefault-features = false\nfeatures = [\"std\"]\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dev-dependencies]\nwasm-bindgen-test = \"=0.3.76\"\n", + "dest": "cargo/vendor/eframe", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/eframe", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui\" \"cargo/vendor/egui\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"An easy-to-use immediate mode GUI that runs on both web and native\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"../../README.md\"\nrepository = \"https://github.com/emilk/egui\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"gui\", \"imgui\", \"immediate\", \"portable\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\ntargets = [\"x86_64-unknown-linux-gnu\", \"wasm32-unknown-unknown\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = []\nbytemuck = [\"epaint/bytemuck\"]\ncallstack = [\"dep:backtrace\"]\ncint = [\"epaint/cint\"]\ncolor-hex = [\"epaint/color-hex\"]\ndefault_fonts = [\"epaint/default_fonts\"]\nexperimental = []\nmint = [\"epaint/mint\"]\nmonochrome_emoji_fonts = [\"default_fonts\", \"epaint/monochrome_emoji_fonts\"]\npersistence = [\"serde\", \"epaint/serde\", \"ron\"]\nrayon = [\"epaint/rayon\"]\nserde = [\"dep:serde\", \"epaint/serde\", \"accesskit/serde\"]\nunity = [\"epaint/unity\"]\nweb_file = [\"dep:web-sys\"]\n_override_unity = [\"epaint/_override_unity\"]\n\n[dependencies]\naccesskit = \"0.25.0\"\nbitflags = \"2.13\"\nitertools = \"0.15.0\"\nnohash-hasher = \"0.2.0\"\nsmallvec = \"1.16\"\nunicode-segmentation = \"1.13\"\n\n[dependencies.emath]\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/emath\"\n\n[dependencies.epaint]\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/epaint\"\n\n[dependencies.ahash]\nversion = \"0.8.12\"\ndefault-features = false\nfeatures = [\"no-rng\", \"std\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.backtrace]\noptional = true\nversion = \"0.3.76\"\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.ron]\noptional = true\nversion = \"0.12.2\"\n\n[dependencies.serde]\noptional = true\nfeatures = [\"derive\", \"rc\", \"derive\"]\nversion = \"1.0\"\n\n[target.\"cfg(target_arch = \\\"wasm32\\\")\".dependencies.web-sys]\noptional = true\nfeatures = [\"File\"]\nversion = \"0.3.103\"\n\n[dev-dependencies.skrifa]\nversion = \"0.44.0\"\ndefault-features = false\nfeatures = [\"std\", \"autohint_shaping\"]\n", + "dest": "cargo/vendor/egui", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui-wgpu\" \"cargo/vendor/egui-wgpu\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui-wgpu\"\nversion = \"0.37.0\"\ndescription = \"Bindings for using egui natively using the wgpu library\"\nauthors = [\"Nils Hasenbanck \", \"embotech \", \"Emil Ernerfeldt \"]\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/egui-wgpu\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/egui-wgpu\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"wgpu\", \"egui\", \"gui\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"**/*.wgsl\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[features]\ndefault = [\"fragile-send-sync-non-atomic-wasm\", \"macos-window-resize-jitter-fix\", \"wgpu/default\", \"wgpu/webgl\"]\ncapture = [\"dep:egui\"]\nwinit = [\"dep:winit\", \"winit/rwh_06\", \"dep:egui\", \"capture\"]\nwayland = [\"winit?/wayland\"]\nx11 = [\"winit?/x11\"]\nfragile-send-sync-non-atomic-wasm = [\"wgpu/fragile-send-sync-non-atomic-wasm\"]\nmacos-window-resize-jitter-fix = [\"wgpu/metal\"]\n\n[dependencies]\nbytemuck = \"1.25\"\ndocument-features = \"0.2.12\"\nthiserror = \"2.0\"\ntype-map = \"0.5.1\"\nweb-time = \"1.1\"\n\n[dependencies.epaint]\ndefault-features = false\nfeatures = [\"bytemuck\"]\nversion = \"0.37.0\"\npath = \"crates/epaint\"\n\n[dependencies.ahash]\nversion = \"0.8.12\"\ndefault-features = false\nfeatures = [\"no-rng\", \"std\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.wgpu]\nfeatures = [\"wgsl\", \"std\"]\nversion = \"30.0\"\ndefault-features = false\n\n[dependencies.egui]\noptional = true\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.winit]\noptional = true\ndefault-features = false\nversion = \"0.30.13\"\n", + "dest": "cargo/vendor/egui-wgpu", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui-wgpu", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui-winit\" \"cargo/vendor/egui-winit\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui-winit\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Bindings for using egui with winit\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/egui-winit\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/egui-winit\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"winit\", \"egui\", \"gui\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[package.metadata.cargo-shear]\nignored = [\"wayland-cursor\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[features]\ndefault = [\"clipboard\", \"clipboard-image\", \"links\", \"wayland\", \"winit/default\", \"x11\"]\naccesskit = [\"dep:accesskit_winit\"]\nandroid-game-activity = [\"winit/android-game-activity\"]\nandroid-native-activity = [\"winit/android-native-activity\"]\nbytemuck = [\"egui/bytemuck\", \"dep:bytemuck\"]\nclipboard = [\"arboard\", \"bytemuck\", \"smithay-clipboard\"]\nclipboard-image = [\"clipboard\", \"dep:image\", \"dep:parking_lot\"]\nlinks = [\"webbrowser\"]\nserde = [\"egui/serde\", \"dep:serde\"]\nwayland = [\"winit/wayland\", \"bytemuck\", \"arboard?/wayland-data-control\"]\nx11 = [\"winit/x11\", \"winit/rwh_06\", \"bytemuck\"]\n\n[dependencies]\nraw-window-handle = \"0.6.2\"\nweb-time = \"1.1\"\n\n[dependencies.egui]\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.winit]\ndefault-features = false\nversion = \"0.30.13\"\n\n[dependencies.accesskit_winit]\noptional = true\nversion = \"0.34.0\"\n\n[dependencies.bytemuck]\noptional = true\nversion = \"1.25\"\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[dependencies.webbrowser]\noptional = true\nversion = \"1.2.4\"\n\n[dependencies.image]\ndefault-features = false\nfeatures = [\"png\"]\noptional = true\nversion = \"0.25.10\"\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies]\nblock2 = \"0.6.2\"\nobjc2 = \"0.6.4\"\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies.objc2-app-kit]\ndefault-features = false\nfeatures = [\"std\", \"block2\", \"NSEvent\", \"NSResponder\"]\nversion = \"0.3.2\"\n\n[target.\"cfg(target_os = \\\"ios\\\")\".dependencies]\nobjc2 = \"0.6.4\"\n\n[target.\"cfg(target_os = \\\"ios\\\")\".dependencies.objc2-foundation]\nfeatures = [\"std\", \"NSThread\"]\nversion = \"0.3.2\"\ndefault-features = false\n\n[target.\"cfg(target_os = \\\"ios\\\")\".dependencies.objc2-ui-kit]\nfeatures = [\"std\", \"objc2-core-foundation\", \"UIApplication\", \"UIGeometry\", \"UIResponder\", \"UIScene\", \"UISceneDefinitions\", \"UIView\", \"UIWindow\", \"UIWindowScene\"]\nversion = \"0.3.2\"\ndefault-features = false\n\n[target.\"cfg(target_os = \\\"windows\\\")\".dependencies.windows-sys]\nfeatures = [\"Win32_Foundation\", \"Win32_Graphics_Dwm\", \"Win32_UI_Input_KeyboardAndMouse\", \"Win32_UI_WindowsAndMessaging\"]\nversion = \"0.61.2\"\n\n[target.\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"dragonfly\\\", target_os=\\\"freebsd\\\", target_os=\\\"netbsd\\\", target_os=\\\"openbsd\\\"))\".dependencies.smithay-clipboard]\noptional = true\nversion = \"0.7.2\"\n\n[target.\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"dragonfly\\\", target_os=\\\"freebsd\\\", target_os=\\\"netbsd\\\", target_os=\\\"openbsd\\\"))\".dependencies.parking_lot]\noptional = true\nversion = \"0.12.5\"\n\n[target.\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"dragonfly\\\", target_os=\\\"freebsd\\\", target_os=\\\"netbsd\\\", target_os=\\\"openbsd\\\"))\".dependencies.wayland-cursor]\noptional = true\nversion = \"0.31.14\"\ndefault-features = false\n\n[target.\"cfg(not(target_os = \\\"android\\\"))\".dependencies.arboard]\noptional = true\nfeatures = [\"image-data\"]\nversion = \"3.6\"\ndefault-features = false\n\n[target.\"cfg(target_os = \\\"linux\\\")\".dev-dependencies.softbuffer]\nversion = \"0.4.8\"\ndefault-features = false\nfeatures = [\"wayland\", \"wayland-dlopen\"]\n\n[[example]]\nname = \"clipboard_image_probe\"\nrequired-features = [\"wayland\", \"clipboard-image\"]\n", + "dest": "cargo/vendor/egui-winit", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui-winit", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_glow\" \"cargo/vendor/egui_glow\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_glow\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Bindings for using egui natively using the glow library\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/egui_glow\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/egui_glow\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"glow\", \"egui\", \"gui\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\", \"src/shader/*.glsl\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[features]\ndefault = []\nclipboard = [\"egui-winit?/clipboard\"]\nlinks = [\"egui-winit?/links\"]\nwinit = [\"egui-winit\", \"dep:winit\"]\nwayland = [\"winit?/wayland\"]\nx11 = [\"winit?/x11\"]\n\n[dependencies]\nbytemuck = \"1.25\"\nglow = \"0.17.0\"\n\n[dependencies.egui]\ndefault-features = false\nfeatures = [\"bytemuck\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.egui-winit]\noptional = true\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui-winit\"\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.winit]\noptional = true\ndefault-features = false\nfeatures = [\"rwh_06\"]\nversion = \"0.30.13\"\n\n[dev-dependencies.glutin]\ndefault-features = false\nversion = \"0.32.3\"\n\n[dev-dependencies.glutin-winit]\ndefault-features = false\nversion = \"0.5.0\"\n\n[[example]]\nname = \"pure_glow\"\nrequired-features = [\"winit\", \"egui/default_fonts\"]\n", + "dest": "cargo/vendor/egui_glow", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_glow", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_kittest\" \"cargo/vendor/egui_kittest\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_kittest\"\nversion = \"0.37.0\"\nauthors = [\"Lucas Meurer \", \"Emil Ernerfeldt \"]\ndescription = \"Testing library for egui based on kittest and AccessKit\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"./README.md\"\nrepository = \"https://github.com/emilk/egui\"\ncategories = [\"gui\", \"development-tools::testing\", \"accessibility\"]\nkeywords = [\"gui\", \"immediate\", \"egui\", \"testing\", \"accesskit\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[features]\nwgpu = [\"dep:egui-wgpu\", \"dep:pollster\", \"dep:image\", \"dep:wgpu\", \"eframe?/wgpu\"]\nsnapshot = [\"dep:dify\", \"dep:image\", \"dep:open\", \"dep:tempfile\", \"image/png\"]\nvideo = [\"snapshot\"]\neframe = [\"dep:eframe\", \"eframe/accesskit\"]\nx11 = [\"eframe?/x11\"]\n\n[dependencies.egui]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[dependencies.eframe]\noptional = true\nversion = \"0.37.0\"\npath = \"crates/eframe\"\ndefault-features = false\n\n[dependencies.kittest]\nversion = \"0.5.0\"\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.serde]\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[dependencies.toml]\nfeatures = [\"parse\", \"serde\"]\nversion = \"1.1\"\ndefault-features = false\n\n[dependencies.egui-wgpu]\noptional = true\nversion = \"0.37.0\"\npath = \"crates/egui-wgpu\"\ndefault-features = false\n\n[dependencies.pollster]\noptional = true\nversion = \"1.0\"\n\n[dependencies.image]\noptional = true\nversion = \"0.25.10\"\ndefault-features = false\n\n[dependencies.wgpu]\nfeatures = [\"metal\", \"dx12\", \"vulkan\", \"gles\", \"std\"]\noptional = true\nversion = \"30.0\"\ndefault-features = false\n\n[dependencies.dify]\noptional = true\nversion = \"0.8.0\"\ndefault-features = false\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.open]\noptional = true\nversion = \"5.4\"\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dependencies.tempfile]\noptional = true\nversion = \"3.27\"\n\n[dev-dependencies]\ntempfile = \"3.27\"\n\n[dev-dependencies.egui]\nfeatures = [\"default_fonts\", \"monochrome_emoji_fonts\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[dev-dependencies.image]\nfeatures = [\"png\"]\nversion = \"0.25.10\"\ndefault-features = false\n\n[dev-dependencies.egui_extras]\nfeatures = [\"image\", \"http\"]\nversion = \"0.37.0\"\npath = \"crates/egui_extras\"\ndefault-features = false\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n", + "dest": "cargo/vendor/egui_kittest", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_kittest", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/either/either-1.18.0.crate", + "sha256": "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34", + "dest": "cargo/vendor/either-1.18.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34\", \"files\": {}}", + "dest": "cargo/vendor/either-1.18.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/elliptic-curve/elliptic-curve-0.13.8.crate", + "sha256": "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47", + "dest": "cargo/vendor/elliptic-curve-0.13.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47\", \"files\": {}}", + "dest": "cargo/vendor/elliptic-curve-0.13.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/emath\" \"cargo/vendor/emath\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"emath\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Minimal 2D math library for GUI work\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/emath\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/emath\"\ncategories = [\"mathematics\", \"gui\"]\nkeywords = [\"math\", \"gui\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = []\n\n[dependencies.bytemuck]\noptional = true\nfeatures = [\"derive\"]\nversion = \"1.25\"\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.mint]\noptional = true\nversion = \"0.5.9\"\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n", + "dest": "cargo/vendor/emath", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/emath", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/endi/endi-1.1.1.crate", + "sha256": "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099", + "dest": "cargo/vendor/endi-1.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099\", \"files\": {}}", + "dest": "cargo/vendor/endi-1.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/enum-as-inner/enum-as-inner-0.6.1.crate", + "sha256": "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc", + "dest": "cargo/vendor/enum-as-inner-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc\", \"files\": {}}", + "dest": "cargo/vendor/enum-as-inner-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/enumflags2/enumflags2-0.7.12.crate", + "sha256": "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef", + "dest": "cargo/vendor/enumflags2-0.7.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef\", \"files\": {}}", + "dest": "cargo/vendor/enumflags2-0.7.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/enumflags2_derive/enumflags2_derive-0.7.12.crate", + "sha256": "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827", + "dest": "cargo/vendor/enumflags2_derive-0.7.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827\", \"files\": {}}", + "dest": "cargo/vendor/enumflags2_derive-0.7.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/epaint\" \"cargo/vendor/epaint\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"epaint\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Minimal 2D graphics library for GUI work\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/epaint\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/epaint\"\ncategories = [\"graphics\", \"gui\"]\nkeywords = [\"graphics\", \"gui\", \"egui\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = []\nbytemuck = [\"dep:bytemuck\", \"emath/bytemuck\", \"ecolor/bytemuck\"]\ncint = [\"ecolor/cint\"]\ncolor-hex = [\"ecolor/color-hex\"]\ncolor_fonts = [\"vello_cpu/png\", \"vello_cpu/text\"]\ndefault_fonts = [\"epaint_default_fonts\"]\nmint = [\"emath/mint\"]\nmonochrome_emoji_fonts = [\"default_fonts\", \"epaint_default_fonts/monochrome_emoji_fonts\"]\nrayon = [\"dep:rayon\"]\nserde = [\"dep:serde\", \"ahash/serde\", \"ecolor/serde\", \"emath/serde\", \"font-types/serde\", \"smallvec/serde\"]\nunity = []\n_override_unity = []\n\n[dependencies]\nharfrust = \"0.12.0\"\nnohash-hasher = \"0.2.0\"\nparking_lot = \"0.12.5\"\nself_cell = \"1.3\"\nsmallvec = \"1.16\"\nunicode-general-category = \"1.1\"\nunicode-segmentation = \"1.13\"\n\n[dependencies.emath]\nversion = \"0.37.0\"\npath = \"crates/emath\"\ndefault-features = false\n\n[dependencies.ecolor]\nversion = \"0.37.0\"\npath = \"crates/ecolor\"\ndefault-features = false\n\n[dependencies.ahash]\nversion = \"0.8.12\"\ndefault-features = false\nfeatures = [\"no-rng\", \"std\"]\n\n[dependencies.font-types]\nversion = \"0.12.4\"\ndefault-features = false\nfeatures = [\"std\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.skrifa]\nversion = \"0.44.0\"\ndefault-features = false\nfeatures = [\"std\", \"autohint_shaping\"]\n\n[dependencies.unicode-bidi]\nversion = \"0.3.18\"\ndefault-features = false\nfeatures = [\"hardcoded-data\", \"std\"]\n\n[dependencies.unicode-properties]\nversion = \"0.1.4\"\ndefault-features = false\nfeatures = [\"emoji\"]\n\n[dependencies.vello_cpu]\nversion = \"0.2.0\"\ndefault-features = false\nfeatures = [\"std\", \"u8_pipeline\", \"f32_pipeline\"]\n\n[dependencies.bytemuck]\noptional = true\nfeatures = [\"derive\"]\nversion = \"1.25\"\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.rayon]\noptional = true\nversion = \"1.12\"\n\n[dependencies.serde]\noptional = true\nfeatures = [\"derive\", \"rc\", \"derive\"]\nversion = \"1.0\"\n\n[dependencies.epaint_default_fonts]\noptional = true\nversion = \"0.37.0\"\npath = \"crates/epaint_default_fonts\"\n\n[dev-dependencies]\nmimalloc = \"0.1.52\"\nsimilar-asserts = \"2.0\"\n\n[dev-dependencies.criterion]\nversion = \"0.8.2\"\ndefault-features = false\n\n[target.\"cfg(not(target_arch = \\\"wasm32\\\"))\".dev-dependencies.vello_cpu]\nfeatures = [\"multithreading\", \"std\", \"u8_pipeline\", \"f32_pipeline\"]\nversion = \"0.2.0\"\ndefault-features = false\n\n[[bench]]\nname = \"benchmark\"\nharness = false\n", + "dest": "cargo/vendor/epaint", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/epaint", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/epaint_default_fonts\" \"cargo/vendor/epaint_default_fonts\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"epaint_default_fonts\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Default fonts for use in epaint / egui\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts\"\nlicense = \"(MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts\"\ncategories = [\"graphics\", \"gui\"]\nkeywords = [\"graphics\", \"gui\", \"egui\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\", \"fonts/*.ttf\", \"fonts/*.txt\", \"fonts/OFL.txt\", \"fonts/UFL.txt\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[features]\ndefault = []\nmonochrome_emoji_fonts = []\n", + "dest": "cargo/vendor/epaint_default_fonts", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/epaint_default_fonts", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equator/equator-0.1.10.crate", + "sha256": "a3b0a88aa91d0ad2b9684e4479aed31a17d3f9051bdbbc634bd2c01bc5a5eee8", + "dest": "cargo/vendor/equator-0.1.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"a3b0a88aa91d0ad2b9684e4479aed31a17d3f9051bdbbc634bd2c01bc5a5eee8\", \"files\": {}}", + "dest": "cargo/vendor/equator-0.1.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equator/equator-0.4.2.crate", + "sha256": "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc", + "dest": "cargo/vendor/equator-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc\", \"files\": {}}", + "dest": "cargo/vendor/equator-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equator-macro/equator-macro-0.1.9.crate", + "sha256": "60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae", + "dest": "cargo/vendor/equator-macro-0.1.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae\", \"files\": {}}", + "dest": "cargo/vendor/equator-macro-0.1.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equator-macro/equator-macro-0.4.2.crate", + "sha256": "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3", + "dest": "cargo/vendor/equator-macro-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3\", \"files\": {}}", + "dest": "cargo/vendor/equator-macro-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equivalent/equivalent-1.0.2.crate", + "sha256": "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f", + "dest": "cargo/vendor/equivalent-1.0.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f\", \"files\": {}}", + "dest": "cargo/vendor/equivalent-1.0.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/errno/errno-0.3.14.crate", + "sha256": "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb", + "dest": "cargo/vendor/errno-0.3.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb\", \"files\": {}}", + "dest": "cargo/vendor/errno-0.3.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/error-code/error-code-3.4.0.crate", + "sha256": "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7", + "dest": "cargo/vendor/error-code-3.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7\", \"files\": {}}", + "dest": "cargo/vendor/error-code-3.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/euclid/euclid-0.22.14.crate", + "sha256": "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06", + "dest": "cargo/vendor/euclid-0.22.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06\", \"files\": {}}", + "dest": "cargo/vendor/euclid-0.22.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/event-listener/event-listener-5.4.2.crate", + "sha256": "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2", + "dest": "cargo/vendor/event-listener-5.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2\", \"files\": {}}", + "dest": "cargo/vendor/event-listener-5.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/event-listener-strategy/event-listener-strategy-0.5.4.crate", + "sha256": "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93", + "dest": "cargo/vendor/event-listener-strategy-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93\", \"files\": {}}", + "dest": "cargo/vendor/event-listener-strategy-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/faer-core/faer-core-0.17.1.crate", + "sha256": "32da0f6262445261106407bdfb548df4f8f1c89bcc3cff1bb6d08e7f525b575a", + "dest": "cargo/vendor/faer-core-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"32da0f6262445261106407bdfb548df4f8f1c89bcc3cff1bb6d08e7f525b575a\", \"files\": {}}", + "dest": "cargo/vendor/faer-core-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/faer-entity/faer-entity-0.17.0.crate", + "sha256": "93fb12f36da3162b3fefc0c7343d905d5bd2c2bf444a7f63ceeaa2bc003311d3", + "dest": "cargo/vendor/faer-entity-0.17.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"93fb12f36da3162b3fefc0c7343d905d5bd2c2bf444a7f63ceeaa2bc003311d3\", \"files\": {}}", + "dest": "cargo/vendor/faer-entity-0.17.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fastrand/fastrand-2.5.0.crate", + "sha256": "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223", + "dest": "cargo/vendor/fastrand-2.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223\", \"files\": {}}", + "dest": "cargo/vendor/fastrand-2.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fax/fax-0.2.7.crate", + "sha256": "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a", + "dest": "cargo/vendor/fax-0.2.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a\", \"files\": {}}", + "dest": "cargo/vendor/fax-0.2.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fdeflate/fdeflate-0.3.7.crate", + "sha256": "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c", + "dest": "cargo/vendor/fdeflate-0.3.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c\", \"files\": {}}", + "dest": "cargo/vendor/fdeflate-0.3.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fearless_simd/fearless_simd-0.4.1.crate", + "sha256": "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693", + "dest": "cargo/vendor/fearless_simd-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693\", \"files\": {}}", + "dest": "cargo/vendor/fearless_simd-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ff/ff-0.13.1.crate", + "sha256": "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393", + "dest": "cargo/vendor/ff-0.13.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393\", \"files\": {}}", + "dest": "cargo/vendor/ff-0.13.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fiat-crypto/fiat-crypto-0.2.9.crate", + "sha256": "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d", + "dest": "cargo/vendor/fiat-crypto-0.2.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d\", \"files\": {}}", + "dest": "cargo/vendor/fiat-crypto-0.2.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/find-msvc-tools/find-msvc-tools-0.1.14.crate", + "sha256": "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484", + "dest": "cargo/vendor/find-msvc-tools-0.1.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484\", \"files\": {}}", + "dest": "cargo/vendor/find-msvc-tools-0.1.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fixedbitset/fixedbitset-0.5.7.crate", + "sha256": "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99", + "dest": "cargo/vendor/fixedbitset-0.5.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99\", \"files\": {}}", + "dest": "cargo/vendor/fixedbitset-0.5.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/flate2/flate2-1.1.10.crate", + "sha256": "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb", + "dest": "cargo/vendor/flate2-1.1.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb\", \"files\": {}}", + "dest": "cargo/vendor/flate2-1.1.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/float8/float8-0.6.1.crate", + "sha256": "719a903cc23e4a89e87962c2a80fdb45cdaad0983a89bd150bb57b4c8571a7d5", + "dest": "cargo/vendor/float8-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"719a903cc23e4a89e87962c2a80fdb45cdaad0983a89bd150bb57b4c8571a7d5\", \"files\": {}}", + "dest": "cargo/vendor/float8-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fnv/fnv-1.0.7.crate", + "sha256": "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1", + "dest": "cargo/vendor/fnv-1.0.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\", \"files\": {}}", + "dest": "cargo/vendor/fnv-1.0.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/foldhash/foldhash-0.1.5.crate", + "sha256": "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2", + "dest": "cargo/vendor/foldhash-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2\", \"files\": {}}", + "dest": "cargo/vendor/foldhash-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/foldhash/foldhash-0.2.0.crate", + "sha256": "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb", + "dest": "cargo/vendor/foldhash-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb\", \"files\": {}}", + "dest": "cargo/vendor/foldhash-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/font-types/font-types-0.12.5.crate", + "sha256": "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6", + "dest": "cargo/vendor/font-types-0.12.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6\", \"files\": {}}", + "dest": "cargo/vendor/font-types-0.12.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/foreign-types/foreign-types-0.5.0.crate", + "sha256": "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965", + "dest": "cargo/vendor/foreign-types-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965\", \"files\": {}}", + "dest": "cargo/vendor/foreign-types-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/foreign-types-macros/foreign-types-macros-0.2.4.crate", + "sha256": "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225", + "dest": "cargo/vendor/foreign-types-macros-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225\", \"files\": {}}", + "dest": "cargo/vendor/foreign-types-macros-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/foreign-types-shared/foreign-types-shared-0.3.1.crate", + "sha256": "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b", + "dest": "cargo/vendor/foreign-types-shared-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b\", \"files\": {}}", + "dest": "cargo/vendor/foreign-types-shared-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-core/futures-core-0.3.34.crate", + "sha256": "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e", + "dest": "cargo/vendor/futures-core-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e\", \"files\": {}}", + "dest": "cargo/vendor/futures-core-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-io/futures-io-0.3.34.crate", + "sha256": "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed", + "dest": "cargo/vendor/futures-io-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed\", \"files\": {}}", + "dest": "cargo/vendor/futures-io-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-lite/futures-lite-2.6.1.crate", + "sha256": "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad", + "dest": "cargo/vendor/futures-lite-2.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad\", \"files\": {}}", + "dest": "cargo/vendor/futures-lite-2.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-macro/futures-macro-0.3.34.crate", + "sha256": "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44", + "dest": "cargo/vendor/futures-macro-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44\", \"files\": {}}", + "dest": "cargo/vendor/futures-macro-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-task/futures-task-0.3.34.crate", + "sha256": "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd", + "dest": "cargo/vendor/futures-task-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd\", \"files\": {}}", + "dest": "cargo/vendor/futures-task-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-util/futures-util-0.3.34.crate", + "sha256": "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc", + "dest": "cargo/vendor/futures-util-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc\", \"files\": {}}", + "dest": "cargo/vendor/futures-util-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm/gemm-0.17.1.crate", + "sha256": "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32", + "dest": "cargo/vendor/gemm-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32\", \"files\": {}}", + "dest": "cargo/vendor/gemm-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm/gemm-0.18.2.crate", + "sha256": "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451", + "dest": "cargo/vendor/gemm-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451\", \"files\": {}}", + "dest": "cargo/vendor/gemm-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm/gemm-0.19.0.crate", + "sha256": "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb", + "dest": "cargo/vendor/gemm-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb\", \"files\": {}}", + "dest": "cargo/vendor/gemm-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c32/gemm-c32-0.17.1.crate", + "sha256": "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0", + "dest": "cargo/vendor/gemm-c32-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c32-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c32/gemm-c32-0.18.2.crate", + "sha256": "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847", + "dest": "cargo/vendor/gemm-c32-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c32-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c32/gemm-c32-0.19.0.crate", + "sha256": "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c", + "dest": "cargo/vendor/gemm-c32-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c32-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c64/gemm-c64-0.17.1.crate", + "sha256": "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a", + "dest": "cargo/vendor/gemm-c64-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c64-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c64/gemm-c64-0.18.2.crate", + "sha256": "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf", + "dest": "cargo/vendor/gemm-c64-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c64-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-c64/gemm-c64-0.19.0.crate", + "sha256": "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f", + "dest": "cargo/vendor/gemm-c64-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f\", \"files\": {}}", + "dest": "cargo/vendor/gemm-c64-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-common/gemm-common-0.17.1.crate", + "sha256": "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8", + "dest": "cargo/vendor/gemm-common-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8\", \"files\": {}}", + "dest": "cargo/vendor/gemm-common-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-common/gemm-common-0.18.2.crate", + "sha256": "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3", + "dest": "cargo/vendor/gemm-common-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3\", \"files\": {}}", + "dest": "cargo/vendor/gemm-common-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-common/gemm-common-0.19.0.crate", + "sha256": "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e", + "dest": "cargo/vendor/gemm-common-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e\", \"files\": {}}", + "dest": "cargo/vendor/gemm-common-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f16/gemm-f16-0.17.1.crate", + "sha256": "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4", + "dest": "cargo/vendor/gemm-f16-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f16-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f16/gemm-f16-0.18.2.crate", + "sha256": "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109", + "dest": "cargo/vendor/gemm-f16-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f16-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f16/gemm-f16-0.19.0.crate", + "sha256": "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e", + "dest": "cargo/vendor/gemm-f16-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f16-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f32/gemm-f32-0.17.1.crate", + "sha256": "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113", + "dest": "cargo/vendor/gemm-f32-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f32-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f32/gemm-f32-0.18.2.crate", + "sha256": "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864", + "dest": "cargo/vendor/gemm-f32-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f32-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f32/gemm-f32-0.19.0.crate", + "sha256": "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c", + "dest": "cargo/vendor/gemm-f32-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f32-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f64/gemm-f64-0.17.1.crate", + "sha256": "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0", + "dest": "cargo/vendor/gemm-f64-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f64-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f64/gemm-f64-0.18.2.crate", + "sha256": "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd", + "dest": "cargo/vendor/gemm-f64-0.18.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f64-0.18.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gemm-f64/gemm-f64-0.19.0.crate", + "sha256": "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a", + "dest": "cargo/vendor/gemm-f64-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a\", \"files\": {}}", + "dest": "cargo/vendor/gemm-f64-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/generic-array/generic-array-0.14.7.crate", + "sha256": "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a", + "dest": "cargo/vendor/generic-array-0.14.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a\", \"files\": {}}", + "dest": "cargo/vendor/generic-array-0.14.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gethostname/gethostname-1.1.0.crate", + "sha256": "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8", + "dest": "cargo/vendor/gethostname-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8\", \"files\": {}}", + "dest": "cargo/vendor/gethostname-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getopts/getopts-0.2.24.crate", + "sha256": "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df", + "dest": "cargo/vendor/getopts-0.2.24" + }, + { + "type": "inline", + "contents": "{\"package\": \"cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df\", \"files\": {}}", + "dest": "cargo/vendor/getopts-0.2.24", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getrandom/getrandom-0.2.17.crate", + "sha256": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0", + "dest": "cargo/vendor/getrandom-0.2.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0\", \"files\": {}}", + "dest": "cargo/vendor/getrandom-0.2.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getrandom/getrandom-0.3.4.crate", + "sha256": "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd", + "dest": "cargo/vendor/getrandom-0.3.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd\", \"files\": {}}", + "dest": "cargo/vendor/getrandom-0.3.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getrandom/getrandom-0.4.3.crate", + "sha256": "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099", + "dest": "cargo/vendor/getrandom-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099\", \"files\": {}}", + "dest": "cargo/vendor/getrandom-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ghash/ghash-0.5.1.crate", + "sha256": "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1", + "dest": "cargo/vendor/ghash-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1\", \"files\": {}}", + "dest": "cargo/vendor/ghash-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gif/gif-0.14.2.crate", + "sha256": "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159", + "dest": "cargo/vendor/gif-0.14.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159\", \"files\": {}}", + "dest": "cargo/vendor/gif-0.14.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gimli/gimli-0.32.3.crate", + "sha256": "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7", + "dest": "cargo/vendor/gimli-0.32.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7\", \"files\": {}}", + "dest": "cargo/vendor/gimli-0.32.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gl_generator/gl_generator-0.14.0.crate", + "sha256": "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d", + "dest": "cargo/vendor/gl_generator-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d\", \"files\": {}}", + "dest": "cargo/vendor/gl_generator-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glifo/glifo-0.3.0.crate", + "sha256": "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be", + "dest": "cargo/vendor/glifo-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be\", \"files\": {}}", + "dest": "cargo/vendor/glifo-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glow/glow-0.17.0.crate", + "sha256": "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5", + "dest": "cargo/vendor/glow-0.17.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5\", \"files\": {}}", + "dest": "cargo/vendor/glow-0.17.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glutin/glutin-0.32.3.crate", + "sha256": "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325", + "dest": "cargo/vendor/glutin-0.32.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325\", \"files\": {}}", + "dest": "cargo/vendor/glutin-0.32.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glutin-winit/glutin-winit-0.5.0.crate", + "sha256": "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f", + "dest": "cargo/vendor/glutin-winit-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f\", \"files\": {}}", + "dest": "cargo/vendor/glutin-winit-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glutin_egl_sys/glutin_egl_sys-0.7.1.crate", + "sha256": "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2", + "dest": "cargo/vendor/glutin_egl_sys-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2\", \"files\": {}}", + "dest": "cargo/vendor/glutin_egl_sys-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glutin_glx_sys/glutin_glx_sys-0.6.1.crate", + "sha256": "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185", + "dest": "cargo/vendor/glutin_glx_sys-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185\", \"files\": {}}", + "dest": "cargo/vendor/glutin_glx_sys-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glutin_wgl_sys/glutin_wgl_sys-0.6.1.crate", + "sha256": "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e", + "dest": "cargo/vendor/glutin_wgl_sys-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e\", \"files\": {}}", + "dest": "cargo/vendor/glutin_wgl_sys-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gpu-allocator/gpu-allocator-0.28.0.crate", + "sha256": "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795", + "dest": "cargo/vendor/gpu-allocator-0.28.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795\", \"files\": {}}", + "dest": "cargo/vendor/gpu-allocator-0.28.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/group/group-0.13.0.crate", + "sha256": "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63", + "dest": "cargo/vendor/group-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63\", \"files\": {}}", + "dest": "cargo/vendor/group-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/guillotiere/guillotiere-0.7.0.crate", + "sha256": "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab", + "dest": "cargo/vendor/guillotiere-0.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab\", \"files\": {}}", + "dest": "cargo/vendor/guillotiere-0.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/half/half-2.7.1.crate", + "sha256": "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b", + "dest": "cargo/vendor/half-2.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b\", \"files\": {}}", + "dest": "cargo/vendor/half-2.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/harfrust/harfrust-0.12.0.crate", + "sha256": "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9", + "dest": "cargo/vendor/harfrust-0.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9\", \"files\": {}}", + "dest": "cargo/vendor/harfrust-0.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.15.5.crate", + "sha256": "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1", + "dest": "cargo/vendor/hashbrown-0.15.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1\", \"files\": {}}", + "dest": "cargo/vendor/hashbrown-0.15.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.16.1.crate", + "sha256": "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100", + "dest": "cargo/vendor/hashbrown-0.16.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100\", \"files\": {}}", + "dest": "cargo/vendor/hashbrown-0.16.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.17.1.crate", + "sha256": "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a", + "dest": "cargo/vendor/hashbrown-0.17.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a\", \"files\": {}}", + "dest": "cargo/vendor/hashbrown-0.17.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/heck/heck-0.5.0.crate", + "sha256": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea", + "dest": "cargo/vendor/heck-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\", \"files\": {}}", + "dest": "cargo/vendor/heck-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/heic-rs/heic-rs-0.1.4.crate", + "sha256": "8883c3bfa6b4ff19a40e52a6dc051c5612cbe21bf408fc6d4875e8dfd777cb66", + "dest": "cargo/vendor/heic-rs-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"8883c3bfa6b4ff19a40e52a6dc051c5612cbe21bf408fc6d4875e8dfd777cb66\", \"files\": {}}", + "dest": "cargo/vendor/heic-rs-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hermit-abi/hermit-abi-0.5.3.crate", + "sha256": "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284", + "dest": "cargo/vendor/hermit-abi-0.5.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284\", \"files\": {}}", + "dest": "cargo/vendor/hermit-abi-0.5.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hex/hex-0.4.3.crate", + "sha256": "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70", + "dest": "cargo/vendor/hex-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\", \"files\": {}}", + "dest": "cargo/vendor/hex-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hkdf/hkdf-0.12.4.crate", + "sha256": "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7", + "dest": "cargo/vendor/hkdf-0.12.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7\", \"files\": {}}", + "dest": "cargo/vendor/hkdf-0.12.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hmac/hmac-0.12.1.crate", + "sha256": "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e", + "dest": "cargo/vendor/hmac-0.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e\", \"files\": {}}", + "dest": "cargo/vendor/hmac-0.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/image/image-0.25.10.crate", + "sha256": "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104", + "dest": "cargo/vendor/image-0.25.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104\", \"files\": {}}", + "dest": "cargo/vendor/image-0.25.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/image-webp/image-webp-0.2.4.crate", + "sha256": "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3", + "dest": "cargo/vendor/image-webp-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3\", \"files\": {}}", + "dest": "cargo/vendor/image-webp-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/imgref/imgref-1.12.3.crate", + "sha256": "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f", + "dest": "cargo/vendor/imgref-1.12.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f\", \"files\": {}}", + "dest": "cargo/vendor/imgref-1.12.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/indexmap/indexmap-2.14.2.crate", + "sha256": "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855", + "dest": "cargo/vendor/indexmap-2.14.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855\", \"files\": {}}", + "dest": "cargo/vendor/indexmap-2.14.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/inout/inout-0.1.4.crate", + "sha256": "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01", + "dest": "cargo/vendor/inout-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01\", \"files\": {}}", + "dest": "cargo/vendor/inout-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/interpolate_name/interpolate_name-0.2.4.crate", + "sha256": "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60", + "dest": "cargo/vendor/interpolate_name-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60\", \"files\": {}}", + "dest": "cargo/vendor/interpolate_name-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is-docker/is-docker-0.2.0.crate", + "sha256": "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3", + "dest": "cargo/vendor/is-docker-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3\", \"files\": {}}", + "dest": "cargo/vendor/is-docker-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is-wsl/is-wsl-0.4.0.crate", + "sha256": "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5", + "dest": "cargo/vendor/is-wsl-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5\", \"files\": {}}", + "dest": "cargo/vendor/is-wsl-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itertools/itertools-0.14.0.crate", + "sha256": "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285", + "dest": "cargo/vendor/itertools-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285\", \"files\": {}}", + "dest": "cargo/vendor/itertools-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itertools/itertools-0.15.0.crate", + "sha256": "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc", + "dest": "cargo/vendor/itertools-0.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc\", \"files\": {}}", + "dest": "cargo/vendor/itertools-0.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itoa/itoa-1.0.18.crate", + "sha256": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682", + "dest": "cargo/vendor/itoa-1.0.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682\", \"files\": {}}", + "dest": "cargo/vendor/itoa-1.0.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jixel/jixel-0.3.4.crate", + "sha256": "278a75e12caacec34be774d62ad05e45fc0b6a52d1f4e9f7fbc0209ecdf794f3", + "dest": "cargo/vendor/jixel-0.3.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"278a75e12caacec34be774d62ad05e45fc0b6a52d1f4e9f7fbc0209ecdf794f3\", \"files\": {}}", + "dest": "cargo/vendor/jixel-0.3.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni/jni-0.22.4.crate", + "sha256": "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498", + "dest": "cargo/vendor/jni-0.22.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498\", \"files\": {}}", + "dest": "cargo/vendor/jni-0.22.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni-macros/jni-macros-0.22.4.crate", + "sha256": "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3", + "dest": "cargo/vendor/jni-macros-0.22.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3\", \"files\": {}}", + "dest": "cargo/vendor/jni-macros-0.22.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni-sys/jni-sys-0.3.1.crate", + "sha256": "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258", + "dest": "cargo/vendor/jni-sys-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258\", \"files\": {}}", + "dest": "cargo/vendor/jni-sys-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni-sys/jni-sys-0.4.1.crate", + "sha256": "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2", + "dest": "cargo/vendor/jni-sys-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2\", \"files\": {}}", + "dest": "cargo/vendor/jni-sys-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni-sys-macros/jni-sys-macros-0.4.1.crate", + "sha256": "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264", + "dest": "cargo/vendor/jni-sys-macros-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264\", \"files\": {}}", + "dest": "cargo/vendor/jni-sys-macros-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jobserver/jobserver-0.1.35.crate", + "sha256": "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3", + "dest": "cargo/vendor/jobserver-0.1.35" + }, + { + "type": "inline", + "contents": "{\"package\": \"1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3\", \"files\": {}}", + "dest": "cargo/vendor/jobserver-0.1.35", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jpeg-decoder/jpeg-decoder-0.3.2.crate", + "sha256": "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07", + "dest": "cargo/vendor/jpeg-decoder-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07\", \"files\": {}}", + "dest": "cargo/vendor/jpeg-decoder-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jpeg-encoder/jpeg-encoder-0.7.1.crate", + "sha256": "a0370574b86f7eca156b9f298392b5e69a23f8c86f3f865add60bbc2e79467a6", + "dest": "cargo/vendor/jpeg-encoder-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a0370574b86f7eca156b9f298392b5e69a23f8c86f3f865add60bbc2e79467a6\", \"files\": {}}", + "dest": "cargo/vendor/jpeg-encoder-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/js-sys/js-sys-0.3.106.crate", + "sha256": "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5", + "dest": "cargo/vendor/js-sys-0.3.106" + }, + { + "type": "inline", + "contents": "{\"package\": \"7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5\", \"files\": {}}", + "dest": "cargo/vendor/js-sys-0.3.106", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-bitstream/jxl-bitstream-1.1.0.crate", + "sha256": "b480e752277e29eb4054f69546887a9b84656fe78c08f54ba5850ced98a378fe", + "dest": "cargo/vendor/jxl-bitstream-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b480e752277e29eb4054f69546887a9b84656fe78c08f54ba5850ced98a378fe\", \"files\": {}}", + "dest": "cargo/vendor/jxl-bitstream-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-coding/jxl-coding-1.0.1.crate", + "sha256": "cd972bcd125e776f1eb241ac50e39f956095a1c2770c64736c968f8946bd9a3c", + "dest": "cargo/vendor/jxl-coding-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"cd972bcd125e776f1eb241ac50e39f956095a1c2770c64736c968f8946bd9a3c\", \"files\": {}}", + "dest": "cargo/vendor/jxl-coding-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-color/jxl-color-0.11.0.crate", + "sha256": "f316b1358c1711755b3ee8e8cb5c4a1dad12e796233088a7a513440782de80b2", + "dest": "cargo/vendor/jxl-color-0.11.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f316b1358c1711755b3ee8e8cb5c4a1dad12e796233088a7a513440782de80b2\", \"files\": {}}", + "dest": "cargo/vendor/jxl-color-0.11.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-frame/jxl-frame-0.13.3.crate", + "sha256": "2d967c6fd669c7c01060b5022d8835fa82fd46b06ffc98b549f17600a097c2b3", + "dest": "cargo/vendor/jxl-frame-0.13.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"2d967c6fd669c7c01060b5022d8835fa82fd46b06ffc98b549f17600a097c2b3\", \"files\": {}}", + "dest": "cargo/vendor/jxl-frame-0.13.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-grid/jxl-grid-0.6.2.crate", + "sha256": "01671307879a033bfa52e6e8784b941aca770b3f3a7d33830b455b6844f793fb", + "dest": "cargo/vendor/jxl-grid-0.6.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"01671307879a033bfa52e6e8784b941aca770b3f3a7d33830b455b6844f793fb\", \"files\": {}}", + "dest": "cargo/vendor/jxl-grid-0.6.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-image/jxl-image-0.13.0.crate", + "sha256": "c5f752d62577c702a94dbbce4045caf08cb58639e8a4d56464b40ecf33ffe565", + "dest": "cargo/vendor/jxl-image-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c5f752d62577c702a94dbbce4045caf08cb58639e8a4d56464b40ecf33ffe565\", \"files\": {}}", + "dest": "cargo/vendor/jxl-image-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-jbr/jxl-jbr-0.2.1.crate", + "sha256": "e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46", + "dest": "cargo/vendor/jxl-jbr-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46\", \"files\": {}}", + "dest": "cargo/vendor/jxl-jbr-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-modular/jxl-modular-0.11.3.crate", + "sha256": "2a2f045b24c738dd91d482be385512b512721ae08a671bd4b27bf1c47f215235", + "dest": "cargo/vendor/jxl-modular-0.11.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"2a2f045b24c738dd91d482be385512b512721ae08a671bd4b27bf1c47f215235\", \"files\": {}}", + "dest": "cargo/vendor/jxl-modular-0.11.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-oxide/jxl-oxide-0.12.6.crate", + "sha256": "d36c662923f47586880211f3bc7c0d83fb3a9b410d278c7bde93450748abeef3", + "dest": "cargo/vendor/jxl-oxide-0.12.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"d36c662923f47586880211f3bc7c0d83fb3a9b410d278c7bde93450748abeef3\", \"files\": {}}", + "dest": "cargo/vendor/jxl-oxide-0.12.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-oxide-common/jxl-oxide-common-1.0.0.crate", + "sha256": "b62394c5021b3a9e7e0dbb2d639d555d019090c9946c39f6d3b09d390db4157b", + "dest": "cargo/vendor/jxl-oxide-common-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b62394c5021b3a9e7e0dbb2d639d555d019090c9946c39f6d3b09d390db4157b\", \"files\": {}}", + "dest": "cargo/vendor/jxl-oxide-common-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-render/jxl-render-0.12.4.crate", + "sha256": "d34386bfdb6a19b5a30cc9beb4d475d537422c31ae8c39bb69640fcce3fcaf19", + "dest": "cargo/vendor/jxl-render-0.12.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"d34386bfdb6a19b5a30cc9beb4d475d537422c31ae8c39bb69640fcce3fcaf19\", \"files\": {}}", + "dest": "cargo/vendor/jxl-render-0.12.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-threadpool/jxl-threadpool-1.0.0.crate", + "sha256": "25f15eb830aa77a7f21148d72e153562a26bfe570139bd4922eab1908dd499d3", + "dest": "cargo/vendor/jxl-threadpool-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"25f15eb830aa77a7f21148d72e153562a26bfe570139bd4922eab1908dd499d3\", \"files\": {}}", + "dest": "cargo/vendor/jxl-threadpool-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jxl-vardct/jxl-vardct-0.11.1.crate", + "sha256": "ce72a18c6d3a47172ab6c479be2bdb56f22066b5d7092663f03b4490820b4511", + "dest": "cargo/vendor/jxl-vardct-0.11.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ce72a18c6d3a47172ab6c479be2bdb56f22066b5d7092663f03b4490820b4511\", \"files\": {}}", + "dest": "cargo/vendor/jxl-vardct-0.11.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/keyboard-types/keyboard-types-0.8.3.crate", + "sha256": "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d", + "dest": "cargo/vendor/keyboard-types-0.8.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d\", \"files\": {}}", + "dest": "cargo/vendor/keyboard-types-0.8.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/khronos-egl/khronos-egl-6.0.0.crate", + "sha256": "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76", + "dest": "cargo/vendor/khronos-egl-6.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76\", \"files\": {}}", + "dest": "cargo/vendor/khronos-egl-6.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/khronos_api/khronos_api-3.1.0.crate", + "sha256": "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc", + "dest": "cargo/vendor/khronos_api-3.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc\", \"files\": {}}", + "dest": "cargo/vendor/khronos_api-3.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/kittest/kittest-0.5.0.crate", + "sha256": "0bb81a721ba76aa095b7f45deb180504f786904e56ca4b8e141f66eb89a78b75", + "dest": "cargo/vendor/kittest-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0bb81a721ba76aa095b7f45deb180504f786904e56ca4b8e141f66eb89a78b75\", \"files\": {}}", + "dest": "cargo/vendor/kittest-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/kurbo/kurbo-0.13.1.crate", + "sha256": "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2", + "dest": "cargo/vendor/kurbo-0.13.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2\", \"files\": {}}", + "dest": "cargo/vendor/kurbo-0.13.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/lazy_static/lazy_static-1.5.1.crate", + "sha256": "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb", + "dest": "cargo/vendor/lazy_static-1.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb\", \"files\": {}}", + "dest": "cargo/vendor/lazy_static-1.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libc/libc-0.2.189.crate", + "sha256": "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2", + "dest": "cargo/vendor/libc-0.2.189" + }, + { + "type": "inline", + "contents": "{\"package\": \"3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2\", \"files\": {}}", + "dest": "cargo/vendor/libc-0.2.189", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libfuzzer-sys/libfuzzer-sys-0.4.13.crate", + "sha256": "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2", + "dest": "cargo/vendor/libfuzzer-sys-0.4.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2\", \"files\": {}}", + "dest": "cargo/vendor/libfuzzer-sys-0.4.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libloading/libloading-0.8.9.crate", + "sha256": "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55", + "dest": "cargo/vendor/libloading-0.8.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55\", \"files\": {}}", + "dest": "cargo/vendor/libloading-0.8.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libm/libm-0.2.16.crate", + "sha256": "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981", + "dest": "cargo/vendor/libm-0.2.16" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981\", \"files\": {}}", + "dest": "cargo/vendor/libm-0.2.16", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libredox/libredox-0.1.25.crate", + "sha256": "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d", + "dest": "cargo/vendor/libredox-0.1.25" + }, + { + "type": "inline", + "contents": "{\"package\": \"61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d\", \"files\": {}}", + "dest": "cargo/vendor/libredox-0.1.25", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/linebender_resource_handle/linebender_resource_handle-0.1.1.crate", + "sha256": "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4", + "dest": "cargo/vendor/linebender_resource_handle-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4\", \"files\": {}}", + "dest": "cargo/vendor/linebender_resource_handle-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.4.15.crate", + "sha256": "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab", + "dest": "cargo/vendor/linux-raw-sys-0.4.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab\", \"files\": {}}", + "dest": "cargo/vendor/linux-raw-sys-0.4.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.12.1.crate", + "sha256": "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53", + "dest": "cargo/vendor/linux-raw-sys-0.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53\", \"files\": {}}", + "dest": "cargo/vendor/linux-raw-sys-0.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/litrs/litrs-1.0.0.crate", + "sha256": "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092", + "dest": "cargo/vendor/litrs-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092\", \"files\": {}}", + "dest": "cargo/vendor/litrs-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/lock_api/lock_api-0.4.14.crate", + "sha256": "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965", + "dest": "cargo/vendor/lock_api-0.4.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965\", \"files\": {}}", + "dest": "cargo/vendor/lock_api-0.4.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/log/log-0.4.34.crate", + "sha256": "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6", + "dest": "cargo/vendor/log-0.4.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6\", \"files\": {}}", + "dest": "cargo/vendor/log-0.4.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/loop9/loop9-0.1.5.crate", + "sha256": "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062", + "dest": "cargo/vendor/loop9-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062\", \"files\": {}}", + "dest": "cargo/vendor/loop9-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/malloc_buf/malloc_buf-0.0.6.crate", + "sha256": "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb", + "dest": "cargo/vendor/malloc_buf-0.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb\", \"files\": {}}", + "dest": "cargo/vendor/malloc_buf-0.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/matrixcompare-core/matrixcompare-core-0.1.0.crate", + "sha256": "b0bdabb30db18805d5290b3da7ceaccbddba795620b86c02145d688e04900a73", + "dest": "cargo/vendor/matrixcompare-core-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b0bdabb30db18805d5290b3da7ceaccbddba795620b86c02145d688e04900a73\", \"files\": {}}", + "dest": "cargo/vendor/matrixcompare-core-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/matrixmultiply/matrixmultiply-0.3.11.crate", + "sha256": "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7", + "dest": "cargo/vendor/matrixmultiply-0.3.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7\", \"files\": {}}", + "dest": "cargo/vendor/matrixmultiply-0.3.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/maybe-rayon/maybe-rayon-0.1.1.crate", + "sha256": "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519", + "dest": "cargo/vendor/maybe-rayon-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519\", \"files\": {}}", + "dest": "cargo/vendor/maybe-rayon-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memchr/memchr-2.8.3.crate", + "sha256": "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98", + "dest": "cargo/vendor/memchr-2.8.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98\", \"files\": {}}", + "dest": "cargo/vendor/memchr-2.8.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memmap2/memmap2-0.9.11.crate", + "sha256": "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0", + "dest": "cargo/vendor/memmap2-0.9.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0\", \"files\": {}}", + "dest": "cargo/vendor/memmap2-0.9.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memoffset/memoffset-0.9.1.crate", + "sha256": "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a", + "dest": "cargo/vendor/memoffset-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a\", \"files\": {}}", + "dest": "cargo/vendor/memoffset-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/metal/metal-0.29.0.crate", + "sha256": "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21", + "dest": "cargo/vendor/metal-0.29.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21\", \"files\": {}}", + "dest": "cargo/vendor/metal-0.29.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.8.9.crate", + "sha256": "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316", + "dest": "cargo/vendor/miniz_oxide-0.8.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316\", \"files\": {}}", + "dest": "cargo/vendor/miniz_oxide-0.8.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.9.1.crate", + "sha256": "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c", + "dest": "cargo/vendor/miniz_oxide-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c\", \"files\": {}}", + "dest": "cargo/vendor/miniz_oxide-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/mintex/mintex-0.1.4.crate", + "sha256": "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536", + "dest": "cargo/vendor/mintex-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536\", \"files\": {}}", + "dest": "cargo/vendor/mintex-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/moxcms/moxcms-0.8.1.crate", + "sha256": "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b", + "dest": "cargo/vendor/moxcms-0.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b\", \"files\": {}}", + "dest": "cargo/vendor/moxcms-0.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/moxcms/moxcms-0.9.1.crate", + "sha256": "396077b717181ce3f9a6b66739b269b82d756be5ec0f76be0bc40a7c51775830", + "dest": "cargo/vendor/moxcms-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"396077b717181ce3f9a6b66739b269b82d756be5ec0f76be0bc40a7c51775830\", \"files\": {}}", + "dest": "cargo/vendor/moxcms-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/muda/muda-0.21.0.crate", + "sha256": "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c", + "dest": "cargo/vendor/muda-0.21.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c\", \"files\": {}}", + "dest": "cargo/vendor/muda-0.21.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/naga/naga-30.0.1.crate", + "sha256": "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130", + "dest": "cargo/vendor/naga-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130\", \"files\": {}}", + "dest": "cargo/vendor/naga-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/naga-types/naga-types-30.0.1.crate", + "sha256": "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49", + "dest": "cargo/vendor/naga-types-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49\", \"files\": {}}", + "dest": "cargo/vendor/naga-types-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndarray/ndarray-0.17.2.crate", + "sha256": "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d", + "dest": "cargo/vendor/ndarray-0.17.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d\", \"files\": {}}", + "dest": "cargo/vendor/ndarray-0.17.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk/ndk-0.9.0.crate", + "sha256": "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4", + "dest": "cargo/vendor/ndk-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4\", \"files\": {}}", + "dest": "cargo/vendor/ndk-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk-context/ndk-context-0.1.1.crate", + "sha256": "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b", + "dest": "cargo/vendor/ndk-context-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b\", \"files\": {}}", + "dest": "cargo/vendor/ndk-context-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk-sys/ndk-sys-0.6.0+11769913.crate", + "sha256": "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873", + "dest": "cargo/vendor/ndk-sys-0.6.0+11769913" + }, + { + "type": "inline", + "contents": "{\"package\": \"ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873\", \"files\": {}}", + "dest": "cargo/vendor/ndk-sys-0.6.0+11769913", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/new_debug_unreachable/new_debug_unreachable-1.0.6.crate", + "sha256": "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086", + "dest": "cargo/vendor/new_debug_unreachable-1.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086\", \"files\": {}}", + "dest": "cargo/vendor/new_debug_unreachable-1.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/no_std_io2/no_std_io2-0.9.4.crate", + "sha256": "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003", + "dest": "cargo/vendor/no_std_io2-0.9.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003\", \"files\": {}}", + "dest": "cargo/vendor/no_std_io2-0.9.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/nohash-hasher/nohash-hasher-0.2.0.crate", + "sha256": "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451", + "dest": "cargo/vendor/nohash-hasher-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451\", \"files\": {}}", + "dest": "cargo/vendor/nohash-hasher-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/nom/nom-8.0.0.crate", + "sha256": "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405", + "dest": "cargo/vendor/nom-8.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405\", \"files\": {}}", + "dest": "cargo/vendor/nom-8.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/noop_proc_macro/noop_proc_macro-0.3.0.crate", + "sha256": "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8", + "dest": "cargo/vendor/noop_proc_macro-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8\", \"files\": {}}", + "dest": "cargo/vendor/noop_proc_macro-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num/num-0.4.3.crate", + "sha256": "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23", + "dest": "cargo/vendor/num-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23\", \"files\": {}}", + "dest": "cargo/vendor/num-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-bigint/num-bigint-0.4.8.crate", + "sha256": "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367", + "dest": "cargo/vendor/num-bigint-0.4.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367\", \"files\": {}}", + "dest": "cargo/vendor/num-bigint-0.4.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-bigint-dig/num-bigint-dig-0.8.6.crate", + "sha256": "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7", + "dest": "cargo/vendor/num-bigint-dig-0.8.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7\", \"files\": {}}", + "dest": "cargo/vendor/num-bigint-dig-0.8.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-complex/num-complex-0.4.6.crate", + "sha256": "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495", + "dest": "cargo/vendor/num-complex-0.4.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495\", \"files\": {}}", + "dest": "cargo/vendor/num-complex-0.4.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-derive/num-derive-0.4.2.crate", + "sha256": "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202", + "dest": "cargo/vendor/num-derive-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202\", \"files\": {}}", + "dest": "cargo/vendor/num-derive-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-integer/num-integer-0.1.47.crate", + "sha256": "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b", + "dest": "cargo/vendor/num-integer-0.1.47" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b\", \"files\": {}}", + "dest": "cargo/vendor/num-integer-0.1.47", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-iter/num-iter-0.1.46.crate", + "sha256": "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b", + "dest": "cargo/vendor/num-iter-0.1.46" + }, + { + "type": "inline", + "contents": "{\"package\": \"c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b\", \"files\": {}}", + "dest": "cargo/vendor/num-iter-0.1.46", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-rational/num-rational-0.4.2.crate", + "sha256": "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824", + "dest": "cargo/vendor/num-rational-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824\", \"files\": {}}", + "dest": "cargo/vendor/num-rational-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-traits/num-traits-0.2.19.crate", + "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841", + "dest": "cargo/vendor/num-traits-0.2.19" + }, + { + "type": "inline", + "contents": "{\"package\": \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\", \"files\": {}}", + "dest": "cargo/vendor/num-traits-0.2.19", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_cpus/num_cpus-1.17.0.crate", + "sha256": "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b", + "dest": "cargo/vendor/num_cpus-1.17.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b\", \"files\": {}}", + "dest": "cargo/vendor/num_cpus-1.17.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_enum/num_enum-0.7.6.crate", + "sha256": "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26", + "dest": "cargo/vendor/num_enum-0.7.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26\", \"files\": {}}", + "dest": "cargo/vendor/num_enum-0.7.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_enum_derive/num_enum_derive-0.7.6.crate", + "sha256": "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8", + "dest": "cargo/vendor/num_enum_derive-0.7.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8\", \"files\": {}}", + "dest": "cargo/vendor/num_enum_derive-0.7.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc/objc-0.2.7.crate", + "sha256": "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1", + "dest": "cargo/vendor/objc-0.2.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1\", \"files\": {}}", + "dest": "cargo/vendor/objc-0.2.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc-sys/objc-sys-0.3.5.crate", + "sha256": "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310", + "dest": "cargo/vendor/objc-sys-0.3.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310\", \"files\": {}}", + "dest": "cargo/vendor/objc-sys-0.3.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2/objc2-0.5.2.crate", + "sha256": "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804", + "dest": "cargo/vendor/objc2-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804\", \"files\": {}}", + "dest": "cargo/vendor/objc2-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2/objc2-0.6.4.crate", + "sha256": "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f", + "dest": "cargo/vendor/objc2-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f\", \"files\": {}}", + "dest": "cargo/vendor/objc2-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-app-kit/objc2-app-kit-0.2.2.crate", + "sha256": "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff", + "dest": "cargo/vendor/objc2-app-kit-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff\", \"files\": {}}", + "dest": "cargo/vendor/objc2-app-kit-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-app-kit/objc2-app-kit-0.3.2.crate", + "sha256": "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c", + "dest": "cargo/vendor/objc2-app-kit-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c\", \"files\": {}}", + "dest": "cargo/vendor/objc2-app-kit-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-cloud-kit/objc2-cloud-kit-0.2.2.crate", + "sha256": "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009", + "dest": "cargo/vendor/objc2-cloud-kit-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009\", \"files\": {}}", + "dest": "cargo/vendor/objc2-cloud-kit-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-contacts/objc2-contacts-0.2.2.crate", + "sha256": "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889", + "dest": "cargo/vendor/objc2-contacts-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889\", \"files\": {}}", + "dest": "cargo/vendor/objc2-contacts-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-core-data/objc2-core-data-0.2.2.crate", + "sha256": "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef", + "dest": "cargo/vendor/objc2-core-data-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef\", \"files\": {}}", + "dest": "cargo/vendor/objc2-core-data-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-core-foundation/objc2-core-foundation-0.3.2.crate", + "sha256": "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536", + "dest": "cargo/vendor/objc2-core-foundation-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536\", \"files\": {}}", + "dest": "cargo/vendor/objc2-core-foundation-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-core-graphics/objc2-core-graphics-0.3.2.crate", + "sha256": "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807", + "dest": "cargo/vendor/objc2-core-graphics-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807\", \"files\": {}}", + "dest": "cargo/vendor/objc2-core-graphics-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-core-image/objc2-core-image-0.2.2.crate", + "sha256": "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80", + "dest": "cargo/vendor/objc2-core-image-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80\", \"files\": {}}", + "dest": "cargo/vendor/objc2-core-image-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-core-location/objc2-core-location-0.2.2.crate", + "sha256": "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781", + "dest": "cargo/vendor/objc2-core-location-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781\", \"files\": {}}", + "dest": "cargo/vendor/objc2-core-location-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-encode/objc2-encode-4.1.0.crate", + "sha256": "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33", + "dest": "cargo/vendor/objc2-encode-4.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33\", \"files\": {}}", + "dest": "cargo/vendor/objc2-encode-4.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-foundation/objc2-foundation-0.2.2.crate", + "sha256": "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8", + "dest": "cargo/vendor/objc2-foundation-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8\", \"files\": {}}", + "dest": "cargo/vendor/objc2-foundation-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-foundation/objc2-foundation-0.3.2.crate", + "sha256": "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272", + "dest": "cargo/vendor/objc2-foundation-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272\", \"files\": {}}", + "dest": "cargo/vendor/objc2-foundation-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-io-surface/objc2-io-surface-0.3.2.crate", + "sha256": "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d", + "dest": "cargo/vendor/objc2-io-surface-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d\", \"files\": {}}", + "dest": "cargo/vendor/objc2-io-surface-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-link-presentation/objc2-link-presentation-0.2.2.crate", + "sha256": "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398", + "dest": "cargo/vendor/objc2-link-presentation-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398\", \"files\": {}}", + "dest": "cargo/vendor/objc2-link-presentation-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-metal/objc2-metal-0.2.2.crate", + "sha256": "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6", + "dest": "cargo/vendor/objc2-metal-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6\", \"files\": {}}", + "dest": "cargo/vendor/objc2-metal-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-metal/objc2-metal-0.3.2.crate", + "sha256": "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794", + "dest": "cargo/vendor/objc2-metal-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794\", \"files\": {}}", + "dest": "cargo/vendor/objc2-metal-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-quartz-core/objc2-quartz-core-0.2.2.crate", + "sha256": "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a", + "dest": "cargo/vendor/objc2-quartz-core-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a\", \"files\": {}}", + "dest": "cargo/vendor/objc2-quartz-core-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-quartz-core/objc2-quartz-core-0.3.2.crate", + "sha256": "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f", + "dest": "cargo/vendor/objc2-quartz-core-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f\", \"files\": {}}", + "dest": "cargo/vendor/objc2-quartz-core-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-symbols/objc2-symbols-0.2.2.crate", + "sha256": "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc", + "dest": "cargo/vendor/objc2-symbols-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc\", \"files\": {}}", + "dest": "cargo/vendor/objc2-symbols-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-ui-kit/objc2-ui-kit-0.2.2.crate", + "sha256": "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f", + "dest": "cargo/vendor/objc2-ui-kit-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f\", \"files\": {}}", + "dest": "cargo/vendor/objc2-ui-kit-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-ui-kit/objc2-ui-kit-0.3.2.crate", + "sha256": "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22", + "dest": "cargo/vendor/objc2-ui-kit-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22\", \"files\": {}}", + "dest": "cargo/vendor/objc2-ui-kit-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-uniform-type-identifiers/objc2-uniform-type-identifiers-0.2.2.crate", + "sha256": "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe", + "dest": "cargo/vendor/objc2-uniform-type-identifiers-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe\", \"files\": {}}", + "dest": "cargo/vendor/objc2-uniform-type-identifiers-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc2-user-notifications/objc2-user-notifications-0.2.2.crate", + "sha256": "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3", + "dest": "cargo/vendor/objc2-user-notifications-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3\", \"files\": {}}", + "dest": "cargo/vendor/objc2-user-notifications-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/object/object-0.37.3.crate", + "sha256": "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe", + "dest": "cargo/vendor/object-0.37.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe\", \"files\": {}}", + "dest": "cargo/vendor/object-0.37.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/once_cell/once_cell-1.21.4.crate", + "sha256": "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50", + "dest": "cargo/vendor/once_cell-1.21.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50\", \"files\": {}}", + "dest": "cargo/vendor/once_cell-1.21.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/opaque-debug/opaque-debug-0.3.1.crate", + "sha256": "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381", + "dest": "cargo/vendor/opaque-debug-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381\", \"files\": {}}", + "dest": "cargo/vendor/opaque-debug-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/open/open-5.4.4.crate", + "sha256": "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc", + "dest": "cargo/vendor/open-5.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc\", \"files\": {}}", + "dest": "cargo/vendor/open-5.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/orbclient/orbclient-0.3.55.crate", + "sha256": "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747", + "dest": "cargo/vendor/orbclient-0.3.55" + }, + { + "type": "inline", + "contents": "{\"package\": \"5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747\", \"files\": {}}", + "dest": "cargo/vendor/orbclient-0.3.55", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ordered-float/ordered-float-5.5.0.crate", + "sha256": "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0", + "dest": "cargo/vendor/ordered-float-5.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0\", \"files\": {}}", + "dest": "cargo/vendor/ordered-float-5.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ordered-stream/ordered-stream-0.2.0.crate", + "sha256": "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50", + "dest": "cargo/vendor/ordered-stream-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50\", \"files\": {}}", + "dest": "cargo/vendor/ordered-stream-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/os_pipe/os_pipe-1.2.3.crate", + "sha256": "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967", + "dest": "cargo/vendor/os_pipe-1.2.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967\", \"files\": {}}", + "dest": "cargo/vendor/os_pipe-1.2.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/owned_ttf_parser/owned_ttf_parser-0.25.1.crate", + "sha256": "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b", + "dest": "cargo/vendor/owned_ttf_parser-0.25.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b\", \"files\": {}}", + "dest": "cargo/vendor/owned_ttf_parser-0.25.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/p256/p256-0.13.2.crate", + "sha256": "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b", + "dest": "cargo/vendor/p256-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b\", \"files\": {}}", + "dest": "cargo/vendor/p256-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/p384/p384-0.13.1.crate", + "sha256": "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6", + "dest": "cargo/vendor/p384-0.13.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6\", \"files\": {}}", + "dest": "cargo/vendor/p384-0.13.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking/parking-2.2.1.crate", + "sha256": "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba", + "dest": "cargo/vendor/parking-2.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba\", \"files\": {}}", + "dest": "cargo/vendor/parking-2.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking_lot/parking_lot-0.12.5.crate", + "sha256": "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a", + "dest": "cargo/vendor/parking_lot-0.12.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a\", \"files\": {}}", + "dest": "cargo/vendor/parking_lot-0.12.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.12.crate", + "sha256": "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1", + "dest": "cargo/vendor/parking_lot_core-0.9.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1\", \"files\": {}}", + "dest": "cargo/vendor/parking_lot_core-0.9.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/paste/paste-1.0.15.crate", + "sha256": "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a", + "dest": "cargo/vendor/paste-1.0.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a\", \"files\": {}}", + "dest": "cargo/vendor/paste-1.0.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pastey/pastey-0.1.1.crate", + "sha256": "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec", + "dest": "cargo/vendor/pastey-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec\", \"files\": {}}", + "dest": "cargo/vendor/pastey-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pem-rfc7468/pem-rfc7468-0.7.0.crate", + "sha256": "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412", + "dest": "cargo/vendor/pem-rfc7468-0.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412\", \"files\": {}}", + "dest": "cargo/vendor/pem-rfc7468-0.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/peniko/peniko-0.6.1.crate", + "sha256": "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6", + "dest": "cargo/vendor/peniko-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6\", \"files\": {}}", + "dest": "cargo/vendor/peniko-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.2.crate", + "sha256": "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220", + "dest": "cargo/vendor/percent-encoding-2.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220\", \"files\": {}}", + "dest": "cargo/vendor/percent-encoding-2.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/petgraph/petgraph-0.8.3.crate", + "sha256": "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455", + "dest": "cargo/vendor/petgraph-0.8.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455\", \"files\": {}}", + "dest": "cargo/vendor/petgraph-0.8.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/phf/phf-0.14.0.crate", + "sha256": "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6", + "dest": "cargo/vendor/phf-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6\", \"files\": {}}", + "dest": "cargo/vendor/phf-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/phf_generator/phf_generator-0.14.0.crate", + "sha256": "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100", + "dest": "cargo/vendor/phf_generator-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100\", \"files\": {}}", + "dest": "cargo/vendor/phf_generator-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/phf_macros/phf_macros-0.14.0.crate", + "sha256": "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085", + "dest": "cargo/vendor/phf_macros-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085\", \"files\": {}}", + "dest": "cargo/vendor/phf_macros-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/phf_shared/phf_shared-0.14.0.crate", + "sha256": "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89", + "dest": "cargo/vendor/phf_shared-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89\", \"files\": {}}", + "dest": "cargo/vendor/phf_shared-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project/pin-project-1.1.13.crate", + "sha256": "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924", + "dest": "cargo/vendor/pin-project-1.1.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-1.1.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project-internal/pin-project-internal-1.1.13.crate", + "sha256": "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b", + "dest": "cargo/vendor/pin-project-internal-1.1.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-internal-1.1.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.17.crate", + "sha256": "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd", + "dest": "cargo/vendor/pin-project-lite-0.2.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-lite-0.2.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/piper/piper-0.2.5.crate", + "sha256": "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1", + "dest": "cargo/vendor/piper-0.2.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1\", \"files\": {}}", + "dest": "cargo/vendor/piper-0.2.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkcs1/pkcs1-0.7.5.crate", + "sha256": "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f", + "dest": "cargo/vendor/pkcs1-0.7.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f\", \"files\": {}}", + "dest": "cargo/vendor/pkcs1-0.7.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkcs5/pkcs5-0.7.1.crate", + "sha256": "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6", + "dest": "cargo/vendor/pkcs5-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6\", \"files\": {}}", + "dest": "cargo/vendor/pkcs5-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkcs8/pkcs8-0.10.2.crate", + "sha256": "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7", + "dest": "cargo/vendor/pkcs8-0.10.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7\", \"files\": {}}", + "dest": "cargo/vendor/pkcs8-0.10.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkg-config/pkg-config-0.3.34.crate", + "sha256": "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548", + "dest": "cargo/vendor/pkg-config-0.3.34" + }, + { + "type": "inline", + "contents": "{\"package\": \"f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548\", \"files\": {}}", + "dest": "cargo/vendor/pkg-config-0.3.34", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/plain/plain-0.2.3.crate", + "sha256": "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6", + "dest": "cargo/vendor/plain-0.2.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6\", \"files\": {}}", + "dest": "cargo/vendor/plain-0.2.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/png/png-0.18.1.crate", + "sha256": "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61", + "dest": "cargo/vendor/png-0.18.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61\", \"files\": {}}", + "dest": "cargo/vendor/png-0.18.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/polling/polling-3.11.0.crate", + "sha256": "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218", + "dest": "cargo/vendor/polling-3.11.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218\", \"files\": {}}", + "dest": "cargo/vendor/polling-3.11.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pollster/pollster-0.4.0.crate", + "sha256": "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3", + "dest": "cargo/vendor/pollster-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3\", \"files\": {}}", + "dest": "cargo/vendor/pollster-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pollster/pollster-1.0.1.crate", + "sha256": "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336", + "dest": "cargo/vendor/pollster-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336\", \"files\": {}}", + "dest": "cargo/vendor/pollster-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/poly1305/poly1305-0.8.0.crate", + "sha256": "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf", + "dest": "cargo/vendor/poly1305-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf\", \"files\": {}}", + "dest": "cargo/vendor/poly1305-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/polycool/polycool-0.4.0.crate", + "sha256": "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6", + "dest": "cargo/vendor/polycool-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6\", \"files\": {}}", + "dest": "cargo/vendor/polycool-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/polyval/polyval-0.6.2.crate", + "sha256": "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25", + "dest": "cargo/vendor/polyval-0.6.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25\", \"files\": {}}", + "dest": "cargo/vendor/polyval-0.6.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/portable-atomic/portable-atomic-1.15.0.crate", + "sha256": "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85", + "dest": "cargo/vendor/portable-atomic-1.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85\", \"files\": {}}", + "dest": "cargo/vendor/portable-atomic-1.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/portable-atomic-util/portable-atomic-util-0.2.8.crate", + "sha256": "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715", + "dest": "cargo/vendor/portable-atomic-util-0.2.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715\", \"files\": {}}", + "dest": "cargo/vendor/portable-atomic-util-0.2.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.21.crate", + "sha256": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9", + "dest": "cargo/vendor/ppv-lite86-0.2.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9\", \"files\": {}}", + "dest": "cargo/vendor/ppv-lite86-0.2.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/presser/presser-0.3.1.crate", + "sha256": "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa", + "dest": "cargo/vendor/presser-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa\", \"files\": {}}", + "dest": "cargo/vendor/presser-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/primeorder/primeorder-0.13.6.crate", + "sha256": "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6", + "dest": "cargo/vendor/primeorder-0.13.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6\", \"files\": {}}", + "dest": "cargo/vendor/primeorder-0.13.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-3.5.0.crate", + "sha256": "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f", + "dest": "cargo/vendor/proc-macro-crate-3.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro-crate-3.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.107.crate", + "sha256": "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9", + "dest": "cargo/vendor/proc-macro2-1.0.107" + }, + { + "type": "inline", + "contents": "{\"package\": \"985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro2-1.0.107", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/profiling/profiling-1.0.18.crate", + "sha256": "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5", + "dest": "cargo/vendor/profiling-1.0.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5\", \"files\": {}}", + "dest": "cargo/vendor/profiling-1.0.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/profiling-procmacros/profiling-procmacros-1.0.18.crate", + "sha256": "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb", + "dest": "cargo/vendor/profiling-procmacros-1.0.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb\", \"files\": {}}", + "dest": "cargo/vendor/profiling-procmacros-1.0.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proptest/proptest-1.11.0.crate", + "sha256": "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744", + "dest": "cargo/vendor/proptest-1.11.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744\", \"files\": {}}", + "dest": "cargo/vendor/proptest-1.11.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pulp/pulp-0.18.22.crate", + "sha256": "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6", + "dest": "cargo/vendor/pulp-0.18.22" + }, + { + "type": "inline", + "contents": "{\"package\": \"a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6\", \"files\": {}}", + "dest": "cargo/vendor/pulp-0.18.22", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pulp/pulp-0.21.5.crate", + "sha256": "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907", + "dest": "cargo/vendor/pulp-0.21.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907\", \"files\": {}}", + "dest": "cargo/vendor/pulp-0.21.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pulp/pulp-0.22.3.crate", + "sha256": "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a", + "dest": "cargo/vendor/pulp-0.22.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a\", \"files\": {}}", + "dest": "cargo/vendor/pulp-0.22.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pulp-wasm-simd-flag/pulp-wasm-simd-flag-0.1.1.crate", + "sha256": "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740", + "dest": "cargo/vendor/pulp-wasm-simd-flag-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740\", \"files\": {}}", + "dest": "cargo/vendor/pulp-wasm-simd-flag-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pxfm/pxfm-0.1.30.crate", + "sha256": "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea", + "dest": "cargo/vendor/pxfm-0.1.30" + }, + { + "type": "inline", + "contents": "{\"package\": \"d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea\", \"files\": {}}", + "dest": "cargo/vendor/pxfm-0.1.30", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quick-error/quick-error-1.2.3.crate", + "sha256": "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0", + "dest": "cargo/vendor/quick-error-1.2.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0\", \"files\": {}}", + "dest": "cargo/vendor/quick-error-1.2.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quick-error/quick-error-2.0.1.crate", + "sha256": "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3", + "dest": "cargo/vendor/quick-error-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3\", \"files\": {}}", + "dest": "cargo/vendor/quick-error-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quick-xml/quick-xml-0.41.0.crate", + "sha256": "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1", + "dest": "cargo/vendor/quick-xml-0.41.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1\", \"files\": {}}", + "dest": "cargo/vendor/quick-xml-0.41.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quote/quote-1.0.47.crate", + "sha256": "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001", + "dest": "cargo/vendor/quote-1.0.47" + }, + { + "type": "inline", + "contents": "{\"package\": \"1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001\", \"files\": {}}", + "dest": "cargo/vendor/quote-1.0.47", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/r-efi/r-efi-5.3.0.crate", + "sha256": "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f", + "dest": "cargo/vendor/r-efi-5.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\", \"files\": {}}", + "dest": "cargo/vendor/r-efi-5.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/r-efi/r-efi-6.0.0.crate", + "sha256": "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf", + "dest": "cargo/vendor/r-efi-6.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf\", \"files\": {}}", + "dest": "cargo/vendor/r-efi-6.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand/rand-0.8.8.crate", + "sha256": "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c", + "dest": "cargo/vendor/rand-0.8.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c\", \"files\": {}}", + "dest": "cargo/vendor/rand-0.8.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand/rand-0.9.5.crate", + "sha256": "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41", + "dest": "cargo/vendor/rand-0.9.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41\", \"files\": {}}", + "dest": "cargo/vendor/rand-0.9.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate", + "sha256": "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88", + "dest": "cargo/vendor/rand_chacha-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\", \"files\": {}}", + "dest": "cargo/vendor/rand_chacha-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.9.0.crate", + "sha256": "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb", + "dest": "cargo/vendor/rand_chacha-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\", \"files\": {}}", + "dest": "cargo/vendor/rand_chacha-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate", + "sha256": "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c", + "dest": "cargo/vendor/rand_core-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\", \"files\": {}}", + "dest": "cargo/vendor/rand_core-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_core/rand_core-0.9.5.crate", + "sha256": "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c", + "dest": "cargo/vendor/rand_core-0.9.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c\", \"files\": {}}", + "dest": "cargo/vendor/rand_core-0.9.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_distr/rand_distr-0.5.1.crate", + "sha256": "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463", + "dest": "cargo/vendor/rand_distr-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463\", \"files\": {}}", + "dest": "cargo/vendor/rand_distr-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_xorshift/rand_xorshift-0.4.0.crate", + "sha256": "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a", + "dest": "cargo/vendor/rand_xorshift-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a\", \"files\": {}}", + "dest": "cargo/vendor/rand_xorshift-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/range-alloc/range-alloc-0.1.5.crate", + "sha256": "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08", + "dest": "cargo/vendor/range-alloc-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08\", \"files\": {}}", + "dest": "cargo/vendor/range-alloc-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rav1e/rav1e-0.8.1.crate", + "sha256": "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b", + "dest": "cargo/vendor/rav1e-0.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b\", \"files\": {}}", + "dest": "cargo/vendor/rav1e-0.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ravif/ravif-0.13.0.crate", + "sha256": "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45", + "dest": "cargo/vendor/ravif-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45\", \"files\": {}}", + "dest": "cargo/vendor/ravif-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/raw-cpuid/raw-cpuid-10.7.0.crate", + "sha256": "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332", + "dest": "cargo/vendor/raw-cpuid-10.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332\", \"files\": {}}", + "dest": "cargo/vendor/raw-cpuid-10.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/raw-cpuid/raw-cpuid-11.6.0.crate", + "sha256": "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186", + "dest": "cargo/vendor/raw-cpuid-11.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186\", \"files\": {}}", + "dest": "cargo/vendor/raw-cpuid-11.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/raw-window-handle/raw-window-handle-0.6.2.crate", + "sha256": "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539", + "dest": "cargo/vendor/raw-window-handle-0.6.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539\", \"files\": {}}", + "dest": "cargo/vendor/raw-window-handle-0.6.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/raw-window-metal/raw-window-metal-1.1.0.crate", + "sha256": "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135", + "dest": "cargo/vendor/raw-window-metal-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135\", \"files\": {}}", + "dest": "cargo/vendor/raw-window-metal-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rawpointer/rawpointer-0.2.1.crate", + "sha256": "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3", + "dest": "cargo/vendor/rawpointer-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3\", \"files\": {}}", + "dest": "cargo/vendor/rawpointer-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rayon/rayon-1.12.0.crate", + "sha256": "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d", + "dest": "cargo/vendor/rayon-1.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d\", \"files\": {}}", + "dest": "cargo/vendor/rayon-1.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rayon-core/rayon-core-1.13.0.crate", + "sha256": "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91", + "dest": "cargo/vendor/rayon-core-1.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91\", \"files\": {}}", + "dest": "cargo/vendor/rayon-core-1.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/read-fonts/read-fonts-0.41.0.crate", + "sha256": "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709", + "dest": "cargo/vendor/read-fonts-0.41.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709\", \"files\": {}}", + "dest": "cargo/vendor/read-fonts-0.41.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/reborrow/reborrow-0.5.5.crate", + "sha256": "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430", + "dest": "cargo/vendor/reborrow-0.5.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430\", \"files\": {}}", + "dest": "cargo/vendor/reborrow-0.5.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/redox_syscall/redox_syscall-0.4.1.crate", + "sha256": "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa", + "dest": "cargo/vendor/redox_syscall-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa\", \"files\": {}}", + "dest": "cargo/vendor/redox_syscall-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/redox_syscall/redox_syscall-0.5.18.crate", + "sha256": "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d", + "dest": "cargo/vendor/redox_syscall-0.5.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d\", \"files\": {}}", + "dest": "cargo/vendor/redox_syscall-0.5.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/redox_syscall/redox_syscall-0.9.4.crate", + "sha256": "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e", + "dest": "cargo/vendor/redox_syscall-0.9.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e\", \"files\": {}}", + "dest": "cargo/vendor/redox_syscall-0.9.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/regex-syntax/regex-syntax-0.8.11.crate", + "sha256": "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4", + "dest": "cargo/vendor/regex-syntax-0.8.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4\", \"files\": {}}", + "dest": "cargo/vendor/regex-syntax-0.8.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/renderdoc-sys/renderdoc-sys-1.1.0.crate", + "sha256": "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832", + "dest": "cargo/vendor/renderdoc-sys-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832\", \"files\": {}}", + "dest": "cargo/vendor/renderdoc-sys-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rfc6979/rfc6979-0.4.0.crate", + "sha256": "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2", + "dest": "cargo/vendor/rfc6979-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2\", \"files\": {}}", + "dest": "cargo/vendor/rfc6979-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rfd/rfd-0.17.2.crate", + "sha256": "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220", + "dest": "cargo/vendor/rfd-0.17.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220\", \"files\": {}}", + "dest": "cargo/vendor/rfd-0.17.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rgb/rgb-0.8.53.crate", + "sha256": "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4", + "dest": "cargo/vendor/rgb-0.8.53" + }, + { + "type": "inline", + "contents": "{\"package\": \"47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4\", \"files\": {}}", + "dest": "cargo/vendor/rgb-0.8.53", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ring/ring-0.17.14.crate", + "sha256": "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7", + "dest": "cargo/vendor/ring-0.17.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7\", \"files\": {}}", + "dest": "cargo/vendor/ring-0.17.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rsa/rsa-0.9.10.crate", + "sha256": "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d", + "dest": "cargo/vendor/rsa-0.9.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d\", \"files\": {}}", + "dest": "cargo/vendor/rsa-0.9.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc-demangle/rustc-demangle-0.1.28.crate", + "sha256": "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb", + "dest": "cargo/vendor/rustc-demangle-0.1.28" + }, + { + "type": "inline", + "contents": "{\"package\": \"b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb\", \"files\": {}}", + "dest": "cargo/vendor/rustc-demangle-0.1.28", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc-hash/rustc-hash-1.1.0.crate", + "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2", + "dest": "cargo/vendor/rustc-hash-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\", \"files\": {}}", + "dest": "cargo/vendor/rustc-hash-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc-hash/rustc-hash-2.1.3.crate", + "sha256": "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d", + "dest": "cargo/vendor/rustc-hash-2.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d\", \"files\": {}}", + "dest": "cargo/vendor/rustc-hash-2.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc_version/rustc_version-0.4.1.crate", + "sha256": "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92", + "dest": "cargo/vendor/rustc_version-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92\", \"files\": {}}", + "dest": "cargo/vendor/rustc_version-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustix/rustix-0.38.44.crate", + "sha256": "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154", + "dest": "cargo/vendor/rustix-0.38.44" + }, + { + "type": "inline", + "contents": "{\"package\": \"fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154\", \"files\": {}}", + "dest": "cargo/vendor/rustix-0.38.44", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustix/rustix-1.1.5.crate", + "sha256": "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d", + "dest": "cargo/vendor/rustix-1.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d\", \"files\": {}}", + "dest": "cargo/vendor/rustix-1.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls/rustls-0.23.45.crate", + "sha256": "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634", + "dest": "cargo/vendor/rustls-0.23.45" + }, + { + "type": "inline", + "contents": "{\"package\": \"0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634\", \"files\": {}}", + "dest": "cargo/vendor/rustls-0.23.45", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.15.1.crate", + "sha256": "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96", + "dest": "cargo/vendor/rustls-pki-types-1.15.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pki-types-1.15.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-rustcrypto/rustls-rustcrypto-0.0.2-alpha.crate", + "sha256": "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557", + "dest": "cargo/vendor/rustls-rustcrypto-0.0.2-alpha" + }, + { + "type": "inline", + "contents": "{\"package\": \"f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557\", \"files\": {}}", + "dest": "cargo/vendor/rustls-rustcrypto-0.0.2-alpha", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.102.8.crate", + "sha256": "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9", + "dest": "cargo/vendor/rustls-webpki-0.102.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9\", \"files\": {}}", + "dest": "cargo/vendor/rustls-webpki-0.102.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.103.15.crate", + "sha256": "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2", + "dest": "cargo/vendor/rustls-webpki-0.103.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2\", \"files\": {}}", + "dest": "cargo/vendor/rustls-webpki-0.103.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustversion/rustversion-1.0.23.crate", + "sha256": "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f", + "dest": "cargo/vendor/rustversion-1.0.23" + }, + { + "type": "inline", + "contents": "{\"package\": \"cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f\", \"files\": {}}", + "dest": "cargo/vendor/rustversion-1.0.23", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rusty-fork/rusty-fork-0.3.1.crate", + "sha256": "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2", + "dest": "cargo/vendor/rusty-fork-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2\", \"files\": {}}", + "dest": "cargo/vendor/rusty-fork-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/safetensors/safetensors-0.4.5.crate", + "sha256": "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6", + "dest": "cargo/vendor/safetensors-0.4.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6\", \"files\": {}}", + "dest": "cargo/vendor/safetensors-0.4.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/safetensors/safetensors-0.7.0.crate", + "sha256": "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5", + "dest": "cargo/vendor/safetensors-0.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5\", \"files\": {}}", + "dest": "cargo/vendor/safetensors-0.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/same-file/same-file-1.0.6.crate", + "sha256": "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502", + "dest": "cargo/vendor/same-file-1.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\", \"files\": {}}", + "dest": "cargo/vendor/same-file-1.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/scoped-tls/scoped-tls-1.0.1.crate", + "sha256": "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294", + "dest": "cargo/vendor/scoped-tls-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294\", \"files\": {}}", + "dest": "cargo/vendor/scoped-tls-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate", + "sha256": "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49", + "dest": "cargo/vendor/scopeguard-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49\", \"files\": {}}", + "dest": "cargo/vendor/scopeguard-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sec1/sec1-0.7.3.crate", + "sha256": "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc", + "dest": "cargo/vendor/sec1-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc\", \"files\": {}}", + "dest": "cargo/vendor/sec1-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/self_cell/self_cell-1.3.0.crate", + "sha256": "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813", + "dest": "cargo/vendor/self_cell-1.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813\", \"files\": {}}", + "dest": "cargo/vendor/self_cell-1.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/semver/semver-1.0.28.crate", + "sha256": "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd", + "dest": "cargo/vendor/semver-1.0.28" + }, + { + "type": "inline", + "contents": "{\"package\": \"8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd\", \"files\": {}}", + "dest": "cargo/vendor/semver-1.0.28", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/send_wrapper/send_wrapper-0.6.0.crate", + "sha256": "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73", + "dest": "cargo/vendor/send_wrapper-0.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73\", \"files\": {}}", + "dest": "cargo/vendor/send_wrapper-0.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/seq-macro/seq-macro-0.3.6.crate", + "sha256": "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc", + "dest": "cargo/vendor/seq-macro-0.3.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc\", \"files\": {}}", + "dest": "cargo/vendor/seq-macro-0.3.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde/serde-1.0.229.crate", + "sha256": "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba", + "dest": "cargo/vendor/serde-1.0.229" + }, + { + "type": "inline", + "contents": "{\"package\": \"4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba\", \"files\": {}}", + "dest": "cargo/vendor/serde-1.0.229", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_core/serde_core-1.0.229.crate", + "sha256": "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48", + "dest": "cargo/vendor/serde_core-1.0.229" + }, + { + "type": "inline", + "contents": "{\"package\": \"67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48\", \"files\": {}}", + "dest": "cargo/vendor/serde_core-1.0.229", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_derive/serde_derive-1.0.229.crate", + "sha256": "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348", + "dest": "cargo/vendor/serde_derive-1.0.229" + }, + { + "type": "inline", + "contents": "{\"package\": \"e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348\", \"files\": {}}", + "dest": "cargo/vendor/serde_derive-1.0.229", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_json/serde_json-1.0.151.crate", + "sha256": "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14", + "dest": "cargo/vendor/serde_json-1.0.151" + }, + { + "type": "inline", + "contents": "{\"package\": \"c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14\", \"files\": {}}", + "dest": "cargo/vendor/serde_json-1.0.151", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_repr/serde_repr-0.1.21.crate", + "sha256": "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906", + "dest": "cargo/vendor/serde_repr-0.1.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906\", \"files\": {}}", + "dest": "cargo/vendor/serde_repr-0.1.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_spanned/serde_spanned-1.1.1.crate", + "sha256": "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26", + "dest": "cargo/vendor/serde_spanned-1.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26\", \"files\": {}}", + "dest": "cargo/vendor/serde_spanned-1.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sha2/sha2-0.10.9.crate", + "sha256": "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283", + "dest": "cargo/vendor/sha2-0.10.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283\", \"files\": {}}", + "dest": "cargo/vendor/sha2-0.10.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/shlex/shlex-2.0.1.crate", + "sha256": "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba", + "dest": "cargo/vendor/shlex-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba\", \"files\": {}}", + "dest": "cargo/vendor/shlex-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/signal-hook-registry/signal-hook-registry-1.4.8.crate", + "sha256": "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b", + "dest": "cargo/vendor/signal-hook-registry-1.4.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b\", \"files\": {}}", + "dest": "cargo/vendor/signal-hook-registry-1.4.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/signature/signature-2.2.0.crate", + "sha256": "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de", + "dest": "cargo/vendor/signature-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de\", \"files\": {}}", + "dest": "cargo/vendor/signature-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/simd-adler32/simd-adler32-0.3.10.crate", + "sha256": "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea", + "dest": "cargo/vendor/simd-adler32-0.3.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea\", \"files\": {}}", + "dest": "cargo/vendor/simd-adler32-0.3.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/simd_cesu8/simd_cesu8-1.2.0.crate", + "sha256": "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520", + "dest": "cargo/vendor/simd_cesu8-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520\", \"files\": {}}", + "dest": "cargo/vendor/simd_cesu8-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/simd_helpers/simd_helpers-0.1.0.crate", + "sha256": "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6", + "dest": "cargo/vendor/simd_helpers-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6\", \"files\": {}}", + "dest": "cargo/vendor/simd_helpers-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/simdutf8/simdutf8-0.1.5.crate", + "sha256": "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e", + "dest": "cargo/vendor/simdutf8-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e\", \"files\": {}}", + "dest": "cargo/vendor/simdutf8-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/siphasher/siphasher-1.0.4.crate", + "sha256": "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256", + "dest": "cargo/vendor/siphasher-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256\", \"files\": {}}", + "dest": "cargo/vendor/siphasher-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/skrifa/skrifa-0.44.0.crate", + "sha256": "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68", + "dest": "cargo/vendor/skrifa-0.44.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68\", \"files\": {}}", + "dest": "cargo/vendor/skrifa-0.44.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/slab/slab-0.4.12.crate", + "sha256": "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5", + "dest": "cargo/vendor/slab-0.4.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5\", \"files\": {}}", + "dest": "cargo/vendor/slab-0.4.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/slotmap/slotmap-1.1.1.crate", + "sha256": "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038", + "dest": "cargo/vendor/slotmap-1.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038\", \"files\": {}}", + "dest": "cargo/vendor/slotmap-1.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smallvec/smallvec-1.16.2.crate", + "sha256": "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e", + "dest": "cargo/vendor/smallvec-1.16.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e\", \"files\": {}}", + "dest": "cargo/vendor/smallvec-1.16.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smithay-client-toolkit/smithay-client-toolkit-0.19.2.crate", + "sha256": "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016", + "dest": "cargo/vendor/smithay-client-toolkit-0.19.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016\", \"files\": {}}", + "dest": "cargo/vendor/smithay-client-toolkit-0.19.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smithay-client-toolkit/smithay-client-toolkit-0.20.0.crate", + "sha256": "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0", + "dest": "cargo/vendor/smithay-client-toolkit-0.20.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0\", \"files\": {}}", + "dest": "cargo/vendor/smithay-client-toolkit-0.20.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smithay-clipboard/smithay-clipboard-0.7.3.crate", + "sha256": "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226", + "dest": "cargo/vendor/smithay-clipboard-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226\", \"files\": {}}", + "dest": "cargo/vendor/smithay-clipboard-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smol_str/smol_str-0.2.2.crate", + "sha256": "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead", + "dest": "cargo/vendor/smol_str-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead\", \"files\": {}}", + "dest": "cargo/vendor/smol_str-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spin/spin-0.9.9.crate", + "sha256": "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e", + "dest": "cargo/vendor/spin-0.9.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e\", \"files\": {}}", + "dest": "cargo/vendor/spin-0.9.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spirv/spirv-0.4.0+sdk-1.4.341.0.crate", + "sha256": "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f", + "dest": "cargo/vendor/spirv-0.4.0+sdk-1.4.341.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f\", \"files\": {}}", + "dest": "cargo/vendor/spirv-0.4.0+sdk-1.4.341.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spki/spki-0.7.3.crate", + "sha256": "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d", + "dest": "cargo/vendor/spki-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d\", \"files\": {}}", + "dest": "cargo/vendor/spki-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate", + "sha256": "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596", + "dest": "cargo/vendor/stable_deref_trait-1.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596\", \"files\": {}}", + "dest": "cargo/vendor/stable_deref_trait-1.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate", + "sha256": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f", + "dest": "cargo/vendor/static_assertions-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\", \"files\": {}}", + "dest": "cargo/vendor/static_assertions-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/subtle/subtle-2.6.1.crate", + "sha256": "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292", + "dest": "cargo/vendor/subtle-2.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292\", \"files\": {}}", + "dest": "cargo/vendor/subtle-2.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/syn/syn-2.0.119.crate", + "sha256": "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297", + "dest": "cargo/vendor/syn-2.0.119" + }, + { + "type": "inline", + "contents": "{\"package\": \"872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297\", \"files\": {}}", + "dest": "cargo/vendor/syn-2.0.119", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/syn/syn-3.0.6.crate", + "sha256": "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee", + "dest": "cargo/vendor/syn-3.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee\", \"files\": {}}", + "dest": "cargo/vendor/syn-3.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate", + "sha256": "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2", + "dest": "cargo/vendor/synstructure-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2\", \"files\": {}}", + "dest": "cargo/vendor/synstructure-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/synstructure/synstructure-0.14.0.crate", + "sha256": "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02", + "dest": "cargo/vendor/synstructure-0.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02\", \"files\": {}}", + "dest": "cargo/vendor/synstructure-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sysctl/sysctl-0.5.5.crate", + "sha256": "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea", + "dest": "cargo/vendor/sysctl-0.5.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea\", \"files\": {}}", + "dest": "cargo/vendor/sysctl-0.5.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sysctl/sysctl-0.6.0.crate", + "sha256": "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc", + "dest": "cargo/vendor/sysctl-0.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc\", \"files\": {}}", + "dest": "cargo/vendor/sysctl-0.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tempfile/tempfile-3.27.0.crate", + "sha256": "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd", + "dest": "cargo/vendor/tempfile-3.27.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd\", \"files\": {}}", + "dest": "cargo/vendor/tempfile-3.27.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/termcolor/termcolor-1.4.1.crate", + "sha256": "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755", + "dest": "cargo/vendor/termcolor-1.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755\", \"files\": {}}", + "dest": "cargo/vendor/termcolor-1.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate", + "sha256": "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52", + "dest": "cargo/vendor/thiserror-1.0.69" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-1.0.69", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror/thiserror-2.0.21.crate", + "sha256": "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e", + "dest": "cargo/vendor/thiserror-2.0.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-2.0.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.69.crate", + "sha256": "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1", + "dest": "cargo/vendor/thiserror-impl-1.0.69" + }, + { + "type": "inline", + "contents": "{\"package\": \"4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-impl-1.0.69", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.21.crate", + "sha256": "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524", + "dest": "cargo/vendor/thiserror-impl-2.0.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-impl-2.0.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thousands/thousands-0.2.0.crate", + "sha256": "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820", + "dest": "cargo/vendor/thousands-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820\", \"files\": {}}", + "dest": "cargo/vendor/thousands-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tiff/tiff-0.11.3.crate", + "sha256": "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52", + "dest": "cargo/vendor/tiff-0.11.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52\", \"files\": {}}", + "dest": "cargo/vendor/tiff-0.11.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio/tokio-1.53.1.crate", + "sha256": "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed", + "dest": "cargo/vendor/tokio-1.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed\", \"files\": {}}", + "dest": "cargo/vendor/tokio-1.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml/toml-1.1.6+spec-1.1.0.crate", + "sha256": "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a", + "dest": "cargo/vendor/toml-1.1.6+spec-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a\", \"files\": {}}", + "dest": "cargo/vendor/toml-1.1.6+spec-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_datetime/toml_datetime-1.1.1+spec-1.1.0.crate", + "sha256": "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7", + "dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7\", \"files\": {}}", + "dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_edit/toml_edit-0.25.15+spec-1.1.0.crate", + "sha256": "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614", + "dest": "cargo/vendor/toml_edit-0.25.15+spec-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614\", \"files\": {}}", + "dest": "cargo/vendor/toml_edit-0.25.15+spec-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_parser/toml_parser-1.1.3+spec-1.1.0.crate", + "sha256": "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56", + "dest": "cargo/vendor/toml_parser-1.1.3+spec-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56\", \"files\": {}}", + "dest": "cargo/vendor/toml_parser-1.1.3+spec-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_writer/toml_writer-1.1.2+spec-1.1.0.crate", + "sha256": "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2", + "dest": "cargo/vendor/toml_writer-1.1.2+spec-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2\", \"files\": {}}", + "dest": "cargo/vendor/toml_writer-1.1.2+spec-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing/tracing-0.1.44.crate", + "sha256": "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100", + "dest": "cargo/vendor/tracing-0.1.44" + }, + { + "type": "inline", + "contents": "{\"package\": \"63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100\", \"files\": {}}", + "dest": "cargo/vendor/tracing-0.1.44", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing-attributes/tracing-attributes-0.1.31.crate", + "sha256": "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da", + "dest": "cargo/vendor/tracing-attributes-0.1.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da\", \"files\": {}}", + "dest": "cargo/vendor/tracing-attributes-0.1.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing-core/tracing-core-0.1.36.crate", + "sha256": "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a", + "dest": "cargo/vendor/tracing-core-0.1.36" + }, + { + "type": "inline", + "contents": "{\"package\": \"db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a\", \"files\": {}}", + "dest": "cargo/vendor/tracing-core-0.1.36", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tree_magic_mini/tree_magic_mini-3.2.2.crate", + "sha256": "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6", + "dest": "cargo/vendor/tree_magic_mini-3.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6\", \"files\": {}}", + "dest": "cargo/vendor/tree_magic_mini-3.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ttf-parser/ttf-parser-0.25.1.crate", + "sha256": "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31", + "dest": "cargo/vendor/ttf-parser-0.25.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31\", \"files\": {}}", + "dest": "cargo/vendor/ttf-parser-0.25.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/type-map/type-map-0.5.1.crate", + "sha256": "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90", + "dest": "cargo/vendor/type-map-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90\", \"files\": {}}", + "dest": "cargo/vendor/type-map-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/typed-path/typed-path-0.12.3.crate", + "sha256": "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e", + "dest": "cargo/vendor/typed-path-0.12.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e\", \"files\": {}}", + "dest": "cargo/vendor/typed-path-0.12.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/typenum/typenum-1.20.1.crate", + "sha256": "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20", + "dest": "cargo/vendor/typenum-1.20.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20\", \"files\": {}}", + "dest": "cargo/vendor/typenum-1.20.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/uds_windows/uds_windows-1.2.1.crate", + "sha256": "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e", + "dest": "cargo/vendor/uds_windows-1.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e\", \"files\": {}}", + "dest": "cargo/vendor/uds_windows-1.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ug/ug-0.5.0.crate", + "sha256": "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0", + "dest": "cargo/vendor/ug-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0\", \"files\": {}}", + "dest": "cargo/vendor/ug-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ug-metal/ug-metal-0.5.0.crate", + "sha256": "9f7adf545a99a086d362efc739e7cf4317c18cbeda22706000fd434d70ea3d95", + "dest": "cargo/vendor/ug-metal-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9f7adf545a99a086d362efc739e7cf4317c18cbeda22706000fd434d70ea3d95\", \"files\": {}}", + "dest": "cargo/vendor/ug-metal-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unarray/unarray-0.1.4.crate", + "sha256": "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94", + "dest": "cargo/vendor/unarray-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94\", \"files\": {}}", + "dest": "cargo/vendor/unarray-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-bidi/unicode-bidi-0.3.18.crate", + "sha256": "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5", + "dest": "cargo/vendor/unicode-bidi-0.3.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5\", \"files\": {}}", + "dest": "cargo/vendor/unicode-bidi-0.3.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-general-category/unicode-general-category-1.1.0.crate", + "sha256": "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f", + "dest": "cargo/vendor/unicode-general-category-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f\", \"files\": {}}", + "dest": "cargo/vendor/unicode-general-category-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-ident/unicode-ident-1.0.26.crate", + "sha256": "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954", + "dest": "cargo/vendor/unicode-ident-1.0.26" + }, + { + "type": "inline", + "contents": "{\"package\": \"d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954\", \"files\": {}}", + "dest": "cargo/vendor/unicode-ident-1.0.26", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-properties/unicode-properties-0.1.4.crate", + "sha256": "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d", + "dest": "cargo/vendor/unicode-properties-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d\", \"files\": {}}", + "dest": "cargo/vendor/unicode-properties-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-segmentation/unicode-segmentation-1.13.3.crate", + "sha256": "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8", + "dest": "cargo/vendor/unicode-segmentation-1.13.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8\", \"files\": {}}", + "dest": "cargo/vendor/unicode-segmentation-1.13.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-width/unicode-width-0.2.2.crate", + "sha256": "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254", + "dest": "cargo/vendor/unicode-width-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254\", \"files\": {}}", + "dest": "cargo/vendor/unicode-width-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/universal-hash/universal-hash-0.5.1.crate", + "sha256": "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea", + "dest": "cargo/vendor/universal-hash-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea\", \"files\": {}}", + "dest": "cargo/vendor/universal-hash-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate", + "sha256": "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1", + "dest": "cargo/vendor/untrusted-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1\", \"files\": {}}", + "dest": "cargo/vendor/untrusted-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/uuid/uuid-1.26.1.crate", + "sha256": "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce", + "dest": "cargo/vendor/uuid-1.26.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce\", \"files\": {}}", + "dest": "cargo/vendor/uuid-1.26.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/v_frame/v_frame-0.3.9.crate", + "sha256": "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2", + "dest": "cargo/vendor/v_frame-0.3.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2\", \"files\": {}}", + "dest": "cargo/vendor/v_frame-0.3.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vello_common/vello_common-0.2.0.crate", + "sha256": "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd", + "dest": "cargo/vendor/vello_common-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd\", \"files\": {}}", + "dest": "cargo/vendor/vello_common-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vello_cpu/vello_cpu-0.2.0.crate", + "sha256": "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9", + "dest": "cargo/vendor/vello_cpu-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9\", \"files\": {}}", + "dest": "cargo/vendor/vello_cpu-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/version_check/version_check-0.9.5.crate", + "sha256": "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a", + "dest": "cargo/vendor/version_check-0.9.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\", \"files\": {}}", + "dest": "cargo/vendor/version_check-0.9.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wait-timeout/wait-timeout-0.2.1.crate", + "sha256": "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11", + "dest": "cargo/vendor/wait-timeout-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11\", \"files\": {}}", + "dest": "cargo/vendor/wait-timeout-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/walkdir/walkdir-2.5.0.crate", + "sha256": "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b", + "dest": "cargo/vendor/walkdir-2.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\", \"files\": {}}", + "dest": "cargo/vendor/walkdir-2.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasi/wasi-0.11.1+wasi-snapshot-preview1.crate", + "sha256": "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b", + "dest": "cargo/vendor/wasi-0.11.1+wasi-snapshot-preview1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b\", \"files\": {}}", + "dest": "cargo/vendor/wasi-0.11.1+wasi-snapshot-preview1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasip2/wasip2-1.0.4+wasi-0.2.12.crate", + "sha256": "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487", + "dest": "cargo/vendor/wasip2-1.0.4+wasi-0.2.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487\", \"files\": {}}", + "dest": "cargo/vendor/wasip2-1.0.4+wasi-0.2.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.129.crate", + "sha256": "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409", + "dest": "cargo/vendor/wasm-bindgen-0.2.129" + }, + { + "type": "inline", + "contents": "{\"package\": \"9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-0.2.129", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.79.crate", + "sha256": "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e", + "dest": "cargo/vendor/wasm-bindgen-futures-0.4.79" + }, + { + "type": "inline", + "contents": "{\"package\": \"3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-futures-0.4.79", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.129.crate", + "sha256": "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535", + "dest": "cargo/vendor/wasm-bindgen-macro-0.2.129" + }, + { + "type": "inline", + "contents": "{\"package\": \"2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-macro-0.2.129", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.129.crate", + "sha256": "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7", + "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.129" + }, + { + "type": "inline", + "contents": "{\"package\": \"6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.129", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.129.crate", + "sha256": "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6", + "dest": "cargo/vendor/wasm-bindgen-shared-0.2.129" + }, + { + "type": "inline", + "contents": "{\"package\": \"23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-shared-0.2.129", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-backend/wayland-backend-0.3.17.crate", + "sha256": "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078", + "dest": "cargo/vendor/wayland-backend-0.3.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078\", \"files\": {}}", + "dest": "cargo/vendor/wayland-backend-0.3.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-client/wayland-client-0.31.15.crate", + "sha256": "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073", + "dest": "cargo/vendor/wayland-client-0.31.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073\", \"files\": {}}", + "dest": "cargo/vendor/wayland-client-0.31.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-csd-frame/wayland-csd-frame-0.3.0.crate", + "sha256": "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e", + "dest": "cargo/vendor/wayland-csd-frame-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e\", \"files\": {}}", + "dest": "cargo/vendor/wayland-csd-frame-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-cursor/wayland-cursor-0.31.14.crate", + "sha256": "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d", + "dest": "cargo/vendor/wayland-cursor-0.31.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d\", \"files\": {}}", + "dest": "cargo/vendor/wayland-cursor-0.31.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-protocols/wayland-protocols-0.32.13.crate", + "sha256": "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6", + "dest": "cargo/vendor/wayland-protocols-0.32.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6\", \"files\": {}}", + "dest": "cargo/vendor/wayland-protocols-0.32.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-protocols-experimental/wayland-protocols-experimental-20250721.0.1.crate", + "sha256": "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1", + "dest": "cargo/vendor/wayland-protocols-experimental-20250721.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1\", \"files\": {}}", + "dest": "cargo/vendor/wayland-protocols-experimental-20250721.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-protocols-misc/wayland-protocols-misc-0.3.12.crate", + "sha256": "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8", + "dest": "cargo/vendor/wayland-protocols-misc-0.3.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8\", \"files\": {}}", + "dest": "cargo/vendor/wayland-protocols-misc-0.3.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-protocols-plasma/wayland-protocols-plasma-0.3.12.crate", + "sha256": "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91", + "dest": "cargo/vendor/wayland-protocols-plasma-0.3.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91\", \"files\": {}}", + "dest": "cargo/vendor/wayland-protocols-plasma-0.3.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-protocols-wlr/wayland-protocols-wlr-0.3.12.crate", + "sha256": "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234", + "dest": "cargo/vendor/wayland-protocols-wlr-0.3.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234\", \"files\": {}}", + "dest": "cargo/vendor/wayland-protocols-wlr-0.3.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-scanner/wayland-scanner-0.31.11.crate", + "sha256": "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0", + "dest": "cargo/vendor/wayland-scanner-0.31.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0\", \"files\": {}}", + "dest": "cargo/vendor/wayland-scanner-0.31.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wayland-sys/wayland-sys-0.31.11.crate", + "sha256": "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be", + "dest": "cargo/vendor/wayland-sys-0.31.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be\", \"files\": {}}", + "dest": "cargo/vendor/wayland-sys-0.31.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/web-sys/web-sys-0.3.106.crate", + "sha256": "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4", + "dest": "cargo/vendor/web-sys-0.3.106" + }, + { + "type": "inline", + "contents": "{\"package\": \"88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4\", \"files\": {}}", + "dest": "cargo/vendor/web-sys-0.3.106", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/web-time/web-time-1.1.0.crate", + "sha256": "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb", + "dest": "cargo/vendor/web-time-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb\", \"files\": {}}", + "dest": "cargo/vendor/web-time-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/webpki-roots/webpki-roots-1.0.9.crate", + "sha256": "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a", + "dest": "cargo/vendor/webpki-roots-1.0.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a\", \"files\": {}}", + "dest": "cargo/vendor/webpki-roots-1.0.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/weezl/weezl-0.1.12.crate", + "sha256": "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88", + "dest": "cargo/vendor/weezl-0.1.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88\", \"files\": {}}", + "dest": "cargo/vendor/weezl-0.1.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu/wgpu-30.0.1.crate", + "sha256": "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e", + "dest": "cargo/vendor/wgpu-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-core/wgpu-core-30.0.1.crate", + "sha256": "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14", + "dest": "cargo/vendor/wgpu-core-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-core-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-core-deps-apple/wgpu-core-deps-apple-30.0.1.crate", + "sha256": "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68", + "dest": "cargo/vendor/wgpu-core-deps-apple-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-core-deps-apple-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-core-deps-emscripten/wgpu-core-deps-emscripten-30.0.1.crate", + "sha256": "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86", + "dest": "cargo/vendor/wgpu-core-deps-emscripten-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-core-deps-emscripten-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-core-deps-wasm/wgpu-core-deps-wasm-30.0.1.crate", + "sha256": "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae", + "dest": "cargo/vendor/wgpu-core-deps-wasm-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-core-deps-wasm-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-core-deps-windows-linux-android/wgpu-core-deps-windows-linux-android-30.0.1.crate", + "sha256": "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f", + "dest": "cargo/vendor/wgpu-core-deps-windows-linux-android-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-core-deps-windows-linux-android-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-hal/wgpu-hal-30.0.1.crate", + "sha256": "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58", + "dest": "cargo/vendor/wgpu-hal-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-hal-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-naga-bridge/wgpu-naga-bridge-30.0.1.crate", + "sha256": "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081", + "dest": "cargo/vendor/wgpu-naga-bridge-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-naga-bridge-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wgpu-types/wgpu-types-30.0.1.crate", + "sha256": "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286", + "dest": "cargo/vendor/wgpu-types-30.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286\", \"files\": {}}", + "dest": "cargo/vendor/wgpu-types-30.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winapi-util/winapi-util-0.1.11.crate", + "sha256": "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22", + "dest": "cargo/vendor/winapi-util-0.1.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22\", \"files\": {}}", + "dest": "cargo/vendor/winapi-util-0.1.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows/windows-0.62.2.crate", + "sha256": "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580", + "dest": "cargo/vendor/windows-0.62.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580\", \"files\": {}}", + "dest": "cargo/vendor/windows-0.62.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-collections/windows-collections-0.3.2.crate", + "sha256": "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610", + "dest": "cargo/vendor/windows-collections-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610\", \"files\": {}}", + "dest": "cargo/vendor/windows-collections-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-core/windows-core-0.62.2.crate", + "sha256": "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb", + "dest": "cargo/vendor/windows-core-0.62.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb\", \"files\": {}}", + "dest": "cargo/vendor/windows-core-0.62.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-future/windows-future-0.3.2.crate", + "sha256": "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb", + "dest": "cargo/vendor/windows-future-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb\", \"files\": {}}", + "dest": "cargo/vendor/windows-future-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-implement/windows-implement-0.60.2.crate", + "sha256": "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf", + "dest": "cargo/vendor/windows-implement-0.60.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf\", \"files\": {}}", + "dest": "cargo/vendor/windows-implement-0.60.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-interface/windows-interface-0.59.3.crate", + "sha256": "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358", + "dest": "cargo/vendor/windows-interface-0.59.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358\", \"files\": {}}", + "dest": "cargo/vendor/windows-interface-0.59.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate", + "sha256": "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5", + "dest": "cargo/vendor/windows-link-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5\", \"files\": {}}", + "dest": "cargo/vendor/windows-link-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-numerics/windows-numerics-0.3.1.crate", + "sha256": "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26", + "dest": "cargo/vendor/windows-numerics-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26\", \"files\": {}}", + "dest": "cargo/vendor/windows-numerics-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate", + "sha256": "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5", + "dest": "cargo/vendor/windows-result-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5\", \"files\": {}}", + "dest": "cargo/vendor/windows-result-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate", + "sha256": "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091", + "dest": "cargo/vendor/windows-strings-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091\", \"files\": {}}", + "dest": "cargo/vendor/windows-strings-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate", + "sha256": "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d", + "dest": "cargo/vendor/windows-sys-0.52.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.52.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate", + "sha256": "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b", + "dest": "cargo/vendor/windows-sys-0.59.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.59.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.60.2.crate", + "sha256": "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb", + "dest": "cargo/vendor/windows-sys-0.60.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.60.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate", + "sha256": "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc", + "dest": "cargo/vendor/windows-sys-0.61.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.61.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate", + "sha256": "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973", + "dest": "cargo/vendor/windows-targets-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973\", \"files\": {}}", + "dest": "cargo/vendor/windows-targets-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-targets/windows-targets-0.53.5.crate", + "sha256": "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3", + "dest": "cargo/vendor/windows-targets-0.53.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3\", \"files\": {}}", + "dest": "cargo/vendor/windows-targets-0.53.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-threading/windows-threading-0.2.1.crate", + "sha256": "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37", + "dest": "cargo/vendor/windows-threading-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37\", \"files\": {}}", + "dest": "cargo/vendor/windows-threading-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.52.6.crate", + "sha256": "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.53.1.crate", + "sha256": "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.52.6.crate", + "sha256": "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469", + "dest": "cargo/vendor/windows_aarch64_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.53.1.crate", + "sha256": "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006", + "dest": "cargo/vendor/windows_aarch64_msvc-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_msvc-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.52.6.crate", + "sha256": "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b", + "dest": "cargo/vendor/windows_i686_gnu-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnu-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.53.1.crate", + "sha256": "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3", + "dest": "cargo/vendor/windows_i686_gnu-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnu-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.52.6.crate", + "sha256": "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66", + "dest": "cargo/vendor/windows_i686_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.53.1.crate", + "sha256": "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c", + "dest": "cargo/vendor/windows_i686_gnullvm-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnullvm-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.52.6.crate", + "sha256": "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66", + "dest": "cargo/vendor/windows_i686_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.53.1.crate", + "sha256": "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2", + "dest": "cargo/vendor/windows_i686_msvc-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_msvc-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.52.6.crate", + "sha256": "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78", + "dest": "cargo/vendor/windows_x86_64_gnu-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnu-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.53.1.crate", + "sha256": "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499", + "dest": "cargo/vendor/windows_x86_64_gnu-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnu-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.52.6.crate", + "sha256": "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.53.1.crate", + "sha256": "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate", + "sha256": "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec", + "dest": "cargo/vendor/windows_x86_64_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.53.1.crate", + "sha256": "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650", + "dest": "cargo/vendor/windows_x86_64_msvc-0.53.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_msvc-0.53.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/winit-a517b38/.\" \"cargo/vendor/winit\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"winit\"\nversion = \"0.30.13\"\nauthors = [\"The winit contributors\", \"Pierre Krieger \"]\ndescription = \"Cross-platform window creation library.\"\nkeywords = [\"windowing\"]\nreadme = \"README.md\"\ndocumentation = \"https://docs.rs/winit\"\ncategories = [\"gui\"]\nrust-version = \"1.70.0\"\nrepository = \"https://github.com/rust-windowing/winit\"\nlicense = \"Apache-2.0\"\nedition = \"2021\"\ninclude = [\"/build.rs\", \"/docs\", \"/examples\", \"/FEATURES.md\", \"/LICENSE\", \"/src\", \"!/src/platform_impl/web/script\", \"/src/platform_impl/web/script/**/*.min.js\", \"/tests\"]\n\n[package.metadata.docs.rs]\nfeatures = [\"rwh_04\", \"rwh_05\", \"rwh_06\", \"serde\", \"mint\", \"android-native-activity\"]\ntargets = [\"i686-pc-windows-msvc\", \"x86_64-pc-windows-msvc\", \"x86_64-apple-darwin\", \"i686-unknown-linux-gnu\", \"x86_64-unknown-linux-gnu\", \"x86_64-apple-ios\", \"aarch64-linux-android\", \"wasm32-unknown-unknown\"]\nrustdoc-args = [\"--cfg\", \"docsrs\"]\n\n[features]\ndefault = [\"rwh_06\", \"x11\", \"wayland\", \"wayland-dlopen\", \"wayland-csd-adwaita\"]\nx11 = [\"x11-dl\", \"bytemuck\", \"percent-encoding\", \"xkbcommon-dl/x11\", \"x11rb\"]\nwayland = [\"wayland-client\", \"wayland-backend\", \"wayland-protocols\", \"wayland-protocols-plasma\", \"sctk\", \"ahash\", \"memmap2\", \"percent-encoding\"]\nwayland-dlopen = [\"wayland-backend/dlopen\"]\nwayland-csd-adwaita = [\"sctk-adwaita\", \"sctk-adwaita/ab_glyph\"]\nwayland-csd-adwaita-crossfont = [\"sctk-adwaita\", \"sctk-adwaita/crossfont\"]\nwayland-csd-adwaita-notitle = [\"sctk-adwaita\"]\nandroid-native-activity = [\"android-activity/native-activity\"]\nandroid-game-activity = [\"android-activity/game-activity\"]\nserde = [\"dep:serde\", \"cursor-icon/serde\", \"smol_str/serde\", \"dpi/serde\"]\nmint = [\"dpi/mint\"]\nrwh_04 = [\"dep:rwh_04\", \"ndk/rwh_04\"]\nrwh_05 = [\"dep:rwh_05\", \"ndk/rwh_05\"]\nrwh_06 = [\"dep:rwh_06\", \"ndk/rwh_06\"]\n\n[build-dependencies]\ncfg_aliases = \"0.2.1\"\n\n[dependencies]\nbitflags = \"2\"\ncursor-icon = \"1.1.0\"\ndpi = \"0.1.1\"\nsmol_str = \"0.2.0\"\n\n[dependencies.rwh_04]\npackage = \"raw-window-handle\"\nversion = \"0.4\"\noptional = true\n\n[dependencies.rwh_05]\npackage = \"raw-window-handle\"\nversion = \"0.5.2\"\nfeatures = [\"std\"]\noptional = true\n\n[dependencies.rwh_06]\npackage = \"raw-window-handle\"\nversion = \"0.6\"\nfeatures = [\"std\"]\noptional = true\n\n[dependencies.serde]\noptional = true\nversion = \"1\"\nfeatures = [\"serde_derive\"]\n\n[dependencies.tracing]\nversion = \"0.1.40\"\ndefault-features = false\n\n[dev-dependencies.image]\nversion = \"0.25.0\"\ndefault-features = false\nfeatures = [\"png\"]\n\n[dev-dependencies.tracing]\nversion = \"0.1.40\"\ndefault-features = false\nfeatures = [\"log\"]\n\n[dev-dependencies.tracing-subscriber]\nversion = \"0.3.18\"\nfeatures = [\"env-filter\"]\n\n[dev-dependencies.winit]\npath = \".\"\nfeatures = [\"rwh_05\"]\n\n[target.\"cfg(not(any(target_os = \\\"android\\\", target_os = \\\"ios\\\")))\".dev-dependencies.softbuffer]\nversion = \"0.4.0\"\ndefault-features = false\nfeatures = [\"x11\", \"x11-dlopen\", \"wayland\", \"wayland-dlopen\"]\n\n[target.\"cfg(target_os = \\\"android\\\")\".dependencies]\nandroid-activity = \"0.6.0\"\n\n[target.\"cfg(target_os = \\\"android\\\")\".dependencies.ndk]\nversion = \"0.9.0\"\ndefault-features = false\n\n[target.\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"macos\\\"))\".dependencies]\nblock2 = \"0.5.1\"\ncore-foundation = \"0.9.3\"\n\n[target.\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"macos\\\"))\".dependencies.objc2]\nversion = \"0.5.2\"\nfeatures = [\"relax-sign-encoding\"]\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies]\ncore-graphics = \"0.23.1\"\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies.objc2-foundation]\nversion = \"0.2.2\"\nfeatures = [\"block2\", \"dispatch\", \"NSArray\", \"NSAttributedString\", \"NSData\", \"NSDictionary\", \"NSDistributedNotificationCenter\", \"NSEnumerator\", \"NSKeyValueObserving\", \"NSNotification\", \"NSObjCRuntime\", \"NSPathUtilities\", \"NSProcessInfo\", \"NSRunLoop\", \"NSString\", \"NSThread\", \"NSValue\"]\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies.objc2-app-kit]\nversion = \"0.2.2\"\nfeatures = [\"NSAppearance\", \"NSApplication\", \"NSBitmapImageRep\", \"NSButton\", \"NSColor\", \"NSControl\", \"NSCursor\", \"NSDragging\", \"NSEvent\", \"NSGraphics\", \"NSGraphicsContext\", \"NSImage\", \"NSImageRep\", \"NSMenu\", \"NSMenuItem\", \"NSOpenGLView\", \"NSPasteboard\", \"NSResponder\", \"NSRunningApplication\", \"NSScreen\", \"NSTextInputClient\", \"NSTextInputContext\", \"NSView\", \"NSWindow\", \"NSWindowScripting\", \"NSWindowTabGroup\"]\n\n[target.\"cfg(target_os = \\\"ios\\\")\".dependencies.objc2-foundation]\nversion = \"0.2.2\"\nfeatures = [\"block2\", \"dispatch\", \"NSArray\", \"NSEnumerator\", \"NSGeometry\", \"NSObjCRuntime\", \"NSOperation\", \"NSString\", \"NSProcessInfo\", \"NSThread\", \"NSSet\"]\n\n[target.\"cfg(target_os = \\\"ios\\\")\".dependencies.objc2-ui-kit]\nversion = \"0.2.2\"\nfeatures = [\"UIApplication\", \"UIDevice\", \"UIEvent\", \"UIGeometry\", \"UIGestureRecognizer\", \"UITextInput\", \"UITextInputTraits\", \"UIOrientation\", \"UIPanGestureRecognizer\", \"UIPinchGestureRecognizer\", \"UIResponder\", \"UIRotationGestureRecognizer\", \"UIScreen\", \"UIScreenMode\", \"UITapGestureRecognizer\", \"UITouch\", \"UITraitCollection\", \"UIView\", \"UIViewController\", \"UIWindow\"]\n\n[target.\"cfg(target_os = \\\"windows\\\")\".dependencies]\nunicode-segmentation = \"1.7.1\"\n\n[target.\"cfg(target_os = \\\"windows\\\")\".dependencies.windows-sys]\nversion = \"0.52.0\"\nfeatures = [\"Win32_Devices_HumanInterfaceDevice\", \"Win32_Foundation\", \"Win32_Globalization\", \"Win32_Graphics_Dwm\", \"Win32_Graphics_Gdi\", \"Win32_Media\", \"Win32_System_Com_StructuredStorage\", \"Win32_System_Com\", \"Win32_System_LibraryLoader\", \"Win32_System_Ole\", \"Win32_Security\", \"Win32_System_SystemInformation\", \"Win32_System_SystemServices\", \"Win32_System_Threading\", \"Win32_System_WindowsProgramming\", \"Win32_UI_Accessibility\", \"Win32_UI_Controls\", \"Win32_UI_HiDpi\", \"Win32_UI_Input_Ime\", \"Win32_UI_Input_KeyboardAndMouse\", \"Win32_UI_Input_Pointer\", \"Win32_UI_Input_Touch\", \"Win32_UI_Shell\", \"Win32_UI_TextServices\", \"Win32_UI_WindowsAndMessaging\"]\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies]\ncalloop = \"0.13.0\"\nlibc = \"0.2.64\"\nxkbcommon-dl = \"0.4.2\"\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.ahash]\nversion = \"0.8.7\"\nfeatures = [\"no-rng\"]\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.bytemuck]\nversion = \"1.13.1\"\ndefault-features = false\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.memmap2]\nversion = \"0.9.0\"\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.percent-encoding]\nversion = \"2.0\"\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.rustix]\nversion = \"0.38.4\"\ndefault-features = false\nfeatures = [\"std\", \"system\", \"thread\", \"process\", \"event\", \"fs\", \"pipe\"]\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.sctk]\npackage = \"smithay-client-toolkit\"\nversion = \"0.19.2\"\ndefault-features = false\nfeatures = [\"calloop\"]\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.sctk-adwaita]\nversion = \"0.10.1\"\ndefault-features = false\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.wayland-backend]\nversion = \"0.3.10\"\ndefault-features = false\nfeatures = [\"client_system\"]\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.wayland-client]\nversion = \"0.31.10\"\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.wayland-protocols]\nversion = \"0.32.8\"\nfeatures = [\"staging\"]\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.wayland-protocols-plasma]\nversion = \"0.3.8\"\nfeatures = [\"client\"]\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.x11-dl]\nversion = \"2.19.1\"\noptional = true\n\n[target.\"cfg(all(unix, not(any(target_os = \\\"redox\\\", target_family = \\\"wasm\\\", target_os = \\\"android\\\", target_os = \\\"ios\\\", target_os = \\\"macos\\\"))))\".dependencies.x11rb]\nversion = \"0.13.0\"\ndefault-features = false\nfeatures = [\"allow-unsafe-code\", \"dl-libxcb\", \"randr\", \"resource_manager\", \"xinput\", \"xkb\"]\noptional = true\n\n[target.\"cfg(target_os = \\\"redox\\\")\".dependencies]\nredox_syscall = \"0.4.1\"\n\n[target.\"cfg(target_os = \\\"redox\\\")\".dependencies.orbclient]\nversion = \"0.3.47\"\ndefault-features = false\n\n[target.\"cfg(target_family = \\\"wasm\\\")\".dependencies]\njs-sys = \"0.3.70\"\npin-project = \"1\"\nwasm-bindgen = \"0.2.93\"\nwasm-bindgen-futures = \"0.4.43\"\nweb-time = \"1\"\n\n[target.\"cfg(target_family = \\\"wasm\\\")\".dependencies.web_sys]\npackage = \"web-sys\"\nversion = \"0.3.70\"\nfeatures = [\"AbortController\", \"AbortSignal\", \"Blob\", \"BlobPropertyBag\", \"console\", \"CssStyleDeclaration\", \"Document\", \"DomException\", \"DomRect\", \"DomRectReadOnly\", \"Element\", \"Event\", \"EventTarget\", \"FocusEvent\", \"HtmlCanvasElement\", \"HtmlElement\", \"HtmlImageElement\", \"ImageBitmap\", \"ImageBitmapOptions\", \"ImageBitmapRenderingContext\", \"ImageData\", \"IntersectionObserver\", \"IntersectionObserverEntry\", \"KeyboardEvent\", \"MediaQueryList\", \"MessageChannel\", \"MessagePort\", \"Navigator\", \"Node\", \"OrientationLockType\", \"OrientationType\", \"PageTransitionEvent\", \"Permissions\", \"PermissionState\", \"PermissionStatus\", \"PointerEvent\", \"PremultiplyAlpha\", \"ResizeObserver\", \"ResizeObserverBoxOptions\", \"ResizeObserverEntry\", \"ResizeObserverOptions\", \"ResizeObserverSize\", \"Screen\", \"ScreenOrientation\", \"Url\", \"VisibilityState\", \"WheelEvent\", \"Window\", \"Worker\"]\n\n[target.\"cfg(target_family = \\\"wasm\\\")\".dev-dependencies]\nconsole_error_panic_hook = \"0.1\"\ntracing-web = \"0.1\"\n\n[target.\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\".dependencies]\natomic-waker = \"1\"\n\n[target.\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\".dependencies.concurrent-queue]\nversion = \"2\"\ndefault-features = false\n\n[[example]]\ndoc-scrape-examples = true\nname = \"window\"\n\n[workspace]\nresolver = \"2\"\nmembers = [\"dpi\"]\n\n[workspace.package]\nrust-version = \"1.70.0\"\nrepository = \"https://github.com/rust-windowing/winit\"\nlicense = \"Apache-2.0\"\nedition = \"2021\"\n\n[workspace.dependencies]\nmint = \"0.5.6\"\n\n[workspace.dependencies.serde]\nversion = \"1\"\nfeatures = [\"serde_derive\"]\n\n[[test]]\nname = \"macos_termination\"\npath = \"tests/macos_termination.rs\"\nharness = false\n\n[[test]]\nname = \"ime_reset_dispatch\"\npath = \"tests/ime_reset_dispatch.rs\"\nharness = false\n", + "dest": "cargo/vendor/winit", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/winit", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winnow/winnow-1.0.4.crate", + "sha256": "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81", + "dest": "cargo/vendor/winnow-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81\", \"files\": {}}", + "dest": "cargo/vendor/winnow-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winresource/winresource-0.1.31.crate", + "sha256": "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087", + "dest": "cargo/vendor/winresource-0.1.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087\", \"files\": {}}", + "dest": "cargo/vendor/winresource-0.1.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wit-bindgen/wit-bindgen-0.57.1.crate", + "sha256": "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e", + "dest": "cargo/vendor/wit-bindgen-0.57.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e\", \"files\": {}}", + "dest": "cargo/vendor/wit-bindgen-0.57.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wl-clipboard-rs/wl-clipboard-rs-0.9.4.crate", + "sha256": "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1", + "dest": "cargo/vendor/wl-clipboard-rs-0.9.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1\", \"files\": {}}", + "dest": "cargo/vendor/wl-clipboard-rs-0.9.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/x11-dl/x11-dl-2.21.0.crate", + "sha256": "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f", + "dest": "cargo/vendor/x11-dl-2.21.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f\", \"files\": {}}", + "dest": "cargo/vendor/x11-dl-2.21.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/x11rb/x11rb-0.13.2.crate", + "sha256": "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414", + "dest": "cargo/vendor/x11rb-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414\", \"files\": {}}", + "dest": "cargo/vendor/x11rb-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/x11rb-protocol/x11rb-protocol-0.13.2.crate", + "sha256": "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd", + "dest": "cargo/vendor/x11rb-protocol-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd\", \"files\": {}}", + "dest": "cargo/vendor/x11rb-protocol-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/x25519-dalek/x25519-dalek-2.0.1.crate", + "sha256": "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277", + "dest": "cargo/vendor/x25519-dalek-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277\", \"files\": {}}", + "dest": "cargo/vendor/x25519-dalek-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/xcursor/xcursor-0.3.11.crate", + "sha256": "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23", + "dest": "cargo/vendor/xcursor-0.3.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23\", \"files\": {}}", + "dest": "cargo/vendor/xcursor-0.3.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/xkbcommon-dl/xkbcommon-dl-0.4.2.crate", + "sha256": "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5", + "dest": "cargo/vendor/xkbcommon-dl-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5\", \"files\": {}}", + "dest": "cargo/vendor/xkbcommon-dl-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/xkeysym/xkeysym-0.2.1.crate", + "sha256": "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56", + "dest": "cargo/vendor/xkeysym-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56\", \"files\": {}}", + "dest": "cargo/vendor/xkeysym-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/xml-rs/xml-rs-0.8.29.crate", + "sha256": "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7", + "dest": "cargo/vendor/xml-rs-0.8.29" + }, + { + "type": "inline", + "contents": "{\"package\": \"e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7\", \"files\": {}}", + "dest": "cargo/vendor/xml-rs-0.8.29", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/y4m/y4m-0.8.0.crate", + "sha256": "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448", + "dest": "cargo/vendor/y4m-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448\", \"files\": {}}", + "dest": "cargo/vendor/y4m-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke/yoke-0.7.5.crate", + "sha256": "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40", + "dest": "cargo/vendor/yoke-0.7.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40\", \"files\": {}}", + "dest": "cargo/vendor/yoke-0.7.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke/yoke-0.8.3.crate", + "sha256": "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5", + "dest": "cargo/vendor/yoke-0.8.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5\", \"files\": {}}", + "dest": "cargo/vendor/yoke-0.8.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke-derive/yoke-derive-0.7.5.crate", + "sha256": "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154", + "dest": "cargo/vendor/yoke-derive-0.7.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154\", \"files\": {}}", + "dest": "cargo/vendor/yoke-derive-0.7.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke-derive/yoke-derive-0.8.4.crate", + "sha256": "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71", + "dest": "cargo/vendor/yoke-derive-0.8.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71\", \"files\": {}}", + "dest": "cargo/vendor/yoke-derive-0.8.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus/zbus-5.19.0.crate", + "sha256": "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907", + "dest": "cargo/vendor/zbus-5.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907\", \"files\": {}}", + "dest": "cargo/vendor/zbus-5.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus-lockstep/zbus-lockstep-0.5.2.crate", + "sha256": "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863", + "dest": "cargo/vendor/zbus-lockstep-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863\", \"files\": {}}", + "dest": "cargo/vendor/zbus-lockstep-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus-lockstep-macros/zbus-lockstep-macros-0.5.2.crate", + "sha256": "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0", + "dest": "cargo/vendor/zbus-lockstep-macros-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0\", \"files\": {}}", + "dest": "cargo/vendor/zbus-lockstep-macros-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_macros/zbus_macros-5.19.0.crate", + "sha256": "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40", + "dest": "cargo/vendor/zbus_macros-5.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40\", \"files\": {}}", + "dest": "cargo/vendor/zbus_macros-5.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_names/zbus_names-4.3.4.crate", + "sha256": "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e", + "dest": "cargo/vendor/zbus_names-4.3.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e\", \"files\": {}}", + "dest": "cargo/vendor/zbus_names-4.3.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_xml/zbus_xml-5.2.1.crate", + "sha256": "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a", + "dest": "cargo/vendor/zbus_xml-5.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a\", \"files\": {}}", + "dest": "cargo/vendor/zbus_xml-5.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zcheapstr/zcheapstr-1.1.0.crate", + "sha256": "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473", + "dest": "cargo/vendor/zcheapstr-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473\", \"files\": {}}", + "dest": "cargo/vendor/zcheapstr-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerocopy/zerocopy-0.8.59.crate", + "sha256": "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb", + "dest": "cargo/vendor/zerocopy-0.8.59" + }, + { + "type": "inline", + "contents": "{\"package\": \"6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb\", \"files\": {}}", + "dest": "cargo/vendor/zerocopy-0.8.59", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerocopy-derive/zerocopy-derive-0.8.59.crate", + "sha256": "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82", + "dest": "cargo/vendor/zerocopy-derive-0.8.59" + }, + { + "type": "inline", + "contents": "{\"package\": \"ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82\", \"files\": {}}", + "dest": "cargo/vendor/zerocopy-derive-0.8.59", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerofrom/zerofrom-0.1.8.crate", + "sha256": "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272", + "dest": "cargo/vendor/zerofrom-0.1.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272\", \"files\": {}}", + "dest": "cargo/vendor/zerofrom-0.1.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerofrom-derive/zerofrom-derive-0.1.8.crate", + "sha256": "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a", + "dest": "cargo/vendor/zerofrom-derive-0.1.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a\", \"files\": {}}", + "dest": "cargo/vendor/zerofrom-derive-0.1.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zeroize/zeroize-1.9.1.crate", + "sha256": "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879", + "dest": "cargo/vendor/zeroize-1.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879\", \"files\": {}}", + "dest": "cargo/vendor/zeroize-1.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zeroize_derive/zeroize_derive-1.5.0.crate", + "sha256": "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328", + "dest": "cargo/vendor/zeroize_derive-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328\", \"files\": {}}", + "dest": "cargo/vendor/zeroize_derive-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zip/zip-7.2.0.crate", + "sha256": "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0", + "dest": "cargo/vendor/zip-7.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0\", \"files\": {}}", + "dest": "cargo/vendor/zip-7.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zlib-rs/zlib-rs-0.6.8.crate", + "sha256": "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112", + "dest": "cargo/vendor/zlib-rs-0.6.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112\", \"files\": {}}", + "dest": "cargo/vendor/zlib-rs-0.6.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zmij/zmij-1.0.23.crate", + "sha256": "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b", + "dest": "cargo/vendor/zmij-1.0.23" + }, + { + "type": "inline", + "contents": "{\"package\": \"29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b\", \"files\": {}}", + "dest": "cargo/vendor/zmij-1.0.23", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zune-core/zune-core-0.5.3.crate", + "sha256": "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b", + "dest": "cargo/vendor/zune-core-0.5.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b\", \"files\": {}}", + "dest": "cargo/vendor/zune-core-0.5.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zune-jpeg/zune-jpeg-0.5.15.crate", + "sha256": "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296", + "dest": "cargo/vendor/zune-jpeg-0.5.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296\", \"files\": {}}", + "dest": "cargo/vendor/zune-jpeg-0.5.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zune-jpegxl/zune-jpegxl-0.5.2.crate", + "sha256": "cba11ecd07cc23351500e840ae03841b7e4997923ec8e4832ee3ae199ea0b6b4", + "dest": "cargo/vendor/zune-jpegxl-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cba11ecd07cc23351500e840ae03841b7e4997923ec8e4832ee3ae199ea0b6b4\", \"files\": {}}", + "dest": "cargo/vendor/zune-jpegxl-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant/zvariant-5.15.0.crate", + "sha256": "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147", + "dest": "cargo/vendor/zvariant-5.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147\", \"files\": {}}", + "dest": "cargo/vendor/zvariant-5.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant_derive/zvariant_derive-5.15.0.crate", + "sha256": "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497", + "dest": "cargo/vendor/zvariant_derive-5.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497\", \"files\": {}}", + "dest": "cargo/vendor/zvariant_derive-5.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant_utils/zvariant_utils-4.2.0.crate", + "sha256": "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3", + "dest": "cargo/vendor/zvariant_utils-4.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3\", \"files\": {}}", + "dest": "cargo/vendor/zvariant_utils-4.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_demo_lib\" \"cargo/vendor/egui_demo_lib\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_demo_lib\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Example library for egui\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/egui_demo_lib\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/egui_demo_lib\"\ncategories = [\"gui\", \"graphics\"]\nkeywords = [\"glow\", \"egui\", \"gui\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\", \"data/*\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[package.metadata.cargo-shear]\nignored = [\"image\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = []\njiff = [\"egui_extras/datepicker\", \"dep:jiff\"]\nserde = [\"egui/serde\", \"dep:serde\", \"egui_extras/serde\"]\nsyntect = [\"egui_extras/syntect\"]\n\n[dependencies.egui]\ndefault-features = false\nfeatures = [\"color-hex\", \"default_fonts\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.egui_extras]\nfeatures = [\"image\", \"svg\"]\nversion = \"0.37.0\"\npath = \"crates/egui_extras\"\ndefault-features = false\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.unicode_names2]\nversion = \"3.1\"\ndefault-features = false\n\n[dependencies.jiff]\noptional = true\nfeatures = [\"std\", \"js\"]\nversion = \"0.2.35\"\ndefault-features = false\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[dev-dependencies]\nmimalloc = \"0.1.52\"\nrand = \"0.10.2\"\n\n[dev-dependencies.criterion]\nversion = \"0.8.2\"\ndefault-features = false\n\n[dev-dependencies.egui]\nfeatures = [\"default_fonts\", \"monochrome_emoji_fonts\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[dev-dependencies.egui_extras]\nfeatures = [\"image\", \"svg\"]\nversion = \"0.37.0\"\npath = \"crates/egui_extras\"\ndefault-features = false\n\n[dev-dependencies.egui_kittest]\nfeatures = [\"wgpu\", \"snapshot\"]\nversion = \"0.37.0\"\npath = \"crates/egui_kittest\"\ndefault-features = false\n\n[dev-dependencies.image]\nfeatures = [\"png\"]\nversion = \"0.25.10\"\ndefault-features = false\n\n[[bench]]\nname = \"benchmark\"\nharness = false\n", + "dest": "cargo/vendor/egui_demo_lib", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_demo_lib", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_extras\" \"cargo/vendor/egui_extras\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_extras\"\nversion = \"0.37.0\"\nauthors = [\"Dominik R\u00f6ssler \", \"Emil Ernerfeldt \", \"Ren\u00e9 R\u00f6ssler \"]\ndescription = \"Extra functionality and widgets for the egui GUI library\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui\"\ncategories = [\"gui\", \"game-development\"]\nkeywords = [\"gui\", \"imgui\", \"immediate\", \"portable\", \"gamedev\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[features]\ndefault = [\"dep:mime_guess2\"]\nall_loaders = [\"file\", \"http\", \"image\", \"svg\", \"gif\", \"webp\"]\ndatepicker = [\"jiff\"]\nfile = [\"dep:mime_guess2\"]\ngif = [\"image\", \"image/gif\"]\nwebp = [\"image\", \"image/webp\"]\nhttp = [\"dep:ehttp\"]\nimage = [\"dep:image\"]\nserde = [\"egui/serde\", \"enum-map/serde\", \"dep:serde\"]\nsvg = [\"resvg\"]\nsvg_text = [\"svg\", \"resvg/text\", \"resvg/system-fonts\"]\nsvg_images = [\"svg\", \"resvg/raster-images\"]\nsyntect = [\"dep:syntect\"]\n\n[dependencies]\nenum-map = \"3.1\"\nitertools = \"0.15.0\"\n\n[dependencies.egui]\ndefault-features = false\nversion = \"0.37.0\"\npath = \"crates/egui\"\n\n[dependencies.ahash]\nversion = \"0.8.12\"\ndefault-features = false\nfeatures = [\"no-rng\", \"std\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.profiling]\nversion = \"1.0\"\ndefault-features = false\n\n[dependencies.serde]\noptional = true\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[dependencies.jiff]\noptional = true\nfeatures = [\"std\", \"tz-system\", \"js\"]\nversion = \"0.2.35\"\ndefault-features = false\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[dependencies.image]\noptional = true\nversion = \"0.25.10\"\ndefault-features = false\n\n[dependencies.mime_guess2]\noptional = true\nversion = \"2.3\"\ndefault-features = false\n\n[dependencies.syntect]\noptional = true\nfeatures = [\"default-fancy\"]\nversion = \"5.3\"\ndefault-features = false\n\n[dependencies.resvg]\noptional = true\nversion = \"0.45.1\"\ndefault-features = false\n\n[dependencies.ehttp]\noptional = true\nversion = \"0.7.1\"\ndefault-features = false\n\n[dev-dependencies]\ntempfile = \"3.27\"\n\n[dev-dependencies.egui]\nfeatures = [\"default_fonts\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[dev-dependencies.egui_kittest]\npath = \"../egui_kittest\"\nfeatures = [\"snapshot\", \"wgpu\"]\n", + "dest": "cargo/vendor/egui_extras", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_extras", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_inspection\" \"cargo/vendor/egui_inspection\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_inspection\"\nversion = \"0.37.0\"\nauthors = [\"Lucas Meurer \", \"Emil Ernerfeldt \"]\ndescription = \"Protocol and egui::Plugin for inspection of egui apps\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"./README.md\"\nrepository = \"https://github.com/emilk/egui\"\ncategories = [\"gui\", \"development-tools::testing\", \"accessibility\"]\nkeywords = [\"gui\", \"egui\", \"inspector\", \"accesskit\", \"mcp\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[features]\ndefault = []\npng = [\"dep:image\", \"image/png\", \"egui/bytemuck\"]\nplugin = [\"png\", \"dep:log\"]\n\n[dependencies]\nserde_bytes = \"0.11.19\"\nrmp-serde = \"1.3\"\n\n[dependencies.egui]\nfeatures = [\"serde\"]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n\n[dependencies.serde]\nversion = \"1.0\"\nfeatures = [\"derive\"]\n\n[dependencies.image]\noptional = true\nversion = \"0.25.10\"\ndefault-features = false\n\n[dependencies.log]\noptional = true\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.document-features]\noptional = true\nversion = \"0.2.12\"\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n", + "dest": "cargo/vendor/egui_inspection", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_inspection", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "shell", + "commands": [ + "cp -r --reflink=auto \"flatpak-cargo/git/egui-6d518fc/crates/egui_system_font_provider\" \"cargo/vendor/egui_system_font_provider\"" + ] + }, + { + "type": "inline", + "contents": "[package]\nname = \"egui_system_font_provider\"\nversion = \"0.37.0\"\nauthors = [\"Emil Ernerfeldt \"]\ndescription = \"Load system fonts on demand for characters the egui fonts lack\"\nedition = \"2024\"\nrust-version = \"1.95\"\nhomepage = \"https://github.com/emilk/egui/tree/main/crates/egui_system_font_provider\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://github.com/emilk/egui/tree/main/crates/egui_system_font_provider\"\ncategories = [\"gui\"]\nkeywords = [\"egui\", \"gui\", \"fonts\", \"text\"]\ninclude = [\"../../LICENSE-APACHE\", \"../../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\nrustdoc-args = [\"--generate-link-to-definition\"]\n\n[lints.rust]\nunsafe_code = \"deny\"\ntrivial_numeric_casts = \"warn\"\nunused_import_braces = \"warn\"\nunused_lifetimes = \"warn\"\ntrivial_casts = \"allow\"\nunused_qualifications = \"allow\"\n\n[lints.rust.future_incompatible]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.nonstandard_style]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.rust_2018_idioms]\nlevel = \"warn\"\npriority = -1\n\n[lints.rust.unexpected_cfgs]\nlevel = \"warn\"\ncheck-cfg = [\"cfg(web_sys_unstable_apis)\"]\n\n[lints.rustdoc]\nall = \"warn\"\n\n[lints.clippy]\nallow_attributes = \"warn\"\nas_ptr_cast_mut = \"warn\"\nbranches_sharing_code = \"warn\"\nclear_with_drain = \"warn\"\nclone_on_ref_ptr = \"warn\"\ncoerce_container_to_any = \"warn\"\ndbg_macro = \"warn\"\ndebug_assert_with_mut_call = \"warn\"\ndefault_union_representation = \"warn\"\nderive_partial_eq_without_eq = \"warn\"\ndisallowed_script_idents = \"warn\"\ndoc_include_without_cfg = \"warn\"\nempty_enum_variants_with_brackets = \"warn\"\nequatable_if_let = \"warn\"\nexit = \"warn\"\nfallible_impl_from = \"warn\"\nfloat_cmp_const = \"warn\"\nfn_to_numeric_cast_any = \"warn\"\nget_unwrap = \"warn\"\nimprecise_flops = \"warn\"\ninfinite_loop = \"warn\"\niter_on_empty_collections = \"warn\"\niter_on_single_items = \"warn\"\niter_over_hash_type = \"warn\"\nlarge_include_file = \"warn\"\nlarge_stack_frames = \"warn\"\nliteral_string_with_formatting_args = \"warn\"\nlossy_float_literal = \"warn\"\nmap_err_ignore = \"warn\"\nmem_forget = \"warn\"\nmissing_assert_message = \"warn\"\nmutex_integer = \"warn\"\nneedless_pass_by_ref_mut = \"warn\"\nneedless_type_cast = \"warn\"\nnon_zero_suggestions = \"warn\"\nnonstandard_macro_braces = \"warn\"\nor_fun_call = \"warn\"\npath_buf_push_overwrite = \"warn\"\npathbuf_init_then_push = \"warn\"\nprecedence_bits = \"warn\"\nprint_stderr = \"warn\"\nprint_stdout = \"warn\"\npub_without_shorthand = \"warn\"\nrc_mutex = \"warn\"\nredundant_type_annotations = \"warn\"\nref_patterns = \"warn\"\nrest_pat_in_fully_bound_structs = \"warn\"\nreturn_and_then = \"warn\"\nset_contains_or_insert = \"warn\"\nsingle_option_map = \"warn\"\nstd_instead_of_core = \"warn\"\nstr_to_string = \"warn\"\nstring_add = \"warn\"\nstring_lit_as_bytes = \"warn\"\nstring_lit_chars_any = \"warn\"\nsuspicious_xor_used_as_pow = \"warn\"\ntodo = \"warn\"\ntoo_long_first_doc_paragraph = \"warn\"\ntrailing_empty_array = \"warn\"\ntrait_duplication_in_bounds = \"warn\"\ntuple_array_conversions = \"warn\"\nundocumented_unsafe_blocks = \"warn\"\nunimplemented = \"warn\"\nuninhabited_references = \"warn\"\nunnecessary_safety_comment = \"warn\"\nunnecessary_safety_doc = \"warn\"\nunnecessary_self_imports = \"warn\"\nunnecessary_struct_initialization = \"warn\"\nunused_peekable = \"warn\"\nunused_rounding = \"warn\"\nunused_trait_names = \"warn\"\nunwrap_used = \"warn\"\nuse_self = \"warn\"\nuseless_let_if_seq = \"warn\"\nverbose_file_reads = \"warn\"\ncast_lossless = \"allow\"\ncast_possible_truncation = \"allow\"\ncast_possible_wrap = \"allow\"\ncast_precision_loss = \"allow\"\ncast_sign_loss = \"allow\"\ncomparison_chain = \"allow\"\ndefault_trait_access = \"allow\"\nfloat_cmp = \"allow\"\ninline_always = \"allow\"\nitems_after_statements = \"allow\"\nmany_single_char_names = \"allow\"\nmissing_panics_doc = \"allow\"\nmust_use_candidate = \"allow\"\nredundant_closure_for_method_calls = \"allow\"\nreturn_self_not_must_use = \"allow\"\nshould_panic_without_expect = \"allow\"\nsimilar_names = \"allow\"\nstruct_excessive_bools = \"allow\"\nstruct_field_names = \"allow\"\ntoo_many_lines = \"allow\"\ntrivially_copy_pass_by_ref = \"allow\"\nunreadable_literal = \"allow\"\nused_underscore_binding = \"allow\"\nassigning_clones = \"allow\"\nmanual_range_contains = \"allow\"\nmap_unwrap_or = \"allow\"\nmultiple_crate_versions = \"allow\"\nwildcard_imports = \"allow\"\nlet_underscore_must_use = \"allow\"\nlet_underscore_untyped = \"allow\"\nself_named_module_files = \"allow\"\nsignificant_drop_tightening = \"allow\"\n\n[lints.clippy.all]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.cargo]\nlevel = \"warn\"\npriority = -1\n\n[lints.clippy.pedantic]\nlevel = \"warn\"\npriority = -1\n\n[lib]\n\n[dependencies]\nsys-locale = \"0.3.2\"\nunicode-script = \"0.5.8\"\n\n[dependencies.epaint]\nfeatures = [\"color_fonts\"]\nversion = \"0.37.0\"\npath = \"crates/epaint\"\ndefault-features = false\n\n[dependencies.fontique]\nversion = \"0.11.1\"\ndefault-features = false\nfeatures = [\"system\", \"fontconfig-dlopen\"]\n\n[dependencies.log]\nversion = \"0.4.34\"\nfeatures = [\"std\"]\n\n[dependencies.poll-promise]\nversion = \"0.3.0\"\ndefault-features = false\n\n[dependencies.skrifa]\nversion = \"0.44.0\"\ndefault-features = false\nfeatures = [\"std\", \"autohint_shaping\"]\n\n[dev-dependencies.egui]\nversion = \"0.37.0\"\npath = \"crates/egui\"\ndefault-features = false\n", + "dest": "cargo/vendor/egui_system_font_provider", + "dest-filename": "Cargo.toml" + }, + { + "type": "inline", + "contents": "{\"package\": null, \"files\": {}}", + "dest": "cargo/vendor/egui_system_font_provider", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "inline", + "contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.\"https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63\"]\ngit = \"https://github.com/storytold/craft-libs\"\nreplace-with = \"vendored-sources\"\nrev = \"82a24b006eefa036b43c09a67fb9a3fbd17ffe63\"\n\n[source.\"https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f\"]\ngit = \"https://github.com/storytold/craft-libs\"\nreplace-with = \"vendored-sources\"\nrev = \"25da1314d443171b4adeaad2cd9454b273f35c6f\"\n\n[source.\"https://github.com/storytold/egui.git?rev=6d518fc799681571963842f95b55a6b68431b15a\"]\ngit = \"https://github.com/storytold/egui\"\nreplace-with = \"vendored-sources\"\nrev = \"6d518fc799681571963842f95b55a6b68431b15a\"\n\n[source.\"https://github.com/storytold/winit.git?rev=a517b3846ee8457d84ee4975110bfda6797c44dc\"]\ngit = \"https://github.com/storytold/winit\"\nreplace-with = \"vendored-sources\"\nrev = \"a517b3846ee8457d84ee4975110bfda6797c44dc\"\n", + "dest": "cargo", + "dest-filename": "config" + } +] \ No newline at end of file diff --git a/packaging/linux/flatpak/generate-cargo-sources.py b/packaging/linux/flatpak/generate-cargo-sources.py new file mode 100755 index 0000000000..43ed785a2a --- /dev/null +++ b/packaging/linux/flatpak/generate-cargo-sources.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 +"""Regenerate cargo-sources.json for the from-source Flatpak manifest. + +The upstream flatpak-cargo-generator is downloaded from a pinned commit and +verified before use; the full script is intentionally not vendored here. Two +small, reviewed patches are applied in memory: + +1. Key cargo source replacements by the full source URL (including ?rev=...) + instead of only the repository URL, so two different revisions of the same + repository stay distinct. +2. Include Cargo.lock's [patch.unused] entries, so crates such as egui_demo_lib + are vendored even though they are not in the resolved package graph. + +This only produces and validates the source inventory; a full Flatpak SDK +build is still required. See README.md in this directory for details. + +The generator runs with stdin connected to /dev/null and is capped at +GENERATOR_TIMEOUT_SECONDS. On POSIX (the Flatpak build path) it starts in its +own session, so a timeout, Ctrl-C (SIGINT) or SIGTERM terminates the generator +and its git descendants without touching any other process; the recorded signal +is replayed with the handler that was installed before generation started, so +the wrapper keeps its normal cancellation behaviour. On Windows only the direct +generator process is terminated; descendants are not tree-killed. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +from urllib.request import urlopen + +UPSTREAM_COMMIT = "9d86d336a39b9af38b86f114b6ce5820fe260f91" +UPSTREAM_URL = ( + "https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/" + f"{UPSTREAM_COMMIT}/cargo/flatpak-cargo-generator.py" +) +UPSTREAM_SHA256 = "0a2db6be87d75910facef28ab46d4d6460802e8419ab850d0caa6a364d26b380" +PATCHED_SHA256 = "1e7c61d98d18e88156fb8cd4b91f526e04a6c504b185ae7b12f775ec1dd48512" + +# Allow cold git caches while bounding stalled inventory regeneration. +GENERATOR_TIMEOUT_SECONDS = 30 * 60 + +# Signals that cancel regeneration without an exception of their own. +CANCELLATION_SIGNALS = (signal.SIGINT, signal.SIGTERM) + +PATCHES = ( + ( + "preserve pinned source identities", + ''' elif branch: + assert len(branch) == 1 + cargo_vendored_entry[repo_url]["branch"] = branch[0] + + logging.info("Adding package %s from %s", name, repo_url) +''', + ''' elif branch: + assert len(branch) == 1 + cargo_vendored_entry[repo_url]["branch"] = branch[0] + + # Preserve distinct pinned source identities from the same repository. + if rev or tag or branch: + source_key = source.removeprefix("git+").split("#", 1)[0] + cargo_vendored_entry = {source_key: cargo_vendored_entry[repo_url]} + + logging.info("Adding package %s from %s", name, repo_url) +''', + ), + ( + "include Cargo.lock patch.unused entries", + ''' pkg_coros = [ + get_package_sources(p, cargo_lock, git_repos) for p in cargo_lock["package"] + ] +''', + ''' pkg_coros = [ + get_package_sources(p, cargo_lock, git_repos) + for p in cargo_lock["package"] + cargo_lock.get("patch", {}).get("unused", []) + ] +''', + ), +) + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def download_upstream() -> bytes: + with urlopen(UPSTREAM_URL, timeout=60) as response: + data = response.read(1_000_001) + if len(data) > 1_000_000: + raise SystemExit("error: upstream generator is unexpectedly large") + actual = sha256(data) + if actual != UPSTREAM_SHA256: + raise SystemExit( + "error: upstream generator checksum mismatch\n" + f" expected {UPSTREAM_SHA256}\n" + f" actual {actual}\n" + f" url {UPSTREAM_URL}" + ) + return data + + +def patched_upstream(data: bytes) -> bytes: + text = data.decode("utf-8") + for name, old, new in PATCHES: + count = text.count(old) + if count != 1: + raise SystemExit( + f"error: patch {name!r} matched {count} times, expected exactly once; " + "the pinned upstream script or patch set changed" + ) + text = text.replace(old, new, 1) + patched = text.encode("utf-8") + actual = sha256(patched) + if actual != PATCHED_SHA256: + raise SystemExit( + "error: patched generator checksum mismatch\n" + f" expected {PATCHED_SHA256}\n" + f" actual {actual}" + ) + return patched + + +def _kill_generator(process: subprocess.Popen) -> None: + """Signal only our generator session; never waits or reaps. + + The reaped check is a plain ``returncode`` read: calling ``Popen.poll`` or + ``Popen.kill`` would wait for the child, which is not safe inside a POSIX + signal handler. On POSIX the generator is a session leader, so its PID is + also the ID of a process group that contains no process we do not own. + Killing that group stops git clones/fetches the generator started. On + Windows only the direct generator process is killed; its descendants may + survive. + """ + if process.returncode is not None: + return + if os.name == "posix": + try: + os.killpg(process.pid, signal.SIGKILL) + except OSError: + # The group is already gone; still try the direct child in case it + # was not a group leader. + try: + os.kill(process.pid, signal.SIGKILL) + except OSError: + pass + else: + try: + process.kill() + except OSError: + # The child exited between the timeout and the kill. + pass + + +def _terminate_generator(process: subprocess.Popen) -> None: + """Stop only our generator session and reap its leader. + + Only called outside signal handlers; the follow-up ``wait`` reaps the + session leader so no zombie or live generator outlives the wrapper. + ``_kill_generator`` skips a child that was already reaped, so this cannot + signal a recycled PID. + """ + _kill_generator(process) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + print( + f"warning: generator process {process.pid} did not exit after SIGKILL", + file=sys.stderr, + ) + + +class _GeneratorRun: + """State shared with the temporary cancellation signal handlers. + + Because the generator runs in its own session, Ctrl-C in the terminal or a + SIGTERM sent to the wrapper no longer reaches it. These handlers record the + signal and stop exactly that session without waiting; ``run_generator`` + then restores the previous handlers and reports the cancellation, so + ``main`` can clean up before the signal is replayed with the caller's own + behaviour. The wait that follows always reaps the leader. + """ + + def __init__(self) -> None: + self.process = None + self.received = None + self.killed = False + + def cancel(self, signum, _frame) -> None: + """Record the first SIGINT/SIGTERM and stop the generator's session.""" + if self.received is None: + self.received = signum + process = self.process + # ``_kill_generator`` reads only ``returncode`` and uses raw POSIX + # signals, so it is safe in a handler. The first signal does the kill; + # later signals must not overwrite the recorded cancellation. + if process is not None and not self.killed: + self.killed = True + _kill_generator(process) + + +class _Cancelled(Exception): + """A cancellation signal arrived while the generator was running.""" + + def __init__(self, signum: int) -> None: + super().__init__(signum) + self.signum = signum + + +def _replay_cancellation(signum: int) -> None: + """Re-deliver a recorded cancellation through the restored handler. + + Called once the temporary handlers are restored and the temporary file and + directory are gone. ``raise_signal`` invokes exactly the handler currently + installed (``KeyboardInterrupt`` for the default SIGINT action, the default + terminating action for SIGTERM, or ``SIG_IGN``), so no handler-kind + branching is needed. + """ + signal.raise_signal(int(signum)) + + +def run_generator( + *, script: Path, cargo_lock: Path, temp_output: str, cwd: Path, environment: dict +) -> int: + """Run the upstream generator in its own session; return its exit status. + + A cancellation or any other error stops the generator's whole session + before it is reported, so no detached generator or git process survives. + """ + run = _GeneratorRun() + previous_handlers = {} + try: + # Install the handlers before Popen: a signal that arrives while the + # child is being spawned (or between Popen returning and the assignment + # below) must not leave an unmanaged session behind. It is recorded, + # and the newly created process is stopped as soon as its PID is known. + # Installing inside the outer try means a partial installation is + # restored even if a later signal.signal() call fails unexpectedly. + for signum in CANCELLATION_SIGNALS: + try: + previous_handlers[signum] = signal.signal(signum, run.cancel) + except (OSError, ValueError): + # signal.signal() only works in the main thread of the main + # interpreter; the exception cleanup below still stops the + # detached generator on KeyboardInterrupt elsewhere. + pass + process = subprocess.Popen( + [sys.executable, str(script), str(cargo_lock), "-o", temp_output], + cwd=str(cwd), + env=environment, + stdin=subprocess.DEVNULL, + start_new_session=os.name == "posix", + ) + run.process = process + if run.received is not None: + _terminate_generator(process) + try: + process.wait(timeout=GENERATOR_TIMEOUT_SECONDS) + except subprocess.TimeoutExpired: + _terminate_generator(process) + if run.received is None: + raise + # A cancellation arrived while reaping the timed-out session; it + # is reported below as the cancellation it is. + except BaseException: + # KeyboardInterrupt without a temporary handler (non-main thread) + # or any other error: never leave the generator detached. + _terminate_generator(process) + raise + returncode = process.returncode + finally: + for signum, handler in previous_handlers.items(): + signal.signal(signum, handler) + # Read the recorded signal after restoring the handlers: a signal that + # arrives between the child being reaped and the restoration above is + # recorded by run.cancel and must still be reported, not silently dropped. + received = run.received + if received is not None: + raise _Cancelled(received) + return returncode + + +def default_cache_dir() -> Path: + base = os.environ.get("XDG_CACHE_HOME") + if base: + return Path(base) / "lightcraft-flatpak-cargo" + return Path.home() / ".cache" / "lightcraft-flatpak-cargo" + + +def ensure_outside_source(cache: Path, repo_root: Path) -> None: + cache = cache.resolve() + if cache == repo_root or repo_root in cache.parents: + raise SystemExit(f"error: cache directory must be outside the source checkout: {cache}") + target = os.environ.get("CARGO_TARGET_DIR") + if target: + target_path = Path(target) + # Cargo runs in repo_root, even when this wrapper was invoked elsewhere. + if not target_path.is_absolute(): + target_path = repo_root / target_path + target_path = target_path.resolve() + if cache == target_path or target_path in cache.parents: + raise SystemExit(f"error: cache directory must be outside CARGO_TARGET_DIR: {cache}") + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[3] + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "cargo_lock", + nargs="?", + default=str(repo_root / "Cargo.lock"), + help="Cargo.lock to read (default: repository Cargo.lock)", + ) + parser.add_argument( + "-o", + "--output", + default=str(repo_root / "packaging/linux/flatpak/cargo-sources.json"), + help="output path (default: packaging/linux/flatpak/cargo-sources.json)", + ) + parser.add_argument( + "--cache-dir", + default=None, + help=( + "git clone cache root (default: $XDG_CACHE_HOME/lightcraft-flatpak-cargo, " + "or ~/.cache/lightcraft-flatpak-cargo)" + ), + ) + args = parser.parse_args() + + cargo_lock = Path(args.cargo_lock).resolve() + output = Path(args.output).resolve() + cache = Path(args.cache_dir).resolve() if args.cache_dir else default_cache_dir().resolve() + if not cargo_lock.is_file(): + raise SystemExit(f"error: Cargo.lock not found: {cargo_lock}") + ensure_outside_source(cache, repo_root) + cache.mkdir(parents=True, exist_ok=True) + output.parent.mkdir(parents=True, exist_ok=True) + + generator = patched_upstream(download_upstream()) + + cancelled = None + with tempfile.TemporaryDirectory(prefix="lightcraft-flatpak-generator-") as tmpdir: + script = Path(tmpdir) / "flatpak-cargo-generator.py" + script.write_bytes(generator) + + descriptor, temp_output = tempfile.mkstemp( + prefix=f".{output.name}.", suffix=".tmp", dir=output.parent + ) + os.close(descriptor) + try: + environment = os.environ.copy() + environment["XDG_CACHE_HOME"] = str(cache) + returncode = run_generator( + script=script, + cargo_lock=cargo_lock, + temp_output=temp_output, + cwd=repo_root, + environment=environment, + ) + if returncode != 0: + return returncode + if os.path.getsize(temp_output) == 0: + raise SystemExit("error: generator produced an empty file") + with open(temp_output, "r", encoding="utf-8") as handle: + sources = json.load(handle) + if not isinstance(sources, list) or not sources: + raise SystemExit("error: generated sources are not a non-empty list") + # Keep the owner-only mkstemp mode through the atomic replacement. + os.replace(temp_output, output) + except subprocess.TimeoutExpired as error: + raise SystemExit( + f"error: inventory generation exceeded {GENERATOR_TIMEOUT_SECONDS} seconds; " + "the existing inventory was not changed" + ) from error + except _Cancelled as event: + # Replayed below, after the temporary file and script directory are + # removed, so cancellation leaves no leftovers. + cancelled = event + finally: + if os.path.exists(temp_output): + os.unlink(temp_output) + + if cancelled is not None: + _replay_cancellation(cancelled.signum) + raise SystemExit(f"error: generator cancelled by signal {cancelled.signum}") + + print(f"wrote {output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packaging/linux/flatpak/test_generate_cargo_sources.py b/packaging/linux/flatpak/test_generate_cargo_sources.py new file mode 100644 index 0000000000..cfa38bc38e --- /dev/null +++ b/packaging/linux/flatpak/test_generate_cargo_sources.py @@ -0,0 +1,483 @@ +"""Failure-path and cancellation checks for the LightCraft inventory wrapper.""" +import importlib.util +import io +import hashlib +import stat +import os +from pathlib import Path +import shutil +import signal +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +sys.dont_write_bytecode = True +SOURCE = Path(__file__).resolve().with_name('generate-cargo-sources.py') +spec = importlib.util.spec_from_file_location('inventory_wrapper', SOURCE) +wrapper = importlib.util.module_from_spec(spec) +spec.loader.exec_module(wrapper) + +# Deliberate sabotage for checking that the cancellation tests are meaningful. +# Run with LIGHTCRAFT_FLATPAK_TEST_BREAK_KILL=1: the process-group kill becomes +# a no-op and the cancellation/timeout tests must fail, proving they observe +# real liveness instead of relying on teardown. +BREAK_KILL = os.environ.get('LIGHTCRAFT_FLATPAK_TEST_BREAK_KILL') == '1' +if BREAK_KILL: + wrapper._kill_generator = lambda process: None + +# The process-group tests use POSIX killpg plus /proc to tell an unreaped +# zombie descendant from a running one; both are Linux-specific here, so they +# are skipped explicitly rather than pretending to be portable POSIX. +LINUX_ONLY = sys.platform.startswith('linux') +LINUX_SKIP = 'process-group cancellation is verified on Linux (/proc liveness, killpg)' + +# Process identity recorded with each PID, so teardown never kills a recycled +# PID: /proc//stat starttime is monotonic per PID on Linux. +PYTHON_IDENTITY = ( + 'def write_identity(path, pid):\n' + ' stat = pathlib.Path(f"/proc/{pid}/stat").read_text()\n' + ' starttime = stat.rsplit(")", 1)[1].split()[19]\n' + ' path.write_text(f"{pid} {starttime}")\n' +) + +# A descendant that keeps writing into the cache until it is killed. +DESCENDANT = ( + 'import os, pathlib, sys, time\n' + + PYTHON_IDENTITY + + 'heartbeat, marker, pidfile = (pathlib.Path(arg) for arg in sys.argv[1:])\n' + 'write_identity(pidfile, os.getpid())\n' + 'with heartbeat.open("ab", buffering=0) as handle:\n' + ' for _ in range(1000):\n' + ' handle.write(b"x")\n' + ' time.sleep(0.02)\n' + 'marker.write_bytes(b"descendant survived")\n' +) + + +def detached_generator(): + """Fake generator: starts a heartbeating descendant, then waits.""" + return ( + 'import os, pathlib, subprocess, sys, time\n' + + PYTHON_IDENTITY + + f'CHILD = {DESCENDANT!r}\n' + 'root = pathlib.Path(sys.argv[-1]).parent\n' + 'write_identity(root / "generator.pid", os.getpid())\n' + 'child_script = root / "descendant.py"\n' + 'child_script.write_text(CHILD)\n' + 'subprocess.Popen([sys.executable, str(child_script), str(root / "cache" / "heartbeat"), str(root / "cache" / "after-timeout"), str(root / "descendant.pid")])\n' + 'deadline = time.monotonic() + 10\n' + 'while time.monotonic() < deadline:\n' + ' heartbeat = root / "cache" / "heartbeat"\n' + ' if heartbeat.exists() and heartbeat.stat().st_size:\n' + ' break\n' + ' time.sleep(0.01)\n' + 'else:\n' + ' raise SystemExit("descendant never wrote a heartbeat")\n' + '(root / "ready").write_bytes(b"handshake")\n' + 'time.sleep(30)\n' + ).encode() + + +# Stands in for `python3 generate-cargo-sources.py` so a real signal can be +# delivered to the wrapper while the detached generator is running. The fake +# payload keeps the test offline and the timeout long enough to be cancelled. +DRIVER = '''\ +import importlib.util +import os +from pathlib import Path +import sys + +source, fixture, lock, output, cache = sys.argv[1:6] +spec = importlib.util.spec_from_file_location("inventory_wrapper", source) +wrapper = importlib.util.module_from_spec(spec) +spec.loader.exec_module(wrapper) +if os.environ.get("LIGHTCRAFT_FLATPAK_TEST_BREAK_KILL") == "1": + wrapper._kill_generator = lambda process: None +payload = Path(fixture).read_bytes() +wrapper.download_upstream = lambda: payload +wrapper.patched_upstream = lambda data: data +wrapper.GENERATOR_TIMEOUT_SECONDS = 300 +sys.argv = ["generate", lock, "-o", output, "--cache-dir", cache] +try: + raise SystemExit(wrapper.main()) +except KeyboardInterrupt: + Path(fixture).with_name("interrupted").write_bytes(b"KeyboardInterrupt") + raise SystemExit(130) +''' + + +class InventoryFailures(unittest.TestCase): + def test_download_accepts_verified_bytes(self): + payload = b'verified fixture' + with patch.object(wrapper, 'urlopen', return_value=io.BytesIO(payload)), patch.object(wrapper, 'UPSTREAM_SHA256', hashlib.sha256(payload).hexdigest()): + self.assertEqual(wrapper.download_upstream(), payload) + + def test_patch_applies_both_changes_and_checks_result_hash(self): + source = '\n'.join(old for _, old, _ in wrapper.PATCHES).encode() + expected = '\n'.join(new for _, _, new in wrapper.PATCHES).encode() + with patch.object(wrapper, 'PATCHED_SHA256', hashlib.sha256(expected).hexdigest()): + self.assertEqual(wrapper.patched_upstream(source), expected) + with patch.object(wrapper, 'PATCHED_SHA256', '0' * 64): + with self.assertRaises(SystemExit): + wrapper.patched_upstream(source) + + def test_success_atomically_replaces_inventory_with_owner_only_file(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + output = root / 'sources.json' + output.write_bytes(b'old inventory') + payload = b'[{"type": "archive", "url": "https://example.invalid/source"}]' + def generate(*args, **kwargs): + temporary = Path(kwargs['temp_output']) + self.assertEqual(output.read_bytes(), b'old inventory') + if os.name == 'posix': + self.assertEqual(stat.S_IMODE(temporary.stat().st_mode), 0o600) + temporary.write_bytes(payload) + return 0 + args = ['generate', str(lock), '-o', str(output), '--cache-dir', str(root / 'cache')] + with patch.object(sys, 'argv', args), patch.object(wrapper, 'download_upstream', return_value=b'fixture'), patch.object(wrapper, 'patched_upstream', return_value=b'fixture'), patch.object(wrapper, 'run_generator', side_effect=generate): + self.assertEqual(wrapper.main(), 0) + self.assertEqual(output.read_bytes(), payload) + if os.name == 'posix': + self.assertEqual(stat.S_IMODE(output.stat().st_mode), 0o600) + self.assertEqual(list(root.glob('.sources.json.*.tmp')), []) + + def test_download_rejects_changed_bytes(self): + with patch.object(wrapper, 'urlopen', return_value=io.BytesIO(b'changed upstream')): + with self.assertRaisesRegex(SystemExit, 'checksum mismatch'): + wrapper.download_upstream() + + def test_download_rejects_excessive_size(self): + with patch.object(wrapper, 'urlopen', return_value=io.BytesIO(b'x' * 1_000_001)): + with self.assertRaisesRegex(SystemExit, 'unexpectedly large'): + wrapper.download_upstream() + + def test_patch_refuses_unrecognized_source(self): + with self.assertRaisesRegex(SystemExit, 'matched 0 times'): + wrapper.patched_upstream(b'not the expected generator') + + def test_cache_cannot_be_inside_checkout_or_target(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + for cache in (root / 'repo', root / 'repo/subdir', root / 'target', root / 'target/subdir'): + with patch.dict(os.environ, {'CARGO_TARGET_DIR': str(root / 'target')}): + with self.assertRaises(SystemExit): + wrapper.ensure_outside_source(cache, root / 'repo') + + def test_relative_target_uses_generator_working_directory(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + # The wrapper can be invoked elsewhere; its child runs in repo_root. + elsewhere = root / 'elsewhere/deeper' + elsewhere.mkdir(parents=True) + previous = Path.cwd() + try: + os.chdir(elsewhere) + with patch.dict(os.environ, {'CARGO_TARGET_DIR': '../target'}): + with self.assertRaisesRegex(SystemExit, 'outside CARGO_TARGET_DIR'): + wrapper.ensure_outside_source(root / 'target/cache', root / 'repo') + finally: + os.chdir(previous) + + def test_timed_out_generator_preserves_inventory(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + output = root / 'sources.json' + output.write_bytes(b'original inventory') + generator = b'import pathlib, sys, time\npathlib.Path(sys.argv[-1]).write_bytes(b"partial")\ntime.sleep(30)\n' + args = ['generate', str(lock), '-o', str(output), '--cache-dir', str(root / 'cache')] + with patch.object(sys, 'argv', args), patch.object(wrapper, 'download_upstream', return_value=generator), patch.object(wrapper, 'patched_upstream', side_effect=lambda data: data), patch.object(wrapper, 'GENERATOR_TIMEOUT_SECONDS', 0.2): + with self.assertRaisesRegex(SystemExit, 'existing inventory was not changed'): + wrapper.main() + self.assertEqual(output.read_bytes(), b'original inventory') + self.assertEqual(list(root.glob('.sources.json.*.tmp')), []) + + def test_generator_signal_handlers_are_restored(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + script = root / 'generator.py' + script.write_text( + 'import pathlib, sys\n' + 'pathlib.Path(sys.argv[-1]).write_text("[]")\n' + ) + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + temp_output = root / 'sources.json' + watched = (signal.SIGINT, signal.SIGTERM) + before = {number: signal.getsignal(number) for number in watched} + returncode = wrapper.run_generator( + script=script, + cargo_lock=lock, + temp_output=str(temp_output), + cwd=root, + environment=os.environ.copy(), + ) + after = {number: signal.getsignal(number) for number in watched} + self.assertEqual(returncode, 0) + self.assertEqual(temp_output.read_text(), '[]') + self.assertEqual(before, after) + + def test_signal_after_wait_is_not_lost_during_handler_restore(self): + root = self._new_temp_root() + script = root / 'generator.py' + script.write_text( + 'import pathlib, sys\n' + 'pathlib.Path(sys.argv[-1]).write_text("[]")\n' + ) + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + temp_output = root / 'sources.json' + watched = (signal.SIGINT, signal.SIGTERM) + before = {number: signal.getsignal(number) for number in watched} + real_signal = signal.signal + calls = {'count': 0} + captured = {} + + def restore_with_signal(signum, handler): + calls['count'] += 1 + if calls['count'] <= len(watched): + captured['handler'] = handler + elif calls['count'] == len(watched) + 1: + # The child has been reaped and run_generator is restoring the + # handlers. Invoke the still-installed run.cancel handler now: + # it records the cancellation after run_generator's old + # snapshot point, so only the post-restoration read can report + # it. This is deterministic, unlike racing a real signal. + captured['handler'](signal.SIGINT, None) + return real_signal(signum, handler) + + environment = os.environ.copy() + output_path = str(temp_output) + try: + with patch.object(wrapper.signal, 'signal', side_effect=restore_with_signal): + with self.assertRaises(wrapper._Cancelled) as caught: + wrapper.run_generator( + script=script, + cargo_lock=lock, + temp_output=output_path, + cwd=root, + environment=environment, + ) + finally: + for number in watched: + real_signal(number, before[number]) + self.assertEqual(caught.exception.signum, signal.SIGINT) + + @unittest.skipUnless(LINUX_ONLY, LINUX_SKIP) + def test_timed_out_generator_stops_descendants(self): + root = self._new_temp_root() + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + output = root / 'sources.json' + output.write_bytes(b'original inventory') + args = ['generate', str(lock), '-o', str(output), '--cache-dir', str(root / 'cache')] + try: + with patch.object(sys, 'argv', args), patch.object(wrapper, 'download_upstream', return_value=detached_generator()), patch.object(wrapper, 'patched_upstream', side_effect=lambda data: data), patch.object(wrapper, 'GENERATOR_TIMEOUT_SECONDS', 3.0): + with self.assertRaisesRegex(SystemExit, 'existing inventory was not changed'): + wrapper.main() + + # Assert liveness and a frozen heartbeat before any teardown kill, + # so a broken group kill cannot be masked by the test's cleanup. + self.assertTrue((root / 'ready').exists()) + self._assert_generator_session_gone(root) + self.assertEqual(output.read_bytes(), b'original inventory') + self.assertEqual(list(root.glob('.sources.json.*.tmp')), []) + except BaseException: + self._preserve_failure_evidence(root, 'timeout') + raise + + @unittest.skipUnless(LINUX_ONLY, LINUX_SKIP) + def test_sigint_cancellation_stops_generator_session(self): + self._assert_cancellation(signal.SIGINT, expected_returncode=130) + + @unittest.skipUnless(LINUX_ONLY, LINUX_SKIP) + def test_sigterm_cancellation_stops_generator_session(self): + self._assert_cancellation(signal.SIGTERM, expected_returncode=-signal.SIGTERM) + + def _assert_cancellation(self, number, *, expected_returncode): + root = self._new_temp_root() + driver, output = self._start_generation_driver(root) + try: + driver.send_signal(number) + returncode = driver.wait(timeout=30) + self.assertEqual(returncode, expected_returncode, (root / 'driver.err').read_text()) + if number == signal.SIGINT: + # KeyboardInterrupt reached the wrapper's caller after cleanup. + self.assertEqual((root / 'interrupted').read_bytes(), b'KeyboardInterrupt') + else: + # SIGTERM kept its default terminating action. + self.assertFalse((root / 'interrupted').exists()) + + # Assert the process group is dead and the heartbeat has stopped + # before any teardown kill. Cleanup is registered with addCleanup, + # so it runs after these assertions (also when they fail). + self._assert_generator_session_gone(root) + self.assertEqual(output.read_bytes(), b'original inventory') + self.assertEqual(list(root.glob('.sources.json.*.tmp')), []) + except BaseException: + self._preserve_failure_evidence(root, f'cancel-{int(number)}') + raise + + def _new_temp_root(self): + temp = tempfile.TemporaryDirectory(prefix='lightcraft-flatpak-test-') + self.addCleanup(temp.cleanup) + root = Path(temp.name).resolve() + # Registered after temp.cleanup, so addCleanup's LIFO order stops any + # surviving test processes before their evidence directory disappears. + self.addCleanup(self._stop_pids, root) + return root + + def _start_generation_driver(self, root): + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + output = root / 'sources.json' + output.write_bytes(b'original inventory') + fixture = root / 'generator-fixture.py' + fixture.write_bytes(detached_generator()) + driver_path = root / 'driver.py' + driver_path.write_text(DRIVER) + environment = { + key: value for key, value in os.environ.items() if key != 'CARGO_TARGET_DIR' + } + stdout = (root / 'driver.out').open('wb') + stderr = (root / 'driver.err').open('wb') + self.addCleanup(stdout.close) + self.addCleanup(stderr.close) + driver = subprocess.Popen( + [ + sys.executable, '-B', str(driver_path), str(SOURCE), str(fixture), + str(lock), str(output), str(root / 'cache'), + ], + cwd=str(root), + env=environment, + stdout=stdout, + stderr=stderr, + ) + self.addCleanup(self._stop_driver, driver, root) + deadline = time.monotonic() + 20 + while not (root / 'ready').exists(): + if driver.poll() is not None: + self.fail( + 'driver exited before the generator was ready:\n' + + (root / 'driver.err').read_text() + ) + if time.monotonic() > deadline: + self.fail('driver did not start the generator descendant') + time.sleep(0.02) + return driver, output + + def _stop_driver(self, driver, root): + if driver.poll() is None: + driver.kill() + try: + driver.wait(timeout=10) + except subprocess.TimeoutExpired: + pass + self._stop_pids(root) + + @staticmethod + def _read_identity(root, name): + try: + pid_text, starttime = (root / name).read_text().split() + return int(pid_text), starttime + except (OSError, ValueError): + return None + + @staticmethod + def _identity_is_running(pid, starttime): + """True only for the recorded process, never for a recycled PID.""" + try: + stat = Path(f'/proc/{pid}/stat').read_text() + except OSError: + return False + fields = stat.rsplit(')', 1)[1].split() + return len(fields) > 19 and fields[0] not in ('Z', 'X') and fields[19] == starttime + + @classmethod + def _stop_pids(cls, root): + for name in ('generator.pid', 'descendant.pid'): + identity = cls._read_identity(root, name) + if identity is None: + continue + pid, starttime = identity + if not cls._identity_is_running(pid, starttime): + continue + try: + os.kill(pid, signal.SIGKILL) + except OSError: + pass + # Let teardown observe the kill before the evidence directory is + # removed; identity checks make this a no-op for unrelated PIDs. + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if not any( + identity is not None and cls._identity_is_running(*identity) + for identity in (cls._read_identity(root, name) for name in ('generator.pid', 'descendant.pid')) + ): + break + time.sleep(0.02) + + @staticmethod + def _preserve_failure_evidence(root, label): + destination = Path(tempfile.mkdtemp(prefix=f'lightcraft-flatpak-{label}-')) + for path in root.rglob('*'): + if not path.is_file(): + continue + target = destination / path.relative_to(root) + try: + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + except OSError: + pass + print(f'failure evidence preserved in {destination}', file=sys.stderr) + + def _assert_generator_session_gone(self, root): + for name in ('generator.pid', 'descendant.pid'): + identity = self._read_identity(root, name) + self.assertIsNotNone(identity, f'{name} was not recorded') + pid, starttime = identity + deadline = time.monotonic() + 2 + while self._identity_is_running(pid, starttime) and time.monotonic() < deadline: + time.sleep(0.02) + self.assertFalse( + self._identity_is_running(pid, starttime), + f'{name} process {pid} is still running', + ) + + heartbeat = (root / 'cache' / 'heartbeat').read_bytes() + self.assertTrue(heartbeat) + time.sleep(0.5) + self.assertEqual((root / 'cache' / 'heartbeat').read_bytes(), heartbeat) + self.assertFalse((root / 'cache' / 'after-timeout').exists()) + + def test_failed_or_invalid_generation_preserves_inventory(self): + for code, payload in [(17, b'partial'), (0, b''), (0, b'invalid json'), (0, b'[]'), (0, b'{}')]: + with self.subTest(code=code, payload=payload), tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + lock = root / 'Cargo.lock' + lock.write_text('version = 4\n') + output = root / 'sources.json' + output.write_bytes(b'original inventory') + def generate(*args, **kwargs): + Path(kwargs['temp_output']).write_bytes(payload) + return code + args = ['generate', str(lock), '-o', str(output), '--cache-dir', str(root / 'cache')] + with patch.object(sys, 'argv', args), patch.object(wrapper, 'download_upstream', return_value=b'fixture'), patch.object(wrapper, 'patched_upstream', return_value=b'fixture'), patch.object(wrapper, 'run_generator', side_effect=generate): + if code: + self.assertEqual(wrapper.main(), code) + else: + with self.assertRaises((SystemExit, ValueError)): + wrapper.main() + self.assertEqual(output.read_bytes(), b'original inventory') + self.assertEqual(list(root.glob('.sources.json.*.tmp')), []) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/sonar-project.properties b/sonar-project.properties index 336e755db7..717d1dfac0 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,9 @@ sonar.projectKey=storytold_lightcraft sonar.organization=storytold sonar.coverage.exclusions=**/tools/**,**/docs/**,**/examples/** + +# Keep packaging tests analyzed as tests, outside production coverage metrics. +sonar.tests=packaging/linux/flatpak +sonar.test.inclusions=packaging/linux/flatpak/test_*.py +sonar.exclusions=packaging/linux/flatpak/test_*.py +sonar.python.coverage.reportPaths=python-coverage.xml