blob: 8d93f2d6b1fcf3914e1ac8ae37a5071d54eb1c58 [file]
use std::{
any::Any,
cell::Cell,
collections::{HashMap, HashSet},
fmt,
marker::PhantomData,
rc::{Rc, Weak},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
pub(crate) const MAX_RETAINED_COMPONENT_STATES: usize = 4096;
#[derive(Default)]
pub(crate) struct RetainedStateStore {
next_handle: u64,
values: HashMap<u64, RetainedStateEntry>,
}
struct RetainedStateEntry {
kind: &'static str,
owner: Option<Rc<crate::runtime::ApplicationGeneration>>,
value: RetainedState,
/// Script subscriptions to the state's events, which end with the slot.
subscriptions: Vec<gpui::Subscription>,
}
impl RetainedStateStore {
#[cfg(test)]
pub fn len(&self) -> usize {
self.values.len()
}
pub fn insert(
&mut self,
kind: &'static str,
owner: Option<Rc<crate::runtime::ApplicationGeneration>>,
value: RetainedState,
) -> Result<u64, String> {
if owner.as_ref().is_some_and(|owner| !owner.is_active()) {
return Err("retained component state owner has already been released".into());
}
if self.values.len() >= MAX_RETAINED_COMPONENT_STATES {
return Err(format!(
"retained component state limit ({MAX_RETAINED_COMPONENT_STATES}) reached"
));
}
let handle = self.next_handle;
self.next_handle = self
.next_handle
.checked_add(1)
.ok_or_else(|| "retained component state handle space exhausted".to_owned())?;
self.values.insert(
handle,
RetainedStateEntry {
kind,
owner,
value,
subscriptions: Vec::new(),
},
);
Ok(handle)
}
/// Keeps `subscription` alive for as long as the state at `handle` is.
pub fn attach(&mut self, handle: u64, subscription: gpui::Subscription) -> anyhow::Result<()> {
let entry = self
.values
.get_mut(&handle)
.ok_or_else(|| anyhow::anyhow!("retained state handle has been released"))?;
entry.subscriptions.push(subscription);
Ok(())
}
/// Subscribes to the input events of the state at `handle`, through its
/// descriptor's [`StateDescriptor::with_input_events`].
pub(crate) fn subscribe(
&self,
handle: u64,
descriptor: &StateDescriptor,
event: crate::entities::InputEventName,
handler: InputEventHandler,
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<gpui::Subscription> {
let subscribe = descriptor
.events
.as_ref()
.ok_or_else(|| anyhow::anyhow!("`{}` has no events", descriptor.export))?;
let entry = self
.values
.get(&handle)
.ok_or_else(|| anyhow::anyhow!("retained state handle has been released"))?;
anyhow::ensure!(
entry.owner.as_ref().is_none_or(|owner| owner.is_active()),
"retained state owner has been released"
);
anyhow::ensure!(
entry.kind == descriptor.kind,
"retained state kind mismatch: expected `{}`, found `{}`",
descriptor.kind,
entry.kind
);
subscribe(&entry.value, event, handler, window, cx).ok_or_else(|| {
anyhow::anyhow!(
"adapter state type does not match registered kind `{}`",
descriptor.kind
)
})
}
pub fn kind(&self, handle: u64) -> Option<&'static str> {
self.values.get(&handle).and_then(|entry| {
entry
.owner
.as_ref()
.is_none_or(|owner| owner.is_active())
.then_some(entry.kind)
})
}
pub fn with<T: Any, R>(
&self,
handle: u64,
kind: &'static str,
body: impl FnOnce(&T) -> R,
) -> anyhow::Result<R> {
let entry = self
.values
.get(&handle)
.ok_or_else(|| anyhow::anyhow!("retained state handle has been released"))?;
anyhow::ensure!(
entry.owner.as_ref().is_none_or(|owner| owner.is_active()),
"retained state owner has been released"
);
anyhow::ensure!(
entry.kind == kind,
"retained state kind mismatch: expected `{kind}`, found `{}`",
entry.kind
);
let value = entry.value.downcast_ref::<T>().ok_or_else(|| {
anyhow::anyhow!("adapter state type does not match registered kind `{kind}`")
})?;
Ok(body(value))
}
pub fn with_mut<T: Any, R>(
&mut self,
handle: u64,
kind: &'static str,
body: impl FnOnce(&mut T) -> R,
) -> anyhow::Result<R> {
let entry = self
.values
.get_mut(&handle)
.ok_or_else(|| anyhow::anyhow!("retained state handle has been released"))?;
anyhow::ensure!(
entry.owner.as_ref().is_none_or(|owner| owner.is_active()),
"retained state owner has been released"
);
anyhow::ensure!(
entry.kind == kind,
"retained state kind mismatch: expected `{kind}`, found `{}`",
entry.kind
);
let value = entry.value.downcast_mut::<T>().ok_or_else(|| {
anyhow::anyhow!("adapter state type does not match registered kind `{kind}`")
})?;
Ok(body(value))
}
/// Drops one state of `kind` ahead of its application, with its
/// subscriptions. Returns whether it was still live.
pub fn remove(&mut self, handle: u64, kind: &'static str) -> bool {
if self
.values
.get(&handle)
.is_none_or(|entry| entry.kind != kind)
{
return false;
}
self.values
.remove(&handle)
.is_some_and(|entry| entry.owner.as_ref().is_none_or(|owner| owner.is_active()))
}
pub fn release_application(&mut self, application: &Rc<crate::runtime::ApplicationGeneration>) {
self.values.retain(|_, entry| {
!entry
.owner
.as_ref()
.is_some_and(|owner| Rc::ptr_eq(owner, application))
});
}
}
use anyhow::Context as _;
use smallvec::SmallVec;
use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, Refineable as _, StyleRefinement,
Styled, Window,
};
pub const COMPONENT_REGISTRY_API_VERSION: u32 = 1;
/// The specifier the shipped `gpui-component` adapter imports under.
///
/// A registry names its own module, because the runtime holds no opinion about
/// which component library it is carrying. This constant only spares the usual
/// caller from repeating the usual answer.
pub const DEFAULT_COMPONENT_MODULE: &str = "gpui-component";
#[derive(Clone)]
/// An owned, repeatable recipe for one element description.
///
/// Each build walks the frozen snapshot again. Clones share the same snapshot
/// lease and recursion guard, so deferred GPUI overlay callbacks never retain
/// an `AnyElement` built against an arena that has already been cleared.
pub struct ComponentElementFactory {
build: Rc<dyn Fn(&mut Window, &mut App) -> anyhow::Result<AnyElement>>,
building: Rc<Cell<bool>>,
}
impl ComponentElementFactory {
pub(crate) fn new(
build: impl Fn(&mut Window, &mut App) -> anyhow::Result<AnyElement> + 'static,
) -> Self {
Self {
build: Rc::new(build),
building: Rc::new(Cell::new(false)),
}
}
pub fn build(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<AnyElement> {
anyhow::ensure!(
!self.building.replace(true),
"component element factory is already building"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
let _reset = Reset(&self.building);
(self.build)(window, cx)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ComponentId(u32);
impl ComponentId {
pub fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Clone)]
pub struct ComponentPayload(Arc<dyn Any + Send + Sync>);
impl ComponentPayload {
pub fn new<T: Any + Send + Sync>(value: T) -> Self {
Self(Arc::new(value))
}
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
self.0.downcast_ref()
}
}
impl fmt::Debug for ComponentPayload {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("ComponentPayload").finish()
}
}
impl PartialEq for ComponentPayload {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ComponentArgument {
String(String),
Number(f64),
Boolean(bool),
Element(u32),
Entity { kind: &'static str, handle: u64 },
Callback(u64),
Enum(String),
Array(Vec<ComponentArgument>),
Optional(Option<Box<ComponentArgument>>),
}
/// A value an adapter may pass back to a JavaScript component callback.
///
/// This intentionally stays closed: opaque shell handles and retained element
/// descriptions never cross back into the script event boundary.
#[derive(Clone, Debug, PartialEq)]
pub enum ComponentCallbackArgument {
String(String),
Number(f64),
Boolean(bool),
/// A list, for a component that reports a set rather than a single value —
/// an accordion says which sections are open, not which one was clicked.
Array(Vec<ComponentCallbackArgument>),
}
/// A deliberately closed value returned by a script-backed component delegate.
#[derive(Clone, Debug, PartialEq)]
pub enum ComponentCallbackValue {
Null,
String(String),
Number(f64),
Boolean(bool),
}
/// Immutable plain data returned by a generation-bound delegate snapshot.
#[derive(Clone, Debug, PartialEq)]
pub enum ComponentDataValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<ComponentDataValue>),
Object(Vec<(String, ComponentDataValue)>),
}
#[derive(Clone, Debug)]
pub struct ComponentDelegateSnapshot {
rows: Arc<[ComponentDataValue]>,
}
impl ComponentDelegateSnapshot {
pub fn new(rows: Vec<ComponentDataValue>) -> Self {
Self { rows: rows.into() }
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn row(&self, index: usize) -> anyhow::Result<&ComponentDataValue> {
self.rows.get(index).ok_or_else(|| {
anyhow::anyhow!(
"delegate row index {index} is out of bounds for {} rows",
self.rows.len()
)
})
}
}
/// One child description owned by a single [`MaterializeRequest`].
///
/// The token is deliberately opaque. It can only be consumed by the request
/// that issued it, and only registered component identities are exposed.
pub struct ComponentChild<'request> {
runtime: Weak<crate::ShellRuntime>,
request_id: u64,
token: u64,
component_name: Option<&'static str>,
request_lifetime: PhantomData<&'request ()>,
}
impl ComponentChild<'_> {
pub fn component_name(&self) -> Option<&'static str> {
self.component_name
}
}
static NEXT_MATERIALIZE_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ChildLane {
Unclaimed,
Ordinary,
Typed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ArgumentSchema {
String,
Number,
Boolean,
Element,
Entity(&'static str),
Callback(&'static str),
Enum(&'static [&'static str]),
Array(Box<ArgumentSchema>),
Optional(Box<ArgumentSchema>),
}
/// One argument of a registered constructor, method, or state factory.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ArgumentDescriptor {
name: &'static str,
schema: ArgumentSchema,
}
impl ArgumentDescriptor {
pub const fn new(name: &'static str, schema: ArgumentSchema) -> Self {
Self { name, schema }
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn schema(&self) -> &ArgumentSchema {
&self.schema
}
}
type PayloadFactory =
dyn Fn(&[ComponentArgument]) -> Result<ComponentPayload, String> + Send + Sync + 'static;
/// A retained, adapter-owned value created on GPUI's application thread.
///
/// Unlike [`ComponentPayload`], the value intentionally need not be `Send` or
/// `Sync`: GPUI entities and component state are owned by the app thread.
pub type RetainedState = Box<dyn Any + 'static>;
type StateFactory = dyn Fn(&[ComponentArgument], &mut Window, &mut App) -> Result<RetainedState, String>
+ Send
+ Sync
+ 'static;
pub(crate) type InputEventHandler =
Box<dyn Fn(&gpui_base::input::InputEvent, &mut Window, &mut App)>;
type SubscribeInputEvents = dyn Fn(
&RetainedState,
crate::entities::InputEventName,
InputEventHandler,
&mut Window,
&mut App,
) -> Option<gpui::Subscription>
+ Send
+ Sync;
type PreparedStateCall =
Box<dyn FnOnce(&mut Window, &mut App) -> anyhow::Result<ComponentDataValue>>;
type PrepareStateCall = dyn Fn(
&RetainedStateStore,
u64,
&'static str,
Vec<ComponentDataValue>,
) -> anyhow::Result<PreparedStateCall>
+ Send
+ Sync;
/// An opt-in operation on retained state. Clone the handle before entering GPUI,
/// so callbacks never execute with the registry's state table borrowed.
#[derive(Clone)]
pub struct StateMethodDescriptor {
name: &'static str,
signature: &'static str,
readonly: bool,
prepare: Arc<PrepareStateCall>,
}
impl StateMethodDescriptor {
pub fn new<T: Any + Clone>(
name: &'static str,
signature: &'static str,
call: impl Fn(
&T,
&[ComponentDataValue],
&mut Window,
&mut App,
) -> anyhow::Result<ComponentDataValue>
+ Send
+ Sync
+ 'static,
) -> Self {
let call = Arc::new(call);
Self {
name,
signature,
readonly: false,
prepare: Arc::new(move |store, handle, kind, args| {
let state = store.with::<T, _>(handle, kind, Clone::clone)?;
let call = call.clone();
Ok(Box::new(move |window, cx| call(&state, &args, window, cx)))
}),
}
}
pub fn with_readonly(mut self, readonly: bool) -> Self {
self.readonly = readonly;
self
}
pub fn name(&self) -> &'static str {
self.name
}
/// TypeScript parameter list and result, e.g. `(): string`.
pub fn signature(&self) -> &'static str {
self.signature
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
pub(crate) fn prepare(
&self,
store: &RetainedStateStore,
handle: u64,
kind: &'static str,
args: Vec<ComponentDataValue>,
) -> anyhow::Result<PreparedStateCall> {
(self.prepare)(store, handle, kind, args)
}
}
#[derive(Clone)]
pub struct StateDescriptor {
methods: Vec<StateMethodDescriptor>,
export: &'static str,
kind: &'static str,
arguments: Vec<ArgumentDescriptor>,
documentation: Option<&'static str>,
factory: Arc<StateFactory>,
events: Option<Arc<SubscribeInputEvents>>,
}
impl StateDescriptor {
/// Registers an app-thread state factory.
///
/// The shell validates arguments first, and only publishes the returned
/// value after the factory succeeds. On error it rolls back callbacks
/// recorded while validating arguments and inserts no retained-state slot.
/// The factory itself is responsible for transactional behavior: it must
/// not perform irreversible external side effects before returning `Ok`.
pub fn new(
export: &'static str,
kind: &'static str,
arguments: Vec<ArgumentDescriptor>,
factory: impl Fn(&[ComponentArgument], &mut Window, &mut App) -> Result<RetainedState, String>
+ Send
+ Sync
+ 'static,
) -> Self {
Self {
methods: Vec::new(),
export,
kind,
arguments,
documentation: None,
factory: Arc::new(factory),
events: None,
}
}
pub fn with_documentation(mut self, documentation: &'static str) -> Self {
self.documentation = Some(documentation);
self
}
/// Gives the state `on(event, handler)`, for a state whose value is an
/// `Entity<InputBaseState<M>>` — the text engine `gpui-base`'s
/// `InputState` and `TextareaState` script bindings sit on.
///
/// The events, their names and their payloads are the ones those bindings
/// already answer: `change`, `submit` (Enter, with `secondary` and
/// `shift`), `focus` and `blur`. A script moving an input between the two
/// modules keeps its subscriptions as they are.
pub fn with_input_events<M: gpui_base::input::InputModeKind>(mut self) -> Self {
self.events = Some(Arc::new(|value, event, handler, window, cx| {
let state =
value.downcast_ref::<gpui::Entity<gpui_base::input::InputBaseState<M>>>()?;
Some(crate::entities::subscribe_to_events(
state, event, window, cx, handler,
))
}));
self
}
/// Whether the state answers `on(event, handler)`; see
/// [`Self::with_input_events`].
pub fn has_input_events(&self) -> bool {
self.events.is_some()
}
pub fn with_methods(mut self, methods: Vec<StateMethodDescriptor>) -> Self {
self.methods = methods;
self
}
pub fn methods(&self) -> &[StateMethodDescriptor] {
&self.methods
}
pub fn export(&self) -> &'static str {
self.export
}
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn arguments(&self) -> &[ArgumentDescriptor] {
&self.arguments
}
pub fn documentation(&self) -> Option<&'static str> {
self.documentation
}
pub(crate) fn create(
&self,
arguments: &[ComponentArgument],
window: &mut Window,
cx: &mut App,
) -> Result<RetainedState, String> {
(self.factory)(arguments, window, cx)
}
}
impl fmt::Debug for StateDescriptor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("StateDescriptor")
.field("export", &self.export)
.field("kind", &self.kind)
.field("arguments", &self.arguments)
.field("documentation", &self.documentation)
.finish_non_exhaustive()
}
}
/// Why a constructor export should no longer be used, and what replaces it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeprecationDescriptor {
replacement: &'static str,
message: &'static str,
}
impl DeprecationDescriptor {
pub fn replacement(&self) -> &'static str {
self.replacement
}
pub fn message(&self) -> &'static str {
self.message
}
}
#[derive(Clone)]
pub struct ConstructorDescriptor {
export: &'static str,
arguments: Vec<ArgumentDescriptor>,
deprecation: Option<DeprecationDescriptor>,
factory: Arc<PayloadFactory>,
}
impl ConstructorDescriptor {
pub fn new(
export: &'static str,
arguments: Vec<ArgumentDescriptor>,
factory: impl Fn(&[ComponentArgument]) -> Result<ComponentPayload, String>
+ Send
+ Sync
+ 'static,
) -> Self {
Self {
export,
arguments,
deprecation: None,
factory: Arc::new(factory),
}
}
pub fn with_deprecation(mut self, replacement: &'static str, message: &'static str) -> Self {
self.deprecation = Some(DeprecationDescriptor {
replacement,
message,
});
self
}
pub fn export(&self) -> &'static str {
self.export
}
pub fn arguments(&self) -> &[ArgumentDescriptor] {
&self.arguments
}
pub fn deprecation(&self) -> Option<&DeprecationDescriptor> {
self.deprecation.as_ref()
}
pub(crate) fn payload(
&self,
arguments: &[ComponentArgument],
) -> Result<ComponentPayload, String> {
(self.factory)(arguments)
}
}
impl fmt::Debug for ConstructorDescriptor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ConstructorDescriptor")
.field("export", &self.export)
.field("arguments", &self.arguments)
.field("deprecation", &self.deprecation)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct MethodDescriptor {
name: &'static str,
arguments: Vec<ArgumentDescriptor>,
documentation: Option<&'static str>,
recorder: Arc<PayloadFactory>,
}
impl MethodDescriptor {
pub fn new(
name: &'static str,
arguments: Vec<ArgumentDescriptor>,
recorder: impl Fn(&[ComponentArgument]) -> Result<ComponentPayload, String>
+ Send
+ Sync
+ 'static,
) -> Self {
Self {
name,
arguments,
documentation: None,
recorder: Arc::new(recorder),
}
}
pub fn with_documentation(mut self, documentation: &'static str) -> Self {
self.documentation = Some(documentation);
self
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn arguments(&self) -> &[ArgumentDescriptor] {
&self.arguments
}
pub fn documentation(&self) -> Option<&'static str> {
self.documentation
}
pub(crate) fn record(
&self,
arguments: &[ComponentArgument],
) -> Result<ComponentPayload, String> {
(self.recorder)(arguments)
}
}
impl fmt::Debug for MethodDescriptor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("MethodDescriptor")
.field("name", &self.name)
.field("arguments", &self.arguments)
.field("documentation", &self.documentation)
.finish_non_exhaustive()
}
}
pub struct MaterializeRequest<'a> {
component_name: &'static str,
payload: &'a ComponentPayload,
operations: &'a [crate::spec::SpecOp],
runtime: &'a Rc<crate::ShellRuntime>,
resolve_element:
&'a mut dyn FnMut(u32, Option<&mut Window>, Option<&mut App>) -> anyhow::Result<AnyElement>,
window: Option<&'a mut Window>,
cx: Option<&'a mut App>,
style: Option<StyleRefinement>,
children: Children,
/// Children this request has not resolved yet, oldest first. A cursor
/// rather than repeated `remove(0)`, so a component with many children
/// costs one pass instead of one shift per child.
child_specs: ChildSpecs,
resolved_child_specs: usize,
issued_children: SmallVec<[(u64, u32); 4]>,
next_child_token: u64,
request_id: u64,
element_id: gpui::ElementId,
child_lane: ChildLane,
slots: Slots,
/// Deferred slots, still unbuilt. A factory leases the snapshot and
/// allocates, so one is built only when an adapter asks for it by name.
slot_factory_specs: SlotSpecs,
make_slot_factory: &'a dyn Fn(u32) -> ComponentElementFactory,
disabled: bool,
selected: bool,
on_click: Option<crate::spec::CallbackId>,
application_owner: Option<(
Rc<crate::runtime::ApplicationGeneration>,
gpui::WeakEntity<crate::ScriptView>,
)>,
}
pub(crate) type Children = SmallVec<[AnyElement; 8]>;
pub(crate) type ChildSpecs = SmallVec<[(u32, Option<&'static str>); 8]>;
pub(crate) type Slots = SmallVec<[(&'static str, AnyElement); 2]>;
pub(crate) type SlotSpecs = SmallVec<[(&'static str, u32); 2]>;
pub(crate) struct MaterializeRequestInit<'a> {
pub component_name: &'static str,
pub element_id: gpui::ElementId,
pub payload: &'a ComponentPayload,
pub operations: &'a [crate::spec::SpecOp],
pub runtime: &'a Rc<crate::ShellRuntime>,
pub resolve_element:
&'a mut dyn FnMut(u32, Option<&mut Window>, Option<&mut App>) -> anyhow::Result<AnyElement>,
pub style: StyleRefinement,
pub children: Children,
pub child_specs: ChildSpecs,
pub slots: Slots,
pub slot_factory_specs: SlotSpecs,
pub make_slot_factory: &'a dyn Fn(u32) -> ComponentElementFactory,
pub disabled: bool,
pub selected: bool,
pub on_click: Option<crate::spec::CallbackId>,
pub application_owner: Option<(
Rc<crate::runtime::ApplicationGeneration>,
gpui::WeakEntity<crate::ScriptView>,
)>,
}
impl<'a> MaterializeRequest<'a> {
pub(crate) fn new(init: MaterializeRequestInit<'a>) -> Self {
Self {
component_name: init.component_name,
payload: init.payload,
operations: init.operations,
runtime: init.runtime,
resolve_element: init.resolve_element,
window: None,
cx: None,
style: Some(init.style),
children: init.children,
child_specs: init.child_specs,
resolved_child_specs: 0,
issued_children: SmallVec::new(),
next_child_token: 0,
request_id: NEXT_MATERIALIZE_REQUEST_ID.fetch_add(1, Ordering::Relaxed),
element_id: init.element_id,
child_lane: ChildLane::Unclaimed,
slots: init.slots,
slot_factory_specs: init.slot_factory_specs,
make_slot_factory: init.make_slot_factory,
disabled: init.disabled,
selected: init.selected,
on_click: init.on_click,
application_owner: init.application_owner,
}
}
/// Returns an application-lifecycle effect capability for this materialization.
/// The effect is deferred beyond render and is retired with the root view.
pub fn app_effects(&self) -> anyhow::Result<ComponentAppEffects> {
let (application, view) = self.application_owner.clone().ok_or_else(|| {
anyhow::anyhow!("component app effects require a root application snapshot")
})?;
Ok(ComponentAppEffects::new(self.runtime, application, view))
}
/// The element id of the spec node being materialized: the script's `key`
/// for it, or its address in the description.
///
/// A component that keeps per-element state — a hitbox, a hover, a cached
/// path — keys it on this, so two of the same component in one description
/// stay apart instead of sharing one slot. Plain elements already carry it;
/// a registered component reads it from here.
pub fn element_id(&self) -> &gpui::ElementId {
&self.element_id
}
pub fn payload(&self) -> &ComponentPayload {
self.payload
}
pub(crate) fn attach_render_authority(&mut self, window: &'a mut Window, cx: &'a mut App) {
self.window = Some(window);
self.cx = Some(cx);
}
/// Runs `body` with the active GPUI render authority.
///
/// This capability exists for components such as overlays whose native
/// constructors require the current [`Window`] and [`App`]. The shell
/// enforces only the references' lifetime and availability during the
/// render phase. These are raw GPUI authorities and can mutate application
/// state; adapters remain responsible for following their own state and
/// callback conventions and must not retain either reference.
pub fn with_window_app<R>(
&mut self,
body: impl FnOnce(&mut Window, &mut App) -> anyhow::Result<R>,
) -> anyhow::Result<R> {
let window = self
.window
.as_deref_mut()
.ok_or_else(|| anyhow::anyhow!("render Window authority is unavailable"))?;
let cx = self
.cx
.as_deref_mut()
.ok_or_else(|| anyhow::anyhow!("render App authority is unavailable"))?;
body(window, cx)
}
/// Reads adapter-owned retained state without allowing render-time mutation.
pub fn with_state<T: Any, R>(
&self,
argument: &ComponentArgument,
body: impl FnOnce(&T) -> R,
) -> anyhow::Result<R> {
let ComponentArgument::Entity { kind, handle } = argument else {
anyhow::bail!("component argument is not retained state");
};
self.runtime
.with_component_state::<T, R>(*handle, kind, body)
}
/// Returns an opaque capability that may update state later from a GPUI event.
pub fn state_handle<T: Any>(
&self,
argument: &ComponentArgument,
) -> anyhow::Result<ComponentState<T>> {
let ComponentArgument::Entity { kind, handle } = argument else {
anyhow::bail!("component argument is not retained state");
};
self.runtime
.with_component_state::<T, _>(*handle, kind, |_| ())?;
Ok(ComponentState {
runtime: Rc::downgrade(self.runtime),
kind,
handle: *handle,
state: PhantomData,
})
}
pub fn methods(&self) -> impl Iterator<Item = &RecordedComponentMethod> {
self.operations
.iter()
.filter_map(|operation| match operation {
crate::spec::SpecOp::RegisteredMethod(method) => Some(method),
_ => None,
})
}
pub fn disabled(&self) -> bool {
self.disabled
}
pub fn selected(&self) -> bool {
self.selected
}
pub fn children_len(&self) -> usize {
self.children.len() + self.remaining_child_specs() + self.issued_children.len()
}
fn remaining_child_specs(&self) -> usize {
self.child_specs.len() - self.resolved_child_specs
}
pub fn take_children(&mut self) -> anyhow::Result<Vec<AnyElement>> {
anyhow::ensure!(
self.child_lane != ChildLane::Typed,
"{} already issued typed children; take_children is exclusive with take_typed_children",
self.component_name
);
self.child_lane = ChildLane::Ordinary;
let mut children = std::mem::take(&mut self.children).into_vec();
while self.resolved_child_specs < self.child_specs.len() {
let (id, _) = self.child_specs[self.resolved_child_specs];
// Resolved before the cursor advances: a child that fails stays
// unconsumed, so `Drop` still reports it.
let child =
(self.resolve_element)(id, self.window.as_deref_mut(), self.cx.as_deref_mut())?;
self.resolved_child_specs += 1;
children.push(child);
}
self.child_specs.clear();
self.resolved_child_specs = 0;
Ok(children)
}
/// Takes opaque child tokens for adapters whose Rust component requires
/// typed children. This is exclusive with [`Self::take_children`].
///
/// Mixing the two lanes is a programming error in the adapter, not a
/// script mistake, so it is reported rather than answered with an empty
/// list: silently rendering a component with none of its children, and
/// only a log line to say why, is the harder failure to find.
pub fn take_typed_children(&mut self) -> anyhow::Result<Vec<ComponentChild<'a>>> {
anyhow::ensure!(
self.child_lane != ChildLane::Ordinary,
"{} already materialized ordinary children; take_typed_children is exclusive with take_children",
self.component_name
);
self.child_lane = ChildLane::Typed;
self.resolved_child_specs = 0;
Ok(std::mem::take(&mut self.child_specs)
.into_iter()
.map(|(id, component_name)| {
let token = self.next_child_token;
self.next_child_token = self
.next_child_token
.checked_add(1)
.expect("a materialization request exhausted its child token space");
self.issued_children.push((token, id));
ComponentChild {
runtime: Rc::downgrade(self.runtime),
request_id: self.request_id,
token,
component_name,
request_lifetime: PhantomData,
}
})
.collect())
}
/// Materializes one opaque child token exactly once.
pub fn materialize_child(
&mut self,
child: &mut ComponentChild<'a>,
) -> anyhow::Result<AnyElement> {
anyhow::ensure!(
child.request_id == self.request_id
&& Weak::ptr_eq(&child.runtime, &Rc::downgrade(self.runtime)),
"component child belongs to a different runtime or materialization request"
);
let index = self
.issued_children
.iter()
.position(|(token, _)| *token == child.token)
.ok_or_else(|| anyhow::anyhow!("component child was already consumed"))?;
let id = self.issued_children[index].1;
let element =
(self.resolve_element)(id, self.window.as_deref_mut(), self.cx.as_deref_mut())?;
// Order does not matter here — the token, not the position, identifies
// the child — so this avoids shifting the rest on every consumed child.
self.issued_children.swap_remove(index);
Ok(element)
}
/// Takes a named, already-materialized slot.
///
/// Slots remain separate from ordinary children because the registered
/// component owns their placement. Any slot left unread is diagnosed when
/// the request is dropped instead of disappearing silently.
///
/// `Ok(None)` means the script wrote no such slot. A slot that exists but
/// cannot be built is an error rather than a second spelling of absence:
/// the two call for opposite responses from the adapter, and collapsing
/// them would let a script's element disappear without a diagnostic.
pub fn take_slot(&mut self, name: &str) -> anyhow::Result<Option<AnyElement>> {
if let Some(index) = self.slots.iter().position(|(held, _)| *held == name) {
if let Some(factory) = self
.slot_factory_specs
.iter()
.position(|(held, _)| *held == name)
{
self.slot_factory_specs.remove(factory);
}
return Ok(Some(self.slots.remove(index).1));
}
if !self
.slot_factory_specs
.iter()
.any(|(held, _)| *held == name)
{
return Ok(None);
}
// Checked before the factory is consumed. Taking it first and then
// failing to find the render authority would drop the script's element
// and leave `unread_parts` with nothing left to report.
anyhow::ensure!(
self.window.is_some() && self.cx.is_some(),
"the `{name}` slot is deferred and needs render Window and App authority to build"
);
let Some(factory) = self.take_slot_factory(name) else {
return Ok(None);
};
let (Some(window), Some(cx)) = (self.window.as_deref_mut(), self.cx.as_deref_mut()) else {
anyhow::bail!("the `{name}` slot lost its render authority");
};
factory
.build(window, cx)
.map(Some)
.with_context(|| format!("failed to materialize the `{name}` slot"))
}
/// Takes the next named slot as a repeatable owned factory.
///
/// Factory and eager access are exclusive: taking either representation
/// consumes the matching slot from the other lane as well. Repeated names
/// are returned in script order.
pub fn take_slot_factory(&mut self, name: &str) -> Option<ComponentElementFactory> {
let index = self
.slot_factory_specs
.iter()
.position(|(held, _)| *held == name)?;
if let Some(eager) = self.slots.iter().position(|(held, _)| *held == name) {
self.slots.remove(eager);
}
let (_, spec) = self.slot_factory_specs.remove(index);
Some((self.make_slot_factory)(spec))
}
/// Takes all slots with `name`, preserving script order.
pub fn take_slots(&mut self, name: &str) -> Vec<AnyElement> {
let mut taken = Vec::new();
let mut index = 0;
while index < self.slots.len() {
if self.slots[index].0 == name {
if let Some(factory) = self
.slot_factory_specs
.iter()
.position(|(held, _)| *held == name)
{
self.slot_factory_specs.remove(factory);
}
taken.push(self.slots.remove(index).1);
} else {
index += 1;
}
}
taken
}
/// Takes every repeatable factory named `name`, preserving script order.
///
/// This is the batch counterpart of [`Self::take_slot_factory`]. It is
/// intentionally distinct from [`Self::take_slots`]: eager elements and
/// deferred factories are exclusive lanes, and silently draining one while
/// returning values from the other would lose deferred content.
pub fn take_slot_factories(&mut self, name: &str) -> Vec<ComponentElementFactory> {
let mut taken = Vec::new();
let mut index = 0;
while index < self.slot_factory_specs.len() {
if self.slot_factory_specs[index].0 == name {
if let Some(eager) = self.slots.iter().position(|(held, _)| *held == name) {
self.slots.remove(eager);
}
let (_, spec) = self.slot_factory_specs.remove(index);
taken.push((self.make_slot_factory)(spec));
} else {
index += 1;
}
}
taken
}
pub fn take_style(&mut self) -> StyleRefinement {
self.style.take().unwrap_or_default()
}
pub fn on_click(&self) -> Option<ComponentClickCallback> {
self.on_click.map(|id| ComponentClickCallback {
runtime: Rc::downgrade(self.runtime),
id,
})
}
/// Applies this node's style and ordinary children exactly once.
pub fn finish<E>(mut self, mut element: E) -> anyhow::Result<AnyElement>
where
E: Styled + ParentElement + IntoElement + 'static,
{
element.style().refine(&self.take_style());
if self.child_lane != ChildLane::Typed {
element.extend(self.take_children()?);
}
Ok(element.into_any_element())
}
fn unread_parts(&self) -> (bool, usize, Vec<&'static str>) {
(
self.style.is_some(),
self.children.len() + self.remaining_child_specs() + self.issued_children.len(),
self.slots
.iter()
.map(|(name, _)| *name)
.chain(
self.slot_factory_specs
.iter()
.filter(|(name, _)| !self.slots.iter().any(|(eager, _)| eager == name))
.map(|(name, _)| *name),
)
.collect(),
)
}
pub fn resolve_element(&mut self, argument: &ComponentArgument) -> anyhow::Result<AnyElement> {
let ComponentArgument::Element(element) = argument else {
anyhow::bail!("component argument is not an Element");
};
(self.resolve_element)(*element, self.window.as_deref_mut(), self.cx.as_deref_mut())
}
pub fn resolve_entity(
&self,
argument: &ComponentArgument,
) -> anyhow::Result<ComponentEntityRef> {
let ComponentArgument::Entity { kind, handle } = argument else {
anyhow::bail!("component argument is not an entity");
};
let actual = self
.runtime
.component_entity_kind(*handle)
.ok_or_else(|| anyhow::anyhow!("component entity is no longer live"))?;
anyhow::ensure!(
actual == *kind,
"component entity kind changed from `{kind}` to `{actual}`"
);
Ok(ComponentEntityRef {
runtime: Rc::downgrade(self.runtime),
kind,
handle: *handle,
})
}
pub fn resolve_callback(
&self,
argument: &ComponentArgument,
) -> anyhow::Result<ComponentCallback> {
let ComponentArgument::Callback(callback) = argument else {
anyhow::bail!("component argument is not a callback");
};
Ok(ComponentCallback {
runtime: Rc::downgrade(self.runtime),
id: *callback,
})
}
pub fn resolve_data_callback(
&self,
argument: &ComponentArgument,
) -> anyhow::Result<ComponentDataCallback> {
let callback = self.resolve_callback(argument)?;
Ok(ComponentDataCallback {
callback,
active: Rc::new(Cell::new(false)),
})
}
pub fn resolve_element_callback(
&self,
argument: &ComponentArgument,
) -> anyhow::Result<ComponentElementCallback> {
let callback = self.resolve_callback(argument)?;
Ok(ComponentElementCallback {
callback,
active: Rc::new(Cell::new(false)),
})
}
}
impl Drop for MaterializeRequest<'_> {
fn drop(&mut self) {
let (style, children, slots) = self.unread_parts();
if style {
tracing::warn!(
"{} did not consume its style; call MaterializeRequest::finish or take_style",
self.component_name
);
}
if children != 0 {
tracing::warn!(
"{} did not consume {} child element(s)",
self.component_name,
children
);
}
for name in slots {
tracing::warn!(
"{} has no `{name}` slot, so the element given to it is not rendered at all",
self.component_name
);
}
}
}
#[derive(Clone)]
pub struct ComponentEntityRef {
runtime: Weak<crate::ShellRuntime>,
kind: &'static str,
handle: u64,
}
impl ComponentEntityRef {
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn is_live(&self) -> bool {
self.runtime
.upgrade()
.is_some_and(|runtime| runtime.component_entity_kind(self.handle) == Some(self.kind))
}
}
/// Opaque authority to update one adapter-owned state value from a GPUI event.
pub struct ComponentState<T: Any> {
runtime: Weak<crate::ShellRuntime>,
kind: &'static str,
handle: u64,
state: PhantomData<fn() -> T>,
}
impl<T: Any> Clone for ComponentState<T> {
fn clone(&self) -> Self {
Self {
runtime: self.runtime.clone(),
kind: self.kind,
handle: self.handle,
state: PhantomData,
}
}
}
impl<T: Any> ComponentState<T> {
pub fn update<R>(
&self,
window: &mut Window,
cx: &mut App,
body: impl FnOnce(&mut T, &mut Window, &mut App) -> R,
) -> anyhow::Result<R> {
let runtime = self
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("retained state runtime has been released"))?;
runtime.update_component_state(self.handle, self.kind, window, cx, body)
}
}
#[derive(Clone)]
pub struct ComponentCallback {
runtime: Weak<crate::ShellRuntime>,
id: u64,
}
#[derive(Clone)]
pub struct ComponentDataCallback {
callback: ComponentCallback,
active: Rc<Cell<bool>>,
}
#[derive(Clone)]
pub struct ComponentElementCallback {
callback: ComponentCallback,
active: Rc<Cell<bool>>,
}
impl ComponentDataCallback {
#[cfg(test)]
pub(crate) fn from_runtime(runtime: &Rc<crate::ShellRuntime>, id: u64) -> Self {
Self {
callback: ComponentCallback::from_runtime(runtime, id),
active: Rc::new(Cell::new(false)),
}
}
pub fn snapshot_with(
&self,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<ComponentDataValue> {
anyhow::ensure!(
!self.active.replace(true),
"component data callback is already running"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false)
}
}
let _reset = Reset(&self.active);
let runtime = self
.callback
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime.dispatch_component_data_callback(self.callback.id, arguments, window, cx)
}
pub fn snapshot_rows_with(
&self,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<ComponentDelegateSnapshot> {
match self.snapshot_with(arguments, window, cx)? {
ComponentDataValue::Array(rows) => Ok(ComponentDelegateSnapshot::new(rows)),
_ => anyhow::bail!("component delegate snapshot callback must return an array of rows"),
}
}
}
impl ComponentElementCallback {
pub(crate) fn from_runtime(runtime: &Rc<crate::ShellRuntime>, id: u64) -> Self {
Self {
callback: ComponentCallback::from_runtime(runtime, id),
active: Rc::new(Cell::new(false)),
}
}
pub fn build_with(
&self,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<Option<AnyElement>> {
anyhow::ensure!(
!self.active.replace(true),
"component element callback is already running"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false)
}
}
let _reset = Reset(&self.active);
let runtime = self
.callback
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime.dispatch_component_element_callback(self.callback.id, arguments, window, cx)
}
pub fn build_data_with(
&self,
arguments: &[ComponentDataValue],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<Option<AnyElement>> {
anyhow::ensure!(
!self.active.replace(true),
"component element callback is already running"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false)
}
}
let _reset = Reset(&self.active);
let runtime = self
.callback
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime.dispatch_component_element_data_callback(self.callback.id, arguments, window, cx)
}
/// Build a frame-owned inline subtree whose child callbacks retire with that frame.
pub fn build_interactive_data_with(
&self,
arguments: &[ComponentDataValue],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<Option<AnyElement>> {
anyhow::ensure!(
!self.active.replace(true),
"component element callback is already running"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false)
}
}
let _reset = Reset(&self.active);
let runtime = self
.callback
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime.dispatch_inline_element_data(self.callback.id, arguments, true, window, cx)
}
}
/// A generation-bound capability for native effects initiated by one GPUI event.
#[derive(Clone)]
pub struct ComponentWindowEffects {
reporter: ComponentCallback,
active: Rc<Cell<bool>>,
}
/// A generation-bound capability for replacing application-wide native state
/// after the current render effect cycle.
#[derive(Clone)]
pub struct ComponentAppEffects {
runtime: Weak<crate::ShellRuntime>,
application: Rc<crate::runtime::ApplicationGeneration>,
view: gpui::WeakEntity<crate::ScriptView>,
}
/// Cleanup returned by an application effect. It runs before replacement and
/// when the owning root view is released.
pub type ComponentAppEffectCleanup = Box<dyn FnOnce(&mut App)>;
impl ComponentAppEffects {
pub(crate) fn new(
runtime: &Rc<crate::ShellRuntime>,
application: Rc<crate::runtime::ApplicationGeneration>,
view: gpui::WeakEntity<crate::ScriptView>,
) -> Self {
Self {
runtime: Rc::downgrade(runtime),
application,
view,
}
}
pub fn replace(
&self,
key: impl Into<String>,
revision: impl Into<String>,
window: &mut Window,
cx: &mut App,
install: impl FnOnce(&mut App) -> ComponentAppEffectCleanup + 'static,
) -> anyhow::Result<()> {
let runtime = self
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component app effect runtime has been released"))?;
runtime.schedule_component_app_effect(
self.application.clone(),
self.view.clone(),
key.into(),
revision.into(),
window,
cx,
Box::new(install),
)
}
}
/// The result of a keyed effect attempt within one event.
pub enum ComponentEffectRun<R> {
Executed(R),
Duplicate,
}
impl<R> ComponentEffectRun<R> {
pub fn executed(&self) -> bool {
matches!(self, Self::Executed(_))
}
}
/// A non-retainable event transaction. Keys are idempotent only within this event.
pub struct ComponentEventEffects<'event> {
completed: HashSet<String>,
in_progress: HashSet<String>,
window: &'event mut Window,
cx: &'event mut App,
}
impl ComponentEventEffects<'_> {
pub fn run_once<R>(
&mut self,
key: impl Into<String>,
body: impl FnOnce(&mut Window, &mut App) -> anyhow::Result<R>,
) -> anyhow::Result<ComponentEffectRun<R>> {
let key = key.into();
if self.completed.contains(&key) {
return Ok(ComponentEffectRun::Duplicate);
}
anyhow::ensure!(
self.in_progress.insert(key.clone()),
"component window effect `{key}` is already running"
);
struct Reset<'a> {
keys: &'a mut HashSet<String>,
key: String,
}
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.keys.remove(&self.key);
}
}
let reset = Reset {
keys: &mut self.in_progress,
key: key.clone(),
};
let value = body(self.window, self.cx)?;
drop(reset);
self.completed.insert(key);
Ok(ComponentEffectRun::Executed(value))
}
}
impl ComponentWindowEffects {
#[cfg(test)]
pub(crate) fn is_active(&self) -> bool {
self.active.get()
}
pub fn event<R>(
&self,
window: &mut Window,
cx: &mut App,
body: impl FnOnce(&mut ComponentEventEffects<'_>) -> anyhow::Result<R>,
) -> anyhow::Result<R> {
anyhow::ensure!(
!self.active.replace(true),
"component window effect event is already running"
);
struct Reset<'a>(&'a Cell<bool>);
impl Drop for Reset<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
let _reset = Reset(&self.active);
let runtime =
self.reporter.runtime.upgrade().ok_or_else(|| {
anyhow::anyhow!("component window effect runtime has been released")
})?;
let result =
runtime.with_component_callback_event(self.reporter.id, window, cx, |window, cx| {
body(&mut ComponentEventEffects {
completed: HashSet::new(),
in_progress: HashSet::new(),
window,
cx,
})
});
if let Err(error) = &result
&& crate::scope::current_phase().is_none_or(crate::scope::ScopePhase::allows_notify)
&& let Err(report_error) = self.reporter.invoke_with(
&[ComponentCallbackArgument::String(error.to_string())],
window,
cx,
)
{
tracing::error!("component window effect error reporter failed: {report_error:#}");
}
result
}
}
#[derive(Clone)]
pub struct ComponentClickCallback {
runtime: Weak<crate::ShellRuntime>,
id: crate::spec::CallbackId,
}
impl ComponentClickCallback {
pub fn invoke(&self, event: &ClickEvent, window: &mut Window, cx: &mut App) {
let Some(runtime) = self.runtime.upgrade() else {
tracing::debug!("component click callback runtime has been released");
return;
};
runtime.dispatch_click(self.id, event, window, cx);
}
}
impl ComponentCallback {
pub(crate) fn from_runtime(runtime: &Rc<crate::ShellRuntime>, id: u64) -> Self {
Self {
runtime: Rc::downgrade(runtime),
id,
}
}
/// Creates an event-scoped native-effect service owned by this callback.
///
/// The callback is both the generation lease and the script error reporter:
/// when an effect transaction returns an error it is invoked with the error
/// message followed by the ordinary script `Context`.
/// Adapters should therefore declare it as `(message: string, cx: Context)
/// => void`. Effects never run during render or layout, and a key is
/// idempotent only within one [`ComponentWindowEffects::event`] call so a
/// later user event may intentionally perform the same command again.
pub fn window_effects(&self) -> ComponentWindowEffects {
ComponentWindowEffects {
reporter: self.clone(),
active: Rc::new(Cell::new(false)),
}
}
pub fn invoke(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
self.invoke_with(&[], window, cx)
}
pub fn invoke_with(
&self,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<()> {
self.invoke_value_with(arguments, window, cx).map(|_| ())
}
pub fn invoke_value_with(
&self,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<ComponentCallbackValue> {
let runtime = self
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime.dispatch_component_callback_value(self.id, arguments, window, cx)
}
/// Invokes the script callback and reports any failure through the shell's
/// tracing subscriber.
///
/// GPUI event closures cannot return an error. Adapters should use this
/// entry point instead of discarding the [`Result`] from [`Self::invoke_with`].
pub fn invoke_data_with(
&self,
arguments: &[ComponentDataValue],
window: &mut Window,
cx: &mut App,
) -> anyhow::Result<()> {
let runtime = self
.runtime
.upgrade()
.ok_or_else(|| anyhow::anyhow!("component callback runtime has been released"))?;
runtime
.dispatch_component_event_data(self.id, arguments, window, cx)
.map(|_| ())
}
pub fn invoke_and_report_with(
&self,
context: &str,
arguments: &[ComponentCallbackArgument],
window: &mut Window,
cx: &mut App,
) {
if let Err(error) = self.invoke_with(arguments, window, cx) {
tracing::error!("{context}: {error:#}");
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RecordedComponentMethod {
name: &'static str,
payload: ComponentPayload,
}
impl RecordedComponentMethod {
pub(crate) fn new(name: &'static str, payload: ComponentPayload) -> Self {
Self { name, payload }
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn payload(&self) -> &ComponentPayload {
&self.payload
}
}
pub trait ComponentMaterializer: Send + Sync + 'static {
fn materialize(&self, request: MaterializeRequest<'_>) -> anyhow::Result<AnyElement>;
}
/// One registered component: what a script can construct, what it can call on
/// the result, and who turns the recording into an element.
///
/// Built rather than written as a literal, so that a later field is an added
/// `with_` method instead of a break in every adapter that registers anything.
pub struct ComponentDescriptor {
name: &'static str,
constructors: Vec<ConstructorDescriptor>,
methods: Vec<MethodDescriptor>,
documentation: Option<&'static str>,
materializer: Arc<dyn ComponentMaterializer>,
}
impl ComponentDescriptor {
pub fn new(name: &'static str, materializer: Arc<dyn ComponentMaterializer>) -> Self {
Self {
name,
constructors: Vec::new(),
methods: Vec::new(),
documentation: None,
materializer,
}
}
pub fn with_constructors(mut self, constructors: Vec<ConstructorDescriptor>) -> Self {
self.constructors = constructors;
self
}
pub fn with_methods(mut self, methods: Vec<MethodDescriptor>) -> Self {
self.methods = methods;
self
}
/// The doc comment carried into `gpui-kit.d.ts` for this component's element
/// interface and every one of its constructor exports.
pub fn with_documentation(mut self, documentation: &'static str) -> Self {
self.documentation = Some(documentation);
self
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn constructors(&self) -> &[ConstructorDescriptor] {
&self.constructors
}
pub fn methods(&self) -> &[MethodDescriptor] {
&self.methods
}
pub fn documentation(&self) -> Option<&'static str> {
self.documentation
}
pub(crate) fn materializer(&self) -> &Arc<dyn ComponentMaterializer> {
&self.materializer
}
}
impl fmt::Debug for ComponentDescriptor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ComponentDescriptor")
.field("name", &self.name)
.field("constructors", &self.constructors)
.field("methods", &self.methods)
.field("documentation", &self.documentation)
.finish_non_exhaustive()
}
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RegistryError {
IncompatibleApiVersion {
expected: u32,
actual: u32,
},
DuplicateComponent(&'static str),
InvalidComponent(&'static str),
DuplicateExport(&'static str),
InvalidExport(&'static str),
InvalidMethod {
component: &'static str,
method: &'static str,
},
InvalidArgument {
component: &'static str,
callable: &'static str,
argument: &'static str,
},
DuplicateArgument {
component: &'static str,
callable: &'static str,
argument: &'static str,
},
InvalidArgumentSchema {
component: &'static str,
callable: &'static str,
argument: &'static str,
reason: &'static str,
},
RequiredArgumentAfterOptional {
component: &'static str,
callable: &'static str,
argument: &'static str,
},
DuplicateMethod {
component: &'static str,
method: &'static str,
},
UndocumentedMethod {
component: &'static str,
method: &'static str,
},
UnreachableMethodVocabulary {
component: &'static str,
method: &'static str,
literal: &'static str,
},
InvalidDeprecationReplacement {
component: &'static str,
export: &'static str,
replacement: &'static str,
},
EmptyConstructorList(&'static str),
InvalidStateKind(&'static str),
DuplicateStateKind(&'static str),
InvalidModuleSpecifier(&'static str),
}
/// Module names the runtime answers to itself. A component catalog may not
/// claim one: the runtime's resolvers run first.
const RUNTIME_MODULE_SPECIFIERS: &[&str] =
&["gpui-kit", "gpui", "gpui-base", "gpui-shell", "gpui-fps"];
/// Opens the window a catalog's components need.
///
/// Some component libraries reach the window through a view they require to be
/// its root — `gpui-component`'s overlays find their host with
/// `window.root::<Root>()` and panic when it is something else. The runtime
/// installs its own [`crate::ShellRoot`] and cannot name such a type, so a
/// catalog that needs one opens the window itself: it is handed the options and
/// a builder for the runtime's root view, and returns an untyped handle.
///
/// A catalog that registers none gets the ordinary window, rooted at
/// `ShellRoot`.
pub type ComponentWindowOpener = fn(
&mut App,
gpui::WindowOptions,
&mut dyn FnMut(&mut Window, &mut App) -> gpui::AnyView,
) -> anyhow::Result<gpui::AnyWindowHandle>;
/// Element methods whose argument the runtime's own prototype checks against a
/// fixed vocabulary, before the call can reach a registered component.
///
/// The prototype entry wins over the generic descriptor dispatch, so a
/// descriptor that declares one of these names inherits that check. If the two
/// vocabularies disagree the method becomes unreachable by *any* value — the
/// prototype rejects the descriptor's spelling and the descriptor rejects the
/// prototype's — and nothing says why. `Popover.anchor` was exactly that.
///
/// A name is not forbidden: `Scroll.axis` and `Scrollbar.mode` share a name and
/// a vocabulary and work correctly. Only disagreement is refused.
///
/// The engine asserts this against its own prelude, so a check added there and
/// not here is a failing test rather than a silently dead binding.
const PRELUDE_CHECKED_VOCABULARIES: &[(&str, &[&str])] = &[
("anchor", &crate::materialize::ANCHOR_NAMES),
("axis", &["horizontal", "vertical"]),
("mode", &["scrolling", "hover", "always"]),
("mouse_button", &["left", "right", "middle"]),
];
impl fmt::Display for RegistryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IncompatibleApiVersion { expected, actual } => write!(
formatter,
"component registry API version {actual} is incompatible; expected {expected}"
),
Self::DuplicateComponent(name) => {
write!(formatter, "component `{name}` is already registered")
}
Self::InvalidComponent(name) => write!(
formatter,
"component name `{name}` is not a valid non-reserved identifier"
),
Self::DuplicateExport(name) => {
write!(
formatter,
"JavaScript export `{name}` is already registered"
)
}
Self::InvalidExport(name) => write!(
formatter,
"JavaScript export `{name}` is not a valid non-reserved identifier"
),
Self::InvalidMethod { component, method } => write!(
formatter,
"component `{component}` method `{method}` is not a valid non-reserved identifier"
),
Self::InvalidArgument {
component,
callable,
argument,
} => write!(
formatter,
"component `{component}` callable `{callable}` argument `{argument}` is not a valid non-reserved identifier"
),
Self::DuplicateArgument {
component,
callable,
argument,
} => write!(
formatter,
"component `{component}` callable `{callable}` repeats argument `{argument}`"
),
Self::InvalidArgumentSchema {
component,
callable,
argument,
reason,
} => write!(
formatter,
"component `{component}` callable `{callable}` argument `{argument}` has an invalid schema: {reason}"
),
Self::RequiredArgumentAfterOptional {
component,
callable,
argument,
} => write!(
formatter,
"component `{component}` callable `{callable}` has required argument `{argument}` after an optional argument"
),
Self::UndocumentedMethod { component, method } => write!(
formatter,
"component `{component}` method `{method}` has no documentation; \
call MethodDescriptor::with_documentation"
),
Self::UnreachableMethodVocabulary {
component,
method,
literal,
} => write!(
formatter,
"component `{component}` method `{method}` declares `{literal}`, which the \
runtime's own `{method}` check rejects before the call can reach the \
component; the method would be unreachable by any value, so either match \
that vocabulary or choose another method name"
),
Self::DuplicateMethod { component, method } => {
write!(
formatter,
"component `{component}` registers method `{method}` twice"
)
}
Self::InvalidDeprecationReplacement {
component,
export,
replacement,
} => write!(
formatter,
"component `{component}` deprecated export `{export}` must name another export from the same descriptor, not `{replacement}`"
),
Self::EmptyConstructorList(name) => {
write!(
formatter,
"component `{name}` has no JavaScript constructor"
)
}
Self::InvalidStateKind(kind) => {
write!(
formatter,
"state kind `{kind}` is not a valid non-reserved identifier"
)
}
Self::DuplicateStateKind(kind) => {
write!(formatter, "state kind `{kind}` is already registered")
}
Self::InvalidModuleSpecifier(specifier) => write!(
formatter,
"`{specifier}` is not a usable component module specifier; it must be non-empty \
and must not be one of the runtime's own modules"
),
}
}
}
impl std::error::Error for RegistryError {}
pub struct ComponentRegistry {
module_specifier: &'static str,
initializer: Option<fn(&mut App)>,
window_opener: Option<ComponentWindowOpener>,
descriptors: Vec<Arc<ComponentDescriptor>>,
names: HashSet<&'static str>,
exports: HashSet<&'static str>,
states: Vec<Arc<StateDescriptor>>,
state_kinds: HashSet<&'static str>,
}
impl ComponentRegistry {
/// Opens a registry whose components a script imports from
/// `module_specifier`.
///
/// The name is the adapter's to choose — [`DEFAULT_COMPONENT_MODULE`] is
/// what the shipped one picks — but it may not be a name the runtime's own
/// modules already answer to, because those resolve first and the catalog
/// would be unreachable rather than overriding.
pub fn new(api_version: u32, module_specifier: &'static str) -> Result<Self, RegistryError> {
if api_version != COMPONENT_REGISTRY_API_VERSION {
return Err(RegistryError::IncompatibleApiVersion {
expected: COMPONENT_REGISTRY_API_VERSION,
actual: api_version,
});
}
if module_specifier.trim().is_empty()
|| RUNTIME_MODULE_SPECIFIERS.contains(&module_specifier)
{
return Err(RegistryError::InvalidModuleSpecifier(module_specifier));
}
Ok(Self {
module_specifier,
initializer: None,
window_opener: None,
descriptors: Vec::new(),
names: HashSet::new(),
exports: HashSet::new(),
states: Vec::new(),
state_kinds: HashSet::new(),
})
}
/// Installs the globals this catalog's components need, at startup.
///
/// A catalog usually needs more than descriptors: a theme, action
/// bindings, whatever its library's own `init` sets up. The runtime cannot
/// know what those are, so the catalog carries the function and the
/// runtime calls it — see [`crate::init_with_components`]. Without this a
/// host that only holds a `FrozenComponentRegistry`, such as the shipped
/// CLI, would start the catalog's components against globals that were
/// never installed.
pub fn with_initializer(mut self, initializer: fn(&mut App)) -> Self {
self.initializer = Some(initializer);
self
}
/// Opens the window this catalog's components need. See
/// [`ComponentWindowOpener`].
pub fn with_window_opener(mut self, opener: ComponentWindowOpener) -> Self {
self.window_opener = Some(opener);
self
}
pub fn register(
&mut self,
descriptor: ComponentDescriptor,
) -> Result<ComponentId, RegistryError> {
if self.names.contains(descriptor.name) {
return Err(RegistryError::DuplicateComponent(descriptor.name));
}
if !is_javascript_identifier(descriptor.name) {
return Err(RegistryError::InvalidComponent(descriptor.name));
}
if descriptor.constructors.is_empty() {
return Err(RegistryError::EmptyConstructorList(descriptor.name));
}
let mut methods = HashSet::new();
for method in &descriptor.methods {
if !is_javascript_identifier(method.name) {
return Err(RegistryError::InvalidMethod {
component: descriptor.name,
method: method.name,
});
}
if !methods.insert(method.name) {
return Err(RegistryError::DuplicateMethod {
component: descriptor.name,
method: method.name,
});
}
if let Some((_, accepted)) = PRELUDE_CHECKED_VOCABULARIES
.iter()
.find(|(name, _)| *name == method.name)
&& let Some(literal) = declared_enum_literals(&method.arguments)
.into_iter()
.find(|literal| !accepted.contains(literal))
{
return Err(RegistryError::UnreachableMethodVocabulary {
component: descriptor.name,
method: method.name,
literal,
});
}
// Every method becomes a line of `gpui-kit.d.ts` that a script author
// reads in an editor. Filling a default sentence in here would make
// the published surface look documented without anyone having
// described it, so an omission is refused instead.
if method.documentation.is_none() {
return Err(RegistryError::UndocumentedMethod {
component: descriptor.name,
method: method.name,
});
}
validate_arguments(descriptor.name, method.name, &method.arguments)?;
}
let mut descriptor_exports = HashSet::new();
for constructor in &descriptor.constructors {
if !is_javascript_identifier(constructor.export) {
return Err(RegistryError::InvalidExport(constructor.export));
}
validate_arguments(descriptor.name, constructor.export, &constructor.arguments)?;
if !descriptor_exports.insert(constructor.export)
|| self.exports.contains(constructor.export)
{
return Err(RegistryError::DuplicateExport(constructor.export));
}
}
for constructor in &descriptor.constructors {
if let Some(deprecation) = &constructor.deprecation
&& (deprecation.replacement == constructor.export
|| !descriptor_exports.contains(deprecation.replacement))
{
return Err(RegistryError::InvalidDeprecationReplacement {
component: descriptor.name,
export: constructor.export,
replacement: deprecation.replacement,
});
}
}
let id = ComponentId(self.descriptors.len() as u32);
self.names.insert(descriptor.name);
self.exports
.extend(descriptor.constructors.iter().map(|entry| entry.export));
self.descriptors.push(Arc::new(descriptor));
Ok(id)
}
pub fn register_state(&mut self, descriptor: StateDescriptor) -> Result<(), RegistryError> {
if !is_javascript_identifier(descriptor.export) {
return Err(RegistryError::InvalidExport(descriptor.export));
}
if !is_javascript_identifier(descriptor.kind) {
return Err(RegistryError::InvalidStateKind(descriptor.kind));
}
if self.exports.contains(descriptor.export) {
return Err(RegistryError::DuplicateExport(descriptor.export));
}
if self.state_kinds.contains(descriptor.kind) {
return Err(RegistryError::DuplicateStateKind(descriptor.kind));
}
let mut method_names = HashSet::new();
for method in &descriptor.methods {
// Every state object already answers `release()`.
if !is_javascript_identifier(method.name)
|| method.name == "release"
|| !method_names.insert(method.name)
{
return Err(RegistryError::InvalidMethod {
component: descriptor.kind,
method: method.name,
});
}
}
validate_arguments(descriptor.kind, descriptor.export, &descriptor.arguments)?;
self.exports.insert(descriptor.export);
self.state_kinds.insert(descriptor.kind);
self.states.push(Arc::new(descriptor));
Ok(())
}
/// Publishes the catalog, consuming the builder.
///
/// Taking `self` is what makes "a frozen registry cannot be registered
/// into" a fact about the type rather than a flag checked at run time.
pub fn freeze(self) -> Result<FrozenComponentRegistry, RegistryError> {
Ok(FrozenComponentRegistry {
module_specifier: Some(self.module_specifier),
initializer: self.initializer,
window_opener: self.window_opener,
descriptors: self.descriptors.into(),
states: self.states.into(),
})
}
}
#[cfg(test)]
pub(crate) fn prelude_checked_vocabularies_for_test()
-> &'static [(&'static str, &'static [&'static str])] {
PRELUDE_CHECKED_VOCABULARIES
}
/// The enum literals an argument list declares, at any nesting depth.
fn declared_enum_literals(arguments: &[ArgumentDescriptor]) -> Vec<&'static str> {
fn walk(schema: &ArgumentSchema, out: &mut Vec<&'static str>) {
match schema {
ArgumentSchema::Enum(values) => out.extend_from_slice(values),
ArgumentSchema::Array(item) | ArgumentSchema::Optional(item) => walk(item, out),
_ => {}
}
}
let mut literals = Vec::new();
for argument in arguments {
walk(&argument.schema, &mut literals);
}
literals
}
fn validate_arguments(
component: &'static str,
callable: &'static str,
arguments: &[ArgumentDescriptor],
) -> Result<(), RegistryError> {
let mut saw_optional = false;
let mut names = HashSet::new();
for argument in arguments {
if !is_javascript_identifier(argument.name) {
return Err(RegistryError::InvalidArgument {
component,
callable,
argument: argument.name,
});
}
if !names.insert(argument.name) {
return Err(RegistryError::DuplicateArgument {
component,
callable,
argument: argument.name,
});
}
if let Err(reason) = validate_argument_schema(&argument.schema, true) {
return Err(RegistryError::InvalidArgumentSchema {
component,
callable,
argument: argument.name,
reason,
});
}
if matches!(argument.schema, ArgumentSchema::Optional(_)) {
saw_optional = true;
} else if saw_optional {
return Err(RegistryError::RequiredArgumentAfterOptional {
component,
callable,
argument: argument.name,
});
}
}
Ok(())
}
fn validate_argument_schema(schema: &ArgumentSchema, top_level: bool) -> Result<(), &'static str> {
match schema {
ArgumentSchema::String
| ArgumentSchema::Number
| ArgumentSchema::Boolean
| ArgumentSchema::Element => Ok(()),
ArgumentSchema::Entity(kind) if kind.trim().is_empty() => {
Err("entity kind must not be empty")
}
ArgumentSchema::Entity(_) => Ok(()),
ArgumentSchema::Callback(signature) if signature.trim().is_empty() => {
Err("callback signature must not be empty")
}
ArgumentSchema::Callback(_) => Ok(()),
ArgumentSchema::Enum([]) => Err("enum must contain at least one literal"),
ArgumentSchema::Enum(values) if values.iter().any(|value| value.is_empty()) => {
Err("enum literals must not be empty")
}
ArgumentSchema::Enum(values) => {
let mut unique = HashSet::with_capacity(values.len());
if values.iter().all(|value| unique.insert(*value)) {
Ok(())
} else {
Err("enum literals must be unique")
}
}
ArgumentSchema::Array(item) => validate_argument_schema(item, false),
ArgumentSchema::Optional(_) if !top_level => {
Err("optional schemas are only valid for top-level arguments")
}
ArgumentSchema::Optional(item) => validate_argument_schema(item, false),
}
}
fn is_javascript_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first == '$' || first.is_ascii_alphabetic())
|| !chars.all(|character| {
character == '_' || character == '$' || character.is_ascii_alphanumeric()
})
{
return false;
}
!matches!(
name,
"arguments"
| "await"
| "break"
| "case"
| "catch"
| "class"
| "const"
| "continue"
| "debugger"
| "default"
| "delete"
| "do"
| "else"
| "enum"
| "eval"
| "export"
| "extends"
| "false"
| "finally"
| "for"
| "function"
| "if"
| "implements"
| "import"
| "in"
| "instanceof"
| "interface"
| "let"
| "new"
| "null"
| "package"
| "private"
| "protected"
| "public"
| "return"
| "static"
| "super"
| "switch"
| "this"
| "throw"
| "true"
| "try"
| "typeof"
| "var"
| "void"
| "while"
| "with"
| "yield"
)
}
#[derive(Clone, Default)]
pub struct FrozenComponentRegistry {
/// `None` for the empty catalog the bare runtime ships, which declares no
/// module at all rather than an empty one.
module_specifier: Option<&'static str>,
initializer: Option<fn(&mut App)>,
window_opener: Option<ComponentWindowOpener>,
descriptors: Arc<[Arc<ComponentDescriptor>]>,
states: Arc<[Arc<StateDescriptor>]>,
}
impl FrozenComponentRegistry {
/// The specifier a script imports this catalog's components from.
pub fn module_specifier(&self) -> Option<&'static str> {
self.module_specifier
}
/// The startup function this catalog registered, if any.
pub fn initializer(&self) -> Option<fn(&mut App)> {
self.initializer
}
/// The window opener this catalog registered, if any.
pub fn window_opener(&self) -> Option<ComponentWindowOpener> {
self.window_opener
}
pub fn descriptors(&self) -> impl ExactSizeIterator<Item = &ComponentDescriptor> {
self.descriptors.iter().map(Arc::as_ref)
}
pub fn descriptor(&self, id: ComponentId) -> Option<&ComponentDescriptor> {
self.descriptors.get(id.0 as usize).map(Arc::as_ref)
}
pub fn states(&self) -> impl ExactSizeIterator<Item = &StateDescriptor> {
self.states.iter().map(Arc::as_ref)
}
pub(crate) fn registered(
&self,
) -> impl ExactSizeIterator<Item = (ComponentId, &ComponentDescriptor)> {
self.descriptors
.iter()
.enumerate()
.map(|(index, descriptor)| (ComponentId(index as u32), descriptor.as_ref()))
}
pub(crate) fn javascript_module_source(&self, state_proof: &str) -> String {
let mut source = format!(
"const __stateHandles = new WeakMap();\nconst __stateProof = {state_proof:?};\n\
function __unwrapState(value) {{\n\
if (__stateHandles.has(value)) return {{ __componentStateHandle: __stateHandles.get(value), __componentStateProof: __stateProof }};\n\
if (Array.isArray(value)) return value.map(__unwrapState);\n\
return value;\n\
}}\n"
);
for state in self.states() {
source.push_str("function ");
source.push_str(state.export);
source.push_str("(...args) { const handle = globalThis.__gpui_components[");
source.push_str(&format!("{:?}", state.export));
source.push_str("](args); const value = Object.freeze({");
for method in state.methods() {
source.push_str(&format!("{:?}: (...args) => globalThis.__gpui_components[{:?}](__stateProof, handle, args),", method.name(), format!("{}.{}", state.export(), method.name())));
}
if state.has_input_events() {
source.push_str(&format!("on: (event, handler) => globalThis.__gpui_components[{:?}](__stateProof, handle, event, handler),", format!("{}.on", state.export())));
}
source.push_str(&format!(
"release: () => globalThis.__gpui_components[{:?}](__stateProof, handle),",
format!("{}.release", state.export())
));
source.push_str("}); __stateHandles.set(value, handle); return value; }\n");
push_new_alias(&mut source, state.export);
}
for descriptor in self.descriptors() {
for constructor in &descriptor.constructors {
source.push_str("function ");
source.push_str(constructor.export);
source.push_str(
"(...args) { return globalThis.__gpui.__element(globalThis.__gpui_components[",
);
source.push_str(&format!("{:?}", constructor.export));
source.push_str("](args.map(__unwrapState))); }\n");
push_new_alias(&mut source, constructor.export);
}
}
source
}
}
/// Exports `name` callable as `name(...)`, `new name(...)` and `name.new(...)`.
///
/// `gpui-base` constructs with `Type.new(...)`, as in Rust, and a script that
/// imports from both modules should not have to remember which one it is
/// calling. The plain call and `new` keep working for scripts written before.
fn push_new_alias(source: &mut String, name: &str) {
source.push_str(&format!("{name}.new = {name};\nexport {{ {name} }};\n"));
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::div;
fn test_slot_factory(_: u32) -> ComponentElementFactory {
ComponentElementFactory::new(|_, _| Ok(div().into_any_element()))
}
#[gpui::test]
fn component_callback_reports_a_released_runtime(cx: &mut gpui::TestAppContext) {
use gpui::{Empty, VisualTestContext};
use std::ops::Deref;
let callback = ComponentCallback {
runtime: Weak::new(),
id: 0,
};
let window = cx.add_window(|_, _| Empty);
let mut context = VisualTestContext::from_window(*window.deref(), cx);
let error = context
.update(|window, cx| callback.invoke_value_with(&[], window, cx))
.unwrap_err();
assert!(error.to_string().contains("runtime has been released"));
}
#[test]
fn component_payloads_cross_the_adapter_boundary_as_send_sync_values() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ComponentPayload>();
}
struct EmptyMaterializer;
impl ComponentMaterializer for EmptyMaterializer {
fn materialize(&self, _request: MaterializeRequest<'_>) -> anyhow::Result<AnyElement> {
Ok(div().into_any_element())
}
}
#[test]
fn materialize_request_keeps_named_slots_separate_until_the_adapter_takes_them() {
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let mut resolve_element = |_, _: Option<&mut Window>, _: Option<&mut App>| {
anyhow::bail!("no element argument expected")
};
let mut request = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Slotted",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut resolve_element,
style: StyleRefinement::default(),
children: smallvec::smallvec![div().into_any_element()],
child_specs: SmallVec::new(),
slots: smallvec::smallvec![("trigger", div().into_any_element())],
slot_factory_specs: smallvec::smallvec![("trigger", 0)],
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let error = request
.with_window_app(|_, _| Ok::<(), anyhow::Error>(()))
.expect_err("a request outside render has no Window/App authority");
assert_eq!(error.to_string(), "render Window authority is unavailable");
assert_eq!(request.unread_parts(), (true, 1, vec!["trigger"]));
assert!(request.take_slot("content").unwrap().is_none());
assert!(request.take_slot("trigger").unwrap().is_some());
assert!(request.take_slot_factory("trigger").is_none());
assert_eq!(request.take_children().unwrap().len(), 1);
let _ = request.take_style();
assert_eq!(request.unread_parts(), (false, 0, Vec::new()));
}
#[test]
fn materialize_request_takes_every_repeated_named_slot_in_order() {
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let mut resolve_element = |_, _: Option<&mut Window>, _: Option<&mut App>| {
anyhow::bail!("no element argument expected")
};
let mut request = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Slotted",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut resolve_element,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: SmallVec::new(),
slots: smallvec::smallvec![
("content", div().into_any_element()),
("content", div().into_any_element()),
("trigger", div().into_any_element()),
],
slot_factory_specs: smallvec::smallvec![("content", 0), ("content", 1), ("content", 2)],
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
assert_eq!(request.take_slots("content").len(), 2);
assert_eq!(request.take_slot_factories("content").len(), 1);
assert!(request.take_slot("content").unwrap().is_none());
assert!(request.take_slot("trigger").unwrap().is_some());
let _ = request.take_style();
assert_eq!(request.unread_parts(), (false, 0, Vec::new()));
}
#[test]
fn typed_child_failure_keeps_the_token_retryable_and_unread() {
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let attempts = std::cell::Cell::new(0);
let mut resolve_element = |_, _: Option<&mut Window>, _: Option<&mut App>| {
attempts.set(attempts.get() + 1);
if attempts.get() == 1 {
anyhow::bail!("candidate failed")
}
Ok(div().into_any_element())
};
let mut request = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Parent",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut resolve_element,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(7, Some("RegisteredChild"))],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut child = request.take_typed_children().unwrap().pop().unwrap();
assert_eq!(child.component_name(), Some("RegisteredChild"));
assert!(request.materialize_child(&mut child).is_err());
assert_eq!(request.unread_parts().1, 1);
assert!(request.materialize_child(&mut child).is_ok());
assert_eq!(request.unread_parts().1, 0);
assert!(request.materialize_child(&mut child).is_err());
}
#[test]
fn repeated_child_spec_ids_receive_independent_exactly_once_tokens() {
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let resolutions = std::cell::Cell::new(0);
let mut resolve_element = |_, _: Option<&mut Window>, _: Option<&mut App>| {
resolutions.set(resolutions.get() + 1);
Ok(div().into_any_element())
};
let mut request = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "RepeatedParent",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut resolve_element,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![
(7, Some("RepeatedChild")),
(7, Some("RepeatedChild"))
],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut children = request.take_typed_children().unwrap();
let mut first = children.remove(0);
let mut second = children.remove(0);
assert!(request.materialize_child(&mut first).is_ok());
assert!(request.materialize_child(&mut first).is_err());
assert!(request.materialize_child(&mut second).is_ok());
assert_eq!(resolutions.get(), 2);
assert_eq!(request.unread_parts().1, 0);
}
#[test]
fn child_lane_stays_exclusive_after_either_lane_is_drained() {
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let mut ordinary_resolver =
|_, _: Option<&mut Window>, _: Option<&mut App>| Ok(div().into_any_element());
let mut ordinary = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Ordinary",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut ordinary_resolver,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(1, None)],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
assert_eq!(ordinary.take_children().unwrap().len(), 1);
assert!(
ordinary.take_typed_children().is_err(),
"the typed lane must report the conflict, not answer with no children"
);
assert_eq!(ordinary.child_lane, ChildLane::Ordinary);
let mut typed_resolver =
|_, _: Option<&mut Window>, _: Option<&mut App>| Ok(div().into_any_element());
let mut typed = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Typed",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut typed_resolver,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(2, None)],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut child = typed.take_typed_children().unwrap().pop().unwrap();
assert_eq!(child.component_name(), None, "built-ins stay opaque");
assert!(typed.materialize_child(&mut child).is_ok());
assert!(
typed.take_children().is_err(),
"the ordinary lane must report the conflict too"
);
assert_eq!(typed.child_lane, ChildLane::Typed);
}
#[test]
fn ordinary_child_failure_propagates_through_the_materializer_boundary() {
struct FinishingMaterializer;
impl ComponentMaterializer for FinishingMaterializer {
fn materialize(&self, request: MaterializeRequest<'_>) -> anyhow::Result<AnyElement> {
request.finish(div())
}
}
let runtime = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let mut resolve_element =
|_, _: Option<&mut Window>, _: Option<&mut App>| anyhow::bail!("child adapter failed");
let request = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "Parent",
payload: &payload,
operations: &operations,
runtime: &runtime,
resolve_element: &mut resolve_element,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(9, Some("FailingChild"))],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let error = FinishingMaterializer
.materialize(request)
.err()
.expect("a failed child must fail its parent materializer");
assert_eq!(error.to_string(), "child adapter failed");
}
#[test]
fn typed_child_rejects_a_different_runtime_and_request() {
let runtime_a = crate::ShellRuntime::new_isolated().unwrap();
let runtime_b = crate::ShellRuntime::new_isolated().unwrap();
let payload = ComponentPayload::new(());
let operations = [];
let mut resolver_a =
|_, _: Option<&mut Window>, _: Option<&mut App>| Ok(div().into_any_element());
let mut resolver_b =
|_, _: Option<&mut Window>, _: Option<&mut App>| Ok(div().into_any_element());
let mut resolver_c =
|_, _: Option<&mut Window>, _: Option<&mut App>| Ok(div().into_any_element());
let mut request_a = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "A",
payload: &payload,
operations: &operations,
runtime: &runtime_a,
resolve_element: &mut resolver_a,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(3, Some("Child"))],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut request_b = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "B",
payload: &payload,
operations: &operations,
runtime: &runtime_b,
resolve_element: &mut resolver_b,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(3, Some("Child"))],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut request_c = MaterializeRequest::new(MaterializeRequestInit {
element_id: gpui::ElementId::Name("test".into()),
component_name: "C",
payload: &payload,
operations: &operations,
runtime: &runtime_a,
resolve_element: &mut resolver_c,
style: StyleRefinement::default(),
children: SmallVec::new(),
child_specs: smallvec::smallvec![(3, Some("Child"))],
slots: SmallVec::new(),
slot_factory_specs: SmallVec::new(),
make_slot_factory: &test_slot_factory,
disabled: false,
selected: false,
on_click: None,
application_owner: None,
});
let mut child = request_a.take_typed_children().unwrap().pop().unwrap();
let error = request_b
.materialize_child(&mut child)
.err()
.expect("foreign child must fail");
assert!(
error
.to_string()
.contains("different runtime or materialization request")
);
let error = request_c
.materialize_child(&mut child)
.err()
.expect("foreign request in the same runtime must fail");
assert!(
error
.to_string()
.contains("different runtime or materialization request")
);
assert_eq!(request_a.unread_parts().1, 1);
}
fn empty_descriptor(
name: &'static str,
constructors: Vec<ConstructorDescriptor>,
) -> ComponentDescriptor {
ComponentDescriptor::new(name, Arc::new(EmptyMaterializer))
.with_constructors(constructors)
.with_methods(Vec::new())
}
fn nullary(export: &'static str) -> ConstructorDescriptor {
ConstructorDescriptor::new(export, Vec::new(), |_| Ok(ComponentPayload::new(())))
}
fn state(export: &'static str, kind: &'static str) -> StateDescriptor {
StateDescriptor::new(export, kind, Vec::new(), |_, _, _| Ok(Box::new(0usize)))
}
#[test]
fn state_exports_share_the_component_export_namespace() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
registry
.register_state(state("InputState", "InputState"))
.unwrap();
assert_eq!(
registry
.register(empty_descriptor("Input", vec![nullary("InputState")]))
.unwrap_err(),
RegistryError::DuplicateExport("InputState")
);
}
#[test]
fn state_descriptors_validate_export_kind_and_arguments() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
assert_eq!(
registry
.register_state(state("class", "State"))
.unwrap_err(),
RegistryError::InvalidExport("class")
);
assert_eq!(
registry
.register_state(state("State", "bad-kind"))
.unwrap_err(),
RegistryError::InvalidStateKind("bad-kind")
);
assert_eq!(
registry
.register_state(StateDescriptor::new(
"State",
"State",
vec![ArgumentDescriptor::new("class", ArgumentSchema::String)],
|_, _, _| Ok(Box::new(())),
))
.unwrap_err(),
RegistryError::InvalidArgument {
component: "State",
callable: "State",
argument: "class",
}
);
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
registry
.register_state(state("FirstState", "SharedState"))
.unwrap();
assert_eq!(
registry
.register_state(state("SecondState", "SharedState"))
.unwrap_err(),
RegistryError::DuplicateStateKind("SharedState")
);
}
#[test]
fn frozen_registry_module_declares_state_exports() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
registry
.register_state(state("InputState", "InputState"))
.unwrap();
let source = registry
.freeze()
.unwrap()
.javascript_module_source("test-proof");
assert!(source.contains("export { InputState }"), "{source}");
}
#[test]
fn retained_state_store_enforces_its_limit_without_overwriting_live_state() {
let mut store = RetainedStateStore::default();
for value in 0..MAX_RETAINED_COMPONENT_STATES {
store.insert("State", None, Box::new(value)).unwrap();
}
let error = store.insert("State", None, Box::new(())).unwrap_err();
assert!(error.contains("limit"), "{error}");
assert_eq!(store.kind(0), Some("State"));
}
#[test]
fn releasing_an_application_recovers_retained_state_capacity() {
let owner = crate::runtime::ApplicationGeneration::new(91);
let mut store = RetainedStateStore::default();
for value in 0..MAX_RETAINED_COMPONENT_STATES {
store
.insert("State", Some(owner.clone()), Box::new(value))
.unwrap();
}
store.release_application(&owner);
assert_eq!(store.len(), 0);
store.insert("State", None, Box::new(())).unwrap();
}
#[test]
fn duplicate_methods_are_rejected_with_the_component_name() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(
ComponentDescriptor::new("Button", Arc::new(EmptyMaterializer))
.with_constructors(vec![nullary("Button")])
.with_methods(vec![
MethodDescriptor::new("disabled", Vec::new(), |_| {
Ok(ComponentPayload::new(()))
})
.with_documentation("Disables the button."),
MethodDescriptor::new("disabled", Vec::new(), |_| {
Ok(ComponentPayload::new(()))
})
.with_documentation("Disables the button, again."),
]),
)
.unwrap_err();
assert_eq!(
error,
RegistryError::DuplicateMethod {
component: "Button",
method: "disabled",
}
);
}
#[test]
fn duplicate_exports_inside_one_descriptor_are_rejected() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![nullary("Button"), nullary("Button")],
))
.unwrap_err();
assert_eq!(error, RegistryError::DuplicateExport("Button"));
}
#[test]
fn invalid_and_reserved_javascript_exports_are_rejected() {
for export in ["not-valid", "class"] {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor("Button", vec![nullary(export)]))
.unwrap_err();
assert_eq!(error, RegistryError::InvalidExport(export));
}
}
#[test]
fn method_and_argument_names_must_be_javascript_identifiers() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor("not-valid", vec![nullary("Button")]))
.unwrap_err();
assert_eq!(error, RegistryError::InvalidComponent("not-valid"));
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(
ComponentDescriptor::new("Button", Arc::new(EmptyMaterializer))
.with_constructors(vec![nullary("Button")])
.with_methods(vec![MethodDescriptor::new("not-valid", Vec::new(), |_| {
Ok(ComponentPayload::new(()))
})]),
)
.unwrap_err();
assert_eq!(
error,
RegistryError::InvalidMethod {
component: "Button",
method: "not-valid",
}
);
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![ConstructorDescriptor::new(
"Button",
vec![ArgumentDescriptor::new("class", ArgumentSchema::String)],
|_| Ok(ComponentPayload::new(())),
)],
))
.unwrap_err();
assert_eq!(
error,
RegistryError::InvalidArgument {
component: "Button",
callable: "Button",
argument: "class",
}
);
}
#[test]
fn required_arguments_cannot_follow_optional_arguments() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![ConstructorDescriptor::new(
"Button",
vec![
ArgumentDescriptor::new(
"label",
ArgumentSchema::Optional(Box::new(ArgumentSchema::String)),
),
ArgumentDescriptor::new("id", ArgumentSchema::String),
],
|_| Ok(ComponentPayload::new(())),
)],
))
.unwrap_err();
assert_eq!(
error,
RegistryError::RequiredArgumentAfterOptional {
component: "Button",
callable: "Button",
argument: "id",
}
);
}
#[test]
fn argument_schemas_are_validated_recursively() {
for (schema, reason) in [
(
ArgumentSchema::Array(Box::new(ArgumentSchema::Enum(&[]))),
"enum must contain at least one literal",
),
(
ArgumentSchema::Optional(Box::new(ArgumentSchema::Array(Box::new(
ArgumentSchema::Enum(&["quiet", "quiet"]),
)))),
"enum literals must be unique",
),
(
ArgumentSchema::Array(Box::new(ArgumentSchema::Enum(&[""]))),
"enum literals must not be empty",
),
(
ArgumentSchema::Array(Box::new(ArgumentSchema::Optional(Box::new(
ArgumentSchema::String,
)))),
"optional schemas are only valid for top-level arguments",
),
(ArgumentSchema::Entity(""), "entity kind must not be empty"),
(
ArgumentSchema::Callback(" "),
"callback signature must not be empty",
),
] {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![ConstructorDescriptor::new(
"Button",
vec![ArgumentDescriptor::new("value", schema)],
|_| Ok(ComponentPayload::new(())),
)],
))
.unwrap_err();
assert_eq!(
error,
RegistryError::InvalidArgumentSchema {
component: "Button",
callable: "Button",
argument: "value",
reason,
}
);
}
}
#[test]
fn one_signature_cannot_repeat_an_argument_name() {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![ConstructorDescriptor::new(
"Button",
vec![
ArgumentDescriptor::new("value", ArgumentSchema::String),
ArgumentDescriptor::new("value", ArgumentSchema::Boolean),
],
|_| Ok(ComponentPayload::new(())),
)],
))
.unwrap_err();
assert_eq!(
error,
RegistryError::DuplicateArgument {
component: "Button",
callable: "Button",
argument: "value",
}
);
}
#[test]
fn deprecated_exports_must_name_another_export_from_the_same_descriptor() {
for replacement in ["MissingButton", "OldButton"] {
let mut registry =
ComponentRegistry::new(COMPONENT_REGISTRY_API_VERSION, DEFAULT_COMPONENT_MODULE)
.unwrap();
let error = registry
.register(empty_descriptor(
"Button",
vec![
nullary("Button"),
nullary("OldButton").with_deprecation(replacement, "Use Button."),
],
))
.unwrap_err();
assert_eq!(
error,
RegistryError::InvalidDeprecationReplacement {
component: "Button",
export: "OldButton",
replacement,
}
);
}
}
}