gpui: Add use state APIs (#34741)
This PR adds a component level state API to GPUI, as well as a few utilities for simplified interactions with entities Release Notes: - N/A
This commit is contained in:
parent
4bdac8026c
commit
2da2ae65a0
10 changed files with 252 additions and 25 deletions
|
@ -422,6 +422,13 @@ impl AppContext for ExampleContext {
|
|||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<gpui::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
|
|
@ -448,15 +448,23 @@ impl App {
|
|||
}
|
||||
|
||||
pub(crate) fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.pending_updates += 1;
|
||||
self.start_update();
|
||||
let result = update(self);
|
||||
self.finish_update();
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn start_update(&mut self) {
|
||||
self.pending_updates += 1;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_update(&mut self) {
|
||||
if !self.flushing_effects && self.pending_updates == 1 {
|
||||
self.flushing_effects = true;
|
||||
self.flush_effects();
|
||||
self.flushing_effects = false;
|
||||
}
|
||||
self.pending_updates -= 1;
|
||||
result
|
||||
}
|
||||
|
||||
/// Arrange a callback to be invoked when the given entity calls `notify` on its respective context.
|
||||
|
@ -868,7 +876,6 @@ impl App {
|
|||
loop {
|
||||
self.release_dropped_entities();
|
||||
self.release_dropped_focus_handles();
|
||||
|
||||
if let Some(effect) = self.pending_effects.pop_front() {
|
||||
match effect {
|
||||
Effect::Notify { emitter } => {
|
||||
|
@ -1819,6 +1826,13 @@ impl AppContext for App {
|
|||
})
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
GpuiBorrow::new(handle.clone(), self)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
@ -2015,3 +2029,79 @@ impl HttpClient for NullHttpClient {
|
|||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to an entity owned by GPUI
|
||||
pub struct GpuiBorrow<'a, T> {
|
||||
inner: Option<Lease<T>>,
|
||||
app: &'a mut App,
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> GpuiBorrow<'a, T> {
|
||||
fn new(inner: Entity<T>, app: &'a mut App) -> Self {
|
||||
app.start_update();
|
||||
let lease = app.entities.lease(&inner);
|
||||
Self {
|
||||
inner: Some(lease),
|
||||
app,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> std::borrow::Borrow<T> for GpuiBorrow<'a, T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.inner.as_ref().unwrap().borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> std::borrow::BorrowMut<T> for GpuiBorrow<'a, T> {
|
||||
fn borrow_mut(&mut self) -> &mut T {
|
||||
self.inner.as_mut().unwrap().borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for GpuiBorrow<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
let lease = self.inner.take().unwrap();
|
||||
self.app.notify(lease.id);
|
||||
self.app.entities.end_lease(lease);
|
||||
self.app.finish_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{AppContext, TestAppContext};
|
||||
|
||||
#[test]
|
||||
fn test_gpui_borrow() {
|
||||
let cx = TestAppContext::single();
|
||||
let observation_count = Rc::new(RefCell::new(0));
|
||||
|
||||
let state = cx.update(|cx| {
|
||||
let state = cx.new(|_| false);
|
||||
cx.observe(&state, {
|
||||
let observation_count = observation_count.clone();
|
||||
move |_, _| {
|
||||
let mut count = observation_count.borrow_mut();
|
||||
*count += 1;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
state
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
// Calling this like this so that we don't clobber the borrow_mut above
|
||||
*std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true;
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
state.write(cx, false);
|
||||
});
|
||||
|
||||
assert_eq!(*observation_count.borrow(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
|||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
|
||||
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures::channel::oneshot;
|
||||
use std::{future::Future, rc::Weak};
|
||||
|
@ -58,6 +58,15 @@ impl AppContext for AsyncApp {
|
|||
Ok(app.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot as_mut with an async context. Try calling update() first"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
@ -364,6 +373,15 @@ impl AppContext for AsyncWindowContext {
|
|||
.update(self, |_, _, cx| cx.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Err(anyhow!(
|
||||
"Cannot use as_mut() from an async context, call `update`"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
|
|
@ -726,6 +726,13 @@ impl<T> AppContext for Context<'_, T> {
|
|||
self.app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
|
||||
where
|
||||
E: 'static,
|
||||
{
|
||||
self.app.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<U, R>(
|
||||
&self,
|
||||
handle: &Entity<U>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{App, AppContext, VisualContext, Window, seal::Sealed};
|
||||
use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashSet;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
@ -105,7 +105,7 @@ impl EntityMap {
|
|||
|
||||
/// Move an entity to the stack.
|
||||
#[track_caller]
|
||||
pub fn lease<'a, T>(&mut self, pointer: &'a Entity<T>) -> Lease<'a, T> {
|
||||
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
|
||||
self.assert_valid_context(pointer);
|
||||
let mut accessed_entities = self.accessed_entities.borrow_mut();
|
||||
accessed_entities.insert(pointer.entity_id);
|
||||
|
@ -117,15 +117,14 @@ impl EntityMap {
|
|||
);
|
||||
Lease {
|
||||
entity,
|
||||
pointer,
|
||||
id: pointer.entity_id,
|
||||
entity_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an entity after moving it to the stack.
|
||||
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
|
||||
self.entities
|
||||
.insert(lease.pointer.entity_id, lease.entity.take().unwrap());
|
||||
self.entities.insert(lease.id, lease.entity.take().unwrap());
|
||||
}
|
||||
|
||||
pub fn read<T: 'static>(&self, entity: &Entity<T>) -> &T {
|
||||
|
@ -187,13 +186,13 @@ fn double_lease_panic<T>(operation: &str) -> ! {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) struct Lease<'a, T> {
|
||||
pub(crate) struct Lease<T> {
|
||||
entity: Option<Box<dyn Any>>,
|
||||
pub pointer: &'a Entity<T>,
|
||||
pub id: EntityId,
|
||||
entity_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: 'static> core::ops::Deref for Lease<'_, T> {
|
||||
impl<T: 'static> core::ops::Deref for Lease<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
@ -201,13 +200,13 @@ impl<T: 'static> core::ops::Deref for Lease<'_, T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> core::ops::DerefMut for Lease<'_, T> {
|
||||
impl<T: 'static> core::ops::DerefMut for Lease<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.entity.as_mut().unwrap().downcast_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Lease<'_, T> {
|
||||
impl<T> Drop for Lease<T> {
|
||||
fn drop(&mut self) {
|
||||
if self.entity.is_some() && !panicking() {
|
||||
panic!("Leases must be ended with EntityMap::end_lease")
|
||||
|
@ -437,6 +436,19 @@ impl<T: 'static> Entity<T> {
|
|||
cx.update_entity(self, update)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
|
||||
cx.as_mut(self)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
|
||||
self.update(cx, |entity, cx| {
|
||||
*entity = value;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
/// the referenced entity still exists, within a visual context that has a window.
|
||||
/// Returns an error if the entity has been released.
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
use rand::{SeedableRng, rngs::StdRng};
|
||||
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||
|
@ -63,6 +64,13 @@ impl AppContext for TestAppContext {
|
|||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with a test app context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
@ -134,6 +142,12 @@ impl TestAppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a single TestAppContext, for non-multi-client tests
|
||||
pub fn single() -> Self {
|
||||
let dispatcher = TestDispatcher::new(StdRng::from_entropy());
|
||||
Self::build(dispatcher, None)
|
||||
}
|
||||
|
||||
/// The name of the test function that created this `TestAppContext`
|
||||
pub fn test_function_name(&self) -> Option<&'static str> {
|
||||
self.fn_name
|
||||
|
@ -914,6 +928,13 @@ impl AppContext for VisualTestContext {
|
|||
self.cx.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
|
|
|
@ -39,7 +39,7 @@ use crate::{
|
|||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
any::{Any, type_name},
|
||||
fmt::{self, Debug, Display},
|
||||
mem, panic,
|
||||
};
|
||||
|
@ -220,14 +220,17 @@ impl<C: RenderOnce> Element for Component<C> {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, element)
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
|
@ -239,7 +242,9 @@ impl<C: RenderOnce> Element for Component<C> {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.prepaint(window, cx);
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.prepaint(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
@ -252,7 +257,9 @@ impl<C: RenderOnce> Element for Component<C> {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.paint(window, cx);
|
||||
window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| {
|
||||
element.paint(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -197,6 +197,11 @@ pub trait AppContext {
|
|||
where
|
||||
T: 'static;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Read a entity from the app context.
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
|
|
|
@ -2424,6 +2424,53 @@ impl Window {
|
|||
result
|
||||
}
|
||||
|
||||
/// Use a piece of state that exists as long this element is being rendered in consecutive frames.
|
||||
pub fn use_keyed_state<S: 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
) -> Entity<S> {
|
||||
let current_view = self.current_view();
|
||||
self.with_global_id(key.into(), |global_id, window| {
|
||||
window.with_element_state(global_id, |state: Option<Entity<S>>, window| {
|
||||
if let Some(state) = state {
|
||||
(state.clone(), state)
|
||||
} else {
|
||||
let new_state = cx.new(|cx| init(window, cx));
|
||||
cx.observe(&new_state, move |_, cx| {
|
||||
cx.notify(current_view);
|
||||
})
|
||||
.detach();
|
||||
(new_state.clone(), new_state)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Immediately push an element ID onto the stack. Useful for simplifying IDs in lists
|
||||
pub fn with_id<R>(&mut self, id: impl Into<ElementId>, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.with_global_id(id.into(), |_, window| f(window))
|
||||
}
|
||||
|
||||
/// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key
|
||||
///
|
||||
/// NOTE: This method uses the location of the caller to generate an ID for this state.
|
||||
/// If this is not sufficient to identify your state (e.g. you're rendering a list item),
|
||||
/// you can provide a custom ElementID using the `use_keyed_state` method.
|
||||
#[track_caller]
|
||||
pub fn use_state<S: 'static>(
|
||||
&mut self,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
) -> Entity<S> {
|
||||
self.use_keyed_state(
|
||||
ElementId::CodeLocation(*core::panic::Location::caller()),
|
||||
cx,
|
||||
init,
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates or initializes state for an element with the given id that lives across multiple
|
||||
/// frames. If an element with this ID existed in the rendered frame, its state will be passed
|
||||
/// to the given closure. The state returned by the closure will be stored so it can be referenced
|
||||
|
@ -4577,6 +4624,8 @@ pub enum ElementId {
|
|||
NamedInteger(SharedString, u64),
|
||||
/// A path.
|
||||
Path(Arc<std::path::Path>),
|
||||
/// A code location.
|
||||
CodeLocation(core::panic::Location<'static>),
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
|
@ -4596,6 +4645,7 @@ impl Display for ElementId {
|
|||
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
||||
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
||||
ElementId::Path(path) => write!(f, "{}", path.display())?,
|
||||
ElementId::CodeLocation(location) => write!(f, "{}", location)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -53,6 +53,16 @@ pub fn derive_app_context(input: TokenStream) -> TokenStream {
|
|||
self.#app_variable.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'y, 'z, T>(
|
||||
&'y mut self,
|
||||
handle: &'z gpui::Entity<T>,
|
||||
) -> Self::Result<gpui::GpuiBorrow<'y, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.#app_variable.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &gpui::Entity<T>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue