open-gpui-canvas 0.2.0

Extensible infinite canvas model and interaction primitives for Open GPUI.
Documentation
use crate::gesture::{CanvasGestureSession, CanvasPreparedGestureCommit};
use crate::record_scope::normalize_selection;
use crate::tool::{CanvasSelection, CanvasTool};
use crate::{
    CanvasConnectionEndpointRole, CanvasDocument, CanvasKindRegistry, CanvasResizeHandle,
    CanvasSnapGuide, CanvasTransaction, CanvasViewport, DocumentError, EdgeId, HitTarget, NodeId,
    ShapeId,
};
use open_gpui::{Axis, Pixels, Point};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum ToolState {
    Idle,
    Pointing {
        origin: Point<Pixels>,
        selection_mode: crate::tool::CanvasSelectionMode,
        base_selection: CanvasSelection,
    },
    Selecting {
        origin: Point<Pixels>,
        current: Point<Pixels>,
        selection_mode: crate::tool::CanvasSelectionMode,
        base_selection: CanvasSelection,
    },
    PendingTranslation {
        origin: Point<Pixels>,
        node_ids: Vec<NodeId>,
        shape_ids: Vec<ShapeId>,
        base_selection: CanvasSelection,
    },
    Translating {
        origin: Point<Pixels>,
        last: Point<Pixels>,
        constraint_axis: Option<Axis>,
        node_ids: Vec<NodeId>,
        shape_ids: Vec<ShapeId>,
        snap_guides: Vec<CanvasSnapGuide>,
    },
    Resizing {
        origin: Point<Pixels>,
        last: Point<Pixels>,
        handle: CanvasResizeHandle,
        node_ids: Vec<NodeId>,
        edge_ids: Vec<EdgeId>,
        shape_ids: Vec<ShapeId>,
        structural: bool,
        snap_guides: Vec<CanvasSnapGuide>,
    },
    Panning {
        origin: Point<Pixels>,
        last: Point<Pixels>,
    },
    Connecting {
        source: crate::CanvasEndpoint,
        current: Point<Pixels>,
    },
    Reconnecting {
        edge_id: EdgeId,
        endpoint: CanvasConnectionEndpointRole,
        fixed: crate::CanvasEndpoint,
        current: Point<Pixels>,
    },
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CanvasToolSession {
    pub(crate) viewport: CanvasViewport,
    pub(crate) tool: CanvasTool,
    pub(crate) state: ToolState,
    pub(crate) selection: CanvasSelection,
    gesture: Option<CanvasGestureSession>,
}

impl CanvasToolSession {
    pub(crate) fn new() -> Self {
        Self {
            viewport: CanvasViewport::default(),
            tool: CanvasTool::Select,
            state: ToolState::Idle,
            selection: CanvasSelection::default(),
            gesture: None,
        }
    }

    pub(crate) fn viewport(&self) -> CanvasViewport {
        self.viewport
    }

    pub(crate) fn tool(&self) -> &CanvasTool {
        &self.tool
    }

    pub(crate) fn state(&self) -> &ToolState {
        &self.state
    }

    pub(crate) fn selection(&self) -> &CanvasSelection {
        &self.selection
    }

    pub(crate) fn snapshot(&self) -> CanvasToolSessionSnapshot {
        CanvasToolSessionSnapshot {
            viewport: self.viewport,
            selection: self.selection.clone(),
            state: self.state.clone(),
        }
    }

    pub(crate) fn retain_selection_for_document(&mut self, document: &CanvasDocument) {
        self.selection = normalize_selection(document, &self.selection);
    }

    pub(crate) fn set_selection(&mut self, selection: CanvasSelection, document: &CanvasDocument) {
        self.selection = normalize_selection(document, &selection);
    }

    pub(crate) fn apply_effect(
        &mut self,
        effect: CanvasToolSessionEffect,
        document: &CanvasDocument,
    ) {
        match effect {
            CanvasToolSessionEffect::SetSelection(selection) => {
                self.set_selection(selection, document);
            }
            CanvasToolSessionEffect::ReplaceSelection(target) => {
                self.replace_selection(target, document);
            }
            CanvasToolSessionEffect::AddSelection(target) => {
                self.add_selection(target, document);
            }
            CanvasToolSessionEffect::RemoveSelection(target) => {
                self.remove_selection(&target, document);
            }
            CanvasToolSessionEffect::ToggleSelection(target) => {
                self.toggle_selection(target, document);
            }
            CanvasToolSessionEffect::ClearSelection => {
                self.clear_selection();
            }
            CanvasToolSessionEffect::SetState(state) => {
                self.set_state(state);
            }
            CanvasToolSessionEffect::PanViewport(delta) => {
                self.pan_viewport(delta);
            }
            CanvasToolSessionEffect::SetViewport(viewport) => {
                self.set_viewport(viewport);
            }
        }
    }

    pub(crate) fn replace_selection(&mut self, target: HitTarget, document: &CanvasDocument) {
        let mut selection = CanvasSelection::default();
        selection.insert_target(target);
        self.set_selection(selection, document);
    }

    pub(crate) fn add_selection(&mut self, target: HitTarget, document: &CanvasDocument) {
        let mut selection = self.selection.clone();
        selection.insert_target(target);
        self.set_selection(selection, document);
    }

    pub(crate) fn remove_selection(&mut self, target: &HitTarget, document: &CanvasDocument) {
        let mut selection = self.selection.clone();
        selection.remove_target(target);
        self.set_selection(selection, document);
    }

    pub(crate) fn toggle_selection(&mut self, target: HitTarget, document: &CanvasDocument) {
        let mut selection = self.selection.clone();
        selection.toggle_target(target);
        self.set_selection(selection, document);
    }

    pub(crate) fn clear_selection(&mut self) {
        self.selection.clear();
    }

    pub(crate) fn set_state(&mut self, state: ToolState) {
        self.state = state;
    }

    pub(crate) fn set_viewport(&mut self, viewport: CanvasViewport) {
        self.viewport = viewport;
    }

    pub(crate) fn pan_viewport(&mut self, delta: Point<Pixels>) {
        self.viewport.pan_by(delta);
    }

    pub(crate) fn set_tool(&mut self, tool: CanvasTool) {
        self.tool = tool;
        self.state = ToolState::Idle;
    }

    pub(crate) fn reset_for_kind_registry_change(&mut self, document: &CanvasDocument) {
        self.retain_selection_for_document(document);
        self.gesture = None;
    }

    pub(crate) fn begin_gesture(&mut self, document: &CanvasDocument) {
        if self.gesture.is_none() {
            self.gesture = Some(CanvasGestureSession::begin(document));
        }
    }

    pub(crate) fn begin_implicit_gesture(
        &mut self,
        document: &CanvasDocument,
    ) -> Option<CanvasGestureSession> {
        self.gesture
            .is_none()
            .then(|| CanvasGestureSession::begin(document))
    }

    pub(crate) fn install_implicit_gesture(&mut self, gesture: CanvasGestureSession) {
        self.gesture = Some(gesture);
    }

    pub(crate) fn prepare_gesture_commit(
        &self,
        current: &CanvasDocument,
        kind_registry: &CanvasKindRegistry,
    ) -> Result<Option<CanvasPreparedGestureCommit>, DocumentError> {
        let Some(gesture) = &self.gesture else {
            return Ok(None);
        };
        gesture.prepare_commit_with_kind_registry(current, kind_registry)
    }

    pub(crate) fn clear_gesture(&mut self) {
        self.gesture = None;
    }

    pub(crate) fn cancel_gesture_transaction(
        &self,
        document: &CanvasDocument,
    ) -> Option<CanvasTransaction> {
        self.gesture
            .as_ref()
            .map(|gesture| gesture.cancel_transaction(document))
    }
}

impl Default for CanvasToolSession {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CanvasToolSessionSnapshot {
    pub(crate) viewport: CanvasViewport,
    pub(crate) selection: CanvasSelection,
    pub(crate) state: ToolState,
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum CanvasToolSessionEffect {
    SetSelection(CanvasSelection),
    ReplaceSelection(HitTarget),
    AddSelection(HitTarget),
    RemoveSelection(HitTarget),
    ToggleSelection(HitTarget),
    ClearSelection,
    SetState(ToolState),
    PanViewport(Point<Pixels>),
    SetViewport(CanvasViewport),
}