Skip to main content

egui_graph_edit/
editor_ui.rs

1use std::collections::HashSet;
2
3use crate::color_hex_utils::*;
4use crate::utils::ColorUtils;
5
6use super::*;
7use egui::epaint::{CubicBezierShape, RectShape};
8use egui::*;
9
10pub type PortLocations = std::collections::HashMap<AnyParameterId, Pos2>;
11pub type NodeRects = std::collections::HashMap<NodeId, Rect>;
12
13const DISTANCE_TO_CONNECT: f32 = 10.0;
14
15/// Nodes communicate certain events to the parent graph when drawn. There is
16/// one special `User` variant which can be used by users as the return value
17/// when executing some custom actions in the UI of the node.
18#[derive(Clone, Debug)]
19pub enum NodeResponse<UserResponse: UserResponseTrait, NodeData: NodeDataTrait> {
20    ConnectEventStarted(NodeId, AnyParameterId),
21    ConnectEventEnded {
22        output: OutputId,
23        input: InputId,
24    },
25    CreatedNode(NodeId),
26    SelectNode(NodeId),
27    /// As a user of this library, prefer listening for `DeleteNodeFull` which
28    /// will also contain the user data for the deleted node.
29    DeleteNodeUi(NodeId),
30    /// Emitted when a node is deleted. The node will no longer exist in the
31    /// graph after this response is returned from the draw function, but its
32    /// contents are passed along with the event.
33    DeleteNodeFull {
34        node_id: NodeId,
35        node: Node<NodeData>,
36    },
37    DisconnectEvent {
38        output: OutputId,
39        input: InputId,
40    },
41    /// Emitted when a node is interacted with, and should be raised
42    RaiseNode(NodeId),
43    MoveNode {
44        node: NodeId,
45        drag_delta: Vec2,
46    },
47    User(UserResponse),
48}
49
50/// The return value of [`draw_graph_editor`]. This value can be used to make
51/// user code react to specific events that happened when drawing the graph.
52#[derive(Clone, Debug)]
53pub struct GraphResponse<UserResponse: UserResponseTrait, NodeData: NodeDataTrait> {
54    /// Events that occurred during this frame of rendering the graph. Check the
55    /// [`UserResponse`] type for a description of each event.
56    pub node_responses: Vec<NodeResponse<UserResponse, NodeData>>,
57    /// Is the mouse currently hovering the graph editor? Note that the node
58    /// finder is considered part of the graph editor, even when it floats
59    /// outside the graph editor rect.
60    pub cursor_in_editor: bool,
61    /// Is the mouse currently hovering the node finder?
62    pub cursor_in_finder: bool,
63    /// Geometry of the connections rendered during this frame.
64    pub connections: Vec<ConnectionRenderInfo>,
65}
66
67impl<UserResponse: UserResponseTrait, NodeData: NodeDataTrait> Default
68    for GraphResponse<UserResponse, NodeData>
69{
70    fn default() -> Self {
71        Self {
72            node_responses: Default::default(),
73            cursor_in_editor: false,
74            cursor_in_finder: false,
75            connections: Default::default(),
76        }
77    }
78}
79
80#[derive(Clone, Copy, Debug)]
81pub struct GraphEditorOptions {
82    /// When false, the graph is rendered but mouse-driven graph interactions are ignored.
83    pub interactions_enabled: bool,
84}
85
86impl Default for GraphEditorOptions {
87    fn default() -> Self {
88        Self {
89            interactions_enabled: true,
90        }
91    }
92}
93
94#[derive(Clone, Debug)]
95pub struct ConnectionRenderInfo {
96    pub output: OutputId,
97    pub input: InputId,
98    pub points: [Pos2; 4],
99    pub color: Color32,
100}
101
102pub struct GraphNodeWidget<'a, NodeData, DataType, ValueType> {
103    pub position: &'a mut Pos2,
104    pub orientation: &'a mut NodeOrientation,
105    pub graph: &'a mut Graph<NodeData, DataType, ValueType>,
106    pub port_locations: &'a mut PortLocations,
107    pub node_rects: &'a mut NodeRects,
108    pub node_id: NodeId,
109    pub ongoing_drag: Option<(NodeId, AnyParameterId)>,
110    pub selected: bool,
111    pub pan: egui::Vec2,
112}
113
114impl<NodeData, DataType, ValueType, NodeTemplate, UserResponse, UserState, CategoryType>
115    GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, UserState>
116where
117    NodeData: NodeDataTrait<
118        Response = UserResponse,
119        UserState = UserState,
120        DataType = DataType,
121        ValueType = ValueType,
122    >,
123    UserResponse: UserResponseTrait,
124    ValueType:
125        WidgetValueTrait<Response = UserResponse, UserState = UserState, NodeData = NodeData>,
126    NodeTemplate: NodeTemplateTrait<
127        NodeData = NodeData,
128        DataType = DataType,
129        ValueType = ValueType,
130        UserState = UserState,
131        CategoryType = CategoryType,
132    >,
133    DataType: DataTypeTrait<UserState>,
134    CategoryType: CategoryTrait,
135{
136    #[must_use]
137    pub fn draw_graph_editor(
138        &mut self,
139        ui: &mut Ui,
140        all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
141        user_state: &mut UserState,
142        prepend_responses: Vec<NodeResponse<UserResponse, NodeData>>,
143    ) -> GraphResponse<UserResponse, NodeData> {
144        self.draw_graph_editor_with_options(
145            ui,
146            all_kinds,
147            user_state,
148            prepend_responses,
149            GraphEditorOptions::default(),
150        )
151    }
152
153    #[must_use]
154    pub fn draw_graph_editor_with_options(
155        &mut self,
156        ui: &mut Ui,
157        all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
158        user_state: &mut UserState,
159        prepend_responses: Vec<NodeResponse<UserResponse, NodeData>>,
160        options: GraphEditorOptions,
161    ) -> GraphResponse<UserResponse, NodeData> {
162        ui.set_clip_rect(ui.max_rect());
163        let clip_rect = ui.clip_rect();
164        // Zoom may have never taken place, so ensure we use parent style
165        if !self.pan_zoom.started {
166            self.zoom(ui, 1.0);
167            self.pan_zoom.started = true;
168        }
169
170        // Zoom only within area where graph is shown
171        if ui.rect_contains_pointer(clip_rect) && options.interactions_enabled {
172            let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
173            if scroll_delta != 0.0 {
174                let zoom_delta = (scroll_delta * 0.002).exp();
175                self.zoom(ui, zoom_delta);
176            }
177        }
178
179        // Render graph zoomed
180        let zoomed_style = self.pan_zoom.zoomed_style.clone();
181        let graph_response = show_zoomed(ui.style().clone(), zoomed_style, ui, |ui| {
182            self.draw_graph_editor_inside_zoom(
183                ui,
184                all_kinds,
185                user_state,
186                prepend_responses,
187                options,
188            )
189        });
190
191        graph_response
192    }
193
194    /// Reset zoom to 1.0
195    pub fn reset_zoom(&mut self, ui: &Ui) {
196        let new_zoom = 1.0 / self.pan_zoom.zoom;
197        self.zoom(ui, new_zoom);
198    }
199
200    /// Zoom within the where you call `draw_graph_editor`. Use values like 1.01, or 0.99 to zoom.
201    /// For example: `let zoom_delta = (scroll_delta * 0.002).exp();`
202    pub fn zoom(&mut self, ui: &Ui, zoom_delta: f32) {
203        // Update zoom, and styles
204        let zoom_before = self.pan_zoom.zoom;
205        self.pan_zoom.zoom(ui.clip_rect(), ui.style(), zoom_delta);
206        if zoom_before != self.pan_zoom.zoom {
207            let actual_delta = self.pan_zoom.zoom / zoom_before;
208            self.update_node_positions_after_zoom(actual_delta);
209        }
210    }
211
212    fn update_node_positions_after_zoom(&mut self, zoom_delta: f32) {
213        // Update node positions, zoom towards center
214        let half_size = self.pan_zoom.clip_rect.size() / 2.0;
215        for (_id, node_pos) in self.node_positions.iter_mut() {
216            // 1. Get node local position (relative to origo)
217            let local_pos = node_pos.to_vec2() - half_size + self.pan_zoom.pan;
218            // 2. Scale local position by zoom delta
219            let scaled_local_pos = (local_pos * zoom_delta).to_pos2();
220            // 3. Transform back to global position
221            *node_pos = scaled_local_pos + half_size - self.pan_zoom.pan;
222            // This way we can retain pan untouched when zooming :)
223        }
224    }
225
226    fn draw_graph_editor_inside_zoom(
227        &mut self,
228        ui: &mut Ui,
229        all_kinds: impl NodeTemplateIter<Item = NodeTemplate>,
230        user_state: &mut UserState,
231        prepend_responses: Vec<NodeResponse<UserResponse, NodeData>>,
232        options: GraphEditorOptions,
233    ) -> GraphResponse<UserResponse, NodeData> {
234        // This causes the graph editor to use as much free space as it can.
235        // (so for windows it will use up to the resizeably set limit
236        // and for a Panel it will fill it completely)
237        let editor_rect = ui.max_rect();
238        let interactions_enabled = options.interactions_enabled;
239        let resp = ui.allocate_rect(editor_rect, Sense::hover());
240
241        let cursor_pos = ui
242            .ctx()
243            .input(|i| i.pointer.hover_pos().unwrap_or(Pos2::ZERO));
244        let mut cursor_in_editor = resp.contains_pointer();
245        let mut cursor_in_finder = false;
246
247        // Gets filled with the node metrics as they are drawn
248        let mut port_locations = PortLocations::new();
249        let mut node_rects = NodeRects::new();
250
251        // The responses returned from node drawing have side effects that are best
252        // executed at the end of this function.
253        let mut delayed_responses: Vec<NodeResponse<UserResponse, NodeData>> = prepend_responses;
254
255        // Used to detect drag events in the background
256        let mut drag_started_on_background = false;
257        let mut drag_released_on_background = false;
258
259        debug_assert_eq!(
260            self.node_order.iter().copied().collect::<HashSet<_>>(),
261            self.graph.iter_nodes().collect::<HashSet<_>>(),
262            "The node_order field of the GraphEditorself was left in an \
263        inconsistent self. It has either more or less values than the graph."
264        );
265
266        // Allocate rect before the nodes, otherwise this will block the interaction
267        // with the nodes.
268        let background_sense = if interactions_enabled {
269            Sense::click().union(Sense::drag())
270        } else {
271            Sense::hover()
272        };
273        let r = ui.allocate_rect(ui.min_rect(), background_sense);
274        if interactions_enabled {
275            if r.drag_started() {
276                drag_started_on_background = true;
277            } else if r.drag_stopped() {
278                drag_released_on_background = true;
279            }
280        }
281
282        /* Draw nodes */
283        for node_id in self.node_order.iter().copied() {
284            let responses = GraphNodeWidget {
285                position: self.node_positions.get_mut(node_id).unwrap(),
286                orientation: self.node_orientations.get_mut(node_id).unwrap(),
287                graph: &mut self.graph,
288                port_locations: &mut port_locations,
289                node_rects: &mut node_rects,
290                node_id,
291                ongoing_drag: self.connection_in_progress,
292                selected: self.selected_nodes.contains(&node_id),
293                pan: self.pan_zoom.pan + editor_rect.min.to_vec2(),
294            }
295            .show(&self.pan_zoom, ui, user_state);
296
297            if interactions_enabled {
298                // Actions executed later
299                delayed_responses.extend(responses);
300            }
301        }
302
303        /* Draw the node finder, if open */
304        let mut should_close_node_finder = false;
305        if let Some(ref mut node_finder) = self.node_finder {
306            let mut node_finder_area =
307                Area::new(ui.id().with("node_finder")).order(Order::Foreground);
308            if let Some(pos) = node_finder.position {
309                node_finder_area = node_finder_area.current_pos(pos);
310            }
311            node_finder_area.show(ui.ctx(), |ui| {
312                if let Some(node_kind) = node_finder.show(ui, all_kinds, user_state) {
313                    let new_node = self.graph.add_node(
314                        node_kind.node_graph_label(user_state),
315                        node_kind.user_data(user_state),
316                        |graph, node_id| node_kind.build_node(graph, user_state, node_id),
317                    );
318                    self.node_positions.insert(
319                        new_node,
320                        node_finder.position.unwrap_or(cursor_pos)
321                            - self.pan_zoom.pan
322                            - editor_rect.min.to_vec2(),
323                    );
324                    self.node_orientations
325                        .insert(new_node, NodeOrientation::LeftToRight);
326                    self.node_order.push(new_node);
327
328                    should_close_node_finder = true;
329                    delayed_responses.push(NodeResponse::CreatedNode(new_node));
330                }
331                let finder_rect = ui.min_rect();
332                // If the cursor is not in the main editor, check if the cursor is in the finder
333                // if the cursor is in the finder, then we can consider that also in the editor.
334                if finder_rect.contains(cursor_pos) {
335                    cursor_in_editor = true;
336                    cursor_in_finder = true;
337                }
338            });
339        }
340        if should_close_node_finder {
341            self.node_finder = None;
342        }
343
344        /* Draw connections */
345        let mut rendered_connections = Vec::new();
346
347        fn port_control(param_id: &AnyParameterId, orientation: NodeOrientation) -> Vec2 {
348            match (param_id, orientation) {
349                (AnyParameterId::Input(_), NodeOrientation::LeftToRight) => -Vec2::X,
350                (AnyParameterId::Input(_), NodeOrientation::RightToLeft) => Vec2::X,
351                (AnyParameterId::Output(_), NodeOrientation::LeftToRight) => Vec2::X,
352                (AnyParameterId::Output(_), NodeOrientation::RightToLeft) => -Vec2::X,
353            }
354        }
355
356        if let Some((_, ref locator)) = self.connection_in_progress {
357            let port_type = self.graph.any_param_type(*locator).unwrap();
358            let connection_color = port_type.data_type_color(user_state);
359            let start_pos = port_locations[locator];
360
361            // Find a port to connect to
362            fn snap_to_ports<
363                NodeData,
364                UserState,
365                DataType: DataTypeTrait<UserState>,
366                ValueType,
367                Key: slotmap::Key + Into<AnyParameterId>,
368                Value,
369            >(
370                graph: &Graph<NodeData, DataType, ValueType>,
371                port_type: &DataType,
372                ports: &SlotMap<Key, Value>,
373                port_locations: &PortLocations,
374                node_orientations: &SecondaryMap<NodeId, NodeOrientation>,
375                cursor_pos: Pos2,
376                default_control: Vec2,
377            ) -> (Pos2, Vec2) {
378                ports
379                    .iter()
380                    .find_map(|(port_id, _)| {
381                        let compatible_ports = graph
382                            .any_param_type(port_id.into())
383                            .map(|other| other == port_type)
384                            .unwrap_or(false);
385
386                        if compatible_ports {
387                            port_locations.get(&port_id.into()).and_then(|port_pos| {
388                                if port_pos.distance(cursor_pos) < DISTANCE_TO_CONNECT {
389                                    let param_id: AnyParameterId = port_id.into();
390                                    let dst_node_id = match param_id {
391                                        AnyParameterId::Output(id) => graph.get_output(id).node,
392                                        AnyParameterId::Input(id) => graph.get_input(id).node,
393                                    };
394                                    let dst_orientation = node_orientations[dst_node_id];
395                                    let dst_control = port_control(&param_id, dst_orientation);
396
397                                    Some((*port_pos, dst_control))
398                                } else {
399                                    None
400                                }
401                            })
402                        } else {
403                            None
404                        }
405                    })
406                    .unwrap_or((cursor_pos, default_control))
407            }
408
409            // Figure out where source connection should point to
410            let src_node_id = match locator {
411                AnyParameterId::Output(out_id) => self.graph.get_output(*out_id).node,
412                AnyParameterId::Input(in_id) => self.graph.get_input(*in_id).node,
413            };
414            let src_orientation = self.node_orientations[src_node_id];
415            let src_control = port_control(locator, src_orientation);
416
417            // Figure out where destination connection should point to
418            let (dst_pos, dst_control) = match locator {
419                AnyParameterId::Output(_) => snap_to_ports(
420                    &self.graph,
421                    port_type,
422                    &self.graph.inputs,
423                    &port_locations,
424                    &self.node_orientations,
425                    cursor_pos,
426                    -src_control,
427                ),
428
429                AnyParameterId::Input(_) => snap_to_ports(
430                    &self.graph,
431                    port_type,
432                    &self.graph.outputs,
433                    &port_locations,
434                    &self.node_orientations,
435                    cursor_pos,
436                    -src_control,
437                ),
438            };
439            draw_connection(
440                &self.pan_zoom,
441                ui.painter(),
442                start_pos,
443                src_control,
444                dst_pos,
445                dst_control,
446                connection_color,
447            );
448        }
449
450        for (input, output) in self.graph.iter_connections() {
451            let port_type = self
452                .graph
453                .any_param_type(AnyParameterId::Output(output))
454                .unwrap();
455            let connection_color = port_type.data_type_color(user_state);
456            let src_pos = port_locations[&AnyParameterId::Output(output)];
457            let dst_pos = port_locations[&AnyParameterId::Input(input)];
458            let src_id = self.graph.get_output(output).node;
459            let dst_id = self.graph.get_input(input).node;
460            let src_orientation = self.node_orientations[src_id];
461            let dst_orientation = self.node_orientations[dst_id];
462            let src_control = port_control(&output.into(), src_orientation);
463            let dst_control = port_control(&input.into(), dst_orientation);
464            let points = draw_connection(
465                &self.pan_zoom,
466                ui.painter(),
467                src_pos,
468                src_control,
469                dst_pos,
470                dst_control,
471                connection_color,
472            );
473            rendered_connections.push(ConnectionRenderInfo {
474                output,
475                input,
476                points,
477                color: connection_color,
478            });
479        }
480
481        /* Handle responses from drawing nodes */
482
483        // Some responses generate additional responses when processed. These
484        // are stored here to report them back to the user.
485        let mut extra_responses: Vec<NodeResponse<UserResponse, NodeData>> = Vec::new();
486
487        if interactions_enabled {
488            for response in delayed_responses.iter() {
489                match response {
490                    NodeResponse::ConnectEventStarted(node_id, port) => {
491                        self.connection_in_progress = Some((*node_id, *port));
492                    }
493                    NodeResponse::ConnectEventEnded { input, output } => {
494                        self.graph.add_connection(*output, *input)
495                    }
496                    NodeResponse::CreatedNode(_) => {
497                        //Convenience NodeResponse for users
498                    }
499                    NodeResponse::SelectNode(node_id) => {
500                        self.selected_nodes = Vec::from([*node_id]);
501                    }
502                    NodeResponse::DeleteNodeUi(node_id) => {
503                        let (node, disc_events) = self.graph.remove_node(*node_id);
504                        // Pass the disconnection responses first so user code can perform cleanup
505                        // before node removal response.
506                        extra_responses.extend(disc_events.into_iter().map(|(input, output)| {
507                            NodeResponse::DisconnectEvent { input, output }
508                        }));
509                        // Pass the full node as a response so library users can
510                        // listen for it and get their user data.
511                        extra_responses.push(NodeResponse::DeleteNodeFull {
512                            node_id: *node_id,
513                            node,
514                        });
515                        self.node_positions.remove(*node_id);
516                        // Make sure to not leave references to old nodes hanging
517                        self.selected_nodes.retain(|id| *id != *node_id);
518                        self.node_order.retain(|id| *id != *node_id);
519                    }
520                    NodeResponse::DisconnectEvent { input, output } => {
521                        let other_node = self.graph.get_output(*output).node;
522                        self.graph.remove_connection(*input);
523                        self.connection_in_progress =
524                            Some((other_node, AnyParameterId::Output(*output)));
525                    }
526                    NodeResponse::RaiseNode(node_id) => {
527                        let old_pos = self
528                            .node_order
529                            .iter()
530                            .position(|id| *id == *node_id)
531                            .expect("Node to be raised should be in `node_order`");
532                        self.node_order.remove(old_pos);
533                        self.node_order.push(*node_id);
534                    }
535                    NodeResponse::MoveNode { node, drag_delta } => {
536                        self.node_positions[*node] += *drag_delta;
537                        // Handle multi-node selection movement
538                        if self.selected_nodes.contains(node) && self.selected_nodes.len() > 1 {
539                            for n in self.selected_nodes.iter().copied() {
540                                if n != *node {
541                                    self.node_positions[n] += *drag_delta;
542                                }
543                            }
544                        }
545                    }
546                    NodeResponse::User(_) => {
547                        // These are handled by the user code.
548                    }
549                    NodeResponse::DeleteNodeFull { .. } => {
550                        unreachable!("The UI should never produce a DeleteNodeFull event.")
551                    }
552                }
553            }
554        }
555
556        // Handle box selection
557        if let Some(box_start) = interactions_enabled
558            .then_some(self.ongoing_box_selection)
559            .flatten()
560        {
561            let selection_rect = Rect::from_two_pos(cursor_pos, box_start);
562            let bg_color = Color32::from_rgba_unmultiplied(200, 200, 200, 20);
563            let stroke_color = Color32::from_rgba_unmultiplied(200, 200, 200, 180);
564            ui.painter().rect(
565                selection_rect,
566                2.0,
567                bg_color,
568                Stroke::new(3.0f32, stroke_color),
569                StrokeKind::Outside,
570            );
571
572            self.selected_nodes = node_rects
573                .into_iter()
574                .filter_map(|(node_id, rect)| {
575                    if selection_rect.intersects(rect) {
576                        Some(node_id)
577                    } else {
578                        None
579                    }
580                })
581                .collect();
582        }
583
584        // Push any responses that were generated during response handling.
585        // These are only informative for the end-user and need no special
586        // treatment here.
587        delayed_responses.extend(extra_responses);
588
589        /* Mouse input handling */
590
591        // This locks the context, so don't hold on to it for too long.
592        let mouse = &ui.ctx().input(|i| i.pointer.clone());
593
594        if interactions_enabled {
595            if mouse.any_released() && self.connection_in_progress.is_some() {
596                self.connection_in_progress = None;
597            }
598
599            if mouse.secondary_released() && !cursor_in_finder {
600                self.node_finder = Some(NodeFinder::new_at(cursor_pos));
601            }
602        }
603        if ui.ctx().input(|i| i.key_pressed(Key::Escape)) {
604            self.node_finder = None;
605        }
606
607        if interactions_enabled && r.dragged() && ui.ctx().input(|i| i.pointer.middle_down()) {
608            self.pan_zoom.pan += ui.ctx().input(|i| i.pointer.delta());
609        }
610
611        // Deselect and deactivate finder if the editor backround is clicked,
612        // *or* if the the mouse clicks off the ui
613        if interactions_enabled && mouse.any_pressed() && !cursor_in_finder {
614            self.selected_nodes = Vec::new();
615            self.node_finder = None;
616        }
617
618        if interactions_enabled && drag_started_on_background && mouse.primary_down() {
619            self.ongoing_box_selection = Some(cursor_pos);
620        }
621        if interactions_enabled && (mouse.primary_released() || drag_released_on_background) {
622            self.ongoing_box_selection = None;
623        }
624
625        GraphResponse {
626            node_responses: delayed_responses,
627            cursor_in_editor,
628            cursor_in_finder,
629            connections: rendered_connections,
630        }
631    }
632}
633
634fn draw_connection(
635    pan_zoom: &PanZoom,
636    painter: &Painter,
637    src_pos: Pos2,
638    src_control: Vec2,
639    dst_pos: Pos2,
640    dst_control: Vec2,
641    color: Color32,
642) -> [Pos2; 4] {
643    let connection_stroke = egui::Stroke {
644        width: 5.0 * pan_zoom.zoom,
645        color,
646    };
647
648    let control_scale = ((dst_pos.x - src_pos.x) / 2.0).abs().max(30.0);
649    let src_control = src_pos + src_control * control_scale;
650    let dst_control = dst_pos + dst_control * control_scale;
651    let points = [src_pos, src_control, dst_control, dst_pos];
652
653    let bezier = CubicBezierShape::from_points_stroke(
654        points,
655        false,
656        Color32::TRANSPARENT,
657        connection_stroke,
658    );
659
660    painter.add(bezier);
661
662    let [r, g, b, a] = color.to_srgba_unmultiplied();
663    let wide_stroke = egui::Stroke {
664        width: 10.0,
665        color: Color32::from_rgba_unmultiplied(r / 2, g / 2, b / 2, a / 2),
666    };
667
668    let wide_bezier =
669        CubicBezierShape::from_points_stroke(points, false, Color32::TRANSPARENT, wide_stroke);
670
671    painter.add(wide_bezier);
672    points
673}
674
675#[derive(Clone, Copy, Debug)]
676struct OuterRectMemory(Rect);
677
678impl<NodeData, DataType, ValueType, UserResponse, UserState>
679    GraphNodeWidget<'_, NodeData, DataType, ValueType>
680where
681    NodeData: NodeDataTrait<
682        Response = UserResponse,
683        UserState = UserState,
684        DataType = DataType,
685        ValueType = ValueType,
686    >,
687    UserResponse: UserResponseTrait,
688    ValueType:
689        WidgetValueTrait<Response = UserResponse, UserState = UserState, NodeData = NodeData>,
690    DataType: DataTypeTrait<UserState>,
691{
692    pub const MAX_NODE_SIZE: [f32; 2] = [200.0, 200.0];
693
694    pub fn show(
695        self,
696        pan_zoom: &PanZoom,
697        ui: &mut Ui,
698        user_state: &mut UserState,
699    ) -> Vec<NodeResponse<UserResponse, NodeData>> {
700        let mut child_ui = ui.new_child(
701            UiBuilder::new()
702                .max_rect(Rect::from_min_size(
703                    *self.position + self.pan,
704                    Self::MAX_NODE_SIZE.into(),
705                ))
706                .layout(*ui.layout())
707                .id(ui.id().with(self.node_id)),
708        );
709
710        Self::show_graph_node(self, pan_zoom, &mut child_ui, user_state)
711    }
712
713    /// Draws this node. Also fills in the list of port locations with all of its ports.
714    /// Returns responses indicating multiple events.
715    fn show_graph_node(
716        self,
717        pan_zoom: &PanZoom,
718        ui: &mut Ui,
719        user_state: &mut UserState,
720    ) -> Vec<NodeResponse<UserResponse, NodeData>> {
721        let margin = egui::vec2(15.0, 5.0) * pan_zoom.zoom;
722        let mut responses = Vec::<NodeResponse<UserResponse, NodeData>>::new();
723
724        let background_color;
725        let text_color;
726        if ui.visuals().dark_mode {
727            background_color = color_from_hex("#3f3f3f").unwrap();
728            text_color = color_from_hex("#fefefe").unwrap();
729        } else {
730            background_color = color_from_hex("#ffffff").unwrap();
731            text_color = color_from_hex("#505050").unwrap();
732        }
733
734        ui.visuals_mut().widgets.noninteractive.fg_stroke =
735            Stroke::new(2.0 * pan_zoom.zoom, text_color);
736
737        // Preallocate shapes to paint below contents
738        let outline_shape = ui.painter().add(Shape::Noop);
739        let background_shape = ui.painter().add(Shape::Noop);
740
741        let mut outer_rect_bounds = ui.available_rect_before_wrap();
742        // Scale hack, otherwise some (larger) rects expand too much when zoomed out
743        outer_rect_bounds.max.x =
744            outer_rect_bounds.min.x + outer_rect_bounds.width() * pan_zoom.zoom;
745        let mut inner_rect = outer_rect_bounds.shrink2(margin);
746
747        // Make sure we don't shrink to the negative:
748        inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
749        inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
750
751        let mut child_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect).layout(*ui.layout()));
752
753        // Get interaction rect from memory, it may expand after the window response on resize.
754        let interaction_rect = ui
755            .ctx()
756            .memory_mut(|mem| {
757                mem.data
758                    .get_temp::<OuterRectMemory>(child_ui.id())
759                    .map(|stored| stored.0)
760            })
761            .unwrap_or(outer_rect_bounds);
762        // After 0.20, layers added over others can block hover interaction. Call this first
763        // before creating the node content.
764        let window_response = ui.interact(
765            interaction_rect,
766            ui.id().with((self.node_id, "window")),
767            Sense::click_and_drag(),
768        );
769
770        let mut title_height = 0.0;
771
772        let mut input_port_heights = vec![];
773        let mut output_port_heights = vec![];
774
775        child_ui.vertical(|ui| {
776            ui.horizontal(|ui| {
777                ui.add(
778                    Label::new(
779                        RichText::new(&self.graph[self.node_id].label)
780                            .text_style(TextStyle::Button)
781                            .color(text_color),
782                    )
783                    .selectable(false),
784                );
785                responses.extend(self.graph[self.node_id].user_data.top_bar_ui(
786                    ui,
787                    self.node_id,
788                    self.graph,
789                    user_state,
790                ));
791                ui.add_space(8.0 * pan_zoom.zoom); // The size of the little h-flip icon
792                ui.add_space(4.0 * pan_zoom.zoom); // margin
793                ui.add_space(8.0 * pan_zoom.zoom); // The size of the little cross icon
794            });
795            ui.add_space(margin.y);
796            title_height = ui.min_size().y;
797
798            // First pass: Draw the inner fields. Compute port heights
799            let input_layout = match self.orientation {
800                NodeOrientation::LeftToRight => Layout::left_to_right(Align::default()),
801                NodeOrientation::RightToLeft => Layout::right_to_left(Align::default()),
802            };
803            let output_layout = match self.orientation {
804                NodeOrientation::LeftToRight => Layout::right_to_left(Align::default()),
805                NodeOrientation::RightToLeft => Layout::left_to_right(Align::default()),
806            };
807
808            let inputs = self.graph[self.node_id].inputs.clone();
809            for (param_name, param_id) in inputs {
810                if self.graph[param_id].shown_inline {
811                    let height_before = ui.min_rect().bottom();
812                    // NOTE: We want to pass the `user_data` to
813                    // `value_widget`, but we can't since that would require
814                    // borrowing the graph twice. Here, we make the
815                    // assumption that the value is cheaply replaced, and
816                    // use `std::mem::take` to temporarily replace it with a
817                    // dummy value. This requires `ValueType` to implement
818                    // Default, but results in a totally safe alternative.
819                    let mut value = std::mem::take(&mut self.graph[param_id].value);
820
821                    ui.with_layout(input_layout, |ui| {
822                        if self.graph.connection(param_id).is_some() {
823                            let node_responses = value.value_widget_connected(
824                                &param_name,
825                                self.node_id,
826                                ui,
827                                user_state,
828                                &self.graph[self.node_id].user_data,
829                            );
830
831                            responses.extend(node_responses.into_iter().map(NodeResponse::User));
832                        } else {
833                            let node_responses = value.value_widget(
834                                &param_name,
835                                self.node_id,
836                                ui,
837                                user_state,
838                                &self.graph[self.node_id].user_data,
839                            );
840
841                            responses.extend(node_responses.into_iter().map(NodeResponse::User));
842                        }
843                    });
844
845                    self.graph[param_id].value = value;
846
847                    self.graph[self.node_id].user_data.separator(
848                        ui,
849                        self.node_id,
850                        AnyParameterId::Input(param_id),
851                        self.graph,
852                        user_state,
853                    );
854
855                    let height_after = ui.min_rect().bottom();
856                    input_port_heights.push((height_before + height_after) / 2.0);
857                }
858            }
859
860            let outputs = self.graph[self.node_id].outputs.clone();
861            for (param_name, param_id) in outputs {
862                let height_before = ui.min_rect().bottom();
863                ui.with_layout(output_layout, |ui| {
864                    responses.extend(self.graph[self.node_id].user_data.output_ui(
865                        ui,
866                        self.node_id,
867                        self.graph,
868                        user_state,
869                        &param_name,
870                    ));
871                });
872
873                self.graph[self.node_id].user_data.separator(
874                    ui,
875                    self.node_id,
876                    AnyParameterId::Output(param_id),
877                    self.graph,
878                    user_state,
879                );
880
881                let height_after = ui.min_rect().bottom();
882                output_port_heights.push((height_before + height_after) / 2.0);
883            }
884
885            responses.extend(self.graph[self.node_id].user_data.bottom_ui(
886                ui,
887                self.node_id,
888                self.graph,
889                user_state,
890            ));
891        });
892
893        // Second pass, iterate again to draw the ports. This happens outside
894        // the child_ui because we want ports to overflow the node background.
895
896        let outer_rect = child_ui.min_rect().expand2(margin);
897        let port_left = outer_rect.left();
898        let port_right = outer_rect.right();
899
900        // Save expanded rect to memory.
901        ui.ctx().memory_mut(|mem| {
902            mem.data
903                .insert_temp(child_ui.id(), OuterRectMemory(outer_rect))
904        });
905
906        #[allow(clippy::too_many_arguments)]
907        fn draw_port<NodeData, DataType, ValueType, UserResponse, UserState>(
908            pan_zoom: &PanZoom,
909            ui: &mut Ui,
910            graph: &Graph<NodeData, DataType, ValueType>,
911            node_id: NodeId,
912            user_state: &mut UserState,
913            port_pos: Pos2,
914            responses: &mut Vec<NodeResponse<UserResponse, NodeData>>,
915            param_id: AnyParameterId,
916            port_locations: &mut PortLocations,
917            ongoing_drag: Option<(NodeId, AnyParameterId)>,
918            is_connected_input: bool,
919        ) where
920            DataType: DataTypeTrait<UserState>,
921            UserResponse: UserResponseTrait,
922            NodeData: NodeDataTrait,
923        {
924            let port_type = graph.any_param_type(param_id).unwrap();
925
926            let port_rect = Rect::from_center_size(
927                port_pos,
928                egui::vec2(DISTANCE_TO_CONNECT * 2.0, DISTANCE_TO_CONNECT * 2.0) * pan_zoom.zoom,
929            );
930
931            let sense = if ongoing_drag.is_some() {
932                Sense::hover()
933            } else {
934                Sense::click_and_drag()
935            };
936
937            let resp = ui.allocate_rect(port_rect, sense);
938
939            // Check if the distance between the port and the mouse is the distance to connect
940            let close_enough = if let Some(pointer_pos) = ui.ctx().pointer_hover_pos() {
941                port_rect.center().distance(pointer_pos) < DISTANCE_TO_CONNECT * pan_zoom.zoom
942            } else {
943                false
944            };
945
946            let port_color = if close_enough {
947                Color32::WHITE
948            } else {
949                port_type.data_type_color(user_state)
950            };
951            ui.painter().circle(
952                port_rect.center(),
953                5.0 * pan_zoom.zoom,
954                port_color,
955                Stroke::NONE,
956            );
957
958            if resp.drag_started() {
959                if is_connected_input {
960                    let input = param_id.assume_input();
961                    let corresp_output = graph
962                        .connection(input)
963                        .expect("Connection data should be valid");
964                    responses.push(NodeResponse::DisconnectEvent {
965                        input: param_id.assume_input(),
966                        output: corresp_output,
967                    });
968                } else {
969                    responses.push(NodeResponse::ConnectEventStarted(node_id, param_id));
970                }
971            }
972
973            if let Some((origin_node, origin_param)) = ongoing_drag {
974                if origin_node != node_id {
975                    // Don't allow self-loops
976                    if graph.any_param_type(origin_param).unwrap() == port_type
977                        && close_enough
978                        && ui.input(|i| i.pointer.any_released())
979                    {
980                        match (param_id, origin_param) {
981                            (AnyParameterId::Input(input), AnyParameterId::Output(output))
982                            | (AnyParameterId::Output(output), AnyParameterId::Input(input)) => {
983                                responses.push(NodeResponse::ConnectEventEnded { input, output });
984                            }
985                            _ => { /* Ignore in-in or out-out connections */ }
986                        }
987                    }
988                }
989            }
990
991            port_locations.insert(param_id, port_rect.center());
992        }
993
994        // Input ports
995        for ((_, param), port_height) in self.graph[self.node_id]
996            .inputs
997            .iter()
998            .zip(input_port_heights)
999        {
1000            let should_draw = match self.graph[*param].kind() {
1001                InputParamKind::ConnectionOnly => true,
1002                InputParamKind::ConstantOnly => false,
1003                InputParamKind::ConnectionOrConstant => true,
1004            };
1005
1006            if should_draw {
1007                let port_pos = match self.orientation {
1008                    NodeOrientation::LeftToRight => pos2(port_left, port_height),
1009                    NodeOrientation::RightToLeft => pos2(port_right, port_height),
1010                };
1011                draw_port(
1012                    pan_zoom,
1013                    ui,
1014                    self.graph,
1015                    self.node_id,
1016                    user_state,
1017                    port_pos,
1018                    &mut responses,
1019                    AnyParameterId::Input(*param),
1020                    self.port_locations,
1021                    self.ongoing_drag,
1022                    self.graph.connection(*param).is_some(),
1023                );
1024            }
1025        }
1026
1027        // Output ports
1028        for ((_, param), port_height) in self.graph[self.node_id]
1029            .outputs
1030            .iter()
1031            .zip(output_port_heights)
1032        {
1033            let port_pos = match self.orientation {
1034                NodeOrientation::LeftToRight => pos2(port_right, port_height),
1035                NodeOrientation::RightToLeft => pos2(port_left, port_height),
1036            };
1037            draw_port(
1038                pan_zoom,
1039                ui,
1040                self.graph,
1041                self.node_id,
1042                user_state,
1043                port_pos,
1044                &mut responses,
1045                AnyParameterId::Output(*param),
1046                self.port_locations,
1047                self.ongoing_drag,
1048                false,
1049            );
1050        }
1051
1052        // Draw the background shape.
1053        // NOTE: This code is a bit more involved than it needs to be because egui
1054        // does not support drawing rectangles with asymmetrical round corners.
1055
1056        let (shape, outline) = {
1057            let rounding_radius = (4.0 * pan_zoom.zoom) as u8;
1058            let corner_radius = CornerRadius::same(rounding_radius);
1059
1060            let titlebar_height = title_height + margin.y;
1061            let titlebar_rect =
1062                Rect::from_min_size(outer_rect.min, vec2(outer_rect.width(), titlebar_height));
1063            let titlebar = Shape::Rect(RectShape {
1064                blur_width: 0.0,
1065                rect: titlebar_rect,
1066                corner_radius,
1067                fill: self.graph[self.node_id]
1068                    .user_data
1069                    .titlebar_color(ui, self.node_id, self.graph, user_state)
1070                    .unwrap_or_else(|| background_color.lighten(0.8)),
1071                stroke: Stroke::NONE,
1072                stroke_kind: StrokeKind::Inside,
1073                round_to_pixels: None,
1074                brush: None,
1075                angle: 0.0,
1076            });
1077
1078            let body_rect = Rect::from_min_size(
1079                outer_rect.min + vec2(0.0, titlebar_height - rounding_radius as f32),
1080                vec2(outer_rect.width(), outer_rect.height() - titlebar_height),
1081            );
1082            let body = Shape::Rect(RectShape {
1083                blur_width: 0.0,
1084                rect: body_rect,
1085                corner_radius: CornerRadius::ZERO,
1086                fill: background_color,
1087                stroke: Stroke::NONE,
1088                stroke_kind: StrokeKind::Inside,
1089                round_to_pixels: None,
1090                brush: None,
1091                angle: 0.0,
1092            });
1093
1094            let bottom_body_rect = Rect::from_min_size(
1095                body_rect.min + vec2(0.0, body_rect.height() - titlebar_height * 0.5),
1096                vec2(outer_rect.width(), titlebar_height),
1097            );
1098            let bottom_body = Shape::Rect(RectShape {
1099                blur_width: 0.0,
1100                rect: bottom_body_rect,
1101                corner_radius,
1102                fill: background_color,
1103                stroke: Stroke::NONE,
1104                stroke_kind: StrokeKind::Inside,
1105                round_to_pixels: None,
1106                brush: None,
1107                angle: 0.0,
1108            });
1109
1110            let node_rect = titlebar_rect.union(body_rect).union(bottom_body_rect);
1111            let outline_color = self.graph[self.node_id].user_data.border_color(
1112                ui,
1113                self.node_id,
1114                self.graph,
1115                user_state,
1116            );
1117            let outline = if self.selected || outline_color.is_some() {
1118                let (fill, width) = if self.selected {
1119                    (Color32::WHITE.lighten(0.8), 1.0)
1120                } else {
1121                    (
1122                        outline_color.unwrap(),
1123                        self.graph[self.node_id].user_data.border_width(
1124                            ui,
1125                            self.node_id,
1126                            self.graph,
1127                            user_state,
1128                        ),
1129                    )
1130                };
1131                Shape::Rect(RectShape {
1132                    blur_width: 0.0,
1133                    rect: node_rect.expand(width.max(0.0) * pan_zoom.zoom),
1134                    corner_radius,
1135                    fill,
1136                    stroke: Stroke::NONE,
1137                    stroke_kind: StrokeKind::Inside,
1138                    round_to_pixels: None,
1139                    brush: None,
1140                    angle: 0.0,
1141                })
1142            } else {
1143                Shape::Noop
1144            };
1145
1146            // Take note of the node rect, so the editor can use it later to compute intersections.
1147            self.node_rects.insert(self.node_id, node_rect);
1148
1149            (Shape::Vec(vec![titlebar, body, bottom_body]), outline)
1150        };
1151
1152        ui.painter().set(background_shape, shape);
1153        ui.painter().set(outline_shape, outline);
1154
1155        // --- Interaction ---
1156
1157        // Titlebar buttons
1158        let can_flip =
1159            self.graph.nodes[self.node_id]
1160                .user_data
1161                .can_flip(self.node_id, self.graph, user_state);
1162
1163        if can_flip && Self::flip_button(pan_zoom, ui, outer_rect).clicked() {
1164            *self.orientation = self.orientation.flip();
1165        }
1166
1167        let can_delete = self.graph.nodes[self.node_id].user_data.can_delete(
1168            self.node_id,
1169            self.graph,
1170            user_state,
1171        );
1172
1173        if can_delete && Self::close_button(pan_zoom, ui, outer_rect).clicked() {
1174            responses.push(NodeResponse::DeleteNodeUi(self.node_id));
1175        };
1176
1177        // Movement
1178        let drag_delta = window_response.drag_delta();
1179        if drag_delta.length_sq() > 0.0 {
1180            responses.push(NodeResponse::MoveNode {
1181                node: self.node_id,
1182                drag_delta,
1183            });
1184            responses.push(NodeResponse::RaiseNode(self.node_id));
1185        }
1186
1187        // Node selection
1188        //
1189        // HACK: Only set the select response when no other response is active.
1190        // This prevents some issues.
1191        if responses.is_empty() && window_response.clicked_by(PointerButton::Primary) {
1192            responses.push(NodeResponse::SelectNode(self.node_id));
1193            responses.push(NodeResponse::RaiseNode(self.node_id));
1194        }
1195
1196        responses
1197    }
1198
1199    fn close_button(pan_zoom: &PanZoom, ui: &mut Ui, node_rect: Rect) -> Response {
1200        // Measurements
1201        let margin = 8.0 * pan_zoom.zoom;
1202        let size = 10.0 * pan_zoom.zoom;
1203        let stroke_width = 2.0;
1204        let offs = margin + size / 2.0;
1205
1206        let position = pos2(node_rect.right() - offs, node_rect.top() + offs);
1207        let rect = Rect::from_center_size(position, vec2(size, size));
1208        let resp = ui.allocate_rect(rect, Sense::click());
1209
1210        let dark_mode = ui.visuals().dark_mode;
1211        let color = if resp.clicked() {
1212            if dark_mode {
1213                color_from_hex("#ffffff").unwrap()
1214            } else {
1215                color_from_hex("#000000").unwrap()
1216            }
1217        } else if resp.hovered() {
1218            if dark_mode {
1219                color_from_hex("#dddddd").unwrap()
1220            } else {
1221                color_from_hex("#222222").unwrap()
1222            }
1223        } else {
1224            #[allow(clippy::collapsible_else_if)]
1225            if dark_mode {
1226                color_from_hex("#aaaaaa").unwrap()
1227            } else {
1228                color_from_hex("#555555").unwrap()
1229            }
1230        };
1231        let stroke = Stroke {
1232            width: stroke_width,
1233            color,
1234        };
1235
1236        ui.painter()
1237            .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1238        ui.painter()
1239            .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1240
1241        resp
1242    }
1243
1244    fn flip_button(pan_zoom: &PanZoom, ui: &mut Ui, node_rect: Rect) -> Response {
1245        // Measurements
1246        let margin = 8.0 * pan_zoom.zoom;
1247        let size = 10.0 * pan_zoom.zoom;
1248        let stroke_width = 2.0;
1249        let offs = margin + size / 2.0;
1250
1251        let position = pos2(node_rect.right() - offs * 2.0 - 4.0, node_rect.top() + offs);
1252        let rect = Rect::from_center_size(position, vec2(size, size));
1253        let resp = ui.allocate_rect(rect, Sense::click());
1254
1255        let dark_mode = ui.visuals().dark_mode;
1256        let color = if resp.clicked() {
1257            if dark_mode {
1258                color_from_hex("#ffffff").unwrap()
1259            } else {
1260                color_from_hex("#000000").unwrap()
1261            }
1262        } else if resp.hovered() {
1263            if dark_mode {
1264                color_from_hex("#dddddd").unwrap()
1265            } else {
1266                color_from_hex("#222222").unwrap()
1267            }
1268        } else {
1269            #[allow(clippy::collapsible_else_if)]
1270            if dark_mode {
1271                color_from_hex("#aaaaaa").unwrap()
1272            } else {
1273                color_from_hex("#555555").unwrap()
1274            }
1275        };
1276        let stroke = Stroke {
1277            width: stroke_width,
1278            color,
1279        };
1280
1281        let lines = [
1282            [rect.left_center(), rect.right_center()],
1283            [
1284                rect.left_center(),
1285                rect.left_center().lerp(rect.center_top(), 0.5),
1286            ],
1287            [
1288                rect.left_center(),
1289                rect.left_center().lerp(rect.center_bottom(), 0.5),
1290            ],
1291            [
1292                rect.right_center(),
1293                rect.right_center().lerp(rect.center_top(), 0.5),
1294            ],
1295            [
1296                rect.right_center(),
1297                rect.right_center().lerp(rect.center_bottom(), 0.5),
1298            ],
1299        ];
1300
1301        for line in lines {
1302            ui.painter().line_segment(line, stroke);
1303        }
1304
1305        resp
1306    }
1307}