1use std::{any::Any, fmt, rc::Rc, sync::Arc};
22
23use gpui_kit::base::AutoScroll;
24use gpui_kit::component::ActiveTheme as _;
25use gpui_kit::{
26 AnyElement, App, AppContext as _, Bounds, Context, CursorStyle, Div, DragMoveEvent, EntityId,
27 Hsla, InteractiveElement as _, IntoElement, Modifiers, ParentElement as _, Pixels, Point,
28 Render, SharedString, Stateful, StatefulInteractiveElement as _, Styled as _, Window, div,
29 point, prelude::FluentBuilder as _, px, relative, size,
30};
31use rust_i18n::t;
32
33use super::{
34 CELL_PADDING, KeyOf, LiveGridDelegate, LiveGridEvent, LiveGridState,
35 frame::PaintedLine,
36 ghost::GhostTextStyle,
37 pinned::BodyLine,
38 selection::LineTarget,
39 stats::{profile_elapsed, profile_start},
40};
41use crate::{
42 engine::GroupPath,
43 model::GridRow,
44 policy::{
45 DropLine, NoRowDrag, NoRowDrop, RowDragGesture, RowDragPressContext, RowDropContext,
46 RowDropPolicy, SelectableLine,
47 },
48 store::StoreSnapshot,
49};
50
51#[derive(Clone)]
55pub struct RowDrag<K> {
56 grid: EntityId,
57 keys: Arc<[K]>,
58 payload: DragPayload,
59}
60
61impl<K> RowDrag<K> {
62 pub fn grid(&self) -> EntityId {
64 self.grid
65 }
66
67 pub fn keys(&self) -> &Arc<[K]> {
70 &self.keys
71 }
72
73 pub fn payload(&self) -> &DragPayload {
75 &self.payload
76 }
77}
78
79impl<K> fmt::Debug for RowDrag<K> {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.debug_struct("RowDrag")
82 .field("grid", &self.grid)
83 .field("keys", &self.keys.len())
84 .field("payload", &self.payload)
85 .finish()
86 }
87}
88
89#[derive(Clone, Default)]
91pub struct DragPayload {
92 data: Option<Arc<dyn Any>>,
93 label: Option<SharedString>,
94}
95
96impl DragPayload {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn with_data<T: 'static>(mut self, data: T) -> Self {
104 self.data = Some(Arc::new(data));
105 self
106 }
107
108 pub fn data<T: 'static>(&self) -> Option<&T> {
110 self.data.as_deref()?.downcast_ref()
111 }
112
113 pub fn with_label(mut self, label: impl Into<SharedString>) -> Self {
116 self.label = Some(label.into());
117 self
118 }
119
120 pub fn label(&self) -> Option<&SharedString> {
122 self.label.as_ref()
123 }
124}
125
126impl fmt::Debug for DragPayload {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.debug_struct("DragPayload")
129 .field("label", &self.label)
130 .field("has_data", &self.data.is_some())
131 .finish()
132 }
133}
134
135#[derive(Clone, Debug)]
137#[non_exhaustive]
138pub enum DropSource<K> {
139 Rows(RowDrag<K>),
141 Payload(DragPayload),
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum DropTarget<K> {
150 Row {
152 key: K,
154 place: DropPlace,
156 },
157 Group(GroupPath),
159 Empty,
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
165#[non_exhaustive]
166pub enum DropPlace {
167 Before,
169 After,
171 Onto,
173}
174
175pub struct RowDragContext<'a, R: GridRow> {
178 keys: &'a [R::Key],
179 pressed: &'a R::Key,
180 snapshot: &'a StoreSnapshot<R>,
181 modifiers: Modifiers,
182}
183
184impl<'a, R: GridRow> RowDragContext<'a, R> {
185 pub fn keys(&self) -> &'a [R::Key] {
187 self.keys
188 }
189
190 pub fn pressed(&self) -> &'a R::Key {
192 self.pressed
193 }
194
195 pub fn row(&self, key: &R::Key) -> Option<&'a Arc<R>> {
197 self.snapshot.row(self.snapshot.row_id(key)?)
198 }
199
200 pub fn modifiers(&self) -> Modifiers {
202 self.modifiers
203 }
204}
205
206type DropRegistrar<D> = Rc<dyn Fn(Stateful<Div>, &mut Context<LiveGridState<D>>) -> Stateful<Div>>;
209
210pub(super) struct DragSlot<D: LiveGridDelegate> {
214 gesture: Box<dyn RowDragGesture<D::Row>>,
215 armed: Option<RowDrag<KeyOf<D>>>,
218 drop_policy: Box<dyn RowDropPolicy<D::Row>>,
219 payloads: Vec<DropRegistrar<D>>,
220 hover: Option<DropHover<KeyOf<D>>>,
223 retarget: bool,
226 auto_scroll: AutoScroll,
227}
228
229impl<D: LiveGridDelegate> Default for DragSlot<D> {
230 fn default() -> Self {
231 Self {
232 gesture: Box::new(NoRowDrag),
233 armed: None,
234 drop_policy: Box::new(NoRowDrop),
235 payloads: Vec::new(),
236 hover: None,
237 retarget: false,
238 auto_scroll: AutoScroll::default(),
239 }
240 }
241}
242
243impl<D: LiveGridDelegate> DragSlot<D> {
244 pub(super) fn is_enabled(&self) -> bool {
247 self.gesture.is_enabled()
248 }
249
250 pub(super) fn is_drag_press(&self, press: &RowDragPressContext<'_, D::Row>) -> bool {
252 self.gesture.is_enabled() && self.gesture.is_drag_press(press)
253 }
254
255 pub(super) fn threshold(&self) -> Pixels {
257 self.gesture.threshold()
258 }
259
260 pub(super) fn unarm(&mut self) {
262 self.armed = None;
263 }
264
265 pub(super) fn is_over(&self) -> bool {
268 self.hover.is_some()
269 }
270}
271
272#[derive(Clone)]
275enum Dragged<K> {
276 Rows(RowDrag<K>),
277 Value(Rc<dyn Any>),
278}
279
280impl<K> Dragged<K> {
281 fn as_ref(&self) -> DraggedRef<'_, K> {
282 match self {
283 Self::Rows(drag) => DraggedRef::Rows(drag),
284 Self::Value(value) => DraggedRef::Value(value.as_ref()),
285 }
286 }
287}
288
289#[derive(Clone, Copy)]
291enum DraggedRef<'a, K> {
292 Rows(&'a RowDrag<K>),
293 Value(&'a dyn Any),
294}
295
296struct DropHover<K> {
299 dragged: Dragged<K>,
300 own: bool,
301 position: Point<Pixels>,
302 modifiers: Modifiers,
303 target: Option<DropTarget<K>>,
304}
305
306struct RowDragGhost {
314 label: SharedString,
315 height: Pixels,
316 offset: Point<Pixels>,
317 text: GhostTextStyle,
318}
319
320impl Render for RowDragGhost {
321 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
322 let theme = cx.theme();
323 self.text
324 .root()
325 .pl(self.offset.x + px(12.))
326 .pt(self.offset.y + px(12.))
327 .child(
328 div()
329 .h(self.height)
330 .max_w(px(320.))
331 .px(CELL_PADDING)
332 .flex()
333 .items_center()
334 .overflow_hidden()
335 .bg(theme.table_head)
336 .text_color(theme.table_head_foreground)
337 .border_1()
338 .border_color(theme.border)
339 .shadow_md()
340 .opacity(0.9)
341 .child(div().min_w(px(0.)).truncate().child(self.label.clone())),
342 )
343 }
344}
345
346fn ghost_label<K>(drag: &RowDrag<K>) -> SharedString {
348 if let Some(label) = drag.payload().label() {
349 return label.clone();
350 }
351 match drag.keys().len() {
352 1 => t!("live_grid.drag.row").into(),
353 count => t!("live_grid.drag.rows", count = count).into(),
354 }
355}
356
357impl<D: LiveGridDelegate> LiveGridState<D> {
358 pub fn with_row_drag_gesture(mut self, gesture: impl RowDragGesture<D::Row>) -> Self {
361 self.drag.gesture = Box::new(gesture);
362 self
363 }
364
365 pub fn with_row_drop_policy(mut self, policy: impl RowDropPolicy<D::Row>) -> Self {
369 self.drag.drop_policy = Box::new(policy);
370 self
371 }
372
373 pub fn with_drop_payload<T: Clone + 'static>(mut self) -> Self {
379 self.drag.payloads.push(Rc::new(|this, cx| {
380 this.on_drag_move(cx.listener(Self::on_payload_drag_move::<T>))
381 .on_drop(cx.listener(Self::on_payload_drop::<T>))
382 }));
383 self
384 }
385
386 pub fn is_dragging_rows(&self) -> bool {
389 self.drag.armed.is_some()
390 }
391
392 #[cfg(any(test, feature = "test-support"))]
394 pub fn row_drag(&self) -> Option<&RowDrag<KeyOf<D>>> {
395 self.drag.armed.as_ref()
396 }
397
398 pub(super) fn is_row_drag_press(
401 &self,
402 key: &KeyOf<D>,
403 column: super::ColumnOf<D>,
404 was_selected: bool,
405 modifiers: Modifiers,
406 click_count: usize,
407 ) -> bool {
408 if !self.drag.is_enabled() {
409 return false;
410 }
411 let Some(id) = self.snapshot.row_id(key) else {
412 return false;
413 };
414 let Some(row) = self.snapshot.row(id) else {
415 return false;
416 };
417 let line = SelectableLine::Row {
418 row: &**row,
419 branch: self.projection.branch(id),
420 };
421 self.drag.is_drag_press(&RowDragPressContext::new(
422 line,
423 column,
424 was_selected,
425 modifiers,
426 self.selection.mode(),
427 click_count,
428 ))
429 }
430
431 pub(super) fn arm_row_drag(
436 &mut self,
437 pressed: &KeyOf<D>,
438 modifiers: Modifiers,
439 cx: &mut Context<Self>,
440 ) -> bool {
441 if self.snapshot.row_id(pressed).is_none() {
442 return false;
443 }
444 let keys = self
445 .command_scope(Some(&LineTarget::Row(pressed.clone())))
446 .shared_keys();
447 let payload = self.delegate.drag_payload(
448 &RowDragContext {
449 keys: &keys,
450 pressed,
451 snapshot: &self.snapshot,
452 modifiers,
453 },
454 cx,
455 );
456 let Some(payload) = payload else {
457 return false;
458 };
459 self.drag.armed = Some(RowDrag {
460 grid: cx.entity_id(),
461 keys,
462 payload,
463 });
464 cx.notify();
465 true
466 }
467
468 pub(super) fn unarm_row_drag(&mut self) {
470 self.drag.unarm();
471 }
472
473 pub(super) fn settle_row_drag(&mut self, cx: &mut Context<Self>) {
476 if self.drag.armed.is_some() && self.press.is_none() && !cx.has_active_drag() {
477 self.drag.unarm();
478 }
479 if self.drag.hover.is_some() && !cx.has_active_drag() {
480 self.end_drop_hover(cx);
481 }
482 }
483
484 pub(super) fn render_rows(
492 &self,
493 body: impl IntoElement,
494 ghost_text: GhostTextStyle,
495 cx: &mut Context<Self>,
496 ) -> AnyElement {
497 let row_height = self.row_height;
498 div()
499 .id("live-grid-rows")
500 .flex()
501 .flex_col()
502 .flex_grow(1.)
503 .flex_shrink(1.)
504 .flex_basis(relative(0.))
505 .w_full()
506 .when(self.drag.is_enabled(), |this| this.on_click(|_, _, _| {}))
507 .on_drag_move(cx.listener(Self::on_row_drag_move))
510 .when(self.drag.drop_policy.is_enabled(), |this| {
511 this.on_drop(cx.listener(Self::on_rows_drop))
512 })
513 .map(|this| {
514 self.drag
515 .payloads
516 .iter()
517 .fold(this, |this, register| register(this, cx))
518 })
519 .when_some(self.drag.armed.clone(), |this, drag| {
522 this.cursor(CursorStyle::ClosedHand).on_drag(
523 drag,
524 move |drag: &RowDrag<KeyOf<D>>, offset, _: &mut Window, cx: &mut App| {
525 let label = ghost_label(drag);
526 cx.new(|_| RowDragGhost {
527 label,
528 height: row_height,
529 offset,
530 text: ghost_text.clone(),
531 })
532 },
533 )
534 })
535 .child(body)
536 .into_any_element()
537 }
538
539 fn on_row_drag_move(
541 &mut self,
542 event: &DragMoveEvent<RowDrag<KeyOf<D>>>,
543 window: &mut Window,
544 cx: &mut Context<Self>,
545 ) {
546 let (position, modifiers) = (event.event.position, event.event.modifiers);
547 let inside = event.bounds.contains(&position);
548 if !inside && self.drag.hover.is_none() {
549 return;
550 }
551 let drag = event.drag(cx).clone();
552 let own = drag.grid() == cx.entity_id();
553 self.follow_drag(
554 Dragged::Rows(drag),
555 own,
556 position,
557 modifiers,
558 inside,
559 window,
560 cx,
561 );
562 }
563
564 fn on_payload_drag_move<T: Clone + 'static>(
567 &mut self,
568 event: &DragMoveEvent<T>,
569 window: &mut Window,
570 cx: &mut Context<Self>,
571 ) {
572 let (position, modifiers) = (event.event.position, event.event.modifiers);
573 let inside = event.bounds.contains(&position);
574 if !inside && self.drag.hover.is_none() {
575 return;
576 }
577 let value = match &self.drag.hover {
578 Some(DropHover {
579 dragged: Dragged::Value(value),
580 ..
581 }) if value.is::<T>() => value.clone(),
582 _ => Rc::new(event.drag(cx).clone()) as Rc<dyn Any>,
583 };
584 self.follow_drag(
585 Dragged::Value(value),
586 false,
587 position,
588 modifiers,
589 inside,
590 window,
591 cx,
592 );
593 }
594
595 #[allow(clippy::too_many_arguments)]
598 fn follow_drag(
599 &mut self,
600 dragged: Dragged<KeyOf<D>>,
601 own: bool,
602 position: Point<Pixels>,
603 modifiers: Modifiers,
604 inside: bool,
605 window: &mut Window,
606 cx: &mut Context<Self>,
607 ) {
608 if !inside {
609 self.end_drop_hover(cx);
612 cx.set_active_drag_cursor_style(CursorStyle::ClosedHand, window);
613 return;
614 }
615 let started = profile_start();
616 let target = self.row_drop_target_at(dragged.as_ref(), own, position, modifiers);
617 self.stats.last_drop_target = profile_elapsed(started);
618 let cursor = match &target {
619 Some(_) if self.drag.drop_policy.is_copy(modifiers) => CursorStyle::DragCopy,
620 Some(_) => CursorStyle::ClosedHand,
621 None if own => CursorStyle::ClosedHand,
623 None => CursorStyle::OperationNotAllowed,
624 };
625 window.defer(cx, move |window, cx| {
629 if cx.active_drag_cursor_style() != Some(cursor) {
630 cx.set_active_drag_cursor_style(cursor, window);
631 }
632 });
633 let changed = self.drag.hover.as_ref().map(|hover| &hover.target) != Some(&target);
634 let entered = self.drag.hover.is_none();
635 self.drag.hover = Some(DropHover {
636 dragged,
637 own,
638 position,
639 modifiers,
640 target,
641 });
642 if self.drag.drop_policy.is_enabled() {
643 let delta = AutoScroll::compute_delta(position.y, self.rows_bounds());
644 self.drag.auto_scroll.last_drag_position = Some(position);
645 self.drag.auto_scroll.set(delta, cx, |delta, state, cx| {
646 state.auto_scroll_drop(delta, cx)
647 });
648 }
649 if changed || entered {
650 cx.notify();
651 }
652 }
653
654 fn auto_scroll_drop(&mut self, delta: Pixels, cx: &mut Context<Self>) {
657 if self.drag.hover.is_none() {
658 self.drag.auto_scroll.stop();
659 return;
660 }
661 let before = (self.scroll_row, self.scroll_offset);
662 self.scroll_rows_by(delta);
663 if (self.scroll_row, self.scroll_offset) != before {
664 self.drag.retarget = true;
665 cx.notify();
666 }
667 }
668
669 pub(super) fn retarget_drop(&mut self) {
672 if !std::mem::take(&mut self.drag.retarget) {
673 return;
674 }
675 let Some(hover) = &self.drag.hover else {
676 return;
677 };
678 let target = self.row_drop_target_at(
679 hover.dragged.as_ref(),
680 hover.own,
681 hover.position,
682 hover.modifiers,
683 );
684 if let Some(hover) = &mut self.drag.hover {
685 hover.target = target;
686 }
687 }
688
689 pub(crate) fn end_drop_hover(&mut self, cx: &mut Context<Self>) {
692 self.drag.auto_scroll.stop();
693 self.drag.retarget = false;
694 if self.drag.hover.take().is_some() {
695 self.reconsider_held(cx);
696 cx.notify();
697 }
698 }
699
700 fn row_drop_target_at(
705 &self,
706 dragged: DraggedRef<'_, KeyOf<D>>,
707 own: bool,
708 position: Point<Pixels>,
709 modifiers: Modifiers,
710 ) -> Option<DropTarget<KeyOf<D>>> {
711 let policy = &self.drag.drop_policy;
712 if !policy.is_enabled() {
713 return None;
714 }
715 let BodyLine::Data(line) = self.body_line_at(position)? else {
716 return None;
717 };
718 let rows = self.rows_bounds();
719 let y = position.y - rows.origin.y + self.scroll_offset;
720 let offset = ((y - self.row_height * line as f32) / self.row_height).clamp(0., 1.);
721 let (snapshot, projection) = (&self.snapshot, &self.projection);
722 let drop_line = match self.painted.get(line) {
723 Some(PaintedLine::Row(id)) => match snapshot.row(*id) {
724 Some(row) => DropLine::Row {
725 key: row.key(),
726 branch: projection.branch(*id),
727 },
728 None => DropLine::Empty,
729 },
730 Some(PaintedLine::Caption(path)) => DropLine::Caption(projection.groups().find(path)?),
731 Some(PaintedLine::Footer(_)) => return None,
732 Some(PaintedLine::Ghost(_)) | None => DropLine::Empty,
733 };
734 let last_row = matches!(drop_line, DropLine::Empty)
736 .then(|| {
737 (0..projection.len())
738 .rev()
739 .filter_map(|ix| projection.row(ix))
740 .find_map(|id| snapshot.row(id))
741 .map(|row| row.key())
742 })
743 .flatten();
744 let (rows_drag, value) = match dragged {
745 DraggedRef::Rows(drag) => (Some(drag), None),
746 DraggedRef::Value(value) => (None, Some(value)),
747 };
748 policy.place(&RowDropContext::new(
749 drop_line,
750 offset,
751 rows_drag,
752 value,
753 own,
754 modifiers,
755 (
756 !self.view.sort().is_empty(),
757 projection.is_grouped(),
758 projection.is_tree(),
759 ),
760 last_row,
761 snapshot,
762 ))
763 }
764
765 fn on_rows_drop(
768 &mut self,
769 drag: &RowDrag<KeyOf<D>>,
770 window: &mut Window,
771 cx: &mut Context<Self>,
772 ) {
773 let own = drag.grid() == cx.entity_id();
774 let source = DropSource::Rows(drag.clone());
775 self.drop_on_rows(DraggedRef::Rows(drag), own, source, window, cx);
776 }
777
778 fn on_payload_drop<T: Clone + 'static>(
780 &mut self,
781 value: &T,
782 window: &mut Window,
783 cx: &mut Context<Self>,
784 ) {
785 let source = DropSource::Payload(DragPayload::new().with_data(value.clone()));
786 self.drop_on_rows(DraggedRef::Value(value), false, source, window, cx);
787 }
788
789 fn drop_on_rows(
790 &mut self,
791 dragged: DraggedRef<'_, KeyOf<D>>,
792 own: bool,
793 source: DropSource<KeyOf<D>>,
794 window: &mut Window,
795 cx: &mut Context<Self>,
796 ) {
797 let (position, modifiers) = (window.mouse_position(), window.modifiers());
798 let target = self.row_drop_target_at(dragged, own, position, modifiers);
799 self.end_drop_hover(cx);
800 if let Some(target) = target {
801 cx.emit(LiveGridEvent::Drop {
802 source,
803 target,
804 modifiers,
805 });
806 }
807 }
808
809 pub(super) fn drop_indicator(
814 &self,
815 rows: Bounds<Pixels>,
816 fill_color: Hsla,
817 border: Hsla,
818 ) -> Vec<(Bounds<Pixels>, Hsla)> {
819 let Some(target) = self
820 .drag
821 .hover
822 .as_ref()
823 .and_then(|hover| hover.target.as_ref())
824 else {
825 return Vec::new();
826 };
827 let top = |line: usize| rows.origin.y + self.row_height * line as f32 - self.scroll_offset;
828 let line_of =
829 |painted: &dyn Fn(&PaintedLine) -> bool| self.painted.iter().position(painted);
830 let mut quads = Vec::new();
831 let bar = |y: Pixels, quads: &mut Vec<_>| {
832 quads.push((
833 Bounds::new(
834 point(rows.origin.x, y - px(1.)),
835 size(rows.size.width, px(2.)),
836 ),
837 border,
838 ));
839 };
840 let frame = |area: Bounds<Pixels>, fill: Option<Hsla>, quads: &mut Vec<_>| {
841 if let Some(color) = fill {
842 quads.push((area, color));
843 }
844 let (w, h, b) = (area.size.width, area.size.height, px(2.));
845 let origin = area.origin;
846 quads.extend([
847 (Bounds::new(origin, size(w, b)), border),
848 (
849 Bounds::new(point(origin.x, origin.y + h - b), size(w, b)),
850 border,
851 ),
852 (Bounds::new(origin, size(b, h)), border),
853 (
854 Bounds::new(point(origin.x + w - b, origin.y), size(b, h)),
855 border,
856 ),
857 ]);
858 };
859 let line_area = |line: usize| {
860 Bounds::new(
861 point(rows.origin.x, top(line)),
862 size(rows.size.width, self.row_height),
863 )
864 };
865 match target {
866 DropTarget::Row { key, place } => {
867 let Some(id) = self.snapshot.row_id(key) else {
868 return quads;
869 };
870 let Some(line) = line_of(&|painted| painted.row() == Some(id)) else {
871 return quads;
872 };
873 match place {
874 DropPlace::Before => bar(top(line), &mut quads),
875 DropPlace::After => bar(top(line + 1), &mut quads),
876 _ => frame(line_area(line), Some(fill_color), &mut quads),
877 }
878 }
879 DropTarget::Group(path) => {
880 if let Some(line) = line_of(&|painted| painted.group() == Some(path)) {
881 frame(line_area(line), Some(fill_color), &mut quads);
882 }
883 }
884 _ => {
885 let shown = self
886 .painted
887 .iter()
888 .rposition(|painted| !matches!(painted, PaintedLine::Ghost(_)));
889 match shown {
890 Some(last) => bar(top(last + 1), &mut quads),
891 None => frame(rows, None, &mut quads),
892 }
893 }
894 }
895 quads
897 .into_iter()
898 .filter_map(|(bounds, color)| {
899 let bounds = bounds.intersect(&rows);
900 (bounds.size.height > px(0.) && bounds.size.width > px(0.))
901 .then_some((bounds, color))
902 })
903 .collect()
904 }
905
906 pub(super) fn cancel_row_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
909 if self.drag.armed.is_none() {
910 return false;
911 }
912 self.drag.unarm();
913 cx.stop_active_drag(window);
914 cx.notify();
915 true
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 #[test]
924 fn test_drag_payload() {
925 let payload = DragPayload::new();
926 assert!(payload.label().is_none() && payload.data::<u32>().is_none());
927 let payload = payload.with_data(7u32).with_label("3 orders");
928 assert_eq!(payload.data::<u32>(), Some(&7));
929 assert_eq!(payload.data::<i64>(), None);
930 assert_eq!(payload.label().map(AsRef::as_ref), Some("3 orders"));
931 let debug = format!("{payload:?}");
932 assert!(debug.contains("3 orders") && debug.contains("has_data: true"));
933 assert_ne!(
934 DropTarget::Row {
935 key: 1,
936 place: DropPlace::Before
937 },
938 DropTarget::Empty
939 );
940 }
941}