rust-ui-architecture
14
总安装量
7
周安装量
#23264
全站排名
安装命令
npx skills add https://github.com/geoffjay/claude-plugins --skill rust-ui-architecture
Agent 安装分布
claude-code
7
windsurf
6
opencode
5
gemini-cli
5
antigravity
4
cursor
3
Skill 文档
Rust UI Architecture
Metadata
This skill provides comprehensive guidance on architecting scalable, maintainable Rust UI applications using GPUI, covering project structure, design patterns, and best practices.
Instructions
Application Structure
Recommended Project Layout
my-gpui-app/
âââ Cargo.toml
âââ src/
â âââ main.rs # Application entry point
â âââ app.rs # Main application struct
â âââ ui/ # UI layer
â â âââ mod.rs
â â âââ views/ # High-level views
â â â âââ mod.rs
â â â âââ main_view.rs
â â â âââ sidebar.rs
â â â âââ editor.rs
â â âââ components/ # Reusable components
â â â âââ mod.rs
â â â âââ button.rs
â â â âââ input.rs
â â â âââ modal.rs
â â âââ theme.rs # Theme definitions
â âââ models/ # Application state
â â âââ mod.rs
â â âââ document.rs
â â âââ project.rs
â â âââ settings.rs
â âââ services/ # External integrations
â â âââ mod.rs
â â âââ file_service.rs
â â âââ api_client.rs
â âââ domain/ # Core business logic
â â âââ mod.rs
â â âââ operations.rs
â âââ utils/ # Utilities
â âââ mod.rs
â âââ helpers.rs
âââ examples/ # Example applications
â âââ basic.rs
âââ tests/ # Integration tests
âââ integration/
âââ ui/
Layer Separation
Four-Layer Architecture
âââââââââââââââââââââââââââââââââââ
â UI Layer (Views) â - GPUI views and components
â â - User interactions
â â - Render logic
âââââââââââââââââââââââââââââââââââ¤
â Application Layer (Models) â - Application state (Model<T>)
â â - State coordination
â â - Business logic orchestration
âââââââââââââââââââââââââââââââââââ¤
â Service Layer (Services) â - File I/O
â â - Network requests
â â - External APIs
âââââââââââââââââââââââââââââââââââ¤
â Domain Layer (Core) â - Pure business logic
â â - Domain types
â â - No dependencies on UI/GPUI
âââââââââââââââââââââââââââââââââââ
Example Implementation
// Domain Layer (pure logic)
pub mod domain {
#[derive(Clone, Debug)]
pub struct Document {
pub id: DocumentId,
pub content: String,
pub language: Language,
}
impl Document {
pub fn word_count(&self) -> usize {
self.content.split_whitespace().count()
}
pub fn is_empty(&self) -> bool {
self.content.trim().is_empty()
}
}
}
// Service Layer (external integration)
pub mod services {
use super::domain::*;
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
pub struct RealFileService;
impl FileService for RealFileService {
fn read(&self, path: &Path) -> Result<String> {
std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read: {}", e))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
std::fs::write(path, content)
.map_err(|e| anyhow::anyhow!("Failed to write: {}", e))
}
}
}
// Application Layer (state management)
pub mod models {
use super::domain::*;
use super::services::*;
pub struct DocumentModel {
document: Document,
file_service: Arc<dyn FileService>,
is_modified: bool,
}
impl DocumentModel {
pub fn new(document: Document, file_service: Arc<dyn FileService>) -> Self {
Self {
document,
file_service,
is_modified: false,
}
}
pub fn update_content(&mut self, content: String) {
self.document.content = content;
self.is_modified = true;
}
pub async fn save(&mut self) -> Result<()> {
self.file_service.write(&self.document.path, &self.document.content)?;
self.is_modified = false;
Ok(())
}
}
}
// UI Layer (views)
pub mod ui {
use gpui::*;
use super::models::*;
pub struct DocumentView {
model: Model<DocumentModel>,
_subscription: Subscription,
}
impl DocumentView {
pub fn new(model: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for DocumentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let model = self.model.read(cx);
div()
.child(format!("Words: {}", model.document.word_count()))
.when(model.is_modified, |this| {
this.child("(modified)")
})
}
}
}
Component Hierarchies
Container-Presenter Pattern
// Container: Manages state and logic
pub struct EditorContainer {
document: Model<DocumentModel>,
_subscription: Subscription,
}
impl EditorContainer {
pub fn new(document: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&document, |_, _, cx| cx.notify());
Self { document, _subscription }
}
fn handle_save(&mut self, cx: &mut ViewContext<Self>) {
let document = self.document.clone();
cx.spawn(|_, mut cx| async move {
cx.update_model(&document, |doc, _| {
doc.save().await
}).await?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
impl Render for EditorContainer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let doc = self.document.read(cx);
EditorPresenter::new(
doc.document.content.clone(),
doc.is_modified,
cx.listener(|this, content, cx| {
this.document.update(cx, |doc, _| {
doc.update_content(content);
});
}),
)
}
}
// Presenter: Pure rendering
pub struct EditorPresenter {
content: String,
is_modified: bool,
on_change: Box<dyn Fn(String, &mut WindowContext)>,
}
impl EditorPresenter {
pub fn new(
content: String,
is_modified: bool,
on_change: impl Fn(String, &mut WindowContext) + 'static,
) -> Self {
Self {
content,
is_modified,
on_change: Box::new(on_change),
}
}
}
impl Render for EditorPresenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
textarea()
.value(&self.content)
.on_input(|value, cx| {
(self.on_change)(value, cx);
})
)
.when(self.is_modified, |this| {
this.child("Unsaved changes")
})
}
}
Module Organization
Feature-Based Structure
src/
âââ features/
â âââ editor/
â â âââ mod.rs
â â âââ model.rs # EditorModel
â â âââ view.rs # EditorView
â â âââ commands.rs # Editor actions
â â âââ components/ # Editor-specific components
â âââ sidebar/
â â âââ mod.rs
â â âââ model.rs
â â âââ view.rs
â â âââ components/
â âââ statusbar/
â âââ mod.rs
â âââ model.rs
â âââ view.rs
Benefits:
- Clear feature boundaries
- Easy to understand and navigate
- Scales well with team size
- Enables feature-based development
State Management Architecture
Unidirectional Data Flow
User Action â Action Dispatch â State Update â View Rerender
â â
âââââââââââââââââ Event Handlers ââââââââââââââ
Implementation:
// Define actions
actions!(app, [AddTodo, ToggleTodo, DeleteTodo]);
// State model
pub struct TodoListModel {
todos: Vec<Todo>,
}
impl TodoListModel {
pub fn add_todo(&mut self, text: String) {
self.todos.push(Todo {
id: TodoId::new(),
text,
completed: false,
});
}
pub fn toggle_todo(&mut self, id: TodoId) {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
}
}
// View with action handlers
pub struct TodoListView {
model: Model<TodoListModel>,
}
impl TodoListView {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, action: &AddTodo, cx| {
this.model.update(cx, |model, cx| {
model.add_todo(action.text.clone());
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &ToggleTodo, cx| {
this.model.update(cx, |model, cx| {
model.toggle_todo(action.id);
cx.notify();
});
}));
}
}
State Ownership Patterns
Single Source of Truth:
pub struct AppModel {
// Root owns all state
documents: Vec<Model<DocumentModel>>,
settings: Model<Settings>,
ui_state: Model<UiState>,
}
Hierarchical Ownership:
pub struct WorkspaceModel {
// Workspace owns workspace-level state
panes: Vec<Model<PaneModel>>,
}
pub struct PaneModel {
// Pane owns pane-level state
tabs: Vec<Model<TabModel>>,
active_index: usize,
}
Separation of Concerns
Clear Boundaries
// â GOOD: Clear responsibilities
// Domain logic (no GPUI)
pub mod document {
pub struct Document {
content: String,
}
impl Document {
pub fn insert(&mut self, pos: usize, text: &str) {
self.content.insert_str(pos, text);
}
}
}
// Application logic (uses GPUI models)
pub mod editor_model {
use gpui::*;
use super::document::Document;
pub struct EditorModel {
document: Document,
cursor_position: usize,
}
impl EditorModel {
pub fn insert_at_cursor(&mut self, text: &str) {
self.document.insert(self.cursor_position, text);
self.cursor_position += text.len();
}
}
}
// UI logic (GPUI views)
pub mod editor_view {
use gpui::*;
use super::editor_model::EditorModel;
pub struct EditorView {
model: Model<EditorModel>,
}
impl Render for EditorView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// Rendering logic
}
}
}
Testability Patterns
Dependency Injection
// Define trait for external dependencies
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
// Production implementation
pub struct RealFileService;
impl FileService for RealFileService {
// Real implementation
}
// Test implementation
#[cfg(test)]
pub struct MockFileService {
read_results: HashMap<PathBuf, Result<String>>,
written_files: RefCell<Vec<(PathBuf, String)>>,
}
#[cfg(test)]
impl FileService for MockFileService {
fn read(&self, path: &Path) -> Result<String> {
self.read_results
.get(path)
.cloned()
.unwrap_or_else(|| Err(anyhow::anyhow!("File not found")))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
self.written_files
.borrow_mut()
.push((path.to_path_buf(), content.to_string()));
Ok(())
}
}
// Model accepts any FileService
pub struct DocumentModel {
file_service: Arc<dyn FileService>,
}
// Tests use mock
#[cfg(test)]
mod tests {
#[test]
fn test_save() {
let mock_service = Arc::new(MockFileService::new());
let model = DocumentModel::new(mock_service.clone());
model.save().unwrap();
assert_eq!(mock_service.written_files.borrow().len(), 1);
}
}
Plugin Architecture
Extension System
// Define plugin trait
pub trait EditorPlugin: Send + Sync {
fn name(&self) -> &str;
fn on_document_open(&self, doc: &Document) -> Result<()>;
fn on_document_save(&self, doc: &Document) -> Result<()>;
}
// Plugin manager
pub struct PluginManager {
plugins: Vec<Box<dyn EditorPlugin>>,
}
impl PluginManager {
pub fn register(&mut self, plugin: Box<dyn EditorPlugin>) {
self.plugins.push(plugin);
}
pub fn notify_document_open(&self, doc: &Document) -> Result<()> {
for plugin in &self.plugins {
plugin.on_document_open(doc)?;
}
Ok(())
}
}
// Example plugin
pub struct AutoSavePlugin {
interval: Duration,
}
impl EditorPlugin for AutoSavePlugin {
fn name(&self) -> &str {
"AutoSave"
}
fn on_document_open(&self, doc: &Document) -> Result<()> {
// Start auto-save timer
Ok(())
}
fn on_document_save(&self, doc: &Document) -> Result<()> {
println!("Document saved: {}", doc.path.display());
Ok(())
}
}
Resources
Design Patterns
Architectural Patterns:
- Model-View pattern (GPUI-specific)
- Container-Presenter (separation of concerns)
- Service-oriented (external dependencies)
- Plugin architecture (extensibility)
Code Organization:
- Feature-based modules
- Layer separation
- Clear boundaries
- Dependency injection
State Management:
- Unidirectional data flow
- Single source of truth
- Hierarchical ownership
- Reactive updates
Best Practices
- Separation of Concerns: Keep UI, logic, and data separate
- Dependency Injection: Use traits for testability
- Feature Organization: Group related code by feature
- State Ownership: Clear ownership hierarchy
- Testable Design: Design for testing from the start
- Documentation: Document architecture decisions
- Modularity: Small, focused modules
- Scalability: Design for growth
Common Patterns
- Repository Pattern: Data access abstraction
- Command Pattern: Action system
- Observer Pattern: Subscriptions
- Factory Pattern: Component creation
- Strategy Pattern: Pluggable behaviors
- Facade Pattern: Simplified interfaces