fix double paste and make selections always floating
This commit is contained in:
parent
6f1a706dd2
commit
73ef9e3b9c
|
|
@ -88,6 +88,13 @@ pub enum ToolState {
|
|||
current: (i32, i32),
|
||||
},
|
||||
|
||||
/// Moving an existing raster selection (and its floating pixels, if any).
|
||||
MovingRasterSelection {
|
||||
/// Canvas position of the pointer at the last processed event, used to
|
||||
/// compute per-frame deltas.
|
||||
last: (i32, i32),
|
||||
},
|
||||
|
||||
/// Dragging selected objects
|
||||
DraggingSelection {
|
||||
start_pos: Point,
|
||||
|
|
|
|||
|
|
@ -1963,17 +1963,50 @@ impl EditorApp {
|
|||
kf.raw_pixels = float.canvas_before;
|
||||
}
|
||||
|
||||
/// Drop (discard) the floating selection keeping the hole punched in the
|
||||
/// canvas. Records a `RasterStrokeAction` for undo. Used by cut (Ctrl+X).
|
||||
fn drop_raster_float(&mut self) {
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::actions::RasterStrokeAction;
|
||||
|
||||
let Some(float) = self.selection.raster_floating.take() else { return };
|
||||
self.selection.raster_selection = None;
|
||||
|
||||
let doc = self.action_executor.document_mut();
|
||||
let Some(AnyLayer::Raster(rl)) = doc.get_layer_mut(&float.layer_id) else { return };
|
||||
let Some(kf) = rl.keyframe_at_mut(float.time) else { return };
|
||||
// raw_pixels already has the hole; record the undo action.
|
||||
let canvas_after = kf.raw_pixels.clone();
|
||||
let (w, h) = (kf.width, kf.height);
|
||||
let action = RasterStrokeAction::new(
|
||||
float.layer_id, float.time,
|
||||
float.canvas_before, canvas_after,
|
||||
w, h,
|
||||
);
|
||||
if let Err(e) = self.action_executor.execute(Box::new(action)) {
|
||||
eprintln!("drop_raster_float: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the current selection to the clipboard
|
||||
fn clipboard_copy_selection(&mut self) {
|
||||
use lightningbeam_core::clipboard::{ClipboardContent, ClipboardLayerType};
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
|
||||
// Raster selection takes priority when on a raster layer
|
||||
if let (Some(layer_id), Some(raster_sel)) = (
|
||||
self.active_layer_id,
|
||||
self.selection.raster_selection.as_ref(),
|
||||
) {
|
||||
// Raster selection takes priority when on a raster layer.
|
||||
// If a floating selection exists (auto-lifted pixels), read directly from
|
||||
// the float so we get exactly the lifted pixels.
|
||||
if let Some(layer_id) = self.active_layer_id {
|
||||
let document = self.action_executor.document();
|
||||
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||
if let Some(float) = &self.selection.raster_floating {
|
||||
self.clipboard_manager.copy(ClipboardContent::RasterPixels {
|
||||
pixels: float.pixels.clone(),
|
||||
width: float.width,
|
||||
height: float.height,
|
||||
});
|
||||
return;
|
||||
} else if let Some(raster_sel) = self.selection.raster_selection.as_ref() {
|
||||
if let Some(AnyLayer::Raster(rl)) = document.get_layer(&layer_id) {
|
||||
if let Some(kf) = rl.keyframe_at(self.playback_time) {
|
||||
let (pixels, w, h) = Self::extract_raster_selection(
|
||||
|
|
@ -1983,9 +2016,11 @@ impl EditorApp {
|
|||
pixels, width: w, height: h,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check what's selected: clip instances take priority, then shapes
|
||||
if !self.selection.clip_instances().is_empty() {
|
||||
|
|
@ -2062,15 +2097,24 @@ impl EditorApp {
|
|||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::actions::RasterStrokeAction;
|
||||
|
||||
// Raster: commit any floating selection first, then erase the marquee region
|
||||
// Raster: if a floating selection exists (auto-lifted), just drop it
|
||||
// (keeps the hole). Otherwise commit any float then erase the marquee region.
|
||||
if let Some(layer_id) = self.active_layer_id {
|
||||
let document = self.action_executor.document();
|
||||
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||
if self.selection.raster_floating.is_some() {
|
||||
self.drop_raster_float();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(layer_id), Some(raster_sel)) = (
|
||||
self.active_layer_id,
|
||||
self.selection.raster_selection.clone(),
|
||||
) {
|
||||
let document = self.action_executor.document();
|
||||
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||
// Committing a floating selection before erasing ensures any
|
||||
// prior paste is baked in before we punch the new hole.
|
||||
self.commit_raster_floating();
|
||||
|
||||
let document = self.action_executor.document_mut();
|
||||
|
|
@ -2314,6 +2358,13 @@ impl EditorApp {
|
|||
}
|
||||
ClipboardContent::RasterPixels { pixels, width, height } => {
|
||||
let Some(layer_id) = self.active_layer_id else { return };
|
||||
|
||||
// Commit any pre-existing floating selection FIRST so that
|
||||
// canvas_before captures the fully-composited state (not the
|
||||
// pre-commit state, which would corrupt the undo snapshot).
|
||||
self.commit_raster_floating();
|
||||
|
||||
// Re-borrow the document after commit to get post-commit state.
|
||||
let document = self.action_executor.document();
|
||||
let layer = document.get_layer(&layer_id);
|
||||
let Some(AnyLayer::Raster(rl)) = layer else { return };
|
||||
|
|
@ -2326,15 +2377,12 @@ impl EditorApp {
|
|||
.map(|s| { let (x0, y0, _, _) = s.bounding_rect(); (x0, y0) })
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
// Snapshot canvas before for undo on commit / restore on cancel.
|
||||
// Snapshot canvas AFTER commit for correct undo on commit / restore on cancel.
|
||||
let canvas_before = kf.raw_pixels.clone();
|
||||
let canvas_w = kf.width;
|
||||
let canvas_h = kf.height;
|
||||
drop(kf); // release immutable borrow before taking mutable
|
||||
|
||||
// Commit any pre-existing floating selection before creating a new one.
|
||||
self.commit_raster_floating();
|
||||
|
||||
use lightningbeam_core::selection::{RasterFloatingSelection, RasterSelection};
|
||||
self.selection.raster_floating = Some(RasterFloatingSelection {
|
||||
pixels,
|
||||
|
|
@ -5821,17 +5869,21 @@ impl eframe::App for EditorApp {
|
|||
// Event::Copy/Cut/Paste instead of regular key events, so
|
||||
// check_shortcuts won't see them via key_pressed().
|
||||
// Skip if a pane (e.g. piano roll) already handled the clipboard event.
|
||||
let mut clipboard_handled = clipboard_consumed;
|
||||
if !clipboard_consumed {
|
||||
for event in &i.events {
|
||||
match event {
|
||||
egui::Event::Copy => {
|
||||
self.handle_menu_action(MenuAction::Copy);
|
||||
clipboard_handled = true;
|
||||
}
|
||||
egui::Event::Cut => {
|
||||
self.handle_menu_action(MenuAction::Cut);
|
||||
clipboard_handled = true;
|
||||
}
|
||||
egui::Event::Paste(_) => {
|
||||
self.handle_menu_action(MenuAction::Paste);
|
||||
clipboard_handled = true;
|
||||
}
|
||||
// When text/plain is absent from the system clipboard egui-winit
|
||||
// falls through to a Key event instead of Event::Paste.
|
||||
|
|
@ -5842,6 +5894,7 @@ impl eframe::App for EditorApp {
|
|||
..
|
||||
} if modifiers.ctrl || modifiers.command => {
|
||||
self.handle_menu_action(MenuAction::Paste);
|
||||
clipboard_handled = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -5850,12 +5903,17 @@ impl eframe::App for EditorApp {
|
|||
|
||||
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
||||
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
||||
// Also skip clipboard actions (Copy/Cut/Paste) if already handled above to prevent
|
||||
// double-firing when egui emits both Event::Key{V} and key_pressed(V) is true.
|
||||
if let Some(action) = MenuSystem::check_shortcuts(i, Some(&self.keymap)) {
|
||||
let is_clipboard = matches!(action, MenuAction::Copy | MenuAction::Cut | MenuAction::Paste);
|
||||
// Only trigger if keyboard isn't claimed OR the shortcut uses modifiers
|
||||
if !wants_keyboard || i.modifiers.ctrl || i.modifiers.command || i.modifiers.alt || i.modifiers.shift {
|
||||
if !(is_clipboard && clipboard_handled) {
|
||||
self.handle_menu_action(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check tool shortcuts (only if no text input is focused;
|
||||
// modifier guard is encoded in the bindings themselves — default tool bindings have no modifiers)
|
||||
|
|
|
|||
|
|
@ -2284,6 +2284,10 @@ pub struct StagePane {
|
|||
/// and updating raw_pixels, so the canvas lives one extra composite frame to
|
||||
/// avoid a flash of the stale Vello scene.
|
||||
pending_canvas_removal: Option<uuid::Uuid>,
|
||||
/// Selection outline saved at stroke mouse-down for post-readback pixel masking.
|
||||
/// Pixels outside the selection are restored from `buffer_before` so strokes
|
||||
/// only affect the area inside the selection outline.
|
||||
stroke_clip_selection: Option<lightningbeam_core::selection::RasterSelection>,
|
||||
/// Synthetic drag/click override for test mode replay (debug builds only)
|
||||
#[cfg(debug_assertions)]
|
||||
replay_override: Option<ReplayDragState>,
|
||||
|
|
@ -2405,6 +2409,7 @@ impl StagePane {
|
|||
pending_undo_before: None,
|
||||
painting_canvas: None,
|
||||
pending_canvas_removal: None,
|
||||
stroke_clip_selection: None,
|
||||
#[cfg(debug_assertions)]
|
||||
replay_override: None,
|
||||
}
|
||||
|
|
@ -4434,6 +4439,63 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
|
||||
/// Lift the pixels enclosed by the current `raster_selection` into a
|
||||
/// `RasterFloatingSelection`, punching a transparent hole in `raw_pixels`.
|
||||
///
|
||||
/// Call this immediately after a marquee / lasso selection is finalized so
|
||||
/// that all downstream operations (drag-move, copy, cut, stroke-masking)
|
||||
/// see a consistent `raster_floating` whenever a selection is active.
|
||||
fn lift_selection_to_float(shared: &mut SharedPaneState) {
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::selection::RasterFloatingSelection;
|
||||
|
||||
// Clone the selection before any mutable borrows.
|
||||
let Some(sel) = shared.selection.raster_selection.clone() else { return };
|
||||
let Some(layer_id) = *shared.active_layer_id else { return };
|
||||
let time = *shared.playback_time;
|
||||
|
||||
// Commit any existing float first (clears raster_selection — re-set below).
|
||||
Self::commit_raster_floating_now(shared);
|
||||
|
||||
let doc = shared.action_executor.document_mut();
|
||||
let Some(AnyLayer::Raster(rl)) = doc.get_layer_mut(&layer_id) else { return };
|
||||
let Some(kf) = rl.keyframe_at_mut(time) else { return };
|
||||
|
||||
let canvas_before = kf.raw_pixels.clone();
|
||||
let (x0, y0, x1, y1) = sel.bounding_rect();
|
||||
let w = (x1 - x0).max(0) as u32;
|
||||
let h = (y1 - y0).max(0) as u32;
|
||||
if w == 0 || h == 0 { return; }
|
||||
|
||||
let mut float_pixels = vec![0u8; (w * h * 4) as usize];
|
||||
for row in 0..h {
|
||||
let sy = y0 + row as i32;
|
||||
if sy < 0 || sy >= kf.height as i32 { continue; }
|
||||
for col in 0..w {
|
||||
let sx = x0 + col as i32;
|
||||
if sx < 0 || sx >= kf.width as i32 { continue; }
|
||||
if !sel.contains_pixel(sx, sy) { continue; }
|
||||
let si = ((sy as u32 * kf.width + sx as u32) * 4) as usize;
|
||||
let di = ((row * w + col) * 4) as usize;
|
||||
float_pixels[di..di + 4].copy_from_slice(&kf.raw_pixels[si..si + 4]);
|
||||
kf.raw_pixels[si..si + 4].fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-set selection (commit_raster_floating_now cleared it) and create float.
|
||||
shared.selection.raster_selection = Some(sel);
|
||||
shared.selection.raster_floating = Some(RasterFloatingSelection {
|
||||
pixels: float_pixels,
|
||||
width: w,
|
||||
height: h,
|
||||
x: x0,
|
||||
y: y0,
|
||||
layer_id,
|
||||
time,
|
||||
canvas_before,
|
||||
});
|
||||
}
|
||||
|
||||
/// `self.pending_raster_dabs` for dispatch by `VelloCallback::prepare()`.
|
||||
///
|
||||
/// The actual pixel rendering happens on the GPU (compute shader). The CPU
|
||||
|
|
@ -4490,6 +4552,10 @@ impl StagePane {
|
|||
// Mouse down: capture buffer_before, start stroke, compute first dab
|
||||
// ----------------------------------------------------------------
|
||||
if self.rsp_drag_started(response) || self.rsp_clicked(response) {
|
||||
// Save selection BEFORE commit clears it — used after readback to
|
||||
// mask the stroke result so only pixels inside the outline change.
|
||||
self.stroke_clip_selection = shared.selection.raster_selection.clone();
|
||||
|
||||
// Commit any floating selection synchronously so buffer_before and
|
||||
// the GPU canvas initial upload see the fully-composited canvas.
|
||||
Self::commit_raster_floating_now(shared);
|
||||
|
|
@ -4695,42 +4761,88 @@ impl StagePane {
|
|||
let (canvas_w, canvas_h) = (kf.width as i32, kf.height as i32);
|
||||
|
||||
if self.rsp_drag_started(response) {
|
||||
Self::commit_raster_floating_now(shared);
|
||||
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||
let inside = shared.selection.raster_selection
|
||||
.as_ref()
|
||||
.map_or(false, |sel| sel.contains_pixel(px, py));
|
||||
|
||||
if inside {
|
||||
// Drag inside the selection — move it (and any floating pixels).
|
||||
// As a safety net, lift the selection if no float exists yet.
|
||||
if shared.selection.raster_floating.is_none() {
|
||||
Self::lift_selection_to_float(shared);
|
||||
}
|
||||
*shared.tool_state = ToolState::MovingRasterSelection { last: (px, py) };
|
||||
} else {
|
||||
// Drag outside — start a new marquee (commit any floating first).
|
||||
Self::commit_raster_floating_now(shared);
|
||||
*shared.tool_state = ToolState::DrawingRasterMarquee {
|
||||
start: (px, py),
|
||||
current: (px, py),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if self.rsp_dragged(response) {
|
||||
if let ToolState::DrawingRasterMarquee { start, ref mut current } = *shared.tool_state {
|
||||
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||
match *shared.tool_state {
|
||||
ToolState::DrawingRasterMarquee { start, ref mut current } => {
|
||||
*current = (px, py);
|
||||
let (x0, x1) = (start.0.min(px).max(0), start.0.max(px).min(canvas_w));
|
||||
let (y0, y1) = (start.1.min(py).max(0), start.1.max(py).min(canvas_h));
|
||||
shared.selection.raster_selection = Some(RasterSelection::Rect(x0, y0, x1, y1));
|
||||
}
|
||||
ToolState::MovingRasterSelection { ref mut last } => {
|
||||
let (dx, dy) = (px - last.0, py - last.1);
|
||||
*last = (px, py);
|
||||
// Shift the marquee.
|
||||
if let Some(ref mut sel) = shared.selection.raster_selection {
|
||||
*sel = match sel {
|
||||
RasterSelection::Rect(x0, y0, x1, y1) =>
|
||||
RasterSelection::Rect(*x0 + dx, *y0 + dy, *x1 + dx, *y1 + dy),
|
||||
RasterSelection::Lasso(pts) =>
|
||||
RasterSelection::Lasso(pts.iter().map(|(x, y)| (x + dx, y + dy)).collect()),
|
||||
};
|
||||
}
|
||||
// Shift floating pixels if any.
|
||||
if let Some(ref mut float) = shared.selection.raster_floating {
|
||||
float.x += dx;
|
||||
float.y += dy;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if self.rsp_drag_stopped(response) {
|
||||
if let ToolState::DrawingRasterMarquee { start, current } = *shared.tool_state {
|
||||
match *shared.tool_state {
|
||||
ToolState::DrawingRasterMarquee { start, current } => {
|
||||
let (x0, x1) = (start.0.min(current.0).max(0), start.0.max(current.0).min(canvas_w));
|
||||
let (y0, y1) = (start.1.min(current.1).max(0), start.1.max(current.1).min(canvas_h));
|
||||
shared.selection.raster_selection = if x1 > x0 && y1 > y0 {
|
||||
Some(RasterSelection::Rect(x0, y0, x1, y1))
|
||||
if x1 > x0 && y1 > y0 {
|
||||
shared.selection.raster_selection = Some(RasterSelection::Rect(x0, y0, x1, y1));
|
||||
Self::lift_selection_to_float(shared);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
*shared.tool_state = ToolState::Idle;
|
||||
shared.selection.raster_selection = None;
|
||||
}
|
||||
}
|
||||
ToolState::MovingRasterSelection { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
*shared.tool_state = ToolState::Idle;
|
||||
}
|
||||
|
||||
if self.rsp_clicked(response) {
|
||||
// A click with no drag: commit float (clicked() fires on release, so
|
||||
// drag_started() may not have fired) then clear the selection.
|
||||
// A click with no drag: if outside the selection, commit any float and
|
||||
// clear; if inside, do nothing (preserves the selection).
|
||||
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||
let inside = shared.selection.raster_selection
|
||||
.as_ref()
|
||||
.map_or(false, |sel| sel.contains_pixel(px, py));
|
||||
if !inside {
|
||||
Self::commit_raster_floating_now(shared);
|
||||
shared.selection.raster_selection = None;
|
||||
}
|
||||
*shared.tool_state = ToolState::Idle;
|
||||
}
|
||||
|
||||
|
|
@ -4778,11 +4890,12 @@ impl StagePane {
|
|||
|
||||
if self.rsp_drag_stopped(response) {
|
||||
if let ToolState::DrawingRasterLasso { ref points } = *shared.tool_state {
|
||||
shared.selection.raster_selection = if points.len() >= 3 {
|
||||
Some(RasterSelection::Lasso(points.clone()))
|
||||
if points.len() >= 3 {
|
||||
shared.selection.raster_selection = Some(RasterSelection::Lasso(points.clone()));
|
||||
Self::lift_selection_to_float(shared);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
shared.selection.raster_selection = None;
|
||||
}
|
||||
}
|
||||
*shared.tool_state = ToolState::Idle;
|
||||
}
|
||||
|
|
@ -7427,11 +7540,28 @@ impl PaneRenderer for StagePane {
|
|||
if let Some(readback) = results.remove(&self.instance_id) {
|
||||
if let Some((layer_id, time, w, h, buffer_before)) = self.pending_undo_before.take() {
|
||||
use lightningbeam_core::actions::RasterStrokeAction;
|
||||
// If a selection was active at stroke-start, restore any pixels
|
||||
// outside the selection outline to their pre-stroke values.
|
||||
let canvas_after = match self.stroke_clip_selection.take() {
|
||||
None => readback.pixels,
|
||||
Some(sel) => {
|
||||
let mut masked = readback.pixels;
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
if !sel.contains_pixel(x as i32, y as i32) {
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
masked[i..i + 4].copy_from_slice(&buffer_before[i..i + 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
masked
|
||||
}
|
||||
};
|
||||
let action = RasterStrokeAction::new(
|
||||
layer_id,
|
||||
time,
|
||||
buffer_before,
|
||||
readback.pixels.clone(),
|
||||
canvas_after,
|
||||
w,
|
||||
h,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue