It is recommand to read the serious GPUI Hello World Tutorial - From Core Concepts to Hello World before reading this article, it provide a basic entry to GPUI since this article is not a beginner tutorial.
This article is part of my studies on GPUI, I will write a series of articles to record my learning process and share my understanding of GPUI.
A entity is something that is owned and managed by GPUI, actully you can consider it as a RC in rust standard library, and we can access it through app context.
To create an entity, we can use cx.new method, and pass a closure that return the struct we want to create as entity.
let my_entity: Entity<MyStruct> = cx.new(|_cx| MyStruct {
// initialize fields
});
This is a simple example of creating an entity of type MyStruct, and we can access this entity through my_entity variable, and
with this way, GPUI can manage the lifecycle of MyStruct instance, and you should access it through GPUI context cx, it can be found in
various places, such as in render method of Render trait, or in new method of other entities, or in event handlers.
In GPUI, if a struct implement Render trait, and we create it to be managed by GPUI App as Entity<T>, it will be considered as a View that is used to render UI elements.
(this code is from GPUI Hello World Tutorial - From Core Concepts to Hello World):
use gpui::{
AppContext, Context, Entity, InteractiveElement, IntoElement, MouseUpEvent,
ParentElement, Render, Styled, Window, div, relative,
};
// this is a thing that we can see the ui got rerender or not.
use crate::utils::random_color::random_color;
struct Counter {
count: i32,
}
struct ReactiveCounter {
count: i32,
}
pub struct View {
counter1: Entity<Counter>,
reactive_counter: Entity<ReactiveCounter>,
}
impl View {
pub fn new(cx: &mut Context<Self>) -> Self {
let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
let second_counter = cx.new(|cx| {
cx.observe(
&first_counter,
|second: &mut ReactiveCounter, first: Entity<Counter>, cx| {
second.count = first.read(cx).count * 3;
},
)
.detach();
ReactiveCounter { count: 0 }
});
Self {
counter1: first_counter,
reactive_counter: second_counter,
}
}
pub fn on_increase(&mut self, _: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
self.counter1.update(cx, |counter, cx| {
counter.count += 1;
cx.notify();
})
}
}
impl Render for View {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.child(
div()
.on_mouse_up(gpui::MouseButton::Left, cx.listener(Self::on_increase))
.h_full()
.w(relative(0.5))
.bg(random_color())
.flex()
.justify_center()
.items_center()
.child(format!(
"COUNTER: {} \nReactive Counter: {}",
self.counter1.read(cx).count,
self.reactive_counter.read(cx).count,
)),
)
.child(div().h_full().w(relative(0.5)).bg(random_color()))
}
}
This is just a basic ui that display "Hello, GPUI World!" in a window. and you can see cx.new create a new Entity<HelloWorld>,
and HelloWorld implement Render trait, so it is a entity can render UI.
Entity can create without implement Render trait, in this case, the entity is not a view that can render UI,
and be consider as just a piece of data that is owned and managed by GPUI App.
impl View {
pub fn new(cx: &mut Context<Self>) -> Self {
let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
let second_counter = cx.new(|cx| {
cx.observe(
&first_counter,
|second: &mut ReactiveCounter, first: Entity<Counter>, cx| {
second.count = first.read(cx).count * 3;
cx.notify()
},
)
.detach();
ReactiveCounter { count: 0 }
});
Self {
counter1: first_counter,
reactive_counter: second_counter,
}
}
}
// impl render here, for simplicity, we don't show here
in such way, when first_counter change, second_counter will be updated automatically, don't forgett to call
cx.notify() to notify GPUI that the entity has changed.
This is a simple way to make an entity reactive to other entity changes, but not the only way, GPUI provide more powerful tool to make entity update to things happe, this also include event-based update.
use gpui::{
AppContext, Context, Entity, EventEmitter, InteractiveElement, IntoElement, MouseUpEvent,
ParentElement, Render, Styled, Window, div, relative,
};
// this is a thing that we can see the ui got rerender or not.
use crate::utils::random_color::random_color;
struct Counter {
count: i32,
}
struct EventCounter {
count: i32,
}
struct EventPayload {
count_now: i32,
}
impl EventEmitter<EventPayload> for Counter {}
pub struct View {
counter1: Entity<Counter>,
event_counter: Entity<EventCounter>,
}
impl View {
pub fn new(cx: &mut Context<Self>) -> Self {
let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
let event_counter = cx.new(|cx: &mut Context<EventCounter>| {
// Note we can set up the callback before the Counter is even created!
cx.subscribe(&first_counter, |second, _first, event, _cx| {
second.count = event.count_now << 1;
})
.detach();
EventCounter { count: 0 }
});
Self {
counter1: first_counter,
event_counter,
}
}
pub fn on_increase(&mut self, _: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
self.counter1.update(cx, |counter, cx| {
counter.count += 1;
cx.notify();
cx.emit(EventPayload {
count_now: counter.count,
});
})
}
}
impl Render for View {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.child(
div()
.on_mouse_up(gpui::MouseButton::Left, cx.listener(Self::on_increase))
.h_full()
.w(relative(0.5))
.bg(random_color())
.flex()
.justify_center()
.items_center()
.child(format!(
"COUNTER: {} \n Event Counter: {}",
self.counter1.read(cx).count,
self.event_counter.read(cx).count
)),
)
.child(div().h_full().w(relative(0.5)).bg(random_color()))
}
}
as you can see, we can use cx.subscribe to subscribe to the entity that send the event, and provide a callback that will be called when the entity emit an event,
in this case, when counter1 emit an event, event_counter will update its count to be counter1.count << 1.
BTW, you can see full code of these example in GPUI Experiment, which is part of my GPUI experiment project to make me understand GPUI better by writing code and testing things out.