Merge: master at 0.13.6, with the drag-ghost file and the shared model lookup ported into the split modules

This commit is contained in:
2026-09-20 21:32:21 +02:00
34 changed files with 1024 additions and 139 deletions
+101
View File
@@ -86,6 +86,107 @@ pub(super) fn compose_drag_image(thumbs: &[slint::Image]) -> slint::Image {
slint::Image::from_rgba8_premultiplied(canvas)
}
/// Hand the composite to the renderer by way of a file.
///
/// **A workaround for a renderer fault, and it should read as one.** The
/// ghost under the cursor is drawn by Slint's own drag overlay, which
/// uploads the image as a texture, draws it, and drops the texture in the
/// same call. With the wgpu FemtoVG renderer that drop is immediate and
/// the draw is deferred to the frame's flush, so by the time the frame is
/// rendered the texture is gone and the renderer binds its placeholder
/// instead — a solid red rectangle the size of the ghost. An image with a
/// cache key is kept in the renderer's texture cache until after the
/// flush; an image built from pixels has none, and only a path gives one.
/// So the composite goes to disk as a PNG and comes back through
/// `load_from_path`. Slint 1.17.1, `draw_image_direct` in the FemtoVG
/// item renderer; the GL FemtoVG renderer is not affected.
///
/// One file per drag, named uniquely: the core caches decoded images by
/// path, so reusing a name would show the previous drag's ghost. The file
/// is removed when the drag ends, or when the next one begins.
///
/// If anything on the way fails the composite is handed over as it is,
/// which on the affected renderer draws the placeholder — no worse than
/// before, and a log line says why.
pub(super) fn drag_image_via_file(composite: slint::Image) -> slint::Image {
let Some(buffer) = composite.to_rgba8() else {
log::warn!("drag ghost: the composite has no pixels to write");
return composite;
};
log::debug!("drag ghost: {}×{}", buffer.width(), buffer.height());
if buffer.width() == 0 || buffer.height() == 0 {
return composite;
}
match write_drag_image(&buffer) {
Ok(path) => match slint::Image::load_from_path(&path) {
Ok(image) => {
forget_drag_image_file();
*DRAG_IMAGE_FILE.lock().unwrap() = Some(path);
image
}
Err(_) => {
log::warn!("drag ghost: {} did not load back", path.display());
let _ = std::fs::remove_file(&path);
composite
}
},
Err(e) => {
log::warn!("drag ghost: {e}");
composite
}
}
}
/// The file the current drag's ghost is loaded from, if any.
static DRAG_IMAGE_FILE: std::sync::Mutex<Option<std::path::PathBuf>> = std::sync::Mutex::new(None);
fn write_drag_image(
buffer: &slint::SharedPixelBuffer<slint::Rgba8Pixel>,
) -> std::io::Result<std::path::PathBuf> {
use std::sync::atomic::{AtomicU64, Ordering};
static SERIAL: AtomicU64 = AtomicU64::new(0);
let dir = crate::library::scratch_dir();
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!(
"drag-{}-{}.png",
std::process::id(),
SERIAL.fetch_add(1, Ordering::Relaxed)
));
let file = std::fs::File::create(&path)?;
let mut encoder = png::Encoder::new(
std::io::BufWriter::new(file),
buffer.width(),
buffer.height(),
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
// Fastest: this is a 160px bitmap written once per drag and read once.
encoder.set_compression(png::Compression::Fastest);
let mut writer = encoder.write_header().map_err(std::io::Error::other)?;
writer
.write_image_data(buffer.as_bytes())
.map_err(std::io::Error::other)?;
writer.finish().map_err(std::io::Error::other)?;
Ok(path)
}
pub(super) fn forget_drag_image_file() {
if let Some(path) = DRAG_IMAGE_FILE.lock().unwrap().take() {
let _ = std::fs::remove_file(path);
}
}
/// Draw one thumbnail into the composite, scaled to `tw`×`th` at `dx`,`dy`.
///
/// Nearest-neighbour: this is a transient 160px cursor bitmap, and a filtered
/// resample would cost more than it could visibly buy. `dim` darkens the layers
/// beneath the top one so the stack reads as depth rather than as a smear.
///
/// The buffer is premultiplied, so the alpha applied here is baked into the
/// colour channels as well.
#[allow(clippy::too_many_arguments)]
/// Draw one thumbnail into the composite, scaled to `tw`×`th` at `dx`,`dy`.
///
/// Nearest-neighbour: this is a transient 160px cursor bitmap, and a filtered
+6 -2
View File
@@ -19,7 +19,10 @@ use crate::library;
use crate::{AppWindow, Collections};
use super::controller::{decide_drop, CollectionsController, Drop};
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
use super::drag::{
arm_hold, arm_spring, collapse_spring_opened, compose_drag_image, drag_image_via_file,
forget_drag_image_file,
};
use super::press::select_row;
use super::rename_menu::unique_name;
use super::trash::{drain_trash, start_restore, start_trash};
@@ -603,7 +606,7 @@ fn wire_drag(
.map(|c| c.thumbnail)
.collect()
};
w.set_library_drag_image(compose_drag_image(&thumbs));
w.set_library_drag_image(drag_image_via_file(compose_drag_image(&thumbs)));
*ctl.dragging.borrow_mut() = carried.clone();
sync_selection(&w, &ctl, &ids);
@@ -763,6 +766,7 @@ fn wire_drag(
// released — it holds a copy of every thumbnail it composited.
sync_lifted(&w, &[], &visible());
w.set_library_drag_image(slint::Image::default());
forget_drag_image_file();
if landed.is_some() {
let borrow = catalog.borrow();
+24 -20
View File
@@ -19,23 +19,26 @@ use dr_types::FaceDetector;
/// fingerprints the model files and a probe before they land would be a
/// probe of nothing.
pub fn init(runtime_dirs: Vec<PathBuf>) {
let dir = crate::library::shared_face_models_dir();
let mut models: Vec<(Role, PathBuf)> = FaceDetector::ALL
// Each file where the app will actually load it from — the user's
// shared directory, else the package's — so a fresh install with models
// only under `/usr/share` probes and compiles for them rather than
// finding nothing and settling on the CPU.
let mut wanted: Vec<(Role, &str)> = FaceDetector::ALL
.iter()
.map(|d| (Role::Detector, dir.join(d.file_name())))
.map(|d| (Role::Detector, d.file_name()))
.collect();
wanted.extend([
(Role::Embedder, "arcface_mbf_b1.onnx"),
(Role::Scene, "yolo26s-sem-ade20k.onnx"),
(Role::Landmarks, crate::library::LANDMARK_MODEL),
(Role::EyeClassifier, crate::library::EYE_MODEL),
(Role::EyeClassifier, crate::library::SUNGLASSES_MODEL),
(Role::Inpainter, crate::library::INPAINT_MODEL),
]);
let models: Vec<(Role, PathBuf)> = wanted
.into_iter()
.filter_map(|(role, name)| Some((role, crate::library::shared_model(name)?)))
.collect();
models.push((Role::Embedder, dir.join("arcface_mbf_b1.onnx")));
models.push((Role::Scene, dir.join("yolo26s-sem-ade20k.onnx")));
models.push((Role::Landmarks, dir.join(crate::library::LANDMARK_MODEL)));
models.push((Role::EyeClassifier, dir.join(crate::library::EYE_MODEL)));
models.push((
Role::EyeClassifier,
dir.join(crate::library::SUNGLASSES_MODEL),
));
if let Some(p) = crate::library::inpaint_model() {
models.push((Role::Inpainter, p));
}
models.retain(|(_, p)| p.is_file());
dr_inference_engine::init(dr_inference_engine::Config {
runtime_dirs,
@@ -75,12 +78,13 @@ pub fn user_runtime_dir() -> PathBuf {
/// Which form the current backend loads `detector` in, given the files on
/// this device — the fact `faces.model_id` has to carry (§7).
///
/// Reads the shared directory only. An account-private model directory can
/// override the file `library::face_models` loads, but not which form the
/// backend wants, and the int8 sibling is something a packager ships, not
/// something a user drops in.
/// Reads the shared and system directories only. An account-private model
/// directory can override the file `library::face_models` loads, but not
/// which form the backend wants, and the int8 sibling is something a
/// packager ships, not something a user drops in.
pub fn detector_form(detector: FaceDetector) -> Form {
let canonical = crate::library::shared_face_models_dir().join(detector.file_name());
let canonical = crate::library::shared_model(detector.file_name())
.unwrap_or_else(|| crate::library::shared_face_models_dir().join(detector.file_name()));
dr_inference_engine::resolve_model(Role::Detector, &canonical).1
}
+22 -4
View File
@@ -227,6 +227,15 @@ pub fn inference_cache_dir() -> PathBuf {
data_root().join("inference")
}
/// Somewhere to put a file that only needs to exist for a moment.
///
/// Under the data root rather than `std::env::temp_dir()`, which on Android
/// names a directory the app cannot write to. Nothing here survives a
/// launch on purpose: whoever writes into it deletes what they wrote.
pub fn scratch_dir() -> PathBuf {
data_root().join("scratch")
}
/// The detector and embedder files, if both are present — and the eye
/// models beside them, if those are.
///
@@ -341,6 +350,18 @@ pub(super) fn system_face_models_dirs() -> Vec<PathBuf> {
.collect()
}
/// The file `name` in the shared user models directory, else in the first
/// system directory that has it — the search every account-independent
/// model lookup makes, and the one the inference engine is told about, so
/// that a package's models under `/usr/share` are probed and compiled for
/// exactly as a user's own would be.
pub fn shared_model(name: &str) -> Option<PathBuf> {
std::iter::once(shared_face_models_dir())
.chain(system_face_models_dirs())
.map(|d| d.join(name))
.find(|p| p.is_file())
}
/// TRACES: FR-MRG-4
/// The panorama border filler, as shipped in `models/inpaint/`.
pub const INPAINT_MODEL: &str = "migan-512.onnx";
@@ -351,10 +372,7 @@ pub const INPAINT_MODEL: &str = "migan-512.onnx";
/// the per-account step, because a fill is not identity-bearing and no
/// library has a reason to pin its own.
pub fn inpaint_model() -> Option<PathBuf> {
std::iter::once(shared_face_models_dir())
.chain(system_face_models_dirs())
.map(|d| d.join(INPAINT_MODEL))
.find(|p| p.is_file())
shared_model(INPAINT_MODEL)
}
#[cfg(test)]