darkly 0.5.0

A GPU-native paint engine on wgpu: brushes, layers, blend modes, masks, selections, and undo.
Documentation
//! Duplicate a layer or group, including all descendants and mask filters,
//! with a fresh subtree of [`LayerId`]s. The duplicate is placed directly
//! above its source, and the act is recorded as a single [`DuplicateAction`]
//! that tombstones every duplicated texture so its GPU resources are released
//! when the action eventually leaves the undo stack.

use super::DarklyEngine;
use crate::document::MoveTarget;
use crate::layer::{Layer, LayerId, LayerNode};
use crate::undo::{CompoundAction, DuplicateAction, UndoAction};

impl DarklyEngine {
    /// Duplicate `source_id` and place the copy directly above it in the
    /// same parent. Returns the id of the new top-level node (the duplicated
    /// raster or group). Returns `None` if the source isn't a layer / group.
    pub fn duplicate_node(&mut self, source_id: LayerId) -> Option<LayerId> {
        let (new_id, action) = self.duplicate_node_inner(source_id)?;
        self.compositor.mark_dirty();
        self.push_undo(action);
        Some(new_id)
    }

    /// Duplicate every id in `ids` in document order and return their new
    /// counterparts as a single undo step. Ids whose ancestor is already in
    /// `ids` are skipped — duplicating the ancestor takes the descendant
    /// with it, so a second pass would produce a redundant copy. Cross-
    /// parent selections are supported: each duplicate lands above its own
    /// source, no matter which parent group that source lives in.
    pub fn duplicate_nodes(&mut self, ids: Vec<LayerId>) -> Vec<LayerId> {
        let mut filtered: Vec<LayerId> = ids
            .iter()
            .copied()
            .filter(|&id| self.doc.find_node(id).is_some())
            .filter(|&id| {
                !ids.iter()
                    .any(|&other| other != id && self.doc.is_ancestor_of(other, id))
            })
            .collect();
        let order = self.doc.all_node_ids_in_order();
        let order_idx = |id: LayerId| order.iter().position(|&x| x == id).unwrap_or(usize::MAX);
        filtered.sort_by_key(|&id| order_idx(id));

        let mut new_ids = Vec::with_capacity(filtered.len());
        let mut actions: Vec<Box<dyn UndoAction>> = Vec::with_capacity(filtered.len());
        for id in filtered {
            if let Some((new_id, action)) = self.duplicate_node_inner(id) {
                new_ids.push(new_id);
                actions.push(action);
            }
        }
        if !actions.is_empty() {
            self.compositor.mark_dirty();
            self.push_undo(Box::new(CompoundAction::new(actions)));
        }
        new_ids
    }

    /// Duplicate a single node and return both the new id and the matching
    /// undo action without pushing it. Shared between [`Self::duplicate_node`]
    /// and [`Self::duplicate_nodes`].
    fn duplicate_node_inner(
        &mut self,
        source_id: LayerId,
    ) -> Option<(LayerId, Box<dyn UndoAction>)> {
        self.doc.find_node(source_id)?;
        let root_new_id = self.clone_subtree(source_id, None, /* is_root: */ true)?;
        // `clone_subtree(..., None, ...)` lands the new node at the top of
        // root via `add_*(None) → IntoGroupTop(root)`. Move it next to the
        // source so each duplicate sits directly above its own original —
        // critical for multi-duplicate, where N "tops" would otherwise
        // stack all duplicates together at the top of the panel.
        //
        // `MoveTarget::After(source)` lands the new node at source's
        // position + 1 in source's parent regardless of whether source is
        // a Layer or a Group. Anchoring `add_*` directly on source would
        // land the duplicate INSIDE the source when source is a Group
        // (the `IntoGroupTop` semantic on group anchors).
        self.doc
            .move_layer(root_new_id, MoveTarget::After(source_id));
        let parent = self.doc.parent_of(root_new_id);
        let position = self.doc.position_in_parent(root_new_id).unwrap_or(0);
        let tombstones = self.collect_pixel_node_ids(root_new_id);
        Some((
            root_new_id,
            Box::new(DuplicateAction::new(
                root_new_id,
                parent,
                position,
                tombstones,
            )),
        ))
    }

    /// Recursive subtree clone. `anchor` is the position the new node should
    /// land at (None → use the doc's default insertion). `is_root=true` adds
    /// the " copy" suffix to the top-level name; deeper clones keep their
    /// original names.
    fn clone_subtree(
        &mut self,
        source_id: LayerId,
        anchor: Option<LayerId>,
        is_root: bool,
    ) -> Option<LayerId> {
        // Snapshot the source node so we can read its properties without
        // holding a borrow across the mutating `add_*` calls below.
        let node = self.doc.find_node(source_id)?;
        let common_name = node.common().name.clone();
        let common_visible = node.common().visible;
        let common_locked = node.common().locked;
        let blend_opacity = node.blend().opacity;
        let blend_mode_reg = node.blend().blend_mode;

        match node {
            LayerNode::Layer(Layer::Raster(r)) => {
                let bounds = r.pixels.bounds;
                let new_id = self.doc.add_raster_layer(anchor);
                // Mirror the source's exact bounds so a same-extent
                // `copy_texture_to_texture` keeps every pixel at the right
                // canvas position.
                if let Some(LayerNode::Layer(Layer::Raster(nr))) = self.doc.find_node_mut(new_id) {
                    nr.pixels.bounds = bounds;
                }

                self.compositor.ensure_raster_layer(
                    &self.gpu.device,
                    &self.gpu.queue,
                    new_id,
                    bounds,
                );

                // Apply blend / visibility / lock / name to the new node.
                let new_name = if is_root {
                    format!("{common_name} copy")
                } else {
                    common_name
                };
                if let Some(LayerNode::Layer(Layer::Raster(nr))) = self.doc.find_node_mut(new_id) {
                    nr.common.name = new_name;
                    nr.common.visible = common_visible;
                    nr.common.locked = common_locked;
                    nr.blend.opacity = blend_opacity;
                    nr.blend.blend_mode = blend_mode_reg;
                }
                self.refresh_blend_uniforms(new_id);

                // Copy pixels from source → new.
                self.clone_node_pixels(source_id, new_id);

                // Duplicate every filter on the source (currently masks).
                self.clone_modifiers(source_id, new_id);

                Some(new_id)
            }
            LayerNode::Group(g) => {
                let children = g.children.clone();
                let passthrough = g.passthrough;
                let collapsed = g.collapsed;
                let new_id = self.doc.add_group(anchor);

                // Apply group properties (name + " copy" suffix on root,
                // blend, visible/locked, passthrough/collapsed).
                let new_name = if is_root {
                    format!("{common_name} copy")
                } else {
                    common_name
                };
                if let Some(LayerNode::Group(ng)) = self.doc.find_node_mut(new_id) {
                    ng.common.name = new_name;
                    ng.common.visible = common_visible;
                    ng.common.locked = common_locked;
                    ng.blend.opacity = blend_opacity;
                    ng.blend.blend_mode = blend_mode_reg;
                    ng.passthrough = passthrough;
                    ng.collapsed = collapsed;
                }
                if !passthrough {
                    self.compositor
                        .ensure_group_state(&self.gpu.device, &self.gpu.queue, new_id);
                    self.refresh_blend_uniforms(new_id);
                }

                // Recursively duplicate every child in source order. Each
                // child is added at the bottom of the new group; subsequent
                // children stack above using the previous as anchor inside
                // the new group.
                let mut last_inside: Option<LayerId> = None;
                for child_id in children {
                    // Add a placeholder child at the right slot, then we
                    // populate by calling clone_subtree which inserts into
                    // the right place.
                    let new_child = self.clone_subtree_into_group(child_id, new_id, last_inside)?;
                    last_inside = Some(new_child);
                }

                // Clone filters on the group itself (e.g. group mask).
                self.clone_modifiers(source_id, new_id);

                Some(new_id)
            }
            LayerNode::Layer(Layer::Void(v)) => {
                let void_type = v.void_type.clone();
                let params = v.params.clone();
                // Carry the source's gizmo transform onto the copy so a
                // duplicated camera keeps its flip / framing rather than
                // resetting to the kind's default seed.
                let transform = v.transform;
                // The duplicated layer's name is overwritten with the
                // source's `"… copy"` below; the display_label here only
                // primes the per-type counter so a later fresh-add picks up
                // a non-colliding number.
                let display_label = self
                    .compositor
                    .void_registry()
                    .display_name(&void_type)
                    .to_string();
                let new_id =
                    self.doc
                        .add_void_layer(void_type, &display_label, params, transform, anchor);

                let new_name = if is_root {
                    format!("{common_name} copy")
                } else {
                    common_name
                };
                if let Some(LayerNode::Layer(Layer::Void(nv))) = self.doc.find_node_mut(new_id) {
                    nv.common.name = new_name;
                    nv.common.visible = common_visible;
                    nv.common.locked = common_locked;
                    nv.blend.opacity = blend_opacity;
                    nv.blend.blend_mode = blend_mode_reg;
                }
                // The compositor void cache + texture are allocated lazily by
                // `sync_compositor_layers` (or directly by `add_void_layer`
                // on the engine side); duplication doesn't need a manual
                // `ensure_*` call because there are no pixels to copy — the
                // procedural output is identical from identical params.
                self.refresh_blend_uniforms(new_id);

                // Voids can carry mask filters like any other host.
                self.clone_modifiers(source_id, new_id);

                Some(new_id)
            }
            LayerNode::Layer(Layer::Filter(f)) => {
                let pipeline = f.pipeline.clone();
                let params = f.params.clone();
                let display_label = self
                    .compositor
                    .filter_pipeline_registry()
                    .display_name(&pipeline)
                    .to_string();
                let new_id = self
                    .doc
                    .add_filter_layer(pipeline, &display_label, params, anchor);

                let new_name = if is_root {
                    format!("{common_name} copy")
                } else {
                    common_name
                };
                if let Some(LayerNode::Layer(Layer::Filter(nf))) = self.doc.find_node_mut(new_id) {
                    nf.common.name = new_name;
                    nf.common.visible = common_visible;
                    nf.common.locked = common_locked;
                    nf.blend.opacity = blend_opacity;
                    nf.blend.blend_mode = blend_mode_reg;
                }
                // No GPU resource to allocate — the filter pipeline is shared
                // and resolved lazily in `compose_filter_arm`. The
                // `refresh_blend_uniforms` call no-ops (a filter layer has no
                // `layer_cache` entry), so it's skipped.

                // Filter layers can carry mask filters like any other host.
                self.clone_modifiers(source_id, new_id);

                Some(new_id)
            }
        }
    }

    /// Clone `source_id` and insert the result inside `dest_parent`, after
    /// `anchor_inside` (None → at the bottom of the group). Wrapper around
    /// `clone_subtree` that handles the "insert inside a specific group"
    /// case the bare anchor argument doesn't model.
    fn clone_subtree_into_group(
        &mut self,
        source_id: LayerId,
        dest_parent: LayerId,
        anchor_inside: Option<LayerId>,
    ) -> Option<LayerId> {
        // Reuse the recursive clone but force placement inside `dest_parent`.
        let new_id = self.clone_subtree(source_id, anchor_inside, false)?;

        // If anchor_inside is None the node landed wherever `add_*` placed
        // it; move it into the destination group at the bottom.
        match anchor_inside {
            None => {
                self.doc
                    .move_layer(new_id, MoveTarget::IntoGroupBottom(dest_parent));
            }
            Some(anchor) if self.doc.parent_of(new_id) != Some(dest_parent) => {
                // anchor_inside lives in `dest_parent`; clone_subtree should
                // already have landed `new_id` next to it. Safety net only.
                self.doc.move_layer(new_id, MoveTarget::After(anchor));
            }
            _ => {}
        }

        Some(new_id)
    }

    /// Duplicate every filter hanging off `src_host` onto `dst_host`. v1
    /// supports mask filters; the loop is generic so other future pixel-
    /// bearing filters fall in by the same path.
    ///
    /// Goes through `Document::add_mask_filter` + compositor allocation
    /// directly instead of [`Self::add_mask`] so we don't push a spurious
    /// `FilterAddAction` that the parent [`DuplicateAction`] already
    /// covers (a single undo step should reverse the whole duplicate).
    fn clone_modifiers(&mut self, src_host: LayerId, dst_host: LayerId) {
        let src_mod_ids = self.doc.filters_of(src_host).to_vec();
        for src_mod_id in src_mod_ids {
            if self.doc.mask_filter_id(src_host) != Some(src_mod_id) {
                continue; // Non-mask filters don't ship in v1.
            }
            let Some(new_mod_id) = self.doc.add_mask_filter(dst_host) else {
                continue;
            };
            let bounds = match self.doc.find_filter(new_mod_id).and_then(|m| m.pixels()) {
                Some(p) => p.bounds,
                None => continue,
            };
            self.compositor.ensure_node_texture(
                &self.gpu.device,
                &self.gpu.queue,
                new_mod_id,
                wgpu::TextureFormat::R8Unorm,
                bounds,
            );
            self.compositor
                .ensure_mask_snapshot_state(&self.gpu.device, dst_host);
            // clone_filter_pixels marks `new_mod_id` dirty internally per
            // the write-site invariant.
            self.clone_filter_pixels(src_mod_id, new_mod_id);
        }
    }
}