Panics · P32 Canvas: panels take their handlers in new; stale nodes, shapes, layers and topics change nothing
This commit is contained in:
1 parent
3741d5d5e8
commit
ffe4f38103
27 files changed
+471
-297
No files matched your search
@@ -104,32 +104,26 @@ impl AutoLayout {
|
||||
pub struct AutoLayoutControls {
|
||||
id: ElementId,
|
||||
layout: AutoLayout,
|
||||
on_change: Option<OnEdit<AutoLayout>>,
|
||||
on_change: OnEdit<AutoLayout>,
|
||||
}
|
||||
|
||||
impl AutoLayoutControls {
|
||||
pub fn new(id: impl Into<ElementId>, layout: AutoLayout) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
layout: AutoLayout,
|
||||
on_change: impl Fn(AutoLayout, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
layout,
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(AutoLayout, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AutoLayoutControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("auto layout controls {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let (id, layout, on_change) = (self.id, self.layout, &on_change);
|
||||
let place = Rc::new(editing(
|
||||
"auto layout",
|
||||
|
||||
@@ -102,25 +102,21 @@ impl Constraints {
|
||||
pub struct ConstraintsEditor {
|
||||
id: ElementId,
|
||||
constraints: Constraints,
|
||||
on_change: Option<OnEdit<Constraints>>,
|
||||
on_change: OnEdit<Constraints>,
|
||||
}
|
||||
|
||||
impl ConstraintsEditor {
|
||||
pub fn new(id: impl Into<ElementId>, constraints: Constraints) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
constraints: Constraints,
|
||||
on_change: impl Fn(Constraints, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
constraints,
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Constraints, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A hairline from `x`, `y` in shares of its box, `len` long across or down.
|
||||
@@ -135,9 +131,7 @@ fn hairline(x: f32, y: f32, len: f32, across: bool, color: Hsla) -> gpui::Div {
|
||||
|
||||
impl RenderOnce for ConstraintsEditor {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("constraints editor {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let (id, now, on_change) = (self.id, self.constraints, &on_change);
|
||||
let theme = cx.theme();
|
||||
let (lit, rest) = (theme.colors.accent, theme.colors.border);
|
||||
|
||||
+8
-26
@@ -99,8 +99,8 @@ pub struct ExportPanel {
|
||||
id: ElementId,
|
||||
name: SharedString,
|
||||
settings: Vec<ExportSetting>,
|
||||
on_change: Option<OnEdit<Vec<ExportSetting>>>,
|
||||
on_export: Option<OnEdit<Vec<String>>>,
|
||||
on_change: OnEdit<Vec<ExportSetting>>,
|
||||
on_export: OnEdit<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ExportPanel {
|
||||
@@ -109,41 +109,23 @@ impl ExportPanel {
|
||||
id: impl Into<ElementId>,
|
||||
name: impl Into<SharedString>,
|
||||
settings: impl IntoIterator<Item = ExportSetting>,
|
||||
on_change: impl Fn(Vec<ExportSetting>, &mut Window, &mut App) + 'static,
|
||||
on_export: impl Fn(Vec<String>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
settings: settings.into_iter().collect(),
|
||||
on_change: None,
|
||||
on_export: None,
|
||||
on_change: Rc::new(on_change),
|
||||
on_export: Rc::new(on_export),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<ExportSetting>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_export(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<String>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_export = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ExportPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("export panel {:?} has no on_change", self.id));
|
||||
let on_export = self
|
||||
.on_export
|
||||
.unwrap_or_else(|| panic!("export panel {:?} has no on_export", self.id));
|
||||
let on_change = self.on_change;
|
||||
let on_export = self.on_export;
|
||||
let (id, settings, on_change) = (self.id, self.settings, &on_change);
|
||||
let files = export_files(&self.name, &settings);
|
||||
let next = added(&settings);
|
||||
|
||||
@@ -168,12 +168,11 @@ pub(super) fn commit(
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let frame = scene
|
||||
.shapes
|
||||
.iter()
|
||||
.find(|shape| shape.key == key)
|
||||
.expect("the resized shape is listed")
|
||||
.frame;
|
||||
let Some(shape) = scene.shapes.iter().find(|shape| shape.key == key) else {
|
||||
log::error!("tool layer: resized shape {key} left the canvas");
|
||||
return;
|
||||
};
|
||||
let frame = shape.frame;
|
||||
let next = resized(frame, handle, (to.0 - from.0, to.1 - from.1), LEAST, square);
|
||||
log::info!("tool layer: resize {key} to {next:?}");
|
||||
if let Some(on_resize) = &handlers.on_resize {
|
||||
@@ -191,6 +190,10 @@ pub(super) fn commit(
|
||||
}
|
||||
}
|
||||
Gesture::Linking { from, to } => {
|
||||
if !scene.shapes.iter().any(|shape| shape.key == from) {
|
||||
log::error!("tool layer: the link's shape {from} left the canvas");
|
||||
return;
|
||||
}
|
||||
let Some(ix) = hit(&scene.shapes, to, REACH / scene.view.zoom) else {
|
||||
log::info!("tool layer: the link from {from} lands on nothing");
|
||||
return;
|
||||
|
||||
+31
-27
@@ -70,18 +70,9 @@ impl Node {
|
||||
(x, self.at.1 + HEAD + ROW * row as f32 + ROW / 2.0)
|
||||
}
|
||||
|
||||
fn row(&self, out: bool, port: &str) -> usize {
|
||||
fn row(&self, out: bool, port: &str) -> Option<usize> {
|
||||
let ports = if out { &self.outputs } else { &self.inputs };
|
||||
ports
|
||||
.iter()
|
||||
.position(|each| each.key == port)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"node {} has no {} {port}",
|
||||
self.key,
|
||||
if out { "output" } else { "input" }
|
||||
)
|
||||
})
|
||||
ports.iter().position(|each| each.key == port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,22 +110,26 @@ pub(crate) struct Socket {
|
||||
pub kind: SharedString,
|
||||
}
|
||||
|
||||
fn node<'a>(nodes: &'a [Node], key: &str) -> &'a Node {
|
||||
nodes
|
||||
.iter()
|
||||
.find(|node| node.key == key)
|
||||
.unwrap_or_else(|| panic!("no node {key} in the graph"))
|
||||
}
|
||||
/// Where a wire starts and ends in canvas units, and the kind it carries.
|
||||
pub(crate) type Ends = ((f32, f32), (f32, f32), SharedString);
|
||||
|
||||
/// An edge's ends in canvas units and the kind it carries.
|
||||
pub(crate) fn ends(nodes: &[Node], edge: &Edge) -> ((f32, f32), (f32, f32), SharedString) {
|
||||
let (from, to) = (node(nodes, &edge.from.0), node(nodes, &edge.to.0));
|
||||
let row = from.row(true, &edge.from.1);
|
||||
(
|
||||
from.socket(true, row),
|
||||
to.socket(false, to.row(false, &edge.to.1)),
|
||||
from.outputs[row].kind.clone(),
|
||||
)
|
||||
/// An edge's ends; none when a node or port it names left the graph.
|
||||
pub(crate) fn ends(nodes: &[Node], edge: &Edge) -> Option<Ends> {
|
||||
let node = |key: &SharedString| nodes.iter().find(|node| node.key == *key);
|
||||
let ends = node(&edge.from.0)
|
||||
.zip(node(&edge.to.0))
|
||||
.and_then(|(from, to)| {
|
||||
let (out, input) = (from.row(true, &edge.from.1)?, to.row(false, &edge.to.1)?);
|
||||
Some((
|
||||
from.socket(true, out),
|
||||
to.socket(false, input),
|
||||
from.outputs[out].kind.clone(),
|
||||
))
|
||||
});
|
||||
if ends.is_none() {
|
||||
log::error!("node editor: edge {edge:?} names a node or port gone from the graph");
|
||||
}
|
||||
ends
|
||||
}
|
||||
|
||||
/// The socket within `reach` of `point`, the topmost node first.
|
||||
@@ -199,7 +194,8 @@ mod tests {
|
||||
#[test]
|
||||
fn sockets_sit_on_the_sides_a_row_apart() {
|
||||
let nodes = graph();
|
||||
let (from, to, kind) = ends(&nodes, &Edge::new(("load", "image"), ("blur", "image")));
|
||||
let (from, to, kind) =
|
||||
ends(&nodes, &Edge::new(("load", "image"), ("blur", "image"))).expect("both ends");
|
||||
assert_eq!(from, (WIDE, HEAD + ROW / 2.0));
|
||||
assert_eq!(to, (300.0, HEAD + ROW / 2.0));
|
||||
assert_eq!(kind, "image");
|
||||
@@ -210,6 +206,14 @@ mod tests {
|
||||
("blur", "radius", false)
|
||||
);
|
||||
assert_eq!(socket_at(&nodes, (150.0, 40.0), 6.0), None);
|
||||
assert_eq!(
|
||||
ends(&nodes, &Edge::new(("load", "image"), ("gone", "image"))),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
ends(&nodes, &Edge::new(("load", "gone"), ("blur", "image"))),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -27,11 +27,12 @@ impl HistoryPanel {
|
||||
at: usize,
|
||||
) -> Self {
|
||||
let steps: Vec<SharedString> = steps.into_iter().map(Into::into).collect();
|
||||
assert!(
|
||||
at <= steps.len(),
|
||||
"history at {at} of {} steps",
|
||||
steps.len()
|
||||
);
|
||||
if at > steps.len() {
|
||||
log::error!(
|
||||
"history panel: at {at} of {} steps; none current",
|
||||
steps.len()
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
steps,
|
||||
|
||||
+8
-11
@@ -25,29 +25,26 @@ const REACH: f64 = 100_000.0;
|
||||
pub struct InspectorPanel {
|
||||
id: ElementId,
|
||||
selection: Vec<Shape>,
|
||||
on_change: Option<OnEdit<Shape>>,
|
||||
on_change: OnEdit<Shape>,
|
||||
}
|
||||
|
||||
impl InspectorPanel {
|
||||
pub fn new(id: impl Into<ElementId>, selection: impl IntoIterator<Item = Shape>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
selection: impl IntoIterator<Item = Shape>,
|
||||
on_change: impl Fn(Shape, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
selection: selection.into_iter().collect(),
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(mut self, handler: impl Fn(Shape, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InspectorPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("inspector panel {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let id = self.id;
|
||||
let [shape] = self.selection.as_slice() else {
|
||||
let title: SharedString = match self.selection.len() {
|
||||
|
||||
+14
-12
@@ -8,30 +8,30 @@ use super::{
|
||||
};
|
||||
use crate::lists::{DropAt, Tree, TreeNode};
|
||||
|
||||
/// The keys in paint order, bottom first, after `dragged` lands `at` `target` in a list drawn top first.
|
||||
/// The keys in paint order, bottom first, after `dragged` lands `at` `target` in a list drawn top first; none, logged, when either left the list.
|
||||
pub(crate) fn restacked(
|
||||
order: &[SharedString],
|
||||
dragged: &SharedString,
|
||||
target: &SharedString,
|
||||
at: DropAt,
|
||||
) -> Vec<SharedString> {
|
||||
assert!(order.contains(dragged), "layer {dragged} is not listed");
|
||||
) -> Option<Vec<SharedString>> {
|
||||
let mut rest: Vec<SharedString> = order
|
||||
.iter()
|
||||
.filter(|key| *key != dragged)
|
||||
.cloned()
|
||||
.collect();
|
||||
let under = rest
|
||||
.iter()
|
||||
.position(|key| key == target)
|
||||
.unwrap_or_else(|| panic!("layer {dragged} lands against {target}, which is not listed"));
|
||||
let under = rest.iter().position(|key| key == target);
|
||||
let (true, Some(under)) = (order.contains(dragged), under) else {
|
||||
log::error!("layer panel: {dragged} or {target} left the layers; nothing moves");
|
||||
return None;
|
||||
};
|
||||
let ix = match at {
|
||||
DropAt::Before => under + 1,
|
||||
DropAt::After => under,
|
||||
DropAt::Inside => panic!("layer {target} holds no layers"),
|
||||
};
|
||||
rest.insert(ix, dragged.clone());
|
||||
rest
|
||||
Some(rest)
|
||||
}
|
||||
|
||||
/// The canvas's layers, topmost first. A box shows or hides each, a press selects and Shift or Command adds, F2 renames, and a drag restacks; a locked layer says so. It fills its box, which the host sizes.
|
||||
@@ -137,7 +137,9 @@ impl RenderOnce for LayerPanel {
|
||||
if let Some(on_restack) = self.on_restack {
|
||||
tree = tree.on_move(move |dragged, target, at, window, cx| {
|
||||
log::info!("layer panel: {dragged} {at:?} {target}");
|
||||
on_restack(&restacked(&order, dragged, target, at), window, cx);
|
||||
if let Some(order) = restacked(&order, dragged, target, at) {
|
||||
on_restack(&order, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
tree
|
||||
@@ -160,15 +162,15 @@ mod tests {
|
||||
let order = keys(&["a", "b", "c"]);
|
||||
assert_eq!(
|
||||
restacked(&order, &"a".into(), &"c".into(), DropAt::Before),
|
||||
keys(&["b", "c", "a"])
|
||||
Some(keys(&["b", "c", "a"]))
|
||||
);
|
||||
assert_eq!(
|
||||
restacked(&order, &"c".into(), &"a".into(), DropAt::After),
|
||||
keys(&["c", "a", "b"])
|
||||
Some(keys(&["c", "a", "b"]))
|
||||
);
|
||||
assert_eq!(
|
||||
restacked(&order, &"a".into(), &"b".into(), DropAt::After),
|
||||
keys(&["a", "b", "c"]),
|
||||
Some(keys(&["a", "b", "c"])),
|
||||
"just under b is where a lies"
|
||||
);
|
||||
}
|
||||
|
||||
+29
-21
@@ -24,37 +24,40 @@ impl Topic {
|
||||
self
|
||||
}
|
||||
|
||||
/// The map with `topic` added last under `parent`.
|
||||
pub fn adding(&self, parent: &str, topic: Topic) -> Topic {
|
||||
/// The map with `topic` added last under `parent`; none, logged, when the map holds no `parent`.
|
||||
pub fn adding(&self, parent: &str, topic: Topic) -> Option<Topic> {
|
||||
let mut next = self.clone();
|
||||
let under = next
|
||||
.find(parent)
|
||||
.unwrap_or_else(|| panic!("no topic {parent} to add under"));
|
||||
let Some(under) = next.find(parent) else {
|
||||
log::error!("mind map: no topic {parent} to add under");
|
||||
return None;
|
||||
};
|
||||
under.children.push(topic);
|
||||
next
|
||||
Some(next)
|
||||
}
|
||||
|
||||
/// The map without `key` and what lies under it.
|
||||
pub fn removing(&self, key: &str) -> Topic {
|
||||
assert!(self.key != key, "the root topic {key} stays");
|
||||
/// The map without `key` and what lies under it; none, logged, for the root or a topic the map lacks.
|
||||
pub fn removing(&self, key: &str) -> Option<Topic> {
|
||||
let mut next = self.clone();
|
||||
let parent = next
|
||||
.parent_of(key)
|
||||
.unwrap_or_else(|| panic!("no topic {key} to remove"));
|
||||
let Some(parent) = next.parent_of(key) else {
|
||||
log::error!("mind map: no topic {key} below the root to remove");
|
||||
return None;
|
||||
};
|
||||
next.find(&parent)
|
||||
.expect("the parent is in the map")
|
||||
.children
|
||||
.retain(|topic| topic.key != key);
|
||||
next
|
||||
Some(next)
|
||||
}
|
||||
|
||||
/// The map with `key` reading `text`.
|
||||
pub fn renaming(&self, key: &str, text: impl Into<SharedString>) -> Topic {
|
||||
/// The map with `key` reading `text`; none, logged, when the map holds no `key`.
|
||||
pub fn renaming(&self, key: &str, text: impl Into<SharedString>) -> Option<Topic> {
|
||||
let mut next = self.clone();
|
||||
next.find(key)
|
||||
.unwrap_or_else(|| panic!("no topic {key} to rename"))
|
||||
.text = text.into();
|
||||
next
|
||||
let Some(topic) = next.find(key) else {
|
||||
log::error!("mind map: no topic {key} to rename");
|
||||
return None;
|
||||
};
|
||||
topic.text = text.into();
|
||||
Some(next)
|
||||
}
|
||||
|
||||
fn find(&mut self, key: &str) -> Option<&mut Topic> {
|
||||
@@ -229,10 +232,15 @@ mod tests {
|
||||
fn edits_add_remove_and_rename_by_key() {
|
||||
let map = map()
|
||||
.adding("b", Topic::new("b1", "Release"))
|
||||
.renaming("c", "Money");
|
||||
.and_then(|map| map.renaming("c", "Money"))
|
||||
.expect("both topics are in the map");
|
||||
assert_eq!(map.children[1].children[0].key, "b1");
|
||||
assert_eq!(map.children[2].text, "Money");
|
||||
let map = map.removing("a");
|
||||
assert_eq!(map.adding("gone", Topic::new("x", "X")), None);
|
||||
assert_eq!(map.renaming("gone", "X"), None);
|
||||
assert_eq!(map.removing(&map.key), None, "the root stays");
|
||||
assert_eq!(map.removing("gone"), None);
|
||||
let map = map.removing("a").expect("a is in the map");
|
||||
assert_eq!(
|
||||
map.children
|
||||
.iter()
|
||||
|
||||
@@ -117,13 +117,13 @@ impl RenderOnce for MindMap {
|
||||
let focus = tab_stop((id.clone(), "focus").into(), true, window, cx);
|
||||
let editing =
|
||||
window.use_keyed_state((id.clone(), "editing"), cx, |_, _| Editing::default());
|
||||
let selected = self.selected.clone();
|
||||
if let Some(key) = &selected {
|
||||
assert!(
|
||||
placed.iter().any(|topic| topic.key == *key),
|
||||
"mind map {id:?}: no topic {key} to select"
|
||||
);
|
||||
}
|
||||
let selected = self.selected.clone().filter(|key| {
|
||||
let placed = placed.iter().any(|topic| topic.key == *key);
|
||||
if !placed {
|
||||
log::error!("mind map {id:?}: no topic {key}; none selected");
|
||||
}
|
||||
placed
|
||||
});
|
||||
let rename = {
|
||||
let (editing, placed, on_rename, home) = (
|
||||
editing.clone(),
|
||||
|
||||
+36
-25
@@ -234,17 +234,20 @@ impl RenderOnce for NodeGraph {
|
||||
let wired = edges
|
||||
.iter()
|
||||
.find(|edge| edge.to == (node_key.clone(), port_key.clone()));
|
||||
match wired {
|
||||
Some(edge) => {
|
||||
let source = nodes
|
||||
.iter()
|
||||
.find(|node| node.key == edge.from.0)
|
||||
.expect("a wired node");
|
||||
let port = source
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|port| port.key == edge.from.1)
|
||||
.expect("a wired port");
|
||||
let source = wired.and_then(|edge| {
|
||||
let port = nodes
|
||||
.iter()
|
||||
.find(|node| node.key == edge.from.0)?
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|port| port.key == edge.from.1);
|
||||
if port.is_none() {
|
||||
log::error!("node editor: edge {edge:?} starts at a port gone from the graph");
|
||||
}
|
||||
Some(edge).zip(port)
|
||||
});
|
||||
match source {
|
||||
Some((edge, port)) => {
|
||||
Pull::Wiring {
|
||||
from: Socket {
|
||||
node: edge.from.0.clone(),
|
||||
@@ -330,16 +333,26 @@ impl RenderOnce for NodeGraph {
|
||||
let (nodes, edges, hue) = (nodes.clone(), edges.clone(), hue.clone());
|
||||
let pending = match &pull {
|
||||
Pull::Wiring { from, to, .. } => {
|
||||
let source = nodes
|
||||
.iter()
|
||||
.find(|node| node.key == from.node)
|
||||
.expect("a wiring node");
|
||||
let row = source
|
||||
.outputs
|
||||
.iter()
|
||||
.position(|port| port.key == from.port)
|
||||
.expect("a wiring port");
|
||||
Some((source.socket(true, row), *to, from.kind.clone()))
|
||||
let source = nodes.iter().find(|node| node.key == from.node);
|
||||
let wiring = source.and_then(|source| {
|
||||
let row = source
|
||||
.outputs
|
||||
.iter()
|
||||
.position(|port| port.key == from.port)?;
|
||||
Some((
|
||||
source.socket(true, row),
|
||||
*to,
|
||||
source.outputs[row].kind.clone(),
|
||||
))
|
||||
});
|
||||
if wiring.is_none() {
|
||||
log::error!(
|
||||
"node editor: the wire's port {}.{} left the graph",
|
||||
from.node,
|
||||
from.port
|
||||
);
|
||||
}
|
||||
wiring
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -353,10 +366,8 @@ impl RenderOnce for NodeGraph {
|
||||
let drawn = edges
|
||||
.iter()
|
||||
.filter(|edge| Some(*edge) != lifted.as_ref())
|
||||
.map(|edge| {
|
||||
let (from, to, kind) = ends(&nodes, edge);
|
||||
(from, to, hue(&kind))
|
||||
})
|
||||
.filter_map(|edge| ends(&nodes, edge))
|
||||
.map(|(from, to, kind)| (from, to, hue(&kind)))
|
||||
.chain(
|
||||
pending
|
||||
.iter()
|
||||
|
||||
@@ -66,12 +66,13 @@ impl ToolPalette {
|
||||
|
||||
impl RenderOnce for ToolPalette {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
assert!(
|
||||
self.tools.contains(&self.tool),
|
||||
"tool palette {:?}: {} is not offered",
|
||||
self.id,
|
||||
self.tool.words()
|
||||
);
|
||||
if !self.tools.contains(&self.tool) {
|
||||
log::error!(
|
||||
"tool palette {:?}: {} is not offered; none chosen",
|
||||
self.id,
|
||||
self.tool.words()
|
||||
);
|
||||
}
|
||||
let on_change = self.on_change;
|
||||
self.tools
|
||||
.into_iter()
|
||||
|
||||
+7
-8
@@ -167,15 +167,14 @@ impl RenderOnce for InfiniteCanvas {
|
||||
.links
|
||||
.iter()
|
||||
.filter_map(|link| {
|
||||
let named = |key: &SharedString| {
|
||||
self.shapes
|
||||
.iter()
|
||||
.find(|shape| shape.key == *key)
|
||||
.unwrap_or_else(|| {
|
||||
panic!("link {} names {key}, which is not on the canvas", link.key)
|
||||
})
|
||||
let named = |key: &SharedString| self.shapes.iter().find(|shape| shape.key == *key);
|
||||
let (Some(from), Some(to)) = (named(&link.from), named(&link.to)) else {
|
||||
log::error!(
|
||||
"canvas: link {} names a shape gone from the canvas",
|
||||
link.key
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let (from, to) = (named(&link.from), named(&link.to));
|
||||
(!from.hidden && !to.hidden).then(|| {
|
||||
let points = route(&from.frame, &to.frame, link.elbow)
|
||||
.into_iter()
|
||||
|
||||
+24
-36
@@ -23,16 +23,20 @@ pub struct ColorPanel {
|
||||
id: ElementId,
|
||||
color: Hsla,
|
||||
swatches: Vec<(SharedString, Hsla)>,
|
||||
on_change: Option<OnEdit<Hsla>>,
|
||||
on_change: OnEdit<Hsla>,
|
||||
}
|
||||
|
||||
impl ColorPanel {
|
||||
pub fn new(id: impl Into<ElementId>, color: impl Into<Hsla>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
color: impl Into<Hsla>,
|
||||
on_change: impl Fn(Hsla, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
color: color.into(),
|
||||
swatches: Vec::new(),
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,18 +51,11 @@ impl ColorPanel {
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(mut self, handler: impl Fn(Hsla, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ColorPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("color panel {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let (picked, chosen) = (on_change.clone(), on_change);
|
||||
let report = |on_change: OnEdit<Hsla>| {
|
||||
move |color: Hsla, window: &mut Window, cx: &mut App| {
|
||||
@@ -98,32 +95,26 @@ pub struct Shadow {
|
||||
pub struct ShadowEditor {
|
||||
id: ElementId,
|
||||
shadows: Vec<Shadow>,
|
||||
on_change: Option<OnEdit<Vec<Shadow>>>,
|
||||
on_change: OnEdit<Vec<Shadow>>,
|
||||
}
|
||||
|
||||
impl ShadowEditor {
|
||||
pub fn new(id: impl Into<ElementId>, shadows: impl IntoIterator<Item = Shadow>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
shadows: impl IntoIterator<Item = Shadow>,
|
||||
on_change: impl Fn(Vec<Shadow>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
shadows: shadows.into_iter().collect(),
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<Shadow>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ShadowEditor {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("shadow editor {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let lift = cx
|
||||
.theme()
|
||||
.elevation(Elevation::Floating)
|
||||
@@ -247,31 +238,28 @@ pub struct Border {
|
||||
pub struct BorderEditor {
|
||||
id: ElementId,
|
||||
border: Border,
|
||||
on_change: Option<OnEdit<Border>>,
|
||||
on_change: OnEdit<Border>,
|
||||
}
|
||||
|
||||
impl BorderEditor {
|
||||
pub fn new(id: impl Into<ElementId>, border: Border) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
border: Border,
|
||||
on_change: impl Fn(Border, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
border,
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(mut self, handler: impl Fn(Border, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
const CORNERS: [&str; 4] = ["TL", "TR", "BR", "BL"];
|
||||
|
||||
impl RenderOnce for BorderEditor {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("border editor {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let (id, border, on_change) = (self.id, self.border, &on_change);
|
||||
let parted = window.use_keyed_state((id.clone(), "split"), cx, |_, _| false);
|
||||
let uneven = border.radii.iter().any(|radius| *radius != border.radii[0]);
|
||||
|
||||
+132
-1
@@ -2,7 +2,7 @@ use gpui::{AnyElement, Entity, IntoElement, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{
|
||||
Stage, said, say, settle, stage,
|
||||
tools::{drag, layer, pair, press},
|
||||
tools::{at, drag, layer, pair, press},
|
||||
};
|
||||
use crate::canvas::{
|
||||
Board, Edge, Link, MindMap, Node, NodeGraph, Port, Tool, Topic, Viewport, Whiteboard,
|
||||
@@ -245,3 +245,134 @@ fn after_writing_on_a_shape_its_board_still_takes_backspace(cx: &mut TestAppCont
|
||||
["shapes [\"a\", \"b\"] links 1", "shapes [\"a\"] links 0"]
|
||||
);
|
||||
}
|
||||
|
||||
/// Presses at `from` and drags to `to` in four moves.
|
||||
fn pull(from: (f32, f32), to: (f32, f32), cx: &mut VisualTestContext) {
|
||||
use gpui::{Modifiers, MouseButton};
|
||||
cx.simulate_mouse_move(at(from.0, from.1), None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at(from.0, from.1), MouseButton::Left, Modifiers::none());
|
||||
for step in 1..=4 {
|
||||
let share = step as f32 / 4.0;
|
||||
let (x, y) = (
|
||||
from.0 + (to.0 - from.0) * share,
|
||||
from.1 + (to.1 - from.1) * share,
|
||||
);
|
||||
cx.simulate_mouse_move(at(x, y), Some(MouseButton::Left), Modifiers::none());
|
||||
settle(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn release(to: (f32, f32), cx: &mut VisualTestContext) {
|
||||
let (left, none) = (gpui::MouseButton::Left, gpui::Modifiers::none());
|
||||
cx.simulate_mouse_up(at(to.0, to.1), left, none);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
/// The owner changes what it hands the part, mid-gesture.
|
||||
fn change(host: &Entity<Stage>, cx: &mut VisualTestContext) {
|
||||
host.update(cx, |stage, cx| {
|
||||
stage.said.push("owner changed".into());
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn f2_on_a_selected_topic_that_left_renames_nothing(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(
|
||||
|_, _| {
|
||||
let view = Viewport::new(-200.0, -150.0, 1.0);
|
||||
MindMap::new("map", Topic::new("root", "Root"), view)
|
||||
.selected(Some("gone"))
|
||||
.into_any_element()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
press(200.0, 150.0, 1, cx);
|
||||
tap("f2", cx);
|
||||
}
|
||||
|
||||
/// Two wireable nodes whose source leaves, or changes kind, once the owner changes.
|
||||
fn shifting(stage: &Stage, owner: Entity<Stage>, retype: bool) -> AnyElement {
|
||||
let mut current = nodes();
|
||||
if !stage.said.is_empty() {
|
||||
match retype {
|
||||
true => current[0].outputs[0].kind = "number".into(),
|
||||
false => drop(current.remove(0)),
|
||||
}
|
||||
}
|
||||
NodeGraph::new("graph", Viewport::new(0.0, 0.0, 1.0), current, [])
|
||||
.on_connect(move |edge, _, cx| {
|
||||
let words = format!(
|
||||
"connect {}.{} {}.{}",
|
||||
edge.from.0, edge.from.1, edge.to.0, edge.to.1
|
||||
);
|
||||
say(&owner, words, cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_wire_whose_source_left_connects_nothing(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|stage, owner| shifting(stage, owner, false), cx);
|
||||
pull((200.0, 46.0), (301.0, 47.0), cx);
|
||||
change(&host, cx);
|
||||
release((301.0, 47.0), cx);
|
||||
assert_eq!(said(&host, cx), ["owner changed"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_wire_whose_source_changed_kind_connects_nothing(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|stage, owner| shifting(stage, owner, true), cx);
|
||||
pull((200.0, 46.0), (301.0, 47.0), cx);
|
||||
change(&host, cx);
|
||||
release((301.0, 47.0), cx);
|
||||
assert_eq!(said(&host, cx), ["owner changed"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_link_whose_shape_left_links_nothing(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(
|
||||
|stage, owner| {
|
||||
let mut shapes = pair();
|
||||
if !stage.said.is_empty() {
|
||||
shapes.remove(0);
|
||||
}
|
||||
let view = Viewport::new(0.0, 0.0, 1.0);
|
||||
let tool =
|
||||
crate::canvas::ToolLayer::new("layer", Tool::Connector, view, shapes.clone())
|
||||
.on_link(move |from, to, _, cx| say(&owner, format!("link {from} {to}"), cx));
|
||||
crate::canvas::InfiniteCanvas::new("plane", view)
|
||||
.shapes(shapes)
|
||||
.layer(tool)
|
||||
.into_any_element()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
pull((50.0, 50.0), (250.0, 60.0), cx);
|
||||
change(&host, cx);
|
||||
release((250.0, 60.0), cx);
|
||||
assert_eq!(said(&host, cx), ["owner changed"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_layer_that_left_mid_drag_restacks_nothing(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(
|
||||
|stage, owner| {
|
||||
let mut shapes = pair();
|
||||
if !stage.said.is_empty() {
|
||||
shapes.remove(0);
|
||||
}
|
||||
crate::canvas::LayerPanel::new("layers", shapes)
|
||||
.on_restack(move |keys, _, cx| say(&owner, format!("restack {keys:?}"), cx))
|
||||
.into_any_element()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let a = cx.debug_bounds("tree-chevron-a").expect("a row").center();
|
||||
let b = cx.debug_bounds("tree-chevron-b").expect("b row").center();
|
||||
pull((100.0, f32::from(a.y)), (100.0, f32::from(b.y)), cx);
|
||||
change(&host, cx);
|
||||
release((100.0, f32::from(b.y)), cx);
|
||||
assert_eq!(said(&host, cx), ["owner changed"]);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use crate::{primitives::FocusNext, theme::Theme};
|
||||
|
||||
mod boards;
|
||||
mod panels;
|
||||
mod stale;
|
||||
mod tools;
|
||||
|
||||
/// A view that shows one canvas part and keeps the viewports and words it heard.
|
||||
|
||||
@@ -59,9 +59,10 @@ fn places(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
||||
padding: (8.0, 8.0),
|
||||
place: (Place::Start, Place::Start),
|
||||
};
|
||||
AutoLayoutControls::new("auto", layout)
|
||||
.on_change(move |next, _, cx| say(&owner, format!("{:?}", next.place), cx))
|
||||
.into_any_element()
|
||||
AutoLayoutControls::new("auto", layout, move |next, _, cx| {
|
||||
say(&owner, format!("{:?}", next.place), cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -89,12 +90,12 @@ fn sizes(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
||||
scale: 1.0,
|
||||
format: ExportFormat::Png,
|
||||
}],
|
||||
move |next, _, cx| {
|
||||
let scales: Vec<f32> = next.iter().map(|setting| setting.scale).collect();
|
||||
say(&added, format!("sizes {scales:?}"), cx)
|
||||
},
|
||||
move |files, _, cx| say(&sent, format!("files {files:?}"), cx),
|
||||
)
|
||||
.on_change(move |next, _, cx| {
|
||||
let scales: Vec<f32> = next.iter().map(|setting| setting.scale).collect();
|
||||
say(&added, format!("sizes {scales:?}"), cx)
|
||||
})
|
||||
.on_export(move |files, _, cx| say(&sent, format!("files {files:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement, Styled, TestAppContext, div, px};
|
||||
|
||||
use super::{Stage, settle, stage};
|
||||
use crate::canvas::{
|
||||
Edge, Frame, HistoryPanel, InfiniteCanvas, Link, MindMap, Node, NodeGraph, Port, Shape,
|
||||
ShapeKind, Tool, ToolPalette, Topic, Viewport,
|
||||
};
|
||||
|
||||
/// Each part handed a key or an edge that its list no longer holds.
|
||||
fn lagging(_: &Stage, _: Entity<Stage>) -> AnyElement {
|
||||
let view = Viewport::new(0.0, 0.0, 1.0);
|
||||
let load = Node::new("load", "Load", (0.0, 0.0)).output(Port::new("image", "Image", "image"));
|
||||
let shape = Shape::new("a", "A", ShapeKind::Rect, Frame::new(0.0, 0.0, 40.0, 40.0));
|
||||
div()
|
||||
.size_full()
|
||||
.child(div().h(px(120.0)).child(NodeGraph::new(
|
||||
"graph",
|
||||
view,
|
||||
[load],
|
||||
[Edge::new(("load", "image"), ("gone", "image"))],
|
||||
)))
|
||||
.child(
|
||||
div().h(px(120.0)).child(
|
||||
InfiniteCanvas::new("plane", view)
|
||||
.shapes([shape])
|
||||
.links([Link::new("l", "a", "gone")]),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().h(px(120.0)).child(
|
||||
MindMap::new("map", Topic::new("root", "Root"), view).selected(Some("gone")),
|
||||
),
|
||||
)
|
||||
.child(ToolPalette::new("tools", Tool::Brush).tools([Tool::Select]))
|
||||
.child(HistoryPanel::new("history", ["Draw"], 4))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn gone_nodes_shapes_topics_tools_and_steps_draw(cx: &mut TestAppContext) {
|
||||
let (_, cx) = stage(lagging, cx);
|
||||
settle(cx);
|
||||
}
|
||||
+12
-10
@@ -228,16 +228,18 @@ impl RenderOnce for ToolLayer {
|
||||
Gesture::Stroke { points } if points.len() > 1 => {
|
||||
vec![stroke_shape(points, self.brush.size, self.brush.hue)]
|
||||
}
|
||||
Gesture::Linking { from, to } => {
|
||||
let start = shapes
|
||||
.iter()
|
||||
.find(|shape| shape.key == *from)
|
||||
.expect("a link starts on a listed shape")
|
||||
.frame
|
||||
.center();
|
||||
let (kind, frame) = drawn(ShapeKind::Arrow(Corner::BottomRight), start, *to, false);
|
||||
vec![Shape::new("+link", "Link", kind, frame)]
|
||||
}
|
||||
Gesture::Linking { from, to } => match shapes.iter().find(|shape| shape.key == *from) {
|
||||
Some(start) => {
|
||||
let start = start.frame.center();
|
||||
let (kind, frame) =
|
||||
drawn(ShapeKind::Arrow(Corner::BottomRight), start, *to, false);
|
||||
vec![Shape::new("+link", "Link", kind, frame)]
|
||||
}
|
||||
None => {
|
||||
log::error!("tool layer: the link's shape {from} left the canvas");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
_ if tool == Tool::Pen && pen.len() > 1 => {
|
||||
vec![stroke_shape(&pen, PEN, self.brush.hue)]
|
||||
}
|
||||
|
||||
@@ -48,34 +48,29 @@ pub struct TypographyPanel {
|
||||
id: ElementId,
|
||||
style: TypeStyle,
|
||||
search: Entity<TextInput>,
|
||||
on_change: Option<OnEdit<TypeStyle>>,
|
||||
on_change: OnEdit<TypeStyle>,
|
||||
}
|
||||
|
||||
impl TypographyPanel {
|
||||
/// `search` is the family field's text, which the owner keeps.
|
||||
pub fn new(id: impl Into<ElementId>, style: TypeStyle, search: &Entity<TextInput>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
style: TypeStyle,
|
||||
search: &Entity<TextInput>,
|
||||
on_change: impl Fn(TypeStyle, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
style,
|
||||
search: search.clone(),
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(TypeStyle, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TypographyPanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("typography panel {:?} has no on_change", self.id));
|
||||
let on_change = self.on_change;
|
||||
let (id, style, on_change) = (self.id, self.style, &on_change);
|
||||
let theme = cx.theme();
|
||||
let family = editing(
|
||||
|
||||
+10
-10
@@ -195,11 +195,11 @@ impl RenderOnce for Whiteboard {
|
||||
let key = key.clone();
|
||||
sized(
|
||||
&|board, _| {
|
||||
let shape = board
|
||||
.shapes
|
||||
.iter_mut()
|
||||
.find(|shape| shape.key == key)
|
||||
.expect("a resized shape is on the board");
|
||||
let Some(shape) = board.shapes.iter_mut().find(|shape| shape.key == key)
|
||||
else {
|
||||
log::error!("whiteboard: resized shape {key} left the board");
|
||||
return;
|
||||
};
|
||||
shape.frame = frame;
|
||||
},
|
||||
window,
|
||||
@@ -235,11 +235,11 @@ impl RenderOnce for Whiteboard {
|
||||
let (key, text) = (key.clone(), text.clone());
|
||||
written(
|
||||
&|board, _| {
|
||||
let shape = board
|
||||
.shapes
|
||||
.iter_mut()
|
||||
.find(|shape| shape.key == key)
|
||||
.expect("a written shape is on the board");
|
||||
let Some(shape) = board.shapes.iter_mut().find(|shape| shape.key == key)
|
||||
else {
|
||||
log::error!("whiteboard: written shape {key} left the board");
|
||||
return;
|
||||
};
|
||||
*shape = shape.written(text.clone());
|
||||
},
|
||||
window,
|
||||
|
||||
@@ -66,6 +66,23 @@ impl Ending {
|
||||
}
|
||||
}
|
||||
Pull::Wiring { from, lifted, to } => {
|
||||
let kind = self
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.key == from.node)
|
||||
.and_then(|node| {
|
||||
let port = node.outputs.iter().find(|port| port.key == from.port)?;
|
||||
Some(port.kind.clone())
|
||||
});
|
||||
let Some(kind) = kind else {
|
||||
log::error!(
|
||||
"node graph: the wire's port {}.{} left the graph",
|
||||
from.node,
|
||||
from.port
|
||||
);
|
||||
return;
|
||||
};
|
||||
let from = Socket { kind, ..from };
|
||||
let target =
|
||||
socket_at(&self.nodes, to, REACH / self.zoom).filter(|to| fits(&from, to));
|
||||
let made = target.map(|to| {
|
||||
|
||||
Reference in new issue
Block a user