Skip to main content

gpui_rhai/
event.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::rc::Rc;
5
6use gpui::{App, SharedString, Window};
7use rhai::{CustomType, TypeBuilder};
8
9use crate::{ComponentInstancePath, ScriptCallback, UiValue};
10
11#[derive(Clone, Debug, PartialEq)]
12pub struct UiEvent {
13    pub name: String,
14    pub payload: UiValue,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
18pub struct LogicalPoint {
19    pub x: f64,
20    pub y: f64,
21}
22
23#[allow(clippy::struct_excessive_bools)]
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct EventModifiers {
26    pub control: bool,
27    pub alt: bool,
28    pub shift: bool,
29    pub platform: bool,
30    pub function: bool,
31}
32
33impl EventModifiers {
34    #[must_use]
35    pub fn into_value(self) -> UiValue {
36        UiValue::Map(BTreeMap::from([
37            ("control".to_owned(), UiValue::Bool(self.control)),
38            ("alt".to_owned(), UiValue::Bool(self.alt)),
39            ("shift".to_owned(), UiValue::Bool(self.shift)),
40            ("platform".to_owned(), UiValue::Bool(self.platform)),
41            ("function".to_owned(), UiValue::Bool(self.function)),
42        ]))
43    }
44}
45
46#[derive(Clone, Debug, PartialEq)]
47pub struct PointerEventData {
48    pub pointer_id: u64,
49    pub pointer_type: String,
50    pub window: LogicalPoint,
51    pub local: LogicalPoint,
52    pub content: LogicalPoint,
53    pub movement: LogicalPoint,
54    pub velocity: LogicalPoint,
55    pub button: Option<String>,
56    pub buttons: Vec<String>,
57    pub modifiers: EventModifiers,
58    pub click_count: usize,
59    pub timestamp_ms: f64,
60    pub captured: bool,
61    pub target: Option<crate::GeometryBounds>,
62}
63
64impl PointerEventData {
65    #[must_use]
66    pub fn into_value(self) -> UiValue {
67        UiValue::Map(BTreeMap::from([
68            (
69                "pointer_id".to_owned(),
70                UiValue::Integer(i64::try_from(self.pointer_id).unwrap_or(i64::MAX)),
71            ),
72            (
73                "pointer_type".to_owned(),
74                UiValue::String(self.pointer_type),
75            ),
76            ("window".to_owned(), point_value(self.window)),
77            ("local".to_owned(), point_value(self.local)),
78            ("content".to_owned(), point_value(self.content)),
79            ("movement".to_owned(), point_value(self.movement)),
80            ("velocity".to_owned(), point_value(self.velocity)),
81            (
82                "button".to_owned(),
83                self.button.map_or(UiValue::Null, UiValue::String),
84            ),
85            (
86                "buttons".to_owned(),
87                UiValue::Array(self.buttons.into_iter().map(UiValue::String).collect()),
88            ),
89            ("modifiers".to_owned(), self.modifiers.into_value()),
90            (
91                "click_count".to_owned(),
92                UiValue::Integer(i64::try_from(self.click_count).unwrap_or(i64::MAX)),
93            ),
94            ("timestamp_ms".to_owned(), UiValue::Float(self.timestamp_ms)),
95            ("captured".to_owned(), UiValue::Bool(self.captured)),
96            (
97                "target".to_owned(),
98                self.target
99                    .map_or(UiValue::Null, crate::GeometryBounds::into_value),
100            ),
101            ("pressure".to_owned(), UiValue::Null),
102            ("tilt_x".to_owned(), UiValue::Null),
103            ("tilt_y".to_owned(), UiValue::Null),
104        ]))
105    }
106}
107
108#[derive(Clone, Debug, PartialEq)]
109pub struct WheelEventData {
110    pub window: LogicalPoint,
111    pub local: LogicalPoint,
112    pub content: LogicalPoint,
113    pub delta: LogicalPoint,
114    pub precise: bool,
115    pub modifiers: EventModifiers,
116    pub timestamp_ms: f64,
117    pub target: Option<crate::GeometryBounds>,
118}
119
120impl WheelEventData {
121    #[must_use]
122    pub fn into_value(self) -> UiValue {
123        UiValue::Map(BTreeMap::from([
124            ("window".to_owned(), point_value(self.window)),
125            ("local".to_owned(), point_value(self.local)),
126            ("content".to_owned(), point_value(self.content)),
127            ("delta".to_owned(), point_value(self.delta)),
128            ("precise".to_owned(), UiValue::Bool(self.precise)),
129            ("modifiers".to_owned(), self.modifiers.into_value()),
130            ("timestamp_ms".to_owned(), UiValue::Float(self.timestamp_ms)),
131            (
132                "target".to_owned(),
133                self.target
134                    .map_or(UiValue::Null, crate::GeometryBounds::into_value),
135            ),
136        ]))
137    }
138}
139
140fn point_value(point: LogicalPoint) -> UiValue {
141    UiValue::Map(BTreeMap::from([
142        ("x".to_owned(), UiValue::Float(point.x)),
143        ("y".to_owned(), UiValue::Float(point.y)),
144    ]))
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub enum EventPropagation {
149    Handled,
150    Propagate,
151}
152
153#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
154pub enum PropagationControl {
155    #[default]
156    Continue,
157    Stop,
158    StopImmediate,
159}
160
161#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
162pub enum PointerCaptureDirective {
163    #[default]
164    None,
165    Capture,
166    Release,
167}
168
169/// Orthogonal response controls returned by Script, Host, and native handlers.
170#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
171pub struct EventResponse {
172    prevent_default: bool,
173    propagation: PropagationControl,
174    pointer_capture: PointerCaptureDirective,
175}
176
177impl EventResponse {
178    #[must_use]
179    pub const fn new() -> Self {
180        Self {
181            prevent_default: false,
182            propagation: PropagationControl::Continue,
183            pointer_capture: PointerCaptureDirective::None,
184        }
185    }
186
187    #[must_use]
188    pub const fn prevent_default(mut self) -> Self {
189        self.prevent_default = true;
190        self
191    }
192
193    #[must_use]
194    pub const fn stop(mut self) -> Self {
195        self.propagation = PropagationControl::Stop;
196        self
197    }
198
199    #[must_use]
200    pub const fn stop_immediate(mut self) -> Self {
201        self.propagation = PropagationControl::StopImmediate;
202        self
203    }
204
205    #[must_use]
206    pub const fn capture_pointer(mut self) -> Self {
207        self.pointer_capture = PointerCaptureDirective::Capture;
208        self
209    }
210
211    #[must_use]
212    pub const fn release_pointer(mut self) -> Self {
213        self.pointer_capture = PointerCaptureDirective::Release;
214        self
215    }
216
217    #[must_use]
218    pub const fn default_prevented(self) -> bool {
219        self.prevent_default
220    }
221
222    #[must_use]
223    pub const fn propagation(self) -> PropagationControl {
224        self.propagation
225    }
226
227    #[must_use]
228    pub const fn pointer_capture(self) -> PointerCaptureDirective {
229        self.pointer_capture
230    }
231
232    #[must_use]
233    pub const fn stops_propagation(self) -> bool {
234        !matches!(self.propagation, PropagationControl::Continue)
235    }
236
237    pub(crate) fn merge(&mut self, response: Self) {
238        self.prevent_default |= response.prevent_default;
239        if !matches!(response.propagation, PropagationControl::Continue) {
240            self.propagation = response.propagation;
241        }
242        if !matches!(response.pointer_capture, PointerCaptureDirective::None) {
243            self.pointer_capture = response.pointer_capture;
244        }
245    }
246}
247
248impl From<EventPropagation> for EventResponse {
249    fn from(value: EventPropagation) -> Self {
250        match value {
251            EventPropagation::Handled => Self::new().stop(),
252            EventPropagation::Propagate => Self::new(),
253        }
254    }
255}
256
257impl From<EventResponse> for EventPropagation {
258    fn from(value: EventResponse) -> Self {
259        if value.stops_propagation() {
260            Self::Handled
261        } else {
262            Self::Propagate
263        }
264    }
265}
266
267impl CustomType for EventResponse {
268    fn build(mut builder: TypeBuilder<Self>) {
269        builder
270            .with_name("EventResponse")
271            .with_fn("prevent_default", |response: &mut Self| {
272                (*response).prevent_default()
273            })
274            .with_fn("stop", |response: &mut Self| (*response).stop())
275            .with_fn("stop_immediate", |response: &mut Self| {
276                (*response).stop_immediate()
277            })
278            .with_fn("capture_pointer", |response: &mut Self| {
279                (*response).capture_pointer()
280            })
281            .with_fn("release_pointer", |response: &mut Self| {
282                (*response).release_pointer()
283            });
284    }
285}
286
287#[derive(Clone, Copy, Debug, Eq, PartialEq)]
288pub enum EventPhase {
289    Capture,
290    Target,
291    Bubble,
292}
293
294#[derive(Clone, Debug, Default)]
295pub struct PointerCaptureRegistry {
296    active: Rc<RefCell<BTreeMap<u64, crate::NodeId>>>,
297}
298
299impl PointerCaptureRegistry {
300    #[must_use]
301    pub fn new() -> Self {
302        Self::default()
303    }
304
305    pub fn capture(&self, pointer_id: u64, node: crate::NodeId) {
306        self.active.borrow_mut().insert(pointer_id, node);
307    }
308
309    pub fn release(&self, pointer_id: u64) -> Option<crate::NodeId> {
310        self.active.borrow_mut().remove(&pointer_id)
311    }
312
313    pub(crate) fn clear(&self) {
314        self.active.borrow_mut().clear();
315    }
316
317    #[must_use]
318    pub fn captured(&self, pointer_id: u64) -> Option<crate::NodeId> {
319        self.active.borrow().get(&pointer_id).copied()
320    }
321
322    pub(crate) fn retain_nodes(&self, nodes: &std::collections::BTreeSet<crate::NodeId>) {
323        self.active
324            .borrow_mut()
325            .retain(|_, node| nodes.contains(node));
326    }
327
328    pub(crate) fn snapshot(&self) -> BTreeMap<u64, crate::NodeId> {
329        self.active.borrow().clone()
330    }
331
332    pub(crate) fn restore(&self, snapshot: BTreeMap<u64, crate::NodeId>) {
333        *self.active.borrow_mut() = snapshot;
334    }
335}
336
337type HostCallbackFn = dyn Fn(UiValue, &mut Window, &mut App) -> EventResponse;
338
339#[derive(Clone)]
340pub struct HostCallback {
341    label: SharedString,
342    handler: Rc<HostCallbackFn>,
343}
344
345impl HostCallback {
346    /// Construct one foreground Host event callback.
347    ///
348    /// # Panics
349    ///
350    /// Panics when the diagnostic label is empty or whitespace-only.
351    #[must_use]
352    pub fn new<R>(
353        label: impl Into<SharedString>,
354        handler: impl Fn(UiValue, &mut Window, &mut App) -> R + 'static,
355    ) -> Self
356    where
357        R: Into<EventResponse>,
358    {
359        let label = label.into();
360        assert!(
361            !label.as_ref().trim().is_empty(),
362            "HostCallback label cannot be empty"
363        );
364        Self {
365            label,
366            handler: Rc::new(move |payload, window, app| handler(payload, window, app).into()),
367        }
368    }
369
370    #[must_use]
371    pub fn label(&self) -> &str {
372        self.label.as_ref()
373    }
374
375    pub(crate) fn invoke(
376        &self,
377        payload: UiValue,
378        window: &mut Window,
379        app: &mut App,
380    ) -> EventResponse {
381        (self.handler)(payload, window, app)
382    }
383}
384
385#[derive(Clone, Debug, PartialEq)]
386pub struct UiEventBinding {
387    phase: EventPhase,
388    handler: UiEventHandler,
389}
390
391impl UiEventBinding {
392    #[must_use]
393    pub fn new(phase: EventPhase, handler: impl Into<UiEventHandler>) -> Self {
394        Self {
395            phase,
396            handler: handler.into(),
397        }
398    }
399
400    #[must_use]
401    pub const fn phase(&self) -> EventPhase {
402        self.phase
403    }
404
405    #[must_use]
406    pub const fn handler(&self) -> &UiEventHandler {
407        &self.handler
408    }
409
410    pub(crate) const fn handler_mut(&mut self) -> &mut UiEventHandler {
411        &mut self.handler
412    }
413}
414
415impl fmt::Debug for HostCallback {
416    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
417        formatter
418            .debug_struct("HostCallback")
419            .field("label", &self.label)
420            .finish_non_exhaustive()
421    }
422}
423
424impl PartialEq for HostCallback {
425    fn eq(&self, other: &Self) -> bool {
426        self.label == other.label && Rc::ptr_eq(&self.handler, &other.handler)
427    }
428}
429
430impl Eq for HostCallback {}
431
432#[derive(Clone, Debug, PartialEq)]
433pub enum UiEventHandler {
434    Script(ScriptCallback),
435    Host(HostCallback),
436    Native(crate::NativeHandlerRef),
437}
438
439impl UiEventHandler {
440    #[must_use]
441    pub fn diagnostic_label(&self) -> String {
442        match self {
443            Self::Script(callback) => format!("script:{}", callback.name()),
444            Self::Host(callback) => format!("host:{}", callback.label()),
445            Self::Native(reference) => {
446                format!("native:{}", reference.descriptor().id)
447            }
448        }
449    }
450
451    pub(crate) const fn as_script_mut(&mut self) -> Option<&mut ScriptCallback> {
452        match self {
453            Self::Script(callback) => Some(callback),
454            Self::Host(_) | Self::Native(_) => None,
455        }
456    }
457
458    #[must_use]
459    pub const fn as_script(&self) -> Option<&ScriptCallback> {
460        match self {
461            Self::Script(callback) => Some(callback),
462            Self::Host(_) | Self::Native(_) => None,
463        }
464    }
465}
466
467impl From<ScriptCallback> for UiEventHandler {
468    fn from(callback: ScriptCallback) -> Self {
469        Self::Script(callback)
470    }
471}
472
473impl From<HostCallback> for UiEventHandler {
474    fn from(callback: HostCallback) -> Self {
475        Self::Host(callback)
476    }
477}
478
479impl From<crate::NativeHandlerRef> for UiEventHandler {
480    fn from(reference: crate::NativeHandlerRef) -> Self {
481        Self::Native(reference)
482    }
483}
484
485#[derive(Clone, Debug, Default)]
486pub struct EventRouter {
487    handlers: BTreeMap<ComponentInstancePath, BTreeMap<String, Vec<UiEventBinding>>>,
488}
489
490impl EventRouter {
491    #[must_use]
492    pub fn new() -> Self {
493        Self::default()
494    }
495
496    pub fn bind(
497        &mut self,
498        target: ComponentInstancePath,
499        event: impl Into<String>,
500        callback: ScriptCallback,
501    ) {
502        self.bind_phase(target, event, EventPhase::Bubble, callback);
503    }
504
505    pub fn bind_phase(
506        &mut self,
507        target: ComponentInstancePath,
508        event: impl Into<String>,
509        phase: EventPhase,
510        handler: impl Into<UiEventHandler>,
511    ) {
512        self.handlers
513            .entry(target)
514            .or_default()
515            .entry(event.into())
516            .or_default()
517            .push(UiEventBinding::new(phase, handler));
518    }
519
520    pub fn unbind_component(&mut self, target: &ComponentInstancePath) {
521        self.handlers.remove(target);
522    }
523
524    /// Bubble a normalized event from its target toward the root.
525    ///
526    /// # Errors
527    ///
528    /// Propagates errors returned by the callback invoker and stops dispatch.
529    pub fn dispatch<E>(
530        &self,
531        target: &ComponentInstancePath,
532        event: &UiEvent,
533        mut invoke: impl FnMut(
534            &ComponentInstancePath,
535            &UiEventHandler,
536            &UiEvent,
537        ) -> Result<EventResponse, E>,
538    ) -> Result<EventDispatchReport, E> {
539        let mut route = Vec::new();
540        let mut current = Some(target.clone());
541        while let Some(path) = current {
542            route.push(path.clone());
543            current = path.parent();
544        }
545        route.reverse();
546        let mut report = EventDispatchReport::default();
547        for path in &route {
548            if self.dispatch_phase(path, event, EventPhase::Capture, &mut invoke, &mut report)? {
549                return Ok(report);
550            }
551        }
552        if self.dispatch_phase(target, event, EventPhase::Target, &mut invoke, &mut report)? {
553            return Ok(report);
554        }
555        for path in route.iter().rev() {
556            if self.dispatch_phase(path, event, EventPhase::Bubble, &mut invoke, &mut report)? {
557                return Ok(report);
558            }
559        }
560        Ok(report)
561    }
562
563    fn dispatch_phase<E>(
564        &self,
565        path: &ComponentInstancePath,
566        event: &UiEvent,
567        phase: EventPhase,
568        invoke: &mut impl FnMut(
569            &ComponentInstancePath,
570            &UiEventHandler,
571            &UiEvent,
572        ) -> Result<EventResponse, E>,
573        report: &mut EventDispatchReport,
574    ) -> Result<bool, E> {
575        report.visited.push((path.clone(), phase));
576        let Some(bindings) = self
577            .handlers
578            .get(path)
579            .and_then(|handlers| handlers.get(&event.name))
580        else {
581            return Ok(false);
582        };
583        let mut stop_route = false;
584        for binding in bindings.iter().filter(|binding| binding.phase == phase) {
585            let response = invoke(path, &binding.handler, event)?;
586            report.response.merge(response);
587            report.invoked = report.invoked.saturating_add(1);
588            if matches!(response.propagation, PropagationControl::StopImmediate) {
589                report.stopped_at = Some(path.clone());
590                return Ok(true);
591            }
592            if matches!(response.propagation, PropagationControl::Stop) {
593                report.stopped_at = Some(path.clone());
594                stop_route = true;
595            }
596        }
597        Ok(stop_route)
598    }
599}
600
601#[derive(Clone, Debug, Default, Eq, PartialEq)]
602pub struct EventDispatchReport {
603    pub visited: Vec<(ComponentInstancePath, EventPhase)>,
604    pub stopped_at: Option<ComponentInstancePath>,
605    pub invoked: usize,
606    pub response: EventResponse,
607}
608
609#[cfg(test)]
610mod tests {
611    use std::cell::Cell;
612
613    use super::*;
614    use crate::{RuntimeEngine, UiNode};
615
616    struct DropSentinel(Rc<Cell<bool>>);
617
618    impl Drop for DropSentinel {
619        fn drop(&mut self) {
620            self.0.set(true);
621        }
622    }
623
624    #[test]
625    fn events_bubble_until_explicitly_handled() {
626        let mut runtime = RuntimeEngine::new();
627        let compiled = runtime
628            .compile(
629                r#"
630                    fn view() { text("event") }
631                    fn child_click(payload) { payload }
632                    fn root_click(payload) { payload }
633                "#,
634            )
635            .unwrap();
636        let root = ComponentInstancePath::root("App", "root");
637        let child = root.child("Button", "save");
638        let mut router = EventRouter::new();
639        router.bind(
640            child.clone(),
641            "click",
642            runtime.callback(&compiled, "child_click").unwrap(),
643        );
644        router.bind(
645            root.clone(),
646            "click",
647            runtime.callback(&compiled, "root_click").unwrap(),
648        );
649        let event = UiEvent {
650            name: "click".to_owned(),
651            payload: UiValue::Null,
652        };
653        let mut order = Vec::new();
654        let report = router
655            .dispatch(&child, &event, |path, _, _| {
656                order.push(path.clone());
657                Ok::<_, ()>(
658                    if path == &root {
659                        EventPropagation::Handled
660                    } else {
661                        EventPropagation::Propagate
662                    }
663                    .into(),
664                )
665            })
666            .unwrap();
667
668        assert_eq!(order, vec![child, root.clone()]);
669        assert_eq!(report.stopped_at, Some(root));
670        assert_eq!(report.invoked, 2);
671    }
672
673    #[test]
674    fn capture_target_bubble_and_immediate_stop_are_ordered() {
675        let mut runtime = RuntimeEngine::new();
676        let compiled = runtime
677            .compile("fn view() { text(\"event\") } fn callback(payload) { payload }")
678            .unwrap();
679        let callback = runtime.callback(&compiled, "callback").unwrap();
680        let root = ComponentInstancePath::root("App", "root");
681        let child = root.child("Box", "child");
682        let mut router = EventRouter::new();
683        router.bind_phase(
684            root.clone(),
685            "pointer_down",
686            EventPhase::Capture,
687            callback.clone(),
688        );
689        router.bind_phase(
690            child.clone(),
691            "pointer_down",
692            EventPhase::Target,
693            callback.clone(),
694        );
695        router.bind_phase(
696            child.clone(),
697            "pointer_down",
698            EventPhase::Target,
699            callback.clone(),
700        );
701        router.bind_phase(root.clone(), "pointer_down", EventPhase::Bubble, callback);
702        let event = UiEvent {
703            name: "pointer_down".to_owned(),
704            payload: UiValue::Null,
705        };
706        let mut order = Vec::new();
707        let report = router
708            .dispatch(&child, &event, |path, _, _| {
709                order.push(path.clone());
710                Ok::<_, ()>(if order.len() == 2 {
711                    EventResponse::new().prevent_default().stop_immediate()
712                } else {
713                    EventResponse::new()
714                })
715            })
716            .unwrap();
717        assert_eq!(order, vec![root, child.clone()]);
718        assert_eq!(report.stopped_at, Some(child));
719        assert_eq!(report.invoked, 2);
720        assert!(report.response.default_prevented());
721    }
722
723    #[test]
724    fn stop_allows_remaining_same_phase_handlers_but_blocks_ancestors() {
725        let mut runtime = RuntimeEngine::new();
726        let compiled = runtime
727            .compile("fn view() { text(\"event\") } fn callback(payload) { payload }")
728            .unwrap();
729        let callback = runtime.callback(&compiled, "callback").unwrap();
730        let root = ComponentInstancePath::root("App", "root");
731        let child = root.child("Box", "child");
732        let mut router = EventRouter::new();
733        router.bind(child.clone(), "click", callback.clone());
734        router.bind(child.clone(), "click", callback.clone());
735        router.bind(root, "click", callback);
736        let mut invoked = 0;
737        let report = router
738            .dispatch(
739                &child,
740                &UiEvent {
741                    name: "click".to_owned(),
742                    payload: UiValue::Null,
743                },
744                |_, _, _| {
745                    invoked += 1;
746                    Ok::<_, ()>(if invoked == 1 {
747                        EventResponse::new().stop()
748                    } else {
749                        EventResponse::new()
750                    })
751                },
752            )
753            .unwrap();
754        assert_eq!(invoked, 2);
755        assert_eq!(report.stopped_at, Some(child));
756    }
757
758    #[test]
759    fn pointer_capture_is_node_scoped_and_clears_on_unmount() {
760        let mut tree = crate::RetainedUiTree::new();
761        tree.reconcile(crate::UiNode::text("drag")).unwrap();
762        let node = tree.root_id().unwrap();
763        let captures = PointerCaptureRegistry::new();
764        captures.capture(0, node);
765        assert_eq!(captures.captured(0), Some(node));
766        captures.retain_nodes(&std::collections::BTreeSet::new());
767        assert_eq!(captures.captured(0), None);
768    }
769
770    #[test]
771    fn host_callback_identity_debug_and_tree_drop_follow_rc_ownership() {
772        let callback = HostCallback::new("widget.input", |_, _, _| EventPropagation::Handled);
773        let cloned = callback.clone();
774        let other = HostCallback::new("widget.input", |_, _, _| EventPropagation::Handled);
775        assert_eq!(callback, cloned);
776        assert_ne!(callback, other);
777        let debug = format!("{callback:?}");
778        assert!(debug.contains("widget.input"));
779        assert!(!debug.contains("0x"));
780
781        let dropped = Rc::new(Cell::new(false));
782        {
783            let sentinel = DropSentinel(Rc::clone(&dropped));
784            let callback = HostCallback::new("widget.drop", move |_, _, _| {
785                let _ = &sentinel;
786                EventPropagation::Handled
787            });
788            let _tree = UiNode::text("host-owned").with_host_handler("click", callback);
789        }
790        assert!(dropped.get());
791    }
792
793    #[test]
794    fn rhai_cannot_construct_host_callbacks() {
795        let mut runtime = RuntimeEngine::new();
796        let compiled = runtime
797            .compile(
798                r#"
799                    fn view() {
800                        text("host").with_host_handler("click", ())
801                    }
802                "#,
803            )
804            .unwrap();
805        assert!(runtime.render(&compiled).is_err());
806    }
807}