ToolbarProvider 工具栏扩展

工具栏是编辑器顶部的一排按钮。rust-agent-flow 内置了一组(缩放、布局、边类型、点阵等),但「数据源切换」「拖拽开关」「主题/语言切换」这类**与具体业务相关**的控件,核心库既不该也不便硬编码。`ToolbarProvider` 就是把这些决定权交还给调用方的扩展点。

扩展点四件套

rust-agent-flow 所有扩展点遵循同一形态:

// 1. trait 定义能力
pub trait ToolbarProvider: Send + Sync {
    fn render_items(&self, ctx: &ToolbarCtx) -> Vec<AnyElement>;
}
// 2. Arc<dyn Trait> 别名,便于跨线程共享
pub type SharedToolbarProvider = Arc<dyn ToolbarProvider>;
// 3. setter 注入到 FlowEditorView
editor.add_toolbar_provider(provider, cx);
// 4. 渲染时按需调用

记住这个模板,本章其余三个扩展点(IDataTypeProvider / SyntaxService / Theme)都是它的变体。

ToolbarCtx:渲染上下文

`render_items` 收到一个 `ToolbarCtx`,里面是构建工具项所需的全部信息:

pub struct ToolbarCtx {
    pub entity: Entity<FlowEditorView>,  // 编辑器句柄,回调中 update 它
    pub theme: Theme,                     // 当前主题颜色
    pub language: Language,               // 当前语言(决定 tooltip 文案)
    pub drag_enabled: bool,               // 拖拽开关状态(决定按钮 selected 态)
}

`entity` 是关键:provider 在按钮 `on_click` 回调里 `entity.update(cx, |this, cx| { ... })` 调用编辑器方法(`set_graph` / `toggle_drag` / `toggle_theme`...)。框架不规定你调什么,只把句柄递给你。

注入与渲染

`add_toolbar_provider` 把 provider push 进 `custom_toolbar: Vec<SharedToolbarProvider>`:

pub fn add_toolbar_provider(&mut self, provider: SharedToolbarProvider, cx) {
    self.custom_toolbar.push(provider);
    cx.notify();
}

**累积注入**:多次调用可注入多个 provider,按注入顺序渲染。工具栏渲染时,内置项结束后画一条竖线分隔符,再依次调用各 provider 的 `render_items`:

[内置: 放大 缩小 适应 布局 边类型 点阵] │ [provider1 项...] │ [provider2 项...]
                                          ▲ 竖线分隔

注意:每个 provider 的元素之间不画分隔符,provider 之间也不画——分隔符仅出现在「内置 ↔ 自定义」边界。

实战:数据源选择器

Demo 的 `DataSourceToolbar` 是典型用法。它持有一份当前数据源状态,切换时调用 `set_graph` 重建图:

pub struct DataSourceToolbar {
    current: Arc<Mutex<DemoDataSource>>,
}

impl ToolbarProvider for DataSourceToolbar {
    fn render_items(&self, ctx: &ToolbarCtx) -> Vec<AnyElement> {
        let lang = ctx.language;
        let current = *self.current.lock().unwrap();
        let entity = ctx.entity.clone();
        let current_state = self.current.clone();

        let btn = Button::new("demo-data-source")
            .icon(IconName::ALargeSmall)
            .small().ghost()
            .tooltip(t(lang, TKey::TbDataSource))
            .dropdown_menu(move |menu, _w, _cx| {
                let mut menu = menu;
                for &ds in DemoDataSource::all() {
                    let label = ds.label(lang);
                    let entity = entity.clone();
                    let current_state = current_state.clone();
                    menu = menu.item(
                        PopupMenuItem::new(label)
                            .checked(ds == current)
                            .on_click(move |_, _, cx| {
                                *current_state.lock().unwrap() = ds;
                                let graph = ds.to_graph();
                                entity.update(cx, |this, cx| this.set_graph(graph, cx));
                            }),
                    );
                }
                menu
            })
            .into_any_element();
        vec![btn]
    }
}

要点:

- `current` 用 `Arc<Mutex<...>>` 是因为闭包被 `move` 进 `DropdownMenu`,需在多份闭包间共享可变状态 - 切换时先更新自身 `current`,再 `set_graph` 重建图——保证下次渲染时 `checked` 正确 - `entity.clone()` 给每个菜单项一份句柄,回调里 `entity.update` 改编辑器

实战:应用控件工具栏

`AppControlsToolbar` 展示「框架提供能力方法、调用方决定 UI 呈现」的分工:

impl ToolbarProvider for AppControlsToolbar {
    fn render_items(&self, ctx: &ToolbarCtx) -> Vec<AnyElement> {
        let lang = ctx.language;
        // 拖拽开关:调 toggle_drag
        let drag_btn = Button::new("app-toggle-drag")
            .icon(FlowIcon::Drag).small().ghost()
            .selected(ctx.drag_enabled)
            .tooltip(t(lang, TKey::TbToggleDrag))
            .on_click(move |_, _, cx| {
                drag_entity.update(cx, |this, cx| this.toggle_drag(cx));
            });
        // 主题切换:调 toggle_theme,图标随当前主题变
        let theme_btn = Button::new("app-toggle-theme")
            .icon(if ctx.theme.is_dark { IconName::Sun } else { IconName::Moon })
            .on_click(move |_, _, cx| {
                theme_entity.update(cx, |this, cx| this.toggle_theme(cx));
            });
        // 语言切换:调 toggle_language
        let lang_btn = Button::new("app-toggle-language")
            .icon(IconName::Globe)
            .on_click(move |_, _, cx| {
                lang_entity.update(cx, |this, cx| this.toggle_language(cx));
            });
        vec![drag_btn, theme_btn, lang_btn]
    }
}

框架只提供 `toggle_drag` / `toggle_theme` / `toggle_language` 三个能力方法;按钮放哪、用什么图标、是否显示文字,全由调用方在 provider 里决定。这就是「能力在框架、呈现归调用方」的边界。

注入时机

通常在 `FlowEditorView::new` 后、首帧渲染前注入:

let view = cx.new(|cx| {
    let mut editor = FlowEditorView::new(graph, cx);
    editor.auto_layout(cx);
    editor.add_toolbar_provider(Arc::new(DataSourceToolbar::new(initial_ds)), cx);
    editor.add_toolbar_provider(Arc::new(AppControlsToolbar::new()), cx);
    editor
});

注入后 `cx.notify()` 触发重绘,工具栏即带上自定义项。

小结

`ToolbarProvider` 是扩展点四件套的范本:trait 定义 `render_items`、`SharedToolbarProvider` 别名共享、`add_toolbar_provider` 累积注入、`ToolbarCtx` 递送编辑器句柄与上下文。Demo 的数据源选择器与应用控件工具栏展示了「能力在框架、呈现归调用方」的分工。

下一节:[IDataTypeProvider 数据类型扩展](data-type-provider.md)