1use std::collections::BTreeMap;
2use std::fmt;
3use std::rc::Rc;
4
5use gpui::{
6 AnyElement, App, Entity, InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
7 SharedString, StatefulInteractiveElement, Styled, Window, div,
8};
9use thiserror::Error;
10
11use crate::{
12 ComponentStateSchema, ObjectField, PrimitiveContext, PrimitiveDescriptor, PrimitiveHandler,
13 PrimitiveId, PrimitiveInstance, PrimitiveTheme, PrimitiveValue, RuntimeEngine,
14 ScriptViewExtension, ScriptViewHandle, UiValue, ValueSchema,
15};
16
17type HostSlotFactory = Rc<dyn Fn(&mut Window, &mut App) -> Result<AnyElement, String>>;
18
19#[derive(Clone, Default)]
25pub struct HostSlotRegistry {
26 slots: BTreeMap<String, HostSlotFactory>,
27}
28
29impl fmt::Debug for HostSlotRegistry {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 formatter
32 .debug_struct("HostSlotRegistry")
33 .field("slots", &self.slots.keys().collect::<Vec<_>>())
34 .finish()
35 }
36}
37
38impl HostSlotRegistry {
39 #[must_use]
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn register(
53 &mut self,
54 name: impl Into<String>,
55 factory: impl Fn(&mut Window, &mut App) -> Result<AnyElement, String> + 'static,
56 ) -> Result<(), HostSlotError> {
57 let name = name.into();
58 validate_name(&name)?;
59 if self.slots.contains_key(&name) {
60 return Err(HostSlotError::Duplicate(name));
61 }
62 self.slots.insert(name, Rc::new(factory));
63 Ok(())
64 }
65
66 pub fn with_slot(
72 mut self,
73 name: impl Into<String>,
74 factory: impl Fn(&mut Window, &mut App) -> Result<AnyElement, String> + 'static,
75 ) -> Result<Self, HostSlotError> {
76 self.register(name, factory)?;
77 Ok(self)
78 }
79
80 pub fn with_entity<V: Render + 'static>(
86 self,
87 name: impl Into<String>,
88 entity: Entity<V>,
89 ) -> Result<Self, HostSlotError> {
90 self.with_slot(name, move |_, _| Ok(entity.clone().into_any_element()))
91 }
92
93 pub fn with_script_view(
104 self,
105 name: impl Into<String>,
106 view: ScriptViewHandle,
107 ) -> Result<Self, HostSlotError> {
108 self.with_slot(name, move |_, _| {
109 view.host_slot_item().map_err(|error| error.to_string())
110 })
111 }
112
113 fn factory(&self, name: &str) -> Option<HostSlotFactory> {
114 self.slots.get(name).cloned()
115 }
116}
117
118impl ScriptViewExtension for HostSlotRegistry {
119 fn configure_engine(&self, engine: &mut RuntimeEngine) -> Result<(), String> {
120 engine
121 .register_primitive(
122 host_slot_primitive_descriptor(),
123 HostSlotPrimitiveHandler {
124 slots: self.clone(),
125 },
126 )
127 .map_err(|error| error.to_string())
128 }
129}
130
131#[derive(Debug, Error, Eq, PartialEq)]
132pub enum HostSlotError {
133 #[error("host slot name `{0}` is invalid")]
134 InvalidName(String),
135 #[error("host slot `{0}` is registered more than once")]
136 Duplicate(String),
137}
138
139fn validate_name(name: &str) -> Result<(), HostSlotError> {
140 if name.is_empty()
141 || name.len() > 128
142 || !name
143 .chars()
144 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
145 {
146 Err(HostSlotError::InvalidName(name.to_owned()))
147 } else {
148 Ok(())
149 }
150}
151
152fn host_slot_primitive_descriptor() -> PrimitiveDescriptor {
153 PrimitiveDescriptor {
154 id: PrimitiveId::parse("gpui_rhai.host_slot")
155 .expect("built-in HostSlot primitive ID is valid"),
156 export: "HostSlot".to_owned(),
157 props: BTreeMap::from([(
158 "name".to_owned(),
159 ObjectField::required(ValueSchema::string()),
160 )]),
161 events: BTreeMap::new(),
162 state: ComponentStateSchema::default(),
163 lifecycle: true,
164 effect: None,
165 }
166}
167
168struct HostSlotPrimitiveHandler {
169 slots: HostSlotRegistry,
170}
171
172impl PrimitiveHandler for HostSlotPrimitiveHandler {
173 fn render(
174 &mut self,
175 instance: &PrimitiveInstance,
176 _: &PrimitiveContext,
177 _: &PrimitiveTheme,
178 window: &mut Window,
179 cx: &mut App,
180 ) -> Result<AnyElement, String> {
181 let Some(PrimitiveValue::Data(UiValue::String(name))) = instance.node.props.get("name")
182 else {
183 return Err("HostSlot.name must be a string".to_owned());
184 };
185 let factory = self
186 .slots
187 .factory(name)
188 .ok_or_else(|| format!("host slot `{name}` is not registered"))?;
189 let child = factory(window, cx)?;
190 let node = instance
191 .id
192 .as_ref()
193 .map(crate::PrimitiveInstanceId::node)
194 .ok_or_else(|| "HostSlot requires retained identity".to_owned())?;
195 let mut boundary = div()
196 .id(SharedString::from(format!("gpui-rhai-host-slot-{node}")))
197 .flex()
198 .flex_col()
199 .size_full()
200 .overflow_hidden()
201 .occlude()
202 .on_any_mouse_down(|_, _, cx| cx.stop_propagation());
203 for button in MouseButton::all() {
204 boundary = boundary.on_mouse_up(button, |_, _, cx| cx.stop_propagation());
205 }
206 Ok(boundary
207 .on_mouse_move(|_, _, cx| cx.stop_propagation())
208 .on_scroll_wheel(|_, _, cx| cx.stop_propagation())
209 .on_click(|_, _, cx| cx.stop_propagation())
210 .child(child)
211 .into_any_element())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn registry_rejects_invalid_and_duplicate_names() {
221 assert_eq!(
222 HostSlotRegistry::new()
223 .with_slot("bad/name", |_, _| Ok(div().into_any_element()))
224 .unwrap_err(),
225 HostSlotError::InvalidName("bad/name".to_owned())
226 );
227
228 let mut slots = HostSlotRegistry::new();
229 slots
230 .register("content", |_, _| Ok(div().into_any_element()))
231 .unwrap();
232 assert_eq!(
233 slots
234 .register("content", |_, _| Ok(div().into_any_element()))
235 .unwrap_err(),
236 HostSlotError::Duplicate("content".to_owned())
237 );
238 }
239
240 #[test]
241 fn extension_registers_a_keyed_opaque_primitive_constructor() {
242 let slots = HostSlotRegistry::new()
243 .with_slot("content", |_, _| Ok(div().into_any_element()))
244 .unwrap();
245 let mut engine = RuntimeEngine::new();
246 slots.configure_engine(&mut engine).unwrap();
247 let compiled = engine
248 .compile(
249 r#"
250 fn view() {
251 gpui_rhai::HostSlot(#{ key: "content", name: "content" })
252 }
253 "#,
254 )
255 .unwrap();
256 let root = engine.render(&compiled).unwrap();
257 let crate::UiNodeKind::Custom { primitive } = root.kind() else {
258 panic!("HostSlot must remain an ordinary custom primitive");
259 };
260 assert_eq!(primitive.primitive.as_str(), "gpui_rhai.host_slot");
261 assert_eq!(primitive.key.as_deref(), Some("content"));
262 assert_eq!(
263 primitive.props.get("name"),
264 Some(&PrimitiveValue::Data(UiValue::String("content".to_owned())))
265 );
266 }
267}