open-gpui-canvas 0.2.0

Extensible infinite canvas model and interaction primitives for Open GPUI.
Documentation
use crate::session::ToolState;
use crate::{
    CanvasConnectionEndpointRole, CanvasDefaultEdgeRouter, CanvasDocument, CanvasEdgeRoute,
    CanvasEdgeRouteKind, CanvasEdgeRouter, CanvasEditor, CanvasEndpoint, CanvasKindRegistry,
    CanvasRuntime, CanvasSelection, CanvasSnapGuide, CanvasViewport, HitTarget,
};
use open_gpui::{Hsla, Pixels, TextAlign, px, rgb};
use std::sync::Arc;

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum CanvasPaintInteractionState {
    Idle,
    Selecting {
        origin: open_gpui::Point<Pixels>,
        current: open_gpui::Point<Pixels>,
    },
    Connecting {
        source: CanvasEndpoint,
        current: open_gpui::Point<Pixels>,
    },
    Reconnecting {
        edge_id: crate::EdgeId,
        endpoint: CanvasConnectionEndpointRole,
        fixed: CanvasEndpoint,
        current: open_gpui::Point<Pixels>,
    },
    Transforming {
        snap_guides: Vec<CanvasSnapGuide>,
    },
}

#[derive(Clone, Debug)]
pub struct CanvasPaintModel {
    pub(super) document: Arc<CanvasDocument>,
    pub(super) runtime: Arc<CanvasRuntime>,
    pub(super) kind_registry: Arc<CanvasKindRegistry>,
    pub(super) viewport: CanvasViewport,
    pub(super) interaction: CanvasPaintInteraction,
}

impl CanvasPaintModel {
    pub fn new(document: CanvasDocument, viewport: CanvasViewport) -> Self {
        Self::new_with_router(document, viewport, &CanvasDefaultEdgeRouter)
    }

    pub fn new_with_kind_registry(
        document: CanvasDocument,
        viewport: CanvasViewport,
        kind_registry: CanvasKindRegistry,
    ) -> Self {
        Self::new_with_router_and_kind_registry(
            document,
            viewport,
            &CanvasDefaultEdgeRouter,
            kind_registry,
        )
    }

    pub fn new_with_router<R>(
        document: CanvasDocument,
        viewport: CanvasViewport,
        router: &R,
    ) -> Self
    where
        R: CanvasEdgeRouter + ?Sized,
    {
        Self::new_with_router_and_kind_registry(
            document,
            viewport,
            router,
            CanvasKindRegistry::open(),
        )
    }

    pub fn new_with_router_and_kind_registry<R>(
        document: CanvasDocument,
        viewport: CanvasViewport,
        router: &R,
        kind_registry: CanvasKindRegistry,
    ) -> Self
    where
        R: CanvasEdgeRouter + ?Sized,
    {
        let runtime =
            CanvasRuntime::rebuild_with_router_and_kind_registry(&document, router, &kind_registry);
        Self {
            document: Arc::new(document),
            runtime: Arc::new(runtime),
            kind_registry: Arc::new(kind_registry),
            viewport,
            interaction: CanvasPaintInteraction::default(),
        }
    }

    pub fn document(&self) -> &CanvasDocument {
        self.document.as_ref()
    }

    pub fn runtime(&self) -> &CanvasRuntime {
        self.runtime.as_ref()
    }

    pub fn kind_registry(&self) -> &CanvasKindRegistry {
        self.kind_registry.as_ref()
    }

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

    pub fn interaction(&self) -> &CanvasPaintInteraction {
        &self.interaction
    }

    pub fn with_interaction(mut self, interaction: CanvasPaintInteraction) -> Self {
        self.interaction = interaction;
        self
    }

    pub fn with_selection(mut self, selection: CanvasSelection) -> Self {
        self.interaction = self.interaction.with_selection(selection);
        self
    }
}

impl From<&CanvasEditor> for CanvasPaintModel {
    fn from(editor: &CanvasEditor) -> Self {
        let session = editor.session_snapshot();
        Self {
            document: editor.document_snapshot(),
            runtime: editor.runtime_snapshot(),
            kind_registry: editor.kind_registry_snapshot(),
            viewport: session.viewport,
            interaction: CanvasPaintInteraction::new(session.selection)
                .with_internal_tool_state(session.state),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct CanvasPaintInteraction {
    selection: CanvasSelection,
    state: CanvasPaintInteractionState,
    hovered_target: Option<HitTarget>,
}

impl CanvasPaintInteraction {
    pub fn new(selection: CanvasSelection) -> Self {
        Self {
            selection,
            state: CanvasPaintInteractionState::Idle,
            hovered_target: None,
        }
    }

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

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

    pub fn hovered_target(&self) -> Option<&HitTarget> {
        self.hovered_target.as_ref()
    }

    pub fn with_selection(mut self, selection: CanvasSelection) -> Self {
        self.selection = selection;
        self
    }

    pub fn with_hovered_target(mut self, target: Option<HitTarget>) -> Self {
        self.hovered_target = target;
        self
    }

    pub(crate) fn with_internal_tool_state(mut self, state: ToolState) -> Self {
        self.state = CanvasPaintInteractionState::from_tool_state(state);
        self
    }
}

impl Default for CanvasPaintInteraction {
    fn default() -> Self {
        Self {
            selection: CanvasSelection::default(),
            state: CanvasPaintInteractionState::Idle,
            hovered_target: None,
        }
    }
}

impl CanvasPaintInteractionState {
    pub(crate) fn from_tool_state(state: ToolState) -> Self {
        match state {
            ToolState::Idle
            | ToolState::Pointing { .. }
            | ToolState::PendingTranslation { .. }
            | ToolState::Panning { .. } => Self::Idle,
            ToolState::Selecting {
                origin, current, ..
            } => Self::Selecting { origin, current },
            ToolState::Connecting { source, current } => Self::Connecting { source, current },
            ToolState::Reconnecting {
                edge_id,
                endpoint,
                fixed,
                current,
                ..
            } => Self::Reconnecting {
                edge_id,
                endpoint,
                fixed,
                current,
            },
            ToolState::Translating { snap_guides, .. }
            | ToolState::Resizing { snap_guides, .. } => Self::Transforming { snap_guides },
        }
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CanvasConnectionPreviewRoute {
    #[default]
    Straight,
    Polyline,
    Orthogonal,
    CubicBezier,
}

impl CanvasConnectionPreviewRoute {
    pub(crate) fn edge_route(self) -> CanvasEdgeRoute {
        match self {
            Self::Straight => CanvasEdgeRoute::straight(),
            Self::Polyline => CanvasEdgeRoute::polyline(std::iter::empty()),
            Self::Orthogonal => CanvasEdgeRoute::orthogonal(),
            Self::CubicBezier => CanvasEdgeRoute::new(CanvasEdgeRouteKind::CUBIC_BEZIER),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CanvasPaintOptions {
    pub include_hidden: bool,
    pub include_handles: bool,
    pub include_interaction_feedback: bool,
    pub cull_margin: Pixels,
    pub connection_preview_route: CanvasConnectionPreviewRoute,
}

impl Default for CanvasPaintOptions {
    fn default() -> Self {
        Self {
            include_hidden: false,
            include_handles: false,
            include_interaction_feedback: true,
            cull_margin: Pixels::ZERO,
            connection_preview_route: CanvasConnectionPreviewRoute::default(),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CanvasPaintTheme {
    pub background: Option<Hsla>,
    pub node_fill: Hsla,
    pub node_stroke: Hsla,
    pub node_stroke_width: Pixels,
    pub node_corner_radius: Pixels,
    pub shape_fill: Hsla,
    pub shape_stroke: Hsla,
    pub shape_stroke_width: Pixels,
    pub edge_stroke: Hsla,
    pub edge_stroke_width: Pixels,
    pub handle_fill: Hsla,
    pub handle_stroke: Hsla,
    pub handle_stroke_width: Pixels,
    pub handle_corner_radius: Pixels,
    pub selection_fill: Hsla,
    pub selection_stroke: Hsla,
    pub selection_stroke_width: Pixels,
    pub selection_corner_radius: Pixels,
    pub selection_bounds_fill: Hsla,
    pub selection_bounds_stroke: Hsla,
    pub selection_bounds_stroke_width: Pixels,
    pub connection_preview_stroke: Hsla,
    pub connection_preview_stroke_width: Pixels,
    pub connection_free_target_fill: Hsla,
    pub connection_free_target_stroke: Hsla,
    pub connection_valid_target_fill: Hsla,
    pub connection_valid_target_stroke: Hsla,
    pub connection_invalid_target_fill: Hsla,
    pub connection_invalid_target_stroke: Hsla,
    pub snap_guide_stroke: Hsla,
    pub snap_guide_stroke_width: Pixels,
    pub label_color: Hsla,
    pub label_font_size: Pixels,
    pub label_line_height: Pixels,
    pub label_line_clamp: Option<usize>,
    pub label_text_align: TextAlign,
}

impl Default for CanvasPaintTheme {
    fn default() -> Self {
        Self {
            background: None,
            node_fill: Hsla::from(rgb(0xffffff)),
            node_stroke: Hsla::from(rgb(0xd0d7de)),
            node_stroke_width: px(1.0),
            node_corner_radius: px(6.0),
            shape_fill: Hsla::from(rgb(0xf6f8fa)),
            shape_stroke: Hsla::from(rgb(0xd0d7de)),
            shape_stroke_width: px(1.0),
            edge_stroke: Hsla::from(rgb(0x57606a)),
            edge_stroke_width: px(2.0),
            handle_fill: Hsla::from(rgb(0x0969da)),
            handle_stroke: Hsla::from(rgb(0xffffff)),
            handle_stroke_width: px(1.0),
            handle_corner_radius: px(6.0),
            selection_fill: Hsla::from(rgb(0x0969da)).alpha(0.08),
            selection_stroke: Hsla::from(rgb(0x0969da)),
            selection_stroke_width: px(2.0),
            selection_corner_radius: px(7.0),
            selection_bounds_fill: Hsla::from(rgb(0x0969da)).alpha(0.08),
            selection_bounds_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
            selection_bounds_stroke_width: px(1.0),
            connection_preview_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
            connection_preview_stroke_width: px(2.0),
            connection_free_target_fill: Hsla::from(rgb(0x0969da)).alpha(0.12),
            connection_free_target_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
            connection_valid_target_fill: Hsla::from(rgb(0x1a7f37)).alpha(0.14),
            connection_valid_target_stroke: Hsla::from(rgb(0x1a7f37)).alpha(0.9),
            connection_invalid_target_fill: Hsla::from(rgb(0xcf222e)).alpha(0.12),
            connection_invalid_target_stroke: Hsla::from(rgb(0xcf222e)).alpha(0.9),
            snap_guide_stroke: Hsla::from(rgb(0xbf8700)).alpha(0.9),
            snap_guide_stroke_width: px(1.0),
            label_color: Hsla::from(rgb(0x24292f)),
            label_font_size: px(14.0),
            label_line_height: px(18.0),
            label_line_clamp: Some(3),
            label_text_align: TextAlign::Center,
        }
    }
}