open-gpui-canvas 0.2.0

Extensible infinite canvas model and interaction primitives for Open GPUI.
Documentation
use crate::document::CanvasRecordId;
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CanvasZOrderCommand {
    BringToFront,
    BringForward,
    SendBackward,
    SendToBack,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct CanvasLayerRecord {
    pub id: CanvasRecordId,
    pub z_index: i32,
    pub ordinal: usize,
    pub selected: bool,
}

pub(crate) fn sort_layer_records(records: &mut [CanvasLayerRecord]) {
    records.sort_by(|left, right| {
        left.z_index
            .cmp(&right.z_index)
            .then_with(|| left.ordinal.cmp(&right.ordinal))
    });
}

pub(crate) fn reorder_layer_z_indices(
    records: &mut Vec<CanvasLayerRecord>,
    command: CanvasZOrderCommand,
) -> Vec<(CanvasRecordId, i32)> {
    if !records.iter().any(|record| record.selected) {
        return Vec::new();
    }

    let original_order = records
        .iter()
        .map(|record| record.id.clone())
        .collect::<Vec<_>>();
    reorder_layer_records(records, command);
    if records
        .iter()
        .map(|record| &record.id)
        .eq(original_order.iter())
    {
        return Vec::new();
    }

    layer_z_index_updates(records)
}

fn reorder_layer_records(records: &mut Vec<CanvasLayerRecord>, command: CanvasZOrderCommand) {
    match command {
        CanvasZOrderCommand::BringToFront => {
            let mut selected = Vec::new();
            records.retain(|record| {
                if record.selected {
                    selected.push(record.clone());
                    false
                } else {
                    true
                }
            });
            records.extend(selected);
        }
        CanvasZOrderCommand::SendToBack => {
            let mut selected = Vec::new();
            records.retain(|record| {
                if record.selected {
                    selected.push(record.clone());
                    false
                } else {
                    true
                }
            });
            selected.extend(records.iter().cloned());
            *records = selected;
        }
        CanvasZOrderCommand::BringForward => {
            let mut index = records.len();
            while index > 0 {
                index -= 1;
                if !records[index].selected {
                    continue;
                }
                let next = index + 1;
                if next < records.len() && !records[next].selected {
                    records.swap(index, next);
                }
            }
        }
        CanvasZOrderCommand::SendBackward => {
            for index in 0..records.len() {
                if !records[index].selected {
                    continue;
                }
                if index > 0 && !records[index - 1].selected {
                    records.swap(index - 1, index);
                }
            }
        }
    }
}

fn layer_z_index_updates(records: &[CanvasLayerRecord]) -> Vec<(CanvasRecordId, i32)> {
    let min_z = records
        .iter()
        .map(|record| record.z_index)
        .min()
        .unwrap_or_default();
    let base_z = normalized_layer_z_order_base(min_z, records.len());

    records
        .iter()
        .enumerate()
        .filter_map(|(index, record)| {
            let z_index = normalized_layer_z_index(base_z, index);
            (z_index != record.z_index).then(|| (record.id.clone(), z_index))
        })
        .collect()
}

fn normalized_layer_z_order_base(min_z: i32, record_count: usize) -> i32 {
    let max_offset = record_count.saturating_sub(1).min(i32::MAX as usize) as i32;
    if min_z > i32::MAX.saturating_sub(max_offset) {
        i32::MAX - max_offset
    } else {
        min_z
    }
}

fn normalized_layer_z_index(base_z: i32, index: usize) -> i32 {
    let offset = index.min(i32::MAX as usize) as i32;
    base_z.saturating_add(offset)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{EdgeId, NodeId, ShapeId};

    #[test]
    fn bring_forward_crosses_sparse_adjacent_layer() {
        let mut records = vec![node("back", 1, 0, true), shape("front", 10, 1, false)];
        sort_layer_records(&mut records);

        let updates = reorder_layer_z_indices(&mut records, CanvasZOrderCommand::BringForward);

        assert_order(&records, ["shape:front", "node:back"]);
        assert_eq!(
            updates,
            vec![
                (CanvasRecordId::Shape(ShapeId::from("front")), 1),
                (CanvasRecordId::Node(NodeId::from("back")), 2),
            ]
        );
    }

    #[test]
    fn send_backward_crosses_duplicate_adjacent_layer() {
        let mut records = vec![node("back", 0, 0, false), shape("front", 0, 1, true)];
        sort_layer_records(&mut records);

        let updates = reorder_layer_z_indices(&mut records, CanvasZOrderCommand::SendBackward);

        assert_order(&records, ["shape:front", "node:back"]);
        assert_eq!(
            updates,
            vec![(CanvasRecordId::Node(NodeId::from("back")), 1)]
        );
    }

    #[test]
    fn bring_to_front_preserves_selected_relative_order() {
        let mut records = vec![
            node("node", 0, 0, true),
            shape("shape", 1, 1, false),
            edge("edge", 2, 2, true),
            shape("top", 3, 3, false),
        ];
        sort_layer_records(&mut records);

        let updates = reorder_layer_z_indices(&mut records, CanvasZOrderCommand::BringToFront);

        assert_order(
            &records,
            ["shape:shape", "shape:top", "node:node", "edge:edge"],
        );
        assert_eq!(
            updates,
            vec![
                (CanvasRecordId::Shape(ShapeId::from("shape")), 0),
                (CanvasRecordId::Shape(ShapeId::from("top")), 1),
                (CanvasRecordId::Node(NodeId::from("node")), 2),
                (CanvasRecordId::Edge(EdgeId::from("edge")), 3),
            ]
        );
    }

    fn node(id: &str, z_index: i32, ordinal: usize, selected: bool) -> CanvasLayerRecord {
        CanvasLayerRecord {
            id: CanvasRecordId::Node(NodeId::from(id)),
            z_index,
            ordinal,
            selected,
        }
    }

    fn edge(id: &str, z_index: i32, ordinal: usize, selected: bool) -> CanvasLayerRecord {
        CanvasLayerRecord {
            id: CanvasRecordId::Edge(EdgeId::from(id)),
            z_index,
            ordinal,
            selected,
        }
    }

    fn shape(id: &str, z_index: i32, ordinal: usize, selected: bool) -> CanvasLayerRecord {
        CanvasLayerRecord {
            id: CanvasRecordId::Shape(ShapeId::from(id)),
            z_index,
            ordinal,
            selected,
        }
    }

    fn assert_order<const N: usize>(records: &[CanvasLayerRecord], expected: [&str; N]) {
        let actual = records
            .iter()
            .map(|record| record.id.to_string())
            .collect::<Vec<_>>();
        assert_eq!(actual, expected);
    }
}