1use dioxus::prelude::*;
5
6use crate::path::{edge_path, EdgeGeometry, EdgePath};
7use crate::ports;
8use crate::state::{FlowCore, Interaction};
9use crate::types::{Edge, Id, MarkerKind, Point, Rect, Side};
10
11pub(crate) const HANDLE_RIM: f64 = 5.0;
14
15#[derive(Clone, PartialEq)]
18pub struct EdgeViewCtx {
19 pub edge: Edge,
20 pub source: Point,
21 pub source_side: Side,
22 pub target: Point,
23 pub target_side: Side,
24 pub path: EdgePath,
27 pub marker_end: Option<String>,
29}
30
31#[component]
32pub(crate) fn EdgeItem(
33 edge: Edge,
34 source: Point,
35 source_side: Side,
36 target: Point,
37 target_side: Side,
38 source_rect: Rect,
39 target_rect: Rect,
40 edge_view: Option<Callback<EdgeViewCtx, Element>>,
41 on_edge_click: Option<EventHandler<Id>>,
42) -> Element {
43 let core = use_context::<FlowCore>();
44 let path = edge_path(
45 edge.kind,
46 &EdgeGeometry::new(source, source_side, target, target_side)
47 .with_rects(source_rect, target_rect),
48 );
49 let marker_end = match edge.marker_end {
50 MarkerKind::ArrowClosed => Some(format!("url(#df-arrowclosed-{})", core.iid)),
51 MarkerKind::Arrow => Some(format!("url(#df-arrow-{})", core.iid)),
52 MarkerKind::None => None,
53 };
54
55 let class = format!(
56 "df-edge{}{}{}",
57 if edge.selected { " df-selected" } else { "" },
58 if edge.animated { " df-animated" } else { "" },
59 edge.class
60 .as_deref()
61 .map(|c| format!(" {c}"))
62 .unwrap_or_default(),
63 );
64
65 if let Some(view) = edge_view {
66 let ctx = EdgeViewCtx {
67 edge: edge.clone(),
68 source,
69 source_side,
70 target,
71 target_side,
72 path: path.clone(),
73 marker_end: marker_end.clone(),
74 };
75 let custom = view.call(ctx);
76 let id = edge.id.clone();
77 let selectable = edge.selectable;
78 return rsx! {
79 g { class,
80 {custom}
81 path {
83 class: "df-edge-hit",
84 d: "{path.d}",
85 onpointerdown: move |evt| {
86 edge_pointer_down(core, &id, selectable, &on_edge_click, evt)
87 },
88 }
89 }
90 };
91 }
92
93 let id = edge.id.clone();
94 let selectable = edge.selectable;
95 rsx! {
96 g { class,
97 path {
98 class: "df-edge-path",
99 d: "{path.d}",
100 "marker-end": marker_end,
101 style: edge.style.as_deref().unwrap_or_default(),
102 }
103 path {
104 class: "df-edge-hit",
105 d: "{path.d}",
106 onpointerdown: move |evt| {
107 edge_pointer_down(core, &id, selectable, &on_edge_click, evt)
108 },
109 }
110 if let Some(label) = edge.label.as_deref() {
111 text {
112 class: "df-edge-label",
113 x: path.label.x,
114 y: path.label.y,
115 "{label}"
116 }
117 }
118 }
119 }
120}
121
122#[derive(Clone, PartialEq)]
130pub struct SeatEdgeViewCtx {
131 pub edge: Edge,
132 pub anchors: ports::EdgeAnchors,
133 pub geometry: ports::EdgeGeometry,
134}
135
136fn seat_geometry(edge: &Edge, anchors: ports::EdgeAnchors) -> ports::EdgeGeometry {
138 let arrows = ports::Arrows::new(
139 edge.marker_start != MarkerKind::None,
140 edge.marker_end != MarkerKind::None,
141 );
142 ports::edge_geometry(
143 anchors.start,
144 anchors.end,
145 arrows,
146 edge.weight,
147 edge.label_position,
148 false,
149 )
150}
151
152#[component]
165pub fn SeatEdges(
166 edges: ReadSignal<Vec<Edge>>,
168 anchors: ReadSignal<std::collections::BTreeMap<Id, ports::EdgeAnchors>>,
170 edge_view: Option<Callback<SeatEdgeViewCtx, Element>>,
172 on_edge_click: Option<EventHandler<Id>>,
174 #[props(default = true)]
178 hit_paths: bool,
179 class: Option<String>,
181) -> Element {
182 let class = format!(
183 "df-edges{}",
184 class
185 .as_deref()
186 .map(|c| format!(" {c}"))
187 .unwrap_or_default()
188 );
189 let edges = edges.read();
190 let anchors = anchors.read();
191 rsx! {
192 svg { class, "aria-hidden": "true",
193 for edge in edges.iter() {
194 if let Some(pair) = anchors.get(&edge.id) {
195 SeatEdgeItem {
196 key: "{edge.id}",
197 edge: edge.clone(),
198 anchors: *pair,
199 edge_view,
200 on_edge_click,
201 hit_paths,
202 }
203 }
204 }
205 }
206 }
207}
208
209#[component]
217pub fn SeatEdgeLabels(
218 edges: ReadSignal<Vec<Edge>>,
220 anchors: ReadSignal<std::collections::BTreeMap<Id, ports::EdgeAnchors>>,
222 label_view: Option<Callback<SeatEdgeViewCtx, Element>>,
224 class: Option<String>,
226) -> Element {
227 let class = format!(
228 "df-edges df-edge-labels{}",
229 class
230 .as_deref()
231 .map(|c| format!(" {c}"))
232 .unwrap_or_default()
233 );
234 let edges = edges.read();
235 let anchors = anchors.read();
236 rsx! {
237 svg { class, "aria-hidden": "true",
238 for edge in edges.iter() {
239 if let Some(pair) = anchors.get(&edge.id) {
240 if let Some(view) = label_view {
241 {view.call(SeatEdgeViewCtx {
242 edge: edge.clone(),
243 anchors: *pair,
244 geometry: seat_geometry(edge, *pair),
245 })}
246 } else if let Some(label) = edge.label.as_deref() {
247 {
248 let at = seat_geometry(edge, *pair).label;
249 rsx! {
250 text {
251 key: "{edge.id}",
252 class: "df-edge-label",
253 x: at.x,
254 y: at.y,
255 "{label}"
256 }
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264}
265
266#[component]
270fn SeatEdgeItem(
271 edge: Edge,
272 anchors: ports::EdgeAnchors,
273 edge_view: Option<Callback<SeatEdgeViewCtx, Element>>,
274 on_edge_click: Option<EventHandler<Id>>,
275 hit_paths: bool,
276) -> Element {
277 let core = use_context::<FlowCore>();
278 let geometry = seat_geometry(&edge, anchors);
279
280 let class = format!(
281 "df-edge{}{}{}",
282 if edge.selected { " df-selected" } else { "" },
283 if edge.animated { " df-animated" } else { "" },
284 edge.class
285 .as_deref()
286 .map(|c| format!(" {c}"))
287 .unwrap_or_default(),
288 );
289
290 let id = edge.id.clone();
291 let selectable = edge.selectable;
292 let hit = hit_paths.then(|| {
295 let d = geometry.outline.clone();
296 rsx! {
297 path {
298 class: "df-edge-hit",
299 d,
300 onpointerdown: move |evt| {
301 edge_pointer_down(core, &id, selectable, &on_edge_click, evt)
302 },
303 }
304 }
305 });
306
307 if let Some(view) = edge_view {
308 let custom = view.call(SeatEdgeViewCtx {
309 edge: edge.clone(),
310 anchors,
311 geometry,
312 });
313 return rsx! {
314 g { class,
315 {custom}
316 {hit}
317 }
318 };
319 }
320
321 rsx! {
322 g { class,
323 path {
324 class: "df-edge-path",
325 d: "{geometry.path}",
326 style: edge.style.as_deref().unwrap_or_default(),
327 }
328 if let Some(arrow) = geometry.start_arrow.as_deref() {
329 path { class: "df-edge-arrow", d: "{arrow}" }
330 }
331 if let Some(arrow) = geometry.end_arrow.as_deref() {
332 path { class: "df-edge-arrow", d: "{arrow}" }
333 }
334 {hit}
335 }
336 }
337}
338
339fn edge_pointer_down(
340 core: FlowCore,
341 id: &Id,
342 selectable: bool,
343 on_edge_click: &Option<EventHandler<Id>>,
344 evt: Event<PointerData>,
345) {
346 if *core.interaction.peek() != Interaction::None {
347 return;
348 }
349 core.cancel_animations();
350 core.interaction.clone().set(Interaction::Pressed);
353 if selectable {
354 let shift = evt.modifiers().shift();
355 let mut edges = core.edges;
356 edges.with_mut(|edges| {
357 for edge in edges.iter_mut() {
358 if &edge.id == id {
359 edge.selected = if shift { !edge.selected } else { true };
360 } else if !shift {
361 edge.selected = false;
362 }
363 }
364 });
365 if !shift {
366 core.deselect_nodes.call(());
367 }
368 }
369 if let Some(handler) = on_edge_click {
370 handler.call(id.clone());
371 }
372}
373
374#[component]
377pub(crate) fn EdgeMarkers(iid: usize) -> Element {
378 rsx! {
379 marker {
380 id: "df-arrowclosed-{iid}",
381 "markerWidth": "12",
382 "markerHeight": "12",
383 "viewBox": "-10 -10 20 20",
384 "refX": "0",
385 "refY": "0",
386 "markerUnits": "strokeWidth",
387 orient: "auto-start-reverse",
388 path {
389 d: "M-7,-4.5 L0,0 L-7,4.5 Z",
390 fill: "context-stroke",
391 stroke: "none",
392 }
393 }
394 marker {
395 id: "df-arrow-{iid}",
396 "markerWidth": "12",
397 "markerHeight": "12",
398 "viewBox": "-10 -10 20 20",
399 "refX": "0",
400 "refY": "0",
401 "markerUnits": "strokeWidth",
402 orient: "auto-start-reverse",
403 path {
404 d: "M-7,-4.5 L0,0 L-7,4.5",
405 fill: "none",
406 stroke: "context-stroke",
407 stroke_width: "1.5",
408 stroke_linecap: "round",
409 }
410 }
411 }
412}