open-gpui-canvas 0.2.0

Extensible infinite canvas model and interaction primitives for Open GPUI.
Documentation
use crate::document::{CanvasEdge, CanvasEdgeRouteKind};
use open_gpui::{Bounds, Pixels, Point};
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CanvasRouteRequest<'a> {
    pub edge: &'a CanvasEdge,
    pub source: Point<Pixels>,
    pub target: Point<Pixels>,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CanvasRoutePath {
    #[serde(default)]
    pub segments: Vec<CanvasRouteSegment>,
}

impl CanvasRoutePath {
    pub fn new(segments: impl IntoIterator<Item = CanvasRouteSegment>) -> Self {
        Self {
            segments: segments.into_iter().collect(),
        }
    }

    pub fn polyline(points: impl IntoIterator<Item = Point<Pixels>>) -> Self {
        let points = points.into_iter().collect::<Vec<_>>();
        Self::new(points.windows(2).map(|points| CanvasRouteSegment::Line {
            from: points[0],
            to: points[1],
        }))
    }

    pub fn orthogonal(points: impl IntoIterator<Item = Point<Pixels>>) -> Self {
        let points = points.into_iter().collect::<Vec<_>>();
        let mut orthogonal_points = Vec::new();

        for points in points.windows(2) {
            append_orthogonal_leg(&mut orthogonal_points, points[0], points[1]);
        }

        Self::polyline(orthogonal_points)
    }

    pub fn cubic_bezier(
        from: Point<Pixels>,
        control_1: Point<Pixels>,
        control_2: Point<Pixels>,
        to: Point<Pixels>,
    ) -> Self {
        Self::new([CanvasRouteSegment::CubicBezier {
            from,
            control_1,
            control_2,
            to,
        }])
    }

    pub fn is_empty(&self) -> bool {
        self.segments.is_empty()
    }

    pub fn document_points(&self) -> Vec<Point<Pixels>> {
        let mut points = Vec::new();
        for segment in &self.segments {
            let start = segment.start();
            if points.last().copied() != Some(start) {
                points.push(start);
            }
            points.push(segment.end());
        }
        points
    }

    pub fn bounds(&self) -> Option<Bounds<Pixels>> {
        let mut points = self
            .segments
            .iter()
            .flat_map(CanvasRouteSegment::bounds_points);
        let first = points.next()?;
        let (min_x, min_y, max_x, max_y) = points.fold(
            (first.x, first.y, first.x, first.y),
            |(min_x, min_y, max_x, max_y), point| {
                (
                    min_x.min(point.x),
                    min_y.min(point.y),
                    max_x.max(point.x),
                    max_y.max(point.y),
                )
            },
        );
        Some(Bounds::from_corners(
            Point::new(min_x, min_y),
            Point::new(max_x, max_y),
        ))
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum CanvasRouteSegment {
    Line {
        from: Point<Pixels>,
        to: Point<Pixels>,
    },
    CubicBezier {
        from: Point<Pixels>,
        control_1: Point<Pixels>,
        control_2: Point<Pixels>,
        to: Point<Pixels>,
    },
}

impl CanvasRouteSegment {
    pub fn start(&self) -> Point<Pixels> {
        match self {
            Self::Line { from, .. } | Self::CubicBezier { from, .. } => *from,
        }
    }

    pub fn end(&self) -> Point<Pixels> {
        match self {
            Self::Line { to, .. } | Self::CubicBezier { to, .. } => *to,
        }
    }

    pub fn bounds_points(&self) -> [Point<Pixels>; 4] {
        match self {
            Self::Line { from, to } => [*from, *to, *from, *to],
            Self::CubicBezier {
                from,
                control_1,
                control_2,
                to,
            } => [*from, *control_1, *control_2, *to],
        }
    }
}

pub trait CanvasEdgeRouter {
    fn route_edge(&self, request: CanvasRouteRequest<'_>) -> CanvasRoutePath;
}

impl<T> CanvasEdgeRouter for &T
where
    T: CanvasEdgeRouter + ?Sized,
{
    fn route_edge(&self, request: CanvasRouteRequest<'_>) -> CanvasRoutePath {
        (*self).route_edge(request)
    }
}

#[derive(Clone, Copy, Debug, Default)]
pub struct CanvasDefaultEdgeRouter;

impl CanvasEdgeRouter for CanvasDefaultEdgeRouter {
    fn route_edge(&self, request: CanvasRouteRequest<'_>) -> CanvasRoutePath {
        let route = &request.edge.route;
        if route.kind.as_str() == CanvasEdgeRouteKind::CUBIC_BEZIER
            && route.control_points.len() >= 2
        {
            return CanvasRoutePath::cubic_bezier(
                request.source,
                route.control_points[0],
                route.control_points[1],
                request.target,
            );
        }

        if route.kind.as_str() == CanvasEdgeRouteKind::CUBIC_BEZIER {
            let (control_1, control_2) =
                default_cubic_control_points(request.source, request.target);
            return CanvasRoutePath::cubic_bezier(
                request.source,
                control_1,
                control_2,
                request.target,
            );
        }

        if route.kind.as_str() == CanvasEdgeRouteKind::ORTHOGONAL {
            return CanvasRoutePath::orthogonal(
                [request.source]
                    .into_iter()
                    .chain(route.waypoints.iter().copied())
                    .chain([request.target]),
            );
        }

        CanvasRoutePath::polyline(
            [request.source]
                .into_iter()
                .chain(route.waypoints.iter().copied())
                .chain([request.target]),
        )
    }
}

fn default_cubic_control_points(
    source: Point<Pixels>,
    target: Point<Pixels>,
) -> (Point<Pixels>, Point<Pixels>) {
    let dx = target.x.as_f32() - source.x.as_f32();
    let offset = (dx.abs() * 0.5)
        .max(40.0)
        .copysign(if dx == 0.0 { 1.0 } else { dx });
    let offset = Pixels::from(offset);
    (
        Point::new(source.x + offset, source.y),
        Point::new(target.x - offset, target.y),
    )
}

fn append_orthogonal_leg(points: &mut Vec<Point<Pixels>>, from: Point<Pixels>, to: Point<Pixels>) {
    push_unique_point(points, from);

    if from.x != to.x && from.y != to.y {
        let mid_x = (from.x + to.x) * 0.5;
        push_unique_point(points, Point::new(mid_x, from.y));
        push_unique_point(points, Point::new(mid_x, to.y));
    }

    push_unique_point(points, to);
}

fn push_unique_point(points: &mut Vec<Point<Pixels>>, point: Point<Pixels>) {
    if points.last().copied() != Some(point) {
        points.push(point);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CanvasEdge, CanvasEdgeRoute, CanvasEndpoint};
    use open_gpui::{point, px};

    #[test]
    fn default_router_emits_polyline_segments_from_waypoints() {
        let mut edge = CanvasEdge::new(
            "edge",
            CanvasEndpoint::new("a", None::<&str>),
            CanvasEndpoint::new("b", None::<&str>),
        );
        edge.route = CanvasEdgeRoute::polyline([point(px(40.0), px(50.0))]);

        let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
            edge: &edge,
            source: point(px(0.0), px(0.0)),
            target: point(px(100.0), px(0.0)),
        });

        assert_eq!(
            path.segments,
            vec![
                CanvasRouteSegment::Line {
                    from: point(px(0.0), px(0.0)),
                    to: point(px(40.0), px(50.0)),
                },
                CanvasRouteSegment::Line {
                    from: point(px(40.0), px(50.0)),
                    to: point(px(100.0), px(0.0)),
                },
            ]
        );
        assert_eq!(
            path.document_points(),
            vec![
                point(px(0.0), px(0.0)),
                point(px(40.0), px(50.0)),
                point(px(100.0), px(0.0)),
            ]
        );
    }

    #[test]
    fn default_router_emits_cubic_segment_from_control_points() {
        let mut edge = CanvasEdge::new(
            "edge",
            CanvasEndpoint::new("a", None::<&str>),
            CanvasEndpoint::new("b", None::<&str>),
        );
        edge.route = CanvasEdgeRoute::new(CanvasEdgeRouteKind::CUBIC_BEZIER);
        edge.route.control_points = vec![point(px(25.0), px(40.0)), point(px(75.0), px(-40.0))];

        let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
            edge: &edge,
            source: point(px(0.0), px(0.0)),
            target: point(px(100.0), px(0.0)),
        });

        assert_eq!(
            path.segments,
            vec![CanvasRouteSegment::CubicBezier {
                from: point(px(0.0), px(0.0)),
                control_1: point(px(25.0), px(40.0)),
                control_2: point(px(75.0), px(-40.0)),
                to: point(px(100.0), px(0.0)),
            }]
        );
        assert_eq!(
            path.bounds().unwrap(),
            Bounds::from_corners(point(px(0.0), px(-40.0)), point(px(100.0), px(40.0)))
        );
    }

    #[test]
    fn default_router_emits_cubic_segment_without_control_points() {
        let mut edge = CanvasEdge::new(
            "edge",
            CanvasEndpoint::new("a", None::<&str>),
            CanvasEndpoint::new("b", None::<&str>),
        );
        edge.route = CanvasEdgeRoute::new(CanvasEdgeRouteKind::CUBIC_BEZIER);

        let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
            edge: &edge,
            source: point(px(0.0), px(0.0)),
            target: point(px(100.0), px(30.0)),
        });

        assert_eq!(path.segments.len(), 1);
        assert!(matches!(
            path.segments[0],
            CanvasRouteSegment::CubicBezier { .. }
        ));
    }

    #[test]
    fn default_router_emits_orthogonal_segments() {
        let mut edge = CanvasEdge::new(
            "edge",
            CanvasEndpoint::new("a", None::<&str>),
            CanvasEndpoint::new("b", None::<&str>),
        );
        edge.route = CanvasEdgeRoute::new(CanvasEdgeRouteKind::ORTHOGONAL);

        let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
            edge: &edge,
            source: point(px(0.0), px(0.0)),
            target: point(px(100.0), px(50.0)),
        });

        assert_eq!(
            path.document_points(),
            vec![
                point(px(0.0), px(0.0)),
                point(px(50.0), px(0.0)),
                point(px(50.0), px(50.0)),
                point(px(100.0), px(50.0)),
            ]
        );
        assert_eq!(
            path.segments,
            vec![
                CanvasRouteSegment::Line {
                    from: point(px(0.0), px(0.0)),
                    to: point(px(50.0), px(0.0)),
                },
                CanvasRouteSegment::Line {
                    from: point(px(50.0), px(0.0)),
                    to: point(px(50.0), px(50.0)),
                },
                CanvasRouteSegment::Line {
                    from: point(px(50.0), px(50.0)),
                    to: point(px(100.0), px(50.0)),
                },
            ]
        );
    }

    #[test]
    fn default_router_routes_orthogonal_waypoints_by_leg() {
        let mut edge = CanvasEdge::new(
            "edge",
            CanvasEndpoint::new("a", None::<&str>),
            CanvasEndpoint::new("b", None::<&str>),
        );
        edge.route = CanvasEdgeRoute::new(CanvasEdgeRouteKind::ORTHOGONAL);
        edge.route.waypoints = vec![point(px(40.0), px(30.0))];

        let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
            edge: &edge,
            source: point(px(0.0), px(0.0)),
            target: point(px(100.0), px(30.0)),
        });

        assert_eq!(
            path.document_points(),
            vec![
                point(px(0.0), px(0.0)),
                point(px(20.0), px(0.0)),
                point(px(20.0), px(30.0)),
                point(px(40.0), px(30.0)),
                point(px(100.0), px(30.0)),
            ]
        );
    }
}