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();