50 KiB
50 KiB
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 集成场景
场景1:Tab 导航布局
// 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())
)
}
}
场景3:ID/密码显示组件
// 客户端 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")
)
}
}