Files
Ely-GPUI-Components/src/canvas/links.rs
T
ZacharyZhang-NYandClaude Opus 5.5 f3822a8d98 Canvas · Graphs and boards: node graph, flow chart, mind map, whiteboard
NodeGraph joins Nodes by wires between typed Ports: a title drags a
node, a drag out of an output lands on an input of the same kind, an
input takes one wire, and a press on a wired input lifts its wire.
Links join shapes on the plane and follow them, straight or at right
angles, with an arrow and words; the Connector tool draws them and the
Note tool drops a sticky note. A double press with Select writes on a
shape. FlowChart draws Steps as ellipses, boxes and diamonds with their
words. MindMap lays topics out from a root, half to each side, and asks
the owner to add, remove and rename by key. Whiteboard keeps its own
tool, view and selection over a board it hands back after each change.
Painting moves to canvas::paint; a press on the tool layer goes through
canvas::press, and the layer focuses itself and asks to delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:22:54 -04:00

114 lines
3.7 KiB
Rust

use gpui::SharedString;
use super::view::Frame;
/// A line from one shape to another that follows them as they move: straight or turning at right angles, with an arrow at its end and words at its middle.
#[derive(Clone, Debug, PartialEq)]
pub struct Link {
pub key: SharedString,
pub from: SharedString,
pub to: SharedString,
pub label: Option<SharedString>,
pub elbow: bool,
}
impl Link {
pub fn new(
key: impl Into<SharedString>,
from: impl Into<SharedString>,
to: impl Into<SharedString>,
) -> Self {
let (from, to) = (from.into(), to.into());
assert!(from != to, "a link from {from} to itself");
Self {
key: key.into(),
from,
to,
label: None,
elbow: false,
}
}
pub fn label(mut self, text: impl Into<SharedString>) -> Self {
self.label = Some(text.into());
self
}
/// Turns at right angles instead of running straight.
pub fn elbow(mut self) -> Self {
self.elbow = true;
self
}
}
/// The points a link runs through, in canvas units: out of the side of `from` that faces `to`, into the side of `to` that faces back. An elbow turns halfway between.
pub(crate) fn route(from: &Frame, to: &Frame, elbow: bool) -> Vec<(f32, f32)> {
let (a, b) = (from.center(), to.center());
let across = (b.0 - a.0).abs() >= (b.1 - a.1).abs();
let (exit, entry) = match (across, b.0 >= a.0, b.1 >= a.1) {
(true, true, _) => ((from.right(), a.1), (to.x, b.1)),
(true, false, _) => ((from.x, a.1), (to.right(), b.1)),
(false, _, true) => ((a.0, from.bottom()), (b.0, to.y)),
(false, _, false) => ((a.0, from.y), (b.0, to.bottom())),
};
match (elbow, across) {
(false, _) => vec![exit, entry],
(true, true) => {
let middle = (exit.0 + entry.0) / 2.0;
vec![exit, (middle, exit.1), (middle, entry.1), entry]
}
(true, false) => {
let middle = (exit.1 + entry.1) / 2.0;
vec![exit, (exit.0, middle), (entry.0, middle), entry]
}
}
}
/// The point halfway along a run of points.
pub(crate) fn halfway(points: &[(f32, f32)]) -> (f32, f32) {
let length =
|(a, b): (&(f32, f32), &(f32, f32))| ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
let whole: f32 = points.iter().zip(&points[1..]).map(length).sum();
let mut left = whole / 2.0;
for (a, b) in points.iter().zip(&points[1..]) {
let span = length((a, b));
if left <= span && span > 0.0 {
let share = left / span;
return (a.0 + (b.0 - a.0) * share, a.1 + (b.1 - a.1) * share);
}
left -= span;
}
*points.last().expect("a run has points")
}
#[cfg(test)]
mod tests {
use super::{halfway, route};
use crate::canvas::Frame;
#[test]
fn a_link_leaves_the_side_that_faces_the_other_shape() {
let (a, b) = (
Frame::new(0.0, 0.0, 100.0, 50.0),
Frame::new(200.0, 100.0, 100.0, 50.0),
);
assert_eq!(route(&a, &b, false), [(100.0, 25.0), (200.0, 125.0)]);
assert_eq!(route(&b, &a, false), [(200.0, 125.0), (100.0, 25.0)]);
let below = Frame::new(0.0, 300.0, 100.0, 50.0);
assert_eq!(route(&a, &below, false), [(50.0, 50.0), (50.0, 300.0)]);
}
#[test]
fn an_elbow_turns_halfway() {
let (a, b) = (
Frame::new(0.0, 0.0, 100.0, 50.0),
Frame::new(200.0, 100.0, 100.0, 50.0),
);
assert_eq!(
route(&a, &b, true),
[(100.0, 25.0), (150.0, 25.0), (150.0, 125.0), (200.0, 125.0)]
);
assert_eq!(halfway(&route(&a, &b, true)), (150.0, 75.0));
}
}