1mod paint;
2mod visual;
3
4use std::{
5 cell::{Cell, RefCell},
6 collections::{HashMap, HashSet},
7 rc::Rc,
8};
9
10use std::time::{Duration, Instant};
11
12use gpui::{
13 AnyElement, App, Bounds, ContentMask, Context, CursorStyle, DispatchPhase, Entity,
14 EventEmitter, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement as _,
15 IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
16 PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollEvent, SharedString, Size, Styled as _,
17 Window, canvas, div, point, px,
18};
19
20use crate::actions::{
21 Activate, ClearSelection, FitView, KEY_CONTEXT, OpenContextMenu, SelectDown, SelectLeft,
22 SelectRight, SelectUp, ZoomIn, ZoomOut,
23};
24
25use crate::geometry::EdgeGeometry;
26use crate::layout::{Body, ForceConfig, Simulation, Vec2};
27use crate::model::{Edge, GraphData, Node};
28use crate::style::{ColorBy, GraphColors, GraphMetrics, LabelMode, NodeSizing, Palette};
29use visual::{GroupInput, Vertex, Visual};
30
31const MIN_ZOOM: f32 = 0.05;
32const MAX_ZOOM: f32 = 6.;
33const DRAG_THRESHOLD: f32 = 3.;
35const EDGE_HIT_SLOP: f32 = 4.;
37const REDUCED_MOTION_BUDGET: Duration = Duration::from_millis(120);
40
41#[derive(Clone, Debug, Default)]
43#[non_exhaustive]
44pub enum Selection {
45 #[default]
46 None,
47 Node(Node),
48 Edge(Edge),
49 EdgeBundle(Vec<Edge>),
51 Group(GroupInfo),
53}
54
55#[derive(Clone, Debug)]
56#[non_exhaustive]
57pub enum NetworkGraphEvent {
58 SelectionChanged(Selection),
59 NodeActivated(Node),
61 GroupToggled {
63 group: SharedString,
64 collapsed: bool,
65 },
66 ContextMenu {
68 target: Selection,
69 position: Point<Pixels>,
70 },
71}
72
73#[derive(Clone, Debug)]
75#[non_exhaustive]
76pub struct CategoryInfo {
77 pub name: SharedString,
78 pub color: Hsla,
79 pub count: usize,
80}
81
82#[derive(Clone, Debug)]
84#[non_exhaustive]
85pub struct GroupInfo {
86 pub id: SharedString,
87 pub label: SharedString,
88 pub color: Hsla,
89 pub size: usize,
91 pub collapsed: bool,
92}
93
94pub type TooltipBuilder =
96 Rc<dyn Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>>;
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum Item {
101 Vertex(usize),
102 Edge(usize),
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107enum SelectionKey {
108 Node(SharedString),
109 Group(SharedString),
110 Edges(Vec<usize>),
112}
113
114struct ModelEdge {
115 data: Edge,
116 source: usize,
117 target: usize,
118}
119
120struct GroupState {
121 id: SharedString,
122 label: SharedString,
123 members: Vec<usize>,
124 collapsed: bool,
125 color_slot: usize,
128 body: Body,
130 anchor: Vec2,
132}
133
134enum DragKind {
135 Pan { start_pan: Point<Pixels> },
136 Vertex { ix: usize, was_pinned: bool },
137}
138
139struct Drag {
140 kind: DragKind,
141 start: Point<Pixels>,
142 moved: bool,
143}
144
145pub struct NetworkGraph {
158 nodes: Vec<Node>,
160 node_bodies: Vec<Body>,
161 edges: Vec<ModelEdge>,
162 index: HashMap<SharedString, usize>,
163 degrees: Vec<usize>,
164 neighbors: Vec<Vec<usize>>,
165 categories: Vec<SharedString>,
169 groups: Vec<GroupState>,
170 next_group_color: usize,
172 group_index: HashMap<SharedString, usize>,
173 node_group: Vec<Option<usize>>,
174
175 vis: Visual,
177 sim: Simulation,
178
179 pan: Point<Pixels>,
181 zoom: f32,
182 auto_fit: bool,
184 bounds: Rc<Cell<Bounds<Pixels>>>,
185 fit_size: Size<Pixels>,
187
188 hovered: Option<Item>,
189 selected: Option<SelectionKey>,
190 drag: Option<Drag>,
191 pointer: Option<Point<Pixels>>,
192 focus_handle: FocusHandle,
193 keyboard_nav: bool,
195 shown_labels: RefCell<HashSet<usize>>,
197
198 label_mode: LabelMode,
199 color_by: ColorBy,
200 arrows: bool,
201 edge_labels: bool,
202 focus_dimming: bool,
204 group_hulls: bool,
205 sticky_drag: bool,
207 palette: Palette,
208 sizing: NodeSizing,
209 colors: Option<GraphColors>,
210 tooltip: Option<TooltipBuilder>,
211 tooltips: bool,
212}
213
214impl EventEmitter<NetworkGraphEvent> for NetworkGraph {}
215
216impl Focusable for NetworkGraph {
217 fn focus_handle(&self, _: &App) -> FocusHandle {
218 self.focus_handle.clone()
219 }
220}
221
222impl NetworkGraph {
223 pub fn new(cx: &mut Context<Self>) -> Self {
224 Self {
225 nodes: Vec::new(),
226 node_bodies: Vec::new(),
227 edges: Vec::new(),
228 index: HashMap::new(),
229 degrees: Vec::new(),
230 neighbors: Vec::new(),
231 categories: Vec::new(),
232 groups: Vec::new(),
233 next_group_color: 0,
234 group_index: HashMap::new(),
235 node_group: Vec::new(),
236 vis: Visual::default(),
237 sim: Simulation::new(ForceConfig::default()),
238 pan: Point::default(),
239 zoom: 1.,
240 auto_fit: true,
241 bounds: Rc::new(Cell::new(Bounds::default())),
242 fit_size: Size::default(),
243 hovered: None,
244 selected: None,
245 drag: None,
246 pointer: None,
247 focus_handle: cx.focus_handle().tab_stop(true),
248 keyboard_nav: false,
249 shown_labels: RefCell::default(),
250 label_mode: LabelMode::default(),
251 color_by: ColorBy::default(),
252 arrows: true,
253 edge_labels: true,
254 focus_dimming: true,
255 group_hulls: true,
256 sticky_drag: false,
257 palette: Palette::default(),
258 sizing: NodeSizing::default(),
259 colors: None,
260 tooltip: None,
261 tooltips: true,
262 }
263 }
264
265 pub fn with_data(mut self, data: GraphData) -> Self {
268 self.load(data);
269 self
270 }
271
272 pub fn with_force_config(mut self, config: ForceConfig) -> Self {
273 self.sim.config = config;
274 self
275 }
276
277 pub fn with_label_mode(mut self, mode: LabelMode) -> Self {
278 self.label_mode = mode;
279 self
280 }
281
282 pub fn with_color_by(mut self, color_by: ColorBy) -> Self {
283 self.color_by = color_by;
284 self
285 }
286
287 pub fn arrows(mut self, arrows: bool) -> Self {
289 self.arrows = arrows;
290 self
291 }
292
293 pub fn edge_labels(mut self, edge_labels: bool) -> Self {
295 self.edge_labels = edge_labels;
296 self
297 }
298
299 pub fn group_hulls(mut self, group_hulls: bool) -> Self {
301 self.group_hulls = group_hulls;
302 self
303 }
304
305 pub fn sticky_drag(mut self, sticky: bool) -> Self {
307 self.sticky_drag = sticky;
308 self
309 }
310
311 pub fn with_palette(mut self, palette: Palette) -> Self {
312 self.palette = palette;
313 self
314 }
315
316 pub fn with_node_sizing(mut self, sizing: NodeSizing) -> Self {
317 self.sizing = sizing;
318 self
319 }
320
321 pub fn with_colors(mut self, colors: GraphColors) -> Self {
323 self.colors = Some(colors);
324 self
325 }
326
327 pub fn with_tooltip(
329 mut self,
330 builder: impl Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>
331 + 'static,
332 ) -> Self {
333 self.tooltip = Some(Rc::new(builder));
334 self
335 }
336
337 pub fn tooltips(mut self, tooltips: bool) -> Self {
339 self.tooltips = tooltips;
340 self
341 }
342
343 pub fn label_mode(&self) -> LabelMode {
346 self.label_mode
347 }
348
349 pub fn set_label_mode(&mut self, mode: LabelMode, cx: &mut Context<Self>) {
350 self.label_mode = mode;
351 cx.notify();
352 }
353
354 pub fn color_by(&self) -> ColorBy {
355 self.color_by
356 }
357
358 pub fn set_color_by(&mut self, color_by: ColorBy, cx: &mut Context<Self>) {
359 self.color_by = color_by;
360 cx.notify();
361 }
362
363 pub fn has_arrows(&self) -> bool {
364 self.arrows
365 }
366
367 pub fn set_arrows(&mut self, arrows: bool, cx: &mut Context<Self>) {
368 self.arrows = arrows;
369 cx.notify();
370 }
371
372 pub fn has_edge_labels(&self) -> bool {
373 self.edge_labels
374 }
375
376 pub fn set_focus_dimming(&mut self, dimming: bool, cx: &mut Context<Self>) {
380 if self.focus_dimming != dimming {
381 self.focus_dimming = dimming;
382 cx.notify();
383 }
384 }
385
386 pub fn focus_dimming(&self) -> bool {
387 self.focus_dimming
388 }
389
390 pub fn set_edge_labels(&mut self, edge_labels: bool, cx: &mut Context<Self>) {
391 self.edge_labels = edge_labels;
392 cx.notify();
393 }
394
395 pub fn has_group_hulls(&self) -> bool {
396 self.group_hulls
397 }
398
399 pub fn set_group_hulls(&mut self, group_hulls: bool, cx: &mut Context<Self>) {
400 self.group_hulls = group_hulls;
401 cx.notify();
402 }
403
404 pub fn is_sticky_drag(&self) -> bool {
405 self.sticky_drag
406 }
407
408 pub fn set_sticky_drag(&mut self, sticky: bool) {
409 self.sticky_drag = sticky;
410 }
411
412 pub fn has_tooltips(&self) -> bool {
413 self.tooltips
414 }
415
416 pub fn set_tooltips(&mut self, tooltips: bool, cx: &mut Context<Self>) {
417 self.tooltips = tooltips;
418 cx.notify();
419 }
420
421 pub fn set_force_config(&mut self, config: ForceConfig, cx: &mut Context<Self>) {
422 self.sim.config = config;
423 self.sim.reheat(0.5);
424 cx.notify();
425 }
426
427 pub fn set_colors(&mut self, colors: Option<GraphColors>, cx: &mut Context<Self>) {
428 self.colors = colors;
429 cx.notify();
430 }
431
432 pub fn set_data(&mut self, data: GraphData, cx: &mut Context<Self>) {
436 let had_selection = self.selected.is_some();
437 self.load(data);
438 if had_selection {
439 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
440 }
441 cx.notify();
442 }
443
444 pub fn merge(&mut self, data: GraphData, anchor: Option<&str>, cx: &mut Context<Self>) {
448 self.cancel_drag();
449 self.sync_back();
450 self.insert(data, anchor);
451 cx.notify();
452 }
453
454 pub fn remove_nodes(&mut self, ids: &[&str], cx: &mut Context<Self>) {
456 let ids: HashSet<&str> = ids.iter().copied().collect();
457 let doomed: Vec<bool> = self
458 .nodes
459 .iter()
460 .map(|n| ids.contains(n.id.as_ref()))
461 .collect();
462 if !doomed.contains(&true) {
463 return;
464 }
465 self.cancel_drag();
466 self.sync_back();
467 let mut remap = vec![usize::MAX; self.nodes.len()];
468 let mut kept = 0;
469 for (i, &d) in doomed.iter().enumerate() {
470 if !d {
471 remap[i] = kept;
472 kept += 1;
473 }
474 }
475 let mut i = 0;
476 self.nodes.retain(|_| (!doomed[i], i += 1).0);
477 let mut i = 0;
478 self.node_bodies.retain(|_| (!doomed[i], i += 1).0);
479 self.edges.retain_mut(|e| {
480 let keep = !doomed[e.source] && !doomed[e.target];
481 e.source = remap[e.source];
482 e.target = remap[e.target];
483 keep
484 });
485 self.index = self
486 .nodes
487 .iter()
488 .enumerate()
489 .map(|(i, n)| (n.id.clone(), i))
490 .collect();
491 if matches!(self.selected, Some(SelectionKey::Edges(_))) {
493 self.selected = None;
494 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
495 }
496 self.rebuild();
497 self.sim.reheat(0.3);
498 self.validate_selection(cx);
499 cx.notify();
500 }
501
502 pub fn clear(&mut self, cx: &mut Context<Self>) {
503 self.set_data(GraphData::default(), cx);
504 }
505
506 pub fn data(&self) -> GraphData {
508 GraphData {
509 nodes: self.nodes.clone(),
510 edges: self.edges.iter().map(|e| e.data.clone()).collect(),
511 }
512 }
513
514 fn load(&mut self, data: GraphData) {
515 self.cancel_drag();
516 self.nodes.clear();
517 self.node_bodies.clear();
518 self.edges.clear();
519 self.index.clear();
520 self.categories.clear();
521 self.groups.clear();
522 self.next_group_color = 0;
523 self.group_index.clear();
524 self.vis = Visual::default();
525 self.hovered = None;
526 self.selected = None;
527 self.drag = None;
528 self.insert(data, None);
529 self.pan = Point::default();
530 self.zoom = 1.;
531 self.auto_fit = true;
532 }
533
534 fn cancel_drag(&mut self) {
538 if let Some(Drag {
539 kind: DragKind::Vertex { ix, was_pinned },
540 moved: true,
541 ..
542 }) = self.drag.take()
543 && !(self.sticky_drag || was_pinned)
544 && let Some(body) = self.vis.bodies.get_mut(ix)
545 {
546 body.fixed = None;
547 }
548 self.sim.alpha_target = 0.;
549 }
550
551 fn insert(&mut self, data: GraphData, anchor: Option<&str>) {
553 let anchor_pos = anchor
554 .and_then(|id| self.index.get(id))
555 .map(|&i| self.vis.bodies[self.vis.node_slot[i]].pos);
556 let first_new = self.nodes.len();
557 for node in data.nodes {
558 if self.index.contains_key(&node.id) {
559 continue;
560 }
561 if !self.categories.contains(&node.category) {
562 self.categories.push(node.category.clone());
563 }
564 self.index.insert(node.id.clone(), self.nodes.len());
565 self.nodes.push(node);
566 self.node_bodies.push(Body::default());
567 }
568 match anchor_pos {
569 Some(center) => {
570 let added = self.nodes.len() - first_new;
571 for (k, body) in self.node_bodies[first_new..].iter_mut().enumerate() {
572 let a = k as f32 / added.max(1) as f32 * std::f32::consts::TAU;
573 body.pos = Vec2::new(center.x + 30. * a.cos(), center.y + 30. * a.sin());
574 }
575 }
576 None => Simulation::initialize_positions(&mut self.node_bodies, first_new),
577 }
578
579 let mut seen: HashSet<(usize, usize, SharedString)> = self
580 .edges
581 .iter()
582 .map(|e| (e.source, e.target, e.data.label.clone()))
583 .collect();
584 for edge in data.edges {
585 let (Some(&s), Some(&t)) = (self.index.get(&edge.source), self.index.get(&edge.target))
586 else {
587 continue;
588 };
589 if seen.insert((s, t, edge.label.clone())) {
590 self.edges.push(ModelEdge {
591 data: edge,
592 source: s,
593 target: t,
594 });
595 }
596 }
597 self.rebuild();
598 self.sim.reheat(if anchor_pos.is_some() { 0.5 } else { 1. });
599 }
600
601 fn sync_back(&mut self) {
603 for (v, vertex) in self.vis.vertices.iter().enumerate() {
604 let body = self.vis.bodies[v].clone();
605 match *vertex {
606 Vertex::Node(i) => self.node_bodies[i] = body,
607 Vertex::Group(g) => self.groups[g].body = body,
608 }
609 }
610 }
611
612 fn rebuild(&mut self) {
614 let n = self.nodes.len();
615 self.degrees = vec![0; n];
616 self.neighbors = vec![Vec::new(); n];
617 for e in &self.edges {
618 self.degrees[e.source] += 1;
619 self.degrees[e.target] += 1;
620 if e.source != e.target {
621 self.neighbors[e.source].push(e.target);
622 self.neighbors[e.target].push(e.source);
623 }
624 }
625 for list in &mut self.neighbors {
626 list.sort_unstable();
627 list.dedup();
628 }
629 for (i, body) in self.node_bodies.iter_mut().enumerate() {
630 body.radius = self.nodes[i]
632 .size
633 .filter(|r| r.is_finite())
634 .unwrap_or_else(|| self.sizing.radius(self.degrees[i]))
635 .max(1.);
636 }
637
638 for g in &mut self.groups {
640 g.members.clear();
641 }
642 self.node_group = vec![None; n];
643 for (i, node) in self.nodes.iter().enumerate() {
644 let Some(id) = &node.group else { continue };
645 let g = *self.group_index.entry(id.clone()).or_insert_with(|| {
646 let color_slot = self.next_group_color;
647 self.next_group_color += 1;
648 self.groups.push(GroupState {
649 id: id.clone(),
650 label: id.clone(),
651 members: Vec::new(),
652 collapsed: false,
653 color_slot,
654 body: Body::default(),
655 anchor: Vec2::default(),
656 });
657 self.groups.len() - 1
658 });
659 self.groups[g].members.push(i);
660 self.node_group[i] = Some(g);
661 }
662 if self.groups.iter().any(|g| g.members.is_empty()) {
663 self.groups.retain(|g| !g.members.is_empty());
664 self.group_index = self
665 .groups
666 .iter()
667 .enumerate()
668 .map(|(g, s)| (s.id.clone(), g))
669 .collect();
670 for (i, node) in self.nodes.iter().enumerate() {
671 self.node_group[i] = node.group.as_ref().map(|id| self.group_index[id]);
672 }
673 }
674 for g in &mut self.groups {
675 g.body.radius = (12. + (g.members.len() as f32).sqrt() * 3.).min(48.);
676 }
677
678 let groups: Vec<GroupInput> = self
679 .groups
680 .iter()
681 .map(|g| GroupInput {
682 collapsed: g.collapsed,
683 body: &g.body,
684 })
685 .collect();
686 let links: Vec<(usize, usize)> = self.edges.iter().map(|e| (e.source, e.target)).collect();
687 self.vis = Visual::build(&self.node_bodies, &self.node_group, &groups, &links);
688 self.hovered = None;
689 self.drag = None;
690 self.shown_labels.take();
692 }
693
694 pub fn node_count(&self) -> usize {
697 self.nodes.len()
698 }
699
700 pub fn edge_count(&self) -> usize {
701 self.edges.len()
702 }
703
704 pub fn node(&self, id: &str) -> Option<&Node> {
705 self.index.get(id).map(|&i| &self.nodes[i])
706 }
707
708 pub fn degree(&self, id: &str) -> usize {
709 self.index.get(id).map_or(0, |&i| self.degrees[i])
710 }
711
712 pub fn neighbors_of(&self, id: &str) -> Vec<Node> {
713 self.index
714 .get(id)
715 .map(|&i| {
716 self.neighbors[i]
717 .iter()
718 .map(|&j| self.nodes[j].clone())
719 .collect()
720 })
721 .unwrap_or_default()
722 }
723
724 pub fn edges_of(&self, id: &str) -> Vec<Edge> {
726 let Some(&i) = self.index.get(id) else {
727 return Vec::new();
728 };
729 self.edges
730 .iter()
731 .filter(|e| e.source == i || e.target == i)
732 .map(|e| e.data.clone())
733 .collect()
734 }
735
736 pub fn categories(&self) -> Vec<CategoryInfo> {
737 let mut counts: HashMap<&SharedString, usize> = HashMap::new();
738 for node in &self.nodes {
739 *counts.entry(&node.category).or_default() += 1;
740 }
741 self.categories
742 .iter()
743 .enumerate()
744 .filter_map(|(i, c)| {
745 Some(CategoryInfo {
746 name: c.clone(),
747 color: self.palette.get(i),
748 count: *counts.get(c)?,
749 })
750 })
751 .collect()
752 }
753
754 pub fn groups(&self) -> Vec<GroupInfo> {
755 (0..self.groups.len()).map(|g| self.group_info(g)).collect()
756 }
757
758 pub fn group(&self, id: &str) -> Option<GroupInfo> {
759 self.group_index.get(id).map(|&g| self.group_info(g))
760 }
761
762 pub fn group_members(&self, id: &str) -> Vec<Node> {
764 self.group_index
765 .get(id)
766 .map(|&g| {
767 self.groups[g]
768 .members
769 .iter()
770 .map(|&i| self.nodes[i].clone())
771 .collect()
772 })
773 .unwrap_or_default()
774 }
775
776 pub fn resolved_colors(&self, cx: &App) -> GraphColors {
778 self.colors
779 .clone()
780 .unwrap_or_else(|| GraphColors::from_theme(cx))
781 }
782
783 pub fn color_of(&self, node: &Node, cx: &App) -> Hsla {
785 if let Some(c) = node.color {
786 return c;
787 }
788 match self.color_by {
789 ColorBy::Category => {
790 let ix = self
791 .categories
792 .iter()
793 .position(|c| c == &node.category)
794 .unwrap_or(0);
795 self.palette.get(ix)
796 }
797 ColorBy::Group => match node.group.as_ref().and_then(|g| self.group_index.get(g)) {
798 Some(&g) => self.group_color(g),
799 None => self.resolved_colors(cx).ungrouped,
800 },
801 }
802 }
803
804 pub fn selection(&self) -> Selection {
805 match &self.selected {
806 None => Selection::None,
807 Some(SelectionKey::Node(id)) => self
808 .node(id)
809 .cloned()
810 .map_or(Selection::None, Selection::Node),
811 Some(SelectionKey::Group(id)) => {
812 self.group(id).map_or(Selection::None, Selection::Group)
813 }
814 Some(SelectionKey::Edges(edges)) => self.edge_selection(edges),
815 }
816 }
817
818 pub fn zoom(&self) -> f32 {
819 self.zoom
820 }
821
822 pub fn is_simulating(&self) -> bool {
824 self.sim.is_active()
825 }
826
827 fn group_info(&self, g: usize) -> GroupInfo {
828 let s = &self.groups[g];
829 GroupInfo {
830 id: s.id.clone(),
831 label: s.label.clone(),
832 color: self.group_color(g),
833 size: s.members.len(),
834 collapsed: s.collapsed,
835 }
836 }
837
838 fn group_color(&self, g: usize) -> Hsla {
839 self.palette.get(self.groups[g].color_slot)
840 }
841
842 fn edge_selection(&self, edges: &[usize]) -> Selection {
843 match edges {
844 [] => Selection::None,
845 [e] => Selection::Edge(self.edges[*e].data.clone()),
846 many => {
847 Selection::EdgeBundle(many.iter().map(|&e| self.edges[e].data.clone()).collect())
848 }
849 }
850 }
851
852 fn key_of(&self, item: Item) -> SelectionKey {
853 match item {
854 Item::Vertex(v) => match self.vis.vertices[v] {
855 Vertex::Node(i) => SelectionKey::Node(self.nodes[i].id.clone()),
856 Vertex::Group(g) => SelectionKey::Group(self.groups[g].id.clone()),
857 },
858 Item::Edge(e) => SelectionKey::Edges(self.vis.edges[e].edges.clone()),
859 }
860 }
861
862 fn item_selection(&self, item: Item) -> Selection {
863 match item {
864 Item::Vertex(v) => match self.vis.vertices[v] {
865 Vertex::Node(i) => Selection::Node(self.nodes[i].clone()),
866 Vertex::Group(g) => Selection::Group(self.group_info(g)),
867 },
868 Item::Edge(e) => self.edge_selection(&self.vis.edges[e].edges),
869 }
870 }
871
872 fn selected_item(&self) -> Option<Item> {
874 match self.selected.as_ref()? {
875 SelectionKey::Node(id) => {
876 let slot = self.vis.node_slot[*self.index.get(id)?];
877 matches!(self.vis.vertices[slot], Vertex::Node(_)).then_some(Item::Vertex(slot))
878 }
879 SelectionKey::Group(id) => {
880 self.vis.group_slot[*self.group_index.get(id)?].map(Item::Vertex)
881 }
882 SelectionKey::Edges(edges) => self
883 .vis
884 .edges
885 .iter()
886 .position(|e| &e.edges == edges)
887 .map(Item::Edge),
888 }
889 }
890
891 fn validate_selection(&mut self, cx: &mut Context<Self>) {
893 if self.selected.is_some() && matches!(self.selection(), Selection::None) {
894 self.selected = None;
895 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
896 }
897 }
898
899 pub fn select_node(&mut self, id: Option<&str>, center: bool, cx: &mut Context<Self>) {
903 let key = id
904 .filter(|id| self.index.contains_key(*id))
905 .map(|id| SelectionKey::Node(id.to_string().into()));
906 if let (Some(id), true) = (id, center)
907 && let Some(&i) = self.index.get(id)
908 {
909 let p = self.vis.bodies[self.vis.node_slot[i]].pos;
910 self.auto_fit = false;
911 self.pan = point(px(-p.x * self.zoom), px(-p.y * self.zoom));
912 cx.notify();
913 }
914 self.set_selected(key, cx);
915 }
916
917 fn set_selected(&mut self, key: Option<SelectionKey>, cx: &mut Context<Self>) {
918 if key == self.selected {
919 return;
920 }
921 self.selected = key;
922 cx.emit(NetworkGraphEvent::SelectionChanged(self.selection()));
923 cx.notify();
924 }
925
926 pub fn set_pinned(&mut self, id: &str, pinned: bool, cx: &mut Context<Self>) {
928 let Some(&i) = self.index.get(id) else { return };
929 let v = self.vis.node_slot[i];
930 if matches!(self.vis.vertices[v], Vertex::Node(_)) {
931 let body = &mut self.vis.bodies[v];
932 body.fixed = pinned.then_some(body.pos);
933 } else {
934 let body = &mut self.node_bodies[i];
935 body.fixed = pinned.then_some(body.pos);
936 }
937 self.sim.reheat(0.1);
938 cx.notify();
939 }
940
941 pub fn is_pinned(&self, id: &str) -> bool {
942 self.index.get(id).is_some_and(|&i| {
943 let v = self.vis.node_slot[i];
944 match self.vis.vertices[v] {
945 Vertex::Node(_) => self.vis.bodies[v].fixed.is_some(),
946 Vertex::Group(_) => self.node_bodies[i].fixed.is_some(),
947 }
948 })
949 }
950
951 pub fn unpin_all(&mut self, cx: &mut Context<Self>) {
952 for b in self.vis.bodies.iter_mut().chain(&mut self.node_bodies) {
953 b.fixed = None;
954 }
955 self.sim.reheat(0.3);
956 cx.notify();
957 }
958
959 pub fn collapse_group(&mut self, id: &str, cx: &mut Context<Self>) {
963 self.set_groups_collapsed(|g| g.id.as_ref() == id, true, cx);
964 }
965
966 pub fn expand_group(&mut self, id: &str, cx: &mut Context<Self>) {
967 self.set_groups_collapsed(|g| g.id.as_ref() == id, false, cx);
968 }
969
970 pub fn toggle_group(&mut self, id: &str, cx: &mut Context<Self>) {
971 if let Some(&g) = self.group_index.get(id) {
972 let collapse = !self.groups[g].collapsed;
973 self.set_groups_collapsed(|s| s.id.as_ref() == id, collapse, cx);
974 }
975 }
976
977 pub fn collapse_all_groups(&mut self, cx: &mut Context<Self>) {
979 self.set_groups_collapsed(|_| true, true, cx);
980 }
981
982 pub fn expand_all_groups(&mut self, cx: &mut Context<Self>) {
983 self.set_groups_collapsed(|_| true, false, cx);
984 }
985
986 pub fn set_group_label(
987 &mut self,
988 id: &str,
989 label: impl Into<SharedString>,
990 cx: &mut Context<Self>,
991 ) {
992 if let Some(&g) = self.group_index.get(id) {
993 self.groups[g].label = label.into();
994 cx.notify();
995 }
996 }
997
998 fn set_groups_collapsed(
999 &mut self,
1000 filter: impl Fn(&GroupState) -> bool,
1001 collapse: bool,
1002 cx: &mut Context<Self>,
1003 ) {
1004 let targets: Vec<usize> = (0..self.groups.len())
1005 .filter(|&g| filter(&self.groups[g]) && self.groups[g].collapsed != collapse)
1006 .collect();
1007 if targets.is_empty() {
1008 return;
1009 }
1010 self.cancel_drag();
1011 self.sync_back();
1012 for &g in &targets {
1013 let members = self.groups[g].members.clone();
1014 if collapse {
1015 let n = members.len().max(1) as f32;
1016 let (sx, sy) = members.iter().fold((0., 0.), |(x, y), &i| {
1017 (x + self.node_bodies[i].pos.x, y + self.node_bodies[i].pos.y)
1018 });
1019 let centroid = Vec2::new(sx / n, sy / n);
1020 let group = &mut self.groups[g];
1021 group.anchor = centroid;
1022 group.body.pos = centroid;
1023 group.body.vel = Vec2::default();
1024 group.body.fixed = None;
1025 } else {
1026 let group = &self.groups[g];
1027 let (dx, dy) = (
1028 group.body.pos.x - group.anchor.x,
1029 group.body.pos.y - group.anchor.y,
1030 );
1031 for i in members {
1032 let b = &mut self.node_bodies[i];
1033 b.pos = Vec2::new(b.pos.x + dx, b.pos.y + dy);
1034 b.vel = Vec2::default();
1035 if let Some(f) = b.fixed {
1036 b.fixed = Some(Vec2::new(f.x + dx, f.y + dy));
1037 }
1038 }
1039 }
1040 self.groups[g].collapsed = collapse;
1041 }
1042 if let (false, Some(SelectionKey::Group(id))) = (collapse, &self.selected)
1044 && targets.iter().any(|&g| &self.groups[g].id == id)
1045 {
1046 self.selected = None;
1047 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
1048 }
1049 self.rebuild();
1050 self.sim.reheat(0.4);
1051 for g in targets {
1052 cx.emit(NetworkGraphEvent::GroupToggled {
1053 group: self.groups[g].id.clone(),
1054 collapsed: collapse,
1055 });
1056 }
1057 cx.notify();
1058 }
1059
1060 pub fn fit_view(&mut self, cx: &mut Context<Self>) {
1064 self.auto_fit = true;
1065 self.apply_fit(1.);
1066 cx.notify();
1067 }
1068
1069 pub fn zoom_by(&mut self, factor: f32, cx: &mut Context<Self>) {
1072 if !(factor.is_finite() && factor > 0.) {
1073 return;
1074 }
1075 self.auto_fit = false;
1076 let center = self.bounds.get().center();
1077 self.zoom_around(center, factor);
1078 cx.notify();
1079 }
1080
1081 pub fn relayout(&mut self, cx: &mut Context<Self>) {
1083 self.cancel_drag();
1084 for b in &mut self.vis.bodies {
1085 b.fixed = None;
1086 }
1087 Simulation::initialize_positions(&mut self.vis.bodies, 0);
1088 self.sim.reheat(1.);
1089 self.auto_fit = true;
1090 cx.notify();
1091 }
1092
1093 fn apply_fit(&mut self, blend: f32) {
1096 let bounds = self.bounds.get();
1097 if self.vis.bodies.is_empty() || bounds.size.width <= px(0.) {
1098 return;
1099 }
1100 self.fit_size = bounds.size;
1101 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
1102 for b in &self.vis.bodies {
1103 x0 = x0.min(b.pos.x - b.radius);
1104 y0 = y0.min(b.pos.y - b.radius);
1105 x1 = x1.max(b.pos.x + b.radius);
1106 y1 = y1.max(b.pos.y + b.radius);
1107 }
1108 let padding = 48.;
1109 let w = (bounds.size.width.as_f32() - padding * 2.).max(1.);
1110 let h = (bounds.size.height.as_f32() - padding * 2.).max(1.);
1111 let zoom = (w / (x1 - x0).max(1.))
1112 .min(h / (y1 - y0).max(1.))
1113 .clamp(MIN_ZOOM, 2.);
1114 let (cx, cy) = ((x0 + x1) / 2., (y0 + y1) / 2.);
1115 let target = point(px(-cx * zoom), px(-cy * zoom));
1116 self.zoom += (zoom - self.zoom) * blend;
1117 self.pan = self.pan + (target - self.pan) * blend;
1118 }
1119
1120 fn zoom_around(&mut self, anchor: Point<Pixels>, factor: f32) {
1121 let world = self.to_world(anchor);
1122 self.zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM);
1123 let center = self.bounds.get().center();
1124 self.pan = anchor - center - point(px(world.x * self.zoom), px(world.y * self.zoom));
1125 }
1126
1127 fn to_screen(&self, p: Vec2) -> Point<Pixels> {
1128 self.bounds.get().center() + self.pan + point(px(p.x * self.zoom), px(p.y * self.zoom))
1129 }
1130
1131 fn to_world(&self, p: Point<Pixels>) -> Vec2 {
1132 let local = p - self.bounds.get().center() - self.pan;
1133 Vec2::new(local.x.as_f32() / self.zoom, local.y.as_f32() / self.zoom)
1134 }
1135
1136 fn edge_geometry(&self, e: usize) -> EdgeGeometry {
1137 let e = &self.vis.edges[e];
1138 let a = self.to_screen(self.vis.bodies[e.source].pos);
1139 let b = self.to_screen(self.vis.bodies[e.target].pos);
1140 EdgeGeometry::new(a, b, e.curvature * 28. * self.zoom.min(1.5))
1141 }
1142
1143 fn hit_test(&self, p: Point<Pixels>) -> Option<Item> {
1144 let w = self.to_world(p);
1145 let slop = 3. / self.zoom;
1146 let vertex = self.vis.bodies.iter().enumerate().rev().find_map(|(i, b)| {
1148 let (dx, dy) = (b.pos.x - w.x, b.pos.y - w.y);
1149 let r = b.radius + slop;
1150 (dx * dx + dy * dy <= r * r).then_some(i)
1151 });
1152 if let Some(v) = vertex {
1153 return Some(Item::Vertex(v));
1154 }
1155 let mut best = (EDGE_HIT_SLOP, None);
1156 for (i, e) in self.vis.edges.iter().enumerate() {
1157 let d = if e.source == e.target {
1158 let c = self.to_screen(self.vis.bodies[e.source].pos);
1159 let s = (self.vis.bodies[e.source].radius * self.zoom).max(6.);
1160 let center = point(c.x + px(s * 1.3), c.y - px(s * 1.3));
1161 let r = (p.x - center.x).as_f32().hypot((p.y - center.y).as_f32());
1162 (r - s * 0.9).abs()
1163 } else {
1164 self.edge_geometry(i).distance_to(p)
1165 };
1166 if d < best.0 {
1167 best = (d, Some(i));
1168 }
1169 }
1170 best.1.map(Item::Edge)
1171 }
1172
1173 fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
1176 self.keyboard_nav = true;
1177 self.zoom_by(1.25, cx);
1178 }
1179
1180 fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
1181 self.keyboard_nav = true;
1182 self.zoom_by(0.8, cx);
1183 }
1184
1185 fn on_fit_view(&mut self, _: &FitView, _: &mut Window, cx: &mut Context<Self>) {
1186 self.keyboard_nav = true;
1187 self.fit_view(cx);
1188 }
1189
1190 fn on_clear_selection(&mut self, _: &ClearSelection, _: &mut Window, cx: &mut Context<Self>) {
1191 self.keyboard_nav = true;
1192 if self.selected.is_some() {
1193 self.set_selected(None, cx);
1194 } else {
1195 cx.propagate();
1196 }
1197 }
1198
1199 fn on_activate(&mut self, _: &Activate, _: &mut Window, cx: &mut Context<Self>) {
1200 self.keyboard_nav = true;
1201 match self.selected.clone() {
1202 Some(SelectionKey::Node(id)) => {
1203 if let Some(node) = self.node(&id).cloned() {
1204 cx.emit(NetworkGraphEvent::NodeActivated(node));
1205 }
1206 }
1207 Some(SelectionKey::Group(id)) => self.toggle_group(&id, cx),
1208 _ => cx.propagate(),
1209 }
1210 }
1211
1212 fn on_open_context_menu(
1213 &mut self,
1214 _: &OpenContextMenu,
1215 _: &mut Window,
1216 cx: &mut Context<Self>,
1217 ) {
1218 self.keyboard_nav = true;
1219 let position = match self.selected_item() {
1220 Some(Item::Vertex(v)) => self.to_screen(self.vis.bodies[v].pos),
1221 Some(Item::Edge(e)) => self.edge_geometry(e).midpoint(),
1222 None => self.bounds.get().center(),
1223 };
1224 cx.emit(NetworkGraphEvent::ContextMenu {
1225 target: self.selection(),
1226 position,
1227 });
1228 }
1229
1230 fn select_toward(&mut self, dx: f32, dy: f32, cx: &mut Context<Self>) {
1233 self.keyboard_nav = true;
1234 let bodies = &self.vis.bodies;
1235 if bodies.is_empty() {
1236 return;
1237 }
1238 let from = match self.selected_item() {
1239 Some(Item::Vertex(v)) => Some(v),
1240 Some(Item::Edge(e)) => Some(self.vis.edges[e].source),
1241 None => None,
1242 };
1243 let target = match from {
1244 None => {
1245 let c = self.to_world(self.bounds.get().center());
1246 (0..bodies.len()).min_by(|&a, &b| {
1247 let d = |i: usize| (bodies[i].pos.x - c.x).hypot(bodies[i].pos.y - c.y);
1248 d(a).total_cmp(&d(b))
1249 })
1250 }
1251 Some(f) => {
1252 let p = bodies[f].pos;
1253 (0..bodies.len())
1254 .filter(|&i| i != f)
1255 .filter_map(|i| {
1256 let (x, y) = (bodies[i].pos.x - p.x, bodies[i].pos.y - p.y);
1257 let dist = x.hypot(y).max(1e-3);
1258 let cos = (x * dx + y * dy) / dist;
1259 (cos > 0.5).then_some((i, dist * (2. - cos)))
1261 })
1262 .min_by(|a, b| a.1.total_cmp(&b.1))
1263 .map(|(i, _)| i)
1264 }
1265 };
1266 let Some(v) = target else { return };
1267 self.reveal(v);
1268 let key = self.key_of(Item::Vertex(v));
1269 self.set_selected(Some(key), cx);
1270 cx.notify();
1271 }
1272
1273 fn reveal(&mut self, v: usize) {
1275 let bounds = self.bounds.get();
1276 let margin = px(48.);
1277 let p = self.to_screen(self.vis.bodies[v].pos);
1278 let inner = Bounds::from_corners(
1279 bounds.origin + point(margin, margin),
1280 bounds.bottom_right() - point(margin, margin),
1281 );
1282 if !inner.contains(&p) {
1283 self.auto_fit = false;
1284 let clamped = point(
1285 p.x.clamp(inner.left(), inner.right()),
1286 p.y.clamp(inner.top(), inner.bottom()),
1287 );
1288 self.pan += clamped - p;
1289 }
1290 }
1291
1292 fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1295 window.focus(&self.focus_handle, cx);
1296 self.keyboard_nav = false;
1297 let hit = self.hit_test(ev.position);
1298 if ev.button == MouseButton::Right {
1299 if let Some(item) = hit {
1300 self.set_selected(Some(self.key_of(item)), cx);
1301 }
1302 let target = hit.map_or(Selection::None, |item| self.item_selection(item));
1303 cx.emit(NetworkGraphEvent::ContextMenu {
1304 target,
1305 position: ev.position,
1306 });
1307 return;
1308 }
1309 if ev.click_count >= 2 {
1310 if let Some(Item::Vertex(v)) = hit {
1311 match self.vis.vertices[v] {
1312 Vertex::Node(i) => {
1313 cx.emit(NetworkGraphEvent::NodeActivated(self.nodes[i].clone()))
1314 }
1315 Vertex::Group(g) => {
1316 let id = self.groups[g].id.clone();
1317 self.expand_group(&id, cx);
1318 }
1319 }
1320 }
1321 return;
1322 }
1323 let kind = match hit {
1324 Some(Item::Vertex(ix)) => DragKind::Vertex {
1325 ix,
1326 was_pinned: self.vis.bodies[ix].fixed.is_some(),
1327 },
1328 _ => DragKind::Pan {
1329 start_pan: self.pan,
1330 },
1331 };
1332 self.drag = Some(Drag {
1333 kind,
1334 start: ev.position,
1335 moved: false,
1336 });
1337 }
1338
1339 fn on_mouse_move(&mut self, ev: &MouseMoveEvent, inside: bool, cx: &mut Context<Self>) {
1340 self.pointer = inside.then_some(ev.position);
1341 if self.drag.is_some() && ev.pressed_button != Some(MouseButton::Left) {
1343 self.cancel_drag();
1344 cx.notify();
1345 }
1346 if let Some(drag) = &mut self.drag {
1347 let delta = ev.position - drag.start;
1348 if !drag.moved {
1349 if delta.x.as_f32().hypot(delta.y.as_f32()) < DRAG_THRESHOLD {
1350 return;
1351 }
1352 drag.moved = true;
1353 }
1354 match drag.kind {
1355 DragKind::Pan { start_pan } => {
1356 self.auto_fit = false;
1357 self.pan = start_pan + delta;
1358 }
1359 DragKind::Vertex { ix, .. } => {
1360 let w = self.to_world(ev.position);
1361 self.vis.bodies[ix].fixed = Some(w);
1362 self.vis.bodies[ix].pos = w;
1363 self.sim.alpha_target = 0.3;
1364 self.sim.reheat(0.3);
1365 self.auto_fit = false;
1366 }
1367 }
1368 cx.notify();
1369 return;
1370 }
1371 if inside || self.hovered.is_some() {
1376 cx.notify();
1377 }
1378 }
1379
1380 fn on_mouse_up(&mut self, ev: &MouseUpEvent, cx: &mut Context<Self>) {
1381 let Some(drag) = self.drag.take() else { return };
1382 match drag.kind {
1383 DragKind::Vertex { ix, was_pinned } => {
1384 self.sim.alpha_target = 0.;
1385 if !drag.moved {
1386 let key = self.key_of(Item::Vertex(ix));
1387 self.set_selected(Some(key), cx);
1388 } else if !(self.sticky_drag || was_pinned) {
1389 self.vis.bodies[ix].fixed = None;
1390 }
1391 }
1392 DragKind::Pan { .. } => {
1393 if !drag.moved {
1394 let key = self.hit_test(ev.position).map(|item| self.key_of(item));
1395 self.set_selected(key, cx);
1396 }
1397 }
1398 }
1399 cx.notify();
1400 }
1401
1402 fn on_scroll(&mut self, ev: &ScrollEvent, cx: &mut Context<Self>) {
1406 self.auto_fit = false;
1407 match ev.delta {
1408 ScrollDelta::Pixels(delta) if !(ev.modifiers.platform || ev.modifiers.control) => {
1409 self.pan += delta;
1410 }
1411 _ => {
1412 let delta = ev.delta.pixel_delta(px(20.));
1413 let factor = (-delta.y.as_f32() * 0.0025).exp();
1414 self.zoom_around(ev.position, factor);
1415 }
1416 }
1417 cx.notify();
1418 }
1419
1420 fn on_pinch(&mut self, ev: &PinchEvent, cx: &mut Context<Self>) {
1421 self.auto_fit = false;
1422 self.zoom_around(ev.position, (1. + ev.delta).max(0.1));
1423 cx.notify();
1424 }
1425}
1426
1427impl Render for NetworkGraph {
1428 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1429 if self.sim.is_active() && !self.vis.bodies.is_empty() {
1430 let reduce_motion = cx.reduce_motion();
1431 let vis = &mut self.vis;
1432 if reduce_motion {
1433 let deadline = Instant::now() + REDUCED_MOTION_BUDGET;
1435 while self.sim.is_active()
1436 && self.sim.alpha_target == 0.
1437 && Instant::now() < deadline
1438 {
1439 self.sim
1440 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1441 }
1442 if self.sim.alpha_target != 0. {
1443 self.sim
1444 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1445 }
1446 } else {
1447 let ticks = if vis.bodies.len() > 1500 { 1 } else { 2 };
1449 for _ in 0..ticks {
1450 self.sim
1451 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1452 }
1453 }
1454 if self.auto_fit {
1455 self.apply_fit(if reduce_motion { 1. } else { 0.25 });
1456 }
1457 if self.sim.is_active() {
1458 window.request_animation_frame();
1459 }
1460 } else if self.auto_fit && self.bounds.get().size != self.fit_size {
1461 self.apply_fit(1.);
1465 }
1466 if self.drag.is_none() {
1469 self.hovered = self.pointer.and_then(|p| self.hit_test(p));
1470 }
1471
1472 let colors = self.resolved_colors(cx);
1473 let metrics = GraphMetrics::from_theme(cx);
1474 let tooltip = self.tooltip_element(&colors, &metrics, window, cx);
1475 let entity = cx.entity();
1476 let bounds_cell = self.bounds.clone();
1477 let auto_fit = self.auto_fit;
1478 let background = colors.background;
1479 let show_focus = self.keyboard_nav && self.focus_handle.is_focused(window);
1480
1481 div()
1482 .id("network-graph")
1483 .track_focus(&self.focus_handle)
1484 .key_context(KEY_CONTEXT)
1485 .on_action(cx.listener(Self::on_zoom_in))
1486 .on_action(cx.listener(Self::on_zoom_out))
1487 .on_action(cx.listener(Self::on_fit_view))
1488 .on_action(cx.listener(Self::on_clear_selection))
1489 .on_action(cx.listener(Self::on_activate))
1490 .on_action(cx.listener(Self::on_open_context_menu))
1491 .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.select_toward(-1., 0., cx)))
1492 .on_action(cx.listener(|this, _: &SelectRight, _, cx| this.select_toward(1., 0., cx)))
1493 .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.select_toward(0., -1., cx)))
1494 .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.select_toward(0., 1., cx)))
1495 .size_full()
1496 .relative()
1497 .overflow_hidden()
1498 .bg(background)
1499 .child(
1500 canvas(
1501 move |bounds, window, _| {
1502 if auto_fit && bounds_cell.get().size != bounds.size {
1503 window.request_animation_frame();
1505 }
1506 bounds_cell.set(bounds);
1507 window.insert_hitbox(bounds, HitboxBehavior::Normal)
1508 },
1509 move |bounds, hitbox: Hitbox, window, cx| {
1510 register_input(&entity, &hitbox, window, cx);
1511 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1512 entity.update(cx, |this, cx| {
1513 this.paint_graph(bounds, &colors, &metrics, show_focus, window, cx)
1514 });
1515 });
1516 },
1517 )
1518 .size_full(),
1519 )
1520 .children(tooltip)
1521 }
1522}
1523
1524fn register_input(
1527 entity: &Entity<NetworkGraph>,
1528 hitbox: &Hitbox,
1529 window: &mut Window,
1530 cx: &mut App,
1531) {
1532 let graph = entity.read(cx);
1533 let cursor = match &graph.drag {
1535 Some(d) if d.moved => CursorStyle::ClosedHand,
1536 _ => CursorStyle::Arrow,
1537 };
1538 window.set_cursor_style(cursor, hitbox);
1539
1540 let (e, h) = (entity.clone(), hitbox.clone());
1541 window.on_mouse_event(move |ev: &MouseDownEvent, phase, window, cx| {
1542 let button_ok = matches!(ev.button, MouseButton::Left | MouseButton::Right);
1543 if phase == DispatchPhase::Bubble && button_ok && h.is_hovered(window) {
1544 e.update(cx, |this, cx| this.on_mouse_down(ev, window, cx));
1545 }
1546 });
1547 let (e, h) = (entity.clone(), hitbox.clone());
1548 window.on_mouse_event(move |ev: &MouseMoveEvent, phase, window, cx| {
1549 if phase != DispatchPhase::Bubble {
1550 return;
1551 }
1552 let inside = h.is_hovered(window);
1553 let graph = e.read(cx);
1554 if inside || graph.drag.is_some() || graph.hovered.is_some() {
1555 e.update(cx, |this, cx| this.on_mouse_move(ev, inside, cx));
1556 }
1557 });
1558 let e = entity.clone();
1559 window.on_mouse_event(move |ev: &MouseUpEvent, phase, _, cx| {
1560 if phase == DispatchPhase::Bubble && ev.button == MouseButton::Left {
1561 e.update(cx, |this, cx| this.on_mouse_up(ev, cx));
1562 }
1563 });
1564 let (e, h) = (entity.clone(), hitbox.clone());
1565 window.on_mouse_event(move |ev: &ScrollEvent, phase, window, cx| {
1566 if phase == DispatchPhase::Bubble && h.should_handle_scroll(window) {
1567 e.update(cx, |this, cx| this.on_scroll(ev, cx));
1568 }
1569 });
1570 let (e, h) = (entity.clone(), hitbox.clone());
1571 window.on_mouse_event(move |ev: &PinchEvent, phase, window, cx| {
1572 if phase == DispatchPhase::Bubble && h.is_hovered(window) {
1573 e.update(cx, |this, cx| this.on_pinch(ev, cx));
1574 }
1575 });
1576}