mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
As in Photoshop, dragging a layer row onto New Layer duplicates it, onto New Group groups it and onto Delete deletes it. A row that is part of the selection carries the whole selection; any other row goes alone. The button under the pointer is highlighted during the drag. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
11f6f00ccc
commit
997acf5885
@@ -222,3 +222,45 @@ fn dragging_down_the_eyes_sweeps_visibility_without_reordering() {
|
||||
assert!(visible(&h, rows[1].layer));
|
||||
}
|
||||
}
|
||||
|
||||
/// #736: a layer row dropped on the footer's New Layer button is duplicated, on New Group it is
|
||||
/// grouped, on Delete it is deleted; a row outside the selection goes alone.
|
||||
#[test]
|
||||
fn dropping_a_row_on_the_footer_buttons_duplicates_groups_and_deletes() {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 64, "height": 48})).unwrap();
|
||||
for i in 0..3 {
|
||||
s.execute("layer.new.layer", json!({"name": format!("L{i}")})).unwrap();
|
||||
}
|
||||
let mut h = harness(s, 1.0, "promedium", 290.0);
|
||||
let names = |h: &Harness<'_, PhotocraftApp>| h.state().session.active().unwrap().doc.layers.iter().map(|l| l.name.clone()).collect::<Vec<_>>();
|
||||
let drag_to = |h: &mut Harness<'static, PhotocraftApp>, row: usize, label: &str| {
|
||||
let from = recorded(&h.ctx)[row].row.center();
|
||||
let to = h.get_by_label(label).rect().center();
|
||||
h.event(egui::Event::PointerMoved(from));
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
for k in 1..=8 {
|
||||
h.event(egui::Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.event(egui::Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
};
|
||||
// Rows top to bottom: L2 (selected), L1, L0, Background. L1 isn't selected: it goes alone.
|
||||
drag_to(&mut h, 1, "Create a new layer");
|
||||
assert_eq!(names(&h), ["Background", "L0", "L1", "L1 copy", "L2"]);
|
||||
let steps = h.state().session.active().unwrap().history.past_len();
|
||||
// Rows: L2, L1 copy (now selected), L1, L0, Background.
|
||||
drag_to(&mut h, 3, "Create a new group");
|
||||
let doc = &h.state().session.active().unwrap().doc;
|
||||
let group = doc.layers.iter().find(|l| l.is_group()).expect("a group");
|
||||
let LayerContent::Group(g) = &group.content else { panic!("not a group") };
|
||||
assert_eq!(g.children.iter().map(|l| l.name.as_str()).collect::<Vec<_>>(), ["L0"]);
|
||||
assert_eq!(h.state().session.active().unwrap().history.past_len(), steps + 1, "one undo step");
|
||||
let n = h.state().session.active().unwrap().doc.layers.len();
|
||||
drag_to(&mut h, 0, "Delete layer");
|
||||
assert!(!names(&h).iter().any(|n| n == "L2"));
|
||||
assert_eq!(h.state().session.active().unwrap().doc.layers.len(), n - 1);
|
||||
}
|
||||
|
||||
@@ -1526,6 +1526,12 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
actions.push(done);
|
||||
}
|
||||
});
|
||||
// A layer being dragged can also be dropped on the footer's Delete, New Layer and Group
|
||||
// buttons (#736); read the drag before it ends.
|
||||
let footer_drag = ctx.data(|d| d.get_temp::<u64>(egui::Id::new("layer-drag"))).map(|id| {
|
||||
let in_selection = selection.iter().any(|s| s.0 == id);
|
||||
(id, in_selection)
|
||||
});
|
||||
// End any layer drag after every row has had a chance to accept the drop.
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
ctx.data_mut(|d| d.remove::<u64>(egui::Id::new("layer-drag")));
|
||||
@@ -1536,15 +1542,21 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "trash", 26.0, false, tl!("Delete layer")).clicked() {
|
||||
let trash = icons::button(ui, "trash", 26.0, false, tl!("Delete layer"));
|
||||
if trash.clicked() {
|
||||
actions.push(("layer.delete".into(), json!({})));
|
||||
}
|
||||
if icons::button(ui, "square-plus", 26.0, false, &crate::shortcuts::tip_label(app, "Create a new layer", "layer.new.layer")).clicked() {
|
||||
actions.extend(footer_drop(ui, &trash, footer_drag, "layer.delete"));
|
||||
let new_layer = icons::button(ui, "square-plus", 26.0, false, &crate::shortcuts::tip_label(app, "Create a new layer", "layer.new.layer"));
|
||||
if new_layer.clicked() {
|
||||
actions.push(("layer.new.layer".into(), json!({})));
|
||||
}
|
||||
if icons::button(ui, "folder", 26.0, false, tl!("Create a new group")).clicked() {
|
||||
actions.extend(footer_drop(ui, &new_layer, footer_drag, "layer.duplicate"));
|
||||
let group = icons::button(ui, "folder", 26.0, false, tl!("Create a new group"));
|
||||
if group.clicked() {
|
||||
actions.push(("layer.new.group".into(), json!({})));
|
||||
}
|
||||
actions.extend(footer_drop(ui, &group, footer_drag, "layer.groupLayers"));
|
||||
let adj = icons::button(ui, "contrast", 26.0, false, tl!("Create new fill or adjustment layer"));
|
||||
egui::Popup::menu(&adj).show(|ui| {
|
||||
ui.set_min_width(190.0);
|
||||
@@ -1622,6 +1634,34 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer row dragged onto a Layers panel footer button, as in Photoshop: onto Delete deletes it,
|
||||
/// onto New Layer duplicates it, onto New Group groups it. A row that is part of the selection
|
||||
/// carries the whole selection; any other row goes alone. Highlights the button while over it and
|
||||
/// returns the command on release.
|
||||
fn footer_drop(ui: &egui::Ui, button: &egui::Response, drag: Option<(u64, bool)>, command: &str) -> Option<(String, Value)> {
|
||||
// Named for screen readers (and tests) after the command a drop runs.
|
||||
button.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, footer_label(command)));
|
||||
let (layer, in_selection) = drag?;
|
||||
let p = ui.ctx().input(|i| i.pointer.interact_pos())?;
|
||||
if !button.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().rect_stroke(button.rect, t.radius_sm, Stroke::new(2.0, t.accent), StrokeKind::Inside);
|
||||
if !ui.ctx().input(|i| i.pointer.any_released()) {
|
||||
return None;
|
||||
}
|
||||
Some((command.into(), if in_selection { json!({}) } else { json!({"layer": layer}) }))
|
||||
}
|
||||
|
||||
fn footer_label(command: &str) -> &'static str {
|
||||
match command {
|
||||
"layer.delete" => tl!("Delete layer"),
|
||||
"layer.duplicate" => tl!("Create a new layer"),
|
||||
_ => tl!("Create a new group"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Photoshop's italic "fx" footer button (no icon-font equivalent).
|
||||
fn fx_button(ui: &mut egui::Ui, size: f32, tip: &str) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
||||
Reference in New Issue
Block a user