Skip to main content

component_shape_gpui/
lib.rs

1//! GPUI-specific component shape runtime contracts.
2//!
3//! This crate is also the GPUI facade for shared component-shape metadata such
4//! as `ComponentShapeMetadata`, `ValueChange`, and `McpInput`.
5//!
6//! Declare owned components with the [`GpuiComponentShape`] derive and external
7//! component/state pairs with [`component_shape!`]. Consumer-side configured
8//! values implement [`GpuiComponentShapeBuilder`] and share the
9//! [`build_component_shape`] construction path with
10//! [`DefaultGpuiComponentShapeBuilder`].
11
12pub use component_shape::{
13    ComponentCapabilities, ComponentPrototyping, ComponentShapeFor, ComponentShapeMetadata,
14    ComponentSuffix, DeclaredComponentShape, McpInput, McpInputShape, McpPrimitiveKind,
15    McpRangeBoundKind, RenderCapability, ValueBindingCapability, ValueChange,
16    component_suffix_from_suffix, is_valid_component_suffix, validate_component_suffix,
17};
18pub use component_shape_gpui_macros::{GpuiComponentShape, component_shape};
19
20/// Renders a component UI value from a component state entity.
21pub trait GpuiComponentRender<State: 'static>: 'static {
22    /// Whether this contract renders a real component.
23    const RENDERS: bool;
24
25    /// Build a render component from the generated form field entity.
26    fn new(entity: &gpui_kit::Entity<State>) -> impl gpui_kit::IntoElement;
27}
28
29/// Marker render contract for shapes that do not publish render metadata.
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
31pub struct NoGpuiRenderComponent;
32
33impl<State: 'static> GpuiComponentRender<State> for NoGpuiRenderComponent {
34    const RENDERS: bool = false;
35
36    fn new(_entity: &gpui_kit::Entity<State>) -> impl gpui_kit::IntoElement {
37        gpui_kit::div()
38    }
39}
40
41/// Shape contract for GPUI components.
42pub trait GpuiComponentShape: ComponentShapeMetadata {
43    /// Backing GPUI component state type.
44    type State: 'static;
45
46    /// Shape-owned render component contract for prototyping output.
47    type RenderComponent: GpuiComponentRender<Self::State>;
48
49    /// Build the component state.
50    fn new(
51        window: &mut gpui_kit::Window,
52        cx: &mut gpui_kit::Context<'_, Self::State>,
53    ) -> Self::State;
54}
55
56/// Configured builder for a GPUI component shape.
57///
58/// Generated code can use this contract when a field selects a component shape
59/// with a configuration expression, such as `Select::<_>.searchable(true)`.
60/// The configured value decides how to initialize the same shape state that the
61/// plain [`GpuiComponentShape::new`] path would otherwise construct.
62pub trait GpuiComponentShapeBuilder<Shape: GpuiComponentShape> {
63    /// Build the configured component state.
64    fn build(
65        self,
66        window: &mut gpui_kit::Window,
67        cx: &mut gpui_kit::Context<'_, Shape::State>,
68    ) -> Shape::State;
69}
70
71/// Default builder for a shape's normal [`GpuiComponentShape::new`] behavior.
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub struct DefaultGpuiComponentShapeBuilder<Shape>(core::marker::PhantomData<fn() -> Shape>);
74
75impl<Shape> DefaultGpuiComponentShapeBuilder<Shape> {
76    /// Creates a builder that delegates to [`GpuiComponentShape::new`].
77    pub const fn new() -> Self {
78        Self(core::marker::PhantomData)
79    }
80}
81
82impl<Shape> GpuiComponentShapeBuilder<Shape> for DefaultGpuiComponentShapeBuilder<Shape>
83where
84    Shape: GpuiComponentShape,
85{
86    fn build(
87        self,
88        window: &mut gpui_kit::Window,
89        cx: &mut gpui_kit::Context<'_, Shape::State>,
90    ) -> Shape::State {
91        Shape::new(window, cx)
92    }
93}
94
95/// Marker for component shapes declared through component-shape GPUI macros.
96#[diagnostic::on_unimplemented(
97    message = "GPUI component shape `{Self}` must be declared with `component_shape_gpui::component_shape!` or `#[derive(component_shape_gpui::GpuiComponentShape)]`",
98    note = "hand-written `GpuiComponentShape` implementations are not accepted by consumers that require declared shapes"
99)]
100pub trait DeclaredGpuiComponentShape: GpuiComponentShape + DeclaredComponentShape {}
101
102/// Marker that a GPUI component shape supports a value type.
103///
104/// This also requires the framework-neutral [`ComponentShapeFor<Value>`]
105/// marker so GPUI value compatibility always carries value-specific shape
106/// metadata for downstream generators and MCP integrations.
107#[diagnostic::on_unimplemented(
108    message = "GPUI component shape `{Self}` is not compatible with value `{Value}`",
109    note = "declare `value = {Value}`, include `{Value}` in `values(...)`, or publish value compatibility through a value-binding impl"
110)]
111pub trait GpuiComponentShapeFor<Value>: GpuiComponentShape + ComponentShapeFor<Value> {}
112
113/// Optional value-binding contract for GPUI component shapes.
114#[diagnostic::on_unimplemented(
115    message = "GPUI component shape `{Self}` does not implement value binding for `{Value}`",
116    note = "add a `GpuiComponentValueBinding<T>` impl inside `component_shape!`, or derive with `value_binding` and a matching state binding"
117)]
118pub trait GpuiComponentValueBinding<Value>: GpuiComponentShape
119where
120    Self::State: gpui_kit::EventEmitter<Self::Event>,
121{
122    /// Event emitted by the component state.
123    type Event: 'static;
124
125    /// Seed component state from the current value.
126    fn seed_value_binding_state(
127        _state: &mut Self::State,
128        _value: Option<&Value>,
129        _window: &mut gpui_kit::Window,
130        _cx: &mut gpui_kit::Context<'_, Self::State>,
131    ) {
132    }
133
134    /// Convert an emitted component event into a normalized value change.
135    fn value_change(state: &Self::State, event: &Self::Event) -> ValueChange<Value>;
136}
137
138/// Value-binding contract implemented by backing component state.
139#[diagnostic::on_unimplemented(
140    message = "GPUI component state `{Self}` does not implement value binding for `{Value}`",
141    note = "implement `GpuiComponentStateValueBinding<T>` for the backing state"
142)]
143pub trait GpuiComponentStateValueBinding<Value>: gpui_kit::EventEmitter<Self::Event> {
144    /// Event emitted by the backing component state.
145    type Event: 'static;
146
147    /// Seed component state from the current value.
148    fn seed_value_binding_state(
149        _state: &mut Self,
150        _value: Option<&Value>,
151        _window: &mut gpui_kit::Window,
152        _cx: &mut gpui_kit::Context<'_, Self>,
153    ) where
154        Self: Sized,
155    {
156    }
157
158    /// Convert an emitted component event into a normalized value change.
159    fn value_change(state: &Self, event: &Self::Event) -> ValueChange<Value>;
160}
161
162/// State type for a GPUI component shape.
163pub type GpuiComponentStateOf<Shape> = <Shape as GpuiComponentShape>::State;
164
165/// Event type for a value-bound GPUI component shape and value.
166pub type GpuiComponentEventOf<Shape, Value> = <Shape as GpuiComponentValueBinding<Value>>::Event;
167
168/// Build component state from a configured shape builder.
169pub fn build_component_shape<Shape, Builder>(
170    builder: Builder,
171    window: &mut gpui_kit::Window,
172    cx: &mut gpui_kit::Context<'_, GpuiComponentStateOf<Shape>>,
173) -> GpuiComponentStateOf<Shape>
174where
175    Shape: GpuiComponentShape,
176    Builder: GpuiComponentShapeBuilder<Shape>,
177{
178    builder.build(window, cx)
179}
180
181/// Seed component state from the current value without spelling out the
182/// associated-type projection at every generated call site.
183pub fn seed_value_binding_state<Shape, Value>(
184    state: &mut GpuiComponentStateOf<Shape>,
185    value: Option<&Value>,
186    window: &mut gpui_kit::Window,
187    cx: &mut gpui_kit::Context<'_, GpuiComponentStateOf<Shape>>,
188) where
189    Shape: GpuiComponentValueBinding<Value>,
190    GpuiComponentStateOf<Shape>: gpui_kit::EventEmitter<GpuiComponentEventOf<Shape, Value>>,
191{
192    Shape::seed_value_binding_state(state, value, window, cx);
193}
194
195/// Convert a component event into a value change without repeating UFCS
196/// projections in generated code.
197pub fn value_change<Shape, Value>(
198    state: &GpuiComponentStateOf<Shape>,
199    event: &GpuiComponentEventOf<Shape, Value>,
200) -> ValueChange<Value>
201where
202    Shape: GpuiComponentValueBinding<Value>,
203    GpuiComponentStateOf<Shape>: gpui_kit::EventEmitter<GpuiComponentEventOf<Shape, Value>>,
204{
205    Shape::value_change(state, event)
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{
211        ComponentShapeMetadata, DefaultGpuiComponentShapeBuilder, GpuiComponentRender,
212        GpuiComponentShape, GpuiComponentShapeBuilder, GpuiComponentStateValueBinding,
213        GpuiComponentValueBinding, NoGpuiRenderComponent, ValueChange, build_component_shape,
214        seed_value_binding_state, value_change,
215    };
216
217    #[derive(Debug, Default, Eq, PartialEq)]
218    struct TestState {
219        value: Option<u32>,
220    }
221
222    impl gpui_kit::Render for TestState {
223        fn render(
224            &mut self,
225            _window: &mut gpui_kit::Window,
226            _cx: &mut gpui_kit::Context<'_, Self>,
227        ) -> impl gpui_kit::IntoElement {
228            gpui_kit::div()
229        }
230    }
231
232    struct TestEvent(Option<u32>);
233
234    impl gpui_kit::EventEmitter<TestEvent> for TestState {}
235
236    struct TestShape;
237
238    impl ComponentShapeMetadata for TestShape {}
239
240    impl GpuiComponentShape for TestShape {
241        type State = TestState;
242        type RenderComponent = NoGpuiRenderComponent;
243
244        fn new(
245            _window: &mut gpui_kit::Window,
246            _cx: &mut gpui_kit::Context<'_, Self::State>,
247        ) -> Self::State {
248            TestState { value: Some(1) }
249        }
250    }
251
252    impl GpuiComponentValueBinding<u32> for TestShape {
253        type Event = TestEvent;
254
255        fn value_change(_state: &Self::State, event: &Self::Event) -> ValueChange<u32> {
256            match event.0 {
257                Some(value) => ValueChange::Set(value),
258                None => ValueChange::Clear,
259            }
260        }
261    }
262
263    impl GpuiComponentStateValueBinding<u32> for TestState {
264        type Event = TestEvent;
265
266        fn value_change(_state: &Self, event: &Self::Event) -> ValueChange<u32> {
267            match event.0 {
268                Some(value) => ValueChange::Set(value),
269                None => ValueChange::Clear,
270            }
271        }
272    }
273
274    struct ConfiguredBuilder(u32);
275
276    impl GpuiComponentShapeBuilder<TestShape> for ConfiguredBuilder {
277        fn build(
278            self,
279            _window: &mut gpui_kit::Window,
280            _cx: &mut gpui_kit::Context<'_, TestState>,
281        ) -> TestState {
282            TestState {
283                value: Some(self.0),
284            }
285        }
286    }
287
288    #[test]
289    fn marker_and_default_builder_metadata_are_stable() {
290        assert_eq!(
291            DefaultGpuiComponentShapeBuilder::<()>::new(),
292            DefaultGpuiComponentShapeBuilder::default()
293        );
294        const {
295            assert!(!<NoGpuiRenderComponent as GpuiComponentRender<()>>::RENDERS);
296        }
297    }
298
299    #[test]
300    fn runtime_helpers_dispatch_through_shape_contracts() {
301        let mut app = gpui_kit::TestApp::new();
302        let mut window = app.open_window(|window, cx| {
303            build_component_shape::<TestShape, _>(
304                DefaultGpuiComponentShapeBuilder::new(),
305                window,
306                cx,
307            )
308        });
309
310        assert_eq!(window.read(|state, _| state.value), Some(1));
311        let root = window.root();
312        let _render = NoGpuiRenderComponent::new(&root);
313
314        window.update(|state, window, cx| {
315            seed_value_binding_state::<TestShape, u32>(state, Some(&7), window, cx);
316            assert_eq!(state.value, Some(1), "the default seed hook is a no-op");
317            assert_eq!(
318                value_change::<TestShape, u32>(state, &TestEvent(Some(9))),
319                ValueChange::Set(9)
320            );
321            assert_eq!(
322                <TestState as GpuiComponentStateValueBinding<u32>>::value_change(
323                    state,
324                    &TestEvent(None),
325                ),
326                ValueChange::Clear
327            );
328            <TestState as GpuiComponentStateValueBinding<u32>>::seed_value_binding_state(
329                state,
330                Some(&11),
331                window,
332                cx,
333            );
334            assert_eq!(
335                state.value,
336                Some(1),
337                "the state default seed hook is a no-op"
338            );
339
340            let configured =
341                build_component_shape::<TestShape, _>(ConfiguredBuilder(42), window, cx);
342            assert_eq!(configured.value, Some(42));
343        });
344    }
345}