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