- name
- gpui-fundamentals
- description
- Core GPUI concepts including contexts, windows, entities, elements, and rendering. Use when learning GPUI basics, understanding the framework architecture, or implementing core UI patterns.
# GPUI Fundamentals
This skill covers the core concepts of GPUI framework for building desktop UI applications in Rust.
## Overview
GPUI is a UI framework that provides:
- **Entities**: Handles to state with lifecycle management
- **Contexts**: Access to global state, windows, and system services
- **Elements**: Composable UI building blocks
- **Rendering**: `Render` trait for creating element trees
- **Concurrency**: Async primitives for background work
## Context Types
Context types allow interaction with global state, windows, entities, and system services. They are passed as the argument named `cx`.
### App
`App` is the root context type, providing access to global state and read/update of entities.
```rust
fn do_something(cx: &mut App) {
// Access global state
// Read/update entities
}
```
### Context<T>
Provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
```rust
struct MyView {
count: usize,
}
impl MyView {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
cx.notify(); // Tell GPUI to re-render
}
}
```
### AsyncApp and AsyncWindowContext
Provided by `cx.spawn()` for async operations. These can be held across await points.
```rust
fn start_async_work(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
// this: WeakEntity<Self>
// cx: &mut AsyncApp
// Do async work
Ok(())
}).detach();
}
```
## Window
`Window` provides access to the state of an application window. It is passed as an argument named `window` and comes **before** `cx` when present.
```rust
impl Render for MyView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().child("Hello")
}
}
```
Used for:
- Managing focus
- Dispatching actions
- Directly drawing
- Getting user input state
## Entities
An `Entity<T>` is a handle to state of type `T`. Entities enable:
- Shared ownership of UI state
- Automatic lifecycle management
- Safe concurrent access
### Creating Entities
```rust
app.run(move |cx| {
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
// Create an entity
cx.new(|_cx| MyView { count: 0 })
})?;
Ok::<_, anyhow::Error>(())
}).detach();
});
```
### Entity Operations
```rust
// Given: thing: Entity<MyView>
// Get entity ID
let id = thing.entity_id();
// Downgrade to weak reference
let weak = thing.downgrade();
// Read (immutable access)
let value = thing.read(cx);
println!("Count: {}", value.count);
// Read with closure
let count = thing.read_with(cx, |view, cx| view.count);
// Update (mutable access)
thing.update(cx, |view, cx| {
view.count += 1;
cx.notify();
});
// Update with window access
thing.update_in(cx, |view, window, cx| {
view.count += 1;
window.dispatch_action(SomeAction.boxed_clone(), cx);
cx.notify();
});
```
### Important Rules
1. **Use inner cx**: Within closures, use the inner `cx` provided to the closure, not the outer `cx`
```rust
// ❌ WRONG
entity.update(cx, |view, inner_cx| {
view.count += 1;
cx.notify(); // Using outer cx - WRONG!
});
// ✅ CORRECT
entity.update(cx, |view, inner_cx| {
view.count += 1;
inner_cx.notify(); // Using inner cx
});
```
2. **Avoid update-while-updating**: Never update an entity while it's already being updated (causes panic)
```rust
// ❌ WRONG - will panic
entity.update(cx, |view, cx| {
entity.update(cx, |view2, cx2| { // Nested update - PANIC!
// ...
});
});
```
## Elements
The `Render` trait is used to render state into an element tree with flexbox layout.
### Render Trait
```rust
use gpui::*;
struct MyView {
text: SharedString,
}
impl Render for MyView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_2()
.child(self.text.clone())
.child("More text")
}
}
```
### RenderOnce Trait
For components constructed just to be turned into elements:
```rust
use gpui::*;
#[derive(IntoElement)]
struct Card {
title: SharedString,
content: SharedString,
}
impl RenderOnce for Card {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div()
.p_4()
.bg(rgb(0x1a1a1a))
.rounded(px(8.0))
.child(
div().font_bold().child(self.title)
)
.child(
div().text_sm().child(self.content)
)
}
}
// Usage
impl Render for MyView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child(Card {
title: "Hello".into(),
content: "World".into(),
})
}
}
```
### SharedString
Use `SharedString` to avoid copying strings. It's either `&'static str` or `Arc<str>`.
```rust
use gpui::*;
struct MyView {
// Efficient string storage
title: SharedString,
}
impl MyView {
fn new() -> Self {
Self {
title: "Hello".into(), // From &str
}
}
fn set_title(&mut self, title: String) {
self.title = title.into(); // From String
}
}
// SharedString implements IntoElement
impl Render for MyView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child(self.title.clone())
}
}
```
## Element Composition
### Basic Composition
```rust
div()
.child("Text")
.child(div().child("Nested"))
.child(another_element())
```
### Conditional Rendering
Use `.when()` for conditional attributes/children:
```rust
div()
.when(is_active, |this| {
this.bg(rgb(0x3b82f6))
})
.when_some(maybe_text, |this, text| {
this.child(text)
})
```
### Multiple Children
```rust
div()
.children(vec![
div().child("Item 1"),
div().child("Item 2"),
div().child("Item 3"),
])
```
## Application Lifecycle
### Basic Application
```rust
use gpui::*;
struct AppView;
impl Render for AppView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child("My App")
}
}
fn main() {
let app = Application::new();
app.run(move |cx| {
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|_| AppView)
})?;
Ok::<_, anyhow::Error>(())
})
.detach();
});
}
```
### Window Options
```rust
use gpui::*;
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: Point { x: px(100.0), y: px(100.0) },
size: Size { width: px(1024.0), height: px(768.0) },
})),
titlebar: Some(TitlebarOptions {
title: Some("My Application".into()),
appears_transparent: false,
..Default::default()
}),
focus: true,
show: true,
..Default::default()
};
cx.open_window(options, |window, cx| {
cx.new(|_| MyView::new())
})?;
```
## Common Patterns
### State Update with Notify
When state changes in a way that affects rendering:
```rust
struct Counter {
count: usize,
}
impl Counter {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
cx.notify(); // Trigger re-render
}
}
```
### Event Handlers
```rust
impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(format!("Count: {}", self.count))
.child(
div()
.child("Increment")
.on_click(cx.listener(|this, _event, _window, cx| {
this.increment(cx);
}))
)
}
}
```
### Using cx.listener
The `cx.listener()` method creates event handlers that receive `&mut Self`:
```rust
.on_click(cx.listener(|this: &mut Self, event, window, cx| {
// this: mutable reference to the entity
// event: the click event
// window: window reference
### Todo List with Entity Management
```rust
use gpui::*;
#[derive(Clone)]
struct TodoItem {
id: usize,
text: String,
completed: bool,
}
struct TodoList {
items: Vec<TodoItem>,
next_id: usize,
input_text: SharedString,
}
impl TodoList {
fn new() -> Self {
Self {
items: Vec::new(),
next_id: 1,
input_text: "".into(),
}
}
fn add_item(&mut self, cx: &mut Context<Self>) {
if !self.input_text.is_empty() {
self.items.push(TodoItem {
id: self.next_id,
text: self.input_text.to_string(),
completed: false,
});
self.next_id += 1;
self.input_text = "".into();
cx.notify();
}
}
fn toggle_item(&mut self, id: usize, cx: &mut Context<Self>) {
if let Some(item) = self.items.iter_mut().find(|i| i.id == id) {
item.completed = !item.completed;
cx.notify();
}
}
fn remove_item(&mut self, id: usize, cx: &mut Context<Self>) {
self.items.retain(|item| item.id != id);
cx.notify();
}
}
impl Render for TodoList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.p_6()
View on GitHub