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

1714 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
## 核心依赖
```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"
```
## 主题系统
### 主题结构
```rust
// 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
```rust
// 主题 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 组件
```rust
// 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 组件
```rust
// 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 组件
```rust
// 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 组件
```rust
// 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()))
)
)
}
}
```
## 组件化设计模式
### 无状态组件
```rust
// 使用 RenderOnce trait
impl RenderOnce for StatelessComponent {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
.bg(cx.theme().colors.surface)
.child("Content")
}
}
```
### 有状态组件
```rust
// 使用 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();
})
)
}
}
```
## 样式系统
### 样式链式调用
```rust
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 导航布局
```rust
// 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连接状态指示器
```rust
// 连接状态组件
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/密码显示组件
```rust
// 客户端 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依赖管理界面
```rust
// 依赖管理 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设置界面
```rust
// 设置 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关于对话框
```rust
// 关于 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")
)
}
}
```