egui-snarl

repository·main·Indexed 20 days ago

https://github.com/zakarumych/egui-snarl

A Rust crate for building customizable, typed, and serializable node-graph user interfaces using the egui framework. It provides a flexible system for creating visual programming tools, featuring a typed data model via the Snarl structure, a SnarlViewer trait for defining node UI and behavior, and extensive visual customization through SnarlStyle and NodeLayout.

Tokens
8.4K
Snippets
28
Records
44
Agent score
69%

What's inside egui-snarl

  1. Overview of egui-snarl

    main
    egui-snarl is a Rust crate designed for creating and manipulating node-graph user interfaces using egui. It is built around a typed data model where the Snarl structure is parameterized by the type of data the nodes hold. This allows for highly customizable node variants, such as using an enum to define different node types.
  2. Handle wire connections and multiconnections

    main

    The SnarlViewer allows you to intercept and control wire connection logic. When a user connects a wire, the viewer is notified and can decide whether to create the connection, ignore it, or trigger side effects (like adding nodes or playing sounds).

    User Interaction Shortcuts:

    • Multiconnections (Bundling): While dragging a new wire, hold Shift and hover over a pin on the same side to add that pin to the current wire bundle.
    • Yanking Wires: Hold Ctrl (or Cmd on macOS) while starting a drag from a pin to yank existing wires from that pin and move them to a new destination.
  3. How to customize node UI with the SnarlViewer trait

    main

    To define how nodes look and behave, you must implement the SnarlViewer trait. This trait is parameterized by your node type and is responsible for:

    • Defining the node's title UI.
    • Determining how many pins a node has.
    • Filling the UI content for each pin (e.g., text inputs, buttons, images, or draggable values).
    • Handling context menus for nodes and the graph background.
    • Responding to wire connection events.

    Because pin content is rendered within a provided egui::Ui, you can place any egui widget inside a pin.

  4. Understand the node layout structure

    main

    Nodes in egui-snarl are organized into five distinct layout spaces. You can use these to structure your node's visual information:

    1. Header: Located at the top. Contains a label (node name by default) and a collapsing button if SnarlStyle::collapsible is enabled.
    2. Input Pins: Located on the left side below the header. Each pin has a shape on the left edge for connecting/disconnecting wires.
    3. Body: An optional center area for user-defined content.
    4. Output Pins: Located on the right side below the header. Each pin has a shape on the right edge.
    5. Footer: An optional area at the bottom for user-defined content.
  5. Configure pin placement with PinPlacement

    main

    You can control the exact positioning of pins relative to the node frame using the PinPlacement enum. This determines whether pins are drawn inside the node's boundary, exactly on the edge, or outside the frame with a specified margin.

    Options:

    • PinPlacement::Inside: Pins are placed within the node frame's inner margin.
    • PinPlacement::Edge: Pins are aligned exactly with the node frame's edges.
    • PinPlacement::Outside { margin }: Pins are placed outside the node frame, offset by the provided margin value.
  6. Use Effects to modify a Snarl

    main

    In egui-snarl, modifications to the graph (like adding nodes or connecting pins) are often handled via deferred execution using the Effects<T> container. Instead of mutating the Snarl<T> directly in every UI interaction, you can collect Effect<T> operations into an Effects<T> instance and then apply them all at once using Snarl::apply_effects.

    This pattern is commonly used by SnarlViewer to populate a list of changes that are subsequently applied to the underlying data model.

    // Example of collecting and applying effects
    let mut effects = Effects::new();
    
    // Record some operations
    effects.insert_node(pos, my_node);
    effects.connect(out_pin, in_pin);
    
    // Apply them to the snarl
    snarl.apply_effects(effects);
  7. How SnarlState manages viewport and selection

    main

    The SnarlState object acts as the central coordinator for the graph's interactive state. It manages several key abstractions:

    • Viewport Transform: Uses to_global (TSTransform) to map between the graph's coordinate space and the UI's screen space. You can adjust this using look_at to center the view on a specific area.
    • Node Selection: Tracks selected_nodes (a list of NodeId) and supports single or multiple selection via select_one_node and select_many_nodes.
    • Rect Selection: Manages area-based selection. Use start_rect_selection(pos), update_rect_selection(pos), and stop_rect_selection() to define a selection rectangle. The resulting Rect can be retrieved via rect_selection().
    • Draw Order: Maintains a draw_order of NodeIds to ensure nodes are rendered in the correct Z-order (e.g., bringing a selected node to the top).
  8. Configure graph appearance with SnarlStyle

    main

    Use SnarlStyle to customize the visual properties of the graph, including:

    • Collapsibility: Enable/disable the header collapsing feature.
    • Wire Scaling: Adjust how wires scale.
    • Background: Configure the background pattern. You can use built-in patterns like Grid or provide a custom function to fill the visual space.
  9. Configure wire rendering styles

    main

    You can control how wires are visually rendered using the WireStyle enum. When two pins connected by a wire have different requested styles, the pick_wire_style function determines the resulting style based on a specific precedence order:

    1. Line: A straight line from endpoint to endpoint. (Highest precedence)
    2. AxisAligned: Straight lines with 90-degree turns and a configurable corner_radius.
    3. Bezier3: A 3rd-degree Bezier curve.
    4. Bezier5: A 5th-degree Bezier curve. (Lowest precedence)

    If any part of the connection requires a Line style, the entire wire becomes a Line. If AxisAligned is requested, it takes precedence over Bezier curves.

    // Example of selecting a style between two pins
    let style = pick_wire_style(WireStyle::Bezier5, WireStyle::AxisAligned { corner_radius: 5.0 });
    // Result: WireStyle::AxisAligned { corner_radius: 5.0 }
  10. Customize node and pin UI with SnarlViewer

    main

    The SnarlViewer<T> trait is the primary extension point for customizing how nodes, pins, and the graph itself are rendered and how they respond to user interactions. By implementing this trait for your data type T, you can control:

    • Node Appearance: Customize the title, frames (node and header), layout, and egui styles.
    • Node Content: Define what is shown in the header, body, and footer, as well as on-hover popups.
    • Pins: Specify the number of input/output pins and define how each pin is rendered using show_input and show_output.
    • Interactions: Implement custom logic for connecting/disconnecting pins, context menus (graph-level, node-level, or dropped-wire level), and wire widgets.
    • Graph Environment: Control the background drawing and the view transform.
  11. Customize node layout with NodeLayoutKind

    main

    The egui-snarl UI supports different visual arrangements for nodes through the NodeLayoutKind abstraction. When implementing a SnarlViewer, you can return different layout kinds via node_layout() to change how input pins, output pins, and the node body are positioned relative to each other.

    Supported layout kinds:

    • NodeLayoutKind::Coil: Places input pins on one side and output pins on the other, with the body in the middle (horizontal arrangement).
    • NodeLayoutKind::Sandwich: Places input pins at the top, the body in the middle, and output pins at the bottom (vertical arrangement).
    • NodeLayoutKind::FlippedSandwich: Places output pins at the top, the body in the middle, and input pins at the bottom.

    Each layout kind affects how the SnarlViewer calculates pin placement and how the draw_node function orchestrates the rendering of pins and the body.

  12. Implement SnarlViewer for custom node rendering

    main

    To customize the visual structure of your nodes, override these methods in your SnarlViewer implementation:

    • title(&mut self, node: &T) -> String: Returns the display name for the node.
    • node_frame(...) -> egui::Frame: Customizes the main container for the node.
    • header_frame(...) -> egui::Frame: Customizes the container for the header UI.
    • node_layout(...) -> NodeLayout: Changes how the 5 parts (header, body, footer, input pins, output pins) are positioned.
    • apply_node_style(&mut self, style: &mut Style, ...): Modifies the egui::Style applied to the node.
    • show_header(...), show_body(...), show_footer(...): Renders custom UI elements in those specific node sections.