主视图结构

`FlowEditorView` 是一个持有大量状态的 GPUI 视图。理解它的字段构成,是理解后续渲染与交互的前提。

字段总览

按职责分组,`FlowEditorView` 的字段大致可以划分为五类:

| 类别 | 字段 | 职责 | |------|------|------| | 数据模型 | `graph: FlowGraph` | 被编辑的有向图 | | 视图变换 | `viewport: Viewport` | offset / scale / 逻辑↔屏幕变换 | | 交互状态 | `interaction: InteractionState` | 当前所处的交互阶段(Idle/Panning/Dragging/...) | | 选择状态 | `selected`, `hovered`, `hovered_plus` | 当前选中节点、悬停节点、悬停的「+」边 | | 节点元信息 | `registry: Arc<NodeRegistry>` | 节点 schema、内容尺寸、默认数据 | | 渲染配置 | `default_edge_type`, `layout_direction`, `show_grid`, `grid_spacing`, `drag_enabled` | 渲染风格开关 | | 主题与语言 | `theme`, `language`, `syntax_service` | 视觉风格与语法高亮 | | 面板 | `panel_view`, `panel_width`, `resizing_panel` | 右侧属性面板 | | 布局缓存 | `cached_body_groups`, `cached_all_body_nodes`, `cached_hidden_nodes` | 折叠后的子图映射 | | 扩展点 | `custom_toolbar`, `data_type_provider` | 外部插件注入 |

构造流程

`FlowEditorView::new(graph, cx)` 做的事情远不止「把 graph 存起来」:

pub fn new(graph: FlowGraph, cx: &mut Context<Self>) -> Self {
    let mut registry = NodeRegistry::new();
    register_all(&mut registry); // 注册所有内置节点类型

    let mut view = Self {
        graph,
        viewport: Viewport::default(),
        interaction: InteractionState::Idle,
        registry: Arc::new(registry),
        selected: None,
        hovered: None,
        hovered_plus: None,
        default_edge_type: EdgeType::SmoothStep,   // 默认 SmoothStep
        layout_direction: LayoutDirection::Horizontal,
        show_grid: true,
        grid_spacing: 24.0,
        drag_enabled: true,
        theme: Theme::default(),
        panel_view: None,
        syntax_service: cx.default_syntax_service(),
        language: Language::Rust,
        cached_body_groups: HashMap::new(),
        cached_all_body_nodes: HashSet::new(),
        cached_hidden_nodes: HashSet::new(),
        custom_toolbar: Vec::new(),
        data_type_provider: None,
        panel_width: Pixels(320.0),
        resizing_panel: false,
    };
    view.relayout();           // 首次布局
    view
}

关键点:构造时立即调用 `relayout()`,确保第一帧渲染时所有节点都已有合法 position。

布局:relayout 的三段式

`relayout()` 是主视图与 Dagre 布局器的握手点,分三步:

┌──────────────────────────────────────────────────────┐
│ 1. sync_node_sizes()                                 │
│    遍历所有节点,用 IFlowNode::content_size 同步      │
│    node.size。保证布局器拿到准确尺寸。                 │
├──────────────────────────────────────────────────────┤
│ 2. DagreLayout.layout(&mut graph, direction)         │
│    调用 Dagre 算法,回填每个节点的 position。          │
├──────────────────────────────────────────────────────┤
│ 3. 更新三份缓存                                       │
│    cached_body_groups:  NodeId -> 折叠子节点集合       │
│    cached_all_body_nodes: 所有 body 节点扁平集合       │
│    cached_hidden_nodes:  当前被折叠隐藏的节点集合      │
└──────────────────────────────────────────────────────┘

为什么需要这三份缓存?因为折叠节点是「把子图收进父节点」的操作,渲染时需要快速判断:

- 这个节点是否是某折叠节点的 body(属于 `cached_all_body_nodes`)? - 这个节点的 body 集合是什么(查 `cached_body_groups`)? - 这个节点当前应不应该被画出来(不在 `cached_hidden_nodes` 里)?

单节点尺寸优化

`sync_node_sizes()` 会遍历所有节点,开销不可忽视。当用户只是修改了单个节点的内容(比如编辑了一段代码),调用 `relayout()` 太重。于是有了 `update_node_size_if_changed(node_id)`:

pub fn update_node_size_if_changed(&mut self, node_id: NodeId) -> bool {
    // 仅当该节点 content_size 真正变化时才返回 true
    // 调用方据此决定是否需要触发完整 relayout
}

返回值约定:`true` 表示尺寸变了,调用方应再触发一次 `relayout()`;`false` 表示可跳过。

公开 API 一览

| 方法 | 作用 | |------|------| | `auto_layout(cx)` | `relayout()` + `cx.notify()`,对外暴露的「重新布局」入口 | | `set_layout_direction(dir)` | 切换横向 / 纵向布局 | | `set_drag_enabled(b)` | 开关节点拖拽 | | `set_grid_spacing(f)` | 调整网格密度 | | `set_show_grid(b)` | 显示 / 隐藏网格 | | `toggle_theme()` / `set_theme(t)` | 切换主题 | | `set_syntax_service(s)` | 注入语法高亮服务 | | `add_toolbar_provider(p)` | 注册自定义工具栏项 | | `set_data_type_provider(p)` | 注入数据类型推断器 | | `set_language(l)` / `toggle_language()` | 切换代码语言 | | `set_graph(graph, cx)` | 整体替换图,并重置 selected/hovered/panel/viewport | | `insert_node_at_edge(edge, kind, cx)` | 在边上插入节点 | | `handle_node_action(node, action, cx)` | 分发 Delete / ToggleCollapse / SetData |

port_sides:方向决定端口朝向

pub fn port_sides(&self) -> (PortSide, PortSide) {
    match self.layout_direction {
        LayoutDirection::Horizontal => (PortSide::Right, PortSide::Left),
        LayoutDirection::Vertical   => (PortSide::Bottom, PortSide::Top),
    }
}

返回 `(out_side, in_side)`。横向布局时输出端口在右、输入端口在左;纵向布局时输出在下、输入在上。这个函数贯穿边几何与命中测试,是「方向感」的单一事实来源。

set_graph:彻底重置

`set_graph(graph, cx)` 不是简单赋值,而是「软重启」:

set_graph
 ├─ graph = new_graph
 ├─ selected   = None
 ├─ hovered    = None
 ├─ hovered_plus = None
 ├─ panel_view = None
 ├─ viewport   = Viewport::default()   // 回到原点,scale=1
 └─ relayout()

这保证切换图时不会残留旧图的选择状态或视口偏移,避免「我看到选中了一个不存在的节点」这类 bug。

小结

`FlowEditorView` 是一个「胖视图」:它直接持有图、视口、交互状态、缓存和扩展点,没有把它们拆到多个 Entity。这种设计牺牲了一些解耦,换来的是渲染与交互之间零成本的数据共享——这正是节点编辑器这种「高频命中测试 + 高频重绘」场景所需要的。

下一节:[渲染管线](render-pipeline.md)