mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-21 22:54:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bf2626ff1 | ||
|
|
85ca0532d4 | ||
|
|
93d02b50dd | ||
|
|
fd124e452e | ||
|
|
9c14f80f32 | ||
|
|
b27989813d |
No files matched your search
@@ -1027,6 +1027,7 @@ fn build() -> Vec<CommandSpec> {
|
||||
v.extend(crate::type_caret_cmds::specs());
|
||||
v.extend(crate::smart_cmds::specs());
|
||||
v.extend(crate::layer_multi_cmds::specs());
|
||||
v.extend(crate::layer_copy_cmds::specs());
|
||||
v.extend(crate::prefs::specs());
|
||||
v.extend(crate::edit_menu_cmds::specs());
|
||||
v.extend(crate::fill_key_cmds::specs());
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Copy layers into another open document (#589): what Photoshop does when layers are dragged from
|
||||
//! the Layers panel, or with the Move tool from the canvas, onto another document's tab or window.
|
||||
//! Photoshop has no menu item for it; `layer.copyToDocument` is the command the drag dispatches and
|
||||
//! agents call.
|
||||
//!
|
||||
//! The copies keep their names, content, masks, styles and blending; they land above the
|
||||
//! destination's active layer as one history step there, and the destination becomes the active
|
||||
//! document. Pixels are converted to the destination's colour profile (Color Settings' intent and
|
||||
//! black point compensation) and bit depth, as Photoshop converts dragged layers.
|
||||
|
||||
use photocraft_color::ColorMode;
|
||||
use photocraft_doc::{Document, LayerId};
|
||||
use photocraft_geom::Rect;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::commands::CommandSpec;
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
const CMD: &str = "layer.copyToDocument";
|
||||
|
||||
fn bad(msg: impl Into<String>) -> EngineError {
|
||||
EngineError::BadParams { cmd: CMD.into(), msg: msg.into() }
|
||||
}
|
||||
|
||||
fn enabled(s: &Session) -> std::result::Result<(), String> {
|
||||
let d = s.active().ok_or("no document open")?;
|
||||
d.active_layer.ok_or("no active layer")?;
|
||||
if s.documents().len() < 2 { Err("no other document is open".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// A document index param (`None` when absent), checked against the open documents.
|
||||
fn doc_index(s: &Session, p: &Value, key: &str) -> Result<Option<usize>> {
|
||||
let Some(v) = p.get(key) else { return Ok(None) };
|
||||
let i = v.as_u64().and_then(|i| usize::try_from(i).ok()).ok_or_else(|| bad(format!("`{key}` must be a document index")))?;
|
||||
if i >= s.documents().len() {
|
||||
return Err(bad(format!("no document {i} (there are {})", s.documents().len())));
|
||||
}
|
||||
Ok(Some(i))
|
||||
}
|
||||
|
||||
/// An `[x, y]` param of finite numbers within ±10⁷ pixels.
|
||||
fn point(p: &Value, key: &str) -> Result<Option<[f64; 2]>> {
|
||||
let Some(v) = p.get(key) else { return Ok(None) };
|
||||
let xy = v.as_array().filter(|a| a.len() == 2).and_then(|a| Some([a[0].as_f64()?, a[1].as_f64()?]));
|
||||
match xy {
|
||||
Some(xy) if xy.iter().all(|c| c.is_finite() && c.abs() <= 1e7) => Ok(Some(xy)),
|
||||
_ => Err(bad(format!("`{key}` must be [x, y] (pixels)"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Modes whose layers convert through the colour engine.
|
||||
fn layered(mode: ColorMode) -> bool {
|
||||
matches!(mode, ColorMode::Rgb | ColorMode::Grayscale | ColorMode::Cmyk | ColorMode::Lab)
|
||||
}
|
||||
|
||||
fn center(r: Rect) -> [f64; 2] {
|
||||
[(f64::from(r.x0) + f64::from(r.x1)) / 2.0, (f64::from(r.y0) + f64::from(r.y1)) / 2.0]
|
||||
}
|
||||
|
||||
/// `layer.copyToDocument`.
|
||||
fn copy_to_document(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dest = doc_index(s, p, "document")?.ok_or_else(|| bad("missing `document` (the destination's index)"))?;
|
||||
let src = match doc_index(s, p, "source")? {
|
||||
Some(i) => i,
|
||||
None => s.active_index().ok_or(EngineError::NoDocument)?,
|
||||
};
|
||||
if src == dest {
|
||||
return Err(bad("the destination is the source document (use layer.duplicate)"));
|
||||
}
|
||||
let (from, to) = (&s.documents()[src], &s.documents()[dest]);
|
||||
let (sdoc, ddoc) = (from.doc.clone(), to.doc.clone());
|
||||
let ids: Vec<LayerId> = match p.get("layers") {
|
||||
None => from.selected_layers(),
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|id| id.as_u64().map(LayerId)).collect::<Option<Vec<_>>>())
|
||||
.and_then(|ids| ids)
|
||||
.ok_or_else(|| bad("`layers` must be an array of layer ids"))?,
|
||||
};
|
||||
if let Some(missing) = ids.iter().find(|id| sdoc.layer(**id).is_none()) {
|
||||
return Err(EngineError::NoLayer(*missing));
|
||||
}
|
||||
let ids = crate::layer_multi_cmds::top_level(&sdoc, &ids);
|
||||
if ids.is_empty() {
|
||||
return Err(bad("no layers to copy"));
|
||||
}
|
||||
for (doc, role) in [(&sdoc, "source"), (&ddoc, "destination")] {
|
||||
if !layered(doc.mode) {
|
||||
return Err(EngineError::Other(format!("layers can't be copied with a {:?} {role} document", doc.mode)));
|
||||
}
|
||||
}
|
||||
// The copies, bottom to top, in a scratch document of the source's colour so they convert
|
||||
// the way a whole document does.
|
||||
let mut copies = Document::new("", sdoc.size, sdoc.mode, sdoc.depth);
|
||||
copies.icc_profile = sdoc.icc_profile.clone();
|
||||
// Type re-lays out at the source's resolution, so it keeps its size in pixels.
|
||||
copies.resolution_dpi = sdoc.resolution_dpi;
|
||||
for id in &ids {
|
||||
let src_layer = sdoc.layer(*id).ok_or(EngineError::NoLayer(*id))?;
|
||||
let mut l = src_layer.duplicate();
|
||||
// A dragged Background arrives as an ordinary layer (the destination keeps its own).
|
||||
if crate::extra_cmds::is_background(src_layer) && sdoc.layers.first().is_some_and(|b| b.id == *id) {
|
||||
l.name = ddoc.next_layer_name("Layer");
|
||||
l.locks = Default::default();
|
||||
}
|
||||
copies.layers.push(l);
|
||||
}
|
||||
if (copies.mode, &copies.icc_profile) != (ddoc.mode, &ddoc.icc_profile) {
|
||||
let profile = crate::color_cmds::document_profile(&ddoc);
|
||||
crate::color_cmds::convert_document(&mut copies, &profile, s.color.settings.intent(), s.color.settings.bpc)?;
|
||||
}
|
||||
if copies.depth != ddoc.depth {
|
||||
crate::image_cmds::for_each_surface(&mut copies.layers, true, &mut |surf, _| {
|
||||
let f = surf.format().with_sample(ddoc.depth);
|
||||
*surf = surf.convert(f);
|
||||
});
|
||||
}
|
||||
// Placement: centred on the destination's canvas, centred on a point, or offset from where
|
||||
// the layers are in the source (default: the same canvas position).
|
||||
let bounds = copies.layers.iter().filter_map(crate::layer_multi_cmds::layer_bounds).fold(Rect::EMPTY, |a, b| a.union(&b));
|
||||
let content = if bounds.is_empty() { sdoc.bounds() } else { bounds };
|
||||
let (dx, dy) = if p.get("center").and_then(Value::as_bool).unwrap_or(false) {
|
||||
let (c, d) = (center(content), center(ddoc.bounds()));
|
||||
(d[0] - c[0], d[1] - c[1])
|
||||
} else if let Some(at) = point(p, "at")? {
|
||||
let c = center(content);
|
||||
(at[0] - c[0], at[1] - c[1])
|
||||
} else {
|
||||
point(p, "offset")?.map_or((0.0, 0.0), |o| (o[0], o[1]))
|
||||
};
|
||||
let (dx, dy) = (dx.round() as i32, dy.round() as i32);
|
||||
if dx != 0 || dy != 0 {
|
||||
let snapshot = copies.clone();
|
||||
for l in &mut copies.layers {
|
||||
crate::commands::translate_layer(&snapshot, l, dx, dy);
|
||||
crate::vector_cmds::translate_vectors(&snapshot, l, f64::from(dx), f64::from(dy));
|
||||
}
|
||||
}
|
||||
let patterns: Vec<_> = sdoc.patterns.iter().filter(|pat| !ddoc.patterns.iter().any(|d| d.id == pat.id)).cloned().collect();
|
||||
s.set_active(dest);
|
||||
let label = if ids.len() == 1 { "Duplicate Layer" } else { "Duplicate Layers" };
|
||||
let edited = s.edit(label, |doc, active| {
|
||||
doc.patterns.extend(patterns);
|
||||
let mut above = *active;
|
||||
let mut new = Vec::with_capacity(copies.layers.len());
|
||||
for l in copies.layers {
|
||||
let id = doc.insert_above(above, l);
|
||||
above = Some(id);
|
||||
new.push(id);
|
||||
}
|
||||
*active = above;
|
||||
Ok(new)
|
||||
});
|
||||
// Nothing changed: the source stays the active document.
|
||||
if edited.is_err() {
|
||||
s.set_active(src);
|
||||
}
|
||||
let new = edited?;
|
||||
let last = new.last().copied();
|
||||
crate::layer_multi_cmds::reselect(s, new.clone(), last);
|
||||
Ok(json!({"document": dest, "layers": new.iter().map(|l| l.0).collect::<Vec<_>>(), "offset": [dx, dy]}))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![CommandSpec {
|
||||
id: CMD,
|
||||
label: "Copy Layers to Document",
|
||||
menu: &[],
|
||||
shortcut: None,
|
||||
params: r##"{"document":index (destination),"layers":[id,…]?=the source's selected layers,"source":index?=active,"center":bool=false (centre on the destination's canvas),"at":[x,y]? (centre the copies on this point),"offset":[dx,dy]?=[0,0] (from their place in the source)}"##,
|
||||
enabled,
|
||||
run: copy_to_document,
|
||||
journal: true,
|
||||
}]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,157 @@
|
||||
use photocraft_color::{ColorMode, PixelFormat, SampleType};
|
||||
use photocraft_doc::{Document, LayerId, Pattern};
|
||||
use photocraft_geom::Rect;
|
||||
use photocraft_raster::Surface;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Document 0 (`w`×`h`, `depth`) with a red 10×10 square at (4, 6) on layer "paint" above the
|
||||
/// Background, and document 1 (60×50, 8-bit RGB). Document 0 is active.
|
||||
fn two_docs(w: u32, h: u32, depth: u32) -> (Session, LayerId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": w, "height": h, "depth": depth})).unwrap();
|
||||
let paint = LayerId(s.execute("layer.new.layer", json!({"name": "paint"})).unwrap()["layer"].as_u64().unwrap());
|
||||
s.execute("select.rect", json!({"x": 4, "y": 6, "width": 10, "height": 10})).unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("select.deselect", json!({})).unwrap();
|
||||
s.execute("file.new", json!({"width": 60, "height": 50})).unwrap();
|
||||
s.set_active(0);
|
||||
(s, paint)
|
||||
}
|
||||
|
||||
fn doc(s: &Session, i: usize) -> &Document {
|
||||
&s.documents()[i].doc
|
||||
}
|
||||
|
||||
fn copied(r: &Value) -> LayerId {
|
||||
LayerId(r["layers"][0].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn bounds(s: &Session, i: usize, id: LayerId) -> Rect {
|
||||
crate::layer_multi_cmds::layer_bounds(doc(s, i).layer(id).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_the_selected_layers_above_the_destinations_active_layer_in_one_step() {
|
||||
let (mut s, paint) = two_docs(40, 30, 8);
|
||||
let source_before = doc(&s, 0).clone();
|
||||
let r = s.execute("layer.copyToDocument", json!({"document": 1})).unwrap();
|
||||
assert_eq!(r["document"], 1);
|
||||
assert_eq!(s.active_index(), Some(1), "the destination becomes the active document");
|
||||
let d = doc(&s, 1);
|
||||
let id = copied(&r);
|
||||
assert_ne!(id, paint, "a copy has its own id");
|
||||
assert_eq!(d.layers.len(), 2);
|
||||
assert_eq!(d.layers[1].id, id, "above the Background, the destination's active layer");
|
||||
assert_eq!(d.layers[1].name, "paint");
|
||||
assert_eq!(s.active().unwrap().active_layer, Some(id));
|
||||
assert_eq!(bounds(&s, 1, id), Rect::new(4, 6, 14, 16), "same canvas position by default");
|
||||
assert_eq!(d.layers[1].surface().unwrap().read_region(Rect::new(5, 7, 6, 8)), vec![1.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(*doc(&s, 0), source_before, "the source is untouched");
|
||||
// One history step in the destination.
|
||||
assert!(s.undo());
|
||||
assert_eq!(doc(&s, 1).layers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_to_the_destinations_depth_and_colour_mode() {
|
||||
for depth in [8, 16, 32] {
|
||||
let (mut s, _) = two_docs(40, 30, depth);
|
||||
s.set_active(1);
|
||||
s.execute("image.mode.grayscale", json!({})).unwrap();
|
||||
s.execute("image.mode.bits16", json!({})).unwrap();
|
||||
s.set_active(0);
|
||||
let r = s.execute("layer.copyToDocument", json!({"document": 1})).unwrap();
|
||||
let id = copied(&r);
|
||||
let surf = doc(&s, 1).layer(id).unwrap().surface().unwrap();
|
||||
assert_eq!(surf.format(), PixelFormat::new(ColorMode::Grayscale, SampleType::U16, true), "from {depth}-bit RGB");
|
||||
let px = surf.read_region(Rect::new(5, 7, 6, 8));
|
||||
assert!(px[0] > 0.1 && px[0] < 0.9 && px[1] == 1.0, "red as a mid grey, opaque: {px:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn places_the_copies_centred_on_the_canvas_on_a_point_or_offset() {
|
||||
let (mut s, paint) = two_docs(40, 30, 8);
|
||||
let place = |s: &mut Session, p: Value| {
|
||||
s.set_active(0);
|
||||
let mut p = p;
|
||||
p["document"] = json!(1);
|
||||
p["layers"] = json!([paint.0]);
|
||||
let r = s.execute("layer.copyToDocument", p).unwrap();
|
||||
bounds(s, 1, copied(&r))
|
||||
};
|
||||
assert_eq!(place(&mut s, json!({"center": true})), Rect::new(25, 20, 35, 30));
|
||||
assert_eq!(place(&mut s, json!({"at": [10.0, 40.0]})), Rect::new(5, 35, 15, 45));
|
||||
assert_eq!(place(&mut s, json!({"offset": [-4, 20]})), Rect::new(0, 26, 10, 36));
|
||||
assert_eq!(doc(&s, 1).layers.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copied_background_is_an_ordinary_layer_and_several_layers_keep_their_order() {
|
||||
let (mut s, paint) = two_docs(40, 30, 8);
|
||||
let bg = doc(&s, 0).layers[0].id;
|
||||
let r = s.execute("layer.copyToDocument", json!({"document": 1, "layers": [paint.0, bg]})).unwrap();
|
||||
let d = doc(&s, 1);
|
||||
let names: Vec<&str> = d.layers.iter().map(|l| l.name.as_str()).collect();
|
||||
assert_eq!(names, ["Background", "Layer 1", "paint"]);
|
||||
assert!(!d.layers[1].locks.transparency && !d.layers[1].locks.position);
|
||||
assert_eq!(r["layers"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(s.active().unwrap().selected_layers().len(), 2, "the copies are selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sources_patterns_come_along_once() {
|
||||
let (mut s, _) = two_docs(40, 30, 8);
|
||||
let mut tile = Surface::new(PixelFormat::new(ColorMode::Rgb, SampleType::U8, true));
|
||||
tile.fill_rect(Rect::new(0, 0, 2, 2), &[0.0, 1.0, 0.0, 1.0]);
|
||||
let pat = Pattern::new("dots", tile, 2, 2);
|
||||
s.edit("add pattern", |d, _| {
|
||||
d.patterns.push(pat.clone());
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("layer.copyToDocument", json!({"document": 1})).unwrap();
|
||||
assert_eq!(doc(&s, 1).patterns.iter().filter(|p| p.id == pat.id).count(), 1);
|
||||
s.set_active(0);
|
||||
s.execute("layer.copyToDocument", json!({"document": 1})).unwrap();
|
||||
assert_eq!(doc(&s, 1).patterns.iter().filter(|p| p.id == pat.id).count(), 1, "not twice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_params_and_states_fail_without_changing_anything() {
|
||||
let (mut s, _) = two_docs(40, 30, 8);
|
||||
let cmd = "layer.copyToDocument";
|
||||
for p in [
|
||||
json!({}),
|
||||
json!({"document": 0}),
|
||||
json!({"document": 7}),
|
||||
json!({"document": -1}),
|
||||
json!({"document": "1"}),
|
||||
json!({"document": 1, "source": 9}),
|
||||
json!({"document": 1, "layers": [999_999]}),
|
||||
json!({"document": 1, "layers": "all"}),
|
||||
json!({"document": 1, "layers": []}),
|
||||
json!({"document": 1, "at": [1.0]}),
|
||||
json!({"document": 1, "at": [f64::MAX, 0.0]}),
|
||||
json!({"document": 1, "offset": ["a", 2]}),
|
||||
Value::Null,
|
||||
] {
|
||||
assert!(s.execute(cmd, p.clone()).is_err(), "{p}");
|
||||
assert_eq!(s.active_index(), Some(0), "{p}: the source stays active");
|
||||
assert_eq!(doc(&s, 1).layers.len(), 1, "{p}");
|
||||
}
|
||||
// An Indexed destination has no layers to add to.
|
||||
s.set_active(1);
|
||||
s.edit("indexed", |d, _| {
|
||||
d.mode = ColorMode::Indexed;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.set_active(0);
|
||||
assert!(s.execute(cmd, json!({"document": 1})).is_err());
|
||||
// A single open document: nothing to copy to.
|
||||
s.close(1);
|
||||
assert!(!s.is_enabled(cmd));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ pub fn set_selection(s: &mut Session, ids: Vec<LayerId>, active: Option<LayerId>
|
||||
|
||||
/// After a structural edit, make `ids` the selection (dropping any that no longer exist), which
|
||||
/// is also what the edit's history state targets.
|
||||
fn reselect(s: &mut Session, ids: Vec<LayerId>, active: Option<LayerId>) {
|
||||
pub(crate) fn reselect(s: &mut Session, ids: Vec<LayerId>, active: Option<LayerId>) {
|
||||
if let Some(st) = s.active_mut() {
|
||||
st.selected_layers = ids;
|
||||
if let Some(a) = active {
|
||||
|
||||
@@ -44,6 +44,7 @@ pub mod group_view_cmds;
|
||||
pub mod image_cmds;
|
||||
pub mod inspect;
|
||||
pub mod jobs;
|
||||
pub mod layer_copy_cmds;
|
||||
pub mod layer_menu_cmds;
|
||||
pub mod layer_multi_cmds;
|
||||
pub mod layer_nav_cmds;
|
||||
|
||||
@@ -941,8 +941,25 @@ pub(crate) fn retain_gpu_documents(app: &mut PhotocraftApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tabs + canvas for the active document, or the start screen.
|
||||
/// Tabs + canvas for the active document, or the start screen; then drops layers dragged onto
|
||||
/// another document (`layer_transfer`).
|
||||
pub fn document_area(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
documents(app, ui);
|
||||
crate::layer_transfer::finish(app, ui.ctx());
|
||||
}
|
||||
|
||||
/// Window › Arrange tiles: each document shown with its tile in `rect`, or `None` when the
|
||||
/// active document fills the area.
|
||||
pub(crate) fn arranged_cells(app: &PhotocraftApp, rect: Rect) -> Option<Vec<(usize, Rect)>> {
|
||||
let idx = app.session.active_index()?;
|
||||
let n = app.session.documents().len();
|
||||
let cells = crate::view_cmds::cells(&app.ui.view.arrange, rect, n)?;
|
||||
let mut shown: Vec<(usize, Rect)> = (0..n).map(|k| (idx + k) % n).zip(cells).collect();
|
||||
shown.sort_by_key(|(d, _)| *d);
|
||||
Some(shown)
|
||||
}
|
||||
|
||||
fn documents(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
app.drop_canvas_rect = None;
|
||||
app.tab_strip = None;
|
||||
retain_gpu_documents(app);
|
||||
@@ -971,13 +988,9 @@ pub fn document_area(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
app.last_canvas_rect = rect;
|
||||
app.drop_canvas_rect = Some(rect);
|
||||
let n = app.session.documents().len();
|
||||
// Window › Arrange: tiled / n-up layouts show several documents side by side; the active one
|
||||
// takes input, a click elsewhere activates that document.
|
||||
if let Some(cells) = crate::view_cmds::cells(&app.ui.view.arrange, rect, n) {
|
||||
let order: Vec<usize> = (0..n).map(|k| (idx + k) % n).collect();
|
||||
let mut shown: Vec<(usize, Rect)> = order.into_iter().zip(cells).collect();
|
||||
shown.sort_by_key(|(d, _)| *d);
|
||||
if let Some(shown) = arranged_cells(app, rect) {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
for (d, cell) in shown {
|
||||
let cell = cell.shrink(1.0);
|
||||
@@ -1011,6 +1024,9 @@ fn tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
let tab_count = app.session.documents().len();
|
||||
let (mut focus_open, mut cancel_open) = (None, None);
|
||||
let focused_open = app.jobs.focus.is_some();
|
||||
// Layers dragged over a tab show its document (`layer_transfer`).
|
||||
let dragging = crate::layer_transfer::pointer_if_armed(app, ui.ctx());
|
||||
let mut drag_over = None;
|
||||
let mut doc_tabs = Vec::with_capacity(tab_count);
|
||||
let frame = egui::Frame::NONE.fill(t.canvas).inner_margin(egui::Margin { left: 8, right: 8, top: 6, bottom: 4 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
@@ -1023,6 +1039,7 @@ fn tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
let meta_g = ui.painter().layout_no_wrap(meta, egui::FontId::proportional(10.5), t.text_faint);
|
||||
let w = name_g.size().x + meta_g.size().x + 44.0;
|
||||
let (r, resp) = ui.allocate_exact_size(egui::vec2(w, 26.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, sel, &st.doc.name));
|
||||
doc_tabs.push(r);
|
||||
if sel {
|
||||
ui.painter().rect_filled(r, t.radius_sm, t.card);
|
||||
@@ -1030,6 +1047,10 @@ fn tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, t.radius_sm, t.hover.gamma_multiply(0.5));
|
||||
}
|
||||
if dragging.is_some_and(|p| r.contains(p)) {
|
||||
drag_over = Some(i);
|
||||
ui.painter().rect_stroke(r, t.radius_sm, Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
let color = if sel { t.text } else { t.text_dim };
|
||||
let ny = r.center().y - name_g.size().y / 2.0;
|
||||
ui.painter().galley_with_override_text_color(egui::pos2(r.left() + 10.0, ny), name_g.clone(), color);
|
||||
@@ -1081,6 +1102,9 @@ fn tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
});
|
||||
});
|
||||
open_tab_clicks(app, activate, focus_open, cancel_open);
|
||||
if let Some(i) = drag_over {
|
||||
crate::layer_transfer::over_tab(app, ui.ctx(), i);
|
||||
}
|
||||
if let Some(i) = close {
|
||||
let _ = crate::menus::invoke(app, ui.ctx(), "file.close", json!({"document": i}));
|
||||
}
|
||||
@@ -1143,6 +1167,8 @@ fn pro_tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
let (mut activate, mut close) = (None, None);
|
||||
let mut tab_action = None;
|
||||
let tab_count = app.session.documents().len();
|
||||
let dragging = crate::layer_transfer::pointer_if_armed(app, ui.ctx());
|
||||
let mut drag_over = None;
|
||||
let mac = ui.ctx().os() == egui::os::OperatingSystem::Mac;
|
||||
let (strip, _) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 26.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
@@ -1168,6 +1194,10 @@ fn pro_tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover.gamma_multiply(0.35));
|
||||
}
|
||||
if dragging.is_some_and(|p| r.contains(p)) {
|
||||
drag_over = Some(i);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
ui.painter().line_segment([r.right_top(), r.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
let (xr, at) = pro_tab_layout(r, g.size().y, mac);
|
||||
let xresp = ui.interact(xr, ui.id().with(("ptabx", i)), Sense::click());
|
||||
@@ -1213,6 +1243,9 @@ fn pro_tabs(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> TabStrip {
|
||||
x = r.right();
|
||||
}
|
||||
open_tab_clicks(app, activate, focus_open, cancel_open);
|
||||
if let Some(i) = drag_over {
|
||||
crate::layer_transfer::over_tab(app, ui.ctx(), i);
|
||||
}
|
||||
if let Some(i) = close {
|
||||
let _ = crate::menus::invoke(app, ui.ctx(), "file.close", json!({"document": i}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//! Dragging layers to another document (#589), as in Photoshop: drag layers from the Layers panel,
|
||||
//! or with the Move tool from the canvas, onto another document's tab. The tab shows its document
|
||||
//! while the pointer is over it (spring-loaded), and releasing over that document's canvas copies
|
||||
//! the layers there with `layer.copyToDocument` (one history step in the destination).
|
||||
//!
|
||||
//! Where the copies land: a Move-tool drag puts the pixel that was grabbed under the pointer, and
|
||||
//! a Layers panel drag centres the layers on it. With ⇧, or when released on the tab itself, they
|
||||
//! keep their place when both documents have the same pixel size and are centred otherwise
|
||||
//! (Photoshop's ⇧-drag). With Window › Arrange tiling several documents, releasing over another
|
||||
//! document's tile drops there.
|
||||
//!
|
||||
//! A Move-tool drag starting inside a selection or moving a floating piece stays a move: Photoshop
|
||||
//! would copy just the selected pixels, which is not supported yet.
|
||||
|
||||
use photocraft_doc::{DocId, LayerId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::state::Tool;
|
||||
|
||||
/// The layers being dragged and where from.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Transfer {
|
||||
source: DocId,
|
||||
layers: Vec<u64>,
|
||||
/// The document point a Move-tool drag grabbed (`None` for a Layers panel drag).
|
||||
grab: Option<[f64; 2]>,
|
||||
/// What the ghost by the pointer says.
|
||||
label: String,
|
||||
}
|
||||
|
||||
fn key() -> egui::Id {
|
||||
egui::Id::new("pc-layer-transfer")
|
||||
}
|
||||
|
||||
fn get(ctx: &egui::Context) -> Option<Transfer> {
|
||||
ctx.data(|d| d.get_temp::<Transfer>(key()))
|
||||
}
|
||||
|
||||
fn clear(ctx: &egui::Context) {
|
||||
ctx.data_mut(|d| d.remove::<Transfer>(key()));
|
||||
}
|
||||
|
||||
/// The layers `id` stands for: the selection when it is one of the selected layers, else itself.
|
||||
fn begin(app: &PhotocraftApp, ctx: &egui::Context, id: LayerId, grab: Option<[f64; 2]>) -> bool {
|
||||
let Some(st) = app.session.active() else { return false };
|
||||
let layers = if st.is_layer_selected(id) { st.selected_layers() } else { vec![id] };
|
||||
let label = match layers.as_slice() {
|
||||
[one] => st.doc.layer(*one).map_or_else(String::new, |l| l.name.clone()),
|
||||
many => crate::i18n::trn(crate::i18n::current(), many.len() as u64, "{n} layer", "{n} layers"),
|
||||
};
|
||||
let t = Transfer { source: st.doc.id, layers: layers.iter().map(|l| l.0).collect(), grab, label };
|
||||
ctx.data_mut(|d| d.insert_temp(key(), t));
|
||||
true
|
||||
}
|
||||
|
||||
/// A Layers panel row drag began on `layer`: it can be dropped on another document.
|
||||
pub fn begin_from_panel(app: &PhotocraftApp, ctx: &egui::Context, layer: LayerId) {
|
||||
begin(app, ctx, layer, None);
|
||||
}
|
||||
|
||||
/// A Move-tool drag of whole layers (not a selection or a floating piece) under way.
|
||||
fn move_drag(app: &PhotocraftApp) -> Option<[f64; 2]> {
|
||||
let d = app.drag.as_ref().filter(|d| d.tool == Tool::Move && d.sel_move.is_none())?;
|
||||
let st = app.session.active()?;
|
||||
let plain = app.ui.transform.is_none() && app.guide_drag.is_none() && st.doc.selection.is_none() && st.floating.is_none();
|
||||
plain.then_some(d.start)
|
||||
}
|
||||
|
||||
/// The pointer, while layers are dragged (or a Move drag could become a transfer): tabs only
|
||||
/// hit-test it then.
|
||||
pub fn pointer_if_armed(app: &PhotocraftApp, ctx: &egui::Context) -> Option<egui::Pos2> {
|
||||
if get(ctx).is_none() && move_drag(app).is_none() {
|
||||
return None;
|
||||
}
|
||||
ctx.input(|i| i.pointer.latest_pos())
|
||||
}
|
||||
|
||||
/// The pointer is over document tab `index` while layers are dragged: a Move drag becomes a
|
||||
/// transfer (the source layers stay where they were), the tab's document is shown, and a release
|
||||
/// right here drops the layers into it.
|
||||
pub fn over_tab(app: &mut PhotocraftApp, ctx: &egui::Context, index: usize) {
|
||||
if get(ctx).is_none() {
|
||||
let Some(grab) = move_drag(app) else { return };
|
||||
let Some(active) = app.session.active().and_then(|st| st.active_layer) else { return };
|
||||
if app.session.active_index() == Some(index) {
|
||||
return;
|
||||
}
|
||||
// The Move drag ends without moving anything; an ⌥-drag's copy is taken back.
|
||||
app.drag = None;
|
||||
app.move_preview = None;
|
||||
crate::move_mods::abandon(app);
|
||||
let active = app.session.active().and_then(|st| st.active_layer).unwrap_or(active);
|
||||
if !begin(app, ctx, active, Some(grab)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if app.session.active_index() != Some(index) {
|
||||
app.session.set_active(index);
|
||||
app.jobs.focus = None;
|
||||
// The Layers panel now lists the other document: its rows don't take the drag.
|
||||
ctx.data_mut(|d| d.remove::<u64>(egui::Id::new("layer-drag")));
|
||||
}
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
drop(app, ctx, index, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every frame after the document area: a release over another document's canvas drops the
|
||||
/// layers there; while the button is held a ghost follows the pointer.
|
||||
pub fn finish(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let Some(t) = get(ctx) else { return };
|
||||
let (pos, released, down) = ctx.input(|i| (i.pointer.latest_pos(), i.pointer.any_released(), i.pointer.primary_down()));
|
||||
let target = pos.and_then(|p| target_at(app, p));
|
||||
if !released {
|
||||
if !down {
|
||||
clear(ctx);
|
||||
} else if let Some(p) = pos
|
||||
&& app.session.active().is_some_and(|st| st.doc.id != t.source)
|
||||
{
|
||||
ghost(ctx, p, &t.label);
|
||||
if target.is_some() {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Copy);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some((dest, at)) = target {
|
||||
drop(app, ctx, dest, at);
|
||||
}
|
||||
clear(ctx);
|
||||
}
|
||||
|
||||
/// The document under screen point `p` that isn't the source, and the document point there
|
||||
/// (`None` in a tile: tiles keep their own views).
|
||||
fn target_at(app: &PhotocraftApp, p: egui::Pos2) -> Option<(usize, Option<[f64; 2]>)> {
|
||||
if !app.last_canvas_rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
match crate::canvas::arranged_cells(app, app.last_canvas_rect) {
|
||||
Some(cells) => cells.into_iter().find(|(_, r)| r.contains(p)).map(|(d, _)| (d, None)),
|
||||
None => Some((app.session.active_index()?, crate::canvas::ViewXform::active(app).map(|xf| xf.to_doc(p)))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the dragged layers into document `dest`, at document point `at` (see the module docs).
|
||||
fn drop(app: &mut PhotocraftApp, ctx: &egui::Context, dest: usize, at: Option<[f64; 2]>) {
|
||||
let Some(t) = get(ctx) else { return };
|
||||
clear(ctx);
|
||||
let docs = app.session.documents();
|
||||
let Some(src) = docs.iter().position(|d| d.doc.id == t.source) else { return };
|
||||
let Some(to) = docs.get(dest).filter(|_| dest != src) else { return };
|
||||
let same_size = to.doc.size == docs[src].doc.size;
|
||||
let mut p = json!({"document": dest, "source": src, "layers": t.layers});
|
||||
match at.filter(|_| !ctx.input(|i| i.modifiers.shift)) {
|
||||
Some(at) => match t.grab {
|
||||
Some(g) => p["offset"] = json!([(at[0] - g[0]).round(), (at[1] - g[1]).round()]),
|
||||
None => p["at"] = json!(at),
|
||||
},
|
||||
None => p["center"] = json!(!same_size),
|
||||
}
|
||||
if let Err(e) = app.run("layer.copyToDocument", p) {
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The dragged layers' name next to the pointer (also the Layers panel's reorder drag).
|
||||
pub fn ghost(ctx: &egui::Context, p: egui::Pos2, label: &str) {
|
||||
let t = crate::theme::Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("layer-transfer-ghost")));
|
||||
let g = painter.layout_no_wrap(label.to_string(), egui::FontId::proportional(12.0), t.text);
|
||||
let r = egui::Rect::from_min_size(p + egui::vec2(12.0, -10.0), g.size() + egui::vec2(16.0, 8.0));
|
||||
painter.rect_filled(r, t.radius_sm, t.card.gamma_multiply(0.95));
|
||||
painter.rect_stroke(r, t.radius_sm, egui::Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
painter.galley(r.min + egui::vec2(8.0, 4.0), g, t.text);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,132 @@
|
||||
//! #589: layers dragged onto another document's tab, then dropped on its canvas, are copied there.
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Modifiers, PointerButton, Pos2, Rect, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use photocraft_doc::LayerId;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::canvas::ViewXform;
|
||||
use crate::state::Tool;
|
||||
|
||||
/// "source-doc" (40×30) with a red 10×10 square at (4, 6) on layer "paint", and "dest-doc"
|
||||
/// (60×50). The source is active.
|
||||
fn harness() -> (Harness<'static, PhotocraftApp>, LayerId) {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 40, "height": 30, "name": "source-doc"})).unwrap();
|
||||
let paint = LayerId(s.execute("layer.new.layer", json!({"name": "paint"})).unwrap()["layer"].as_u64().unwrap());
|
||||
s.execute("select.rect", json!({"x": 4, "y": 6, "width": 10, "height": 10})).unwrap();
|
||||
s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("select.deselect", json!({})).unwrap();
|
||||
s.execute("file.new", json!({"width": 60, "height": 50, "name": "dest-doc"})).unwrap();
|
||||
s.set_active(0);
|
||||
let mut h = Harness::builder().with_size(vec2(1440.0, 900.0)).with_max_steps(64).build_eframe(move |cc| {
|
||||
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
|
||||
PhotocraftApp::new(s, crate::Services::default())
|
||||
});
|
||||
h.run_steps(8);
|
||||
(h, paint)
|
||||
}
|
||||
|
||||
/// A document tab, found by its accessible title ("name @ 100% (…)", see `pro_tabs`).
|
||||
fn tab(h: &Harness<'_, PhotocraftApp>, name: &str) -> Pos2 {
|
||||
h.get_by_label_contains(&format!("{name} @")).rect().center()
|
||||
}
|
||||
|
||||
/// Press at `path[0]`, move through the rest (a few frames at each) and release at the last
|
||||
/// point with `release` held.
|
||||
fn drag_along(h: &mut Harness<'_, PhotocraftApp>, path: &[Pos2], release: Modifiers) {
|
||||
let first = path[0];
|
||||
h.event(egui::Event::PointerMoved(first));
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: first, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
for w in path.windows(2) {
|
||||
for k in 1..=4 {
|
||||
h.event(egui::Event::PointerMoved(w[0] + (w[1] - w[0]) * (k as f32 / 4.0)));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
let last = path[path.len() - 1];
|
||||
h.event(egui::Event::ModifiersChanged(release));
|
||||
h.event(egui::Event::PointerButton { pos: last, button: PointerButton::Primary, pressed: false, modifiers: release });
|
||||
h.run_steps(3);
|
||||
h.event(egui::Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
/// The copy in "dest-doc" (its active layer) and its bounds.
|
||||
fn copy(h: &Harness<'_, PhotocraftApp>) -> (String, Rect) {
|
||||
let st = h.state().session.active().unwrap();
|
||||
assert_eq!(st.doc.name, "dest-doc", "the destination is shown");
|
||||
let l = st.active_layer.and_then(|id| st.doc.layer(id)).unwrap();
|
||||
let b = photocraft_engine::layer_multi_cmds::layer_bounds(l).unwrap();
|
||||
(l.name.clone(), Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32)))
|
||||
}
|
||||
|
||||
fn source_square(h: &Harness<'_, PhotocraftApp>, paint: LayerId) -> photocraft_geom::Rect {
|
||||
let doc = &h.state().session.documents()[0].doc;
|
||||
photocraft_engine::layer_multi_cmds::layer_bounds(doc.layer(paint).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layers_panel_drag_onto_a_tab_drops_the_layer_where_it_is_released() {
|
||||
let (mut h, _) = harness();
|
||||
let row = h.get_by_role_and_label(Role::Button, "paint").rect().center();
|
||||
let over_tab = tab(&h, "dest-doc");
|
||||
let drop = h.state().last_canvas_rect.center();
|
||||
drag_along(&mut h, &[row, over_tab, drop], Modifiers::NONE);
|
||||
let at = ViewXform::active(h.state()).unwrap().to_doc(drop);
|
||||
let (name, r) = copy(&h);
|
||||
assert_eq!(name, "paint");
|
||||
assert!((r.center().x - at[0] as f32).abs() <= 1.0 && (r.center().y - at[1] as f32).abs() <= 1.0, "centred on the drop point {at:?}: {r:?}");
|
||||
assert_eq!(h.state().session.documents()[0].doc.layers.len(), 2, "the source keeps its layers");
|
||||
assert_eq!(h.state().session.active().unwrap().doc.layers.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_centres_the_copy_when_the_documents_differ_in_size() {
|
||||
let (mut h, _) = harness();
|
||||
let row = h.get_by_role_and_label(Role::Button, "paint").rect().center();
|
||||
let over_tab = tab(&h, "dest-doc");
|
||||
let drop = h.state().last_canvas_rect.center() + vec2(80.0, 60.0);
|
||||
drag_along(&mut h, &[row, over_tab, drop], Modifiers::SHIFT);
|
||||
assert_eq!(copy(&h).1, Rect::from_min_max(egui::pos2(25.0, 20.0), egui::pos2(35.0, 30.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_tool_drag_onto_a_tab_copies_the_layer_under_the_pointer_and_leaves_the_source() {
|
||||
let (mut h, paint) = harness();
|
||||
h.state_mut().ui.tool = Tool::Move;
|
||||
h.run_steps(2);
|
||||
let before = source_square(&h, paint);
|
||||
let steps = h.state().session.documents()[0].history.entries().len();
|
||||
let grab = ViewXform::active(h.state()).unwrap().to_screen(9.0, 11.0);
|
||||
let grab_doc = ViewXform::active(h.state()).unwrap().to_doc(grab);
|
||||
let over_tab = tab(&h, "dest-doc");
|
||||
let drop = h.state().last_canvas_rect.center();
|
||||
drag_along(&mut h, &[grab, over_tab, drop], Modifiers::NONE);
|
||||
let at = ViewXform::active(h.state()).unwrap().to_doc(drop);
|
||||
let (name, r) = copy(&h);
|
||||
assert_eq!(name, "paint");
|
||||
// The grabbed pixel lands under the pointer.
|
||||
let want = egui::pos2((9.0 + at[0] - grab_doc[0]) as f32, (11.0 + at[1] - grab_doc[1]) as f32);
|
||||
assert!((r.center() - want).length() <= 1.5, "{r:?} around {want:?}");
|
||||
assert_eq!(source_square(&h, paint), before, "the source layer did not move");
|
||||
assert_eq!(h.state().session.documents()[0].history.entries().len(), steps, "no Move step in the source");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_back_on_the_source_copies_nothing() {
|
||||
let (mut h, _) = harness();
|
||||
let row = h.get_by_role_and_label(Role::Button, "paint").rect().center();
|
||||
let (dest_tab, source_tab) = (tab(&h, "dest-doc"), tab(&h, "source-doc"));
|
||||
let drop = h.state().last_canvas_rect.center();
|
||||
drag_along(&mut h, &[row, dest_tab, source_tab, drop], Modifiers::NONE);
|
||||
let docs = h.state().session.documents();
|
||||
assert_eq!((docs[0].doc.layers.len(), docs[1].doc.layers.len()), (2, 1));
|
||||
assert_eq!(h.state().session.active_index(), Some(0));
|
||||
}
|
||||
@@ -73,6 +73,7 @@ pub mod layer_props_ui;
|
||||
mod layer_reveal;
|
||||
pub mod layer_row_ui;
|
||||
pub mod layer_style;
|
||||
mod layer_transfer;
|
||||
pub mod layer_tree_ui;
|
||||
pub mod links;
|
||||
pub mod liquify_ui;
|
||||
|
||||
@@ -153,6 +153,14 @@ pub fn finish(app: &mut PhotocraftApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Call when a Move drag ends without moving (its layers were dragged to another document): an
|
||||
/// ⌥-drag's copy is taken back.
|
||||
pub fn abandon(app: &mut PhotocraftApp) {
|
||||
if std::mem::take(&mut app.move_mods).dup_from.is_some() {
|
||||
app.session.undo();
|
||||
}
|
||||
}
|
||||
|
||||
/// Arrow keys with the Move tool (or while Free Transform is active): nudge 1 px, ⇧ 10 px;
|
||||
/// ⌥ duplicates the layers first. Returns true when a key was used.
|
||||
pub fn arrow_keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool {
|
||||
|
||||
@@ -1714,6 +1714,9 @@ fn layer_row(
|
||||
let row_h = if t.pro { 32.0 } else { 46.0 };
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click_and_drag());
|
||||
layer_drag_and_drop(ctx, ui, l, rect, &resp, actions);
|
||||
if resp.drag_started() {
|
||||
crate::layer_transfer::begin_from_panel(app, ctx, l.id);
|
||||
}
|
||||
// Rows are painted: name them for screen readers and UI tests.
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, &l.name));
|
||||
// A row scrolled out of view only keeps its place (#125): a layout's hundreds of rows would
|
||||
@@ -2452,13 +2455,7 @@ fn layer_drag_and_drop(ctx: &egui::Context, ui: &egui::Ui, l: &Layer, rect: Rect
|
||||
if dragged == l.id.0 {
|
||||
// Ghost label following the pointer.
|
||||
if let Some(p) = pointer {
|
||||
let layer = egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("layer-drag-ghost"));
|
||||
let painter = ctx.layer_painter(layer);
|
||||
let g = painter.layout_no_wrap(l.name.clone(), egui::FontId::proportional(12.0), t.text);
|
||||
let r = Rect::from_min_size(p + vec2(12.0, -10.0), g.size() + vec2(16.0, 8.0));
|
||||
painter.rect_filled(r, t.radius_sm, t.card.gamma_multiply(0.95));
|
||||
painter.rect_stroke(r, t.radius_sm, Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
painter.galley(r.min + vec2(8.0, 4.0), g, t.text);
|
||||
crate::layer_transfer::ghost(ctx, p, &l.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user