1use std::any::Any;
73use std::rc::Rc;
74use std::sync::atomic::{AtomicU64, Ordering};
75
76use teksilo_canvas::{Canvas, Point, Vec2};
77use teksilo_core::event::Key;
78use teksilo_core::signal::{Prop, Signal};
79use teksilo_core::widget::{EventContext, PaintContext};
80use teksilo_i18n::LocalizedString;
81
82use crate::item::ItemId;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct MagnetId(pub(crate) u64);
91
92impl MagnetId {
93 pub(crate) fn next() -> Self {
95 static COUNTER: AtomicU64 = AtomicU64::new(1);
96 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
97 }
98
99 pub fn as_u64(self) -> u64 {
101 self.0
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum MagnetRole {
115 Source,
117 Target,
119 Bidirectional,
121}
122
123#[derive(Clone)]
130pub struct Magnet {
131 pub(crate) local_pos: Point,
132 pub(crate) role: MagnetRole,
133 pub(crate) payload: Option<Rc<dyn Any>>,
134 pub(crate) enabled: bool,
135 pub(crate) label: Option<LocalizedString>,
136}
137
138impl std::fmt::Debug for Magnet {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("Magnet")
141 .field("local_pos", &self.local_pos)
142 .field("role", &self.role)
143 .field("has_payload", &self.payload.is_some())
144 .field("enabled", &self.enabled)
145 .field("has_label", &self.label.is_some())
146 .finish()
147 }
148}
149
150impl Magnet {
151 pub fn new(local_pos: Point) -> Self {
154 Self {
155 local_pos,
156 role: MagnetRole::Bidirectional,
157 payload: None,
158 enabled: true,
159 label: None,
160 }
161 }
162
163 pub fn role(mut self, role: MagnetRole) -> Self {
165 self.role = role;
166 self
167 }
168
169 pub fn payload<P: 'static>(mut self, payload: P) -> Self {
172 self.payload = Some(Rc::new(payload));
173 self
174 }
175
176 pub fn payload_rc(mut self, payload: Rc<dyn Any>) -> Self {
179 self.payload = Some(payload);
180 self
181 }
182
183 pub fn label(mut self, label: impl Into<LocalizedString>) -> Self {
186 self.label = Some(label.into());
187 self
188 }
189
190 pub fn enabled(mut self, on: bool) -> Self {
193 self.enabled = on;
194 self
195 }
196}
197
198#[derive(Clone)]
208pub struct MagnetRef {
209 pub id: MagnetId,
211 pub item: ItemId,
213 pub role: MagnetRole,
215 pub payload: Option<Rc<dyn Any>>,
217 pub scene_pos: Point,
219}
220
221impl std::fmt::Debug for MagnetRef {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("MagnetRef")
224 .field("id", &self.id)
225 .field("item", &self.item)
226 .field("role", &self.role)
227 .field("has_payload", &self.payload.is_some())
228 .field("scene_pos", &self.scene_pos)
229 .finish()
230 }
231}
232
233impl MagnetRef {
234 pub fn payload_as<P: 'static>(&self) -> Option<&P> {
238 self.payload.as_ref().and_then(|p| p.downcast_ref::<P>())
239 }
240}
241
242pub enum MagnetVerdict {
245 Reject,
247 Accept(Option<Rc<dyn Any>>),
250}
251
252impl MagnetVerdict {
253 pub fn accept() -> Self {
255 MagnetVerdict::Accept(None)
256 }
257
258 pub fn accept_with<P: 'static>(payload: P) -> Self {
260 MagnetVerdict::Accept(Some(Rc::new(payload)))
261 }
262
263 pub fn is_accept(&self) -> bool {
265 matches!(self, MagnetVerdict::Accept(_))
266 }
267}
268
269#[derive(Clone)]
277pub struct MagnetConnection {
278 pub from: MagnetRef,
280 pub to: MagnetRef,
282 pub payload: Option<Rc<dyn Any>>,
284}
285
286impl std::fmt::Debug for MagnetConnection {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 f.debug_struct("MagnetConnection")
289 .field("from", &self.from)
290 .field("to", &self.to)
291 .field("has_payload", &self.payload.is_some())
292 .finish()
293 }
294}
295
296impl MagnetConnection {
297 pub fn payload_as<P: 'static>(&self) -> Option<&P> {
299 self.payload.as_ref().and_then(|p| p.downcast_ref::<P>())
300 }
301}
302
303#[derive(Clone)]
312pub struct MagnetSnap {
313 pub from: MagnetId,
315 pub to: MagnetId,
317 pub snap_vector: Vec2,
320 pub payload: Option<Rc<dyn Any>>,
322 pub distance: f32,
325}
326
327impl std::fmt::Debug for MagnetSnap {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 f.debug_struct("MagnetSnap")
330 .field("from", &self.from)
331 .field("to", &self.to)
332 .field("snap_vector", &self.snap_vector)
333 .field("has_payload", &self.payload.is_some())
334 .field("distance", &self.distance)
335 .finish()
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum MarkerVisibility {
342 Always,
345 DuringInteraction,
349 Never,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum MagnetVisualState {
357 Idle,
359 Candidate,
362 Snapped,
364 Focused,
366 PendingSource,
368}
369
370#[derive(Debug, Clone, Copy)]
372pub struct MagnetMarker {
373 pub id: MagnetId,
375 pub scene_pos: Point,
377 pub role: MagnetRole,
379 pub state: MagnetVisualState,
381}
382
383#[derive(Debug, Clone)]
388pub struct MagnetFeedback {
389 pub zoom: f32,
392 pub markers: Vec<MagnetMarker>,
394 pub connector: Option<(Point, Point, bool)>,
401}
402
403#[derive(Clone)]
412pub struct MagnetismConfig {
413 pub(crate) predicate: Rc<dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict>,
414 pub(crate) on_connect: Rc<dyn Fn(&MagnetConnection, &mut EventContext)>,
415 pub(crate) capture_px: f32,
416 pub(crate) markers: MarkerVisibility,
417 pub(crate) feedback: Option<Rc<dyn Fn(&mut Canvas, &PaintContext, &MagnetFeedback)>>,
418 pub(crate) connect_key: Key,
419 pub(crate) enabled: Signal<bool>,
420}
421
422impl std::fmt::Debug for MagnetismConfig {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 f.debug_struct("MagnetismConfig")
425 .field("capture_px", &self.capture_px)
426 .field("markers", &self.markers)
427 .field("has_custom_feedback", &self.feedback.is_some())
428 .field("connect_key", &self.connect_key)
429 .field("enabled", &self.enabled.get())
430 .finish()
431 }
432}
433
434impl MagnetismConfig {
435 pub fn new(predicate: impl Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict + 'static) -> Self {
441 Self {
442 predicate: Rc::new(predicate),
443 on_connect: Rc::new(|_, _| {}),
444 capture_px: 14.0,
445 markers: MarkerVisibility::DuringInteraction,
446 feedback: None,
447 connect_key: Key::Character('m'),
448 enabled: Signal::new(true),
449 }
450 }
451
452 pub fn on_connect(
457 mut self,
458 f: impl Fn(&MagnetConnection, &mut EventContext) + 'static,
459 ) -> Self {
460 self.on_connect = Rc::new(f);
461 self
462 }
463
464 pub fn capture_px(mut self, px: f32) -> Self {
468 self.capture_px = px.max(0.0);
469 self
470 }
471
472 pub fn markers(mut self, markers: MarkerVisibility) -> Self {
475 self.markers = markers;
476 self
477 }
478
479 pub fn feedback(
483 mut self,
484 f: impl Fn(&mut Canvas, &PaintContext, &MagnetFeedback) + 'static,
485 ) -> Self {
486 self.feedback = Some(Rc::new(f));
487 self
488 }
489
490 pub fn connect_key(mut self, key: Key) -> Self {
493 self.connect_key = key;
494 self
495 }
496
497 pub fn enabled(mut self, on: impl Into<Prop<bool>>) -> Self {
500 self.enabled = on.into().as_signal();
501 self
502 }
503
504 pub fn enabled_signal(&self) -> Signal<bool> {
506 self.enabled.clone()
507 }
508
509 pub fn is_enabled(&self) -> bool {
511 self.enabled.get()
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::scene::Scene;
519 use teksilo_canvas::{Point, Transform2D};
520
521 fn source_to_target(a: &MagnetRef, b: &MagnetRef) -> MagnetVerdict {
524 if a.item == b.item {
525 return MagnetVerdict::Reject;
526 }
527 match (a.role, b.role) {
528 (MagnetRole::Source, MagnetRole::Target) | (MagnetRole::Target, MagnetRole::Source) => {
529 MagnetVerdict::accept()
530 }
531 _ => MagnetVerdict::Reject,
532 }
533 }
534
535 #[test]
536 fn magnet_ids_are_unique_and_monotonic() {
537 let a = MagnetId::next();
538 let b = MagnetId::next();
539 assert_ne!(a, b);
540 assert!(b.as_u64() > a.as_u64());
541 }
542
543 #[test]
544 fn magnet_builder_defaults_and_setters() {
545 let m = Magnet::new(Point::new(3.0, 4.0));
546 assert_eq!(m.local_pos, Point::new(3.0, 4.0));
547 assert_eq!(m.role, MagnetRole::Bidirectional);
548 assert!(m.enabled);
549 assert!(m.payload.is_none());
550
551 let m = Magnet::new(Point::ZERO)
552 .role(MagnetRole::Source)
553 .payload(42_u32)
554 .enabled(false);
555 assert_eq!(m.role, MagnetRole::Source);
556 assert!(!m.enabled);
557 assert_eq!(
558 m.payload.as_ref().unwrap().downcast_ref::<u32>(),
559 Some(&42_u32)
560 );
561 }
562
563 #[test]
564 fn verdict_helpers() {
565 assert!(MagnetVerdict::accept().is_accept());
566 assert!(MagnetVerdict::accept_with(7_i32).is_accept());
567 assert!(!MagnetVerdict::Reject.is_accept());
568 }
569
570 #[test]
573 fn item_snap_snaps_to_nearest_accepting_magnet() {
574 let mut scene = Scene::new();
575 let dragged = scene.add_item(
577 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
578 Point::new(0.0, 0.0),
579 );
580 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
581
582 let target = scene.add_item(
585 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
586 Point::new(100.0, 0.0),
587 );
588 let tmag = scene.add_magnet(target, Magnet::new(Point::ZERO).role(MagnetRole::Target));
589
590 let snap = scene
593 .compute_item_snap(dragged, Vec2::new(95.0, 0.0), 20.0, &source_to_target)
594 .expect("expected a snap");
595 assert_eq!(snap.to, tmag);
596 assert!((snap.snap_vector.x - 5.0).abs() < 1e-3);
598 assert!(snap.snap_vector.y.abs() < 1e-3);
599 }
600
601 #[test]
602 fn item_snap_ignores_pairs_beyond_radius() {
603 let mut scene = Scene::new();
604 let dragged = scene.add_item(
605 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
606 Point::ZERO,
607 );
608 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
609 let target = scene.add_item(
610 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
611 Point::new(100.0, 0.0),
612 );
613 scene.add_magnet(target, Magnet::new(Point::ZERO).role(MagnetRole::Target));
614
615 let snap = scene.compute_item_snap(dragged, Vec2::new(50.0, 0.0), 20.0, &source_to_target);
617 assert!(snap.is_none());
618 }
619
620 #[test]
621 fn item_snap_respects_rejecting_predicate() {
622 let mut scene = Scene::new();
623 let dragged = scene.add_item(
624 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
625 Point::ZERO,
626 );
627 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
629 let target = scene.add_item(
630 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
631 Point::new(100.0, 0.0),
632 );
633 scene.add_magnet(target, Magnet::new(Point::ZERO).role(MagnetRole::Source));
634
635 let snap = scene.compute_item_snap(dragged, Vec2::new(98.0, 0.0), 20.0, &source_to_target);
636 assert!(snap.is_none());
637 }
638
639 #[test]
640 fn item_snap_excludes_dragged_items_own_magnets() {
641 let mut scene = Scene::new();
642 let dragged = scene.add_item(
643 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
644 Point::ZERO,
645 );
646 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
649 scene.add_magnet(
650 dragged,
651 Magnet::new(Point::new(2.0, 0.0)).role(MagnetRole::Target),
652 );
653
654 let snap = scene.compute_item_snap(dragged, Vec2::ZERO, 20.0, &source_to_target);
655 assert!(snap.is_none(), "a dragged item must not snap to itself");
656 }
657
658 #[test]
659 fn item_snap_picks_global_minimum_distance() {
660 let mut scene = Scene::new();
661 let dragged = scene.add_item(
662 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
663 Point::ZERO,
664 );
665 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
666
667 let near = scene.add_item(
669 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
670 Point::new(10.0, 0.0),
671 );
672 let near_mag = scene.add_magnet(near, Magnet::new(Point::ZERO).role(MagnetRole::Target));
673 let far = scene.add_item(
674 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
675 Point::new(18.0, 0.0),
676 );
677 scene.add_magnet(far, Magnet::new(Point::ZERO).role(MagnetRole::Target));
678
679 let snap = scene
681 .compute_item_snap(dragged, Vec2::new(8.0, 0.0), 20.0, &source_to_target)
682 .expect("snap");
683 assert_eq!(snap.to, near_mag);
684 }
685
686 #[test]
687 fn item_snap_carries_verdict_payload() {
688 let mut scene = Scene::new();
689 let dragged = scene.add_item(
690 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
691 Point::ZERO,
692 );
693 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
694 let target = scene.add_item(
695 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
696 Point::new(100.0, 0.0),
697 );
698 scene.add_magnet(target, Magnet::new(Point::ZERO).role(MagnetRole::Target));
699
700 let pred = |a: &MagnetRef, b: &MagnetRef| {
701 if a.item != b.item {
702 MagnetVerdict::accept_with(String::from("edge"))
703 } else {
704 let _ = b;
705 MagnetVerdict::Reject
706 }
707 };
708 let snap = scene
709 .compute_item_snap(dragged, Vec2::new(98.0, 0.0), 20.0, &pred)
710 .expect("snap");
711 assert_eq!(
712 snap.payload.as_ref().unwrap().downcast_ref::<String>(),
713 Some(&String::from("edge"))
714 );
715 }
716
717 #[test]
718 fn item_snap_honors_item_transform() {
719 let mut scene = Scene::new();
720 let dragged = scene.add_item(
721 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
722 Point::ZERO,
723 );
724 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
725
726 let target = scene.add_item(
728 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
729 Point::ZERO,
730 );
731 scene.set_transform(target, Transform2D::scale(2.0, 2.0));
732 let tmag = scene.add_magnet(
733 target,
734 Magnet::new(Point::new(10.0, 0.0)).role(MagnetRole::Target),
735 );
736 assert_eq!(scene.magnet_scene_pos(tmag), Some(Point::new(20.0, 0.0)));
738
739 let snap = scene
740 .compute_item_snap(dragged, Vec2::new(19.0, 0.0), 20.0, &source_to_target)
741 .expect("snap");
742 assert_eq!(snap.to, tmag);
743 assert!((snap.snap_vector.x - 1.0).abs() < 1e-3);
744 }
745
746 #[test]
749 fn port_snap_returns_nearest_accepting_target() {
750 let mut scene = Scene::new();
751 let src_item = scene.add_item(
752 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
753 Point::ZERO,
754 );
755 let source = scene.add_magnet(src_item, Magnet::new(Point::ZERO).role(MagnetRole::Source));
756 let target = scene.add_item(
757 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
758 Point::new(50.0, 0.0),
759 );
760 let tmag = scene.add_magnet(target, Magnet::new(Point::ZERO).role(MagnetRole::Target));
761
762 let res = scene
764 .compute_port_snap(source, Point::new(52.0, 1.0), 20.0, &source_to_target)
765 .expect("port snap");
766 assert_eq!(res.0.id, tmag);
767 }
768
769 #[test]
770 fn port_snap_excludes_source_and_rejects_far() {
771 let mut scene = Scene::new();
772 let src_item = scene.add_item(
773 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
774 Point::ZERO,
775 );
776 let source = scene.add_magnet(src_item, Magnet::new(Point::ZERO).role(MagnetRole::Source));
777
778 let res = scene.compute_port_snap(source, Point::new(0.0, 0.0), 20.0, &source_to_target);
780 assert!(res.is_none());
781 }
782
783 #[test]
786 fn magnet_storage_add_remove_clear() {
787 let mut scene = Scene::new();
788 let item = scene.add_item(
789 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
790 Point::ZERO,
791 );
792 let a = scene.add_magnet(item, Magnet::new(Point::ZERO));
793 let b = scene.add_magnet(item, Magnet::new(Point::new(5.0, 5.0)));
794 assert_eq!(scene.magnet_ids_of(item).len(), 2);
795
796 scene.remove_magnet(a);
797 assert_eq!(scene.magnet_ids_of(item), vec![b]);
798 assert!(scene.magnet(a).is_none());
799
800 scene.clear_magnets(item);
801 assert!(scene.magnet_ids_of(item).is_empty());
802 }
803
804 #[test]
805 fn removing_item_drops_its_magnets() {
806 let mut scene = Scene::new();
807 let item = scene.add_item(
808 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
809 Point::ZERO,
810 );
811 let m = scene.add_magnet(item, Magnet::new(Point::ZERO));
812 assert!(scene.magnet(m).is_some());
813 scene.remove(item);
814 assert!(scene.magnet(m).is_none());
815 assert!(scene.magnet_ids_of(item).is_empty());
816 }
817
818 #[test]
819 fn disabled_magnets_are_not_snap_candidates() {
820 let mut scene = Scene::new();
821 let dragged = scene.add_item(
822 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
823 Point::ZERO,
824 );
825 scene.add_magnet(dragged, Magnet::new(Point::ZERO).role(MagnetRole::Source));
826 let target = scene.add_item(
827 crate::RectItem::new(teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0)),
828 Point::new(100.0, 0.0),
829 );
830 scene.add_magnet(
832 target,
833 Magnet::new(Point::ZERO)
834 .role(MagnetRole::Target)
835 .enabled(false),
836 );
837 let snap = scene.compute_item_snap(dragged, Vec2::new(98.0, 0.0), 20.0, &source_to_target);
838 assert!(snap.is_none());
839 }
840}