Files
qiming/qimingclaw/specs/agent-client/vendorsdoc/gpui-component.md

50 KiB
Raw Permalink Blame History

gpui-component

项目概述

基于 GPUI 的跨平台桌面 UI 组件库,提供 60+ 组件,受 macOS/Windows 控件和 shadcn/ui 启发。

本地路径: vendors/gpui-component

目录结构

gpui-component/
├── Cargo.toml                          # workspace 配置
├── crates/
│   ├── ui/                             # 核心 UI 库
│   │   ├── Cargo.toml
│   │   ├── src/
│   │   │   ├── lib.rs                  # 库入口
│   │   │   ├── init.rs                 # 初始化
│   │   │   ├── app.rs                  # 应用生命周期
│   │   │   ├── theme/                  # 主题系统
│   │   │   │   ├── mod.rs
│   │   │   │   ├── color.rs
│   │   │   │   ├── registry.rs
│   │   │   │   ├── schema.rs
│   │   │   │   └── theme_color.rs
│   │   │   ├── input/                  # 输入组件
│   │   │   │   ├── mod.rs
│   │   │   │   ├── input.rs
│   │   │   │   ├── number_input.rs
│   │   │   │   ├── state.rs
│   │   │   │   └── lsp/                # LSP 集成
│   │   │   ├── button/                 # 按钮组件
│   │   │   │   └── mod.rs
│   │   │   ├── chart/                  # 图表组件
│   │   │   │   └── mod.rs
│   │   │   ├── dock/                   # 停靠面板
│   │   │   │   └── mod.rs
│   │   │   ├── menu/                   # 菜单系统
│   │   │   │   └── mod.rs
│   │   │   ├── list/                   # 列表组件
│   │   │   │   └── mod.rs
│   │   │   ├── table/                  # 表格组件
│   │   │   │   ├── mod.rs
│   │   │   │   ├── table.rs
│   │   │   │   └── data_table.rs       # 高级表格
│   │   │   ├── dialog/                 # 对话框
│   │   │   │   └── mod.rs
│   │   │   ├── navigation/             # 导航组件
│   │   │   │   ├── mod.rs
│   │   │   │   ├── breadcrumb.rs
│   │   │   │   ├── pagination.rs
│   │   │   │   └── tabs.rs
│   │   │   ├── feedback/               # 反馈组件
│   │   │   │   ├── mod.rs
│   │   │   │   ├── alert.rs
│   │   │   │   ├── loading.rs
│   │   │   │   ├── progress.rs
│   │   │   │   └── toast.rs
│   │   │   ├── overlay/                # 浮层组件
│   │   │   │   ├── mod.rs
│   │   │   │   ├── popover.rs
│   │   │   │   ├── tooltip.rs
│   │   │   │   └── sheet.rs
│   │   │   ├── form/                   # 表单组件
│   │   │   │   ├── mod.rs
│   │   │   │   └── form.rs
│   │   │   ├── scroll/                 # 滚动组件
│   │   │   │   └── mod.rs
│   │   │   ├── avatar/                 # 头像组件
│   │   │   │   └── mod.rs
│   │   │   ├── badge/                  # 徽章组件
│   │   │   │   └── mod.rs
│   │   │   ├── card/                   # 卡片组件
│   │   │   │   └── mod.rs
│   │   │   ├── divider/                # 分隔线
│   │   │   │   └── mod.rs
│   │   │   ├── icon/                   # 图标
│   │   │   │   └── mod.rs
│   │   │   ├── image/                  # 图像
│   │   │   │   └── mod.rs
│   │   │   ├── select/                 # 选择器
│   │   │   │   └── mod.rs
│   │   │   ├── checkbox/               # 复选框
│   │   │   │   └── mod.rs
│   │   │   ├── radio/                  # 单选框
│   │   │   │   └── mod.rs
│   │   │   ├── switch/                 # 开关
│   │   │   │   └── mod.rs
│   │   │   ├── slider/                 # 滑块
│   │   │   │   └── mod.rs
│   │   │   ├── date_picker/            # 日期选择
│   │   │   │   └── mod.rs
│   │   │   ├── color_picker/           # 颜色选择
│   │   │   │   └── mod.rs
│   │   │   ├── tree/                   # 树形组件
│   │   │   │   └── mod.rs
│   │   │   ├── virtual_list/           # 虚拟列表
│   │   │   │   └── mod.rs
│   │   │   ├── markdown/               # Markdown
│   │   │   │   └── mod.rs
│   │   │   └── ...
│   │   └── Cargo.toml
│   │
│   ├── story/                          # 组件展示应用
│   │   ├── Cargo.toml
│   │   ├── src/
│   │   │   ├── main.rs
│   │   │   ├── app.rs
│   │   │   ├── storybook.rs            # StoryBook 框架
│   │   │   └── stories/                # 组件示例
│   │   │       ├── button_story.rs
│   │   │       ├── input_story.rs
│   │   │       └── ...
│   │   └── Cargo.toml
│   │
│   ├── macros/                         # 过程宏
│   │   ├── Cargo.toml
│   │   ├── src/
│   │   │   └── lib.rs
│   │   └── Cargo.toml
│   │
│   ├── assets/                         # 静态资源
│   │   ├── src/
│   │   │   └── lib.rs
│   │   ├── icons/                      # 图标资源
│   │   └── themes/                     # 主题文件
│   │       └── Cargo.toml
│   │
│   └── webview/                        # WebView 支持
│       ├── Cargo.toml
│       └── src/
│           ├── lib.rs
│           └── webview.rs
│
└── Cargo.toml

核心依赖

[workspace]
members = ["crates/*"]

[dependencies]
# GPUI 框架
gpui = "1.0"

# 网络
reqwest = { version = "0.11", features = ["json", "blocking"] }

# 文本处理
ropey = "1.6"
unicode-width = "0.1"

# 文件系统
notify = { version = "6.0", features = ["default"] }

# 序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 日志
tracing = "0.1"
tracing-subscriber = "0.3"

# 国际化
rust-i18n = "2.0"

# LSP
lsp-types = "0.94"

主题系统

主题结构

// crates/ui/src/theme/mod.rs

use std::sync::Arc;

/// 主题配置
#[derive(Clone, Debug, Default)]
pub struct Theme {
    /// 颜色配置
    pub colors: ThemeColor,

    /// 亮色主题配置
    pub light_theme: Rc<ThemeConfig>,

    /// 暗色主题配置
    pub dark_theme: Rc<ThemeConfig>,

    /// 当前主题模式
    pub mode: ThemeMode,

    /// 字体配置
    pub font_family: SharedString,
    pub font_size: Pixels,

    /// 圆角
    pub radius: Pixels,

    /// 滚动条显示
    pub scrollbar_show: ScrollbarShow,

    /// 语法高亮主题
    pub highlight_theme: Arc<HighlightTheme>,
}

pub enum ThemeMode {
    Light,
    Dark,
    System,
}

pub struct ThemeConfig {
    /// 背景色
    pub background: Hsla,
    /// 前景色
    pub foreground: Hsla,
    /// 边框色
    pub border: Hsla,
    /// 主要色
    pub primary: Hsla,
    /// 次要色
    pub secondary: Hsla,
    /// 强调色
    pub accent: Hsla,
    /// 成功色
    pub success: Hsla,
    /// 警告色
    pub warning: Hsla,
    /// 错误色
    pub error: Hsla,
    /// 信息色
    pub info: Hsla,
    /// 悬停色
    pub hover: Hsla,
    /// 激活色
    pub active: Hsla,
    /// 选中色
    pub selected: Hsla,
    /// 禁用色
    pub disabled: Hsla,
    /// 边框样式
    pub border: BorderStyle,
    /// 阴影
    pub shadow: Shadow,
}

/// 主题颜色
#[derive(Clone, Debug)]
pub struct ThemeColor {
    pub app_background: Hsla,
    pub sidebar_background: Hsla,
    pub panel_background: Hsla,
    pub editor_background: Hsla,
    pub status_bar_background: Hsla,
    pub title_bar_background: Hsla,
    // ...
}

ActiveTheme Trait

// 主题 Trait用于获取当前主题
pub trait ActiveTheme {
    fn theme(&self) -> &Theme;
}

impl ActiveTheme for AppContext {
    fn theme(&self) -> &Theme {
        Theme::global(self)
    }
}

// 使用示例
fn my_component(cx: &mut ViewContext<Self>) -> impl IntoElement {
    div()
        .bg(cx.theme().colors.background)
        .text_color(cx.theme().colors.foreground)
        .border_1()
        .border_color(cx.theme().colors.border)
}

核心组件

Button 组件

// crates/ui/src/button/mod.rs

use gpui::{div, prelude::*, AnyElement, ViewContext};

pub struct Button {
    /// 按钮文本
    label: SharedString,
    /// 图标
    icon: Option<SharedString>,
    /// 变体
    variant: ButtonVariant,
    /// 尺寸
    size: ButtonSize,
    /// 是否加载中
    loading: bool,
    /// 是否禁用
    disabled: bool,
    /// 点击事件
    on_click: Arc<dyn Fn(&mut WindowContext)>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonVariant {
    Primary,
    Secondary,
    Outline,
    Ghost,
    Danger,
    Link,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonSize {
    Small,
    Medium,
    Large,
}

impl Button {
    pub fn new(
        label: impl Into<SharedString>,
        on_click: impl Fn(&mut WindowContext) + 'static,
    ) -> Self {
        Self {
            label: label.into(),
            icon: None,
            variant: ButtonVariant::Primary,
            size: ButtonSize::Medium,
            loading: false,
            disabled: false,
            on_click: Arc::new(on_click),
        }
    }

    pub fn with_icon(mut self, icon: impl Into<SharedString>) -> Self {
        self.icon = Some(icon.into());
        self
    }

    pub fn variant(mut self, variant: ButtonVariant) -> Self {
        self.variant = variant;
        self
    }

    pub fn size(mut self, size: ButtonSize) -> Self {
        self.size = size;
        self
    }

    pub fn loading(mut self, loading: bool) -> Self {
        self.loading = loading;
        self
    }

    pub fn disabled(mut self, disabled: bool) -> Self {
        self.disabled = disabled;
        self
    }
}

impl Render for Button {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let variant_style = match self.variant {
            ButtonVariant::Primary => button_primary_style(cx),
            ButtonVariant::Secondary => button_secondary_style(cx),
            ButtonVariant::Outline => button_outline_style(cx),
            ButtonVariant::Ghost => button_ghost_style(cx),
            ButtonVariant::Danger => button_danger_style(cx),
            ButtonVariant::Link => button_link_style(cx),
        };

        let size_style = match self.size {
            ButtonSize::Small => button_sm_style(cx),
            ButtonSize::Medium => button_md_style(cx),
            ButtonSize::Large => button_lg_style(cx),
        };

        let disabled = self.disabled || self.loading;

        div()
            .id("button")
            .px_4()
            .py_2()
            .rounded-lg()
            .font_medium()
            .cursor(disabled.then_some(Cursor::NotAllowed))
            .text_color(disabled.then_some(cx.theme().colors.disabled))
            .bg(disabled.then_some(variant_style.bg).unwrap_or(variant_style.bg))
            .border_1()
            .border_color(disabled.then_some(cx.theme().colors.disabled).unwrap_or(variant_style.border))
            .transition()
            .when(!disabled, |this| {
                this.hover(|s| s.bg(variant_style.hover_bg))
                    .active(|s| s.bg(variant_style.active_bg))
            })
            .when(self.loading, |this| {
                this.child(
                    div()
                        .animate()
                        .spin()
                        .w_4()
                        .h_4()
                        .border_2()
                        .border_current()
                        .rounded_full()
                )
            })
            .when(!self.loading && self.icon.is_some(), |this| {
                this.child(div().child(self.icon.clone().unwrap()))
            })
            .when(!self.loading, |this| {
                this.on_click(move |_, cx| {
                    if !disabled {
                        (self.on_click)(cx);
                    }
                })
            })
            .child(self.label.clone())
    }
}

/// 便捷函数
pub fn button(
    label: impl Into<SharedString>,
    on_click: impl Fn(&mut WindowContext) + 'static,
) -> Button {
    Button::new(label, on_click)
}

pub fn primary_button(
    label: impl Into<SharedString>,
    on_click: impl Fn(&mut WindowContext) + 'static,
) -> Button {
    Button::new(label, on_click).variant(ButtonVariant::Primary)
}

pub fn secondary_button(
    label: impl Into<SharedString>,
    on_click: impl Fn(&mut WindowContext) + 'static,
) -> Button {
    Button::new(label, on_click).variant(ButtonVariant::Secondary)
}

Input 组件

// crates/ui/src/input/mod.rs

use ropey::Rope;

pub struct Input {
    /// 绑定值
    value: SharedString,
    /// 占位符
    placeholder: SharedString,
    /// 是否只读
    readonly: bool,
    /// 是否禁用
    disabled: bool,
    /// 多行模式
    multiline: bool,
    /// 高度(多行模式)
    rows: usize,
    /// 焦点状态
    focused: bool,
    /// 事件
    on_change: Arc<dyn Fn(String, &mut WindowContext)>,
    on_focus: Arc<dyn Fn(&mut WindowContext)>,
    on_blur: Arc<dyn Fn(&mut WindowContext)>,
    on_enter: Arc<dyn Fn(&mut WindowContext)>,
}

impl Input {
    pub fn new(
        value: impl Into<SharedString>,
        on_change: impl Fn(String, &mut WindowContext) + 'static,
    ) -> Self {
        Self {
            value: value.into(),
            placeholder: "".into(),
            readonly: false,
            disabled: false,
            multiline: false,
            rows: 3,
            focused: false,
            on_change: Arc::new(on_change),
            on_focus: Arc::new(|_| {}),
            on_blur: Arc::new(|_| {}),
            on_enter: Arc::new(|_| {}),
        }
    }

    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
        self.placeholder = placeholder.into();
        self
    }

    pub fn disabled(mut self, disabled: bool) -> Self {
        self.disabled = disabled;
        self
    }

    pub fn multiline(mut self, rows: usize) -> Self {
        self.multiline = true;
        self.rows = rows;
        self
    }
}

impl Render for Input {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let theme = cx.theme();

        div()
            .w_full()
            .bg(theme.colors.surface)
            .border_1()
            .border_color(
                if self.focused {
                    theme.colors.primary
                } else {
                    theme.colors.border
                },
            )
            .rounded_md()
            .px_3()
            .py_2()
            .when(self.multiline, |this| this.min_h_10())
            .when(!self.multiline, |this| this.h_10())
            .child(
                if self.multiline {
                    TextArea::new(self.value.clone(), move |s, _| s)
                        .placeholder(self.placeholder.clone())
                        .disabled(self.disabled)
                        .on_change(self.on_change.clone())
                        .into_any_element()
                } else {
                    TextInput::new(self.value.clone(), move |s, _| s)
                        .placeholder(self.placeholder.clone())
                        .disabled(self.disabled)
                        .on_change(self.on_change.clone())
                        .into_any_element()
                }
            )
    }
}

DataTable 组件

// crates/ui/src/table/data_table.rs

use std::collections::HashMap;

/// 表格列配置
pub struct TableColumn<T> {
    /// 列标题
    pub title: SharedString,
    /// 列宽
    pub width: Pixels,
    /// 渲染函数
    pub render: Arc<dyn Fn(&T, &mut WindowContext) -> AnyElement>,
    /// 是否可排序
    pub sortable: bool,
    /// 对齐方式
    pub align: Align,
}

/// 表格状态
pub struct DataTableState<T> {
    /// 数据
    data: Vec<T>,
    /// 排序字段
    sort_by: Option<String>,
    /// 排序方向
    sort_direction: SortDirection,
    /// 过滤文本
    filter: String,
    /// 分页
    pagination: Option<PaginationState>,
}

/// 分页状态
pub struct PaginationState {
    pub page: usize,
    pub page_size: usize,
    pub total: usize,
}

pub enum SortDirection {
    Asc,
    Desc,
}

Dialog 组件

// crates/ui/src/dialog/mod.rs

pub struct Dialog {
    /// 标题
    title: SharedString,
    /// 内容
    content: AnyElement,
    /// 底部按钮
    actions: Vec<Button>,
    /// 是否模态
    modal: bool,
    /// 关闭回调
    on_close: Option<Arc<dyn Fn(&mut WindowContext)>>,
}

impl Dialog {
    pub fn new(title: impl Into<SharedString>) -> Self {
        Self {
            title: title.into(),
            content: div().into_any_element(),
            actions: vec![],
            modal: true,
            on_close: None,
        }
    }

    pub fn content(mut self, content: impl IntoElement) -> Self {
        self.content = content.into_any_element();
        self
    }

    pub fn action(mut self, button: Button) -> Self {
        self.actions.push(button);
        self
    }

    pub fn on_close(mut self, on_close: impl Fn(&mut WindowContext) + 'static) -> Self {
        self.on_close = Some(Arc::new(on_close));
        self
    }
}

impl Render for Dialog {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let theme = cx.theme();

        // 遮罩层
        div()
            .absolute()
            .inset_0()
            .bg(theme.colors.overlay)
            .flex()
            .items_center()
            .justify_center()
            .when(self.modal, |this| this.on_click_stop_propagation())
            .child(
                // 对话框主体
                div()
                    .w_96()
                    .max_w_80percent()
                    .bg(theme.colors.surface)
                    .rounded_lg()
                    .shadow_lg()
                    .border_1()
                    .border_color(theme.colors.border)
                    .child(
                        div()
                            .px_4()
                            .py_3()
                            .border_b_1()
                            .border_color(theme.colors.border)
                            .child(
                                h3()
                                    .text_lg()
                                    .font_semibold()
                                    .text_color(theme.colors.foreground)
                                    .child(self.title.clone())
                            )
                    )
                    .child(
                        div()
                            .px_4()
                            .py_3()
                            .child(self.content.clone())
                    )
                    .child(
                        div()
                            .px_4()
                            .py_3()
                            .border_t_1()
                            .border_color(theme.colors.border)
                            .flex()
                            .justify_end()
                            .gap_2()
                            .children(self.actions.iter().map(|btn| btn.clone().into_any_element()))
                    )
            )
    }
}

组件化设计模式

无状态组件

// 使用 RenderOnce trait
impl RenderOnce for StatelessComponent {
    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
        div()
            .bg(cx.theme().colors.surface)
            .child("Content")
    }
}

有状态组件

// 使用 Render trait
struct MyComponent {
    count: usize,
    text: SharedString,
}

impl Render for MyComponent {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .child(
                h1()
                    .text_2xl()
                    .font_bold()
                    .child("My Component")
            )
            .child(
                div()
                    .mt_4()
                    .child(format!("Count: {}", self.count))
            )
            .child(
                button("Increment", move |_, cx| {
                    self.count += 1;
                    cx.notify();
                })
            )
    }
}

样式系统

样式链式调用

div()
    // 尺寸
    .w_full()
    .h_10()
    .min_w_0()
    .max_w_80percent()

    // 间距
    .p_4()
    .px_4()
    .py_2()
    .m_auto()
    .mt_4()

    // 布局
    .flex()
    .flex_col()
    .items_center()
    .justify_center()
    .gap_4()
    .grid()
    .grid_cols_3()

    // 颜色
    .bg(cx.theme().colors.background)
    .text_color(cx.theme().colors.foreground)
    .border_color(cx.theme().colors.border)

    // 边框
    .border_1()
    .border_2()
    .border_t_1()
    .rounded_md()
    .rounded_lg()
    .rounded_full()

    // 阴影
    .shadow_sm()
    .shadow_md()
    .shadow_lg()

    // 交互
    .cursor_pointer()
    .transition()
    .hover(|s| s.bg(cx.theme().colors.hover))
    .active(|s| s.bg(cx.theme().colors.active))
    .focus(|s| s.border_color(cx.theme().colors.primary))

    // 动画
    .animate()
    .duration_150()
    .ease_in_out()

可复用代码

模块 路径 用途
主题系统 crates/ui/src/theme/ 颜色、字体、样式
基础组件 crates/ui/src/button/input/ 按钮、输入框
布局组件 crates/ui/src/dialog/dock/ 对话框、停靠面板
数据组件 crates/ui/src/table/list/ 表格、列表
表单组件 crates/ui/src/form/ 表单验证
导航组件 crates/ui/src/navigation/ 标签页、面包屑

在本项目中的使用

用于实现 agent-client 的所有 UI 界面:

agent-client (GPUI 应用)
    │
    ├── gpui-component (UI 组件库)
    │       │
    │       ├── 聊天对话界面 (List, Input, Button)
    │       ├── 设置界面 (Form, Select, Switch)
    │       ├── 关于界面 (Dialog, Card)
    │       ├── 依赖管理 (DataTable, Badge)
    │       └── 状态栏 (Icon, Tooltip)
    │
    └── 自定义组件
            ├── 连接状态指示器
            ├── ID/密码显示
            └── 主题切换

与 agent-client 集成场景

场景1Tab 导航布局

// agent-client 主界面 Tab 导航

use gpui::{div, h1, h2, p, prelude::*, tabs, TabBar, TabPosition};

pub struct AgentClientApp {
    // 当前选中的 Tab
    active_tab: usize,
    // Tab 配置
    tabs: Vec<TabConfig>,
}

struct TabConfig {
    id: usize,
    title: &'static str,
    icon: &'static str,
    enabled: bool,
}

impl AgentClientApp {
    pub fn new() -> Self {
        Self {
            active_tab: 0,
            tabs: vec![
                TabConfig { id: 0, title: "Agent", icon: "agent_icon", enabled: cfg!(feature = "agent-chat") },
                TabConfig { id: 1, title: "Dependencies", icon: "deps_icon", enabled: true },
                TabConfig { id: 2, title: "Settings", icon: "settings_icon", enabled: true },
                TabConfig { id: 3, title: "About", icon: "about_icon", enabled: true },
            ],
        }
    }
}

impl Render for AgentClientApp {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .h_full()
            .w_full()
            .bg(cx.theme().colors.app_background)
            .child(
                // 顶部标题栏
                self.render_title_bar(cx)
            )
            .child(
                // Tab 导航
                self.render_tabs(cx)
            )
            .child(
                // Tab 内容区
                self.render_tab_content(cx)
            )
            .child(
                // 底部状态栏
                self.render_status_bar(cx)
            )
    }

    fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_row()
            .bg(cx.theme().colors.surface)
            .border_b_1()
            .border_color(cx.theme().colors.border)
            .children(self.tabs.iter().filter(|t| t.enabled).enumerate().map(|(idx, tab)| {
                let tab_id = tab.id;
                let is_active = self.active_tab == tab_id;

                div()
                    .px_4()
                    .py_3()
                    .cursor_pointer()
                    .text_color(if is_active {
                        cx.theme().colors.primary
                    } else {
                        cx.theme().colors.foreground
                    })
                    .border_b_2()
                    .border_color(if is_active {
                        cx.theme().colors.primary
                    } else {
                        cx.transparent()
                    })
                    .font_medium()
                    .when(is_active, |this| this.bg(cx.theme().colors.hover))
                    .on_click(cx.listener(move |this, _, _cx| {
                        this.active_tab = tab_id;
                    }))
                    .child(tab.title)
            }))
    }

    fn render_tab_content(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex_1()
            .overflow_hidden()
            .child(match self.active_tab {
                0 => self.render_agent_tab(cx).into_any_element(),
                1 => self.render_dependencies_tab(cx).into_any_element(),
                2 => self.render_settings_tab(cx).into_any_element(),
                3 => self.render_about_tab(cx).into_any_element(),
                _ => div().into_any_element(),
            })
    }
}

场景2连接状态指示器

// 连接状态组件

use crate::ui::{icon::Icon, tooltip::Tooltip};

pub struct ConnectionStatus {
    // 连接状态
    status: ConnectionState,
    // 最后更新时间
    last_update: DateTime<Utc>,
    // 提示文本
    tooltip_text: SharedString,
}

pub enum ConnectionState {
    Connected {
        mode: ConnectionMode,
        latency: u32,
    },
    Connecting,
    Disconnected {
        reason: DisconnectReason,
    },
    Error {
        message: String,
    },
}

pub enum ConnectionMode {
    P2P,
    Relay,
}

pub enum DisconnectReason {
    NetworkUnavailable,
    ServerUnreachable,
    AuthenticationFailed,
    SessionTimeout,
}

impl ConnectionStatus {
    pub fn new() -> Self {
        Self {
            status: ConnectionState::Disconnected {
                reason: DisconnectReason::NetworkUnavailable,
            },
            last_update: Utc::now(),
            tooltip_text: "Disconnected".into(),
        }
    }

    /// 更新连接状态
    pub fn update_status(&mut self, status: ConnectionState) {
        self.status = status;
        self.last_update = Utc::now();
        self.tooltip_text = self.generate_tooltip_text();
    }

    fn generate_tooltip_text(&self) -> SharedString {
        match &self.status {
            ConnectionState::Connected { mode, latency } => {
                match mode {
                    ConnectionMode::P2P => format!("Connected (P2P) - {}ms", latency),
                    ConnectionMode::Relay => format!("Connected (Relay) - {}ms", latency),
                }
            }
            ConnectionState::Connecting => "Connecting...".into(),
            ConnectionState::Disconnected { reason } => {
                match reason {
                    DisconnectReason::NetworkUnavailable => "No network connection",
                    DisconnectReason::ServerUnreachable => "Cannot reach server",
                    DisconnectReason::AuthenticationFailed => "Authentication failed",
                    DisconnectReason::SessionTimeout => "Session timed out",
                }.into()
            }
            ConnectionState::Error { message } => message.as_str().into(),
        }
    }
}

impl Render for ConnectionStatus {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let (icon, color) = match &self.status {
            ConnectionState::Connected { mode, .. } => (
                match mode {
                    ConnectionMode::P2P => "wifi",
                    ConnectionMode::Relay => "relay",
                },
                cx.theme().colors.success,
            ),
            ConnectionState::Connecting => ("loading", cx.theme().colors.info),
            ConnectionState::Disconnected { .. } => ("offline", cx.theme().colors.error),
            ConnectionState::Error { .. } => ("error", cx.theme().colors.warning),
        };

        Tooltip::new(
            div()
                .flex()
                .items_center()
                .gap_2()
                .child(
                    Icon::new(icon)
                        .size(IconSize::Small)
                        .color(color)
                )
                .child(
                    // 状态文字
                    div()
                        .text_sm()
                        .text_color(color)
                        .child(self.tooltip_text.clone())
                ),
            self.tooltip_text.clone()
        )
    }
}

/// 状态栏组件
pub struct StatusBar {
    // 连接状态
    connection_status: View<ConnectionStatus>,
    // 客户端 ID
    client_id: SharedString,
    // 当前时间
    current_time: SharedString,
}

impl Render for StatusBar {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_row()
            .items_center()
            .justify_between()
            .px_4()
            .py_2()
            .h_10()
            .bg(cx.theme().colors.status_bar_background)
            .border_t_1()
            .border_color(cx.theme().colors.border)
            .child(
                // 左侧:客户端 ID
                div()
                    .flex()
                    .items_center()
                    .gap_2()
                    .child(
                        Icon::new("id")
                            .size(IconSize::Small)
                            .color(cx.theme().colors.muted)
                    )
                    .child(
                        div()
                            .text_sm()
                            .text_color(cx.theme().colors.muted)
                            .child(format!("ID: {}", self.client_id))
                    )
            )
            .child(
                // 中间:连接状态
                self.connection_status.clone()
            )
            .child(
                // 右侧:当前时间
                div()
                    .text_sm()
                    .text_color(cx.theme().colors.muted)
                    .child(self.current_time.clone())
            )
    }
}

场景3ID/密码显示组件

// 客户端 ID 和密码显示

pub struct IdPasswordDisplay {
    // 客户端 ID
    client_id: SharedString,
    // 密码(是否显示)
    password_masked: bool,
    // 当前密码
    password: SharedString,
}

impl IdPasswordDisplay {
    pub fn new(client_id: &str, password: &str) -> Self {
        Self {
            client_id: client_id.into(),
            password: password.into(),
            password_masked: true,
        }
    }

    /// 复制 ID 到剪贴板
    pub fn copy_id(&self, cx: &mut WindowContext) {
        cx.write_to_clipboard(ClipboardItem::new(self.client_id.to_string()));
    }

    /// 复制密码到剪贴板
    pub fn copy_password(&self, cx: &mut WindowContext) {
        cx.write_to_clipboard(ClipboardItem::new(self.password.to_string()));
    }

    /// 切换密码可见性
    pub fn toggle_password_visibility(&mut self) {
        self.password_masked = !self.password_masked;
    }

    /// 修改密码
    pub fn update_password(&mut self, new_password: &str) {
        self.password = new_password.into();
    }
}

impl Render for IdPasswordDisplay {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .gap_6()
            .p_6()
            .rounded_lg()
            .bg(cx.theme().colors.surface)
            .border_1()
            .border_color(cx.theme().colors.border)
            .child(
                // ID 显示区
                self.render_id_section(cx)
            )
            .child(
                // 密码显示区
                self.render_password_section(cx)
            )
    }

    fn render_id_section(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .gap_2()
            .child(
                div()
                    .text_sm()
                    .font_medium()
                    .text_color(cx.theme().colors.muted)
                    .child("Your Client ID")
            )
            .child(
                div()
                    .flex()
                    .items_center()
                    .gap_2()
                    .p_3()
                    .rounded_md()
                    .bg(cx.theme().colors.editor_background)
                    .child(
                        div()
                            .text_xl()
                            .font_bold()
                            .font_mono()
                            .text_color(cx.theme().colors.foreground)
                            .child(self.client_id.clone())
                    )
                    .child(
                        Button::new("copy-id", cx, |_, cx| {
                            self.copy_id(cx);
                        })
                        .icon("copy")
                        .variant(ButtonVariant::Ghost)
                        .into_any_element()
                    )
            )
    }

    fn render_password_section(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .gap_2()
            .child(
                div()
                    .text_sm()
                    .font_medium()
                    .text_color(cx.theme().colors.muted)
                    .child("Access Password")
            )
            .child(
                div()
                    .flex()
                    .items_center()
                    .gap_2()
                    .p_3()
                    .rounded_md()
                    .bg(cx.theme().colors.editor_background)
                    .child(
                        div()
                            .text_lg()
                            .font_mono()
                            .text_color(cx.theme().colors.foreground)
                            .child(if self.password_masked {
                                "••••••••".to_string()
                            } else {
                                self.password.clone()
                            })
                    )
                    .child(
                        Button::new("toggle-visibility", cx, |_, _| {
                            self.toggle_password_visibility();
                        })
                        .icon(if self.password_masked { "eye" } else { "eye-off" })
                        .variant(ButtonVariant::Ghost)
                        .into_any_element()
                    )
                    .child(
                        Button::new("copy-password", cx, |_, cx| {
                            self.copy_password(cx);
                        })
                        .icon("copy")
                        .variant(ButtonVariant::Ghost)
                        .into_any_element()
                    )
            )
            .child(
                div()
                    .text_xs()
                    .text_color(cx.theme().colors.warning)
                    .child("Share this password with administrators to allow remote access")
            )
    }
}

场景4依赖管理界面

// 依赖管理 Tab

pub struct DependenciesTab {
    // 依赖列表状态
    dependencies: Vec<DependencyInfo>,
    // 正在安装的依赖
    installing: Arc<DashMap<String, f32>>, // name -> progress
    // 刷新任务
    refresh_task: Option<Task<()>>,
}

pub struct DependencyInfo {
    pub name: String,
    pub display_name: String,
    pub version: Option<String>,
    pub source: DependencySource,
    pub status: DependencyStatus,
    pub required_version: Option<String>,
    pub install_command: Option<String>,
}

pub enum DependencySource {
    System,      // 系统全局
    Client,      // 客户端隔离目录
    NotInstalled,
}

pub enum DependencyStatus {
    Installed,
    Installing { progress: f32 },
    NotInstalled,
    UpdateAvailable { current: String, latest: String },
    Error { message: String },
}

impl DependenciesTab {
    pub fn new() -> Self {
        let mut this = Self {
            dependencies: Vec::new(),
            installing: Arc::new(DashMap::new()),
            refresh_task: None,
        };
        this.refresh_dependencies();
        this
    }

    /// 刷新依赖状态
    pub fn refresh_dependencies(&mut self) {
        let installing = self.installing.clone();
        self.refresh_task = Some(cx.spawn(|this, mut cx| async move {
            // 模拟检查依赖状态
            let deps = vec![
                DependencyInfo {
                    name: "node".into(),
                    display_name: "Node.js".into(),
                    version: Some("v20.10.0".into()),
                    source: DependencySource::System,
                    status: DependencyStatus::Installed,
                    required_version: Some(">=18.0.0".into()),
                    install_command: None,
                },
                DependencyInfo {
                    name: "npm".into(),
                    display_name: "npm".into(),
                    version: Some("10.2.0".into()),
                    source: DependencySource::System,
                    status: DependencyStatus::Installed,
                    required_version: None,
                    install_command: None,
                },
                DependencyInfo {
                    name: "opencode".into(),
                    display_name: "opencode".into(),
                    version: None,
                    source: DependencySource::NotInstalled,
                    status: DependencyStatus::NotInstalled,
                    required_version: None,
                    install_command: Some("npm install -g opencode".into()),
                },
            ];

            this.update(&mut cx, |this, _| {
                this.dependencies = deps;
            });
        }));
    }

    /// 安装依赖
    pub fn install_dependency(&mut self, name: &str) {
        let name = name.to_string();
        let installing = self.installing.clone();

        cx.spawn(|this, mut cx| async move {
            installing.insert(name.clone(), 0.0);

            // 模拟安装过程
            for i in 0..=100 {
                tokio::time::sleep(Duration::from_millis(100)).await;
                installing.insert(name.clone(), i as f32 / 100.0);
            }

            installing.remove(&name);

            this.update(&mut cx, |this, _| {
                this.refresh_dependencies();
            });
        });
    }
}

impl Render for DependenciesTab {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .h_full()
            .p_6()
            .gap_4()
            .child(
                // 标题和操作
                div()
                    .flex()
                    .justify_between()
                    .items_center()
                    .child(
                        h2()
                            .text-xl()
                            .font_semibold()
                            .child("Dependency Management")
                    )
                    .child(
                        Button::new("refresh", cx, |_, _| {
                            self.refresh_dependencies();
                        })
                        .icon("refresh")
                        .variant(ButtonVariant::Secondary)
                    )
            )
            .child(
                // 依赖列表
                DataTable::new()
                    .columns(vec![
                        TableColumn::new("name", "Name", px(200.), |d: &DependencyInfo, _| {
                            div().child(d.display_name.clone()).into_any_element()
                        }),
                        TableColumn::new("version", "Version", px(150.), |d: &DependencyInfo, _| {
                            div().child(d.version.clone().unwrap_or("-".into())).into_any_element()
                        }),
                        TableColumn::new("source", "Source", px(120.), |d: &DependencyInfo, _| {
                            let source_label = match d.source {
                                DependencySource::System => "System",
                                DependencySource::Client => "Client",
                                DependencySource::NotInstalled => "-",
                            };
                            div()
                                .text_sm()
                                .text_color(cx.theme().colors.muted)
                                .child(source_label)
                                .into_any_element()
                        }),
                        TableColumn::new("status", "Status", px(150.), |d: &DependencyInfo, _| {
                            self.render_status_badge(d, cx).into_any_element()
                        }),
                        TableColumn::new("actions", "Actions", px(200.), |d: &DependencyInfo, _| {
                            self.render_action_buttons(d, cx).into_any_element()
                        }),
                    ])
                    .items(self.dependencies.clone())
            )
    }

    fn render_status_badge(&self, dep: &DependencyInfo, cx: &mut WindowContext) -> impl IntoElement {
        match &dep.status {
            DependencyStatus::Installed => Badge::new("Installed")
                .variant(BadgeVariant::Success),
            DependencyStatus::Installing { progress } => Badge::new(&format!("Installing {:.0}%", progress * 100.0))
                .variant(BadgeVariant::Info),
            DependencyStatus::NotInstalled => Badge::new("Not Installed")
                .variant(BadgeVariant::Warning),
            DependencyStatus::UpdateAvailable { current, latest } => Badge::new(&format!("Update to {}", latest))
                .variant(BadgeVariant::Secondary),
            DependencyStatus::Error { message } => Badge::new(message)
                .variant(BadgeVariant::Danger),
        }
    }

    fn render_action_buttons(&self, dep: &DependencyInfo, cx: &mut WindowContext) -> impl IntoElement {
        match &dep.status {
            DependencyStatus::Installed => div()
                .flex()
                .gap_2()
                .child(
                    Button::new("update", cx, |_, _| {})
                        .icon("download")
                        .variant(ButtonVariant::Ghost)
                        .size(ButtonSize::Small)
                )
                .child(
                    Button::new("uninstall", cx, |_, _| {})
                        .icon("trash")
                        .variant(ButtonVariant::Ghost)
                        .size(ButtonSize::Small)
                ),
            DependencyStatus::NotInstalled => Button::new("install", cx, |_, _| {
                self.install_dependency(&dep.name);
            })
                .icon("install")
                .variant(ButtonVariant::Primary)
                .size(ButtonSize::Small),
            DependencyStatus::Installing { .. } => Button::new("cancel", cx, |_, _| {})
                .icon("x")
                .variant(ButtonVariant::Ghost)
                .size(ButtonSize::Small),
            _ => div(),
        }
    }
}

场景5设置界面

// 设置 Tab

pub struct SettingsTab {
    // data-server 配置
    server_address: SharedString,
    // 自动启动
    auto_launch: bool,
    // 主题模式
    theme_mode: ThemeMode,
    // 工作目录
    work_directory: SharedString,
}

impl SettingsTab {
    pub fn new() -> Self {
        Self {
            server_address: "hbbs.example.com:21116".into(),
            auto_launch: false,
            theme_mode: ThemeMode::System,
            work_directory: "~/.qiming-agent".into(),
        }
    }
}

impl Render for SettingsTab {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        Form::new()
            .section(
                FormSection::new("Server Configuration")
                    .description("Configure the data-server connection")
                    .child(
                        FormItem::new("Server Address")
                            .input(Input::new(self.server_address.clone(), |s, _| {
                                self.server_address = s.into();
                            }).placeholder("hbbs.example.com:21116"))
                    )
                    .child(
                        FormItem::new("Relay Server")
                            .input(Input::new("", |_, _| {}).placeholder("hbbr.example.com:21117"))
                    )
            )
            .section(
                FormSection::new("Startup")
                    .child(
                        FormItem::new("Auto Launch")
                            .description("Start the application when system boots")
                            .control(
                                Switch::new(self.auto_launch, |enabled, _| {
                                    self.auto_launch = enabled;
                                })
                            )
                    )
            )
            .section(
                FormSection::new("Appearance")
                    .child(
                        FormItem::new("Theme")
                            .control(
                                Select::new(vec![
                                    ("Light", "light"),
                                    ("Dark", "dark"),
                                    ("System", "system"),
                                ], &format!("{:?}", self.theme_mode))
                            )
                    )
            )
            .section(
                FormSection::new("Files")
                    .child(
                        FormItem::new("Work Directory")
                            .description("Directory for storing files and cache")
                            .input(Input::new(self.work_directory.clone(), |s, _| {
                                self.work_directory = s.into();
                            }))
                    )
            )
            .action(
                Button::new("save", cx, |_, _| {
                    // 保存设置
                })
                    .icon("save")
                    .variant(ButtonVariant::Primary)
            )
    }
}

场景6关于对话框

// 关于 Tab

pub struct AboutTab {
    // 版本信息
    version: String,
    // 构建信息
    build_info: String,
}

impl AboutTab {
    pub fn new() -> Self {
        Self {
            version: env!("CARGO_PKG_VERSION").to_string(),
            build_info: format!("Built on {}", chrono::Utc::now().format("%Y-%m-%d")),
        }
    }
}

impl Render for AboutTab {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .items_center()
            .gap_6()
            .p_8()
            .child(
                // 应用图标
                div()
                    .w_24()
                    .h_24()
                    .rounded_xl()
                    .bg(cx.theme().colors.primary)
                    .flex()
                    .items_center()
                    .justify_center()
                    .child(
                        Icon::new("app_logo")
                            .size(IconSize::Large)
                            .color(gpui::white())
                    )
            )
            .child(
                // 应用名称
                h1()
                    .text_2xl()
                    .font_bold()
                    .child("qiming-agent")
            )
            .child(
                // 版本信息
                div()
                    .flex()
                    .flex_col()
                    .items_center()
                    .gap_1()
                    .child(
                        div()
                            .text_sm()
                            .text_color(cx.theme().colors.muted)
                            .child(format!("Version {}", self.version))
                    )
                    .child(
                        div()
                            .text_xs()
                            .text_color(cx.theme().colors.muted)
                            .child(self.build_info.clone())
                    )
            )
            .child(
                // 链接
                div()
                    .flex()
                    .gap_4()
                    .child(
                        Link::new("Website", "https://example.com")
                    )
                    .child(
                        Link::new("Documentation", "https://docs.example.com")
                    )
                    .child(
                        Link::new("Report Issue", "https://github.com/example/issues")
                    )
            )
            .child(
                // 许可证
                div()
                    .text_sm()
                    .text_color(cx.theme().colors.muted)
                    .child("Licensed under MIT License")
            )
    }
}