| name | rust-ui-architecture |
| description | Architecture patterns for Rust UI applications including GPUI-specific patterns, code organization, modularity, and scalability. Use when user needs guidance on application architecture, code organization, or scaling UI applications. |
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
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()
}
}
}
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))
}
}
}
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(())
}
}
}
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
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);
});
}),
)
}
}
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:
actions!(app, [AddTodo, ToggleTodo, DeleteTodo]);
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;
}
}
}
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 {
documents: Vec<Model<DocumentModel>>,
settings: Model<Settings>,
ui_state: Model<UiState>,
}
Hierarchical Ownership:
pub struct WorkspaceModel {
panes: Vec<Model<PaneModel>>,
}
pub struct PaneModel {
tabs: Vec<Model<TabModel>>,
active_index: usize,
}
Separation of Concerns
Clear Boundaries
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);
}
}
}
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();
}
}
}
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 {
}
}
}
Testability Patterns
Dependency Injection
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 {
}
#[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(())
}
}
pub struct DocumentModel {
file_service: Arc<dyn FileService>,
}
#[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
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<()>;
}
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(())
}
}
pub struct AutoSavePlugin {
interval: Duration,
}
impl EditorPlugin for AutoSavePlugin {
fn name(&self) -> &str {
"AutoSave"
}
fn on_document_open(&self, doc: &Document) -> Result<()> {
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