1mod paint;
2mod visual;
3
4use std::{cell::Cell, collections::HashMap, rc::Rc};
5
6use std::time::{Duration, Instant};
7
8use gpui::{
9 AnyElement, App, Bounds, ContentMask, Context, CursorStyle, DispatchPhase, Entity,
10 EventEmitter, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement as _,
11 IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
12 Pixels, Point, Render, ScrollWheelEvent, SharedString, Styled as _, Window, canvas, div, point,
13 px,
14};
15
16use crate::actions::{
17 Activate, ClearSelection, FitView, KEY_CONTEXT, OpenContextMenu, SelectDown, SelectLeft,
18 SelectRight, SelectUp, ZoomIn, ZoomOut,
19};
20
21use crate::geometry::EdgeGeometry;
22use crate::layout::{Body, ForceConfig, Simulation, Vec2};
23use crate::model::{Edge, GraphData, Node};
24use crate::style::{ColorBy, GraphColors, GraphMetrics, LabelMode, NodeSizing, Palette};
25use visual::{GroupInput, Vertex, Visual};
26
27const MIN_ZOOM: f32 = 0.05;
28const MAX_ZOOM: f32 = 6.;
29const DRAG_THRESHOLD: f32 = 3.;
31const EDGE_HIT_SLOP: f32 = 4.;
33const REDUCED_MOTION_BUDGET: Duration = Duration::from_millis(120);
36
37#[derive(Clone, Debug, Default)]
39#[non_exhaustive]
40pub enum Selection {
41 #[default]
42 None,
43 Node(Node),
44 Edge(Edge),
45 EdgeBundle(Vec<Edge>),
47 Group(GroupInfo),
49}
50
51#[derive(Clone, Debug)]
52#[non_exhaustive]
53pub enum NetworkGraphEvent {
54 SelectionChanged(Selection),
55 NodeActivated(Node),
57 GroupToggled { group: SharedString, collapsed: bool },
59 ContextMenu { target: Selection, position: Point<Pixels> },
61}
62
63#[derive(Clone, Debug)]
65#[non_exhaustive]
66pub struct CategoryInfo {
67 pub name: SharedString,
68 pub color: Hsla,
69 pub count: usize,
70}
71
72#[derive(Clone, Debug)]
74#[non_exhaustive]
75pub struct GroupInfo {
76 pub id: SharedString,
77 pub label: SharedString,
78 pub color: Hsla,
79 pub size: usize,
81 pub collapsed: bool,
82}
83
84pub type TooltipBuilder =
86 Rc<dyn Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>>;
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90enum Item {
91 Vertex(usize),
92 Edge(usize),
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97enum SelectionKey {
98 Node(SharedString),
99 Group(SharedString),
100 Edges(Vec<usize>),
102}
103
104struct ModelEdge {
105 data: Edge,
106 source: usize,
107 target: usize,
108}
109
110struct GroupState {
111 id: SharedString,
112 label: SharedString,
113 members: Vec<usize>,
114 collapsed: bool,
115 body: Body,
117 anchor: Vec2,
119}
120
121enum DragKind {
122 Pan { start_pan: Point<Pixels> },
123 Vertex { ix: usize, was_pinned: bool },
124}
125
126struct Drag {
127 kind: DragKind,
128 start: Point<Pixels>,
129 moved: bool,
130}
131
132pub struct NetworkGraph {
145 nodes: Vec<Node>,
147 node_bodies: Vec<Body>,
148 edges: Vec<ModelEdge>,
149 index: HashMap<SharedString, usize>,
150 degrees: Vec<usize>,
151 neighbors: Vec<Vec<usize>>,
152 categories: Vec<SharedString>,
153 groups: Vec<GroupState>,
154 group_index: HashMap<SharedString, usize>,
155 node_group: Vec<Option<usize>>,
156
157 vis: Visual,
159 sim: Simulation,
160
161 pan: Point<Pixels>,
163 zoom: f32,
164 auto_fit: bool,
166 bounds: Rc<Cell<Bounds<Pixels>>>,
167
168 hovered: Option<Item>,
169 selected: Option<SelectionKey>,
170 drag: Option<Drag>,
171 pointer: Option<Point<Pixels>>,
172 focus_handle: FocusHandle,
173 keyboard_nav: bool,
175
176 label_mode: LabelMode,
177 color_by: ColorBy,
178 arrows: bool,
179 edge_labels: bool,
180 group_hulls: bool,
181 sticky_drag: bool,
183 palette: Palette,
184 sizing: NodeSizing,
185 colors: Option<GraphColors>,
186 tooltip: Option<TooltipBuilder>,
187 tooltips: bool,
188}
189
190impl EventEmitter<NetworkGraphEvent> for NetworkGraph {}
191
192impl Focusable for NetworkGraph {
193 fn focus_handle(&self, _: &App) -> FocusHandle {
194 self.focus_handle.clone()
195 }
196}
197
198impl NetworkGraph {
199 pub fn new(cx: &mut Context<Self>) -> Self {
200 Self {
201 nodes: Vec::new(),
202 node_bodies: Vec::new(),
203 edges: Vec::new(),
204 index: HashMap::new(),
205 degrees: Vec::new(),
206 neighbors: Vec::new(),
207 categories: Vec::new(),
208 groups: Vec::new(),
209 group_index: HashMap::new(),
210 node_group: Vec::new(),
211 vis: Visual::default(),
212 sim: Simulation::new(ForceConfig::default()),
213 pan: Point::default(),
214 zoom: 1.,
215 auto_fit: true,
216 bounds: Rc::new(Cell::new(Bounds::default())),
217 hovered: None,
218 selected: None,
219 drag: None,
220 pointer: None,
221 focus_handle: cx.focus_handle().tab_stop(true),
222 keyboard_nav: false,
223 label_mode: LabelMode::default(),
224 color_by: ColorBy::default(),
225 arrows: true,
226 edge_labels: true,
227 group_hulls: true,
228 sticky_drag: false,
229 palette: Palette::default(),
230 sizing: NodeSizing::default(),
231 colors: None,
232 tooltip: None,
233 tooltips: true,
234 }
235 }
236
237 pub fn with_data(mut self, data: GraphData) -> Self {
240 self.load(data);
241 self
242 }
243
244 pub fn with_force_config(mut self, config: ForceConfig) -> Self {
245 self.sim.config = config;
246 self
247 }
248
249 pub fn with_label_mode(mut self, mode: LabelMode) -> Self {
250 self.label_mode = mode;
251 self
252 }
253
254 pub fn with_color_by(mut self, color_by: ColorBy) -> Self {
255 self.color_by = color_by;
256 self
257 }
258
259 pub fn arrows(mut self, arrows: bool) -> Self {
261 self.arrows = arrows;
262 self
263 }
264
265 pub fn edge_labels(mut self, edge_labels: bool) -> Self {
267 self.edge_labels = edge_labels;
268 self
269 }
270
271 pub fn group_hulls(mut self, group_hulls: bool) -> Self {
273 self.group_hulls = group_hulls;
274 self
275 }
276
277 pub fn sticky_drag(mut self, sticky: bool) -> Self {
279 self.sticky_drag = sticky;
280 self
281 }
282
283 pub fn with_palette(mut self, palette: Palette) -> Self {
284 self.palette = palette;
285 self
286 }
287
288 pub fn with_node_sizing(mut self, sizing: NodeSizing) -> Self {
289 self.sizing = sizing;
290 self
291 }
292
293 pub fn with_colors(mut self, colors: GraphColors) -> Self {
295 self.colors = Some(colors);
296 self
297 }
298
299 pub fn with_tooltip(
301 mut self,
302 builder: impl Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement> + 'static,
303 ) -> Self {
304 self.tooltip = Some(Rc::new(builder));
305 self
306 }
307
308 pub fn tooltips(mut self, tooltips: bool) -> Self {
310 self.tooltips = tooltips;
311 self
312 }
313
314 pub fn label_mode(&self) -> LabelMode {
317 self.label_mode
318 }
319
320 pub fn set_label_mode(&mut self, mode: LabelMode, cx: &mut Context<Self>) {
321 self.label_mode = mode;
322 cx.notify();
323 }
324
325 pub fn color_by(&self) -> ColorBy {
326 self.color_by
327 }
328
329 pub fn set_color_by(&mut self, color_by: ColorBy, cx: &mut Context<Self>) {
330 self.color_by = color_by;
331 cx.notify();
332 }
333
334 pub fn has_arrows(&self) -> bool {
335 self.arrows
336 }
337
338 pub fn set_arrows(&mut self, arrows: bool, cx: &mut Context<Self>) {
339 self.arrows = arrows;
340 cx.notify();
341 }
342
343 pub fn has_edge_labels(&self) -> bool {
344 self.edge_labels
345 }
346
347 pub fn set_edge_labels(&mut self, edge_labels: bool, cx: &mut Context<Self>) {
348 self.edge_labels = edge_labels;
349 cx.notify();
350 }
351
352 pub fn has_group_hulls(&self) -> bool {
353 self.group_hulls
354 }
355
356 pub fn set_group_hulls(&mut self, group_hulls: bool, cx: &mut Context<Self>) {
357 self.group_hulls = group_hulls;
358 cx.notify();
359 }
360
361 pub fn is_sticky_drag(&self) -> bool {
362 self.sticky_drag
363 }
364
365 pub fn set_sticky_drag(&mut self, sticky: bool) {
366 self.sticky_drag = sticky;
367 }
368
369 pub fn has_tooltips(&self) -> bool {
370 self.tooltips
371 }
372
373 pub fn set_tooltips(&mut self, tooltips: bool, cx: &mut Context<Self>) {
374 self.tooltips = tooltips;
375 cx.notify();
376 }
377
378 pub fn set_force_config(&mut self, config: ForceConfig, cx: &mut Context<Self>) {
379 self.sim.config = config;
380 self.sim.reheat(0.5);
381 cx.notify();
382 }
383
384 pub fn set_colors(&mut self, colors: Option<GraphColors>, cx: &mut Context<Self>) {
385 self.colors = colors;
386 cx.notify();
387 }
388
389 pub fn set_data(&mut self, data: GraphData, cx: &mut Context<Self>) {
393 let had_selection = self.selected.is_some();
394 self.load(data);
395 if had_selection {
396 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
397 }
398 cx.notify();
399 }
400
401 pub fn merge(&mut self, data: GraphData, anchor: Option<&str>, cx: &mut Context<Self>) {
405 self.sync_back();
406 self.insert(data, anchor);
407 cx.notify();
408 }
409
410 pub fn remove_nodes(&mut self, ids: &[&str], cx: &mut Context<Self>) {
412 let doomed: Vec<bool> = self.nodes.iter().map(|n| ids.contains(&n.id.as_ref())).collect();
413 if !doomed.contains(&true) {
414 return;
415 }
416 self.sync_back();
417 let mut remap = vec![usize::MAX; self.nodes.len()];
418 let mut kept = 0;
419 for (i, &d) in doomed.iter().enumerate() {
420 if !d {
421 remap[i] = kept;
422 kept += 1;
423 }
424 }
425 let mut i = 0;
426 self.nodes.retain(|_| (!doomed[i], i += 1).0);
427 let mut i = 0;
428 self.node_bodies.retain(|_| (!doomed[i], i += 1).0);
429 self.edges.retain_mut(|e| {
430 let keep = !doomed[e.source] && !doomed[e.target];
431 e.source = remap[e.source];
432 e.target = remap[e.target];
433 keep
434 });
435 self.index = self.nodes.iter().enumerate().map(|(i, n)| (n.id.clone(), i)).collect();
436 self.categories.retain(|c| self.nodes.iter().any(|n| &n.category == c));
437 if matches!(self.selected, Some(SelectionKey::Edges(_))) {
439 self.selected = None;
440 }
441 self.rebuild();
442 self.sim.reheat(0.3);
443 self.validate_selection(cx);
444 cx.notify();
445 }
446
447 pub fn clear(&mut self, cx: &mut Context<Self>) {
448 self.set_data(GraphData::default(), cx);
449 }
450
451 pub fn data(&self) -> GraphData {
453 GraphData {
454 nodes: self.nodes.clone(),
455 edges: self.edges.iter().map(|e| e.data.clone()).collect(),
456 }
457 }
458
459 fn load(&mut self, data: GraphData) {
460 self.nodes.clear();
461 self.node_bodies.clear();
462 self.edges.clear();
463 self.index.clear();
464 self.categories.clear();
465 self.groups.clear();
466 self.group_index.clear();
467 self.vis = Visual::default();
468 self.hovered = None;
469 self.selected = None;
470 self.drag = None;
471 self.insert(data, None);
472 self.pan = Point::default();
473 self.zoom = 1.;
474 self.auto_fit = true;
475 }
476
477 fn insert(&mut self, data: GraphData, anchor: Option<&str>) {
479 let anchor_pos = anchor
480 .and_then(|id| self.index.get(id))
481 .map(|&i| self.vis.bodies[self.vis.node_slot[i]].pos);
482 let first_new = self.nodes.len();
483 for node in data.nodes {
484 if self.index.contains_key(&node.id) {
485 continue;
486 }
487 if !self.categories.contains(&node.category) {
488 self.categories.push(node.category.clone());
489 }
490 self.index.insert(node.id.clone(), self.nodes.len());
491 self.nodes.push(node);
492 self.node_bodies.push(Body::default());
493 }
494 match anchor_pos {
495 Some(center) => {
496 let added = self.nodes.len() - first_new;
497 for (k, body) in self.node_bodies[first_new..].iter_mut().enumerate() {
498 let a = k as f32 / added.max(1) as f32 * std::f32::consts::TAU;
499 body.pos = Vec2::new(center.x + 30. * a.cos(), center.y + 30. * a.sin());
500 }
501 }
502 None => Simulation::initialize_positions(&mut self.node_bodies, first_new),
503 }
504
505 for edge in data.edges {
506 let (Some(&s), Some(&t)) = (self.index.get(&edge.source), self.index.get(&edge.target)) else {
507 continue;
508 };
509 let duplicate = self.edges.iter().any(|e| e.source == s && e.target == t && e.data.label == edge.label);
510 if !duplicate {
511 self.edges.push(ModelEdge { data: edge, source: s, target: t });
512 }
513 }
514 self.rebuild();
515 self.sim.reheat(if anchor_pos.is_some() { 0.5 } else { 1. });
516 }
517
518 fn sync_back(&mut self) {
520 for (v, vertex) in self.vis.vertices.iter().enumerate() {
521 let body = self.vis.bodies[v].clone();
522 match *vertex {
523 Vertex::Node(i) => self.node_bodies[i] = body,
524 Vertex::Group(g) => self.groups[g].body = body,
525 }
526 }
527 }
528
529 fn rebuild(&mut self) {
531 let n = self.nodes.len();
532 self.degrees = vec![0; n];
533 self.neighbors = vec![Vec::new(); n];
534 for e in &self.edges {
535 self.degrees[e.source] += 1;
536 self.degrees[e.target] += 1;
537 if e.source != e.target {
538 self.neighbors[e.source].push(e.target);
539 self.neighbors[e.target].push(e.source);
540 }
541 }
542 for list in &mut self.neighbors {
543 list.sort_unstable();
544 list.dedup();
545 }
546 for (i, body) in self.node_bodies.iter_mut().enumerate() {
547 body.radius = self.nodes[i].size.unwrap_or_else(|| self.sizing.radius(self.degrees[i]));
548 }
549
550 for g in &mut self.groups {
552 g.members.clear();
553 }
554 self.node_group = vec![None; n];
555 for (i, node) in self.nodes.iter().enumerate() {
556 let Some(id) = &node.group else { continue };
557 let g = *self.group_index.entry(id.clone()).or_insert_with(|| {
558 self.groups.push(GroupState {
559 id: id.clone(),
560 label: id.clone(),
561 members: Vec::new(),
562 collapsed: false,
563 body: Body::default(),
564 anchor: Vec2::default(),
565 });
566 self.groups.len() - 1
567 });
568 self.groups[g].members.push(i);
569 self.node_group[i] = Some(g);
570 }
571 if self.groups.iter().any(|g| g.members.is_empty()) {
572 self.groups.retain(|g| !g.members.is_empty());
573 self.group_index = self.groups.iter().enumerate().map(|(g, s)| (s.id.clone(), g)).collect();
574 for (i, node) in self.nodes.iter().enumerate() {
575 self.node_group[i] = node.group.as_ref().map(|id| self.group_index[id]);
576 }
577 }
578 for g in &mut self.groups {
579 g.body.radius = (12. + (g.members.len() as f32).sqrt() * 3.).min(48.);
580 }
581
582 let groups: Vec<GroupInput> = self
583 .groups
584 .iter()
585 .map(|g| GroupInput { collapsed: g.collapsed, body: &g.body })
586 .collect();
587 let links: Vec<(usize, usize)> = self.edges.iter().map(|e| (e.source, e.target)).collect();
588 self.vis = Visual::build(&self.node_bodies, &self.node_group, &groups, &links);
589 self.hovered = None;
590 self.drag = None;
591 }
592
593 pub fn node_count(&self) -> usize {
596 self.nodes.len()
597 }
598
599 pub fn edge_count(&self) -> usize {
600 self.edges.len()
601 }
602
603 pub fn node(&self, id: &str) -> Option<&Node> {
604 self.index.get(id).map(|&i| &self.nodes[i])
605 }
606
607 pub fn degree(&self, id: &str) -> usize {
608 self.index.get(id).map_or(0, |&i| self.degrees[i])
609 }
610
611 pub fn neighbors_of(&self, id: &str) -> Vec<Node> {
612 self.index
613 .get(id)
614 .map(|&i| self.neighbors[i].iter().map(|&j| self.nodes[j].clone()).collect())
615 .unwrap_or_default()
616 }
617
618 pub fn edges_of(&self, id: &str) -> Vec<Edge> {
620 let Some(&i) = self.index.get(id) else { return Vec::new() };
621 self.edges
622 .iter()
623 .filter(|e| e.source == i || e.target == i)
624 .map(|e| e.data.clone())
625 .collect()
626 }
627
628 pub fn categories(&self) -> Vec<CategoryInfo> {
629 self.categories
630 .iter()
631 .enumerate()
632 .map(|(i, c)| CategoryInfo {
633 name: c.clone(),
634 color: self.palette.get(i),
635 count: self.nodes.iter().filter(|n| &n.category == c).count(),
636 })
637 .collect()
638 }
639
640 pub fn groups(&self) -> Vec<GroupInfo> {
641 (0..self.groups.len()).map(|g| self.group_info(g)).collect()
642 }
643
644 pub fn group(&self, id: &str) -> Option<GroupInfo> {
645 self.group_index.get(id).map(|&g| self.group_info(g))
646 }
647
648 pub fn group_members(&self, id: &str) -> Vec<Node> {
650 self.group_index
651 .get(id)
652 .map(|&g| self.groups[g].members.iter().map(|&i| self.nodes[i].clone()).collect())
653 .unwrap_or_default()
654 }
655
656 pub fn resolved_colors(&self, cx: &App) -> GraphColors {
658 self.colors.clone().unwrap_or_else(|| GraphColors::from_theme(cx))
659 }
660
661 pub fn color_of(&self, node: &Node, cx: &App) -> Hsla {
663 if let Some(c) = node.color {
664 return c;
665 }
666 match self.color_by {
667 ColorBy::Category => {
668 let ix = self.categories.iter().position(|c| c == &node.category).unwrap_or(0);
669 self.palette.get(ix)
670 }
671 ColorBy::Group => match node.group.as_ref().and_then(|g| self.group_index.get(g)) {
672 Some(&g) => self.group_color(g),
673 None => self.resolved_colors(cx).ungrouped,
674 },
675 }
676 }
677
678 pub fn selection(&self) -> Selection {
679 match &self.selected {
680 None => Selection::None,
681 Some(SelectionKey::Node(id)) => self.node(id).cloned().map_or(Selection::None, Selection::Node),
682 Some(SelectionKey::Group(id)) => self.group(id).map_or(Selection::None, Selection::Group),
683 Some(SelectionKey::Edges(edges)) => self.edge_selection(edges),
684 }
685 }
686
687 pub fn zoom(&self) -> f32 {
688 self.zoom
689 }
690
691 pub fn is_simulating(&self) -> bool {
693 self.sim.is_active()
694 }
695
696 fn group_info(&self, g: usize) -> GroupInfo {
697 let s = &self.groups[g];
698 GroupInfo {
699 id: s.id.clone(),
700 label: s.label.clone(),
701 color: self.group_color(g),
702 size: s.members.len(),
703 collapsed: s.collapsed,
704 }
705 }
706
707 fn group_color(&self, g: usize) -> Hsla {
708 self.palette.get(g)
709 }
710
711 fn edge_selection(&self, edges: &[usize]) -> Selection {
712 match edges {
713 [] => Selection::None,
714 [e] => Selection::Edge(self.edges[*e].data.clone()),
715 many => Selection::EdgeBundle(many.iter().map(|&e| self.edges[e].data.clone()).collect()),
716 }
717 }
718
719 fn key_of(&self, item: Item) -> SelectionKey {
720 match item {
721 Item::Vertex(v) => match self.vis.vertices[v] {
722 Vertex::Node(i) => SelectionKey::Node(self.nodes[i].id.clone()),
723 Vertex::Group(g) => SelectionKey::Group(self.groups[g].id.clone()),
724 },
725 Item::Edge(e) => SelectionKey::Edges(self.vis.edges[e].edges.clone()),
726 }
727 }
728
729 fn item_selection(&self, item: Item) -> Selection {
730 match item {
731 Item::Vertex(v) => match self.vis.vertices[v] {
732 Vertex::Node(i) => Selection::Node(self.nodes[i].clone()),
733 Vertex::Group(g) => Selection::Group(self.group_info(g)),
734 },
735 Item::Edge(e) => self.edge_selection(&self.vis.edges[e].edges),
736 }
737 }
738
739 fn selected_item(&self) -> Option<Item> {
741 match self.selected.as_ref()? {
742 SelectionKey::Node(id) => {
743 let slot = self.vis.node_slot[*self.index.get(id)?];
744 matches!(self.vis.vertices[slot], Vertex::Node(_)).then_some(Item::Vertex(slot))
745 }
746 SelectionKey::Group(id) => self.vis.group_slot[*self.group_index.get(id)?].map(Item::Vertex),
747 SelectionKey::Edges(edges) => {
748 self.vis.edges.iter().position(|e| &e.edges == edges).map(Item::Edge)
749 }
750 }
751 }
752
753 fn validate_selection(&mut self, cx: &mut Context<Self>) {
755 if self.selected.is_some() && matches!(self.selection(), Selection::None) {
756 self.selected = None;
757 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
758 }
759 }
760
761 pub fn select_node(&mut self, id: Option<&str>, center: bool, cx: &mut Context<Self>) {
765 let key = id.filter(|id| self.index.contains_key(*id)).map(|id| SelectionKey::Node(id.to_string().into()));
766 if let (Some(id), true) = (id, center) {
767 if let Some(&i) = self.index.get(id) {
768 let p = self.vis.bodies[self.vis.node_slot[i]].pos;
769 self.auto_fit = false;
770 self.pan = point(px(-p.x * self.zoom), px(-p.y * self.zoom));
771 cx.notify();
772 }
773 }
774 self.set_selected(key, cx);
775 }
776
777 fn set_selected(&mut self, key: Option<SelectionKey>, cx: &mut Context<Self>) {
778 if key == self.selected {
779 return;
780 }
781 self.selected = key;
782 cx.emit(NetworkGraphEvent::SelectionChanged(self.selection()));
783 cx.notify();
784 }
785
786 pub fn set_pinned(&mut self, id: &str, pinned: bool, cx: &mut Context<Self>) {
788 let Some(&i) = self.index.get(id) else { return };
789 let v = self.vis.node_slot[i];
790 if matches!(self.vis.vertices[v], Vertex::Node(_)) {
791 let body = &mut self.vis.bodies[v];
792 body.fixed = pinned.then_some(body.pos);
793 } else {
794 let body = &mut self.node_bodies[i];
795 body.fixed = pinned.then_some(body.pos);
796 }
797 self.sim.reheat(0.1);
798 cx.notify();
799 }
800
801 pub fn is_pinned(&self, id: &str) -> bool {
802 self.index.get(id).is_some_and(|&i| {
803 let v = self.vis.node_slot[i];
804 match self.vis.vertices[v] {
805 Vertex::Node(_) => self.vis.bodies[v].fixed.is_some(),
806 Vertex::Group(_) => self.node_bodies[i].fixed.is_some(),
807 }
808 })
809 }
810
811 pub fn unpin_all(&mut self, cx: &mut Context<Self>) {
812 for b in self.vis.bodies.iter_mut().chain(&mut self.node_bodies) {
813 b.fixed = None;
814 }
815 self.sim.reheat(0.3);
816 cx.notify();
817 }
818
819 pub fn collapse_group(&mut self, id: &str, cx: &mut Context<Self>) {
823 self.set_groups_collapsed(|g| g.id.as_ref() == id, true, cx);
824 }
825
826 pub fn expand_group(&mut self, id: &str, cx: &mut Context<Self>) {
827 self.set_groups_collapsed(|g| g.id.as_ref() == id, false, cx);
828 }
829
830 pub fn toggle_group(&mut self, id: &str, cx: &mut Context<Self>) {
831 if let Some(&g) = self.group_index.get(id) {
832 let collapse = !self.groups[g].collapsed;
833 self.set_groups_collapsed(|s| s.id.as_ref() == id, collapse, cx);
834 }
835 }
836
837 pub fn collapse_all_groups(&mut self, cx: &mut Context<Self>) {
839 self.set_groups_collapsed(|_| true, true, cx);
840 }
841
842 pub fn expand_all_groups(&mut self, cx: &mut Context<Self>) {
843 self.set_groups_collapsed(|_| true, false, cx);
844 }
845
846 pub fn set_group_label(&mut self, id: &str, label: impl Into<SharedString>, cx: &mut Context<Self>) {
847 if let Some(&g) = self.group_index.get(id) {
848 self.groups[g].label = label.into();
849 cx.notify();
850 }
851 }
852
853 fn set_groups_collapsed(
854 &mut self,
855 filter: impl Fn(&GroupState) -> bool,
856 collapse: bool,
857 cx: &mut Context<Self>,
858 ) {
859 let targets: Vec<usize> = (0..self.groups.len())
860 .filter(|&g| filter(&self.groups[g]) && self.groups[g].collapsed != collapse)
861 .collect();
862 if targets.is_empty() {
863 return;
864 }
865 self.sync_back();
866 for &g in &targets {
867 let members = self.groups[g].members.clone();
868 if collapse {
869 let n = members.len().max(1) as f32;
870 let (sx, sy) = members.iter().fold((0., 0.), |(x, y), &i| {
871 (x + self.node_bodies[i].pos.x, y + self.node_bodies[i].pos.y)
872 });
873 let centroid = Vec2::new(sx / n, sy / n);
874 let group = &mut self.groups[g];
875 group.anchor = centroid;
876 group.body.pos = centroid;
877 group.body.vel = Vec2::default();
878 group.body.fixed = None;
879 } else {
880 let group = &self.groups[g];
881 let (dx, dy) = (group.body.pos.x - group.anchor.x, group.body.pos.y - group.anchor.y);
882 for i in members {
883 let b = &mut self.node_bodies[i];
884 b.pos = Vec2::new(b.pos.x + dx, b.pos.y + dy);
885 b.vel = Vec2::default();
886 if let Some(f) = b.fixed {
887 b.fixed = Some(Vec2::new(f.x + dx, f.y + dy));
888 }
889 }
890 }
891 self.groups[g].collapsed = collapse;
892 }
893 if let (false, Some(SelectionKey::Group(id))) = (collapse, &self.selected) {
895 if targets.iter().any(|&g| &self.groups[g].id == id) {
896 self.selected = None;
897 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
898 }
899 }
900 self.rebuild();
901 self.sim.reheat(0.4);
902 for g in targets {
903 cx.emit(NetworkGraphEvent::GroupToggled { group: self.groups[g].id.clone(), collapsed: collapse });
904 }
905 cx.notify();
906 }
907
908 pub fn fit_view(&mut self, cx: &mut Context<Self>) {
912 self.auto_fit = true;
913 self.apply_fit(1.);
914 cx.notify();
915 }
916
917 pub fn zoom_by(&mut self, factor: f32, cx: &mut Context<Self>) {
918 self.auto_fit = false;
919 let center = self.bounds.get().center();
920 self.zoom_around(center, factor);
921 cx.notify();
922 }
923
924 pub fn relayout(&mut self, cx: &mut Context<Self>) {
926 for b in &mut self.vis.bodies {
927 b.fixed = None;
928 }
929 Simulation::initialize_positions(&mut self.vis.bodies, 0);
930 self.sim.reheat(1.);
931 self.auto_fit = true;
932 cx.notify();
933 }
934
935 fn apply_fit(&mut self, blend: f32) {
936 let bounds = self.bounds.get();
937 if self.vis.bodies.is_empty() || bounds.size.width <= px(0.) {
938 return;
939 }
940 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
941 for b in &self.vis.bodies {
942 x0 = x0.min(b.pos.x - b.radius);
943 y0 = y0.min(b.pos.y - b.radius);
944 x1 = x1.max(b.pos.x + b.radius);
945 y1 = y1.max(b.pos.y + b.radius);
946 }
947 let padding = 48.;
948 let w = (bounds.size.width.as_f32() - padding * 2.).max(1.);
949 let h = (bounds.size.height.as_f32() - padding * 2.).max(1.);
950 let zoom = (w / (x1 - x0).max(1.)).min(h / (y1 - y0).max(1.)).clamp(MIN_ZOOM, 2.);
951 let (cx, cy) = ((x0 + x1) / 2., (y0 + y1) / 2.);
952 let target = point(px(-cx * zoom), px(-cy * zoom));
953 self.zoom += (zoom - self.zoom) * blend;
954 self.pan = self.pan + (target - self.pan) * blend;
955 }
956
957 fn zoom_around(&mut self, anchor: Point<Pixels>, factor: f32) {
958 let world = self.to_world(anchor);
959 self.zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM);
960 let center = self.bounds.get().center();
961 self.pan = anchor - center - point(px(world.x * self.zoom), px(world.y * self.zoom));
962 }
963
964 fn to_screen(&self, p: Vec2) -> Point<Pixels> {
965 self.bounds.get().center() + self.pan + point(px(p.x * self.zoom), px(p.y * self.zoom))
966 }
967
968 fn to_world(&self, p: Point<Pixels>) -> Vec2 {
969 let local = p - self.bounds.get().center() - self.pan;
970 Vec2::new(local.x.as_f32() / self.zoom, local.y.as_f32() / self.zoom)
971 }
972
973 fn edge_geometry(&self, e: usize) -> EdgeGeometry {
974 let e = &self.vis.edges[e];
975 let a = self.to_screen(self.vis.bodies[e.source].pos);
976 let b = self.to_screen(self.vis.bodies[e.target].pos);
977 EdgeGeometry::new(a, b, e.curvature * 28. * self.zoom.min(1.5))
978 }
979
980 fn hit_test(&self, p: Point<Pixels>) -> Option<Item> {
981 let w = self.to_world(p);
982 let slop = 3. / self.zoom;
983 let vertex = self.vis.bodies.iter().enumerate().rev().find_map(|(i, b)| {
985 let (dx, dy) = (b.pos.x - w.x, b.pos.y - w.y);
986 let r = b.radius + slop;
987 (dx * dx + dy * dy <= r * r).then_some(i)
988 });
989 if let Some(v) = vertex {
990 return Some(Item::Vertex(v));
991 }
992 let mut best = (EDGE_HIT_SLOP, None);
993 for (i, e) in self.vis.edges.iter().enumerate() {
994 let d = if e.source == e.target {
995 let c = self.to_screen(self.vis.bodies[e.source].pos);
996 let s = (self.vis.bodies[e.source].radius * self.zoom).max(6.);
997 let center = point(c.x + px(s * 1.3), c.y - px(s * 1.3));
998 let r = (p.x - center.x).as_f32().hypot((p.y - center.y).as_f32());
999 (r - s * 0.9).abs()
1000 } else {
1001 self.edge_geometry(i).distance_to(p)
1002 };
1003 if d < best.0 {
1004 best = (d, Some(i));
1005 }
1006 }
1007 best.1.map(Item::Edge)
1008 }
1009
1010 fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
1013 self.keyboard_nav = true;
1014 self.zoom_by(1.25, cx);
1015 }
1016
1017 fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
1018 self.keyboard_nav = true;
1019 self.zoom_by(0.8, cx);
1020 }
1021
1022 fn on_fit_view(&mut self, _: &FitView, _: &mut Window, cx: &mut Context<Self>) {
1023 self.keyboard_nav = true;
1024 self.fit_view(cx);
1025 }
1026
1027 fn on_clear_selection(&mut self, _: &ClearSelection, _: &mut Window, cx: &mut Context<Self>) {
1028 self.keyboard_nav = true;
1029 if self.selected.is_some() {
1030 self.set_selected(None, cx);
1031 } else {
1032 cx.propagate();
1033 }
1034 }
1035
1036 fn on_activate(&mut self, _: &Activate, _: &mut Window, cx: &mut Context<Self>) {
1037 self.keyboard_nav = true;
1038 match self.selected.clone() {
1039 Some(SelectionKey::Node(id)) => {
1040 if let Some(node) = self.node(&id).cloned() {
1041 cx.emit(NetworkGraphEvent::NodeActivated(node));
1042 }
1043 }
1044 Some(SelectionKey::Group(id)) => self.toggle_group(&id, cx),
1045 _ => cx.propagate(),
1046 }
1047 }
1048
1049 fn on_open_context_menu(&mut self, _: &OpenContextMenu, _: &mut Window, cx: &mut Context<Self>) {
1050 self.keyboard_nav = true;
1051 let position = match self.selected_item() {
1052 Some(Item::Vertex(v)) => self.to_screen(self.vis.bodies[v].pos),
1053 Some(Item::Edge(e)) => self.edge_geometry(e).midpoint(),
1054 None => self.bounds.get().center(),
1055 };
1056 cx.emit(NetworkGraphEvent::ContextMenu { target: self.selection(), position });
1057 }
1058
1059 fn select_toward(&mut self, dx: f32, dy: f32, cx: &mut Context<Self>) {
1062 self.keyboard_nav = true;
1063 let bodies = &self.vis.bodies;
1064 if bodies.is_empty() {
1065 return;
1066 }
1067 let from = match self.selected_item() {
1068 Some(Item::Vertex(v)) => Some(v),
1069 Some(Item::Edge(e)) => Some(self.vis.edges[e].source),
1070 None => None,
1071 };
1072 let target = match from {
1073 None => {
1074 let c = self.to_world(self.bounds.get().center());
1075 (0..bodies.len()).min_by(|&a, &b| {
1076 let d = |i: usize| (bodies[i].pos.x - c.x).hypot(bodies[i].pos.y - c.y);
1077 d(a).total_cmp(&d(b))
1078 })
1079 }
1080 Some(f) => {
1081 let p = bodies[f].pos;
1082 (0..bodies.len())
1083 .filter(|&i| i != f)
1084 .filter_map(|i| {
1085 let (x, y) = (bodies[i].pos.x - p.x, bodies[i].pos.y - p.y);
1086 let dist = x.hypot(y).max(1e-3);
1087 let cos = (x * dx + y * dy) / dist;
1088 (cos > 0.5).then(|| (i, dist * (2. - cos)))
1090 })
1091 .min_by(|a, b| a.1.total_cmp(&b.1))
1092 .map(|(i, _)| i)
1093 }
1094 };
1095 let Some(v) = target else { return };
1096 self.reveal(v);
1097 let key = self.key_of(Item::Vertex(v));
1098 self.set_selected(Some(key), cx);
1099 cx.notify();
1100 }
1101
1102 fn reveal(&mut self, v: usize) {
1104 let bounds = self.bounds.get();
1105 let margin = px(48.);
1106 let p = self.to_screen(self.vis.bodies[v].pos);
1107 let inner = Bounds::from_corners(bounds.origin + point(margin, margin), bounds.bottom_right() - point(margin, margin));
1108 if !inner.contains(&p) {
1109 self.auto_fit = false;
1110 let clamped = point(p.x.clamp(inner.left(), inner.right()), p.y.clamp(inner.top(), inner.bottom()));
1111 self.pan = self.pan + (clamped - p);
1112 }
1113 }
1114
1115 fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1118 window.focus(&self.focus_handle, cx);
1119 self.keyboard_nav = false;
1120 let hit = self.hit_test(ev.position);
1121 if ev.button == MouseButton::Right {
1122 if let Some(item) = hit {
1123 self.set_selected(Some(self.key_of(item)), cx);
1124 }
1125 let target = hit.map_or(Selection::None, |item| self.item_selection(item));
1126 cx.emit(NetworkGraphEvent::ContextMenu { target, position: ev.position });
1127 return;
1128 }
1129 if ev.click_count >= 2 {
1130 match hit {
1131 Some(Item::Vertex(v)) => match self.vis.vertices[v] {
1132 Vertex::Node(i) => cx.emit(NetworkGraphEvent::NodeActivated(self.nodes[i].clone())),
1133 Vertex::Group(g) => {
1134 let id = self.groups[g].id.clone();
1135 self.expand_group(&id, cx);
1136 }
1137 },
1138 _ => {}
1139 }
1140 return;
1141 }
1142 let kind = match hit {
1143 Some(Item::Vertex(ix)) => {
1144 DragKind::Vertex { ix, was_pinned: self.vis.bodies[ix].fixed.is_some() }
1145 }
1146 _ => DragKind::Pan { start_pan: self.pan },
1147 };
1148 self.drag = Some(Drag { kind, start: ev.position, moved: false });
1149 }
1150
1151 fn on_mouse_move(&mut self, ev: &MouseMoveEvent, inside: bool, cx: &mut Context<Self>) {
1152 self.pointer = inside.then_some(ev.position);
1153 if let Some(drag) = &mut self.drag {
1154 let delta = ev.position - drag.start;
1155 if !drag.moved {
1156 if delta.x.as_f32().hypot(delta.y.as_f32()) < DRAG_THRESHOLD {
1157 return;
1158 }
1159 drag.moved = true;
1160 }
1161 match drag.kind {
1162 DragKind::Pan { start_pan } => {
1163 self.auto_fit = false;
1164 self.pan = start_pan + delta;
1165 }
1166 DragKind::Vertex { ix, .. } => {
1167 let w = self.to_world(ev.position);
1168 self.vis.bodies[ix].fixed = Some(w);
1169 self.vis.bodies[ix].pos = w;
1170 self.sim.alpha_target = 0.3;
1171 self.sim.reheat(0.3);
1172 self.auto_fit = false;
1173 }
1174 }
1175 cx.notify();
1176 return;
1177 }
1178 let hovered = if inside { self.hit_test(ev.position) } else { None };
1179 if hovered != self.hovered || hovered.is_some() {
1180 self.hovered = hovered;
1182 cx.notify();
1183 }
1184 }
1185
1186 fn on_mouse_up(&mut self, ev: &MouseUpEvent, cx: &mut Context<Self>) {
1187 let Some(drag) = self.drag.take() else { return };
1188 match drag.kind {
1189 DragKind::Vertex { ix, was_pinned } => {
1190 self.sim.alpha_target = 0.;
1191 if !drag.moved {
1192 let key = self.key_of(Item::Vertex(ix));
1193 self.set_selected(Some(key), cx);
1194 } else if !(self.sticky_drag || was_pinned) {
1195 self.vis.bodies[ix].fixed = None;
1196 }
1197 }
1198 DragKind::Pan { .. } => {
1199 if !drag.moved {
1200 let key = self.hit_test(ev.position).map(|item| self.key_of(item));
1201 self.set_selected(key, cx);
1202 }
1203 }
1204 }
1205 cx.notify();
1206 }
1207
1208 fn on_scroll(&mut self, ev: &ScrollWheelEvent, cx: &mut Context<Self>) {
1209 let delta = ev.delta.pixel_delta(px(20.));
1210 let factor = (-delta.y.as_f32() * 0.0025).exp();
1211 self.auto_fit = false;
1212 self.zoom_around(ev.position, factor);
1213 cx.notify();
1214 }
1215}
1216
1217impl Render for NetworkGraph {
1218 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1219 if self.sim.is_active() && !self.vis.bodies.is_empty() {
1220 let reduce_motion = cx.reduce_motion();
1221 let vis = &mut self.vis;
1222 if reduce_motion {
1223 let deadline = Instant::now() + REDUCED_MOTION_BUDGET;
1225 while self.sim.is_active() && self.sim.alpha_target == 0. && Instant::now() < deadline {
1226 self.sim.tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1227 }
1228 if self.sim.alpha_target != 0. {
1229 self.sim.tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1230 }
1231 } else {
1232 let ticks = if vis.bodies.len() > 1500 { 1 } else { 2 };
1234 for _ in 0..ticks {
1235 self.sim.tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1236 }
1237 }
1238 if self.auto_fit {
1239 self.apply_fit(if reduce_motion { 1. } else { 0.25 });
1240 }
1241 if self.sim.is_active() {
1242 window.request_animation_frame();
1243 }
1244 }
1245
1246 let colors = self.resolved_colors(cx);
1247 let metrics = GraphMetrics::from_theme(cx);
1248 let tooltip = self.tooltip_element(&colors, &metrics, window, cx);
1249 let entity = cx.entity();
1250 let bounds_cell = self.bounds.clone();
1251 let background = colors.background;
1252 let show_focus = self.keyboard_nav && self.focus_handle.is_focused(window);
1253
1254 div()
1255 .id("network-graph")
1256 .track_focus(&self.focus_handle)
1257 .key_context(KEY_CONTEXT)
1258 .on_action(cx.listener(Self::on_zoom_in))
1259 .on_action(cx.listener(Self::on_zoom_out))
1260 .on_action(cx.listener(Self::on_fit_view))
1261 .on_action(cx.listener(Self::on_clear_selection))
1262 .on_action(cx.listener(Self::on_activate))
1263 .on_action(cx.listener(Self::on_open_context_menu))
1264 .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.select_toward(-1., 0., cx)))
1265 .on_action(cx.listener(|this, _: &SelectRight, _, cx| this.select_toward(1., 0., cx)))
1266 .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.select_toward(0., -1., cx)))
1267 .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.select_toward(0., 1., cx)))
1268 .size_full()
1269 .relative()
1270 .overflow_hidden()
1271 .bg(background)
1272 .child(
1273 canvas(
1274 move |bounds, window, _| {
1275 bounds_cell.set(bounds);
1276 window.insert_hitbox(bounds, HitboxBehavior::Normal)
1277 },
1278 move |bounds, hitbox: Hitbox, window, cx| {
1279 register_input(&entity, &hitbox, window, cx);
1280 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1281 entity.update(cx, |this, cx| {
1282 this.paint_graph(bounds, &colors, &metrics, show_focus, window, cx)
1283 });
1284 });
1285 },
1286 )
1287 .size_full(),
1288 )
1289 .children(tooltip)
1290 }
1291}
1292
1293fn register_input(entity: &Entity<NetworkGraph>, hitbox: &Hitbox, window: &mut Window, cx: &mut App) {
1296 let graph = entity.read(cx);
1297 let cursor = match &graph.drag {
1299 Some(d) if d.moved => CursorStyle::ClosedHand,
1300 _ => CursorStyle::Arrow,
1301 };
1302 window.set_cursor_style(cursor, hitbox);
1303
1304 let (e, h) = (entity.clone(), hitbox.clone());
1305 window.on_mouse_event(move |ev: &MouseDownEvent, phase, window, cx| {
1306 let button_ok = matches!(ev.button, MouseButton::Left | MouseButton::Right);
1307 if phase == DispatchPhase::Bubble && button_ok && h.is_hovered(window) {
1308 e.update(cx, |this, cx| this.on_mouse_down(ev, window, cx));
1309 }
1310 });
1311 let (e, h) = (entity.clone(), hitbox.clone());
1312 window.on_mouse_event(move |ev: &MouseMoveEvent, phase, window, cx| {
1313 if phase != DispatchPhase::Bubble {
1314 return;
1315 }
1316 let inside = h.is_hovered(window);
1317 let graph = e.read(cx);
1318 if inside || graph.drag.is_some() || graph.hovered.is_some() {
1319 e.update(cx, |this, cx| this.on_mouse_move(ev, inside, cx));
1320 }
1321 });
1322 let e = entity.clone();
1323 window.on_mouse_event(move |ev: &MouseUpEvent, phase, _, cx| {
1324 if phase == DispatchPhase::Bubble && ev.button == MouseButton::Left {
1325 e.update(cx, |this, cx| this.on_mouse_up(ev, cx));
1326 }
1327 });
1328 let (e, h) = (entity.clone(), hitbox.clone());
1329 window.on_mouse_event(move |ev: &ScrollWheelEvent, phase, window, cx| {
1330 if phase == DispatchPhase::Bubble && h.should_handle_scroll(window) {
1331 e.update(cx, |this, cx| this.on_scroll(ev, cx));
1332 }
1333 });
1334}