GraphView

Represents a graph visualization control that can display nodes and edges with various layout algorithms and interactive features. It also supports optional multi-selection (checkbox column and bulk shortcuts), mouse-driven creation of nodes (Ctrl+click on empty space) and edges (right-drag between nodes) via GraphViewEvents, and tooltips from GraphNode::write_description on hover.

To create a GraphView use GraphView::new method (with 2 parameters: a layout and initialization flags).

let gv = GraphView::new(layout!("x:10,y:5,w:40,h:20"), graphview::Flags::ScrollBars | graphview::Flags::SearchBar);

or the macro graphview!

let gv1 = graphview!("x:10,y:5,w:40,h:20,flags:[ScrollBars,SearchBar]");
let gv2 = graphview!("d:f,arrange:GridPacked,routing:Orthogonal");
let gv3 = graphview!("d:f,flags:[ScrollBars,SearchBar,MultiSelect]");

A GraphView supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
flagsListNoGraphView initialization flags
background or backDictNoBackground character for the GraphView
left-scroll-margin or lsmIntegerNoLeft margin for scrollbars/search components
top-scroll-margin or tsmIntegerNoTop margin for scrollbars/search components
line-type or edge-line-type or eltStringNoLine type for drawing edges
routing or edge-routingStringNoEdge routing algorithm
arrange or arrange-nodesStringNoNode arrangement algorithm
arrow-heads or arrowsBoolNoEnable/disable arrow heads on directed edges
highlight-incoming-edges or hieBoolNoHighlight incoming edges of current node
highlight-outgoing-edges or hoeBoolNoHighlight outgoing edges of current node

A GraphView supports the following initialization flags:

  • graphview::Flags::ScrollBars or ScrollBars (for macro initialization) - enables scrollbars for navigating large graphs.
  • graphview::Flags::SearchBar or SearchBar (for macro initialization) - enables a search bar for finding nodes.
  • graphview::Flags::MultiSelect or MultiSelect (for macro initialization) - enables multi-selection UI: a checkbox gutter on each node (☑/☐), keyboard shortcuts to toggle or bulk-select nodes, and mouse gestures for Ctrl+click toggling and moving multiple selected nodes together. When this flag is off, behavior matches single-selection graphs as before.

A GraphView supports the following edge line types:

  • LineType::Single or Single - single line edges
  • LineType::Double or Double - double line edges
  • LineType::SingleThick or SingleThick - thick single line edges
  • LineType::Border or Border - border style edges
  • LineType::Ascii or Ascii - ASCII character edges
  • LineType::AsciiRound or AsciiRound - ASCII rounded edges
  • LineType::SingleRound or SingleRound - single rounded edges
  • LineType::Braille or Braille - braille character edges

A GraphView supports the following edge routing algorithms:

  • graphview::EdgeRouting::Direct or Direct - draw edges as direct lines between nodes
  • graphview::EdgeRouting::Orthogonal or Orthogonal - draw edges as orthogonal lines (straight lines with right-angled corners) between attachment points on node rectangles. This usually produces a clearer layout than direct lines, especially on larger graphs.

A GraphView supports the following node arrangement algorithms:

  • graphview::ArrangeMethod::None or None - no automatic arrangement (use custom positions)
  • graphview::ArrangeMethod::Grid or Grid - arrange nodes in a grid with spacing
  • graphview::ArrangeMethod::GridPacked or GridPacked - arrange nodes in a compact grid
  • graphview::ArrangeMethod::Circular or Circular - arrange nodes in a circle
  • graphview::ArrangeMethod::Hierarchical or Hierarchical - arrange nodes hierarchically with spacing
  • graphview::ArrangeMethod::HierarchicalPacked or HierarchicalPacked - arrange nodes hierarchically, compactly
  • graphview::ArrangeMethod::ForceDirected or ForceDirected - use force-directed layout algorithm

Some examples that use these parameters:

let search_enabled = graphview!("x:0,y:0,w:50,h:30,flags:[SearchBar]");
let styled_graph = graphview!("d:f,line-type:Double,routing:Orthogonal,arrange:Circular,arrows:true");
let highlighted = graphview!("d:f,hie:true,hoe:true,back:{.,gray,black}");

Events

To intercept events from a GraphView, implement GraphViewEvents<T> on the window that runs the event loop. Every method has a default implementation that returns EventProcessStatus::Ignored, so you only override what you need.

pub trait GraphViewEvents<T> {
    fn on_current_node_changed(&mut self, handle: Handle<GraphView<T>>) -> EventProcessStatus;
    fn on_node_action(&mut self, handle: Handle<GraphView<T>>, node_index: usize) -> EventProcessStatus;
    // Ctrl+click on empty canvas — graph coordinates (see Mouse interaction).
    fn on_request_new_node(&mut self, handle: Handle<GraphView<T>>, point: Point) -> EventProcessStatus;
    // Right-drag finished on another node — zero-based indices as u32.
    fn on_request_new_edge(&mut self, handle: Handle<GraphView<T>>, from: u32, to: u32) -> EventProcessStatus;
    // MultiSelect only — selection set changed (user or modify_graph / set_selected).
    fn on_selection_changed(&mut self, handle: Handle<GraphView<T>>) -> EventProcessStatus;
}

Handle on_request_new_node / on_request_new_edge by calling GraphView::modify_graph to append nodes or edges, or ignore them if your graph is read-only.

Methods

Besides the Common methods for all Controls a GraphView also has the following additional methods:

MethodPurpose
set_graph(...)Sets the graph data to be displayed. Takes a Graph<T> object containing nodes and edges.
graph()Returns a reference to the current graph data.
set_background(...)Sets the background character for the GraphView.
clear_background()Clears the background character, making it transparent.
set_edge_routing(...)Sets the edge routing algorithm (Direct or Orthogonal).
set_edge_line_type(...)Sets the line type used for drawing edges.
enable_edge_highlighting(...)Enables or disables highlighting of incoming and outgoing edges for the current node. Takes two boolean parameters: incoming and outgoing.
enable_arrow_heads(...)Enables or disables arrow heads on directed edges.
arrange_nodes(...)Applies a layout algorithm to arrange the nodes in the graph. Takes an ArrangeMethod parameter.
modify_graph(...)Runs a closure with an EditableGraph so you can add or remove nodes and edges, change the current selection, and edit node or edge properties; the control repaints and refreshes geometry when the closure reports changes.
selected_count()Returns how many nodes have selected == true (meaningful when MultiSelect is enabled).

Building a Graph<T>

Associated functionPurpose
Graph::new(nodes, edges)Takes owned Vec<Node<T>> and Vec<Edge>. Drops edges whose endpoints are not valid node indices, then rebuilds each node’s edges_in / edges_out lists.
Graph::with_slices(nodes, edges, directed)Clones each T into a default-styled node via NodeBuilder, then builds edges from (from, to) pairs with the same directed value on every edge. Requires T: Clone.
Graph::with_slices_and_border(...)Same as with_slices, but every node is created with the given LineType border.

Graph::default() yields an empty graph (no nodes, no edges) with default internal buffers.

Reading a graph (graph())

The reference from graph() is &Graph<T>: read-only access to nodes, edges, and the current selection.

MethodPurpose
current_node()Some(&Node<T>) for the focused node, or None when there are no nodes.
node(index)Some(&Node<T>) at the zero-based index, or None if out of range.
nodes_count()Number of nodes.

In-place editing (modify_graph)

Call graph_view.modify_graph(|g| { ... }) so the closure receives an EditableGraph. The closure can call:

MethodPurpose
node(index)Some(EditableNode) to read or change bounds, position, size, label alignment, text attribute, or border for that node.
nodes_count(), add_node, delete_nodeInspect size, append a built Node<T>, or remove a node (incident edges removed; remaining edge endpoints renumbered).
edge(index)Some(EditableEdge) to read or change per-edge line type and character attribute.
edges_count(), add_edge, delete_edgeEdge list maintenance; add_edge returns false if endpoints are invalid.
set_current_node, current_nodeUpdate or read the selection index tracked for this edit (applied to the inner Graph when the closure finishes).

After the closure returns, the control updates edge connectivity, may resize the backing surface, and repaints if anything changed. If MultiSelect is enabled and the closure changes any node’s multi-selection state (set_selected), on_selection_changed may be dispatched after the closure returns.

graph_view.modify_graph(|g| {
    if let Some(mut n) = g.node(0) {
        n.set_value("Updated");
    }
});

Node and EditableNode

On a read-only Node<T> from graph().node(i) or graph().current_node():

MethodPurpose
value()Reference to the user payload T (your GraphNode type).
is_selected()Whether the node is in the multi-selection set. Meaningful when the GraphView was created with Flags::MultiSelect; otherwise this flag is not used by the control.

Inside modify_graph, EditableNode also exposes layout and style APIs, for example bounds / set_bounds, position / set_position, size / set_size, value / value_mut / set_value, text alignment and attribute get/set/clear, border get/set/clear, and set_selected / is_selected for the multi-selection checkbox state. Most setters flip an internal “changed” flag so the graph view knows to repaint; value_mut does not set that flag by itself.

EditableEdge

Inside modify_graph, EditableEdge exposes from_node_id, to_node_id, and directed (read-only), plus attribute / set_attribute and line_type / set_line_type for styling overrides.

Key association

The following keys are processed by a GraphView control if it has focus:

KeyPurpose
Arrow KeysNavigate between nodes in the specified direction (finds the closest node in that direction).
Ctrl+Arrow KeysWithout MultiSelect: move the current node by one cell. With MultiSelect: move every selected node by one cell in that direction; if that moves nothing, falls back to nudging the current node (same as non–multi-select).
SpaceWhen MultiSelect is enabled: toggle whether the current node is in the selection set (same effect as a short Ctrl+click on that node). Ignored if multi-select UI is off or the graph is empty.
Ctrl+AWhen MultiSelect is enabled: if every visible (non–search-filtered) node is selected, clear selection on those nodes; otherwise select all visible nodes. Moves the current node to the first visible node. Ignored if multi-select UI is off or the graph is empty.
EnterTriggers a node action event (on_node_action) for the current node.
Ctrl+TabMove to the next node in the graph (by index).
Ctrl+Shift+TabMove to the previous node in the graph (by index).
Alt+Arrow KeysScroll the view in the specified direction (when scrollbars are enabled).
Page Up/Page DownScroll the view up or down by one page.
HomeScroll to the top-left of the graph.
EndScroll to the bottom-right of the graph.
EscapeClear search text (if search bar is active), or exit search mode.
Enter (in search)Go to next matching node.
Ctrl+Enter (in search)Go to previous matching node.

Mouse interaction

The GraphView supports various mouse interactions:

  • Click (left) on a node: Sets that node as current. With MultiSelect, a normal click also clears other selected nodes and selects only this one, then begins a drag if you keep the button down (see below).
  • Ctrl+click (left) on a node (MultiSelect only): If you release without moving more than a few pixels, toggles that node’s inclusion in the selection set and raises on_selection_changed when the set changes. If you drag past the threshold, behaves like dragging the selection (see Drag nodes).
  • Double-click (left) on a node: Triggers on_node_action for that node.
  • Drag nodes: Press on a node and drag to move it. With MultiSelect, all selected nodes move together when you drag from a selected node; dragging from an unselected node moves only that node (and selects it on press, unless Ctrl+click mode applies).
  • Click / drag empty graph area (left): Dragging pans the view (same as before). Releasing after a click with no movement clears the multi-selection set (MultiSelect only). Ctrl+click on empty space raises on_request_new_node with the graph-space Point under the cursor so you can create a node at that position.
  • Right button — new edge: Press right on a node and drag; a preview line follows the pointer. Release right on a different node to raise on_request_new_edge(from, to) with zero-based indices as u32. Releasing elsewhere cancels. Moving the mouse out of the control or losing focus clears the preview.
  • Mouse wheel: Scrolls the view (including horizontal where supported).
  • Hover: Updates hover highlighting; if GraphNode::write_description returns non-empty text, it is shown as a tooltip over the node rectangle.

Graph item

A GraphView can display a graph data structure. The graph data structure is a collection of nodes and edges. Each node can contain a data object of type T that must implement the GraphNode trait.

pub trait GraphNode {
    fn write_label(&self, f: &mut dyn std::fmt::Write, size: Size) -> std::fmt::Result;
    fn write_description(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
        Ok(())
    }
    fn prefered_size(&self) -> Size;
}

where:

  • write_label is used to write the label of the node to the formatter (this method is required and must be implemented by the data object)
  • write_description is used to write the description of the node. The default implementation writes nothing; if you override it, the text can be shown as a tooltip when the user hovers over the node.
  • prefered_size is used to get the preferred size of the node. This method is being used when a graph is provided without an explicit size for a node.

Remarks: By default, the GraphNode trait is implemented for the following types:

  • &str
  • String
  • u8, u16, u32, u64, usize
  • i8, i16, i32, i64, isize

Creating a graph

A graph can be created in several ways:

  • Using the Graph::new method (a vector of nodes and a vector of edges; invalid edges are removed as described above)
    let graph = Graph::new(nodes, edges);
    
  • Using the Graph::with_slices method (and providing slices of nodes and edges)
    let graph = Graph::with_slices(nodes, edges, directed);
    
  • Using the Graph::with_slices_and_border method (and providing slices of nodes and edges, and a border type)
    let graph = Graph::with_slices_and_border(nodes, edges, border, directed);
    

To create a node (that will further be used in the graph) you can use the NodeBuilder struct.

let node = NodeBuilder::new(node_data).build();

with the following builder methods:

  • size - the size of the node (if not provided, the preferred size as it is returned by the GraphNode implementation will be used)
  • position - the position of the node (if not provided, the node will be placed at the top-left corner of the graph)
  • border - the border type for the node (if not provided, no border will be drawn)
  • text_alignment - the text alignment for the node (if not provided, the text will be centered)
  • text_attribute - the text attribute for the node (if not provided, the text will be displayed in the default attribute extracted from the terminal theme)

Similarly, to create an edge (that will further be used in the graph) you can use the EdgeBuilder struct.

let edge = EdgeBuilder::new(from, to).build();

with the following builder methods:

  • directed - if the edge is directed (with an arrow) or not (if not provided, the edge will be undirected)
  • attribute - the attribute for the edge (if not provided, the edge will be displayed in the default attribute extracted from the terminal theme)
  • line_type - the line type for the edge (if not provided, the default line type will be used)

When building an edge, the from and to parameters are the indices of the nodes in the graph. For example, in a graph with 5 nodes, the edge from node 2 to node 4 will be created with EdgeBuilder::new(2, 4).build() (indices are zero-based).

Some examples on how to create a graph:

  1. A simple graph with 5 nodes and 4 edges (using slices):
    let graph = Graph::with_slices(
                     &["A", "B", "C", "D", "E"],         // nodes (of type &str)
                     &[(0, 1), (0, 2), (1, 3), (2, 4)],  // edges between nodes
                     true                                // directed edges
                 );
    
  2. A graph with a border around each node:
    let graph = Graph::with_slices_and_border(
                     &["A", "B", "C", "D", "E"],         // nodes (of type &str)
                     &[(0, 1), (0, 2), (1, 3), (2, 4)],  // edges between nodes
                     LineType::Double,                   // border type
                     true                                // directed edges
                 );
    
  3. A more complex graph where each node is created manually:
    let nodes = vec![
         NodeBuilder::new("A").size(Size::new(10, 1)).build(),
         NodeBuilder::new("B").build(),
         NodeBuilder::new("C").text_alignment(TextAlignment::Left).build(),
         NodeBuilder::new("D").text_alignment(TextAlignment::Right).build(),
         NodeBuilder::new("E").border(LineType::Ascii).build(),
     ];
     let edges = vec![
         EdgeBuilder::new(0, 1).build(),
         EdgeBuilder::new(0, 2).build(),
         EdgeBuilder::new(1, 3).build(),
         EdgeBuilder::new(2, 4).build(),
     ];
     let graph = Graph::new(nodes, edges);                
    

Example

The following code creates a window with a GraphView displaying a simple hierarchy. When nodes are selected or activated, the window title updates to show the current action. The other GraphViewEvents methods (on_request_new_node, on_request_new_edge, on_selection_changed) are left at their default implementations here.

For a full editor sample (app bar, modify_graph, MultiSelect, and handlers for new node/edge and selection), see the examples/graph_editor example in the AppCUI-rs repository.

use appcui::prelude::*;

type NodeValue = &'static str;

#[Window(events = GraphViewEvents<NodeValue>)]
struct MyWin {
    graph_view: Handle<GraphView<NodeValue>>,
}
impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: Window::new("Graph Demo", layout!("d:f"), window::Flags::None),
            graph_view: Handle::None,
        };

        // Create a simple graph
        let nodes = &["Root", "Child 1", "Child 2", "Grandchild"];
        let edges = &[(0, 1), (0, 2), (1, 3)];
        let graph = graphview::Graph::with_slices(nodes, edges, true);

        // Create and configure the GraphView
        let mut gv = graphview!("d:f,arrange:Grid,routing:Orthogonal,arrows:true,flags:[ScrollBars,SearchBar],hie:true");
        gv.set_graph(graph);

        win.graph_view = win.add(gv);
        win
    }
}
impl GraphViewEvents<NodeValue> for MyWin {
    fn on_current_node_changed(&mut self, handle: Handle<GraphView<&'static str>>) -> EventProcessStatus {
        if let Some(gv) = self.control(handle) {
            if let Some(node) = gv.graph().current_node() {
                let title = format!("Graph Demo - Selected: {}", node.value());
                self.set_title(&title);
            }
        }
        EventProcessStatus::Processed
    }

    fn on_node_action(&mut self, handle: Handle<GraphView<&'static str>>, node_index: usize) -> EventProcessStatus {
        if let Some(gv) = self.control(handle) {
            if let Some(node) = gv.graph().node(node_index) {
                let title = format!("Graph Demo - Action on: {}", node.value());
                self.set_title(&title);
            }
        }
        EventProcessStatus::Processed
    }
}


fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}