Skip to main content

egui_graph/
lib.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::hash::Hash;
3use std::sync::{Arc, Mutex};
4
5#[cfg(feature = "layout")]
6pub use layout::layout;
7pub use node::{FramedResponse, NodeCtx, NodeId, NodeInteraction};
8pub use socket::layout::{grid::SocketGrid, SocketLayout};
9pub use socket::{SocketKind, SocketResponses};
10
11pub mod bezier;
12pub mod edge;
13#[cfg(feature = "layout")]
14pub mod layout;
15pub mod node;
16pub mod socket;
17
18/// The main interface for the `Graph` widget.
19pub struct Graph {
20    background: bool,
21    dot_grid: bool,
22    zoom_range: egui::Rangef,
23    max_inner_size: Option<egui::Vec2>,
24    center_view: bool,
25    id: egui::Id,
26    /// If set, overwrite the graph's selected nodes at the start of the frame.
27    selected_nodes: Option<HashSet<NodeId>>,
28    /// When `true`, prevents structural changes while preserving navigation and
29    /// selection.
30    ///
31    /// Unlike `Ui::set_enabled(false)` which disables all interaction
32    /// (including panning, zooming, and selection), `immutable` only prevents
33    /// structural changes - node positions, edges, and node content remain
34    /// view-only while navigation and selection continue to work.
35    immutable: bool,
36}
37
38/// State related to the graph UI.
39#[derive(Clone, Default)]
40pub struct GraphTempMemory {
41    /// The most recently recorded size of each node.
42    ///
43    /// Primarily used to check for node selection, as we don't know the size of the node until the
44    /// contents have been instantiated.
45    node_sizes: NodeSizes,
46    /// The currently selected nodes and edges.
47    selection: Selection,
48    /// Whether or not the primary button was pressed on the graph area and is still down.
49    ///
50    /// Used for tracking selection and dragging.
51    pressed: Option<Pressed>,
52    /// Collect information about the layout of each node's sockets during node instantiation.
53    ///
54    /// This is used to provide the position and normal of each socket when instantiating edges.
55    sockets: HashMap<NodeId, NodeSockets>,
56    /// The socket that is currently closest to the mouse.
57    ///
58    /// Always `Some` while the pointer is over the graph area, `None` otherwise.
59    closest_socket: Option<socket::Socket>,
60}
61
62type NodeSizes = HashMap<NodeId, egui::Vec2>;
63
64#[derive(Clone, Default)]
65struct Selection {
66    /// The set of currently selected nodes.
67    nodes: HashSet<NodeId>,
68    /// Whether the selection was modified this frame.
69    changed: bool,
70}
71
72/// State related to the last press of the primary pointer button over the graph.
73#[derive(Clone, Debug)]
74struct Pressed {
75    /// Whether or not the pointer is currently over one of the selected nodes.
76    ///
77    /// This is used to assist with determining whether or not nodes should deselect. E.g. if
78    /// multiple nodes are selected and a non-selected node is pressed, then we should deselect the
79    /// originally selected nodes. However, if a selected node is pressed, then the selection
80    /// should stay the same and a drag will begin.
81    over_selection_at_origin: bool,
82    /// The origin of the pointer over the graph at the begining of the press.
83    origin_pos: egui::Pos2,
84    /// The current position over the graph.
85    current_pos: egui::Pos2,
86    /// The action performed by this press.
87    action: PressAction,
88}
89
90#[derive(Clone, Debug)]
91enum PressAction {
92    /// A node was pressed and a drag is taking place.
93    DragNodes {
94        /// The node that was pressed to initiate the drag.
95        ///
96        /// We don't know exactly which until the node itself emits the pressed event, so this
97        /// remains `None` until we know.
98        node: Option<PressedNode>,
99    },
100    /// The graph was pressed and we are performing a selection.
101    Select,
102    /// A node's socket was pressed in order to start creating a connection.
103    Socket(socket::Socket),
104}
105
106#[derive(Clone, Debug)]
107struct PressedNode {
108    /// Unique Id of the node.
109    id: NodeId,
110    /// The position of the node over the graph at the origin of the press.
111    position_at_origin: egui::Pos2,
112}
113
114/// Configuration for the graph.
115// TODO: Consider storing this in graph widget "memory"?
116// The thing is, it might be nice to let the user modify these externally.
117#[derive(Debug, Clone, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
119pub struct View {
120    /// The visible area of the graph's [`Scene`][egui::containers::Scene].
121    pub scene_rect: egui::Rect,
122    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_sorted_layout"))]
123    pub layout: Layout,
124}
125
126#[cfg(feature = "serde")]
127fn serialize_sorted_layout<S: serde::Serializer>(layout: &Layout, s: S) -> Result<S::Ok, S::Error> {
128    use serde::Serialize;
129    let sorted: BTreeMap<_, _> = layout.iter().collect();
130    sorted.serialize(s)
131}
132
133/// The location of the top-left of each node relative to the centre of the graph area.
134pub type Layout = HashMap<NodeId, egui::Pos2>;
135
136/// The context returned by the `Graph` widget. Allows for setting nodes and edges.
137pub struct Show<'a> {
138    /// Useful for accessing the `GraphTempMemory`.
139    graph_id: egui::Id,
140    /// The full area covered by the `Graph` within the UI.
141    graph_rect: egui::Rect,
142    /// If a selection is being performed with the pointer, this is the covered area.
143    selection_rect: Option<egui::Rect>,
144    /// Whether or not the primary mouse button was just released to perform the selection.
145    select: bool,
146    /// The closest socket within pressable range of the pointer.
147    closest_socket: Option<socket::Socket>,
148    /// Whether or not the primary mouse button was just released to end edge creation.
149    socket_press_released: Option<socket::Socket>,
150    /// Track all nodes that were visited this update.
151    ///
152    /// We will use this to remove old node state on `drop`.
153    visited: &'a mut HashSet<NodeId>,
154    layout: &'a mut Layout,
155    /// Whether the graph is in immutable (view-only) mode.
156    immutable: bool,
157}
158
159/// Information about the inputs and outputs for a particular node.
160#[derive(Clone)]
161pub struct NodeSockets {
162    flow: egui::Direction,
163    inputs: BTreeMap<usize, egui::Pos2>,
164    outputs: BTreeMap<usize, egui::Pos2>,
165}
166
167/// A context to assist with the instantiation of node widgets.
168pub struct NodesCtx<'a> {
169    pub graph_id: egui::Id,
170    graph_rect: egui::Rect,
171    selection_rect: Option<egui::Rect>,
172    select: bool,
173    socket_press_released: Option<socket::Socket>,
174    visited: &'a mut HashSet<NodeId>,
175    layout: &'a mut Layout,
176    /// Whether the graph is in immutable (view-only) mode.
177    pub immutable: bool,
178}
179
180/// A context to assist with the instantiation of edge widgets.
181pub struct EdgesCtx {
182    graph_id: egui::Id,
183    graph_rect: egui::Rect,
184    selection_rect: Option<egui::Rect>,
185    closest_socket: Option<socket::Socket>,
186    /// Whether the graph is in immutable (view-only) mode.
187    pub immutable: bool,
188}
189
190/// The set of detected graph interaction for a single graph widget update prior
191/// to node interaction.
192struct GraphInteraction {
193    pressed: Option<Pressed>,
194    socket_press_released: Option<socket::Socket>,
195    select: bool,
196    selection_rect: Option<egui::Rect>,
197    drag_nodes_delta: egui::Vec2,
198}
199
200/// The response returned by [`Graph::show`].
201pub struct GraphResponse<R> {
202    /// The user's return value from the content closure.
203    pub inner: R,
204    /// The egui [`Response`][egui::Response] for the graph's scene area.
205    pub response: egui::Response,
206    /// The set of selected nodes, present only when the selection changed this frame.
207    pub selection_changed: Option<HashSet<NodeId>>,
208}
209
210impl Graph {
211    /// The default zoom range.
212    ///
213    /// Allows zooming out 4x, but does not allow zooming in past the
214    /// pixel-perfect default level.
215    pub const DEFAULT_ZOOM_RANGE: egui::Rangef = egui::Rangef {
216        min: 0.25,
217        max: 1.0,
218    };
219    pub const DEFAULT_CENTER_VIEW: bool = false;
220
221    /// Begin building the new graph widget.
222    pub fn new(id_src: impl Hash) -> Self {
223        Self::from_id(id(id_src))
224    }
225
226    /// The same as [`Graph::new`], but allows providing an `egui::Id` directly.
227    pub fn from_id(id: egui::Id) -> Self {
228        Self {
229            background: true,
230            dot_grid: true,
231            zoom_range: Self::DEFAULT_ZOOM_RANGE,
232            max_inner_size: None,
233            center_view: Self::DEFAULT_CENTER_VIEW,
234            id,
235            selected_nodes: None,
236            immutable: false,
237        }
238    }
239
240    /// Whether or not to fill the background. Default is `true`.
241    pub fn background(mut self, show: bool) -> Self {
242        self.background = show;
243        self
244    }
245
246    /// Whether or not to show the dot grid. Default is `true`.
247    pub fn dot_grid(mut self, show: bool) -> Self {
248        self.dot_grid = show;
249        self
250    }
251
252    /// Set the allowed zoom range.
253    ///
254    /// A zoom < 1.0 zooms out, while a zoom > 1.0 zooms in.
255    ///
256    /// Default: [Graph::DEFAULT_ZOOM_RANGE].
257    pub fn zoom_range(mut self, zoom_range: impl Into<egui::Rangef>) -> Self {
258        self.zoom_range = zoom_range.into();
259        self
260    }
261
262    /// Set the maximum size of the scene's inner [`Ui`] that will be created.
263    #[inline]
264    pub fn max_inner_size(mut self, max_inner_size: impl Into<egui::Vec2>) -> Self {
265        self.max_inner_size = Some(max_inner_size.into());
266        self
267    }
268
269    /// Whether or not to center the view around the content of the graph.
270    ///
271    /// Default: [Self::DEFAULT_CENTER_VIEW].
272    pub fn center_view(mut self, center_view: bool) -> Self {
273        self.center_view = center_view;
274        self
275    }
276
277    /// Set the selected nodes for this frame.
278    ///
279    /// This overwrites the current selection in the graph's temporary memory
280    /// at the start of the next `show` call.
281    pub fn selected_nodes(mut self, nodes: HashSet<NodeId>) -> Self {
282        self.selected_nodes = Some(nodes);
283        self
284    }
285
286    /// Set immutable (view-only) mode.
287    ///
288    /// When `true`, prevents structural changes while preserving navigation
289    /// and selection. Node dragging, edge creation/deletion, node deletion,
290    /// and node content widgets are all disabled.
291    ///
292    /// Default: `false`.
293    pub fn immutable(mut self, immutable: bool) -> Self {
294        self.immutable = immutable;
295        self
296    }
297
298    /// Begin showing the Graph.
299    ///
300    /// Returns a [`GraphResponse`] containing the user's return value,
301    /// the scene [`egui::Response`], and the current set of selected nodes.
302    pub fn show<R>(
303        mut self,
304        view: &mut View,
305        ui: &mut egui::Ui,
306        content: impl FnOnce(&mut egui::Ui, Show) -> R,
307    ) -> GraphResponse<R> {
308        // The full area to be occuppied by the graph.
309        let graph_rect = ui.available_rect_before_wrap();
310
311        let View {
312            ref mut scene_rect,
313            ref mut layout,
314        } = *view;
315
316        // Create the Scene.
317        let mut scene = egui::containers::Scene::new()
318            .zoom_range(self.zoom_range.clone())
319            .drag_pan_buttons(egui::containers::DragPanButtons::MIDDLE);
320        if let Some(max_inner_size) = self.max_inner_size {
321            scene = scene.max_inner_size(max_inner_size);
322        }
323
324        // Track the bounding area of all widgets in the scene.
325        let mut bounding_rect = None;
326
327        let scene_response = scene.show(ui, scene_rect, |ui| {
328            // Draw the selection rectangle if there is one.
329            let mut selection_rect = None;
330            let mut select = false;
331            let mut closest_socket = None;
332            let mut socket_press_released = None;
333
334            // Check for interactions with the scene area.
335            let scene_response = ui.response();
336            let ptr_on_graph = scene_response.hovered();
337
338            // Check for selection rectangle and node dragging.
339            let gmem_arc = memory(ui, self.id);
340            let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
341
342            // Apply externally-provided selection if set.
343            if let Some(nodes) = self.selected_nodes.take() {
344                gmem.selection.nodes = nodes;
345            }
346
347            // Reset the selection dirty flag for this frame.
348            gmem.selection.changed = false;
349
350            // FIXME: Here we grab the global pointer and transform its position
351            // to the graph scene space in order to check for initialising node
352            // drag events. However, doing this means we run the risk of
353            // incorrectly responding to events that should be captured by
354            // widgets floating above (like a window floating above the graph).
355            // We should change this to get the pointer only if it is hovered or
356            // interacting with the scene or any of its child nodes somehow.
357            let pointer = ui.input(|i| i.pointer.clone());
358            if let Some(ptr_global) = pointer.interact_pos().or(pointer.hover_pos()) {
359                let ptr_graph = ui
360                    .ctx()
361                    .layer_transform_from_global(ui.layer_id())
362                    .unwrap_or_default()
363                    .mul_pos(ptr_global);
364
365                // Check for the closest socket.
366                closest_socket = ui.response().hover_pos().and_then(|pos| {
367                    find_closest_socket(pos, layout, &gmem, ui).map(|(socket, _dist_sqrd)| socket)
368                });
369
370                // When immutable, suppress socket presses (map to Select).
371                let closest_socket_for_interaction =
372                    if self.immutable { None } else { closest_socket };
373
374                // Check for graph interactions.
375                let interaction = graph_interaction(
376                    layout,
377                    &pointer,
378                    closest_socket_for_interaction,
379                    ptr_on_graph,
380                    ptr_graph,
381                    gmem.pressed.as_ref(),
382                );
383
384                // Apply drag delta to all selected nodes (skip when immutable).
385                if !self.immutable && interaction.drag_nodes_delta != egui::Vec2::ZERO {
386                    if let Some(pressed) = gmem.pressed.as_ref() {
387                        if let PressAction::DragNodes { .. } = pressed.action {
388                            for &n_id in &gmem.selection.nodes {
389                                if let Some(pos) = layout.get_mut(&n_id) {
390                                    *pos += interaction.drag_nodes_delta;
391                                }
392                            }
393                        }
394                    }
395                }
396
397                gmem.pressed = interaction.pressed;
398                gmem.closest_socket = closest_socket;
399                selection_rect = interaction.selection_rect;
400                select = interaction.select;
401                socket_press_released = interaction.socket_press_released;
402            }
403
404            // Paint the background rect.
405            let visible_rect = ui.clip_rect();
406            if self.background {
407                paint_background(visible_rect, ui);
408            }
409
410            // Paint some subtle dots to check camera movement.
411            if self.dot_grid {
412                paint_dot_grid(visible_rect, ui);
413            }
414
415            // Draw the selection area if there is one.
416            // TODO: Do this when `Show` is `drop`ped or finalised.
417            if let Some(sel_rect) = selection_rect {
418                paint_selection_area(sel_rect, ui);
419            }
420
421            let mut visited = HashSet::default();
422
423            let show = Show {
424                graph_id: self.id,
425                graph_rect,
426                selection_rect,
427                select,
428                closest_socket,
429                socket_press_released,
430                visited: &mut visited,
431                layout,
432                immutable: self.immutable,
433            };
434
435            // Drop the lock before running the content.
436            std::mem::drop(gmem);
437
438            let output = content(ui, show);
439
440            prune_unused_nodes(self.id, &visited, ui);
441            bounding_rect = Some(ui.min_rect());
442
443            // Snapshot selection only if it changed this frame.
444            let gmem_arc = memory(ui, self.id);
445            let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
446            let selection_changed = if gmem.selection.changed {
447                Some(gmem.selection.nodes.clone())
448            } else {
449                None
450            };
451
452            (output, selection_changed)
453        });
454
455        if self.center_view {
456            if let Some(rect) = bounding_rect {
457                view.scene_rect = rect.expand(rect.width() * 0.1);
458            }
459        }
460
461        let (inner, selection_changed) = scene_response.inner;
462        GraphResponse {
463            inner,
464            response: scene_response.response,
465            selection_changed,
466        }
467    }
468}
469
470impl GraphTempMemory {
471    /// Get the recorded sizes of all nodes.
472    pub fn node_sizes(&self) -> &NodeSizes {
473        &self.node_sizes
474    }
475}
476
477impl NodeSockets {
478    /// The screen position and normal of the input at the given index.
479    ///
480    /// Returns `None` if there is no input at the given index.
481    pub fn input(&self, ix: usize) -> Option<(egui::Pos2, egui::Vec2)> {
482        self.inputs
483            .get(&ix)
484            .map(|&pos| (pos, input_normal(self.flow)))
485    }
486
487    /// The screen position and normal of the output at the given index.
488    ///
489    /// Returns `None` if there is no output at the given index.
490    pub fn output(&self, ix: usize) -> Option<(egui::Pos2, egui::Vec2)> {
491        self.outputs
492            .get(&ix)
493            .map(|&pos| (pos, output_normal(self.flow)))
494    }
495
496    /// Produces an iterator yielding the index, position, and normal for each input.
497    pub fn inputs(&self) -> impl Iterator<Item = (usize, egui::Pos2, egui::Vec2)> + '_ {
498        let norm = input_normal(self.flow);
499        self.inputs.iter().map(move |(&ix, &pos)| (ix, pos, norm))
500    }
501
502    /// Produces an iterator yielding the index, position, and normal for each output.
503    pub fn outputs(&self) -> impl Iterator<Item = (usize, egui::Pos2, egui::Vec2)> + '_ {
504        let norm = output_normal(self.flow);
505        self.outputs.iter().map(move |(&ix, &pos)| (ix, pos, norm))
506    }
507}
508
509fn input_normal(flow: egui::Direction) -> egui::Vec2 {
510    match flow {
511        egui::Direction::LeftToRight => egui::Vec2::new(-1.0, 0.0),
512        egui::Direction::RightToLeft => egui::Vec2::new(1.0, 0.0),
513        egui::Direction::TopDown => egui::Vec2::new(0.0, -1.0),
514        egui::Direction::BottomUp => egui::Vec2::new(0.0, 1.0),
515    }
516}
517
518fn output_normal(flow: egui::Direction) -> egui::Vec2 {
519    match flow {
520        egui::Direction::LeftToRight => egui::Vec2::new(1.0, 0.0),
521        egui::Direction::RightToLeft => egui::Vec2::new(-1.0, 0.0),
522        egui::Direction::TopDown => egui::Vec2::new(0.0, 1.0),
523        egui::Direction::BottomUp => egui::Vec2::new(0.0, -1.0),
524    }
525}
526
527impl<'a> Show<'a> {
528    /// Instantiate the nodes of the graph.
529    pub fn nodes(
530        mut self,
531        ui: &mut egui::Ui,
532        content: impl FnOnce(&mut NodesCtx, &mut egui::Ui),
533    ) -> Self {
534        {
535            let Self {
536                graph_id,
537                graph_rect,
538                selection_rect,
539                select,
540                socket_press_released,
541                ref mut visited,
542                ref mut layout,
543                immutable,
544                ..
545            } = self;
546            let mut ctx = NodesCtx {
547                graph_id,
548                graph_rect,
549                selection_rect,
550                select,
551                socket_press_released,
552                visited: &mut *visited,
553                layout: &mut *layout,
554                immutable,
555            };
556            content(&mut ctx, ui);
557        }
558        self
559    }
560
561    /// Instantiate the edges of the graph.
562    pub fn edges(
563        self,
564        ui: &mut egui::Ui,
565        content: impl FnOnce(&mut EdgesCtx, &mut egui::Ui),
566    ) -> Self {
567        {
568            let Self {
569                graph_rect,
570                graph_id,
571                selection_rect,
572                closest_socket,
573                immutable,
574                ..
575            } = self;
576            let mut ctx = EdgesCtx {
577                graph_id,
578                graph_rect,
579                selection_rect,
580                closest_socket,
581                immutable,
582            };
583            content(&mut ctx, ui);
584        }
585        self
586    }
587}
588
589/// If a node didn't appear this update, it's likely because the user has
590/// removed the node from their graph, so we should stop tracking it.
591fn prune_unused_nodes(graph_id: egui::Id, visited: &HashSet<NodeId>, ui: &mut egui::Ui) {
592    let gmem_arc = memory(ui, graph_id);
593    let mut gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
594    gmem.node_sizes.retain(|k, _| visited.contains(k));
595    gmem.selection.nodes.retain(|k| visited.contains(k));
596    if let Some(socket) = gmem.closest_socket.as_ref() {
597        if !visited.contains(&socket.node) {
598            gmem.closest_socket = None;
599        }
600    }
601    if let Some(pressed) = gmem.pressed.as_ref() {
602        match pressed.action {
603            PressAction::DragNodes {
604                node: Some(PressedNode { id: n, .. }),
605            }
606            | PressAction::Socket(socket::Socket { node: n, .. })
607                if !visited.contains(&n) =>
608            {
609                gmem.pressed = None
610            }
611            _ => (),
612        }
613    }
614}
615
616impl EdgesCtx {
617    /// Retrieves the position and normal of the specified input for the given node.
618    ///
619    /// Returns `None` if either the `node` or `input` do not exist.
620    pub fn input(
621        &self,
622        ui: &egui::Ui,
623        node: NodeId,
624        input: usize,
625    ) -> Option<(egui::Pos2, egui::Vec2)> {
626        let gmem_arc = crate::memory(ui, self.graph_id);
627        let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
628        gmem.sockets
629            .get(&node)
630            .and_then(|sockets| sockets.input(input))
631    }
632
633    /// Retrieves the position and normal of the specified output for the given node.
634    ///
635    /// Returns `None` if either the `node` or `output` do not exist.
636    pub fn output(
637        &self,
638        ui: &egui::Ui,
639        node: NodeId,
640        output: usize,
641    ) -> Option<(egui::Pos2, egui::Vec2)> {
642        let gmem_arc = memory(ui, self.graph_id);
643        let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
644        gmem.sockets
645            .get(&node)
646            .and_then(|sockets| sockets.output(output))
647    }
648
649    /// If the user is in the progress of creating an edge, this returns the relevant info.
650    pub fn in_progress(&self, ui: &egui::Ui) -> Option<EdgeInProgress> {
651        let gmem_arc = memory(ui, self.graph_id);
652        let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
653        let pressed = gmem.pressed.as_ref()?;
654        let start = match pressed.action {
655            PressAction::Socket(socket) => {
656                let sockets = gmem.sockets.get(&socket.node)?;
657                let (pos, normal) = match socket.kind {
658                    socket::SocketKind::Input => sockets.input(socket.index)?,
659                    socket::SocketKind::Output => sockets.output(socket.index)?,
660                };
661                socket::PositionedSocket {
662                    socket,
663                    pos,
664                    normal,
665                }
666            }
667            _ => return None,
668        };
669        let (end_pos, end_socket) = match gmem.closest_socket {
670            Some(socket) if socket.kind != start.socket.kind => {
671                let sockets = gmem.sockets.get(&socket.node)?;
672                let (pos, normal) = match socket.kind {
673                    socket::SocketKind::Input => sockets.input(socket.index)?,
674                    socket::SocketKind::Output => sockets.output(socket.index)?,
675                };
676                (pos, Some((socket.kind, normal)))
677            }
678            _ => (pressed.current_pos, None),
679        };
680        Some(EdgeInProgress {
681            start,
682            end_pos,
683            end_socket,
684        })
685    }
686
687    /// The full rect occuppied by the graph widget.
688    pub fn graph_rect(&self) -> egui::Rect {
689        self.graph_rect
690    }
691}
692
693pub struct EdgeInProgress {
694    /// The socket at the start end of the edge.
695    pub start: socket::PositionedSocket,
696    /// The end position of the edge in progress.
697    ///
698    /// If there is no socket within the interaction radius, this will be the pointer position.
699    /// Otherwise, this will be the position of the closest socket who's `SocketKind` is opposite
700    /// to `start.kind`.
701    pub end_pos: egui::Pos2,
702    /// The closest socket who's `SocketKind` is opposite to `start.kind`.
703    ///
704    /// This is `None` in the case that there are no sockets within the interaction radius.
705    pub end_socket: Option<(socket::SocketKind, egui::Vec2)>,
706}
707
708impl EdgeInProgress {
709    pub fn bezier_cubic(&self) -> bezier::Cubic {
710        let start = (self.start.pos, self.start.normal);
711        let end_normal = self
712            .end_socket
713            .as_ref()
714            .map(|&(_, n)| n)
715            .unwrap_or(-self.start.normal);
716        let end = (self.end_pos, end_normal);
717        bezier::Cubic::from_edge_points(start, end)
718    }
719
720    /// Short-hand for painting the in-progress edge with some reasonable defaults.
721    ///
722    /// If you require custom styling of the in-progress edge, use
723    /// [`EdgeInProgress::bezier_cubic`] or the individual fields to paint it
724    /// however you wish.
725    pub fn show(&self, ui: &egui::Ui) {
726        let dist_per_pt = crate::edge::Edge::DEFAULT_DISTANCE_PER_POINT;
727        let bezier = self.bezier_cubic();
728        let pts = bezier.flatten(dist_per_pt).collect();
729        let stroke = ui.visuals().widgets.active.fg_stroke;
730        ui.painter().add(egui::Shape::line(pts, stroke));
731    }
732}
733
734impl Default for View {
735    fn default() -> Self {
736        Self {
737            scene_rect: egui::Rect::ZERO,
738            layout: Default::default(),
739        }
740    }
741}
742
743/// Find the socket that is closest to the given point.
744///
745/// Returns the socket alongside the squared distance from the socket.
746fn find_closest_socket(
747    pos_graph: egui::Pos2,
748    layout: &Layout,
749    gmem: &GraphTempMemory,
750    ui: &egui::Ui,
751) -> Option<(socket::Socket, f32)> {
752    // TODO: if we wanted to be super efficient, we could maintain a quadtree of
753    // nodes and sockets...
754    let mut closest_socket = None;
755    let socket_radius = ui
756        .spacing()
757        .interact_size
758        .x
759        .min(ui.spacing().interact_size.y);
760    let visible_rect = ui.clip_rect();
761    let socket_radius_sq = socket_radius * socket_radius;
762    for (&n_id, &n_graph) in layout {
763        // Only check visible nodes.
764        let n_screen = n_graph;
765        let size = match gmem.node_sizes.get(&n_id) {
766            None => continue,
767            Some(&size) => size,
768        };
769        let rect = egui::Rect::from_min_size(n_screen, size);
770        if !visible_rect.intersects(rect) {
771            continue;
772        }
773        let sockets = match gmem.sockets.get(&n_id) {
774            None => continue,
775            Some(sockets) => sockets,
776        };
777
778        // Check inputs.
779        for (ix, p, _) in sockets.inputs() {
780            let dist_sq = pos_graph.distance_sq(p);
781            if dist_sq < socket_radius_sq {
782                let socket = socket::Socket {
783                    node: n_id,
784                    kind: socket::SocketKind::Input,
785                    index: ix,
786                };
787                closest_socket = match closest_socket {
788                    None => Some((socket, dist_sq)),
789                    Some((_, d_sq)) if dist_sq < d_sq => Some((socket, dist_sq)),
790                    _ => closest_socket,
791                }
792            }
793        }
794
795        // Check outputs.
796        for (ix, p, _) in sockets.outputs() {
797            let dist_sq = pos_graph.distance_sq(p);
798            if dist_sq < socket_radius_sq {
799                let socket = socket::Socket {
800                    node: n_id,
801                    kind: socket::SocketKind::Output,
802                    index: ix,
803                };
804                closest_socket = match closest_socket {
805                    None => Some((socket, dist_sq)),
806                    Some((_, d_sq)) if dist_sq < d_sq => Some((socket, dist_sq)),
807                    _ => closest_socket,
808                }
809            }
810        }
811    }
812
813    closest_socket
814}
815
816/// Interpret some basic interactions from the state of the graph and recent input.
817fn graph_interaction(
818    layout: &Layout,
819    pointer: &egui::PointerState,
820    closest_socket: Option<socket::Socket>,
821    ptr_on_graph: bool,
822    ptr_graph: egui::Pos2,
823    pressed: Option<&Pressed>,
824) -> GraphInteraction {
825    let mut select = false;
826    let mut socket_press_released = None;
827    let mut drag_nodes_delta = egui::Vec2::ZERO;
828    let mut selection_rect = None;
829
830    // Check for selecting/dragging.
831    let pressed: Option<Pressed> = if let Some(pressed) = pressed {
832        match pressed.action {
833            PressAction::DragNodes {
834                node: Some(ref node),
835            } => {
836                // Determine the drag delta.
837                let delta = ptr_graph - pressed.origin_pos;
838                let target = node.position_at_origin + delta;
839                if let Some(current) = layout.get(&node.id) {
840                    drag_nodes_delta = target - *current;
841                }
842            }
843            PressAction::Select => {
844                let min = pressed.origin_pos;
845                let max = ptr_graph;
846                selection_rect = Some(egui::Rect::from_two_pos(min, max));
847            }
848            _ => (),
849        }
850
851        // The press action has ended.
852        if pointer.primary_released() {
853            match pressed.action {
854                PressAction::Select => select = true,
855                PressAction::Socket(socket) => socket_press_released = Some(socket),
856                _ => (),
857            }
858            None
859        } else {
860            Some(Pressed {
861                current_pos: ptr_graph,
862                ..pressed.clone()
863            })
864        }
865    // Check for the beginning of a socket press or rectangular selection.
866    } else if ptr_on_graph
867        && pointer.button_down(egui::PointerButton::Primary)
868        && pointer.button_pressed(egui::PointerButton::Primary)
869    {
870        // Choose which press action based on whether or not a socket was pressed.
871        let action = match closest_socket {
872            Some(socket) => PressAction::Socket(socket),
873            None => {
874                let min = ptr_graph;
875                let max = ptr_graph;
876                selection_rect = Some(egui::Rect::from_two_pos(min, max));
877                PressAction::Select
878            }
879        };
880
881        let pressed = Pressed {
882            over_selection_at_origin: false,
883            origin_pos: ptr_graph,
884            current_pos: ptr_graph,
885            action,
886        };
887        Some(pressed)
888
889    // Otherwise, pass through existing state.
890    } else {
891        pressed.cloned()
892    };
893
894    GraphInteraction {
895        pressed,
896        socket_press_released,
897        select,
898        selection_rect,
899        drag_nodes_delta,
900    }
901}
902
903// Paint a subtle dot grid to check camera movement.
904fn paint_dot_grid(visible_rect: egui::Rect, ui: &mut egui::Ui) {
905    let dot_step = ui.spacing().interact_size.y;
906    let vis = ui.style().noninteractive();
907    let x_dots = (visible_rect.min.x / dot_step) as i32..=(visible_rect.max.x / dot_step) as i32;
908    let y_dots = (visible_rect.min.y / dot_step) as i32..=(visible_rect.max.y / dot_step) as i32;
909    for x_dot in x_dots {
910        for y_dot in y_dots.clone() {
911            let x = x_dot as f32 * dot_step;
912            let y = y_dot as f32 * dot_step;
913            let r = egui::Rect::from_center_size([x, y].into(), [1.0; 2].into());
914            let color = vis.bg_stroke.color;
915            ui.painter().circle_filled(r.center(), 0.5, color);
916        }
917    }
918}
919
920// Paint the background rect.
921fn paint_background(visible_rect: egui::Rect, ui: &mut egui::Ui) {
922    let vis = ui.style().noninteractive();
923    let stroke = egui::Stroke {
924        width: 0.0,
925        ..vis.bg_stroke
926    };
927    let fill = vis.bg_fill;
928    ui.painter()
929        .rect(visible_rect, 0.0, fill, stroke, egui::StrokeKind::Inside);
930}
931
932/// Paint the selection area rectangle.
933fn paint_selection_area(sel_rect: egui::Rect, ui: &mut egui::Ui) {
934    let color = ui.visuals().weak_text_color();
935    let fill = color.linear_multiply(0.125);
936    let width = 1.0;
937    let stroke = egui::Stroke { width, color };
938    ui.painter()
939        .rect(sel_rect, 0.0, fill, stroke, egui::StrokeKind::Inside);
940}
941
942/// Combines the given id src with the `TypeId` of the `Graph` to produce a unique `egui::Id`.
943pub fn id(id_src: impl Hash) -> egui::Id {
944    egui::Id::new((std::any::TypeId::of::<Graph>(), id_src))
945}
946
947/// Access the graph's temporary memory for the given graph ID.
948///
949/// This allows reading graph state like node sizes without cloning.
950/// If no memory exists for the graph ID, a default GraphTempMemory is created and stored.
951pub fn with_graph_memory<R>(
952    ctx: &egui::Context,
953    graph_id: egui::Id,
954    f: impl FnOnce(&GraphTempMemory) -> R,
955) -> R {
956    let gmem_arc = ctx.data_mut(|d| {
957        d.get_temp_mut_or_default::<Arc<Mutex<GraphTempMemory>>>(graph_id)
958            .clone()
959    });
960    let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
961    f(&*gmem)
962}
963
964/// Checks if a node with the given ID is currently selected in the specified graph.
965pub fn is_node_selected(ui: &egui::Ui, graph_id: egui::Id, node_id: NodeId) -> bool {
966    let gmem_arc = memory(ui, graph_id);
967    let gmem = gmem_arc.lock().expect("failed to lock graph temp memory");
968    gmem.selection.nodes.contains(&node_id)
969}
970
971/// Short-hand for retrieving access to the graph's temporary memory from the `Ui`.
972fn memory(ui: &egui::Ui, graph_id: egui::Id) -> Arc<Mutex<GraphTempMemory>> {
973    ui.ctx().data_mut(|d| {
974        d.get_temp_mut_or_default::<Arc<Mutex<GraphTempMemory>>>(graph_id)
975            .clone()
976    })
977}