Skip to main content

gpui_graph/view/
mod.rs

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.;
33/// Pointer travel (px) before a press becomes a drag instead of a click.
34const DRAG_THRESHOLD: f32 = 3.;
35/// Pointer distance (px) within which an edge counts as hit.
36const EDGE_HIT_SLOP: f32 = 4.;
37/// With reduced motion, the layout is computed within this budget per frame
38/// instead of being animated.
39const REDUCED_MOTION_BUDGET: Duration = Duration::from_millis(120);
40
41/// What the user has selected or is pointing at.
42#[derive(Clone, Debug, Default)]
43#[non_exhaustive]
44pub enum Selection {
45    #[default]
46    None,
47    Node(Node),
48    Edge(Edge),
49    /// Several edges drawn as one, e.g. from a node into a collapsed group.
50    EdgeBundle(Vec<Edge>),
51    /// A collapsed group's super node.
52    Group(GroupInfo),
53}
54
55#[derive(Clone, Debug)]
56#[non_exhaustive]
57pub enum NetworkGraphEvent {
58    SelectionChanged(Selection),
59    /// Double-click on a node: the usual "expand neighbors" gesture in graph explorers.
60    NodeActivated(Node),
61    /// A group was collapsed or expanded, by the user or through the API.
62    GroupToggled {
63        group: SharedString,
64        collapsed: bool,
65    },
66    /// Right-click. `position` is in window coordinates, ready for a popover anchor.
67    ContextMenu {
68        target: Selection,
69        position: Point<Pixels>,
70    },
71}
72
73/// A category present in the graph, for legends.
74#[derive(Clone, Debug)]
75#[non_exhaustive]
76pub struct CategoryInfo {
77    pub name: SharedString,
78    pub color: Hsla,
79    pub count: usize,
80}
81
82/// A group (community) present in the graph.
83#[derive(Clone, Debug)]
84#[non_exhaustive]
85pub struct GroupInfo {
86    pub id: SharedString,
87    pub label: SharedString,
88    pub color: Hsla,
89    /// Number of member nodes.
90    pub size: usize,
91    pub collapsed: bool,
92}
93
94/// Builds tooltip content for the hovered item; `None` hides the tooltip.
95pub type TooltipBuilder =
96    Rc<dyn Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>>;
97
98/// A hovered / hit item, by index into the visual graph. Transient.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum Item {
101    Vertex(usize),
102    Edge(usize),
103}
104
105/// A selected item by stable identity, so it survives collapse/expand and merges.
106#[derive(Clone, Debug, PartialEq, Eq)]
107enum SelectionKey {
108    Node(SharedString),
109    Group(SharedString),
110    /// Model edge indices of the selected visual edge.
111    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    /// Super node body while collapsed.
126    body: Body,
127    /// Members' centroid when collapsed; expanding moves them by the super node's travel.
128    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
142/// An interactive, force-directed network graph.
143///
144/// ```ignore
145/// let graph = cx.new(|cx| {
146///     NetworkGraph::new(cx)
147///         .with_data(GraphData::new(nodes, edges))
148///         .with_label_mode(LabelMode::Auto)
149/// });
150/// cx.subscribe(&graph, |this, _, event: &NetworkGraphEvent, cx| { /* ... */ }).detach();
151/// ```
152///
153/// Place it in a sized container: it fills its parent.
154pub struct NetworkGraph {
155    // Model: every node and edge, with persisted positions.
156    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    // What is drawn and simulated.
168    vis: Visual,
169    sim: Simulation,
170
171    /// Screen position = bounds.center + pan + world * zoom.
172    pan: Point<Pixels>,
173    zoom: f32,
174    /// Keep fitting the view while the layout settles, until the user moves it.
175    auto_fit: bool,
176    bounds: Rc<Cell<Bounds<Pixels>>>,
177
178    hovered: Option<Item>,
179    /// The pointer moved since `hovered` was last hit-tested.
180    hover_stale: bool,
181    selected: Option<SelectionKey>,
182    drag: Option<Drag>,
183    pointer: Option<Point<Pixels>>,
184    focus_handle: FocusHandle,
185    /// The last interaction came from the keyboard: show focus indicators.
186    keyboard_nav: bool,
187    /// Vertices whose label was drawn last frame; they win placement ties.
188    shown_labels: RefCell<HashSet<usize>>,
189
190    label_mode: LabelMode,
191    color_by: ColorBy,
192    arrows: bool,
193    edge_labels: bool,
194    /// Whether items outside the hovered / selected one's neighborhood fade.
195    focus_dimming: bool,
196    group_hulls: bool,
197    /// Dragged nodes stay where they are dropped.
198    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    // ---- construction options -----------------------------------------------
257
258    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    /// Draw direction arrows on edges (default on).
279    pub fn arrows(mut self, arrows: bool) -> Self {
280        self.arrows = arrows;
281        self
282    }
283
284    /// Draw relationship labels on edges (default on).
285    pub fn edge_labels(mut self, edge_labels: bool) -> Self {
286        self.edge_labels = edge_labels;
287        self
288    }
289
290    /// Outline each expanded group with a rounded hull (default on).
291    pub fn group_hulls(mut self, group_hulls: bool) -> Self {
292        self.group_hulls = group_hulls;
293        self
294    }
295
296    /// Keep dragged nodes pinned where they are dropped (default off).
297    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    /// Fixed colors instead of following the gpui-base theme.
313    pub fn with_colors(mut self, colors: GraphColors) -> Self {
314        self.colors = Some(colors);
315        self
316    }
317
318    /// Replace the built-in tooltip. Return `None` from the builder to hide it.
319    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    /// Show tooltips for the hovered item (default on).
329    pub fn tooltips(mut self, tooltips: bool) -> Self {
330        self.tooltips = tooltips;
331        self
332    }
333
334    // ---- runtime settings ---------------------------------------------------
335
336    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    /// Whether hovering or selecting an item fades everything outside its
368    /// neighborhood. Off, the item and its edges are still emphasized, and
369    /// every other node stays as readable — and as easy to aim at — as before.
370    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    // ---- data -------------------------------------------------------------
424
425    /// Replace the whole graph (a new query result) and reset the viewport.
426    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    /// Add nodes and edges to the current graph, e.g. expanding a node. Existing
436    /// ids are kept; new nodes start around `anchor` so the layout stays stable.
437    /// New members of a collapsed group join it without expanding it.
438    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    /// Remove nodes and every edge touching them.
445    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        // Model edge indices shifted; an edge selection cannot be carried over.
482        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    /// The current graph contents (including nodes inside collapsed groups).
496    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    /// Requires the model to be in sync with the visual graph (see `sync_back`).
522    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    /// Persist simulated positions from the visual graph into the model.
571    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    /// Recompute model topology and groups, then the visual graph.
582    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        // Groups keep their state (collapsed, position, label) across rebuilds.
605        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        // Vertex indices changed.
654        self.shown_labels.take();
655    }
656
657    // ---- queries ------------------------------------------------------------
658
659    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    /// Edges touching a node, in either direction.
688    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    /// Member nodes of a group.
720    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    /// The colors in effect: the override, or the current theme.
734    pub fn resolved_colors(&self, cx: &App) -> GraphColors {
735        self.colors
736            .clone()
737            .unwrap_or_else(|| GraphColors::from_theme(cx))
738    }
739
740    /// The color a node is painted with.
741    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    /// Whether the layout is still moving.
780    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    /// The selection as a visual item, if it is currently drawn.
830    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    /// Drop a selection whose target no longer exists, notifying listeners.
849    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    // ---- selection & viewport ---------------------------------------------
857
858    /// Select a node by id (`None` clears), optionally centering it.
859    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    /// Pin or release a node at its current position.
884    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    // ---- groups -------------------------------------------------------------
917
918    /// Collapse a group into a single super node.
919    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    /// Community overview: every group becomes one node.
935    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        // A selected super node no longer exists once expanded.
999        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    // ---- viewport -----------------------------------------------------------
1017
1018    /// Fit everything into view and keep fitting while the layout settles.
1019    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    /// Restart the layout from scratch (d3 `simulation.alpha(1).restart()`).
1033    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        // Topmost first: vertices are painted in index order, above edges.
1094        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    // ---- keyboard -----------------------------------------------------------
1121
1122    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    /// Move the selection to the nearest vertex in a direction (spatial navigation).
1178    /// With nothing selected, start from the vertex nearest the viewport center.
1179    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                        // Within ±60° of the direction; prefer straight ahead.
1207                        (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    /// Pan just enough to bring a vertex into view.
1221    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    // ---- input --------------------------------------------------------------
1240
1241    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        // Hit-testing every edge takes about half a millisecond on a large
1314        // graph, and a high-rate mouse sends several moves per frame: test
1315        // once per frame, in `render`, at the latest position. Re-render
1316        // while hovering, too, so the tooltip follows the pointer.
1317        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    /// A trackpad's two-finger drag pans, as in maps and design tools, and a
1346    /// pinch zooms (`on_pinch`); a mouse wheel, or any scroll with ⌘ or Ctrl
1347    /// held, zooms around the pointer.
1348    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                // Settle off-screen instead of animating the layout.
1380                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                // Small graphs can afford two ticks per frame and settle twice as fast.
1394                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
1455/// Mouse handling is registered at the window level during paint so drags keep
1456/// tracking outside the graph's bounds.
1457fn 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    // Pointing hand is reserved for links; hover state is shown by the halo.
1465    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}