1use std::cell::RefCell;
4use std::collections::{BTreeMap, BTreeSet};
5use std::rc::Rc;
6
7use gpui::{
8 AnyElement, App, AppContext, Background, Bounds, Context, DispatchPhase, Edges, Element,
9 ElementId, Entity, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
10 InteractiveElement, IntoElement, KeyDownEvent, LayoutId, MouseButton, MouseDownEvent,
11 PaintQuad, ParentElement, Pixels, Render, Style, Styled, Window, div, px, relative, rgba, size,
12};
13
14use crate::{
15 ComponentStateSchema, EventSchema, ObjectField, PrimitiveContext, PrimitiveDescriptor,
16 PrimitiveHandler, PrimitiveId, PrimitiveInstance, PrimitiveInstanceId, PrimitiveProps,
17 PrimitiveTheme, Rgba8, UiValue, ValueSchema,
18};
19
20#[derive(Clone)]
21struct DragSourceConfig {
22 id: String,
23 source_id: String,
24 payload_type: String,
25 payload: UiValue,
26 operation: crate::interaction::DragOperation,
27 threshold: f64,
28 keyboard_target: Option<String>,
29 disabled: bool,
30 focus: Option<FocusHandle>,
31 accent: Rgba8,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35struct DragSourceFingerprint {
36 source_id: String,
37 payload_type: String,
38 payload: UiValue,
39 operation: crate::interaction::DragOperation,
40 disabled: bool,
41}
42
43#[derive(Clone)]
44struct DragSourceState(Rc<RefCell<DragSourceFingerprint>>);
45
46impl DragSourceState {
47 fn new(config: &DragSourceConfig) -> Self {
48 Self(Rc::new(RefCell::new(source_fingerprint(config))))
49 }
50}
51
52fn source_fingerprint(config: &DragSourceConfig) -> DragSourceFingerprint {
53 DragSourceFingerprint {
54 source_id: config.source_id.clone(),
55 payload_type: config.payload_type.clone(),
56 payload: config.payload.clone(),
57 operation: config.operation,
58 disabled: config.disabled,
59 }
60}
61
62struct SourcePrepaint {
63 hitbox: Hitbox,
64}
65
66struct DragSourceElement {
67 config: DragSourceConfig,
68 context: PrimitiveContext,
69}
70
71impl IntoElement for DragSourceElement {
72 type Element = Self;
73
74 fn into_element(self) -> Self::Element {
75 self
76 }
77}
78
79impl Element for DragSourceElement {
80 type RequestLayoutState = ();
81 type PrepaintState = SourcePrepaint;
82
83 fn id(&self) -> Option<ElementId> {
84 Some(ElementId::Name(self.config.id.clone().into()))
85 }
86
87 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
88 None
89 }
90
91 fn request_layout(
92 &mut self,
93 _: Option<&GlobalElementId>,
94 _: Option<&InspectorElementId>,
95 window: &mut Window,
96 cx: &mut App,
97 ) -> (LayoutId, ()) {
98 (
99 window.request_layout(
100 Style {
101 size: size(relative(1.0).into(), relative(1.0).into()),
102 ..Style::default()
103 },
104 None,
105 cx,
106 ),
107 (),
108 )
109 }
110
111 fn prepaint(
112 &mut self,
113 _: Option<&GlobalElementId>,
114 _: Option<&InspectorElementId>,
115 bounds: Bounds<Pixels>,
116 (): &mut (),
117 window: &mut Window,
118 cx: &mut App,
119 ) -> SourcePrepaint {
120 let state = window
121 .use_state(cx, |_, _| DragSourceState::new(&self.config))
122 .read(cx)
123 .clone();
124 let changed = {
125 let next = source_fingerprint(&self.config);
126 let mut fingerprint = state.0.borrow_mut();
127 let changed = *fingerprint != next;
128 *fingerprint = next;
129 changed
130 };
131 let owner = self.context.interaction_owner(&self.config.id);
132 if changed {
133 self.context.cancel_interaction(&owner, window, cx);
134 }
135 SourcePrepaint {
136 hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
137 }
138 }
139
140 fn paint(
141 &mut self,
142 _: Option<&GlobalElementId>,
143 _: Option<&InspectorElementId>,
144 bounds: Bounds<Pixels>,
145 (): &mut (),
146 prepaint: &mut SourcePrepaint,
147 window: &mut Window,
148 _: &mut App,
149 ) {
150 let owner = self.context.interaction_owner(&self.config.id);
151 self.context.present_interaction(owner.clone());
152 if self.context.app_drag_source_active(&owner) {
153 paint_feedback(bounds, self.config.accent, 0x16, window);
154 }
155 register_source_pointer(prepaint, &self.config, &self.context, window);
156 }
157}
158
159fn register_source_pointer(
160 prepaint: &SourcePrepaint,
161 config: &DragSourceConfig,
162 context: &PrimitiveContext,
163 window: &mut Window,
164) {
165 let hitbox = prepaint.hitbox.clone();
166 let config = config.clone();
167 let context = context.clone();
168 let view = window.current_view();
169 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
170 if phase != DispatchPhase::Bubble
171 || event.button != MouseButton::Left
172 || config.disabled
173 || !hitbox.is_hovered(window)
174 {
175 return;
176 }
177 if let Some(focus) = config.focus.as_ref() {
178 focus.focus(window, cx);
179 }
180 let owner = context.interaction_owner(&config.id);
181 let spec = drag_spec(&config, owner.clone(), view);
182 let finish_context = context.clone();
183 context.begin_application_drag(
184 spec,
185 event.position,
186 config.threshold,
187 move |result, cancelled, window, cx| {
188 finish_context.propose("drag_end", drag_end_value(&result, cancelled), window, cx);
189 },
190 window,
191 cx,
192 );
193 cx.stop_propagation();
194 });
195}
196
197fn drag_spec(
198 config: &DragSourceConfig,
199 owner: crate::interaction::InteractionOwner,
200 notify: gpui::EntityId,
201) -> crate::interaction::ApplicationDragSpec {
202 crate::interaction::ApplicationDragSpec::new(
203 owner,
204 config.source_id.clone(),
205 config.payload_type.clone(),
206 config.payload.clone(),
207 config.operation,
208 notify,
209 )
210}
211
212#[derive(Clone)]
213struct DropZoneConfig {
214 id: String,
215 target_id: String,
216 payload_types: BTreeSet<String>,
217 operations: BTreeSet<crate::interaction::DragOperation>,
218 priority: i64,
219 disabled: bool,
220 accent: Rgba8,
221 danger: Rgba8,
222}
223
224struct ZonePrepaint {
225 bounds: Bounds<Pixels>,
226 hitbox: Hitbox,
227}
228
229struct DropZoneElement {
230 config: DropZoneConfig,
231 context: PrimitiveContext,
232}
233
234impl IntoElement for DropZoneElement {
235 type Element = Self;
236
237 fn into_element(self) -> Self::Element {
238 self
239 }
240}
241
242impl Element for DropZoneElement {
243 type RequestLayoutState = ();
244 type PrepaintState = ZonePrepaint;
245
246 fn id(&self) -> Option<ElementId> {
247 Some(ElementId::Name(self.config.id.clone().into()))
248 }
249
250 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
251 None
252 }
253
254 fn request_layout(
255 &mut self,
256 _: Option<&GlobalElementId>,
257 _: Option<&InspectorElementId>,
258 window: &mut Window,
259 cx: &mut App,
260 ) -> (LayoutId, ()) {
261 (
262 window.request_layout(
263 Style {
264 size: size(relative(1.0).into(), relative(1.0).into()),
265 ..Style::default()
266 },
267 None,
268 cx,
269 ),
270 (),
271 )
272 }
273
274 fn prepaint(
275 &mut self,
276 _: Option<&GlobalElementId>,
277 _: Option<&InspectorElementId>,
278 bounds: Bounds<Pixels>,
279 (): &mut (),
280 window: &mut Window,
281 _: &mut App,
282 ) -> ZonePrepaint {
283 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
284 ZonePrepaint { bounds, hitbox }
285 }
286
287 fn paint(
288 &mut self,
289 _: Option<&GlobalElementId>,
290 _: Option<&InspectorElementId>,
291 _: Bounds<Pixels>,
292 (): &mut (),
293 prepaint: &mut ZonePrepaint,
294 window: &mut Window,
295 _: &mut App,
296 ) {
297 let owner = self.context.interaction_owner(&self.config.id);
298 if self.config.disabled {
299 return;
300 }
301 let Ok(bounds) = crate::GeometryBounds::new(
302 f64::from(prepaint.bounds.origin.x),
303 f64::from(prepaint.bounds.origin.y),
304 f64::from(prepaint.bounds.size.width),
305 f64::from(prepaint.bounds.size.height),
306 ) else {
307 return;
308 };
309 let target_context = self.context.clone();
310 let target_id = self.config.target_id.clone();
311 self.context
312 .register_drop_target(crate::interaction::DropTargetRegistration::new(
313 owner.clone(),
314 self.config.target_id.clone(),
315 bounds,
316 self.config.payload_types.clone(),
317 self.config.operations.clone(),
318 self.config.priority,
319 prepaint.hitbox.clone(),
320 self.context.ancestor_scroll_handles(),
321 window.current_view(),
322 move |drag, position, window, cx| {
323 target_context.propose(
324 "drop",
325 drop_value(drag, &target_id, position),
326 window,
327 cx,
328 );
329 },
330 ));
331 match self.context.drop_target_state(&owner) {
332 crate::interaction::DropTargetState::Active => {
333 paint_feedback(prepaint.bounds, self.config.accent, 0x28, window);
334 }
335 crate::interaction::DropTargetState::Eligible => {
336 paint_feedback(prepaint.bounds, self.config.accent, 0x0e, window);
337 }
338 crate::interaction::DropTargetState::Invalid => {
339 paint_feedback(prepaint.bounds, self.config.danger, 0x12, window);
340 }
341 crate::interaction::DropTargetState::Idle => {}
342 }
343 }
344}
345
346fn paint_feedback(bounds: Bounds<Pixels>, color: Rgba8, alpha: u8, window: &mut Window) {
347 let translucent = Rgba8::from_rgba_hex((color.as_rgba_hex() & 0xffff_ff00) | u32::from(alpha));
348 window.paint_quad(PaintQuad {
349 bounds,
350 corner_radii: px(0.0).into(),
351 background: Background::from(rgba(translucent.as_rgba_hex())),
352 border_widths: Edges::all(px(1.0)),
353 border_color: rgba(color.as_rgba_hex()).into(),
354 border_style: gpui::BorderStyle::default(),
355 });
356}
357
358fn drop_value(
359 drag: &crate::interaction::ApplicationDragSpec,
360 target_id: &str,
361 position: gpui::Point<Pixels>,
362) -> UiValue {
363 UiValue::Map(BTreeMap::from([
364 (
365 "source_id".to_owned(),
366 UiValue::String(drag.source_id().to_owned()),
367 ),
368 (
369 "target_id".to_owned(),
370 UiValue::String(target_id.to_owned()),
371 ),
372 (
373 "payload_type".to_owned(),
374 UiValue::String(drag.payload_type().to_owned()),
375 ),
376 ("payload".to_owned(), drag.payload().clone()),
377 (
378 "operation".to_owned(),
379 UiValue::String(drag.operation().as_str().to_owned()),
380 ),
381 ("x".to_owned(), UiValue::Float(f64::from(position.x))),
382 ("y".to_owned(), UiValue::Float(f64::from(position.y))),
383 ]))
384}
385
386fn drag_end_value(result: &crate::interaction::ApplicationDropResult, cancelled: bool) -> UiValue {
387 UiValue::Map(BTreeMap::from([
388 ("accepted".to_owned(), UiValue::Bool(result.accepted)),
389 (
390 "target_id".to_owned(),
391 result
392 .target_id
393 .as_ref()
394 .map_or(UiValue::Null, |target| UiValue::String(target.clone())),
395 ),
396 (
397 "operation".to_owned(),
398 UiValue::String(result.operation.as_str().to_owned()),
399 ),
400 ("cancelled".to_owned(), UiValue::Bool(cancelled)),
401 ]))
402}
403
404struct DragSourceEntity {
405 focus: FocusHandle,
406 config: DragSourceConfig,
407 context: PrimitiveContext,
408}
409
410impl DragSourceEntity {
411 fn new(
412 mut config: DragSourceConfig,
413 context: PrimitiveContext,
414 cx: &mut Context<Self>,
415 ) -> Self {
416 let focus = config.focus.clone().unwrap_or_else(|| cx.focus_handle());
417 config.focus = Some(focus.clone());
418 Self {
419 focus,
420 config,
421 context,
422 }
423 }
424
425 fn update(&mut self, mut config: DragSourceConfig, context: PrimitiveContext) {
426 config.focus = Some(self.focus.clone());
427 self.config = config;
428 self.context = context;
429 }
430
431 fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
432 if self.config.disabled || !matches!(event.keystroke.key.as_str(), "enter" | "space") {
433 return;
434 }
435 let Some(target) = self.config.keyboard_target.as_deref() else {
436 return;
437 };
438 let owner = self.context.interaction_owner(&self.config.id);
439 let spec = drag_spec(&self.config, owner, cx.entity_id());
440 let result = self
441 .context
442 .perform_keyboard_drop(&spec, target, window, cx);
443 self.context
444 .propose("drag_end", drag_end_value(&result, false), window, cx);
445 cx.stop_propagation();
446 }
447}
448
449impl Render for DragSourceEntity {
450 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
451 div()
452 .size_full()
453 .track_focus(&self.focus.clone().tab_stop(!self.config.disabled))
454 .on_key_down(cx.listener(Self::key_down))
455 .child(DragSourceElement {
456 config: self.config.clone(),
457 context: self.context.clone(),
458 })
459 }
460}
461
462#[derive(Default)]
463pub struct DragSourcePrimitiveHandler {
464 instances: BTreeMap<PrimitiveInstanceId, Entity<DragSourceEntity>>,
465}
466
467impl PrimitiveHandler for DragSourcePrimitiveHandler {
468 fn uses_primary_focus(&self) -> bool {
469 true
470 }
471
472 fn render(
473 &mut self,
474 instance: &PrimitiveInstance,
475 context: &PrimitiveContext,
476 theme: &PrimitiveTheme,
477 _: &mut Window,
478 cx: &mut App,
479 ) -> Result<AnyElement, String> {
480 let id = instance
481 .id
482 .clone()
483 .ok_or_else(|| "DragSourcePrimitive requires a stable key".to_owned())?;
484 let config = parse_source(
485 &instance.node.props,
486 instance.focus_handle().cloned(),
487 theme,
488 )?;
489 let entity = if let Some(entity) = self.instances.get(&id) {
490 entity.clone()
491 } else {
492 let entity = cx.new(|cx| DragSourceEntity::new(config.clone(), context.clone(), cx));
493 self.instances.insert(id.clone(), entity.clone());
494 entity
495 };
496 entity.update(cx, |source, _| source.update(config, context.clone()));
497 Ok(entity.into_any_element())
498 }
499
500 fn unmount(&mut self, instance: &PrimitiveInstanceId) {
501 self.instances.remove(instance);
502 }
503}
504
505#[derive(Default)]
506pub struct DropZonePrimitiveHandler;
507
508impl PrimitiveHandler for DropZonePrimitiveHandler {
509 fn render(
510 &mut self,
511 instance: &PrimitiveInstance,
512 context: &PrimitiveContext,
513 theme: &PrimitiveTheme,
514 _: &mut Window,
515 _: &mut App,
516 ) -> Result<AnyElement, String> {
517 Ok(DropZoneElement {
518 config: parse_target(&instance.node.props, theme)?,
519 context: context.clone(),
520 }
521 .into_any_element())
522 }
523}
524
525fn parse_source(
526 props: &PrimitiveProps,
527 focus: Option<FocusHandle>,
528 theme: &PrimitiveTheme,
529) -> Result<DragSourceConfig, String> {
530 let source_id = required_safe_string(props, "source_id")?;
531 let payload_type = required_safe_string(props, "payload_type")?;
532 let operation = props
533 .string("operation")
534 .and_then(crate::interaction::DragOperation::parse)
535 .ok_or_else(|| "drag source operation must be copy or move".to_owned())?;
536 let threshold = props.number("threshold").unwrap_or(4.0);
537 if !threshold.is_finite() || !(0.0..=64.0).contains(&threshold) {
538 return Err("drag source threshold must be finite and in [0,64]".to_owned());
539 }
540 let payload = props
541 .data("payload")
542 .cloned()
543 .ok_or_else(|| "drag source requires payload".to_owned())?;
544 Ok(DragSourceConfig {
545 id: format!("gpui-rhai-drag-source:{source_id}"),
546 source_id,
547 payload_type,
548 payload,
549 operation,
550 threshold,
551 keyboard_target: props.string("keyboard_target").map(str::to_owned),
552 disabled: props.boolean("disabled").unwrap_or(false),
553 focus,
554 accent: theme
555 .color("accent")
556 .unwrap_or(Rgba8::from_rgba_hex(0x3b82_f6ff)),
557 })
558}
559
560fn parse_target(props: &PrimitiveProps, theme: &PrimitiveTheme) -> Result<DropZoneConfig, String> {
561 let target_id = required_safe_string(props, "target_id")?;
562 let payload_types = string_set(props, "payload_types")?;
563 let operations = operation_set(props, "operations")?;
564 if payload_types.is_empty() || operations.is_empty() {
565 return Err("drop zone requires at least one payload type and operation".to_owned());
566 }
567 Ok(DropZoneConfig {
568 id: format!("gpui-rhai-drop-zone:{target_id}"),
569 target_id,
570 payload_types,
571 operations,
572 priority: props.integer("priority").unwrap_or(0),
573 disabled: props.boolean("disabled").unwrap_or(false),
574 accent: theme
575 .color("accent")
576 .unwrap_or(Rgba8::from_rgba_hex(0x3b82_f6ff)),
577 danger: theme
578 .color("danger")
579 .unwrap_or(Rgba8::from_rgba_hex(0xdc26_26ff)),
580 })
581}
582
583fn required_safe_string(props: &PrimitiveProps, name: &str) -> Result<String, String> {
584 let value = props
585 .string(name)
586 .ok_or_else(|| format!("{name} is required"))?;
587 if value.is_empty() || value.len() > 128 || value.chars().any(char::is_control) {
588 Err(format!("{name} must be 1-128 non-control characters"))
589 } else {
590 Ok(value.to_owned())
591 }
592}
593
594fn string_set(props: &PrimitiveProps, name: &str) -> Result<BTreeSet<String>, String> {
595 let Some(UiValue::Array(values)) = props.data(name) else {
596 return Err(format!("{name} must be an array"));
597 };
598 values
599 .iter()
600 .map(|value| match value {
601 UiValue::String(value)
602 if !value.is_empty()
603 && value.len() <= 128
604 && !value.chars().any(char::is_control) =>
605 {
606 Ok(value.clone())
607 }
608 _ => Err(format!("{name} entries must be safe strings")),
609 })
610 .collect()
611}
612
613fn operation_set(
614 props: &PrimitiveProps,
615 name: &str,
616) -> Result<BTreeSet<crate::interaction::DragOperation>, String> {
617 let Some(UiValue::Array(values)) = props.data(name) else {
618 return Err(format!("{name} must be an array"));
619 };
620 values
621 .iter()
622 .map(|value| match value {
623 UiValue::String(value) => crate::interaction::DragOperation::parse(value)
624 .ok_or_else(|| format!("{name} entries must be copy or move")),
625 _ => Err(format!("{name} entries must be strings")),
626 })
627 .collect()
628}
629
630fn drag_end_schema() -> ValueSchema {
631 ValueSchema::object(BTreeMap::from([
632 (
633 "accepted".to_owned(),
634 ObjectField::required(ValueSchema::Bool),
635 ),
636 (
637 "target_id".to_owned(),
638 ObjectField::required(ValueSchema::optional(ValueSchema::string())),
639 ),
640 (
641 "operation".to_owned(),
642 ObjectField::required(ValueSchema::String {
643 allowed: vec!["copy".to_owned(), "move".to_owned()],
644 }),
645 ),
646 (
647 "cancelled".to_owned(),
648 ObjectField::required(ValueSchema::Bool),
649 ),
650 ]))
651}
652
653fn drop_schema() -> ValueSchema {
654 ValueSchema::object(BTreeMap::from([
655 (
656 "source_id".to_owned(),
657 ObjectField::required(ValueSchema::string()),
658 ),
659 (
660 "target_id".to_owned(),
661 ObjectField::required(ValueSchema::string()),
662 ),
663 (
664 "payload_type".to_owned(),
665 ObjectField::required(ValueSchema::string()),
666 ),
667 (
668 "payload".to_owned(),
669 ObjectField::required(ValueSchema::UiValue),
670 ),
671 (
672 "operation".to_owned(),
673 ObjectField::required(ValueSchema::String {
674 allowed: vec!["copy".to_owned(), "move".to_owned()],
675 }),
676 ),
677 ("x".to_owned(), ObjectField::required(ValueSchema::number())),
678 ("y".to_owned(), ObjectField::required(ValueSchema::number())),
679 ]))
680}
681
682#[must_use]
688pub fn drag_source_primitive_descriptor() -> PrimitiveDescriptor {
689 PrimitiveDescriptor {
690 id: PrimitiveId::parse("gpui_rhai.drag_source").expect("static primitive ID"),
691 export: "DragSourcePrimitive".to_owned(),
692 props: BTreeMap::from([
693 (
694 "source_id".to_owned(),
695 ObjectField::required(ValueSchema::string()),
696 ),
697 (
698 "payload_type".to_owned(),
699 ObjectField::required(ValueSchema::string()),
700 ),
701 (
702 "payload".to_owned(),
703 ObjectField::required(ValueSchema::UiValue),
704 ),
705 (
706 "operation".to_owned(),
707 ObjectField::required(ValueSchema::String {
708 allowed: vec!["copy".to_owned(), "move".to_owned()],
709 }),
710 ),
711 (
712 "threshold".to_owned(),
713 ObjectField::optional(ValueSchema::bounded_number(Some(0.0), Some(64.0))),
714 ),
715 (
716 "keyboard_target".to_owned(),
717 ObjectField::optional(ValueSchema::optional(ValueSchema::string())),
718 ),
719 (
720 "disabled".to_owned(),
721 ObjectField::optional(ValueSchema::Bool).with_default(UiValue::Bool(false)),
722 ),
723 (
724 "on_drag_end".to_owned(),
725 ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
726 ),
727 ]),
728 events: BTreeMap::from([(
729 "drag_end".to_owned(),
730 EventSchema {
731 payload: drag_end_schema(),
732 },
733 )]),
734 state: ComponentStateSchema::default(),
735 lifecycle: true,
736 effect: None,
737 }
738}
739
740#[must_use]
746pub fn drop_zone_primitive_descriptor() -> PrimitiveDescriptor {
747 PrimitiveDescriptor {
748 id: PrimitiveId::parse("gpui_rhai.drop_zone").expect("static primitive ID"),
749 export: "DropZonePrimitive".to_owned(),
750 props: BTreeMap::from([
751 (
752 "target_id".to_owned(),
753 ObjectField::required(ValueSchema::string()),
754 ),
755 (
756 "payload_types".to_owned(),
757 ObjectField::required(ValueSchema::Array {
758 items: Box::new(ValueSchema::string()),
759 max_items: Some(32),
760 }),
761 ),
762 (
763 "operations".to_owned(),
764 ObjectField::required(ValueSchema::Array {
765 items: Box::new(ValueSchema::String {
766 allowed: vec!["copy".to_owned(), "move".to_owned()],
767 }),
768 max_items: Some(2),
769 }),
770 ),
771 (
772 "priority".to_owned(),
773 ObjectField::optional(ValueSchema::integer()),
774 ),
775 (
776 "disabled".to_owned(),
777 ObjectField::optional(ValueSchema::Bool).with_default(UiValue::Bool(false)),
778 ),
779 (
780 "on_drop".to_owned(),
781 ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
782 ),
783 ]),
784 events: BTreeMap::from([(
785 "drop".to_owned(),
786 EventSchema {
787 payload: drop_schema(),
788 },
789 )]),
790 state: ComponentStateSchema::default(),
791 lifecycle: false,
792 effect: None,
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn typed_target_acceptance_distinguishes_payload_and_operation() {
802 let props = PrimitiveProps::new()
803 .with(
804 "payload_types",
805 crate::PrimitiveValue::Data(UiValue::Array(vec![UiValue::String(
806 "card".to_owned(),
807 )])),
808 )
809 .with(
810 "operations",
811 crate::PrimitiveValue::Data(UiValue::Array(vec![UiValue::String(
812 "move".to_owned(),
813 )])),
814 );
815 assert_eq!(
816 string_set(&props, "payload_types").unwrap(),
817 BTreeSet::from(["card".to_owned()])
818 );
819 assert_eq!(
820 operation_set(&props, "operations").unwrap(),
821 BTreeSet::from([crate::interaction::DragOperation::Move])
822 );
823 }
824}