diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 703926e..0a9630b 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -16,6 +16,7 @@ use crate::dialogs; use crate::dock::state::{DockState, PaneType}; use crate::io::tablet::TabletState; use crate::panels; +use crate::selection; use crate::selection::{CropState, SelectionTransform, TransformHandle}; use crate::theme::ThemeState; use hcie_engine_api::{BlendMode, BrushTip, BrushStyle, Engine, FilterType, LayerStyle, Tool, ZOOM_MAX, ZOOM_MIN}; @@ -2126,6 +2127,18 @@ impl HcieIcedApp { let doc = &self.documents[self.active_doc]; let w = doc.engine.canvas_width(); let h = doc.engine.canvas_height(); + + // Check if there's a selection + if let Some(ref mask) = doc.selection_mask { + // Copy selected pixels only + let pixels = doc.composite_raw.clone(); + if let Some(transform) = selection::clipboard::extract_masked_pixels(&pixels, mask, w, h) { + self.documents[self.active_doc].internal_clipboard = Some(transform); + return Task::perform(async {}, |_| Message::NoOp); + } + } + + // No selection - copy entire canvas let pixels = doc.composite_raw.clone(); return Task::perform( async move { crate::io::clipboard::copy_image_to_clipboard(&pixels, w, h) }, @@ -2133,6 +2146,17 @@ impl HcieIcedApp { ); } Message::PasteImage => { + let doc = &self.documents[self.active_doc]; + + // Check internal clipboard first + if let Some(ref transform) = doc.internal_clipboard { + // Enter transform mode with the clipboard content + self.documents[self.active_doc].selection_transform = Some(transform.clone()); + self.documents[self.active_doc].transform_drag_handle = TransformHandle::None; + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // Fall back to system clipboard return Task::perform( async { crate::io::clipboard::paste_image_from_clipboard() }, Message::ImagePasted, diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/selection/clipboard.rs b/hcie-iced-app/crates/hcie-iced-gui/src/selection/clipboard.rs new file mode 100644 index 0000000..e497250 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/selection/clipboard.rs @@ -0,0 +1,134 @@ +//! Selection-aware clipboard operations. +//! +//! Provides functions for extracting pixels within a selection mask +//! and compositing floating pixels back onto a layer with alpha blending. + +use super::transform::SelectionTransform; + +/// Extract pixels from a layer using a selection mask. +/// +/// # Arguments +/// * `layer_pixels` - RGBA pixel data of the source layer (row-major, 4 bytes per pixel) +/// * `mask` - Selection mask as alpha channel values (1 byte per pixel, same dimensions as layer) +/// * `canvas_w` - Width of the canvas in pixels +/// * `canvas_h` - Height of the canvas in pixels +/// +/// # Returns +/// `Some(SelectionTransform)` containing the extracted pixels and their position, +/// or `None` if the mask is empty or dimensions are invalid. +pub fn extract_masked_pixels( + layer_pixels: &[u8], + mask: &[u8], + canvas_w: u32, + canvas_h: u32, +) -> Option { + if mask.is_empty() || canvas_w == 0 || canvas_h == 0 { + return None; + } + + // Find bounding box of non-zero mask pixels + let mut min_x = canvas_w; + let mut min_y = canvas_h; + let mut max_x = 0u32; + let mut max_y = 0u32; + let mut has_selection = false; + + for y in 0..canvas_h { + for x in 0..canvas_w { + if mask[(y * canvas_w + x) as usize] > 0 { + min_x = min_x.min(x); + min_y = min_y.min(y); + max_x = max_x.max(x); + max_y = max_y.max(y); + has_selection = true; + } + } + } + + if !has_selection { + return None; + } + + let bw = max_x - min_x + 1; + let bh = max_y - min_y + 1; + let mut pixels = vec![0u8; (bw * bh * 4) as usize]; + + let layer_w = canvas_w as usize; + + for y in 0..bh as usize { + for x in 0..bw as usize { + let src_x = min_x as usize + x; + let src_y = min_y as usize + y; + let mask_idx = src_y * canvas_w as usize + src_x; + if mask_idx < mask.len() && mask[mask_idx] > 0 { + let px_idx = (src_y * layer_w + src_x) * 4; + if px_idx + 3 < layer_pixels.len() { + let dst_idx = (y * bw as usize + x) * 4; + pixels[dst_idx] = layer_pixels[px_idx]; + pixels[dst_idx + 1] = layer_pixels[px_idx + 1]; + pixels[dst_idx + 2] = layer_pixels[px_idx + 2]; + pixels[dst_idx + 3] = layer_pixels[px_idx + 3]; + } + } + } + } + + Some(SelectionTransform { + pixels, + width: bw, + height: bh, + pos: iced::Vector::new(min_x as f32, min_y as f32), + size: iced::Vector::new(bw as f32, bh as f32), + rotation: 0.0, + }) +} + +/// Composite floating pixels back onto a layer. +/// +/// Uses alpha blending to combine the floating pixels with the existing layer data. +/// Pixels outside the canvas bounds are clipped. +/// +/// # Arguments +/// * `layer_pixels` - Mutable RGBA pixel data of the target layer (row-major, 4 bytes per pixel) +/// * `floating` - The floating selection transform containing pixels to composite +/// * `canvas_w` - Width of the canvas in pixels +/// * `canvas_h` - Height of the canvas in pixels +pub fn composite_floating_to_layer( + layer_pixels: &mut [u8], + floating: &SelectionTransform, + canvas_w: u32, + canvas_h: u32, +) { + if floating.is_empty() { + return; + } + + let x = floating.pos.x as i32; + let y = floating.pos.y as i32; + let w = floating.width as i32; + let h = floating.height as i32; + + for fy in 0..h { + for fx in 0..w { + let dst_x = x + fx; + let dst_y = y + fy; + + if dst_x < 0 || dst_y < 0 || dst_x >= canvas_w as i32 || dst_y >= canvas_h as i32 { + continue; + } + + let src_idx = ((fy * w + fx) * 4) as usize; + let dst_idx = ((dst_y * canvas_w as i32 + dst_x) * 4) as usize; + + if src_idx + 3 < floating.pixels.len() && dst_idx + 3 < layer_pixels.len() { + let src_a = floating.pixels[src_idx + 3] as f32 / 255.0; + let inv_a = 1.0 - src_a; + + layer_pixels[dst_idx] = (floating.pixels[src_idx] as f32 * src_a + layer_pixels[dst_idx] as f32 * inv_a) as u8; + layer_pixels[dst_idx + 1] = (floating.pixels[src_idx + 1] as f32 * src_a + layer_pixels[dst_idx + 1] as f32 * inv_a) as u8; + layer_pixels[dst_idx + 2] = (floating.pixels[src_idx + 2] as f32 * src_a + layer_pixels[dst_idx + 2] as f32 * inv_a) as u8; + layer_pixels[dst_idx + 3] = ((src_a * 255.0 + layer_pixels[dst_idx + 3] as f32 * inv_a).min(255.0)) as u8; + } + } + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/selection/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/selection/mod.rs index ee4392c..2f99449 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/selection/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/selection/mod.rs @@ -3,6 +3,7 @@ //! Contains types for managing floating pixel selections (cut/copy-paste), //! transform handles, and the crop tool. +pub mod clipboard; pub mod crop; pub mod transform;