| 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.
fn do_something(cx: &mut App) {
}
Context
Provided when updating an Entity<T>. This context dereferences into App, so functions which take &App can also take &Context<T>.
struct MyView {
count: usize,
}
impl MyView {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();
}
}
AsyncApp and AsyncWindowContext
Provided by cx.spawn() for async operations. These can be held across await points.
fn start_async_work(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
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.
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
app.run(move |cx| {
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|_cx| MyView { count: 0 })
})?;
Ok::<_, anyhow::Error>(())
}).detach();
});
Entity Operations
let id = thing.entity_id();
let weak = thing.downgrade();
let value = thing.read(cx);
println!("Count: {}", value.count);
let count = thing.read_with(cx, |view, cx| view.count);
thing.update(cx, |view, cx| {
view.count += 1;
cx.notify();
});
thing.update_in(cx, |view, window, cx| {
view.count += 1;
window.dispatch_action(SomeAction.boxed_clone(), cx);
cx.notify();
});
Important Rules
- Use inner cx: Within closures, use the inner
cx provided to the closure, not the outer cx
entity.update(cx, |view, inner_cx| {
view.count += 1;
cx.notify();
});
entity.update(cx, |view, inner_cx| {
view.count += 1;
inner_cx.notify();
});
- Avoid update-while-updating: Never update an entity while it's already being updated (causes panic)
entity.update(cx, |view, cx| {
entity.update(cx, |view2, cx2| {
});
});
Elements
The Render trait is used to render state into an element tree with flexbox layout.
Render Trait
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:
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)
)
}
}
impl Render for MyView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl {
().(Card {
title: .(),
content: .(),
})
}
}
SharedString
Use SharedString to avoid copying strings. It's either &'static str or Arc<str>.
use gpui::*;
struct MyView {
title: SharedString,
}
impl MyView {
fn new() -> Self {
Self {
title: "Hello".into(),
}
}
fn set_title(&mut self, title: String) {
self.title = title.into();
}
}
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
div()
.child("Text")
.child(div().child("Nested"))
.child(another_element())
Conditional Rendering
Use .when() for conditional attributes/children:
div()
.when(is_active, |this| {
this.bg(rgb(0x3b82f6))
})
.when_some(maybe_text, |this, text| {
this.child(text)
})
Multiple Children
div()
.children(vec![
div().child("Item 1"),
div().child("Item 2"),
div().child("Item 3"),
])
Application Lifecycle
Basic Application
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
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:
struct Counter {
count: usize,
}
impl Counter {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();
}
}
Event Handlers
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:
.on_click(cx.listener(|this: &mut Self, event, window, cx| {
### 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,
});
.next_id += ;
.input_text = .();
cx.();
}
}
(& , id: , cx: & Context<>) {
(item) = .items.().(|i| i.id == id) {
item.completed = !item.completed;
cx.();
}
}
(& , id: , cx: & Context<>) {
.items.(|item| item.id != id);
cx.();
}
}
{
(& , _window: & Window, cx: & Context<>) {
()
.()
.()
.(())
.()
.()
.()
.(
()
.()
.()
.(())
.()
)
.(
()
.()
.()
.(
()
.()
.()
.()
.(())
.()
.(())
.()
.(.input_text.())
)
.(
()
.()
.()
.(())
.()
.()
.()
.(cx.(|this, _event, _window, cx| {
this.(cx);
}))
)
)
.(
()
.()
.()
.()
.(
.items.().(|item| {
= item.id;
()
.()
.()
.()
.()
.(())
.()
.(
()
.(())
.(())
.()
.(())
.(())
.()
.(item.completed, |this| {
this.(())
})
.(cx.( |this, _event, _window, cx| {
this.(id, cx);
}))
)
.(
()
.()
.( item.completed {
()
} {
()
})
.(item.completed, |this| {
this.()
})
.(&item.text)
)
.(
()
.()
.()
.(())
.(())
.()
.()
.(cx.( |this, _event, _window, cx| {
this.(id, cx);
}))
)
})
)
)
}
}
Performance Tips
1. Minimize Entity Updates
entity.update(cx, |view, cx| { view.x = 10; cx.notify(); });
entity.update(cx, |view, cx| { view.y = 20; cx.notify(); });
entity.update(cx, |view, cx| { view.z = 30; cx.notify(); });
entity.update(cx, |view, cx| {
view.x = 10;
view.y = 20;
view.z = 30;
cx.notify();
});
2. Use WeakEntity for Callbacks
struct Parent {
child: Entity<Child>,
}
struct Callback {
target: WeakEntity<Target>,
}
3. Batch Notifications
struct BatchUpdate {
needs_notify: bool,
}
impl BatchUpdate {
fn update_multiple(&mut self, cx: &mut Context<Self>) {
self.field1 = value1;
self.field2 = value2;
self.field3 = value3;
cx.notify();
}
}
4. Avoid Unnecessary Clones
div().child(self.text.clone().to_string())
div().child(self.text.clone())
Common Mistakes
| Mistake | Fix |
|---|
Using outer cx in update closure | Use the inner cx provided to closure |
| Nested entity updates | Restructure to avoid updating entity while updating |
Forgetting cx.notify() | Call after state changes that affect rendering |
Not using SharedString | Use SharedString for text to avoid copies |
Update without window in Render | Use Context methods that don't need Window |
Calling .unwrap() on entity operations | Use ? or handle errors properly |
Not storing Subscription | Store in struct field to keep subscription alive |
Using smol::Timer in tests | Use cx.background_executor.timer() |
Summary
- Entities (
Entity<T>): Handles to shared state
- Contexts (
App, Context<T>): Access to framework services
- Window: Window-specific operations
- Elements: Built with
div() and styled with methods
- Render: Trait for converting state to UI
- SharedString: Efficient string type for UI text
References