Skip to main content

open_gpui_canvas/
tool.rs

1use crate::gesture::CanvasPreparedGestureCommit;
2use crate::layer::CanvasZOrderCommand;
3use crate::session::{CanvasToolSession, CanvasToolSessionEffect, CanvasToolSessionSnapshot};
4use crate::{
5    CanvasClipboardPayload, CanvasConnectionEndpointRole, CanvasDefaultEdgeRouter, CanvasDocument,
6    CanvasDocumentDiff, CanvasEdgeRouter, CanvasEndpoint, CanvasKindRegistry,
7    CanvasPasteTransaction, CanvasRecordId, CanvasRuntime, CanvasStore, CanvasStoreChange,
8    CanvasStoreListenerId, CanvasTransaction, CanvasViewport, DocumentCommand, DocumentError,
9    EdgeId, HitTarget, NodeId, ShapeId,
10};
11use indexmap::IndexSet;
12use open_gpui::{Bounds, Pixels, Point};
13use serde::{Deserialize, Serialize};
14use std::{fmt, sync::Arc};
15
16mod action;
17mod builtin;
18mod clipboard;
19mod context;
20mod group;
21mod history;
22mod registry;
23mod select;
24mod z_order;
25
26use crate::session::ToolState;
27pub use action::CanvasToolIntent;
28pub(crate) use action::{CanvasEditorAction, CanvasToolEffect};
29use builtin::BuiltInCanvasTool;
30pub use context::CanvasToolContext;
31pub(crate) use context::CanvasToolReducerContext;
32pub(crate) use context::RECONNECT_HANDLE_VIEW_SIZE;
33pub use history::CanvasHistory;
34pub use registry::{CanvasToolReducer, CanvasToolRegistry, CanvasToolRegistryError};
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
37pub enum PointerButton {
38    Primary,
39    Secondary,
40    Middle,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
44pub enum CanvasKey {
45    Delete,
46    Backspace,
47    Escape,
48    Enter,
49    Character(String),
50    Named(String),
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
54pub struct CanvasKeyModifiers {
55    pub shift: bool,
56    pub alt: bool,
57    pub control: bool,
58    pub platform: bool,
59    pub function: bool,
60}
61
62impl CanvasKeyModifiers {
63    pub const NONE: Self = Self {
64        shift: false,
65        alt: false,
66        control: false,
67        platform: false,
68        function: false,
69    };
70
71    pub fn modified(self) -> bool {
72        self.shift || self.alt || self.control || self.platform || self.function
73    }
74}
75
76impl Default for CanvasKeyModifiers {
77    fn default() -> Self {
78        Self::NONE
79    }
80}
81
82#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83pub enum CanvasEvent {
84    PointerDown {
85        position: Point<Pixels>,
86        button: PointerButton,
87        #[serde(default)]
88        modifiers: CanvasKeyModifiers,
89    },
90    PointerMove {
91        position: Point<Pixels>,
92        #[serde(default)]
93        modifiers: CanvasKeyModifiers,
94    },
95    PointerUp {
96        position: Point<Pixels>,
97        button: PointerButton,
98        #[serde(default)]
99        modifiers: CanvasKeyModifiers,
100    },
101    Wheel {
102        delta: Point<Pixels>,
103    },
104    KeyDown {
105        key: CanvasKey,
106        modifiers: CanvasKeyModifiers,
107        repeat: bool,
108    },
109    Cancel,
110}
111
112#[derive(Clone, Debug, PartialEq)]
113pub struct CanvasConnectionDragState {
114    pub source: CanvasEndpoint,
115    pub current: Point<Pixels>,
116}
117
118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
119pub enum CanvasConnectionRelease {
120    Connected(CanvasConnectedRelease),
121    Dropped(CanvasDroppedConnectionRelease),
122    Reconnected(CanvasReconnectedRelease),
123    ReconnectDropped(CanvasDroppedReconnectRelease),
124    Rejected(CanvasRejectedConnectionRelease),
125}
126
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128pub struct CanvasConnectedRelease {
129    pub source: CanvasEndpoint,
130    pub target: CanvasEndpoint,
131    pub edge_id: EdgeId,
132    pub position: Point<Pixels>,
133}
134
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
136pub struct CanvasDroppedConnectionRelease {
137    pub source: CanvasEndpoint,
138    pub position: Point<Pixels>,
139}
140
141#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142pub struct CanvasReconnectedRelease {
143    pub edge_id: EdgeId,
144    pub endpoint: CanvasConnectionEndpointRole,
145    pub fixed: CanvasEndpoint,
146    pub replacement: CanvasEndpoint,
147    pub position: Point<Pixels>,
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
151pub struct CanvasDroppedReconnectRelease {
152    pub edge_id: EdgeId,
153    pub endpoint: CanvasConnectionEndpointRole,
154    pub fixed: CanvasEndpoint,
155    pub position: Point<Pixels>,
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
159pub enum CanvasConnectionRejectReason {
160    InvalidSource,
161    InvalidTarget,
162    NoTarget,
163    SameEndpoint,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct CanvasRejectedConnectionRelease {
168    pub reason: CanvasConnectionRejectReason,
169    pub source: Option<CanvasEndpoint>,
170    pub edge_id: Option<EdgeId>,
171    pub endpoint: Option<CanvasConnectionEndpointRole>,
172    pub position: Point<Pixels>,
173}
174
175#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
176#[serde(transparent)]
177pub struct CanvasToolId(String);
178
179impl CanvasToolId {
180    pub fn new(id: impl Into<String>) -> Self {
181        Self(id.into())
182    }
183
184    pub fn as_str(&self) -> &str {
185        &self.0
186    }
187}
188
189impl From<&str> for CanvasToolId {
190    fn from(value: &str) -> Self {
191        Self::new(value)
192    }
193}
194
195impl From<String> for CanvasToolId {
196    fn from(value: String) -> Self {
197        Self::new(value)
198    }
199}
200
201impl fmt::Display for CanvasToolId {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        f.write_str(self.as_str())
204    }
205}
206
207#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
208pub enum CanvasTool {
209    Select,
210    Pan,
211    Connect,
212    Custom(CanvasToolId),
213}
214
215impl CanvasTool {
216    pub fn custom(id: impl Into<CanvasToolId>) -> Self {
217        Self::Custom(id.into())
218    }
219
220    pub fn custom_id(&self) -> Option<&CanvasToolId> {
221        match self {
222            Self::Custom(id) => Some(id),
223            _ => None,
224        }
225    }
226}
227
228#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
229pub enum CanvasSelectionMode {
230    #[default]
231    Replace,
232    Add,
233}
234
235#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
236pub struct CanvasSelection {
237    nodes: IndexSet<NodeId>,
238    edges: IndexSet<EdgeId>,
239    shapes: IndexSet<ShapeId>,
240    handles: IndexSet<CanvasEndpoint>,
241}
242
243impl CanvasSelection {
244    pub fn clear(&mut self) {
245        self.nodes.clear();
246        self.edges.clear();
247        self.shapes.clear();
248        self.handles.clear();
249    }
250
251    pub fn replace_with(&mut self, target: HitTarget) {
252        self.clear();
253        self.insert_target(target);
254    }
255
256    pub fn contains_node(&self, id: &NodeId) -> bool {
257        self.nodes.contains(id)
258    }
259
260    pub fn contains_edge(&self, id: &EdgeId) -> bool {
261        self.edges.contains(id)
262    }
263
264    pub fn contains_shape(&self, id: &ShapeId) -> bool {
265        self.shapes.contains(id)
266    }
267
268    pub fn contains_handle(&self, endpoint: &CanvasEndpoint) -> bool {
269        self.handles.contains(endpoint)
270    }
271
272    pub fn contains_target(&self, target: &HitTarget) -> bool {
273        match target {
274            HitTarget::Node(id) => self.contains_node(id),
275            HitTarget::Handle { node_id, handle_id } => self.contains_handle(&CanvasEndpoint {
276                node_id: node_id.clone(),
277                handle_id: Some(handle_id.clone()),
278            }),
279            HitTarget::Edge(id) => self.contains_edge(id),
280            HitTarget::Shape(id) => self.contains_shape(id),
281        }
282    }
283
284    pub fn insert_node(&mut self, id: NodeId) -> bool {
285        self.nodes.insert(id)
286    }
287
288    pub fn insert_edge(&mut self, id: EdgeId) -> bool {
289        self.edges.insert(id)
290    }
291
292    pub fn insert_shape(&mut self, id: ShapeId) -> bool {
293        self.shapes.insert(id)
294    }
295
296    pub fn insert_handle(&mut self, endpoint: CanvasEndpoint) -> bool {
297        self.handles.insert(endpoint)
298    }
299
300    pub fn insert_target(&mut self, target: HitTarget) -> bool {
301        match target {
302            HitTarget::Node(id) => self.insert_node(id),
303            HitTarget::Handle { node_id, handle_id } => self.insert_handle(CanvasEndpoint {
304                node_id,
305                handle_id: Some(handle_id),
306            }),
307            HitTarget::Edge(id) => self.insert_edge(id),
308            HitTarget::Shape(id) => self.insert_shape(id),
309        }
310    }
311
312    pub fn remove_node(&mut self, id: &NodeId) -> bool {
313        self.nodes.shift_remove(id)
314    }
315
316    pub fn remove_edge(&mut self, id: &EdgeId) -> bool {
317        self.edges.shift_remove(id)
318    }
319
320    pub fn remove_shape(&mut self, id: &ShapeId) -> bool {
321        self.shapes.shift_remove(id)
322    }
323
324    pub fn remove_handle(&mut self, endpoint: &CanvasEndpoint) -> bool {
325        self.handles.shift_remove(endpoint)
326    }
327
328    pub fn remove_target(&mut self, target: &HitTarget) -> bool {
329        match target {
330            HitTarget::Node(id) => self.remove_node(id),
331            HitTarget::Handle { node_id, handle_id } => self.remove_handle(&CanvasEndpoint {
332                node_id: node_id.clone(),
333                handle_id: Some(handle_id.clone()),
334            }),
335            HitTarget::Edge(id) => self.remove_edge(id),
336            HitTarget::Shape(id) => self.remove_shape(id),
337        }
338    }
339
340    pub fn toggle_target(&mut self, target: HitTarget) -> bool {
341        if self.contains_target(&target) {
342            self.remove_target(&target);
343            false
344        } else {
345            self.insert_target(target);
346            true
347        }
348    }
349
350    pub fn extend_selection(&mut self, selection: CanvasSelection) {
351        self.nodes.extend(selection.nodes);
352        self.edges.extend(selection.edges);
353        self.shapes.extend(selection.shapes);
354        self.handles.extend(selection.handles);
355    }
356
357    pub fn clear_shapes(&mut self) {
358        self.shapes.clear();
359    }
360
361    pub fn is_empty(&self) -> bool {
362        self.nodes.is_empty()
363            && self.edges.is_empty()
364            && self.shapes.is_empty()
365            && self.handles.is_empty()
366    }
367
368    pub fn selected_nodes(&self) -> impl Iterator<Item = &NodeId> {
369        self.nodes.iter()
370    }
371
372    pub fn selected_edges(&self) -> impl Iterator<Item = &EdgeId> {
373        self.edges.iter()
374    }
375
376    pub fn selected_shapes(&self) -> impl Iterator<Item = &ShapeId> {
377        self.shapes.iter()
378    }
379
380    pub fn selected_handles(&self) -> impl Iterator<Item = &CanvasEndpoint> {
381        self.handles.iter()
382    }
383
384    pub fn retain_document(&mut self, document: &CanvasDocument) {
385        self.nodes.retain(|id| document.contains_node(id));
386        self.edges.retain(|id| document.contains_edge(id));
387        self.shapes.retain(|id| document.contains_shape(id));
388        self.handles
389            .retain(|endpoint| document.validate_endpoint(endpoint).is_ok());
390    }
391
392    pub(crate) fn selected_records(&self) -> impl Iterator<Item = CanvasRecordId> + '_ {
393        self.selected_nodes()
394            .cloned()
395            .map(CanvasRecordId::Node)
396            .chain(self.selected_edges().cloned().map(CanvasRecordId::Edge))
397            .chain(self.selected_shapes().cloned().map(CanvasRecordId::Shape))
398    }
399
400    pub(crate) fn insert_record(&mut self, record_id: CanvasRecordId) -> bool {
401        match record_id {
402            CanvasRecordId::Node(id) => self.insert_node(id),
403            CanvasRecordId::Edge(id) => self.insert_edge(id),
404            CanvasRecordId::Shape(id) => self.insert_shape(id),
405        }
406    }
407}
408
409pub struct CanvasEditor {
410    store: CanvasStore,
411    session: CanvasToolSession,
412    connection_release: Option<CanvasConnectionRelease>,
413}
414
415impl Default for CanvasEditor {
416    fn default() -> Self {
417        Self::new(CanvasDocument::default())
418    }
419}
420
421impl CanvasEditor {
422    pub fn new(document: CanvasDocument) -> Self {
423        Self::new_with_router(document, CanvasDefaultEdgeRouter)
424    }
425
426    pub fn new_with_router<R>(document: CanvasDocument, edge_router: R) -> Self
427    where
428        R: CanvasEdgeRouter + Send + Sync + 'static,
429    {
430        Self {
431            store: CanvasStore::new_with_router(document, edge_router),
432            session: CanvasToolSession::default(),
433            connection_release: None,
434        }
435    }
436
437    pub fn try_new_with_kind_registry(
438        document: CanvasDocument,
439        kind_registry: CanvasKindRegistry,
440    ) -> Result<Self, DocumentError> {
441        Self::try_new_with_router_and_kind_registry(
442            document,
443            CanvasDefaultEdgeRouter,
444            kind_registry,
445        )
446    }
447
448    pub fn try_new_with_router_and_kind_registry<R>(
449        document: CanvasDocument,
450        edge_router: R,
451        kind_registry: CanvasKindRegistry,
452    ) -> Result<Self, DocumentError>
453    where
454        R: CanvasEdgeRouter + Send + Sync + 'static,
455    {
456        Ok(Self {
457            store: CanvasStore::try_new_with_router_and_kind_registry(
458                document,
459                edge_router,
460                kind_registry,
461            )?,
462            session: CanvasToolSession::default(),
463            connection_release: None,
464        })
465    }
466
467    pub fn apply(&mut self, command: DocumentCommand) -> Result<(), DocumentError> {
468        self.apply_transaction(CanvasTransaction::single(command))
469    }
470
471    pub fn apply_all(
472        &mut self,
473        commands: impl IntoIterator<Item = DocumentCommand>,
474    ) -> Result<(), DocumentError> {
475        self.apply_transaction(CanvasTransaction::new(commands))
476    }
477
478    pub fn document(&self) -> &CanvasDocument {
479        self.store.document()
480    }
481
482    pub fn viewport(&self) -> CanvasViewport {
483        self.session.viewport()
484    }
485
486    pub fn tool(&self) -> &CanvasTool {
487        self.session.tool()
488    }
489
490    pub(crate) fn state(&self) -> &ToolState {
491        self.session.state()
492    }
493
494    pub fn runtime(&self) -> &CanvasRuntime {
495        self.store.runtime()
496    }
497
498    pub(crate) fn document_snapshot(&self) -> Arc<CanvasDocument> {
499        self.store.document_snapshot()
500    }
501
502    pub(crate) fn runtime_snapshot(&self) -> Arc<CanvasRuntime> {
503        self.store.runtime_snapshot()
504    }
505
506    pub(crate) fn kind_registry_snapshot(&self) -> Arc<CanvasKindRegistry> {
507        self.store.kind_registry_snapshot()
508    }
509
510    pub(crate) fn session_snapshot(&self) -> CanvasToolSessionSnapshot {
511        self.session.snapshot()
512    }
513
514    pub fn edge_router(&self) -> &(dyn CanvasEdgeRouter + Send + Sync) {
515        self.store.edge_router()
516    }
517
518    pub fn kind_registry(&self) -> &CanvasKindRegistry {
519        self.store.kind_registry()
520    }
521
522    pub fn selection(&self) -> &CanvasSelection {
523        self.session.selection()
524    }
525
526    pub fn history(&self) -> &CanvasHistory {
527        self.store.history()
528    }
529
530    pub fn store(&self) -> &CanvasStore {
531        &self.store
532    }
533
534    pub(crate) fn store_mut(&mut self) -> &mut CanvasStore {
535        &mut self.store
536    }
537
538    pub fn listen(
539        &mut self,
540        listener: impl Fn(&CanvasStoreChange) + Send + Sync + 'static,
541    ) -> CanvasStoreListenerId {
542        self.store.listen(listener)
543    }
544
545    pub fn remove_listener(&mut self, id: CanvasStoreListenerId) -> bool {
546        self.store.remove_listener(id)
547    }
548
549    #[cfg(test)]
550    fn history_mut_for_test(&mut self) -> &mut CanvasHistory {
551        self.store.history_mut_for_test()
552    }
553
554    pub(crate) fn retain_selection_for_current_document(&mut self) {
555        let document = self.store.document_snapshot();
556        self.session
557            .retain_selection_for_document(document.as_ref());
558    }
559
560    pub fn apply_transaction(
561        &mut self,
562        transaction: CanvasTransaction,
563    ) -> Result<(), DocumentError> {
564        self.apply_transaction_with_diff(transaction).map(drop)
565    }
566
567    pub fn apply_transaction_with_diff(
568        &mut self,
569        transaction: CanvasTransaction,
570    ) -> Result<CanvasDocumentDiff, DocumentError> {
571        if transaction.is_empty() {
572            return Ok(CanvasDocumentDiff::default());
573        }
574
575        let diff = self.store.apply_transaction(transaction)?;
576        if !diff.is_empty() {
577            self.retain_selection_for_current_document();
578        }
579        Ok(diff)
580    }
581
582    pub(crate) fn apply_tool_effect(
583        &mut self,
584        effect: CanvasToolEffect,
585    ) -> Result<(), DocumentError> {
586        self.apply_editor_action(effect.into())
587    }
588
589    fn apply_editor_action(&mut self, action: CanvasEditorAction) -> Result<(), DocumentError> {
590        match action {
591            CanvasEditorAction::ApplyTransaction(transaction) => {
592                self.apply_transaction(transaction)?;
593            }
594            CanvasEditorAction::BeginGesture => {
595                self.begin_gesture();
596            }
597            CanvasEditorAction::UpdateGesture(transaction) => {
598                self.update_gesture(transaction)?;
599            }
600            CanvasEditorAction::CommitGesture => {
601                self.commit_gesture()?;
602            }
603            CanvasEditorAction::CancelGesture => {
604                self.cancel_gesture()?;
605            }
606            CanvasEditorAction::SetTool(tool) => {
607                self.set_tool(tool)?;
608            }
609            CanvasEditorAction::SetConnectionRelease(release) => {
610                self.connection_release = release;
611            }
612            CanvasEditorAction::Session(effect) => {
613                self.apply_session_effect(effect);
614            }
615        }
616
617        Ok(())
618    }
619
620    pub(crate) fn prepare_gesture_commit(
621        &self,
622    ) -> Result<Option<CanvasPreparedGestureCommit>, DocumentError> {
623        self.session
624            .prepare_gesture_commit(self.document(), self.kind_registry())
625    }
626
627    pub(crate) fn apply_prepared_gesture_store_change(
628        &mut self,
629        prepared: CanvasPreparedGestureCommit,
630    ) -> Option<CanvasStoreChange> {
631        self.session.clear_gesture();
632        let change = self.store.apply_prepared_gesture_commit(prepared)?;
633        self.retain_selection_for_current_document();
634        Some(change)
635    }
636
637    pub(crate) fn apply_tool_effects(
638        &mut self,
639        effects: impl IntoIterator<Item = CanvasToolEffect>,
640    ) -> Result<(), DocumentError> {
641        for effect in effects {
642            self.apply_tool_effect(effect)?;
643        }
644
645        Ok(())
646    }
647
648    fn apply_session_effect(&mut self, effect: CanvasToolSessionEffect) {
649        let document = self.store.document_snapshot();
650        self.session.apply_effect(effect, document.as_ref());
651    }
652
653    pub fn apply_tool_intent(&mut self, intent: CanvasToolIntent) -> Result<(), DocumentError> {
654        self.apply_editor_action(intent.into())
655    }
656
657    pub(crate) fn apply_custom_tool_intent(
658        &mut self,
659        intent: CanvasToolIntent,
660    ) -> Result<(), DocumentError> {
661        match intent {
662            CanvasToolIntent::ApplyTransaction(transaction) => {
663                if transaction.is_empty() {
664                    return Ok(());
665                }
666
667                self.apply_tool_effects([
668                    CanvasToolEffect::BeginGesture,
669                    CanvasToolEffect::UpdateGesture(transaction),
670                ])?;
671            }
672            CanvasToolIntent::CommitTransaction => {
673                self.apply_tool_effect(CanvasToolEffect::CommitGesture)?;
674            }
675            CanvasToolIntent::CancelTransaction => {
676                self.apply_tool_effect(CanvasToolEffect::CancelGesture)?;
677            }
678            intent => {
679                self.apply_tool_intent(intent)?;
680            }
681        }
682
683        Ok(())
684    }
685
686    pub fn undo(&mut self) -> Result<bool, DocumentError> {
687        let changed = self.store.undo()?;
688        if changed {
689            self.retain_selection_for_current_document();
690        }
691        Ok(changed)
692    }
693
694    pub fn redo(&mut self) -> Result<bool, DocumentError> {
695        let changed = self.store.redo()?;
696        if changed {
697            self.retain_selection_for_current_document();
698        }
699        Ok(changed)
700    }
701
702    pub fn rebuild_index(&mut self) {
703        self.rebuild_runtime();
704    }
705
706    pub fn rebuild_runtime(&mut self) {
707        self.store.rebuild_runtime();
708    }
709
710    pub fn set_edge_router<R>(&mut self, edge_router: R)
711    where
712        R: CanvasEdgeRouter + Send + Sync + 'static,
713    {
714        self.store.set_edge_router(edge_router);
715    }
716
717    pub fn set_kind_registry(
718        &mut self,
719        kind_registry: CanvasKindRegistry,
720    ) -> Result<(), DocumentError> {
721        self.store.set_kind_registry(kind_registry)?;
722        let document = self.store.document_snapshot();
723        self.session
724            .reset_for_kind_registry_change(document.as_ref());
725        Ok(())
726    }
727
728    pub fn set_tool(&mut self, tool: CanvasTool) -> Result<(), DocumentError> {
729        self.cancel_gesture()?;
730        self.session.set_tool(tool);
731        Ok(())
732    }
733
734    pub fn set_viewport(&mut self, viewport: CanvasViewport) {
735        self.session.set_viewport(viewport);
736    }
737
738    pub fn is_tool_state_idle(&self) -> bool {
739        matches!(self.state(), ToolState::Idle)
740    }
741
742    pub fn connection_drag_state(&self) -> Option<CanvasConnectionDragState> {
743        match self.state() {
744            ToolState::Connecting { source, current } => Some(CanvasConnectionDragState {
745                source: source.clone(),
746                current: *current,
747            }),
748            _ => None,
749        }
750    }
751
752    pub fn take_connection_release(&mut self) -> Option<CanvasConnectionRelease> {
753        self.connection_release.take()
754    }
755
756    pub fn tool_context(&self) -> CanvasToolContext<'_> {
757        CanvasToolContext {
758            document: self.document(),
759            viewport: self.session.viewport(),
760            tool: self.tool(),
761            runtime: self.runtime(),
762            edge_router: self.edge_router(),
763            kind_registry: self.kind_registry(),
764            selection: self.selection(),
765            history: self.history(),
766        }
767    }
768
769    pub(crate) fn reducer_context(&self) -> CanvasToolReducerContext<'_> {
770        CanvasToolReducerContext {
771            document: self.document(),
772            viewport: self.viewport(),
773            state: self.state(),
774            runtime: self.runtime(),
775            edge_router: self.edge_router(),
776            kind_registry: self.kind_registry(),
777            selection: self.selection(),
778        }
779    }
780
781    pub fn handle_event(&mut self, event: CanvasEvent) -> Result<(), DocumentError> {
782        let effects = self.event_effects(event)?;
783        self.apply_tool_effects(effects)
784    }
785
786    pub fn delete_selection(&mut self) -> Result<bool, DocumentError> {
787        let transaction = self.delete_selection_transaction();
788        if transaction.is_empty() {
789            return Ok(false);
790        }
791
792        self.apply_transaction(transaction)?;
793        Ok(true)
794    }
795
796    pub fn copy_selection(&self) -> Option<CanvasClipboardPayload> {
797        clipboard::copy_selection(self.document(), self.selection())
798    }
799
800    pub fn cut_selection(&mut self) -> Result<Option<CanvasClipboardPayload>, DocumentError> {
801        let Some(payload) = self.copy_selection() else {
802            return Ok(None);
803        };
804        self.delete_selection()?;
805        Ok(Some(payload))
806    }
807
808    pub fn paste_clipboard(
809        &mut self,
810        payload: &CanvasClipboardPayload,
811        offset: Point<Pixels>,
812    ) -> Result<bool, DocumentError> {
813        let pasted = clipboard::paste_clipboard(self.document(), payload, offset);
814        self.apply_paste_transaction(pasted)
815    }
816
817    pub fn duplicate_selection(&mut self, offset: Point<Pixels>) -> Result<bool, DocumentError> {
818        let Some(pasted) =
819            clipboard::duplicate_selection(self.document(), self.selection(), offset)
820        else {
821            return Ok(false);
822        };
823        self.apply_paste_transaction(pasted)
824    }
825
826    pub fn group_selection(&mut self, group_id: impl Into<ShapeId>) -> Result<bool, DocumentError> {
827        let Some(edit) =
828            group::group_selection_edit(self.document(), self.selection(), group_id.into())
829        else {
830            return Ok(false);
831        };
832        self.apply_group_edit(edit)
833    }
834
835    pub fn ungroup_selection(&mut self) -> Result<bool, DocumentError> {
836        let Some(edit) = group::ungroup_selection_edit(self.document(), self.selection()) else {
837            return Ok(false);
838        };
839        self.apply_group_edit(edit)
840    }
841
842    pub fn reorder_selection(
843        &mut self,
844        command: CanvasZOrderCommand,
845    ) -> Result<bool, DocumentError> {
846        let transaction = self.reorder_selection_transaction(command);
847        if transaction.is_empty() {
848            return Ok(false);
849        }
850
851        self.apply_transaction(transaction)?;
852        Ok(true)
853    }
854
855    pub(crate) fn event_effects(
856        &self,
857        event: CanvasEvent,
858    ) -> Result<Vec<CanvasToolEffect>, DocumentError> {
859        let Some(tool) = BuiltInCanvasTool::from_canvas_tool(self.tool()) else {
860            return Ok(Vec::new());
861        };
862        tool.handle_event(self.reducer_context(), event)
863    }
864
865    pub fn handle_event_with_custom_tool<T>(
866        &mut self,
867        event: CanvasEvent,
868        custom_tool: &mut T,
869    ) -> Result<(), DocumentError>
870    where
871        T: CanvasToolReducer + ?Sized,
872    {
873        if BuiltInCanvasTool::from_canvas_tool(self.tool()).is_some() {
874            let effects = self.event_effects(event)?;
875            self.apply_tool_effects(effects)
876        } else {
877            let intents = custom_tool.handle_event(self.tool_context(), event)?;
878            for intent in intents {
879                self.apply_custom_tool_intent(intent)?;
880            }
881
882            Ok(())
883        }
884    }
885
886    pub fn handle_event_with_tool_registry(
887        &mut self,
888        event: CanvasEvent,
889        registry: &mut CanvasToolRegistry,
890    ) -> Result<(), CanvasToolRegistryError> {
891        if let Some(tool_id) = self.tool().custom_id().cloned() {
892            let reducer = registry
893                .reducer_mut(&tool_id)
894                .ok_or_else(|| CanvasToolRegistryError::MissingTool(tool_id.clone()))?;
895            let intents = reducer.handle_event(self.tool_context(), event)?;
896            for intent in intents {
897                self.apply_custom_tool_intent(intent)?;
898            }
899        } else {
900            let effects = self.event_effects(event)?;
901            self.apply_tool_effects(effects)?;
902        }
903
904        Ok(())
905    }
906
907    fn begin_gesture(&mut self) {
908        let document = self.store.document_snapshot();
909        self.session.begin_gesture(document.as_ref());
910    }
911
912    fn update_gesture(
913        &mut self,
914        transaction: CanvasTransaction,
915    ) -> Result<CanvasDocumentDiff, DocumentError> {
916        if transaction.is_empty() {
917            return Ok(CanvasDocumentDiff::default());
918        }
919
920        let document = self.store.document_snapshot();
921        let implicit_gesture = self.session.begin_implicit_gesture(document.as_ref());
922        let diff = self.apply_transient_transaction(transaction)?;
923        if let Some(gesture) = implicit_gesture {
924            self.session.install_implicit_gesture(gesture);
925        }
926        Ok(diff)
927    }
928
929    fn apply_transient_transaction(
930        &mut self,
931        transaction: CanvasTransaction,
932    ) -> Result<CanvasDocumentDiff, DocumentError> {
933        let diff = self.store.apply_transient_transaction(transaction)?;
934        if !diff.is_empty() {
935            self.retain_selection_for_current_document();
936        }
937        Ok(diff)
938    }
939
940    fn commit_gesture(&mut self) -> Result<CanvasDocumentDiff, DocumentError> {
941        let Some(prepared) = self.prepare_gesture_commit()? else {
942            self.session.clear_gesture();
943            return Ok(CanvasDocumentDiff::default());
944        };
945        let Some(change) = self.apply_prepared_gesture_store_change(prepared) else {
946            return Ok(CanvasDocumentDiff::default());
947        };
948        Ok(change.diff().clone())
949    }
950
951    fn cancel_gesture(&mut self) -> Result<CanvasDocumentDiff, DocumentError> {
952        let Some(transaction) = self.session.cancel_gesture_transaction(self.document()) else {
953            return Ok(CanvasDocumentDiff::default());
954        };
955        let diff = self.apply_transient_transaction(transaction)?;
956        self.session.clear_gesture();
957        Ok(diff)
958    }
959
960    fn delete_selection_transaction(&self) -> CanvasTransaction {
961        self.reducer_context().delete_selection_transaction()
962    }
963
964    fn apply_paste_transaction(
965        &mut self,
966        pasted: CanvasPasteTransaction,
967    ) -> Result<bool, DocumentError> {
968        let Some((transaction, selection)) = clipboard::paste_transaction_parts(pasted) else {
969            return Ok(false);
970        };
971
972        self.apply_transaction(transaction)?;
973        let document = self.store.document_snapshot();
974        self.session.set_selection(selection, document.as_ref());
975        Ok(true)
976    }
977
978    fn apply_group_edit(&mut self, edit: group::CanvasGroupEdit) -> Result<bool, DocumentError> {
979        self.apply_transaction(edit.transaction)?;
980        let document = self.store.document_snapshot();
981        self.session
982            .set_selection(edit.selection, document.as_ref());
983        Ok(true)
984    }
985
986    fn reorder_selection_transaction(&self, command: CanvasZOrderCommand) -> CanvasTransaction {
987        z_order::reorder_selection_transaction(self.document(), self.selection(), command)
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use std::sync::{Arc, Mutex};
994
995    use super::*;
996    use crate::{
997        CanvasEdge, CanvasKindRegistry, CanvasNode, CanvasNodeGeometryPolicy, CanvasNodeHitTest,
998        CanvasNodeInteractionPolicy, CanvasNodeKind, CanvasNodeResizeProposal,
999        CanvasNodeSchemaPolicy, CanvasNodeTransformPolicy, CanvasRecordId, CanvasRecordKind,
1000        CanvasRecordScopeOptions, CanvasResizeHandle, CanvasRoutePath, CanvasRouteRequest,
1001        CanvasSchemaError, CanvasShape, CanvasTransformTarget, CanvasValue, HandleId, HitOptions,
1002        canvas_transform_handles,
1003        test_support::{connected_pair_fixture, document_fixture},
1004    };
1005    use open_gpui::{point, px, size};
1006    use serde_json::{Value, json};
1007
1008    #[derive(Default)]
1009    struct StampTool {
1010        calls: usize,
1011        last_tool_id: Option<CanvasToolId>,
1012        last_hit: Option<HitTarget>,
1013    }
1014
1015    impl CanvasToolReducer for StampTool {
1016        fn handle_event(
1017            &mut self,
1018            context: CanvasToolContext<'_>,
1019            event: CanvasEvent,
1020        ) -> Result<Vec<CanvasToolIntent>, DocumentError> {
1021            self.calls += 1;
1022            self.last_tool_id = context.active_custom_tool_id().cloned();
1023
1024            let CanvasEvent::PointerDown {
1025                position,
1026                button: PointerButton::Primary,
1027                ..
1028            } = event
1029            else {
1030                return Ok(Vec::new());
1031            };
1032
1033            self.last_hit = context
1034                .hit_test_view(position, HitOptions::default())
1035                .next()
1036                .map(|record| record.target.clone());
1037
1038            let node_id = NodeId::new(format!("stamp-{}", context.document().node_count()));
1039            let mut selection = CanvasSelection::default();
1040            selection.nodes.insert(node_id.clone());
1041
1042            Ok(vec![
1043                CanvasToolIntent::ApplyTransaction(CanvasTransaction::single(
1044                    DocumentCommand::InsertNode(CanvasNode::new(
1045                        node_id.clone(),
1046                        context.document_position(position),
1047                        size(px(20.0), px(20.0)),
1048                    )),
1049                )),
1050                CanvasToolIntent::SetSelection(selection),
1051                CanvasToolIntent::CommitTransaction,
1052            ])
1053        }
1054    }
1055
1056    struct RequiredTitleNodeKind;
1057
1058    impl CanvasNodeSchemaPolicy for RequiredTitleNodeKind {
1059        fn default_data(&self) -> CanvasValue {
1060            CanvasValue::from_iter([("title".to_string(), json!("Untitled"))])
1061        }
1062
1063        fn migrate_node(&self, node: &mut CanvasNode) -> Result<(), CanvasSchemaError> {
1064            if let Some(value) = node.data.remove("label") {
1065                node.data.insert("title".to_string(), value);
1066            }
1067            Ok(())
1068        }
1069
1070        fn validate_node(&self, node: &CanvasNode) -> Result<(), CanvasSchemaError> {
1071            match node.data.get("title") {
1072                Some(Value::String(title)) if !title.trim().is_empty() => Ok(()),
1073                Some(_) => Err(CanvasSchemaError::invalid_data(
1074                    CanvasRecordKind::Node,
1075                    node.id.clone(),
1076                    &node.kind,
1077                    "title must be a non-empty string",
1078                )),
1079                None => Err(CanvasSchemaError::missing_required_data(
1080                    CanvasRecordKind::Node,
1081                    node.id.clone(),
1082                    &node.kind,
1083                    "title",
1084                )),
1085            }
1086        }
1087    }
1088
1089    struct WideBoundsNodeKind;
1090
1091    impl CanvasNodeGeometryPolicy for WideBoundsNodeKind {
1092        fn node_bounds(&self, node: &CanvasNode) -> Option<Bounds<Pixels>> {
1093            Some(Bounds::new(
1094                node.position,
1095                size(node.size.width + px(30.0), node.size.height),
1096            ))
1097        }
1098    }
1099
1100    struct MinimumResizeNodeKind;
1101
1102    impl CanvasNodeTransformPolicy for MinimumResizeNodeKind {
1103        fn resize_node_bounds(
1104            &self,
1105            proposal: CanvasNodeResizeProposal<'_>,
1106        ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
1107            Ok(Bounds::new(
1108                proposal.bounds.origin,
1109                size(
1110                    proposal.bounds.size.width.max(px(64.0)),
1111                    proposal.bounds.size.height.max(px(48.0)),
1112                ),
1113            ))
1114        }
1115    }
1116
1117    struct RejectResizeNodeKind;
1118
1119    impl CanvasNodeTransformPolicy for RejectResizeNodeKind {
1120        fn resize_node_bounds(
1121            &self,
1122            proposal: CanvasNodeResizeProposal<'_>,
1123        ) -> Result<Bounds<Pixels>, CanvasSchemaError> {
1124            Err(CanvasSchemaError::invalid_data(
1125                CanvasRecordKind::Node,
1126                proposal.node.id.clone(),
1127                &proposal.node.kind,
1128                "resize is disabled",
1129            ))
1130        }
1131    }
1132
1133    struct RightHalfNodeKind;
1134
1135    impl CanvasNodeInteractionPolicy for RightHalfNodeKind {
1136        fn node_contains_point(&self, hit: CanvasNodeHitTest<'_>) -> Option<bool> {
1137            Some(hit.point.x >= hit.bounds.center().x)
1138        }
1139    }
1140
1141    struct WholeNodeEndpointKind;
1142
1143    impl CanvasNodeInteractionPolicy for WholeNodeEndpointKind {
1144        fn node_accepts_connection_endpoint(
1145            &self,
1146            _node: &CanvasNode,
1147            _role: CanvasConnectionEndpointRole,
1148        ) -> bool {
1149            true
1150        }
1151    }
1152
1153    fn required_title_node_kind() -> CanvasNodeKind {
1154        CanvasNodeKind::new().with_schema_policy(RequiredTitleNodeKind)
1155    }
1156
1157    fn wide_bounds_node_kind() -> CanvasNodeKind {
1158        CanvasNodeKind::new().with_geometry_policy(WideBoundsNodeKind)
1159    }
1160
1161    fn minimum_resize_node_kind() -> CanvasNodeKind {
1162        CanvasNodeKind::new().with_transform_policy(MinimumResizeNodeKind)
1163    }
1164
1165    fn reject_resize_node_kind() -> CanvasNodeKind {
1166        CanvasNodeKind::new().with_transform_policy(RejectResizeNodeKind)
1167    }
1168
1169    fn right_half_node_kind() -> CanvasNodeKind {
1170        CanvasNodeKind::new().with_interaction_policy(RightHalfNodeKind)
1171    }
1172
1173    fn whole_node_endpoint_kind() -> CanvasNodeKind {
1174        CanvasNodeKind::new().with_interaction_policy(WholeNodeEndpointKind)
1175    }
1176
1177    #[test]
1178    fn canvas_selection_adds_removes_and_toggles_targets() {
1179        let mut selection = CanvasSelection::default();
1180        let node = HitTarget::Node(NodeId::from("node"));
1181        let handle = HitTarget::Handle {
1182            node_id: NodeId::from("node"),
1183            handle_id: HandleId::from("handle"),
1184        };
1185        let edge = HitTarget::Edge(EdgeId::from("edge"));
1186        let shape = HitTarget::Shape(ShapeId::from("shape"));
1187
1188        assert!(selection.insert_target(node.clone()));
1189        assert!(!selection.insert_target(node.clone()));
1190        assert!(selection.contains_target(&node));
1191        assert!(!selection.toggle_target(node.clone()));
1192        assert!(!selection.contains_target(&node));
1193
1194        assert!(selection.toggle_target(handle.clone()));
1195        assert!(selection.insert_target(edge.clone()));
1196        assert!(selection.insert_target(shape.clone()));
1197        assert!(selection.contains_target(&handle));
1198        assert!(selection.remove_target(&edge));
1199        assert!(!selection.contains_target(&edge));
1200        assert!(selection.contains_target(&shape));
1201    }
1202
1203    #[test]
1204    fn select_tool_translates_node() {
1205        let document = document_fixture()
1206            .node(CanvasNode::new(
1207                "n1",
1208                point(px(0.0), px(0.0)),
1209                size(px(100.0), px(100.0)),
1210            ))
1211            .build();
1212        let mut editor = CanvasEditor::new(document);
1213
1214        editor
1215            .handle_event(CanvasEvent::PointerDown {
1216                position: point(px(10.0), px(10.0)),
1217                button: PointerButton::Primary,
1218                modifiers: CanvasKeyModifiers::default(),
1219            })
1220            .unwrap();
1221        editor
1222            .handle_event(CanvasEvent::PointerMove {
1223                position: point(px(20.0), px(25.0)),
1224                modifiers: CanvasKeyModifiers::default(),
1225            })
1226            .unwrap();
1227        editor
1228            .handle_event(CanvasEvent::PointerUp {
1229                position: point(px(20.0), px(25.0)),
1230                button: PointerButton::Primary,
1231                modifiers: CanvasKeyModifiers::default(),
1232            })
1233            .unwrap();
1234
1235        let node = editor.document().node(&NodeId::from("n1")).unwrap();
1236        assert_eq!(node.position, point(px(10.0), px(15.0)));
1237        assert_eq!(
1238            editor
1239                .session
1240                .selection
1241                .nodes
1242                .iter()
1243                .cloned()
1244                .collect::<Vec<_>>(),
1245            vec![NodeId::from("n1")]
1246        );
1247        assert_eq!(editor.session.state, ToolState::Idle);
1248        assert_eq!(editor.history().undo_depth(), 1);
1249
1250        assert!(editor.undo().unwrap());
1251        let node = editor.document().node(&NodeId::from("n1")).unwrap();
1252        assert_eq!(node.position, point(px(0.0), px(0.0)));
1253        assert_eq!(editor.history().redo_depth(), 1);
1254
1255        assert!(editor.redo().unwrap());
1256        let node = editor.document().node(&NodeId::from("n1")).unwrap();
1257        assert_eq!(node.position, point(px(10.0), px(15.0)));
1258    }
1259
1260    #[test]
1261    fn select_tool_waits_for_drag_threshold_before_translating_node() {
1262        let document = document_fixture()
1263            .node(CanvasNode::new(
1264                "n1",
1265                point(px(0.0), px(0.0)),
1266                size(px(100.0), px(100.0)),
1267            ))
1268            .build();
1269        let mut editor = CanvasEditor::new(document);
1270
1271        editor
1272            .handle_event(CanvasEvent::PointerDown {
1273                position: point(px(10.0), px(10.0)),
1274                button: PointerButton::Primary,
1275                modifiers: CanvasKeyModifiers::default(),
1276            })
1277            .unwrap();
1278
1279        assert!(!editor.is_tool_state_idle());
1280        assert!(matches!(
1281            editor.session.state,
1282            ToolState::PendingTranslation { .. }
1283        ));
1284        assert_eq!(editor.history().undo_depth(), 0);
1285
1286        editor
1287            .handle_event(CanvasEvent::PointerMove {
1288                position: point(px(12.0), px(12.0)),
1289                modifiers: CanvasKeyModifiers::default(),
1290            })
1291            .unwrap();
1292
1293        assert!(matches!(
1294            editor.session.state,
1295            ToolState::PendingTranslation { .. }
1296        ));
1297        assert_eq!(
1298            editor
1299                .document()
1300                .node(&NodeId::from("n1"))
1301                .unwrap()
1302                .position,
1303            point(px(0.0), px(0.0))
1304        );
1305        assert_eq!(editor.history().undo_depth(), 0);
1306
1307        editor
1308            .handle_event(CanvasEvent::PointerUp {
1309                position: point(px(12.0), px(12.0)),
1310                button: PointerButton::Primary,
1311                modifiers: CanvasKeyModifiers::default(),
1312            })
1313            .unwrap();
1314
1315        assert_eq!(editor.session.state, ToolState::Idle);
1316        assert_eq!(editor.history().undo_depth(), 0);
1317        assert_eq!(
1318            editor
1319                .session
1320                .selection
1321                .nodes
1322                .iter()
1323                .cloned()
1324                .collect::<Vec<_>>(),
1325            vec![NodeId::from("n1")]
1326        );
1327    }
1328
1329    #[test]
1330    fn select_tool_cancel_pending_translation_restores_base_selection() {
1331        let document = document_fixture()
1332            .node(CanvasNode::new(
1333                "base",
1334                point(px(120.0), px(0.0)),
1335                size(px(100.0), px(100.0)),
1336            ))
1337            .node(CanvasNode::new(
1338                "next",
1339                point(px(0.0), px(0.0)),
1340                size(px(100.0), px(100.0)),
1341            ))
1342            .build();
1343        let mut editor = CanvasEditor::new(document);
1344        editor.session.selection.nodes.insert(NodeId::from("base"));
1345
1346        editor
1347            .handle_event(CanvasEvent::PointerDown {
1348                position: point(px(10.0), px(10.0)),
1349                button: PointerButton::Primary,
1350                modifiers: CanvasKeyModifiers::default(),
1351            })
1352            .unwrap();
1353        assert!(matches!(
1354            editor.session.state,
1355            ToolState::PendingTranslation { .. }
1356        ));
1357
1358        editor.handle_event(CanvasEvent::Cancel).unwrap();
1359
1360        assert_eq!(editor.session.state, ToolState::Idle);
1361        assert_eq!(
1362            editor
1363                .session
1364                .selection
1365                .nodes
1366                .iter()
1367                .cloned()
1368                .collect::<Vec<_>>(),
1369            vec![NodeId::from("base")]
1370        );
1371        assert_eq!(editor.history().undo_depth(), 0);
1372    }
1373
1374    #[test]
1375    fn select_tool_translates_shape() {
1376        let document = document_fixture()
1377            .shape(CanvasShape::new(
1378                "shape",
1379                Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(80.0))),
1380            ))
1381            .build();
1382        let mut editor = CanvasEditor::new(document);
1383
1384        editor
1385            .handle_event(CanvasEvent::PointerDown {
1386                position: point(px(10.0), px(10.0)),
1387                button: PointerButton::Primary,
1388                modifiers: CanvasKeyModifiers::default(),
1389            })
1390            .unwrap();
1391        editor
1392            .handle_event(CanvasEvent::PointerMove {
1393                position: point(px(30.0), px(25.0)),
1394                modifiers: CanvasKeyModifiers::default(),
1395            })
1396            .unwrap();
1397        editor
1398            .handle_event(CanvasEvent::PointerUp {
1399                position: point(px(30.0), px(25.0)),
1400                button: PointerButton::Primary,
1401                modifiers: CanvasKeyModifiers::default(),
1402            })
1403            .unwrap();
1404
1405        let shape = editor.document().shape(&ShapeId::from("shape")).unwrap();
1406        assert_eq!(shape.bounds.origin, point(px(20.0), px(15.0)));
1407        assert_eq!(
1408            editor
1409                .session
1410                .selection
1411                .shapes
1412                .iter()
1413                .cloned()
1414                .collect::<Vec<_>>(),
1415            vec![ShapeId::from("shape")]
1416        );
1417        assert_eq!(editor.history().undo_depth(), 1);
1418
1419        assert!(editor.undo().unwrap());
1420        let shape = editor.document().shape(&ShapeId::from("shape")).unwrap();
1421        assert_eq!(shape.bounds.origin, point(px(0.0), px(0.0)));
1422    }
1423
1424    #[test]
1425    fn select_tool_ignores_locked_node_hits() {
1426        let mut node = CanvasNode::new(
1427            "locked",
1428            point(px(0.0), px(0.0)),
1429            size(px(100.0), px(100.0)),
1430        );
1431        node.locked = true;
1432        let document = document_fixture().node(node).build();
1433        let mut editor = CanvasEditor::new(document);
1434
1435        editor
1436            .handle_event(CanvasEvent::PointerDown {
1437                position: point(px(10.0), px(10.0)),
1438                button: PointerButton::Primary,
1439                modifiers: CanvasKeyModifiers::default(),
1440            })
1441            .unwrap();
1442        editor
1443            .handle_event(CanvasEvent::PointerMove {
1444                position: point(px(30.0), px(30.0)),
1445                modifiers: CanvasKeyModifiers::default(),
1446            })
1447            .unwrap();
1448        editor
1449            .handle_event(CanvasEvent::PointerUp {
1450                position: point(px(30.0), px(30.0)),
1451                button: PointerButton::Primary,
1452                modifiers: CanvasKeyModifiers::default(),
1453            })
1454            .unwrap();
1455
1456        assert!(editor.session.selection.is_empty());
1457        assert_eq!(
1458            editor
1459                .document()
1460                .node(&NodeId::from("locked"))
1461                .unwrap()
1462                .position,
1463            point(px(0.0), px(0.0))
1464        );
1465        assert_eq!(editor.history().undo_depth(), 0);
1466    }
1467
1468    #[test]
1469    fn select_tool_clears_selection_when_canvas_is_pressed() {
1470        let document = document_fixture()
1471            .node(CanvasNode::new(
1472                "n1",
1473                point(px(0.0), px(0.0)),
1474                size(px(100.0), px(100.0)),
1475            ))
1476            .build();
1477        let mut editor = CanvasEditor::new(document);
1478
1479        editor
1480            .handle_event(CanvasEvent::PointerDown {
1481                position: point(px(10.0), px(10.0)),
1482                button: PointerButton::Primary,
1483                modifiers: CanvasKeyModifiers::default(),
1484            })
1485            .unwrap();
1486        assert!(!editor.session.selection.is_empty());
1487
1488        editor
1489            .handle_event(CanvasEvent::PointerUp {
1490                position: point(px(10.0), px(10.0)),
1491                button: PointerButton::Primary,
1492                modifiers: CanvasKeyModifiers::default(),
1493            })
1494            .unwrap();
1495        editor
1496            .handle_event(CanvasEvent::PointerDown {
1497                position: point(px(300.0), px(300.0)),
1498                button: PointerButton::Primary,
1499                modifiers: CanvasKeyModifiers::default(),
1500            })
1501            .unwrap();
1502
1503        assert!(editor.session.selection.is_empty());
1504    }
1505
1506    #[test]
1507    fn select_tool_cancel_restores_selection_after_canvas_press() {
1508        let document = document_fixture()
1509            .node(CanvasNode::new(
1510                "base",
1511                point(px(0.0), px(0.0)),
1512                size(px(100.0), px(100.0)),
1513            ))
1514            .build();
1515        let mut editor = CanvasEditor::new(document);
1516        editor.session.selection.nodes.insert(NodeId::from("base"));
1517
1518        editor
1519            .handle_event(CanvasEvent::PointerDown {
1520                position: point(px(300.0), px(300.0)),
1521                button: PointerButton::Primary,
1522                modifiers: CanvasKeyModifiers::default(),
1523            })
1524            .unwrap();
1525
1526        assert!(editor.session.selection.is_empty());
1527
1528        editor.handle_event(CanvasEvent::Cancel).unwrap();
1529
1530        assert_eq!(
1531            editor
1532                .session
1533                .selection
1534                .nodes
1535                .iter()
1536                .cloned()
1537                .collect::<Vec<_>>(),
1538            vec![NodeId::from("base")]
1539        );
1540        assert_eq!(editor.session.state, ToolState::Idle);
1541    }
1542
1543    #[test]
1544    fn select_tool_cancel_clears_selection_when_idle() {
1545        let document = document_fixture()
1546            .node(CanvasNode::new(
1547                "base",
1548                point(px(0.0), px(0.0)),
1549                size(px(100.0), px(100.0)),
1550            ))
1551            .build();
1552        let mut editor = CanvasEditor::new(document);
1553        editor.session.selection.nodes.insert(NodeId::from("base"));
1554
1555        editor.handle_event(CanvasEvent::Cancel).unwrap();
1556
1557        assert!(editor.session.selection.is_empty());
1558        assert_eq!(editor.session.state, ToolState::Idle);
1559        assert_eq!(editor.history().undo_depth(), 0);
1560    }
1561
1562    #[test]
1563    fn select_tool_shift_click_toggles_selection() {
1564        let document = document_fixture()
1565            .node(CanvasNode::new(
1566                "a",
1567                point(px(0.0), px(0.0)),
1568                size(px(100.0), px(100.0)),
1569            ))
1570            .node(CanvasNode::new(
1571                "b",
1572                point(px(200.0), px(0.0)),
1573                size(px(100.0), px(100.0)),
1574            ))
1575            .build();
1576        let mut editor = CanvasEditor::new(document);
1577        editor.session.selection.nodes.insert(NodeId::from("a"));
1578
1579        editor
1580            .handle_event(CanvasEvent::PointerDown {
1581                position: point(px(210.0), px(10.0)),
1582                button: PointerButton::Primary,
1583                modifiers: CanvasKeyModifiers {
1584                    shift: true,
1585                    ..CanvasKeyModifiers::default()
1586                },
1587            })
1588            .unwrap();
1589
1590        assert_eq!(
1591            editor
1592                .session
1593                .selection
1594                .nodes
1595                .iter()
1596                .cloned()
1597                .collect::<Vec<_>>(),
1598            vec![NodeId::from("a"), NodeId::from("b")]
1599        );
1600        assert_eq!(editor.session.state, ToolState::Idle);
1601        assert_eq!(editor.history().undo_depth(), 0);
1602
1603        editor
1604            .handle_event(CanvasEvent::PointerDown {
1605                position: point(px(210.0), px(10.0)),
1606                button: PointerButton::Primary,
1607                modifiers: CanvasKeyModifiers {
1608                    shift: true,
1609                    ..CanvasKeyModifiers::default()
1610                },
1611            })
1612            .unwrap();
1613
1614        assert_eq!(
1615            editor
1616                .session
1617                .selection
1618                .nodes
1619                .iter()
1620                .cloned()
1621                .collect::<Vec<_>>(),
1622            vec![NodeId::from("a")]
1623        );
1624        assert_eq!(editor.session.state, ToolState::Idle);
1625        assert_eq!(editor.history().undo_depth(), 0);
1626    }
1627
1628    #[test]
1629    fn select_tool_uses_registered_precise_hit_policy() {
1630        let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
1631        node.kind = "right-half".to_string();
1632        let document = document_fixture().node(node).build();
1633        let mut registry = CanvasKindRegistry::open();
1634        registry.register_node_kind("right-half", right_half_node_kind());
1635        let mut editor =
1636            CanvasEditor::try_new_with_kind_registry(document.clone(), registry.clone()).unwrap();
1637
1638        editor
1639            .handle_event(CanvasEvent::PointerDown {
1640                position: point(px(25.0), px(25.0)),
1641                button: PointerButton::Primary,
1642                modifiers: CanvasKeyModifiers::default(),
1643            })
1644            .unwrap();
1645
1646        assert!(editor.session.selection.is_empty());
1647        assert!(
1648            editor
1649                .runtime()
1650                .hit_test(point(px(25.0), px(25.0)), HitOptions::default())
1651                .next()
1652                .is_some()
1653        );
1654        assert!(
1655            editor
1656                .runtime()
1657                .precise_hit_test_with_kind_registry(
1658                    editor.document(),
1659                    editor.kind_registry(),
1660                    point(px(25.0), px(25.0)),
1661                    HitOptions::default(),
1662                )
1663                .next()
1664                .is_none()
1665        );
1666
1667        let mut editor = CanvasEditor::try_new_with_kind_registry(document, registry).unwrap();
1668        editor
1669            .handle_event(CanvasEvent::PointerDown {
1670                position: point(px(75.0), px(25.0)),
1671                button: PointerButton::Primary,
1672                modifiers: CanvasKeyModifiers::default(),
1673            })
1674            .unwrap();
1675
1676        assert_eq!(
1677            editor
1678                .session
1679                .selection
1680                .nodes
1681                .iter()
1682                .cloned()
1683                .collect::<Vec<_>>(),
1684            vec![NodeId::from("a")]
1685        );
1686    }
1687
1688    #[test]
1689    fn select_tool_delete_key_removes_selected_records() {
1690        let document = document_fixture()
1691            .node(CanvasNode::new(
1692                "a",
1693                point(px(0.0), px(0.0)),
1694                size(px(100.0), px(100.0)),
1695            ))
1696            .node(CanvasNode::new(
1697                "b",
1698                point(px(200.0), px(0.0)),
1699                size(px(100.0), px(100.0)),
1700            ))
1701            .edge(CanvasEdge::new(
1702                "a-b",
1703                CanvasEndpoint::new("a", None::<&str>),
1704                CanvasEndpoint::new("b", None::<&str>),
1705            ))
1706            .shape(CanvasShape::new(
1707                "shape",
1708                Bounds::new(point(px(0.0), px(200.0)), size(px(40.0), px(40.0))),
1709            ))
1710            .build();
1711        let mut editor = CanvasEditor::new(document);
1712        editor.session.selection.nodes.insert(NodeId::from("a"));
1713        editor.session.selection.edges.insert(EdgeId::from("a-b"));
1714        editor
1715            .session
1716            .selection
1717            .shapes
1718            .insert(ShapeId::from("shape"));
1719
1720        editor
1721            .handle_event(CanvasEvent::KeyDown {
1722                key: CanvasKey::Delete,
1723                modifiers: CanvasKeyModifiers::default(),
1724                repeat: false,
1725            })
1726            .unwrap();
1727
1728        assert!(!editor.document().contains_node(&NodeId::from("a")));
1729        assert!(editor.document().contains_node(&NodeId::from("b")));
1730        assert!(editor.document().edge_count() == 0);
1731        assert!(editor.document().shape_count() == 0);
1732        assert!(editor.session.selection.is_empty());
1733        assert_eq!(editor.history().undo_depth(), 1);
1734
1735        assert!(editor.undo().unwrap());
1736        assert!(editor.document().contains_node(&NodeId::from("a")));
1737        assert!(editor.document().contains_edge(&EdgeId::from("a-b")));
1738        assert!(editor.document().contains_shape(&ShapeId::from("shape")));
1739    }
1740
1741    #[test]
1742    fn select_tool_delete_key_removes_related_descendants() {
1743        let mut document = document_fixture()
1744            .shape(CanvasShape::new(
1745                "frame",
1746                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
1747            ))
1748            .shape(CanvasShape::new(
1749                "group",
1750                Bounds::new(point(px(40.0), px(0.0)), size(px(50.0), px(50.0))),
1751            ))
1752            .node(CanvasNode::new(
1753                "leaf",
1754                point(px(60.0), px(0.0)),
1755                size(px(20.0), px(20.0)),
1756            ))
1757            .node(CanvasNode::new(
1758                "outside",
1759                point(px(260.0), px(0.0)),
1760                size(px(20.0), px(20.0)),
1761            ))
1762            .edge(CanvasEdge::new(
1763                "leaf-outside",
1764                CanvasEndpoint::new("leaf", None::<&str>),
1765                CanvasEndpoint::new("outside", None::<&str>),
1766            ))
1767            .build();
1768        document
1769            .apply_transaction(CanvasTransaction::new([
1770                DocumentCommand::SetRecordParent {
1771                    child: CanvasRecordId::Shape(ShapeId::from("group")),
1772                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
1773                },
1774                DocumentCommand::AddRecordToGroup {
1775                    group: CanvasRecordId::Shape(ShapeId::from("group")),
1776                    member: CanvasRecordId::Node(NodeId::from("leaf")),
1777                },
1778            ]))
1779            .unwrap();
1780        let mut editor = CanvasEditor::new(document);
1781        editor
1782            .session
1783            .selection
1784            .shapes
1785            .insert(ShapeId::from("frame"));
1786
1787        editor
1788            .handle_event(CanvasEvent::KeyDown {
1789                key: CanvasKey::Delete,
1790                modifiers: CanvasKeyModifiers::default(),
1791                repeat: false,
1792            })
1793            .unwrap();
1794
1795        assert!(!editor.document().contains_shape(&ShapeId::from("frame")));
1796        assert!(!editor.document().contains_shape(&ShapeId::from("group")));
1797        assert!(!editor.document().contains_node(&NodeId::from("leaf")));
1798        assert!(editor.document().contains_node(&NodeId::from("outside")));
1799        assert!(
1800            !editor
1801                .document()
1802                .contains_edge(&EdgeId::from("leaf-outside"))
1803        );
1804        assert!(editor.document().relations().is_empty());
1805        assert!(editor.session.selection.is_empty());
1806
1807        assert!(editor.undo().unwrap());
1808        let group = CanvasRecordId::Shape(ShapeId::from("group"));
1809        let frame = CanvasRecordId::Shape(ShapeId::from("frame"));
1810        let leaf = CanvasRecordId::Node(NodeId::from("leaf"));
1811        assert!(editor.document().contains_shape(&ShapeId::from("frame")));
1812        assert!(editor.document().contains_shape(&ShapeId::from("group")));
1813        assert!(editor.document().contains_node(&NodeId::from("leaf")));
1814        assert!(
1815            editor
1816                .document()
1817                .contains_edge(&EdgeId::from("leaf-outside"))
1818        );
1819        assert_eq!(
1820            editor.document().relations().parent_of(&group),
1821            Some(&frame)
1822        );
1823        assert_eq!(
1824            editor
1825                .document()
1826                .relations()
1827                .members_of(&group)
1828                .cloned()
1829                .collect::<Vec<_>>(),
1830            vec![leaf]
1831        );
1832    }
1833
1834    #[test]
1835    fn select_tool_delete_key_skips_locked_selected_records() {
1836        let mut locked_node = CanvasNode::new(
1837            "locked-node",
1838            point(px(0.0), px(0.0)),
1839            size(px(100.0), px(100.0)),
1840        );
1841        locked_node.locked = true;
1842        let mut locked_edge = CanvasEdge::new(
1843            "locked-edge",
1844            CanvasEndpoint::new("a", None::<&str>),
1845            CanvasEndpoint::new("b", None::<&str>),
1846        );
1847        locked_edge.locked = true;
1848        let mut locked_shape = CanvasShape::new(
1849            "locked-shape",
1850            Bounds::new(point(px(0.0), px(200.0)), size(px(40.0), px(40.0))),
1851        );
1852        locked_shape.locked = true;
1853        let document = document_fixture()
1854            .node(locked_node)
1855            .node(CanvasNode::new(
1856                "a",
1857                point(px(200.0), px(0.0)),
1858                size(px(100.0), px(100.0)),
1859            ))
1860            .node(CanvasNode::new(
1861                "b",
1862                point(px(400.0), px(0.0)),
1863                size(px(100.0), px(100.0)),
1864            ))
1865            .edge(locked_edge)
1866            .shape(locked_shape)
1867            .build();
1868        let mut editor = CanvasEditor::new(document);
1869        editor
1870            .session
1871            .selection
1872            .nodes
1873            .insert(NodeId::from("locked-node"));
1874        editor
1875            .session
1876            .selection
1877            .edges
1878            .insert(EdgeId::from("locked-edge"));
1879        editor
1880            .session
1881            .selection
1882            .shapes
1883            .insert(ShapeId::from("locked-shape"));
1884
1885        editor
1886            .handle_event(CanvasEvent::KeyDown {
1887                key: CanvasKey::Backspace,
1888                modifiers: CanvasKeyModifiers::default(),
1889                repeat: false,
1890            })
1891            .unwrap();
1892
1893        assert!(
1894            editor
1895                .document()
1896                .contains_node(&NodeId::from("locked-node"))
1897        );
1898        assert!(
1899            editor
1900                .document()
1901                .contains_edge(&EdgeId::from("locked-edge"))
1902        );
1903        assert!(
1904            editor
1905                .document()
1906                .contains_shape(&ShapeId::from("locked-shape"))
1907        );
1908        assert_eq!(editor.history().undo_depth(), 0);
1909    }
1910
1911    #[test]
1912    fn editor_duplicate_selection_remaps_internal_edges_and_selects_paste() {
1913        let mut document = document_fixture()
1914            .node(CanvasNode::new(
1915                "a",
1916                point(px(0.0), px(0.0)),
1917                size(px(100.0), px(100.0)),
1918            ))
1919            .node(CanvasNode::new(
1920                "b",
1921                point(px(200.0), px(0.0)),
1922                size(px(100.0), px(100.0)),
1923            ))
1924            .node(CanvasNode::new(
1925                "outside",
1926                point(px(400.0), px(0.0)),
1927                size(px(100.0), px(100.0)),
1928            ))
1929            .edge(CanvasEdge::new(
1930                "a-b",
1931                CanvasEndpoint::new("a", None::<&str>),
1932                CanvasEndpoint::new("b", None::<&str>),
1933            ))
1934            .edge(CanvasEdge::new(
1935                "a-outside",
1936                CanvasEndpoint::new("a", None::<&str>),
1937                CanvasEndpoint::new("outside", None::<&str>),
1938            ))
1939            .shape(CanvasShape::new(
1940                "frame",
1941                Bounds::new(point(px(-20.0), px(-20.0)), size(px(360.0), px(160.0))),
1942            ))
1943            .build();
1944        document
1945            .apply_transaction(CanvasTransaction::new([
1946                DocumentCommand::SetRecordParent {
1947                    child: CanvasRecordId::Node(NodeId::from("a")),
1948                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
1949                },
1950                DocumentCommand::AddRecordToGroup {
1951                    group: CanvasRecordId::Shape(ShapeId::from("frame")),
1952                    member: CanvasRecordId::Node(NodeId::from("a")),
1953                },
1954            ]))
1955            .unwrap();
1956        let mut editor = CanvasEditor::new(document);
1957        editor.session.selection.nodes.insert(NodeId::from("a"));
1958        editor.session.selection.nodes.insert(NodeId::from("b"));
1959        editor
1960            .session
1961            .selection
1962            .shapes
1963            .insert(ShapeId::from("frame"));
1964
1965        assert!(
1966            editor
1967                .duplicate_selection(point(px(20.0), px(30.0)))
1968                .unwrap()
1969        );
1970
1971        assert!(editor.document().contains_node(&NodeId::from("a-copy")));
1972        assert!(editor.document().contains_node(&NodeId::from("b-copy")));
1973        assert!(editor.document().contains_edge(&EdgeId::from("a-b-copy")));
1974        assert!(
1975            editor
1976                .document()
1977                .contains_shape(&ShapeId::from("frame-copy"))
1978        );
1979        assert!(
1980            !editor
1981                .document()
1982                .contains_edge(&EdgeId::from("a-outside-copy"))
1983        );
1984        assert_eq!(
1985            editor
1986                .document()
1987                .node(&NodeId::from("a-copy"))
1988                .unwrap()
1989                .position,
1990            point(px(20.0), px(30.0))
1991        );
1992        assert_eq!(
1993            editor
1994                .session
1995                .selection
1996                .nodes
1997                .iter()
1998                .cloned()
1999                .collect::<Vec<_>>(),
2000            vec![NodeId::from("b-copy")]
2001        );
2002        assert!(editor.session.selection.edges.is_empty());
2003        assert_eq!(
2004            editor
2005                .session
2006                .selection
2007                .shapes
2008                .iter()
2009                .cloned()
2010                .collect::<Vec<_>>(),
2011            vec![ShapeId::from("frame-copy")]
2012        );
2013        let copied_node = CanvasRecordId::Node(NodeId::from("a-copy"));
2014        let copied_frame = CanvasRecordId::Shape(ShapeId::from("frame-copy"));
2015        assert_eq!(
2016            editor.document().relations().parent_of(&copied_node),
2017            Some(&copied_frame)
2018        );
2019        assert_eq!(
2020            editor
2021                .document()
2022                .relations()
2023                .members_of(&copied_frame)
2024                .cloned()
2025                .collect::<Vec<_>>(),
2026            vec![copied_node]
2027        );
2028        assert_eq!(editor.history().undo_depth(), 1);
2029        assert!(
2030            editor
2031                .runtime()
2032                .hit_test(point(px(25.0), px(35.0)), HitOptions::default())
2033                .any(|record| record.target == HitTarget::Node(NodeId::from("a-copy")))
2034        );
2035
2036        assert!(editor.undo().unwrap());
2037        assert!(!editor.document().contains_node(&NodeId::from("a-copy")));
2038        assert!(!editor.document().contains_edge(&EdgeId::from("a-b-copy")));
2039        assert!(
2040            !editor
2041                .document()
2042                .contains_shape(&ShapeId::from("frame-copy"))
2043        );
2044    }
2045
2046    #[test]
2047    fn editor_cut_and_paste_selection_use_command_path() {
2048        let document = document_fixture()
2049            .node(CanvasNode::new(
2050                "a",
2051                point(px(0.0), px(0.0)),
2052                size(px(100.0), px(100.0)),
2053            ))
2054            .shape(CanvasShape::new(
2055                "shape",
2056                Bounds::new(point(px(0.0), px(200.0)), size(px(40.0), px(40.0))),
2057            ))
2058            .build();
2059        let mut editor = CanvasEditor::new(document);
2060        editor.session.selection.nodes.insert(NodeId::from("a"));
2061        editor
2062            .session
2063            .selection
2064            .shapes
2065            .insert(ShapeId::from("shape"));
2066
2067        let payload = editor.cut_selection().unwrap().unwrap();
2068        assert!(!editor.document().contains_node(&NodeId::from("a")));
2069        assert!(!editor.document().contains_shape(&ShapeId::from("shape")));
2070        assert!(editor.session.selection.is_empty());
2071        assert_eq!(editor.history().undo_depth(), 1);
2072
2073        assert!(
2074            editor
2075                .paste_clipboard(&payload, point(px(10.0), px(20.0)))
2076                .unwrap()
2077        );
2078        assert!(editor.document().contains_node(&NodeId::from("a-copy")));
2079        assert!(
2080            editor
2081                .document()
2082                .contains_shape(&ShapeId::from("shape-copy"))
2083        );
2084        assert_eq!(
2085            editor
2086                .session
2087                .selection
2088                .nodes
2089                .iter()
2090                .cloned()
2091                .collect::<Vec<_>>(),
2092            vec![NodeId::from("a-copy")]
2093        );
2094        assert_eq!(
2095            editor
2096                .session
2097                .selection
2098                .shapes
2099                .iter()
2100                .cloned()
2101                .collect::<Vec<_>>(),
2102            vec![ShapeId::from("shape-copy")]
2103        );
2104        assert_eq!(editor.history().undo_depth(), 2);
2105    }
2106
2107    #[test]
2108    fn editor_groups_selection_with_internal_edges_and_selects_group() {
2109        let document = document_fixture()
2110            .node(CanvasNode::new(
2111                "a",
2112                point(px(0.0), px(0.0)),
2113                size(px(100.0), px(100.0)),
2114            ))
2115            .node(CanvasNode::new(
2116                "b",
2117                point(px(200.0), px(0.0)),
2118                size(px(100.0), px(100.0)),
2119            ))
2120            .node(CanvasNode::new(
2121                "outside",
2122                point(px(400.0), px(0.0)),
2123                size(px(100.0), px(100.0)),
2124            ))
2125            .edge(CanvasEdge::new(
2126                "a-b",
2127                CanvasEndpoint::new("a", None::<&str>),
2128                CanvasEndpoint::new("b", None::<&str>),
2129            ))
2130            .edge(CanvasEdge::new(
2131                "a-outside",
2132                CanvasEndpoint::new("a", None::<&str>),
2133                CanvasEndpoint::new("outside", None::<&str>),
2134            ))
2135            .shape(CanvasShape::new(
2136                "shape",
2137                Bounds::new(point(px(50.0), px(160.0)), size(px(80.0), px(40.0))),
2138            ))
2139            .build();
2140        let mut editor = CanvasEditor::new(document);
2141        editor.session.selection.nodes.insert(NodeId::from("a"));
2142        editor.session.selection.nodes.insert(NodeId::from("b"));
2143        editor
2144            .session
2145            .selection
2146            .shapes
2147            .insert(ShapeId::from("shape"));
2148
2149        assert!(editor.group_selection("group").unwrap());
2150
2151        let group = CanvasRecordId::Shape(ShapeId::from("group"));
2152        let member_a = CanvasRecordId::Node(NodeId::from("a"));
2153        let member_b = CanvasRecordId::Node(NodeId::from("b"));
2154        let member_shape = CanvasRecordId::Shape(ShapeId::from("shape"));
2155        let internal_edge = CanvasRecordId::Edge(EdgeId::from("a-b"));
2156        let external_edge = CanvasRecordId::Edge(EdgeId::from("a-outside"));
2157        assert_eq!(
2158            editor
2159                .document()
2160                .shape(&ShapeId::from("group"))
2161                .unwrap()
2162                .kind,
2163            "group"
2164        );
2165        for member in [&member_a, &member_b, &member_shape, &internal_edge] {
2166            assert_eq!(
2167                editor.document().relations().parent_of(member),
2168                Some(&group)
2169            );
2170            assert!(
2171                editor
2172                    .document()
2173                    .relations()
2174                    .members_of(&group)
2175                    .any(|candidate| candidate == member)
2176            );
2177        }
2178        assert_eq!(
2179            editor.document().relations().parent_of(&external_edge),
2180            None
2181        );
2182        assert!(
2183            !editor
2184                .document()
2185                .relations()
2186                .members_of(&group)
2187                .any(|candidate| candidate == &external_edge)
2188        );
2189        assert_eq!(
2190            editor
2191                .session
2192                .selection
2193                .shapes
2194                .iter()
2195                .cloned()
2196                .collect::<Vec<_>>(),
2197            vec![ShapeId::from("group")]
2198        );
2199        assert!(editor.session.selection.nodes.is_empty());
2200        assert!(editor.session.selection.edges.is_empty());
2201        assert_eq!(editor.history().undo_depth(), 1);
2202
2203        assert!(editor.undo().unwrap());
2204        assert!(!editor.document().contains_shape(&ShapeId::from("group")));
2205        assert!(editor.document().relations().is_empty());
2206        assert!(editor.document().contains_edge(&EdgeId::from("a-b")));
2207    }
2208
2209    #[test]
2210    fn select_tool_hits_group_border_but_not_transparent_interior() {
2211        let document = document_fixture()
2212            .node(CanvasNode::new(
2213                "a",
2214                point(px(10.0), px(10.0)),
2215                size(px(40.0), px(40.0)),
2216            ))
2217            .node(CanvasNode::new(
2218                "b",
2219                point(px(120.0), px(10.0)),
2220                size(px(40.0), px(40.0)),
2221            ))
2222            .build();
2223        let mut editor = CanvasEditor::new(document);
2224        editor.session.selection.nodes.insert(NodeId::from("a"));
2225        editor.session.selection.nodes.insert(NodeId::from("b"));
2226        assert!(editor.group_selection("group").unwrap());
2227        editor.handle_event(CanvasEvent::Cancel).unwrap();
2228
2229        assert_eq!(
2230            editor
2231                .runtime()
2232                .precise_hit_test_with_kind_registry(
2233                    editor.document(),
2234                    editor.kind_registry(),
2235                    point(px(20.0), px(20.0)),
2236                    HitOptions::default(),
2237                )
2238                .map(|record| record.target.clone())
2239                .collect::<Vec<_>>(),
2240            vec![HitTarget::Node(NodeId::from("a"))]
2241        );
2242
2243        editor
2244            .handle_event(CanvasEvent::PointerDown {
2245                position: point(px(20.0), px(20.0)),
2246                button: PointerButton::Primary,
2247                modifiers: CanvasKeyModifiers::default(),
2248            })
2249            .unwrap();
2250
2251        assert_eq!(
2252            editor
2253                .selection()
2254                .selected_nodes()
2255                .cloned()
2256                .collect::<Vec<_>>(),
2257            vec![NodeId::from("a")]
2258        );
2259
2260        editor.handle_event(CanvasEvent::Cancel).unwrap();
2261        editor
2262            .handle_event(CanvasEvent::PointerDown {
2263                position: point(px(85.0), px(10.0)),
2264                button: PointerButton::Primary,
2265                modifiers: CanvasKeyModifiers::default(),
2266            })
2267            .unwrap();
2268
2269        assert_eq!(
2270            editor
2271                .selection()
2272                .selected_shapes()
2273                .cloned()
2274                .collect::<Vec<_>>(),
2275            vec![ShapeId::from("group")]
2276        );
2277    }
2278
2279    #[test]
2280    fn editor_ungroups_selected_groups_and_selects_members() {
2281        let document = document_fixture()
2282            .node(CanvasNode::new(
2283                "a",
2284                point(px(0.0), px(0.0)),
2285                size(px(100.0), px(100.0)),
2286            ))
2287            .node(CanvasNode::new(
2288                "b",
2289                point(px(200.0), px(0.0)),
2290                size(px(100.0), px(100.0)),
2291            ))
2292            .edge(CanvasEdge::new(
2293                "a-b",
2294                CanvasEndpoint::new("a", None::<&str>),
2295                CanvasEndpoint::new("b", None::<&str>),
2296            ))
2297            .build();
2298        let mut editor = CanvasEditor::new(document);
2299        editor.session.selection.nodes.insert(NodeId::from("a"));
2300        editor.session.selection.nodes.insert(NodeId::from("b"));
2301        assert!(editor.group_selection("group").unwrap());
2302
2303        assert!(editor.ungroup_selection().unwrap());
2304
2305        assert!(!editor.document().contains_shape(&ShapeId::from("group")));
2306        assert!(editor.document().relations().is_empty());
2307        assert_eq!(
2308            editor
2309                .session
2310                .selection
2311                .nodes
2312                .iter()
2313                .cloned()
2314                .collect::<Vec<_>>(),
2315            vec![NodeId::from("a"), NodeId::from("b")]
2316        );
2317        assert_eq!(
2318            editor
2319                .session
2320                .selection
2321                .edges
2322                .iter()
2323                .cloned()
2324                .collect::<Vec<_>>(),
2325            Vec::<EdgeId>::new()
2326        );
2327        assert!(editor.session.selection.shapes.is_empty());
2328        assert!(editor.document().contains_edge(&EdgeId::from("a-b")));
2329
2330        assert!(editor.undo().unwrap());
2331        let group = CanvasRecordId::Shape(ShapeId::from("group"));
2332        assert!(editor.document().contains_shape(&ShapeId::from("group")));
2333        let expected_members: IndexSet<CanvasRecordId> = IndexSet::from_iter([
2334            CanvasRecordId::Node(NodeId::from("a")),
2335            CanvasRecordId::Node(NodeId::from("b")),
2336            CanvasRecordId::Edge(EdgeId::from("a-b")),
2337        ]);
2338        assert_eq!(
2339            editor
2340                .document()
2341                .relations()
2342                .members_of(&group)
2343                .cloned()
2344                .collect::<IndexSet<_>>(),
2345            expected_members
2346        );
2347    }
2348
2349    #[test]
2350    fn editor_group_selection_skips_locked_and_hidden_records() {
2351        let mut locked = CanvasNode::new(
2352            "locked",
2353            point(px(400.0), px(0.0)),
2354            size(px(100.0), px(100.0)),
2355        );
2356        locked.locked = true;
2357        let mut hidden = CanvasShape::new(
2358            "hidden",
2359            Bounds::new(point(px(0.0), px(200.0)), size(px(40.0), px(40.0))),
2360        );
2361        hidden.hidden = true;
2362        let document = document_fixture()
2363            .node(CanvasNode::new(
2364                "a",
2365                point(px(0.0), px(0.0)),
2366                size(px(100.0), px(100.0)),
2367            ))
2368            .node(CanvasNode::new(
2369                "b",
2370                point(px(200.0), px(0.0)),
2371                size(px(100.0), px(100.0)),
2372            ))
2373            .node(locked)
2374            .edge(CanvasEdge::new(
2375                "a-b",
2376                CanvasEndpoint::new("a", None::<&str>),
2377                CanvasEndpoint::new("b", None::<&str>),
2378            ))
2379            .shape(hidden)
2380            .build();
2381        let mut editor = CanvasEditor::new(document);
2382        editor.session.selection.nodes.insert(NodeId::from("a"));
2383        editor.session.selection.nodes.insert(NodeId::from("b"));
2384        editor
2385            .session
2386            .selection
2387            .nodes
2388            .insert(NodeId::from("locked"));
2389        editor
2390            .session
2391            .selection
2392            .shapes
2393            .insert(ShapeId::from("hidden"));
2394
2395        assert!(editor.group_selection("group").unwrap());
2396
2397        let group = CanvasRecordId::Shape(ShapeId::from("group"));
2398        let expected_members: IndexSet<CanvasRecordId> = IndexSet::from_iter([
2399            CanvasRecordId::Node(NodeId::from("a")),
2400            CanvasRecordId::Node(NodeId::from("b")),
2401            CanvasRecordId::Edge(EdgeId::from("a-b")),
2402        ]);
2403        assert_eq!(
2404            editor
2405                .document()
2406                .relations()
2407                .members_of(&group)
2408                .cloned()
2409                .collect::<IndexSet<_>>(),
2410            expected_members
2411        );
2412        assert_eq!(
2413            editor
2414                .document()
2415                .relations()
2416                .parent_of(&CanvasRecordId::Node(NodeId::from("locked"))),
2417            None
2418        );
2419        assert_eq!(
2420            editor
2421                .document()
2422                .relations()
2423                .parent_of(&CanvasRecordId::Shape(ShapeId::from("hidden"))),
2424            None
2425        );
2426    }
2427
2428    #[test]
2429    fn editor_groups_existing_group_as_atomic_member() {
2430        let mut inner_group = CanvasShape::new(
2431            "inner-group",
2432            Bounds::new(point(px(0.0), px(0.0)), size(px(120.0), px(120.0))),
2433        );
2434        inner_group.kind = "group".to_string();
2435        let mut document = document_fixture()
2436            .shape(inner_group)
2437            .node(CanvasNode::new(
2438                "leaf",
2439                point(px(10.0), px(10.0)),
2440                size(px(20.0), px(20.0)),
2441            ))
2442            .node(CanvasNode::new(
2443                "peer",
2444                point(px(200.0), px(0.0)),
2445                size(px(100.0), px(100.0)),
2446            ))
2447            .build();
2448        document
2449            .apply_transaction(CanvasTransaction::new([
2450                DocumentCommand::SetRecordParent {
2451                    child: CanvasRecordId::Node(NodeId::from("leaf")),
2452                    parent: CanvasRecordId::Shape(ShapeId::from("inner-group")),
2453                },
2454                DocumentCommand::AddRecordToGroup {
2455                    group: CanvasRecordId::Shape(ShapeId::from("inner-group")),
2456                    member: CanvasRecordId::Node(NodeId::from("leaf")),
2457                },
2458            ]))
2459            .unwrap();
2460        let mut editor = CanvasEditor::new(document);
2461        editor
2462            .session
2463            .selection
2464            .shapes
2465            .insert(ShapeId::from("inner-group"));
2466        editor.session.selection.nodes.insert(NodeId::from("peer"));
2467
2468        assert!(editor.group_selection("outer-group").unwrap());
2469
2470        let outer_group = CanvasRecordId::Shape(ShapeId::from("outer-group"));
2471        let expected_members: IndexSet<CanvasRecordId> = IndexSet::from_iter([
2472            CanvasRecordId::Node(NodeId::from("peer")),
2473            CanvasRecordId::Shape(ShapeId::from("inner-group")),
2474        ]);
2475        assert_eq!(
2476            editor
2477                .document()
2478                .relations()
2479                .members_of(&outer_group)
2480                .cloned()
2481                .collect::<IndexSet<_>>(),
2482            expected_members
2483        );
2484        assert_eq!(
2485            editor
2486                .document()
2487                .relations()
2488                .parent_of(&CanvasRecordId::Node(NodeId::from("leaf"))),
2489            Some(&CanvasRecordId::Shape(ShapeId::from("inner-group")))
2490        );
2491    }
2492
2493    #[test]
2494    fn editor_group_selection_preserves_common_parent_membership() {
2495        let mut frame = CanvasShape::new(
2496            "frame",
2497            Bounds::new(point(px(-20.0), px(-20.0)), size(px(360.0), px(180.0))),
2498        );
2499        frame.kind = "group".to_string();
2500        let mut document = document_fixture()
2501            .shape(frame)
2502            .node(CanvasNode::new(
2503                "a",
2504                point(px(0.0), px(0.0)),
2505                size(px(100.0), px(100.0)),
2506            ))
2507            .node(CanvasNode::new(
2508                "b",
2509                point(px(200.0), px(0.0)),
2510                size(px(100.0), px(100.0)),
2511            ))
2512            .edge(CanvasEdge::new(
2513                "a-b",
2514                CanvasEndpoint::new("a", None::<&str>),
2515                CanvasEndpoint::new("b", None::<&str>),
2516            ))
2517            .build();
2518        let frame = CanvasRecordId::Shape(ShapeId::from("frame"));
2519        for member in [
2520            CanvasRecordId::Node(NodeId::from("a")),
2521            CanvasRecordId::Node(NodeId::from("b")),
2522        ] {
2523            document
2524                .apply_transaction(CanvasTransaction::new([
2525                    DocumentCommand::SetRecordParent {
2526                        child: member.clone(),
2527                        parent: frame.clone(),
2528                    },
2529                    DocumentCommand::AddRecordToGroup {
2530                        group: frame.clone(),
2531                        member,
2532                    },
2533                ]))
2534                .unwrap();
2535        }
2536        let mut editor = CanvasEditor::new(document);
2537        editor.session.selection.nodes.insert(NodeId::from("a"));
2538        editor.session.selection.nodes.insert(NodeId::from("b"));
2539
2540        assert!(editor.group_selection("group").unwrap());
2541
2542        let group = CanvasRecordId::Shape(ShapeId::from("group"));
2543        assert_eq!(
2544            editor.document().relations().parent_of(&group),
2545            Some(&frame)
2546        );
2547        assert!(
2548            editor
2549                .document()
2550                .relations()
2551                .members_of(&frame)
2552                .any(|member| member == &group)
2553        );
2554        for member in [
2555            CanvasRecordId::Node(NodeId::from("a")),
2556            CanvasRecordId::Node(NodeId::from("b")),
2557            CanvasRecordId::Edge(EdgeId::from("a-b")),
2558        ] {
2559            assert_eq!(
2560                editor.document().relations().parent_of(&member),
2561                Some(&group)
2562            );
2563            assert!(
2564                !editor
2565                    .document()
2566                    .relations()
2567                    .members_of(&frame)
2568                    .any(|candidate| candidate == &member)
2569            );
2570        }
2571
2572        assert!(editor.ungroup_selection().unwrap());
2573
2574        assert!(!editor.document().contains_shape(&ShapeId::from("group")));
2575        for member in [
2576            CanvasRecordId::Node(NodeId::from("a")),
2577            CanvasRecordId::Node(NodeId::from("b")),
2578            CanvasRecordId::Edge(EdgeId::from("a-b")),
2579        ] {
2580            assert_eq!(
2581                editor.document().relations().parent_of(&member),
2582                Some(&frame)
2583            );
2584            assert!(
2585                editor
2586                    .document()
2587                    .relations()
2588                    .members_of(&frame)
2589                    .any(|candidate| candidate == &member)
2590            );
2591        }
2592    }
2593
2594    #[test]
2595    fn editor_group_selection_ignores_selected_descendant_of_selected_group() {
2596        let mut inner_group = CanvasShape::new(
2597            "inner-group",
2598            Bounds::new(point(px(0.0), px(0.0)), size(px(120.0), px(120.0))),
2599        );
2600        inner_group.kind = "group".to_string();
2601        let mut document = document_fixture()
2602            .shape(inner_group)
2603            .node(CanvasNode::new(
2604                "leaf",
2605                point(px(10.0), px(10.0)),
2606                size(px(20.0), px(20.0)),
2607            ))
2608            .node(CanvasNode::new(
2609                "peer",
2610                point(px(200.0), px(0.0)),
2611                size(px(100.0), px(100.0)),
2612            ))
2613            .build();
2614        document
2615            .apply_transaction(CanvasTransaction::new([
2616                DocumentCommand::SetRecordParent {
2617                    child: CanvasRecordId::Node(NodeId::from("leaf")),
2618                    parent: CanvasRecordId::Shape(ShapeId::from("inner-group")),
2619                },
2620                DocumentCommand::AddRecordToGroup {
2621                    group: CanvasRecordId::Shape(ShapeId::from("inner-group")),
2622                    member: CanvasRecordId::Node(NodeId::from("leaf")),
2623                },
2624            ]))
2625            .unwrap();
2626        let mut editor = CanvasEditor::new(document);
2627        editor
2628            .session
2629            .selection
2630            .shapes
2631            .insert(ShapeId::from("inner-group"));
2632        editor.session.selection.nodes.insert(NodeId::from("leaf"));
2633        editor.session.selection.nodes.insert(NodeId::from("peer"));
2634
2635        assert!(editor.group_selection("outer-group").unwrap());
2636
2637        let outer_group = CanvasRecordId::Shape(ShapeId::from("outer-group"));
2638        let expected_members: IndexSet<CanvasRecordId> = IndexSet::from_iter([
2639            CanvasRecordId::Node(NodeId::from("peer")),
2640            CanvasRecordId::Shape(ShapeId::from("inner-group")),
2641        ]);
2642        assert_eq!(
2643            editor
2644                .document()
2645                .relations()
2646                .members_of(&outer_group)
2647                .cloned()
2648                .collect::<IndexSet<_>>(),
2649            expected_members
2650        );
2651        assert_eq!(
2652            editor
2653                .document()
2654                .relations()
2655                .parent_of(&CanvasRecordId::Node(NodeId::from("leaf"))),
2656            Some(&CanvasRecordId::Shape(ShapeId::from("inner-group")))
2657        );
2658    }
2659
2660    #[test]
2661    fn editor_reorders_selected_records_and_supports_undo() {
2662        let mut back = CanvasNode::new("back", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2663        back.z_index = 1;
2664        let mut front = CanvasNode::new(
2665            "front",
2666            point(px(10.0), px(10.0)),
2667            size(px(100.0), px(100.0)),
2668        );
2669        front.z_index = 2;
2670        let document = document_fixture().node(back).node(front).build();
2671        let mut editor = CanvasEditor::new(document);
2672        editor.session.selection.nodes.insert(NodeId::from("back"));
2673
2674        assert!(
2675            editor
2676                .reorder_selection(CanvasZOrderCommand::BringToFront)
2677                .unwrap()
2678        );
2679        assert_eq!(
2680            editor
2681                .document()
2682                .node(&NodeId::from("back"))
2683                .unwrap()
2684                .z_index,
2685            2
2686        );
2687        assert_eq!(editor.history().undo_depth(), 1);
2688        assert_eq!(
2689            editor
2690                .runtime()
2691                .hit_test(point(px(20.0), px(20.0)), HitOptions::default())
2692                .next()
2693                .map(|record| record.target.clone()),
2694            Some(HitTarget::Node(NodeId::from("back")))
2695        );
2696
2697        assert!(editor.undo().unwrap());
2698        assert_eq!(
2699            editor
2700                .document()
2701                .node(&NodeId::from("back"))
2702                .unwrap()
2703                .z_index,
2704            1
2705        );
2706    }
2707
2708    #[test]
2709    fn bring_forward_crosses_sparse_adjacent_layer() {
2710        let mut back = CanvasNode::new("back", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2711        back.z_index = 1;
2712        let mut front = CanvasShape::new(
2713            "front",
2714            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2715        );
2716        front.z_index = 10;
2717        let document = document_fixture().node(back).shape(front).build();
2718        let mut editor = CanvasEditor::new(document);
2719        editor.session.selection.nodes.insert(NodeId::from("back"));
2720
2721        assert!(
2722            editor
2723                .reorder_selection(CanvasZOrderCommand::BringForward)
2724                .unwrap()
2725        );
2726
2727        assert_eq!(
2728            editor
2729                .runtime()
2730                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
2731                .next()
2732                .map(|record| record.target.clone()),
2733            Some(HitTarget::Node(NodeId::from("back")))
2734        );
2735        assert!(
2736            editor
2737                .document()
2738                .node(&NodeId::from("back"))
2739                .unwrap()
2740                .z_index
2741                > editor
2742                    .document()
2743                    .shape(&ShapeId::from("front"))
2744                    .unwrap()
2745                    .z_index
2746        );
2747    }
2748
2749    #[test]
2750    fn send_backward_crosses_duplicate_adjacent_layer() {
2751        let mut back = CanvasNode::new("back", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2752        back.z_index = 0;
2753        let mut front = CanvasShape::new(
2754            "front",
2755            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2756        );
2757        front.z_index = 0;
2758        let document = document_fixture().node(back).shape(front).build();
2759        let mut editor = CanvasEditor::new(document);
2760        editor
2761            .session
2762            .selection
2763            .shapes
2764            .insert(ShapeId::from("front"));
2765
2766        assert!(
2767            editor
2768                .reorder_selection(CanvasZOrderCommand::SendBackward)
2769                .unwrap()
2770        );
2771
2772        assert_eq!(
2773            editor
2774                .runtime()
2775                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
2776                .next()
2777                .map(|record| record.target.clone()),
2778            Some(HitTarget::Node(NodeId::from("back")))
2779        );
2780        assert!(
2781            editor
2782                .document()
2783                .shape(&ShapeId::from("front"))
2784                .unwrap()
2785                .z_index
2786                < editor
2787                    .document()
2788                    .node(&NodeId::from("back"))
2789                    .unwrap()
2790                    .z_index
2791        );
2792    }
2793
2794    #[test]
2795    fn z_order_multi_select_preserves_relative_order_across_record_kinds() {
2796        let mut node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2797        node.z_index = 0;
2798        let mut shape = CanvasShape::new(
2799            "shape",
2800            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2801        );
2802        shape.z_index = 1;
2803        let mut edge = CanvasEdge::new(
2804            "edge",
2805            CanvasEndpoint::new("node", None::<&str>),
2806            CanvasEndpoint::new("node", None::<&str>),
2807        );
2808        edge.z_index = 2;
2809        let mut top = CanvasShape::new(
2810            "top",
2811            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2812        );
2813        top.z_index = 3;
2814        let document = document_fixture()
2815            .node(node)
2816            .shape(shape)
2817            .shape(top)
2818            .edge(edge)
2819            .build();
2820        let mut editor = CanvasEditor::new(document);
2821        editor.session.selection.nodes.insert(NodeId::from("node"));
2822        editor.session.selection.edges.insert(EdgeId::from("edge"));
2823
2824        assert!(
2825            editor
2826                .reorder_selection(CanvasZOrderCommand::BringToFront)
2827                .unwrap()
2828        );
2829
2830        let hits = editor
2831            .runtime()
2832            .hit_test(
2833                point(px(50.0), px(50.0)),
2834                HitOptions {
2835                    include_handles: false,
2836                    margin: px(24.0),
2837                    ..HitOptions::default()
2838                },
2839            )
2840            .map(|record| record.target.clone())
2841            .collect::<Vec<_>>();
2842        assert_eq!(
2843            hits,
2844            vec![
2845                HitTarget::Edge(EdgeId::from("edge")),
2846                HitTarget::Node(NodeId::from("node")),
2847                HitTarget::Shape(ShapeId::from("top")),
2848                HitTarget::Shape(ShapeId::from("shape")),
2849            ]
2850        );
2851    }
2852
2853    #[test]
2854    fn z_order_selected_parent_reorders_related_descendants() {
2855        let mut child =
2856            CanvasNode::new("child", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2857        child.z_index = 0;
2858        let mut peer = CanvasNode::new("peer", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
2859        peer.z_index = 1;
2860        let mut edge = CanvasEdge::new(
2861            "child-peer",
2862            CanvasEndpoint::new("child", None::<&str>),
2863            CanvasEndpoint::new("peer", None::<&str>),
2864        );
2865        edge.z_index = 2;
2866        let mut frame = CanvasShape::new(
2867            "frame",
2868            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2869        );
2870        frame.z_index = 3;
2871        let mut top = CanvasShape::new(
2872            "top",
2873            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
2874        );
2875        top.z_index = 4;
2876        let mut document = document_fixture()
2877            .node(child)
2878            .node(peer)
2879            .edge(edge)
2880            .shape(frame)
2881            .shape(top)
2882            .build();
2883        document
2884            .apply_transaction(CanvasTransaction::new([
2885                DocumentCommand::SetRecordParent {
2886                    child: CanvasRecordId::Node(NodeId::from("child")),
2887                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
2888                },
2889                DocumentCommand::AddRecordToGroup {
2890                    group: CanvasRecordId::Shape(ShapeId::from("frame")),
2891                    member: CanvasRecordId::Node(NodeId::from("peer")),
2892                },
2893            ]))
2894            .unwrap();
2895        let mut editor = CanvasEditor::new(document);
2896        editor
2897            .session
2898            .selection
2899            .shapes
2900            .insert(ShapeId::from("frame"));
2901
2902        assert!(
2903            editor
2904                .reorder_selection(CanvasZOrderCommand::BringToFront)
2905                .unwrap()
2906        );
2907
2908        let hits = editor
2909            .runtime()
2910            .hit_test(
2911                point(px(50.0), px(50.0)),
2912                HitOptions {
2913                    include_handles: false,
2914                    margin: px(24.0),
2915                    ..HitOptions::default()
2916                },
2917            )
2918            .map(|record| record.target.clone())
2919            .collect::<Vec<_>>();
2920        assert_eq!(
2921            hits,
2922            vec![
2923                HitTarget::Shape(ShapeId::from("frame")),
2924                HitTarget::Edge(EdgeId::from("child-peer")),
2925                HitTarget::Node(NodeId::from("peer")),
2926                HitTarget::Node(NodeId::from("child")),
2927                HitTarget::Shape(ShapeId::from("top")),
2928            ]
2929        );
2930
2931        assert_eq!(editor.history().undo_depth(), 1);
2932        assert!(editor.undo().unwrap());
2933        assert_eq!(
2934            editor
2935                .runtime()
2936                .hit_test(
2937                    point(px(50.0), px(50.0)),
2938                    HitOptions {
2939                        include_handles: false,
2940                        margin: px(24.0),
2941                        ..HitOptions::default()
2942                    },
2943                )
2944                .next()
2945                .map(|record| record.target.clone()),
2946            Some(HitTarget::Shape(ShapeId::from("top")))
2947        );
2948    }
2949
2950    #[test]
2951    fn canvas_transform_handles_follow_selected_record_bounds() {
2952        let document = document_fixture()
2953            .node(CanvasNode::new(
2954                "node",
2955                point(px(10.0), px(20.0)),
2956                size(px(100.0), px(80.0)),
2957            ))
2958            .shape(CanvasShape::new(
2959                "shape",
2960                Bounds::new(point(px(200.0), px(40.0)), size(px(50.0), px(30.0))),
2961            ))
2962            .build();
2963        let mut selection = CanvasSelection::default();
2964        selection.nodes.insert(NodeId::from("node"));
2965        selection.shapes.insert(ShapeId::from("shape"));
2966
2967        let handles =
2968            canvas_transform_handles(&document, &selection, CanvasViewport::default(), None);
2969
2970        assert_eq!(handles.len(), 8);
2971        assert!(handles.iter().any(|handle| {
2972            handle.target == CanvasTransformTarget::Node(NodeId::from("node"))
2973                && handle.handle == CanvasResizeHandle::BottomRight
2974                && handle
2975                    .document_bounds
2976                    .contains(&point(px(110.0), px(100.0)))
2977        }));
2978        assert!(handles.iter().any(|handle| {
2979            handle.target == CanvasTransformTarget::Shape(ShapeId::from("shape"))
2980                && handle.handle == CanvasResizeHandle::TopLeft
2981                && handle.document_bounds.contains(&point(px(200.0), px(40.0)))
2982        }));
2983    }
2984
2985    #[test]
2986    fn canvas_transform_handles_use_registered_geometry_bounds() {
2987        let mut node =
2988            CanvasNode::new("node", point(px(10.0), px(20.0)), size(px(100.0), px(80.0)));
2989        node.kind = "wide".to_string();
2990        let document = document_fixture().node(node).build();
2991        let mut selection = CanvasSelection::default();
2992        selection.nodes.insert(NodeId::from("node"));
2993        let mut registry = CanvasKindRegistry::open();
2994        registry.register_node_kind("wide", wide_bounds_node_kind());
2995
2996        let handles = canvas_transform_handles(
2997            &document,
2998            &selection,
2999            CanvasViewport::default(),
3000            Some(&registry),
3001        );
3002
3003        assert_eq!(handles.len(), 4);
3004        assert!(handles.iter().any(|handle| {
3005            handle.target == CanvasTransformTarget::Node(NodeId::from("node"))
3006                && handle.handle == CanvasResizeHandle::BottomRight
3007                && handle
3008                    .document_bounds
3009                    .contains(&point(px(140.0), px(100.0)))
3010        }));
3011    }
3012
3013    #[test]
3014    fn select_tool_resizes_selected_node_with_one_undo_entry() {
3015        let document = document_fixture()
3016            .node(CanvasNode::new(
3017                "node",
3018                point(px(10.0), px(20.0)),
3019                size(px(100.0), px(80.0)),
3020            ))
3021            .build();
3022        let mut editor = CanvasEditor::new(document);
3023        editor.session.selection.nodes.insert(NodeId::from("node"));
3024
3025        editor
3026            .handle_event(CanvasEvent::PointerDown {
3027                position: point(px(110.0), px(100.0)),
3028                button: PointerButton::Primary,
3029                modifiers: CanvasKeyModifiers::default(),
3030            })
3031            .unwrap();
3032        editor
3033            .handle_event(CanvasEvent::PointerMove {
3034                position: point(px(130.0), px(125.0)),
3035                modifiers: CanvasKeyModifiers::default(),
3036            })
3037            .unwrap();
3038        editor
3039            .handle_event(CanvasEvent::PointerUp {
3040                position: point(px(130.0), px(125.0)),
3041                button: PointerButton::Primary,
3042                modifiers: CanvasKeyModifiers::default(),
3043            })
3044            .unwrap();
3045
3046        let node = &editor.document().node(&NodeId::from("node")).unwrap();
3047        assert_eq!(node.position, point(px(10.0), px(20.0)));
3048        assert_eq!(node.size, size(px(120.0), px(105.0)));
3049        assert_eq!(editor.history().undo_depth(), 1);
3050        assert!(matches!(editor.session.state, ToolState::Idle));
3051        assert!(
3052            editor
3053                .runtime()
3054                .hit_test(point(px(128.0), px(123.0)), HitOptions::default())
3055                .any(|record| record.target == HitTarget::Node(NodeId::from("node")))
3056        );
3057
3058        assert!(editor.undo().unwrap());
3059        let node = &editor.document().node(&NodeId::from("node")).unwrap();
3060        assert_eq!(node.size, size(px(100.0), px(80.0)));
3061    }
3062
3063    #[test]
3064    fn select_tool_resizes_group_and_structural_descendants() {
3065        let mut edge = CanvasEdge::new(
3066            "a-b",
3067            CanvasEndpoint::new("a", None::<&str>),
3068            CanvasEndpoint::new("b", None::<&str>),
3069        );
3070        edge.route = crate::CanvasEdgeRoute::polyline([point(px(30.0), px(70.0))]);
3071        edge.route.control_points = vec![point(px(40.0), px(20.0))];
3072        let document = document_fixture()
3073            .node(CanvasNode::new(
3074                "a",
3075                point(px(10.0), px(10.0)),
3076                size(px(20.0), px(20.0)),
3077            ))
3078            .node(CanvasNode::new(
3079                "b",
3080                point(px(50.0), px(50.0)),
3081                size(px(20.0), px(20.0)),
3082            ))
3083            .edge(edge)
3084            .build();
3085        let mut editor = CanvasEditor::new(document);
3086        editor.session.selection.nodes.insert(NodeId::from("a"));
3087        editor.session.selection.nodes.insert(NodeId::from("b"));
3088        assert!(editor.group_selection("group").unwrap());
3089        editor.handle_event(CanvasEvent::Cancel).unwrap();
3090        editor
3091            .session
3092            .selection
3093            .shapes
3094            .insert(ShapeId::from("group"));
3095
3096        editor
3097            .handle_event(CanvasEvent::PointerDown {
3098                position: point(px(70.0), px(70.0)),
3099                button: PointerButton::Primary,
3100                modifiers: CanvasKeyModifiers::default(),
3101            })
3102            .unwrap();
3103        editor
3104            .handle_event(CanvasEvent::PointerMove {
3105                position: point(px(130.0), px(130.0)),
3106                modifiers: CanvasKeyModifiers::default(),
3107            })
3108            .unwrap();
3109        editor
3110            .handle_event(CanvasEvent::PointerUp {
3111                position: point(px(130.0), px(130.0)),
3112                button: PointerButton::Primary,
3113                modifiers: CanvasKeyModifiers::default(),
3114            })
3115            .unwrap();
3116
3117        let a = editor.document().node(&NodeId::from("a")).unwrap();
3118        let b = editor.document().node(&NodeId::from("b")).unwrap();
3119        let edge = editor.document().edge(&EdgeId::from("a-b")).unwrap();
3120        let group = editor.document().shape(&ShapeId::from("group")).unwrap();
3121        assert_eq!(a.position, point(px(10.0), px(10.0)));
3122        assert_eq!(a.size, size(px(40.0), px(40.0)));
3123        assert_eq!(b.position, point(px(90.0), px(90.0)));
3124        assert_eq!(b.size, size(px(40.0), px(40.0)));
3125        assert_eq!(edge.route.waypoints, vec![point(px(50.0), px(130.0))]);
3126        assert_eq!(edge.route.control_points, vec![point(px(70.0), px(30.0))]);
3127        assert_eq!(
3128            group.bounds,
3129            Bounds::new(point(px(10.0), px(10.0)), size(px(120.0), px(120.0)))
3130        );
3131        assert_eq!(editor.history().undo_depth(), 2);
3132
3133        assert!(editor.undo().unwrap());
3134        let a = editor.document().node(&NodeId::from("a")).unwrap();
3135        let b = editor.document().node(&NodeId::from("b")).unwrap();
3136        let edge = editor.document().edge(&EdgeId::from("a-b")).unwrap();
3137        let group = editor.document().shape(&ShapeId::from("group")).unwrap();
3138        assert_eq!(a.position, point(px(10.0), px(10.0)));
3139        assert_eq!(a.size, size(px(20.0), px(20.0)));
3140        assert_eq!(b.position, point(px(50.0), px(50.0)));
3141        assert_eq!(b.size, size(px(20.0), px(20.0)));
3142        assert_eq!(edge.route.waypoints, vec![point(px(30.0), px(70.0))]);
3143        assert_eq!(edge.route.control_points, vec![point(px(40.0), px(20.0))]);
3144        assert_eq!(
3145            group.bounds,
3146            Bounds::new(point(px(10.0), px(10.0)), size(px(60.0), px(60.0)))
3147        );
3148    }
3149
3150    #[test]
3151    fn select_tool_direct_multi_select_resize_stays_per_record() {
3152        let mut edge = CanvasEdge::new(
3153            "a-b",
3154            CanvasEndpoint::new("a", None::<&str>),
3155            CanvasEndpoint::new("b", None::<&str>),
3156        );
3157        edge.route = crate::CanvasEdgeRoute::polyline([point(px(30.0), px(70.0))]);
3158        let document = document_fixture()
3159            .node(CanvasNode::new(
3160                "a",
3161                point(px(10.0), px(10.0)),
3162                size(px(20.0), px(20.0)),
3163            ))
3164            .node(CanvasNode::new(
3165                "b",
3166                point(px(50.0), px(50.0)),
3167                size(px(20.0), px(20.0)),
3168            ))
3169            .edge(edge)
3170            .build();
3171        let mut editor = CanvasEditor::new(document);
3172        editor.session.selection.nodes.insert(NodeId::from("a"));
3173        editor.session.selection.nodes.insert(NodeId::from("b"));
3174
3175        editor
3176            .handle_event(CanvasEvent::PointerDown {
3177                position: point(px(70.0), px(70.0)),
3178                button: PointerButton::Primary,
3179                modifiers: CanvasKeyModifiers::default(),
3180            })
3181            .unwrap();
3182        editor
3183            .handle_event(CanvasEvent::PointerMove {
3184                position: point(px(90.0), px(90.0)),
3185                modifiers: CanvasKeyModifiers::default(),
3186            })
3187            .unwrap();
3188        editor
3189            .handle_event(CanvasEvent::PointerUp {
3190                position: point(px(90.0), px(90.0)),
3191                button: PointerButton::Primary,
3192                modifiers: CanvasKeyModifiers::default(),
3193            })
3194            .unwrap();
3195
3196        let a = editor.document().node(&NodeId::from("a")).unwrap();
3197        let b = editor.document().node(&NodeId::from("b")).unwrap();
3198        let edge = editor.document().edge(&EdgeId::from("a-b")).unwrap();
3199        assert_eq!(a.position, point(px(10.0), px(10.0)));
3200        assert_eq!(a.size, size(px(40.0), px(40.0)));
3201        assert_eq!(b.position, point(px(50.0), px(50.0)));
3202        assert_eq!(b.size, size(px(40.0), px(40.0)));
3203        assert_eq!(edge.route.waypoints, vec![point(px(30.0), px(70.0))]);
3204    }
3205
3206    #[test]
3207    fn select_tool_resize_uses_registered_kind_policy() {
3208        let mut node =
3209            CanvasNode::new("node", point(px(10.0), px(20.0)), size(px(100.0), px(80.0)));
3210        node.kind = "min-resize".to_string();
3211        let document = document_fixture().node(node).build();
3212        let mut registry = CanvasKindRegistry::open();
3213        registry.register_node_kind("min-resize", minimum_resize_node_kind());
3214        let mut editor = CanvasEditor::try_new_with_kind_registry(document, registry).unwrap();
3215        editor.session.selection.nodes.insert(NodeId::from("node"));
3216
3217        editor
3218            .handle_event(CanvasEvent::PointerDown {
3219                position: point(px(110.0), px(100.0)),
3220                button: PointerButton::Primary,
3221                modifiers: CanvasKeyModifiers::default(),
3222            })
3223            .unwrap();
3224        editor
3225            .handle_event(CanvasEvent::PointerMove {
3226                position: point(px(-100.0), px(-100.0)),
3227                modifiers: CanvasKeyModifiers::default(),
3228            })
3229            .unwrap();
3230        editor
3231            .handle_event(CanvasEvent::PointerUp {
3232                position: point(px(-100.0), px(-100.0)),
3233                button: PointerButton::Primary,
3234                modifiers: CanvasKeyModifiers::default(),
3235            })
3236            .unwrap();
3237
3238        let node = &editor.document().node(&NodeId::from("node")).unwrap();
3239        assert_eq!(node.position, point(px(10.0), px(20.0)));
3240        assert_eq!(node.size, size(px(64.0), px(48.0)));
3241        assert_eq!(editor.history().undo_depth(), 1);
3242    }
3243
3244    #[test]
3245    fn select_tool_resize_policy_rejection_is_atomic() {
3246        let mut node =
3247            CanvasNode::new("node", point(px(10.0), px(20.0)), size(px(100.0), px(80.0)));
3248        node.kind = "reject-resize".to_string();
3249        let original = node.clone();
3250        let document = document_fixture().node(node).build();
3251        let mut registry = CanvasKindRegistry::open();
3252        registry.register_node_kind("reject-resize", reject_resize_node_kind());
3253        let mut editor = CanvasEditor::try_new_with_kind_registry(document, registry).unwrap();
3254        editor.session.selection.nodes.insert(NodeId::from("node"));
3255
3256        editor
3257            .handle_event(CanvasEvent::PointerDown {
3258                position: point(px(110.0), px(100.0)),
3259                button: PointerButton::Primary,
3260                modifiers: CanvasKeyModifiers::default(),
3261            })
3262            .unwrap();
3263        let err = editor
3264            .handle_event(CanvasEvent::PointerMove {
3265                position: point(px(130.0), px(125.0)),
3266                modifiers: CanvasKeyModifiers::default(),
3267            })
3268            .unwrap_err();
3269
3270        assert!(matches!(
3271            err,
3272            DocumentError::Schema(CanvasSchemaError::InvalidData {
3273                record_kind: CanvasRecordKind::Node,
3274                record_id: crate::CanvasRecordId::Node(id),
3275                kind,
3276                message,
3277            }) if id == NodeId::from("node")
3278                && kind == "reject-resize"
3279                && message == "resize is disabled"
3280        ));
3281        assert_eq!(
3282            editor.document().node(&NodeId::from("node")).unwrap(),
3283            &original
3284        );
3285        assert_eq!(editor.history().undo_depth(), 0);
3286        assert!(
3287            editor
3288                .runtime()
3289                .hit_test(point(px(108.0), px(98.0)), HitOptions::default())
3290                .any(|record| record.target == HitTarget::Node(NodeId::from("node")))
3291        );
3292    }
3293
3294    #[test]
3295    fn select_tool_cancel_restores_resize_baseline() {
3296        let document = document_fixture()
3297            .shape(CanvasShape::new(
3298                "shape",
3299                Bounds::new(point(px(10.0), px(20.0)), size(px(100.0), px(80.0))),
3300            ))
3301            .build();
3302        let mut editor = CanvasEditor::new(document);
3303        editor
3304            .session
3305            .selection
3306            .shapes
3307            .insert(ShapeId::from("shape"));
3308
3309        editor
3310            .handle_event(CanvasEvent::PointerDown {
3311                position: point(px(10.0), px(20.0)),
3312                button: PointerButton::Primary,
3313                modifiers: CanvasKeyModifiers::default(),
3314            })
3315            .unwrap();
3316        editor
3317            .handle_event(CanvasEvent::PointerMove {
3318                position: point(px(30.0), px(45.0)),
3319                modifiers: CanvasKeyModifiers::default(),
3320            })
3321            .unwrap();
3322        assert_eq!(
3323            editor
3324                .document()
3325                .shape(&ShapeId::from("shape"))
3326                .unwrap()
3327                .bounds,
3328            Bounds::new(point(px(30.0), px(45.0)), size(px(80.0), px(55.0)))
3329        );
3330
3331        editor.handle_event(CanvasEvent::Cancel).unwrap();
3332
3333        assert_eq!(
3334            editor
3335                .document()
3336                .shape(&ShapeId::from("shape"))
3337                .unwrap()
3338                .bounds,
3339            Bounds::new(point(px(10.0), px(20.0)), size(px(100.0), px(80.0)))
3340        );
3341        assert_eq!(editor.history().undo_depth(), 0);
3342        assert!(matches!(editor.session.state, ToolState::Idle));
3343    }
3344
3345    #[test]
3346    fn select_tool_box_selects_intersecting_records() {
3347        let mut locked = CanvasNode::new(
3348            "locked",
3349            point(px(15.0), px(15.0)),
3350            size(px(20.0), px(20.0)),
3351        );
3352        locked.locked = true;
3353        let document = document_fixture()
3354            .node(CanvasNode::new(
3355                "inside",
3356                point(px(10.0), px(10.0)),
3357                size(px(20.0), px(20.0)),
3358            ))
3359            .node(CanvasNode::new(
3360                "outside",
3361                point(px(200.0), px(200.0)),
3362                size(px(20.0), px(20.0)),
3363            ))
3364            .node(locked)
3365            .build();
3366        let mut editor = CanvasEditor::new(document);
3367
3368        editor
3369            .handle_event(CanvasEvent::PointerDown {
3370                position: point(px(0.0), px(0.0)),
3371                button: PointerButton::Primary,
3372                modifiers: CanvasKeyModifiers::default(),
3373            })
3374            .unwrap();
3375        editor
3376            .handle_event(CanvasEvent::PointerMove {
3377                position: point(px(50.0), px(50.0)),
3378                modifiers: CanvasKeyModifiers::default(),
3379            })
3380            .unwrap();
3381        editor
3382            .handle_event(CanvasEvent::PointerUp {
3383                position: point(px(50.0), px(50.0)),
3384                button: PointerButton::Primary,
3385                modifiers: CanvasKeyModifiers::default(),
3386            })
3387            .unwrap();
3388
3389        assert_eq!(
3390            editor
3391                .session
3392                .selection
3393                .nodes
3394                .iter()
3395                .cloned()
3396                .collect::<Vec<_>>(),
3397            vec![NodeId::from("inside")]
3398        );
3399        assert_eq!(editor.session.state, ToolState::Idle);
3400    }
3401
3402    #[test]
3403    fn select_tool_box_select_respects_group_transparent_interior() {
3404        let document = document_fixture()
3405            .node(CanvasNode::new(
3406                "a",
3407                point(px(80.0), px(80.0)),
3408                size(px(20.0), px(20.0)),
3409            ))
3410            .node(CanvasNode::new(
3411                "b",
3412                point(px(10.0), px(10.0)),
3413                size(px(20.0), px(20.0)),
3414            ))
3415            .node(CanvasNode::new(
3416                "c",
3417                point(px(170.0), px(170.0)),
3418                size(px(20.0), px(20.0)),
3419            ))
3420            .build();
3421        let mut editor = CanvasEditor::new(document);
3422        editor.session.selection.nodes.insert(NodeId::from("a"));
3423        editor.session.selection.nodes.insert(NodeId::from("b"));
3424        editor.session.selection.nodes.insert(NodeId::from("c"));
3425        assert!(editor.group_selection("group").unwrap());
3426        editor.handle_event(CanvasEvent::Cancel).unwrap();
3427
3428        editor
3429            .handle_event(CanvasEvent::PointerDown {
3430                position: point(px(75.0), px(75.0)),
3431                button: PointerButton::Primary,
3432                modifiers: CanvasKeyModifiers::default(),
3433            })
3434            .unwrap();
3435        editor
3436            .handle_event(CanvasEvent::PointerMove {
3437                position: point(px(115.0), px(115.0)),
3438                modifiers: CanvasKeyModifiers::default(),
3439            })
3440            .unwrap();
3441        editor
3442            .handle_event(CanvasEvent::PointerUp {
3443                position: point(px(115.0), px(115.0)),
3444                button: PointerButton::Primary,
3445                modifiers: CanvasKeyModifiers::default(),
3446            })
3447            .unwrap();
3448
3449        assert_eq!(
3450            editor
3451                .selection()
3452                .selected_nodes()
3453                .cloned()
3454                .collect::<Vec<_>>(),
3455            vec![NodeId::from("a")]
3456        );
3457        assert!(editor.selection().selected_shapes().next().is_none());
3458    }
3459
3460    #[test]
3461    fn select_tool_cancel_restores_selection_after_box_select() {
3462        let document = document_fixture()
3463            .node(CanvasNode::new(
3464                "base",
3465                point(px(200.0), px(200.0)),
3466                size(px(20.0), px(20.0)),
3467            ))
3468            .node(CanvasNode::new(
3469                "inside",
3470                point(px(10.0), px(10.0)),
3471                size(px(20.0), px(20.0)),
3472            ))
3473            .build();
3474        let mut editor = CanvasEditor::new(document);
3475        editor.session.selection.nodes.insert(NodeId::from("base"));
3476
3477        editor
3478            .handle_event(CanvasEvent::PointerDown {
3479                position: point(px(0.0), px(0.0)),
3480                button: PointerButton::Primary,
3481                modifiers: CanvasKeyModifiers::default(),
3482            })
3483            .unwrap();
3484        editor
3485            .handle_event(CanvasEvent::PointerMove {
3486                position: point(px(40.0), px(40.0)),
3487                modifiers: CanvasKeyModifiers::default(),
3488            })
3489            .unwrap();
3490
3491        assert_eq!(
3492            editor
3493                .session
3494                .selection
3495                .nodes
3496                .iter()
3497                .cloned()
3498                .collect::<Vec<_>>(),
3499            vec![NodeId::from("inside")]
3500        );
3501
3502        editor.handle_event(CanvasEvent::Cancel).unwrap();
3503
3504        assert_eq!(
3505            editor
3506                .session
3507                .selection
3508                .nodes
3509                .iter()
3510                .cloned()
3511                .collect::<Vec<_>>(),
3512            vec![NodeId::from("base")]
3513        );
3514        assert_eq!(editor.session.state, ToolState::Idle);
3515    }
3516
3517    #[test]
3518    fn translating_selected_record_moves_node_and_shape_selection() {
3519        let document = document_fixture()
3520            .node(CanvasNode::new(
3521                "a",
3522                point(px(0.0), px(0.0)),
3523                size(px(100.0), px(100.0)),
3524            ))
3525            .node(CanvasNode::new(
3526                "b",
3527                point(px(200.0), px(0.0)),
3528                size(px(100.0), px(100.0)),
3529            ))
3530            .shape(CanvasShape::new(
3531                "shape",
3532                Bounds::new(point(px(400.0), px(0.0)), size(px(100.0), px(100.0))),
3533            ))
3534            .build();
3535        let mut editor = CanvasEditor::new(document);
3536        editor.session.selection.nodes.insert(NodeId::from("a"));
3537        editor.session.selection.nodes.insert(NodeId::from("b"));
3538        editor
3539            .session
3540            .selection
3541            .shapes
3542            .insert(ShapeId::from("shape"));
3543
3544        editor
3545            .handle_event(CanvasEvent::PointerDown {
3546                position: point(px(10.0), px(10.0)),
3547                button: PointerButton::Primary,
3548                modifiers: CanvasKeyModifiers::default(),
3549            })
3550            .unwrap();
3551        editor
3552            .handle_event(CanvasEvent::PointerMove {
3553                position: point(px(20.0), px(30.0)),
3554                modifiers: CanvasKeyModifiers::default(),
3555            })
3556            .unwrap();
3557        editor
3558            .handle_event(CanvasEvent::PointerUp {
3559                position: point(px(20.0), px(30.0)),
3560                button: PointerButton::Primary,
3561                modifiers: CanvasKeyModifiers::default(),
3562            })
3563            .unwrap();
3564
3565        assert_eq!(
3566            editor.document().node(&NodeId::from("a")).unwrap().position,
3567            point(px(10.0), px(20.0))
3568        );
3569        assert_eq!(
3570            editor.document().node(&NodeId::from("b")).unwrap().position,
3571            point(px(210.0), px(20.0))
3572        );
3573        assert_eq!(
3574            editor
3575                .document()
3576                .shape(&ShapeId::from("shape"))
3577                .unwrap()
3578                .bounds
3579                .origin,
3580            point(px(410.0), px(20.0))
3581        );
3582        assert_eq!(editor.history().undo_depth(), 1);
3583    }
3584
3585    #[test]
3586    fn translating_selected_parent_moves_related_descendants() {
3587        let mut document = document_fixture()
3588            .shape(CanvasShape::new(
3589                "frame",
3590                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
3591            ))
3592            .shape(CanvasShape::new(
3593                "group",
3594                Bounds::new(point(px(40.0), px(0.0)), size(px(50.0), px(50.0))),
3595            ))
3596            .node(CanvasNode::new(
3597                "leaf",
3598                point(px(60.0), px(0.0)),
3599                size(px(20.0), px(20.0)),
3600            ))
3601            .build();
3602        document
3603            .apply_transaction(CanvasTransaction::new([
3604                DocumentCommand::SetRecordParent {
3605                    child: CanvasRecordId::Shape(ShapeId::from("group")),
3606                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
3607                },
3608                DocumentCommand::AddRecordToGroup {
3609                    group: CanvasRecordId::Shape(ShapeId::from("group")),
3610                    member: CanvasRecordId::Node(NodeId::from("leaf")),
3611                },
3612            ]))
3613            .unwrap();
3614        let mut editor = CanvasEditor::new(document);
3615        editor
3616            .session
3617            .selection
3618            .shapes
3619            .insert(ShapeId::from("frame"));
3620
3621        editor
3622            .handle_event(CanvasEvent::PointerDown {
3623                position: point(px(150.0), px(150.0)),
3624                button: PointerButton::Primary,
3625                modifiers: CanvasKeyModifiers::default(),
3626            })
3627            .unwrap();
3628        editor
3629            .handle_event(CanvasEvent::PointerMove {
3630                position: point(px(160.0), px(170.0)),
3631                modifiers: CanvasKeyModifiers::default(),
3632            })
3633            .unwrap();
3634        editor
3635            .handle_event(CanvasEvent::PointerUp {
3636                position: point(px(160.0), px(170.0)),
3637                button: PointerButton::Primary,
3638                modifiers: CanvasKeyModifiers::default(),
3639            })
3640            .unwrap();
3641
3642        assert_eq!(
3643            editor
3644                .document()
3645                .shape(&ShapeId::from("frame"))
3646                .unwrap()
3647                .bounds
3648                .origin,
3649            point(px(10.0), px(20.0))
3650        );
3651        assert_eq!(
3652            editor
3653                .document()
3654                .shape(&ShapeId::from("group"))
3655                .unwrap()
3656                .bounds
3657                .origin,
3658            point(px(50.0), px(20.0))
3659        );
3660        assert_eq!(
3661            editor
3662                .document()
3663                .node(&NodeId::from("leaf"))
3664                .unwrap()
3665                .position,
3666            point(px(70.0), px(20.0))
3667        );
3668        assert_eq!(editor.history().undo_depth(), 1);
3669    }
3670
3671    #[test]
3672    fn translating_from_related_descendant_keeps_parent_selection() {
3673        let mut document = document_fixture()
3674            .shape(CanvasShape::new(
3675                "frame",
3676                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
3677            ))
3678            .node(CanvasNode::new(
3679                "leaf",
3680                point(px(60.0), px(40.0)),
3681                size(px(20.0), px(20.0)),
3682            ))
3683            .build();
3684        document
3685            .apply_transaction(CanvasTransaction::single(
3686                DocumentCommand::SetRecordParent {
3687                    child: CanvasRecordId::Node(NodeId::from("leaf")),
3688                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
3689                },
3690            ))
3691            .unwrap();
3692        let mut editor = CanvasEditor::new(document);
3693        editor
3694            .session
3695            .selection
3696            .shapes
3697            .insert(ShapeId::from("frame"));
3698
3699        editor
3700            .handle_event(CanvasEvent::PointerDown {
3701                position: point(px(65.0), px(45.0)),
3702                button: PointerButton::Primary,
3703                modifiers: CanvasKeyModifiers::default(),
3704            })
3705            .unwrap();
3706        editor
3707            .handle_event(CanvasEvent::PointerMove {
3708                position: point(px(75.0), px(65.0)),
3709                modifiers: CanvasKeyModifiers::default(),
3710            })
3711            .unwrap();
3712        editor
3713            .handle_event(CanvasEvent::PointerUp {
3714                position: point(px(75.0), px(65.0)),
3715                button: PointerButton::Primary,
3716                modifiers: CanvasKeyModifiers::default(),
3717            })
3718            .unwrap();
3719
3720        assert_eq!(
3721            editor
3722                .session
3723                .selection
3724                .shapes
3725                .iter()
3726                .cloned()
3727                .collect::<Vec<_>>(),
3728            vec![ShapeId::from("frame")]
3729        );
3730        assert!(editor.session.selection.nodes.is_empty());
3731        assert_eq!(
3732            editor
3733                .document()
3734                .shape(&ShapeId::from("frame"))
3735                .unwrap()
3736                .bounds
3737                .origin,
3738            point(px(10.0), px(20.0))
3739        );
3740        assert_eq!(
3741            editor
3742                .document()
3743                .node(&NodeId::from("leaf"))
3744                .unwrap()
3745                .position,
3746            point(px(70.0), px(60.0))
3747        );
3748        assert_eq!(editor.history().undo_depth(), 1);
3749    }
3750
3751    #[test]
3752    fn translating_selected_node_with_shift_locks_to_dominant_axis() {
3753        let document = document_fixture()
3754            .node(CanvasNode::new(
3755                "a",
3756                point(px(0.0), px(0.0)),
3757                size(px(100.0), px(100.0)),
3758            ))
3759            .build();
3760        let mut editor = CanvasEditor::new(document);
3761        editor.session.selection.nodes.insert(NodeId::from("a"));
3762
3763        editor
3764            .handle_event(CanvasEvent::PointerDown {
3765                position: point(px(10.0), px(10.0)),
3766                button: PointerButton::Primary,
3767                modifiers: CanvasKeyModifiers::default(),
3768            })
3769            .unwrap();
3770        editor
3771            .handle_event(CanvasEvent::PointerMove {
3772                position: point(px(20.0), px(30.0)),
3773                modifiers: CanvasKeyModifiers {
3774                    shift: true,
3775                    ..CanvasKeyModifiers::default()
3776                },
3777            })
3778            .unwrap();
3779
3780        assert_eq!(
3781            editor.document().node(&NodeId::from("a")).unwrap().position,
3782            point(px(0.0), px(20.0))
3783        );
3784
3785        editor
3786            .handle_event(CanvasEvent::PointerMove {
3787                position: point(px(80.0), px(35.0)),
3788                modifiers: CanvasKeyModifiers {
3789                    shift: true,
3790                    ..CanvasKeyModifiers::default()
3791                },
3792            })
3793            .unwrap();
3794        editor
3795            .handle_event(CanvasEvent::PointerUp {
3796                position: point(px(80.0), px(35.0)),
3797                button: PointerButton::Primary,
3798                modifiers: CanvasKeyModifiers {
3799                    shift: true,
3800                    ..CanvasKeyModifiers::default()
3801                },
3802            })
3803            .unwrap();
3804
3805        assert_eq!(
3806            editor.document().node(&NodeId::from("a")).unwrap().position,
3807            point(px(0.0), px(25.0))
3808        );
3809        assert_eq!(editor.history().undo_depth(), 1);
3810    }
3811
3812    #[test]
3813    fn translating_selected_node_snaps_to_nearby_alignment() {
3814        let document = document_fixture()
3815            .node(CanvasNode::new(
3816                "active",
3817                point(px(0.0), px(0.0)),
3818                size(px(40.0), px(40.0)),
3819            ))
3820            .node(CanvasNode::new(
3821                "target",
3822                point(px(100.0), px(0.0)),
3823                size(px(40.0), px(40.0)),
3824            ))
3825            .build();
3826        let mut editor = CanvasEditor::new(document);
3827        editor
3828            .session
3829            .selection
3830            .nodes
3831            .insert(NodeId::from("active"));
3832
3833        editor
3834            .handle_event(CanvasEvent::PointerDown {
3835                position: point(px(10.0), px(10.0)),
3836                button: PointerButton::Primary,
3837                modifiers: CanvasKeyModifiers::default(),
3838            })
3839            .unwrap();
3840        editor
3841            .handle_event(CanvasEvent::PointerMove {
3842                position: point(px(106.0), px(10.0)),
3843                modifiers: CanvasKeyModifiers::default(),
3844            })
3845            .unwrap();
3846
3847        assert_eq!(
3848            editor
3849                .document()
3850                .node(&NodeId::from("active"))
3851                .unwrap()
3852                .position,
3853            point(px(100.0), px(0.0))
3854        );
3855        assert!(matches!(
3856            &editor.session.state,
3857            ToolState::Translating { snap_guides, .. } if !snap_guides.is_empty()
3858        ));
3859
3860        editor
3861            .handle_event(CanvasEvent::PointerUp {
3862                position: point(px(106.0), px(10.0)),
3863                button: PointerButton::Primary,
3864                modifiers: CanvasKeyModifiers::default(),
3865            })
3866            .unwrap();
3867        assert_eq!(editor.history().undo_depth(), 1);
3868    }
3869
3870    #[test]
3871    fn select_tool_shift_box_adds_to_base_selection_without_accumulating() {
3872        let document = document_fixture()
3873            .node(CanvasNode::new(
3874                "base",
3875                point(px(200.0), px(200.0)),
3876                size(px(20.0), px(20.0)),
3877            ))
3878            .node(CanvasNode::new(
3879                "inside",
3880                point(px(10.0), px(10.0)),
3881                size(px(20.0), px(20.0)),
3882            ))
3883            .node(CanvasNode::new(
3884                "outside",
3885                point(px(100.0), px(100.0)),
3886                size(px(20.0), px(20.0)),
3887            ))
3888            .build();
3889        let mut editor = CanvasEditor::new(document);
3890        editor.session.selection.nodes.insert(NodeId::from("base"));
3891
3892        editor
3893            .handle_event(CanvasEvent::PointerDown {
3894                position: point(px(0.0), px(0.0)),
3895                button: PointerButton::Primary,
3896                modifiers: CanvasKeyModifiers {
3897                    shift: true,
3898                    ..CanvasKeyModifiers::default()
3899                },
3900            })
3901            .unwrap();
3902        editor
3903            .handle_event(CanvasEvent::PointerMove {
3904                position: point(px(40.0), px(40.0)),
3905                modifiers: CanvasKeyModifiers::default(),
3906            })
3907            .unwrap();
3908
3909        assert_eq!(
3910            editor
3911                .session
3912                .selection
3913                .nodes
3914                .iter()
3915                .cloned()
3916                .collect::<Vec<_>>(),
3917            vec![NodeId::from("base"), NodeId::from("inside")]
3918        );
3919
3920        editor
3921            .handle_event(CanvasEvent::PointerMove {
3922                position: point(px(-40.0), px(-40.0)),
3923                modifiers: CanvasKeyModifiers::default(),
3924            })
3925            .unwrap();
3926
3927        assert_eq!(
3928            editor
3929                .session
3930                .selection
3931                .nodes
3932                .iter()
3933                .cloned()
3934                .collect::<Vec<_>>(),
3935            vec![NodeId::from("base")]
3936        );
3937    }
3938
3939    #[test]
3940    fn translating_selected_nodes_skips_locked_nodes() {
3941        let mut locked = CanvasNode::new(
3942            "locked",
3943            point(px(200.0), px(0.0)),
3944            size(px(100.0), px(100.0)),
3945        );
3946        locked.locked = true;
3947        let document = document_fixture()
3948            .node(CanvasNode::new(
3949                "free",
3950                point(px(0.0), px(0.0)),
3951                size(px(100.0), px(100.0)),
3952            ))
3953            .node(locked)
3954            .build();
3955        let mut editor = CanvasEditor::new(document);
3956        editor.session.selection.nodes.insert(NodeId::from("free"));
3957        editor
3958            .session
3959            .selection
3960            .nodes
3961            .insert(NodeId::from("locked"));
3962
3963        editor
3964            .handle_event(CanvasEvent::PointerDown {
3965                position: point(px(10.0), px(10.0)),
3966                button: PointerButton::Primary,
3967                modifiers: CanvasKeyModifiers::default(),
3968            })
3969            .unwrap();
3970        editor
3971            .handle_event(CanvasEvent::PointerMove {
3972                position: point(px(20.0), px(30.0)),
3973                modifiers: CanvasKeyModifiers::default(),
3974            })
3975            .unwrap();
3976        editor
3977            .handle_event(CanvasEvent::PointerUp {
3978                position: point(px(20.0), px(30.0)),
3979                button: PointerButton::Primary,
3980                modifiers: CanvasKeyModifiers::default(),
3981            })
3982            .unwrap();
3983
3984        assert_eq!(
3985            editor
3986                .document()
3987                .node(&NodeId::from("free"))
3988                .unwrap()
3989                .position,
3990            point(px(10.0), px(20.0))
3991        );
3992        assert_eq!(
3993            editor
3994                .document()
3995                .node(&NodeId::from("locked"))
3996                .unwrap()
3997                .position,
3998            point(px(200.0), px(0.0))
3999        );
4000        assert_eq!(editor.history().undo_depth(), 1);
4001    }
4002
4003    #[test]
4004    fn pan_tool_moves_viewport() {
4005        let mut editor = CanvasEditor::default();
4006        editor.set_tool(CanvasTool::Pan).unwrap();
4007
4008        editor
4009            .handle_event(CanvasEvent::PointerDown {
4010                position: point(px(10.0), px(10.0)),
4011                button: PointerButton::Primary,
4012                modifiers: CanvasKeyModifiers::default(),
4013            })
4014            .unwrap();
4015        editor
4016            .handle_event(CanvasEvent::PointerMove {
4017                position: point(px(20.0), px(25.0)),
4018                modifiers: CanvasKeyModifiers::default(),
4019            })
4020            .unwrap();
4021
4022        assert_eq!(editor.session.viewport.origin, point(px(-10.0), px(-15.0)));
4023    }
4024
4025    #[test]
4026    fn connect_tool_ignores_node_body_endpoints_by_default() {
4027        let document = document_fixture()
4028            .node(CanvasNode::new(
4029                "a",
4030                point(px(0.0), px(0.0)),
4031                size(px(100.0), px(100.0)),
4032            ))
4033            .node(CanvasNode::new(
4034                "b",
4035                point(px(200.0), px(0.0)),
4036                size(px(100.0), px(100.0)),
4037            ))
4038            .build();
4039        let mut editor = CanvasEditor::new(document);
4040        editor.set_tool(CanvasTool::Connect).unwrap();
4041
4042        editor
4043            .handle_event(CanvasEvent::PointerDown {
4044                position: point(px(10.0), px(10.0)),
4045                button: PointerButton::Primary,
4046                modifiers: CanvasKeyModifiers::default(),
4047            })
4048            .unwrap();
4049        editor
4050            .handle_event(CanvasEvent::PointerUp {
4051                position: point(px(210.0), px(10.0)),
4052                button: PointerButton::Primary,
4053                modifiers: CanvasKeyModifiers::default(),
4054            })
4055            .unwrap();
4056
4057        assert_eq!(editor.document().edge_count(), 0);
4058        assert_eq!(editor.history().undo_depth(), 0);
4059        assert_eq!(editor.take_connection_release(), None);
4060    }
4061
4062    #[test]
4063    fn connect_tool_creates_edge_between_policy_node_endpoints() {
4064        let mut a = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4065        a.kind = "whole-node".to_owned();
4066        let mut b = CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4067        b.kind = "whole-node".to_owned();
4068
4069        let document = document_fixture().node(a).node(b).build();
4070        let mut registry = CanvasKindRegistry::default();
4071        registry.register_node_kind("whole-node", whole_node_endpoint_kind());
4072
4073        let mut editor = CanvasEditor::new(document);
4074        editor.set_kind_registry(registry).unwrap();
4075        editor.set_tool(CanvasTool::Connect).unwrap();
4076
4077        editor
4078            .handle_event(CanvasEvent::PointerDown {
4079                position: point(px(10.0), px(10.0)),
4080                button: PointerButton::Primary,
4081                modifiers: CanvasKeyModifiers::default(),
4082            })
4083            .unwrap();
4084        editor
4085            .handle_event(CanvasEvent::PointerUp {
4086                position: point(px(210.0), px(10.0)),
4087                button: PointerButton::Primary,
4088                modifiers: CanvasKeyModifiers::default(),
4089            })
4090            .unwrap();
4091
4092        assert_eq!(editor.document().edge_count(), 1);
4093        assert_eq!(editor.history().undo_depth(), 1);
4094        assert_eq!(
4095            editor.take_connection_release(),
4096            Some(CanvasConnectionRelease::Connected(CanvasConnectedRelease {
4097                source: CanvasEndpoint::new("a", None::<&str>),
4098                target: CanvasEndpoint::new("b", None::<&str>),
4099                edge_id: EdgeId::from("a->b:0"),
4100                position: point(px(210.0), px(10.0)),
4101            }))
4102        );
4103    }
4104
4105    #[test]
4106    fn connect_tool_reports_dropped_release_for_empty_canvas() {
4107        use crate::{CanvasHandle, HandleRole};
4108
4109        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4110        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4111        source_handle.role = HandleRole::Source;
4112        source.handles.push(source_handle);
4113        let target = CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4114        let document = document_fixture().node(source).node(target).build();
4115        let mut editor = CanvasEditor::new(document);
4116        editor.set_tool(CanvasTool::Connect).unwrap();
4117
4118        editor
4119            .handle_event(CanvasEvent::PointerDown {
4120                position: point(px(100.0), px(50.0)),
4121                button: PointerButton::Primary,
4122                modifiers: CanvasKeyModifiers::default(),
4123            })
4124            .unwrap();
4125        editor
4126            .handle_event(CanvasEvent::PointerUp {
4127                position: point(px(320.0), px(180.0)),
4128                button: PointerButton::Primary,
4129                modifiers: CanvasKeyModifiers::default(),
4130            })
4131            .unwrap();
4132
4133        assert_eq!(editor.document().edge_count(), 0);
4134        assert_eq!(
4135            editor.take_connection_release(),
4136            Some(CanvasConnectionRelease::Dropped(
4137                CanvasDroppedConnectionRelease {
4138                    source: CanvasEndpoint::new("a", Some("out")),
4139                    position: point(px(320.0), px(180.0)),
4140                }
4141            ))
4142        );
4143        assert_eq!(editor.take_connection_release(), None);
4144    }
4145
4146    #[test]
4147    fn connect_tool_ignores_locked_endpoints() {
4148        let mut locked =
4149            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4150        locked.locked = true;
4151        let document = document_fixture()
4152            .node(CanvasNode::new(
4153                "a",
4154                point(px(0.0), px(0.0)),
4155                size(px(100.0), px(100.0)),
4156            ))
4157            .node(locked)
4158            .build();
4159        let mut editor = CanvasEditor::new(document);
4160        editor.set_tool(CanvasTool::Connect).unwrap();
4161
4162        editor
4163            .handle_event(CanvasEvent::PointerDown {
4164                position: point(px(10.0), px(10.0)),
4165                button: PointerButton::Primary,
4166                modifiers: CanvasKeyModifiers::default(),
4167            })
4168            .unwrap();
4169        editor
4170            .handle_event(CanvasEvent::PointerUp {
4171                position: point(px(210.0), px(10.0)),
4172                button: PointerButton::Primary,
4173                modifiers: CanvasKeyModifiers::default(),
4174            })
4175            .unwrap();
4176
4177        assert!(editor.document().edge_count() == 0);
4178        assert_eq!(editor.history().undo_depth(), 0);
4179    }
4180
4181    #[test]
4182    fn connect_tool_uses_handles_when_available() {
4183        use crate::{CanvasHandle, HandleId, HandleRole};
4184
4185        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4186        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4187        source_handle.role = HandleRole::Source;
4188        source.handles.push(source_handle);
4189
4190        let mut target =
4191            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4192        let mut target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4193        target_handle.role = HandleRole::Target;
4194        target.handles.push(target_handle);
4195
4196        let document = document_fixture().node(source).node(target).build();
4197        let mut editor = CanvasEditor::new(document);
4198        editor.set_tool(CanvasTool::Connect).unwrap();
4199
4200        editor
4201            .handle_event(CanvasEvent::PointerDown {
4202                position: point(px(100.0), px(50.0)),
4203                button: PointerButton::Primary,
4204                modifiers: CanvasKeyModifiers::default(),
4205            })
4206            .unwrap();
4207        editor
4208            .handle_event(CanvasEvent::PointerUp {
4209                position: point(px(200.0), px(50.0)),
4210                button: PointerButton::Primary,
4211                modifiers: CanvasKeyModifiers::default(),
4212            })
4213            .unwrap();
4214
4215        let edge = editor.document().edges().next().unwrap();
4216        assert_eq!(edge.source.handle_id, Some(HandleId::from("out")));
4217        assert_eq!(edge.target.handle_id, Some(HandleId::from("in")));
4218    }
4219
4220    #[test]
4221    fn pointer_owner_prioritizes_source_handle_before_node_drag() {
4222        use crate::{CanvasHandle, HandleRole};
4223
4224        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4225        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4226        source_handle.role = HandleRole::Source;
4227        source.handles.push(source_handle);
4228
4229        let document = document_fixture().node(source).build();
4230        let editor = CanvasEditor::new(document);
4231
4232        assert_eq!(
4233            editor
4234                .reducer_context()
4235                .pointer_owner_at(point(px(100.0), px(50.0))),
4236            context::CanvasPointerOwner::ConnectionSource(CanvasEndpoint::new("a", Some("out"))),
4237        );
4238    }
4239
4240    #[test]
4241    fn pointer_owner_classifies_node_body_and_empty_pane() {
4242        let node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4243        let document = document_fixture().node(node).build();
4244        let editor = CanvasEditor::new(document);
4245
4246        assert_eq!(
4247            editor
4248                .reducer_context()
4249                .pointer_owner_at(point(px(50.0), px(50.0))),
4250            context::CanvasPointerOwner::NodeDrag(HitTarget::Node(NodeId::from("a"))),
4251        );
4252        assert_eq!(
4253            editor
4254                .reducer_context()
4255                .pointer_owner_at(point(px(150.0), px(150.0))),
4256            context::CanvasPointerOwner::Pane,
4257        );
4258    }
4259
4260    #[test]
4261    fn select_tool_starts_connection_from_source_handle_before_node_drag() {
4262        use crate::{CanvasHandle, HandleId, HandleRole};
4263
4264        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4265        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4266        source_handle.role = HandleRole::Source;
4267        source.handles.push(source_handle);
4268
4269        let mut target =
4270            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4271        let mut target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4272        target_handle.role = HandleRole::Target;
4273        target.handles.push(target_handle);
4274
4275        let document = document_fixture().node(source).node(target).build();
4276        let mut editor = CanvasEditor::new(document);
4277
4278        editor
4279            .handle_event(CanvasEvent::PointerDown {
4280                position: point(px(100.0), px(50.0)),
4281                button: PointerButton::Primary,
4282                modifiers: CanvasKeyModifiers::default(),
4283            })
4284            .unwrap();
4285        assert_eq!(
4286            editor.connection_drag_state(),
4287            Some(CanvasConnectionDragState {
4288                source: CanvasEndpoint::new("a", Some("out")),
4289                current: point(px(100.0), px(50.0)),
4290            })
4291        );
4292
4293        editor
4294            .handle_event(CanvasEvent::PointerUp {
4295                position: point(px(200.0), px(50.0)),
4296                button: PointerButton::Primary,
4297                modifiers: CanvasKeyModifiers::default(),
4298            })
4299            .unwrap();
4300
4301        let edge = editor.document().edges().next().unwrap();
4302        assert_eq!(edge.source.handle_id, Some(HandleId::from("out")));
4303        assert_eq!(edge.target.handle_id, Some(HandleId::from("in")));
4304        assert_eq!(
4305            editor.take_connection_release(),
4306            Some(CanvasConnectionRelease::Connected(CanvasConnectedRelease {
4307                source: CanvasEndpoint::new("a", Some("out")),
4308                target: CanvasEndpoint::new("b", Some("in")),
4309                edge_id: EdgeId::from("a->b:0"),
4310                position: point(px(200.0), px(50.0)),
4311            }))
4312        );
4313    }
4314
4315    #[test]
4316    fn select_tool_reconnects_selected_edge_target_handle() {
4317        use crate::{CanvasHandle, HandleId, HandleRole};
4318
4319        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4320        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4321        source_handle.role = HandleRole::Source;
4322        source.handles.push(source_handle);
4323
4324        let mut first_target =
4325            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4326        let mut first_target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4327        first_target_handle.role = HandleRole::Target;
4328        first_target.handles.push(first_target_handle);
4329
4330        let mut second_target =
4331            CanvasNode::new("c", point(px(400.0), px(0.0)), size(px(100.0), px(100.0)));
4332        let mut second_target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4333        second_target_handle.role = HandleRole::Target;
4334        second_target.handles.push(second_target_handle);
4335
4336        let document = document_fixture()
4337            .node(source)
4338            .node(first_target)
4339            .node(second_target)
4340            .edge(CanvasEdge::new(
4341                "edge",
4342                CanvasEndpoint::new("a", Some("out")),
4343                CanvasEndpoint::new("b", Some("in")),
4344            ))
4345            .build();
4346        let mut editor = CanvasEditor::new(document);
4347        editor
4348            .apply_tool_effect(CanvasToolEffect::AddSelection(HitTarget::Edge(
4349                EdgeId::from("edge"),
4350            )))
4351            .unwrap();
4352
4353        editor
4354            .handle_event(CanvasEvent::PointerDown {
4355                position: point(px(200.0), px(50.0)),
4356                button: PointerButton::Primary,
4357                modifiers: CanvasKeyModifiers::default(),
4358            })
4359            .unwrap();
4360        editor
4361            .handle_event(CanvasEvent::PointerMove {
4362                position: point(px(400.0), px(50.0)),
4363                modifiers: CanvasKeyModifiers::default(),
4364            })
4365            .unwrap();
4366        editor
4367            .handle_event(CanvasEvent::PointerUp {
4368                position: point(px(400.0), px(50.0)),
4369                button: PointerButton::Primary,
4370                modifiers: CanvasKeyModifiers::default(),
4371            })
4372            .unwrap();
4373
4374        let edge = editor.document().edge(&EdgeId::from("edge")).unwrap();
4375        assert_eq!(edge.source.node_id, NodeId::from("a"));
4376        assert_eq!(edge.source.handle_id, Some(HandleId::from("out")));
4377        assert_eq!(edge.target.node_id, NodeId::from("c"));
4378        assert_eq!(edge.target.handle_id, Some(HandleId::from("in")));
4379        assert_eq!(editor.document().edge_count(), 1);
4380        assert_eq!(editor.history().undo_depth(), 1);
4381        assert_eq!(
4382            editor.take_connection_release(),
4383            Some(CanvasConnectionRelease::Reconnected(
4384                CanvasReconnectedRelease {
4385                    edge_id: EdgeId::from("edge"),
4386                    endpoint: CanvasConnectionEndpointRole::Target,
4387                    fixed: CanvasEndpoint::new("a", Some("out")),
4388                    replacement: CanvasEndpoint::new("c", Some("in")),
4389                    position: point(px(400.0), px(50.0)),
4390                }
4391            ))
4392        );
4393    }
4394
4395    #[test]
4396    fn select_tool_reconnects_selected_edge_source_handle() {
4397        use crate::{CanvasHandle, HandleId, HandleRole};
4398
4399        let mut first_source =
4400            CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4401        let mut first_source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4402        first_source_handle.role = HandleRole::Source;
4403        first_source.handles.push(first_source_handle);
4404
4405        let mut second_source =
4406            CanvasNode::new("c", point(px(-200.0), px(0.0)), size(px(100.0), px(100.0)));
4407        let mut second_source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4408        second_source_handle.role = HandleRole::Source;
4409        second_source.handles.push(second_source_handle);
4410
4411        let mut target =
4412            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4413        let mut target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4414        target_handle.role = HandleRole::Target;
4415        target.handles.push(target_handle);
4416
4417        let document = document_fixture()
4418            .node(first_source)
4419            .node(second_source)
4420            .node(target)
4421            .edge(CanvasEdge::new(
4422                "edge",
4423                CanvasEndpoint::new("a", Some("out")),
4424                CanvasEndpoint::new("b", Some("in")),
4425            ))
4426            .build();
4427        let mut editor = CanvasEditor::new(document);
4428        editor
4429            .apply_tool_effect(CanvasToolEffect::AddSelection(HitTarget::Edge(
4430                EdgeId::from("edge"),
4431            )))
4432            .unwrap();
4433
4434        editor
4435            .handle_event(CanvasEvent::PointerDown {
4436                position: point(px(100.0), px(50.0)),
4437                button: PointerButton::Primary,
4438                modifiers: CanvasKeyModifiers::default(),
4439            })
4440            .unwrap();
4441        editor
4442            .handle_event(CanvasEvent::PointerMove {
4443                position: point(px(-100.0), px(50.0)),
4444                modifiers: CanvasKeyModifiers::default(),
4445            })
4446            .unwrap();
4447        editor
4448            .handle_event(CanvasEvent::PointerUp {
4449                position: point(px(-100.0), px(50.0)),
4450                button: PointerButton::Primary,
4451                modifiers: CanvasKeyModifiers::default(),
4452            })
4453            .unwrap();
4454
4455        let edge = editor.document().edge(&EdgeId::from("edge")).unwrap();
4456        assert_eq!(edge.source.node_id, NodeId::from("c"));
4457        assert_eq!(edge.source.handle_id, Some(HandleId::from("out")));
4458        assert_eq!(edge.target.node_id, NodeId::from("b"));
4459        assert_eq!(edge.target.handle_id, Some(HandleId::from("in")));
4460        assert_eq!(editor.document().edge_count(), 1);
4461        assert_eq!(editor.history().undo_depth(), 1);
4462        assert_eq!(
4463            editor.take_connection_release(),
4464            Some(CanvasConnectionRelease::Reconnected(
4465                CanvasReconnectedRelease {
4466                    edge_id: EdgeId::from("edge"),
4467                    endpoint: CanvasConnectionEndpointRole::Source,
4468                    fixed: CanvasEndpoint::new("b", Some("in")),
4469                    replacement: CanvasEndpoint::new("c", Some("out")),
4470                    position: point(px(-100.0), px(50.0)),
4471                }
4472            ))
4473        );
4474    }
4475
4476    #[test]
4477    fn select_tool_reconnect_pointer_down_clears_stale_release() {
4478        use crate::{CanvasHandle, HandleRole};
4479
4480        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4481        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4482        source_handle.role = HandleRole::Source;
4483        source.handles.push(source_handle);
4484
4485        let mut target =
4486            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4487        let mut target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4488        target_handle.role = HandleRole::Target;
4489        target.handles.push(target_handle);
4490
4491        let document = document_fixture()
4492            .node(source)
4493            .node(target)
4494            .edge(CanvasEdge::new(
4495                "edge",
4496                CanvasEndpoint::new("a", Some("out")),
4497                CanvasEndpoint::new("b", Some("in")),
4498            ))
4499            .build();
4500        let mut editor = CanvasEditor::new(document);
4501        editor
4502            .apply_tool_effect(CanvasToolEffect::AddSelection(HitTarget::Edge(
4503                EdgeId::from("edge"),
4504            )))
4505            .unwrap();
4506        editor
4507            .apply_tool_effect(CanvasToolEffect::SetConnectionRelease(Some(
4508                CanvasConnectionRelease::Rejected(CanvasRejectedConnectionRelease {
4509                    reason: CanvasConnectionRejectReason::InvalidTarget,
4510                    source: None,
4511                    edge_id: Some(EdgeId::from("edge")),
4512                    endpoint: Some(CanvasConnectionEndpointRole::Target),
4513                    position: point(px(999.0), px(999.0)),
4514                }),
4515            )))
4516            .unwrap();
4517
4518        editor
4519            .handle_event(CanvasEvent::PointerDown {
4520                position: point(px(200.0), px(50.0)),
4521                button: PointerButton::Primary,
4522                modifiers: CanvasKeyModifiers::default(),
4523            })
4524            .unwrap();
4525
4526        assert_eq!(editor.take_connection_release(), None);
4527    }
4528
4529    #[test]
4530    fn select_tool_reports_dropped_reconnect_release_for_empty_canvas() {
4531        use crate::{CanvasHandle, HandleId, HandleRole};
4532
4533        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4534        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4535        source_handle.role = HandleRole::Source;
4536        source.handles.push(source_handle);
4537
4538        let mut target =
4539            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4540        let mut target_handle = CanvasHandle::new("in", point(px(0.0), px(50.0)));
4541        target_handle.role = HandleRole::Target;
4542        target.handles.push(target_handle);
4543
4544        let document = document_fixture()
4545            .node(source)
4546            .node(target)
4547            .edge(CanvasEdge::new(
4548                "edge",
4549                CanvasEndpoint::new("a", Some("out")),
4550                CanvasEndpoint::new("b", Some("in")),
4551            ))
4552            .build();
4553        let mut editor = CanvasEditor::new(document);
4554        editor
4555            .apply_tool_effect(CanvasToolEffect::AddSelection(HitTarget::Edge(
4556                EdgeId::from("edge"),
4557            )))
4558            .unwrap();
4559
4560        editor
4561            .handle_event(CanvasEvent::PointerDown {
4562                position: point(px(200.0), px(50.0)),
4563                button: PointerButton::Primary,
4564                modifiers: CanvasKeyModifiers::default(),
4565            })
4566            .unwrap();
4567        editor
4568            .handle_event(CanvasEvent::PointerUp {
4569                position: point(px(340.0), px(180.0)),
4570                button: PointerButton::Primary,
4571                modifiers: CanvasKeyModifiers::default(),
4572            })
4573            .unwrap();
4574
4575        let edge = editor.document().edge(&EdgeId::from("edge")).unwrap();
4576        assert_eq!(edge.source.node_id, NodeId::from("a"));
4577        assert_eq!(edge.source.handle_id, Some(HandleId::from("out")));
4578        assert_eq!(edge.target.node_id, NodeId::from("b"));
4579        assert_eq!(edge.target.handle_id, Some(HandleId::from("in")));
4580        assert_eq!(editor.history().undo_depth(), 0);
4581        assert_eq!(
4582            editor.take_connection_release(),
4583            Some(CanvasConnectionRelease::ReconnectDropped(
4584                CanvasDroppedReconnectRelease {
4585                    edge_id: EdgeId::from("edge"),
4586                    endpoint: CanvasConnectionEndpointRole::Target,
4587                    fixed: CanvasEndpoint::new("a", Some("out")),
4588                    position: point(px(340.0), px(180.0)),
4589                }
4590            ))
4591        );
4592    }
4593
4594    #[test]
4595    fn connect_tool_exposes_read_only_drag_state() {
4596        use crate::{CanvasHandle, HandleId, HandleRole};
4597
4598        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4599        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4600        source_handle.role = HandleRole::Source;
4601        source.handles.push(source_handle);
4602        let target = CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4603        let document = document_fixture().node(source).node(target).build();
4604        let mut editor = CanvasEditor::new(document);
4605        editor.set_tool(CanvasTool::Connect).unwrap();
4606
4607        editor
4608            .handle_event(CanvasEvent::PointerDown {
4609                position: point(px(100.0), px(50.0)),
4610                button: PointerButton::Primary,
4611                modifiers: CanvasKeyModifiers::default(),
4612            })
4613            .unwrap();
4614
4615        let drag = editor
4616            .connection_drag_state()
4617            .expect("connect drag should expose source state");
4618        assert_eq!(drag.source.node_id, NodeId::from("a"));
4619        assert_eq!(drag.source.handle_id, Some(HandleId::from("out")));
4620        assert_eq!(drag.current, point(px(100.0), px(50.0)));
4621    }
4622
4623    #[test]
4624    fn connect_tool_does_not_start_from_target_only_handle() {
4625        use crate::{CanvasHandle, HandleRole};
4626
4627        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4628        let mut target_only = CanvasHandle::new("in", point(px(100.0), px(50.0)));
4629        target_only.role = HandleRole::Target;
4630        source.handles.push(target_only);
4631        let target = CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4632
4633        let document = document_fixture().node(source).node(target).build();
4634        let mut editor = CanvasEditor::new(document);
4635        editor.set_tool(CanvasTool::Connect).unwrap();
4636
4637        editor
4638            .handle_event(CanvasEvent::PointerDown {
4639                position: point(px(100.0), px(50.0)),
4640                button: PointerButton::Primary,
4641                modifiers: CanvasKeyModifiers::default(),
4642            })
4643            .unwrap();
4644        editor
4645            .handle_event(CanvasEvent::PointerUp {
4646                position: point(px(210.0), px(10.0)),
4647                button: PointerButton::Primary,
4648                modifiers: CanvasKeyModifiers::default(),
4649            })
4650            .unwrap();
4651
4652        assert!(matches!(editor.session.state, ToolState::Idle));
4653        assert!(editor.document().edge_count() == 0);
4654        assert_eq!(editor.history().undo_depth(), 0);
4655        assert_eq!(
4656            editor.take_connection_release(),
4657            Some(CanvasConnectionRelease::Rejected(
4658                CanvasRejectedConnectionRelease {
4659                    reason: CanvasConnectionRejectReason::InvalidSource,
4660                    source: None,
4661                    edge_id: None,
4662                    endpoint: Some(CanvasConnectionEndpointRole::Source),
4663                    position: point(px(100.0), px(50.0)),
4664                }
4665            ))
4666        );
4667    }
4668
4669    #[test]
4670    fn connect_tool_does_not_end_on_source_only_handle() {
4671        use crate::{CanvasHandle, HandleRole};
4672
4673        let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
4674        let mut source_handle = CanvasHandle::new("out", point(px(100.0), px(50.0)));
4675        source_handle.role = HandleRole::Source;
4676        source.handles.push(source_handle);
4677
4678        let mut target =
4679            CanvasNode::new("b", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
4680        let mut invalid_target_handle = CanvasHandle::new("out", point(px(0.0), px(50.0)));
4681        invalid_target_handle.role = HandleRole::Source;
4682        target.handles.push(invalid_target_handle);
4683
4684        let document = document_fixture().node(source).node(target).build();
4685        let mut editor = CanvasEditor::new(document);
4686        editor.set_tool(CanvasTool::Connect).unwrap();
4687
4688        editor
4689            .handle_event(CanvasEvent::PointerDown {
4690                position: point(px(100.0), px(50.0)),
4691                button: PointerButton::Primary,
4692                modifiers: CanvasKeyModifiers::default(),
4693            })
4694            .unwrap();
4695        editor
4696            .handle_event(CanvasEvent::PointerUp {
4697                position: point(px(200.0), px(50.0)),
4698                button: PointerButton::Primary,
4699                modifiers: CanvasKeyModifiers::default(),
4700            })
4701            .unwrap();
4702
4703        assert!(matches!(editor.session.state, ToolState::Idle));
4704        assert!(editor.document().edge_count() == 0);
4705        assert_eq!(editor.history().undo_depth(), 0);
4706    }
4707
4708    #[test]
4709    fn custom_tool_reducer_applies_effects_through_editor() {
4710        let document = document_fixture()
4711            .node(CanvasNode::new(
4712                "anchor",
4713                point(px(100.0), px(50.0)),
4714                size(px(80.0), px(80.0)),
4715            ))
4716            .build();
4717        let mut editor = CanvasEditor::new(document);
4718        editor.session.viewport = CanvasViewport::new(point(px(100.0), px(50.0)), 2.0).unwrap();
4719        editor.set_tool(CanvasTool::custom("stamp")).unwrap();
4720        let mut tool = StampTool::default();
4721
4722        editor
4723            .handle_event_with_custom_tool(
4724                CanvasEvent::PointerDown {
4725                    position: point(px(20.0), px(10.0)),
4726                    button: PointerButton::Primary,
4727                    modifiers: CanvasKeyModifiers::default(),
4728                },
4729                &mut tool,
4730            )
4731            .unwrap();
4732
4733        assert_eq!(tool.calls, 1);
4734        assert_eq!(tool.last_tool_id, Some(CanvasToolId::from("stamp")));
4735        assert_eq!(tool.last_hit, Some(HitTarget::Node(NodeId::from("anchor"))));
4736
4737        let stamped = editor.document().node(&NodeId::from("stamp-1")).unwrap();
4738        assert_eq!(stamped.position, point(px(110.0), px(55.0)));
4739        assert_eq!(editor.history().undo_depth(), 1);
4740        assert_eq!(
4741            editor
4742                .session
4743                .selection
4744                .nodes
4745                .iter()
4746                .cloned()
4747                .collect::<Vec<_>>(),
4748            vec![NodeId::from("stamp-1")]
4749        );
4750        assert_eq!(editor.session.state, ToolState::Idle);
4751
4752        assert!(editor.undo().unwrap());
4753        assert!(!editor.document().contains_node(&NodeId::from("stamp-1")));
4754    }
4755
4756    #[test]
4757    fn custom_tool_context_exposes_selection_record_scope() {
4758        #[derive(Clone)]
4759        struct ScopeProbeTool {
4760            observed: Arc<Mutex<Vec<CanvasRecordId>>>,
4761        }
4762
4763        impl CanvasToolReducer for ScopeProbeTool {
4764            fn handle_event(
4765                &mut self,
4766                context: CanvasToolContext<'_>,
4767                _event: CanvasEvent,
4768            ) -> Result<Vec<CanvasToolIntent>, DocumentError> {
4769                let scope = context
4770                    .selection_record_scope(
4771                        CanvasRecordScopeOptions::structural_with_internal_edges(),
4772                    )
4773                    .records()
4774                    .cloned()
4775                    .collect::<Vec<_>>();
4776                *self.observed.lock().unwrap() = scope;
4777                Ok(Vec::new())
4778            }
4779        }
4780
4781        let mut document = document_fixture()
4782            .shape(CanvasShape::new(
4783                "frame",
4784                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
4785            ))
4786            .node(CanvasNode::new(
4787                "child",
4788                point(px(20.0), px(20.0)),
4789                size(px(40.0), px(40.0)),
4790            ))
4791            .node(CanvasNode::new(
4792                "peer",
4793                point(px(80.0), px(20.0)),
4794                size(px(40.0), px(40.0)),
4795            ))
4796            .edge(CanvasEdge::new(
4797                "child-peer",
4798                CanvasEndpoint::new("child", None::<&str>),
4799                CanvasEndpoint::new("peer", None::<&str>),
4800            ))
4801            .build();
4802        document
4803            .apply_transaction(CanvasTransaction::new([
4804                DocumentCommand::SetRecordParent {
4805                    child: CanvasRecordId::Node(NodeId::from("child")),
4806                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
4807                },
4808                DocumentCommand::AddRecordToGroup {
4809                    group: CanvasRecordId::Shape(ShapeId::from("frame")),
4810                    member: CanvasRecordId::Node(NodeId::from("peer")),
4811                },
4812            ]))
4813            .unwrap();
4814
4815        let observed = Arc::new(Mutex::new(Vec::new()));
4816        let mut editor = CanvasEditor::new(document);
4817        editor.set_tool(CanvasTool::custom("probe")).unwrap();
4818        editor
4819            .session
4820            .selection
4821            .shapes
4822            .insert(ShapeId::from("frame"));
4823        let mut tool = ScopeProbeTool {
4824            observed: Arc::clone(&observed),
4825        };
4826
4827        editor
4828            .handle_event_with_custom_tool(
4829                CanvasEvent::PointerDown {
4830                    position: point(px(10.0), px(10.0)),
4831                    button: PointerButton::Primary,
4832                    modifiers: CanvasKeyModifiers::default(),
4833                },
4834                &mut tool,
4835            )
4836            .unwrap();
4837
4838        assert_eq!(
4839            *observed.lock().unwrap(),
4840            vec![
4841                CanvasRecordId::Shape(ShapeId::from("frame")),
4842                CanvasRecordId::Node(NodeId::from("child")),
4843                CanvasRecordId::Node(NodeId::from("peer")),
4844                CanvasRecordId::Edge(EdgeId::from("child-peer")),
4845            ]
4846        );
4847    }
4848
4849    #[test]
4850    fn custom_tool_entry_uses_builtin_tools_without_calling_custom_reducer() {
4851        let document = document_fixture()
4852            .node(CanvasNode::new(
4853                "n1",
4854                point(px(0.0), px(0.0)),
4855                size(px(100.0), px(100.0)),
4856            ))
4857            .build();
4858        let mut editor = CanvasEditor::new(document);
4859        let mut tool = StampTool::default();
4860
4861        editor
4862            .handle_event_with_custom_tool(
4863                CanvasEvent::PointerDown {
4864                    position: point(px(10.0), px(10.0)),
4865                    button: PointerButton::Primary,
4866                    modifiers: CanvasKeyModifiers::default(),
4867                },
4868                &mut tool,
4869            )
4870            .unwrap();
4871
4872        assert_eq!(tool.calls, 0);
4873        assert_eq!(
4874            editor
4875                .session
4876                .selection
4877                .nodes
4878                .iter()
4879                .cloned()
4880                .collect::<Vec<_>>(),
4881            vec![NodeId::from("n1")]
4882        );
4883    }
4884
4885    #[test]
4886    fn tool_registry_dispatches_registered_custom_tool() {
4887        let document = document_fixture()
4888            .node(CanvasNode::new(
4889                "anchor",
4890                point(px(100.0), px(50.0)),
4891                size(px(80.0), px(80.0)),
4892            ))
4893            .build();
4894        let mut editor = CanvasEditor::new(document);
4895        editor.session.viewport = CanvasViewport::new(point(px(100.0), px(50.0)), 2.0).unwrap();
4896        editor.set_tool(CanvasTool::custom("stamp")).unwrap();
4897        let mut registry = CanvasToolRegistry::new();
4898
4899        assert!(registry.is_empty());
4900        assert!(registry.insert("stamp", StampTool::default()).is_none());
4901        assert!(registry.contains(&CanvasToolId::from("stamp")));
4902        assert_eq!(
4903            registry.ids().cloned().collect::<Vec<_>>(),
4904            vec![CanvasToolId::from("stamp")]
4905        );
4906
4907        editor
4908            .handle_event_with_tool_registry(
4909                CanvasEvent::PointerDown {
4910                    position: point(px(20.0), px(10.0)),
4911                    button: PointerButton::Primary,
4912                    modifiers: CanvasKeyModifiers::default(),
4913                },
4914                &mut registry,
4915            )
4916            .unwrap();
4917
4918        let stamped = editor.document().node(&NodeId::from("stamp-1")).unwrap();
4919        assert_eq!(stamped.position, point(px(110.0), px(55.0)));
4920        assert_eq!(editor.history().undo_depth(), 1);
4921        assert!(registry.remove(&CanvasToolId::from("stamp")).is_some());
4922        assert!(!registry.contains(&CanvasToolId::from("stamp")));
4923    }
4924
4925    #[test]
4926    fn tool_registry_accepts_boxed_reducers() {
4927        let mut registry = CanvasToolRegistry::new();
4928
4929        assert!(
4930            registry
4931                .insert_boxed("stamp", Box::new(StampTool::default()))
4932                .is_none()
4933        );
4934
4935        assert_eq!(registry.len(), 1);
4936        assert!(registry.reducer_mut(&CanvasToolId::from("stamp")).is_some());
4937    }
4938
4939    #[test]
4940    fn tool_registry_reports_missing_custom_tool() {
4941        let mut editor = CanvasEditor::default();
4942        editor.set_tool(CanvasTool::custom("missing")).unwrap();
4943        let mut registry = CanvasToolRegistry::new();
4944
4945        let err = editor
4946            .handle_event_with_tool_registry(CanvasEvent::Cancel, &mut registry)
4947            .unwrap_err();
4948
4949        assert_eq!(
4950            err,
4951            CanvasToolRegistryError::MissingTool(CanvasToolId::from("missing"))
4952        );
4953    }
4954
4955    #[test]
4956    fn tool_registry_entry_uses_builtin_tools_without_registered_reducer() {
4957        let document = document_fixture()
4958            .node(CanvasNode::new(
4959                "n1",
4960                point(px(0.0), px(0.0)),
4961                size(px(100.0), px(100.0)),
4962            ))
4963            .build();
4964        let mut editor = CanvasEditor::new(document);
4965        let mut registry = CanvasToolRegistry::new();
4966
4967        editor
4968            .handle_event_with_tool_registry(
4969                CanvasEvent::PointerDown {
4970                    position: point(px(10.0), px(10.0)),
4971                    button: PointerButton::Primary,
4972                    modifiers: CanvasKeyModifiers::default(),
4973                },
4974                &mut registry,
4975            )
4976            .unwrap();
4977
4978        assert_eq!(
4979            editor
4980                .session
4981                .selection
4982                .nodes
4983                .iter()
4984                .cloned()
4985                .collect::<Vec<_>>(),
4986            vec![NodeId::from("n1")]
4987        );
4988    }
4989
4990    #[test]
4991    fn set_tool_effect_switches_tool_and_resets_state() {
4992        let mut editor = CanvasEditor::default();
4993        editor.session.state = ToolState::Pointing {
4994            origin: point(px(10.0), px(20.0)),
4995            selection_mode: CanvasSelectionMode::Replace,
4996            base_selection: CanvasSelection::default(),
4997        };
4998
4999        editor
5000            .apply_tool_effect(CanvasToolEffect::SetTool(CanvasTool::custom("stamp")))
5001            .unwrap();
5002
5003        assert_eq!(editor.session.tool, CanvasTool::custom("stamp"));
5004        assert_eq!(editor.session.state, ToolState::Idle);
5005    }
5006
5007    #[test]
5008    fn direct_transactions_clear_redo_history() {
5009        let mut editor = CanvasEditor::default();
5010        editor
5011            .apply(DocumentCommand::InsertNode(CanvasNode::new(
5012                "a",
5013                point(px(0.0), px(0.0)),
5014                size(px(100.0), px(100.0)),
5015            )))
5016            .unwrap();
5017
5018        assert!(editor.undo().unwrap());
5019        assert_eq!(editor.history().redo_depth(), 1);
5020
5021        editor
5022            .apply(DocumentCommand::InsertNode(CanvasNode::new(
5023                "b",
5024                point(px(100.0), px(0.0)),
5025                size(px(100.0), px(100.0)),
5026            )))
5027            .unwrap();
5028
5029        assert_eq!(editor.history().undo_depth(), 1);
5030        assert_eq!(editor.history().redo_depth(), 0);
5031        assert!(editor.document().contains_node(&NodeId::from("b")));
5032        assert!(!editor.document().contains_node(&NodeId::from("a")));
5033    }
5034
5035    #[test]
5036    fn no_op_committed_transactions_do_not_push_history_or_clear_redo() {
5037        let document = document_fixture()
5038            .node(CanvasNode::new(
5039                "child",
5040                point(px(0.0), px(0.0)),
5041                size(px(10.0), px(10.0)),
5042            ))
5043            .shape(CanvasShape::new(
5044                "frame",
5045                Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
5046            ))
5047            .build();
5048        let mut editor = CanvasEditor::new(document);
5049        let relation_transaction = CanvasTransaction::single(DocumentCommand::SetRecordParent {
5050            child: CanvasRecordId::Node(NodeId::from("child")),
5051            parent: CanvasRecordId::Shape(ShapeId::from("frame")),
5052        });
5053
5054        let first_diff = editor
5055            .apply_transaction_with_diff(relation_transaction.clone())
5056            .unwrap();
5057        assert!(!first_diff.is_empty());
5058        assert_eq!(editor.history().undo_depth(), 1);
5059
5060        assert!(editor.undo().unwrap());
5061        assert_eq!(editor.history().undo_depth(), 0);
5062        assert_eq!(editor.history().redo_depth(), 1);
5063
5064        let second_diff = editor
5065            .apply_transaction_with_diff(CanvasTransaction::single(
5066                DocumentCommand::ClearRecordParent {
5067                    child: CanvasRecordId::Node(NodeId::from("child")),
5068                },
5069            ))
5070            .unwrap();
5071
5072        assert!(second_diff.is_empty());
5073        assert_eq!(editor.history().undo_depth(), 0);
5074        assert_eq!(editor.history().redo_depth(), 1);
5075    }
5076
5077    #[test]
5078    fn relation_order_only_transactions_do_not_push_history() {
5079        let document = document_fixture()
5080            .node(CanvasNode::new(
5081                "member",
5082                point(px(0.0), px(0.0)),
5083                size(px(10.0), px(10.0)),
5084            ))
5085            .build();
5086        let mut document = document;
5087        for id in ["group-a", "group-b"] {
5088            document
5089                .insert_shape(CanvasShape::new(
5090                    id,
5091                    Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
5092                ))
5093                .unwrap();
5094        }
5095        let member = CanvasRecordId::Node(NodeId::from("member"));
5096        let group_a = CanvasRecordId::Shape(ShapeId::from("group-a"));
5097        let group_b = CanvasRecordId::Shape(ShapeId::from("group-b"));
5098        document
5099            .apply_transaction(CanvasTransaction::new([
5100                DocumentCommand::AddRecordToGroup {
5101                    group: group_a.clone(),
5102                    member: member.clone(),
5103                },
5104                DocumentCommand::AddRecordToGroup {
5105                    group: group_b,
5106                    member: member.clone(),
5107                },
5108            ]))
5109            .unwrap();
5110        let mut editor = CanvasEditor::new(document);
5111
5112        let diff = editor
5113            .apply_transaction_with_diff(CanvasTransaction::new([
5114                DocumentCommand::RemoveRecordFromGroup {
5115                    group: group_a.clone(),
5116                    member: member.clone(),
5117                },
5118                DocumentCommand::AddRecordToGroup {
5119                    group: group_a,
5120                    member,
5121                },
5122            ]))
5123            .unwrap();
5124
5125        assert!(diff.is_empty());
5126        assert_eq!(editor.history().undo_depth(), 0);
5127    }
5128
5129    #[test]
5130    fn no_op_undo_and_redo_discard_stale_history_entries() {
5131        let document = document_fixture()
5132            .node(CanvasNode::new(
5133                "child",
5134                point(px(0.0), px(0.0)),
5135                size(px(10.0), px(10.0)),
5136            ))
5137            .build();
5138        let mut editor = CanvasEditor::new(document);
5139        let noop = CanvasTransaction::single(DocumentCommand::ClearRecordParent {
5140            child: CanvasRecordId::Node(NodeId::from("child")),
5141        });
5142        editor.history_mut_for_test().push_undo(noop.clone());
5143        editor.history_mut_for_test().push_redo(noop);
5144
5145        assert!(!editor.undo().unwrap());
5146        assert_eq!(editor.history().undo_depth(), 0);
5147        assert_eq!(editor.history().redo_depth(), 1);
5148
5149        assert!(!editor.redo().unwrap());
5150        assert_eq!(editor.history().undo_depth(), 0);
5151        assert_eq!(editor.history().redo_depth(), 0);
5152    }
5153
5154    #[test]
5155    fn editor_transactions_return_document_diff() {
5156        let mut editor = CanvasEditor::default();
5157
5158        let diff = editor
5159            .apply_transaction_with_diff(CanvasTransaction::single(DocumentCommand::InsertNode(
5160                CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
5161            )))
5162            .unwrap();
5163
5164        assert_eq!(
5165            diff.inserted.iter().cloned().collect::<Vec<_>>(),
5166            vec![crate::CanvasRecordId::Node(NodeId::from("a"))]
5167        );
5168        assert!(editor.history().can_undo());
5169    }
5170
5171    #[test]
5172    fn editor_kind_registry_normalizes_and_validates_transactions() {
5173        let mut registry = CanvasKindRegistry::open();
5174        registry.register_node_kind("note", required_title_node_kind());
5175        let mut editor =
5176            CanvasEditor::try_new_with_kind_registry(document_fixture().build(), registry).unwrap();
5177
5178        let mut note = CanvasNode::new("note", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5179        note.kind = "note".to_string();
5180        note.data.insert("label".to_string(), json!("Migrated"));
5181
5182        editor.apply(DocumentCommand::InsertNode(note)).unwrap();
5183
5184        assert_eq!(
5185            editor
5186                .document()
5187                .node(&NodeId::from("note"))
5188                .unwrap()
5189                .data
5190                .get("title"),
5191            Some(&json!("Migrated"))
5192        );
5193        assert_eq!(editor.history().undo_depth(), 1);
5194
5195        let mut invalid = CanvasNode::new(
5196            "invalid",
5197            point(px(0.0), px(0.0)),
5198            size(px(100.0), px(100.0)),
5199        );
5200        invalid.kind = "note".to_string();
5201        invalid.data.insert("title".to_string(), json!(false));
5202        let err = editor
5203            .apply(DocumentCommand::InsertNode(invalid))
5204            .unwrap_err();
5205
5206        assert!(matches!(
5207            err,
5208            DocumentError::Schema(CanvasSchemaError::InvalidData {
5209                record_kind: CanvasRecordKind::Node,
5210                record_id: crate::CanvasRecordId::Node(id),
5211                kind,
5212                ..
5213            }) if id == NodeId::from("invalid") && kind == "note"
5214        ));
5215        assert!(!editor.document().contains_node(&NodeId::from("invalid")));
5216        assert_eq!(editor.history().undo_depth(), 1);
5217    }
5218
5219    #[test]
5220    fn editor_set_kind_registry_normalizes_document_and_clears_stale_history() {
5221        let mut note = CanvasNode::new("note", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5222        note.kind = "note".to_string();
5223        note.data.insert("label".to_string(), json!("Migrated"));
5224        let mut editor = CanvasEditor::default();
5225        editor.apply(DocumentCommand::InsertNode(note)).unwrap();
5226        assert_eq!(editor.history().undo_depth(), 1);
5227
5228        let mut registry = CanvasKindRegistry::open();
5229        registry.register_node_kind("note", required_title_node_kind());
5230        editor.set_kind_registry(registry).unwrap();
5231
5232        assert_eq!(
5233            editor
5234                .document()
5235                .node(&NodeId::from("note"))
5236                .unwrap()
5237                .data
5238                .get("title"),
5239            Some(&json!("Migrated"))
5240        );
5241        assert_eq!(editor.history().undo_depth(), 0);
5242        assert!(
5243            editor
5244                .runtime()
5245                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5246                .next()
5247                .is_some()
5248        );
5249    }
5250
5251    #[test]
5252    fn editor_set_kind_registry_rejects_invalid_existing_document_atomically() {
5253        let mut note = CanvasNode::new("note", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5254        note.kind = "note".to_string();
5255        note.data.insert("title".to_string(), json!(false));
5256        let document = document_fixture().node(note).build();
5257        let mut editor = CanvasEditor::new(document);
5258
5259        let mut registry = CanvasKindRegistry::open();
5260        registry.register_node_kind("note", required_title_node_kind());
5261        let err = editor.set_kind_registry(registry).unwrap_err();
5262
5263        assert!(matches!(
5264            err,
5265            DocumentError::Schema(CanvasSchemaError::InvalidData {
5266                record_id: crate::CanvasRecordId::Node(id),
5267                ..
5268            }) if id == NodeId::from("note")
5269        ));
5270        assert_eq!(
5271            editor
5272                .document()
5273                .node(&NodeId::from("note"))
5274                .unwrap()
5275                .data
5276                .get("title"),
5277            Some(&json!(false))
5278        );
5279        assert!(editor.kind_registry().node_kind("note").is_none());
5280    }
5281
5282    #[test]
5283    fn tool_effect_applies_recorded_transaction() {
5284        let mut editor = CanvasEditor::default();
5285
5286        editor
5287            .apply_tool_effect(CanvasToolEffect::ApplyTransaction(
5288                CanvasTransaction::single(DocumentCommand::InsertNode(CanvasNode::new(
5289                    "a",
5290                    point(px(0.0), px(0.0)),
5291                    size(px(100.0), px(100.0)),
5292                ))),
5293            ))
5294            .unwrap();
5295
5296        assert!(editor.document().contains_node(&NodeId::from("a")));
5297        assert_eq!(editor.history().undo_depth(), 1);
5298        assert!(
5299            editor
5300                .runtime()
5301                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5302                .next()
5303                .is_some()
5304        );
5305    }
5306
5307    #[test]
5308    fn tool_effect_updates_gesture_without_history() {
5309        let mut editor = CanvasEditor::default();
5310
5311        editor
5312            .apply_tool_effect(CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5313                DocumentCommand::InsertNode(CanvasNode::new(
5314                    "a",
5315                    point(px(0.0), px(0.0)),
5316                    size(px(100.0), px(100.0)),
5317                )),
5318            )))
5319            .unwrap();
5320
5321        assert!(editor.document().contains_node(&NodeId::from("a")));
5322        assert_eq!(editor.history().undo_depth(), 0);
5323        assert!(
5324            editor
5325                .runtime()
5326                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5327                .next()
5328                .is_some()
5329        );
5330    }
5331
5332    #[test]
5333    fn gesture_update_uses_kind_registry_validation() {
5334        let mut registry = CanvasKindRegistry::open();
5335        registry.register_node_kind("note", required_title_node_kind());
5336        let mut editor =
5337            CanvasEditor::try_new_with_kind_registry(document_fixture().build(), registry).unwrap();
5338        let mut note = CanvasNode::new("note", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5339        note.kind = "note".to_string();
5340        note.data.insert("title".to_string(), json!("Valid"));
5341        editor
5342            .apply(DocumentCommand::InsertNode(note.clone()))
5343            .unwrap();
5344
5345        let mut invalid = note.clone();
5346        invalid.data.insert("title".to_string(), json!(false));
5347        let err = editor
5348            .apply_tool_effects([
5349                CanvasToolEffect::BeginGesture,
5350                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5351                    DocumentCommand::UpdateNode(invalid),
5352                )),
5353            ])
5354            .unwrap_err();
5355
5356        assert!(matches!(
5357            err,
5358            DocumentError::Schema(CanvasSchemaError::InvalidData {
5359                record_id: crate::CanvasRecordId::Node(id),
5360                ..
5361            }) if id == NodeId::from("note")
5362        ));
5363        assert_eq!(
5364            editor.document().node(&NodeId::from("note")).unwrap(),
5365            &note
5366        );
5367        assert_eq!(editor.history().undo_depth(), 1);
5368    }
5369
5370    #[test]
5371    fn gesture_commit_pushes_one_undo_entry() {
5372        let mut editor = CanvasEditor::default();
5373        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5374        let first = CanvasNode::new("a", point(px(12.0), px(0.0)), size(px(100.0), px(100.0)));
5375        let second = CanvasNode::new("a", point(px(40.0), px(0.0)), size(px(100.0), px(100.0)));
5376        editor
5377            .apply(DocumentCommand::InsertNode(original.clone()))
5378            .unwrap();
5379
5380        editor
5381            .apply_tool_effects([
5382                CanvasToolEffect::BeginGesture,
5383                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5384                    DocumentCommand::UpdateNode(first),
5385                )),
5386                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5387                    DocumentCommand::UpdateNode(second.clone()),
5388                )),
5389                CanvasToolEffect::CommitGesture,
5390            ])
5391            .unwrap();
5392
5393        assert_eq!(editor.document().node(&NodeId::from("a")).unwrap(), &second);
5394        assert_eq!(editor.history().undo_depth(), 2);
5395        assert!(editor.undo().unwrap());
5396        assert_eq!(
5397            editor.document().node(&NodeId::from("a")).unwrap(),
5398            &original
5399        );
5400    }
5401
5402    #[test]
5403    fn gesture_updates_notify_listeners_only_on_commit() {
5404        let mut editor = CanvasEditor::default();
5405        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5406        let moved = CanvasNode::new("a", point(px(40.0), px(0.0)), size(px(100.0), px(100.0)));
5407        editor
5408            .apply(DocumentCommand::InsertNode(original.clone()))
5409            .unwrap();
5410        let baseline_depth = editor.history().undo_depth();
5411        let changes = Arc::new(Mutex::new(Vec::new()));
5412        let observed = Arc::clone(&changes);
5413        editor.listen(move |change| observed.lock().unwrap().push(change.clone()));
5414
5415        editor
5416            .apply_tool_effects([
5417                CanvasToolEffect::BeginGesture,
5418                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5419                    DocumentCommand::UpdateNode(moved.clone()),
5420                )),
5421            ])
5422            .unwrap();
5423
5424        assert!(changes.lock().unwrap().is_empty());
5425        assert_eq!(editor.history().undo_depth(), baseline_depth);
5426
5427        editor
5428            .apply_tool_effect(CanvasToolEffect::CommitGesture)
5429            .unwrap();
5430
5431        let changes = changes.lock().unwrap();
5432        assert_eq!(changes.len(), 1);
5433        let change = &changes[0];
5434        assert_eq!(change.source(), crate::CanvasStoreMutationSource::Gesture);
5435        assert_eq!(
5436            change.history_effect(),
5437            crate::CanvasStoreHistoryEffect::PushUndo
5438        );
5439        assert_eq!(change.document().node(&NodeId::from("a")).unwrap(), &moved);
5440        assert_eq!(editor.history().undo_depth(), baseline_depth + 1);
5441    }
5442
5443    #[test]
5444    fn gesture_commit_records_relation_updates() {
5445        let document = document_fixture()
5446            .node(CanvasNode::new(
5447                "child",
5448                point(px(0.0), px(0.0)),
5449                size(px(100.0), px(100.0)),
5450            ))
5451            .shape(CanvasShape::new(
5452                "frame",
5453                Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
5454            ))
5455            .build();
5456        let child = CanvasRecordId::Node(NodeId::from("child"));
5457        let frame = CanvasRecordId::Shape(ShapeId::from("frame"));
5458        let mut editor = CanvasEditor::new(document);
5459
5460        editor
5461            .apply_tool_effects([
5462                CanvasToolEffect::BeginGesture,
5463                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5464                    DocumentCommand::SetRecordParent {
5465                        child: child.clone(),
5466                        parent: frame.clone(),
5467                    },
5468                )),
5469                CanvasToolEffect::CommitGesture,
5470            ])
5471            .unwrap();
5472
5473        assert_eq!(
5474            editor.document().relations().parent_of(&child),
5475            Some(&frame)
5476        );
5477        assert_eq!(editor.history().undo_depth(), 1);
5478
5479        assert!(editor.undo().unwrap());
5480        assert_eq!(editor.document().relations().parent_of(&child), None);
5481    }
5482
5483    #[test]
5484    fn empty_gesture_commit_does_not_push_history() {
5485        let document = document_fixture()
5486            .node(CanvasNode::new(
5487                "child",
5488                point(px(0.0), px(0.0)),
5489                size(px(100.0), px(100.0)),
5490            ))
5491            .build();
5492        let mut editor = CanvasEditor::new(document);
5493
5494        editor
5495            .apply_tool_effects([
5496                CanvasToolEffect::BeginGesture,
5497                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5498                    DocumentCommand::ClearRecordParent {
5499                        child: CanvasRecordId::Node(NodeId::from("child")),
5500                    },
5501                )),
5502            ])
5503            .unwrap();
5504
5505        editor
5506            .apply_tool_effect(CanvasToolEffect::CommitGesture)
5507            .unwrap();
5508
5509        assert_eq!(editor.history().undo_depth(), 0);
5510        assert!(editor.is_tool_state_idle());
5511    }
5512
5513    #[test]
5514    fn set_tool_cancels_active_gesture_before_switching() {
5515        let mut editor = CanvasEditor::default();
5516        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5517        let moved = CanvasNode::new("a", point(px(400.0), px(0.0)), size(px(100.0), px(100.0)));
5518        editor
5519            .apply(DocumentCommand::InsertNode(original.clone()))
5520            .unwrap();
5521
5522        editor
5523            .apply_tool_effects([
5524                CanvasToolEffect::BeginGesture,
5525                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5526                    DocumentCommand::UpdateNode(moved),
5527                )),
5528            ])
5529            .unwrap();
5530        assert_eq!(
5531            editor.document().node(&NodeId::from("a")).unwrap().position,
5532            point(px(400.0), px(0.0))
5533        );
5534
5535        editor.set_tool(CanvasTool::Pan).unwrap();
5536
5537        assert_eq!(editor.tool(), &CanvasTool::Pan);
5538        assert_eq!(
5539            editor.document().node(&NodeId::from("a")).unwrap(),
5540            &original
5541        );
5542        assert_eq!(editor.history().undo_depth(), 1);
5543        assert!(
5544            editor
5545                .runtime()
5546                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5547                .any(|record| record.target == HitTarget::Node(NodeId::from("a")))
5548        );
5549        assert!(
5550            editor
5551                .runtime()
5552                .hit_test(point(px(410.0), px(10.0)), HitOptions::default())
5553                .next()
5554                .is_none()
5555        );
5556    }
5557
5558    #[test]
5559    fn begin_gesture_preserves_existing_baseline() {
5560        let mut editor = CanvasEditor::default();
5561        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5562        let first = CanvasNode::new("a", point(px(100.0), px(0.0)), size(px(100.0), px(100.0)));
5563        let second = CanvasNode::new("a", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
5564        editor
5565            .apply(DocumentCommand::InsertNode(original.clone()))
5566            .unwrap();
5567
5568        editor
5569            .apply_tool_effects([
5570                CanvasToolEffect::BeginGesture,
5571                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5572                    DocumentCommand::UpdateNode(first),
5573                )),
5574                CanvasToolEffect::BeginGesture,
5575                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5576                    DocumentCommand::UpdateNode(second),
5577                )),
5578                CanvasToolEffect::CancelGesture,
5579            ])
5580            .unwrap();
5581
5582        assert_eq!(
5583            editor.document().node(&NodeId::from("a")).unwrap(),
5584            &original
5585        );
5586        assert_eq!(editor.history().undo_depth(), 1);
5587    }
5588
5589    #[test]
5590    fn public_tool_intents_commit_transaction_as_one_undo_entry() {
5591        let mut editor = CanvasEditor::default();
5592        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5593        let first = CanvasNode::new("a", point(px(100.0), px(0.0)), size(px(100.0), px(100.0)));
5594        let second = CanvasNode::new("a", point(px(200.0), px(0.0)), size(px(100.0), px(100.0)));
5595        editor
5596            .apply(DocumentCommand::InsertNode(original.clone()))
5597            .unwrap();
5598        let baseline_depth = editor.history().undo_depth();
5599
5600        for intent in [
5601            CanvasToolIntent::ApplyTransaction(CanvasTransaction::single(
5602                DocumentCommand::UpdateNode(first),
5603            )),
5604            CanvasToolIntent::ApplyTransaction(CanvasTransaction::single(
5605                DocumentCommand::UpdateNode(second.clone()),
5606            )),
5607            CanvasToolIntent::CommitTransaction,
5608        ] {
5609            editor.apply_custom_tool_intent(intent).unwrap();
5610        }
5611
5612        assert_eq!(editor.document().node(&NodeId::from("a")).unwrap(), &second);
5613        assert_eq!(editor.history().undo_depth(), baseline_depth + 1);
5614        assert!(editor.undo().unwrap());
5615        assert_eq!(
5616            editor.document().node(&NodeId::from("a")).unwrap(),
5617            &original
5618        );
5619    }
5620
5621    #[test]
5622    fn public_tool_intents_cancel_transaction_without_history() {
5623        let mut editor = CanvasEditor::default();
5624        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5625        let moved = CanvasNode::new("a", point(px(100.0), px(0.0)), size(px(100.0), px(100.0)));
5626        editor
5627            .apply(DocumentCommand::InsertNode(original.clone()))
5628            .unwrap();
5629        let baseline_depth = editor.history().undo_depth();
5630
5631        for intent in [
5632            CanvasToolIntent::ApplyTransaction(CanvasTransaction::single(
5633                DocumentCommand::UpdateNode(moved),
5634            )),
5635            CanvasToolIntent::CancelTransaction,
5636        ] {
5637            editor.apply_custom_tool_intent(intent).unwrap();
5638        }
5639
5640        assert_eq!(
5641            editor.document().node(&NodeId::from("a")).unwrap(),
5642            &original
5643        );
5644        assert_eq!(editor.history().undo_depth(), baseline_depth);
5645    }
5646
5647    #[test]
5648    fn gesture_cancel_restores_document_without_history() {
5649        let mut editor = CanvasEditor::default();
5650        let original = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
5651        let moved = CanvasNode::new("a", point(px(40.0), px(0.0)), size(px(100.0), px(100.0)));
5652        editor
5653            .apply(DocumentCommand::InsertNode(original.clone()))
5654            .unwrap();
5655        let undo_depth = editor.history().undo_depth();
5656
5657        editor
5658            .apply_tool_effects([
5659                CanvasToolEffect::BeginGesture,
5660                CanvasToolEffect::UpdateGesture(CanvasTransaction::single(
5661                    DocumentCommand::UpdateNode(moved),
5662                )),
5663                CanvasToolEffect::CancelGesture,
5664            ])
5665            .unwrap();
5666
5667        assert_eq!(
5668            editor.document().node(&NodeId::from("a")).unwrap(),
5669            &original
5670        );
5671        assert_eq!(editor.history().undo_depth(), undo_depth);
5672    }
5673
5674    #[test]
5675    fn tool_effects_update_transient_editor_state() {
5676        let mut editor = CanvasEditor::default();
5677        editor
5678            .apply(DocumentCommand::InsertNode(CanvasNode::new(
5679                "a",
5680                point(px(0.0), px(0.0)),
5681                size(px(100.0), px(100.0)),
5682            )))
5683            .unwrap();
5684
5685        let mut selection = CanvasSelection::default();
5686        selection.nodes.insert(NodeId::from("a"));
5687        selection.nodes.insert(NodeId::from("missing"));
5688
5689        editor
5690            .apply_tool_effects([
5691                CanvasToolEffect::SetSelection(selection),
5692                CanvasToolEffect::SetState(ToolState::Pointing {
5693                    origin: point(px(10.0), px(20.0)),
5694                    selection_mode: CanvasSelectionMode::Replace,
5695                    base_selection: CanvasSelection::default(),
5696                }),
5697                CanvasToolEffect::PanViewport(point(px(5.0), px(-3.0))),
5698            ])
5699            .unwrap();
5700
5701        assert_eq!(
5702            editor
5703                .session
5704                .selection
5705                .nodes
5706                .iter()
5707                .cloned()
5708                .collect::<Vec<_>>(),
5709            vec![NodeId::from("a")]
5710        );
5711        assert_eq!(
5712            editor.session.state,
5713            ToolState::Pointing {
5714                origin: point(px(10.0), px(20.0)),
5715                selection_mode: CanvasSelectionMode::Replace,
5716                base_selection: CanvasSelection::default(),
5717            }
5718        );
5719        assert_eq!(editor.session.viewport.origin, point(px(5.0), px(-3.0)));
5720    }
5721
5722    #[test]
5723    fn tool_effects_update_selection_incrementally() {
5724        let document = document_fixture()
5725            .node(CanvasNode::new(
5726                "a",
5727                point(px(0.0), px(0.0)),
5728                size(px(100.0), px(100.0)),
5729            ))
5730            .node(CanvasNode::new(
5731                "b",
5732                point(px(200.0), px(0.0)),
5733                size(px(100.0), px(100.0)),
5734            ))
5735            .edge(CanvasEdge::new(
5736                "a-b",
5737                CanvasEndpoint::new("a", None::<&str>),
5738                CanvasEndpoint::new("b", None::<&str>),
5739            ))
5740            .shape(CanvasShape::new(
5741                "shape",
5742                Bounds::new(point(px(0.0), px(200.0)), size(px(40.0), px(40.0))),
5743            ))
5744            .build();
5745        let mut editor = CanvasEditor::new(document);
5746
5747        editor
5748            .apply_tool_effects([
5749                CanvasToolEffect::AddSelection(HitTarget::Node(NodeId::from("a"))),
5750                CanvasToolEffect::ToggleSelection(HitTarget::Shape(ShapeId::from("shape"))),
5751                CanvasToolEffect::ToggleSelection(HitTarget::Edge(EdgeId::from("a-b"))),
5752                CanvasToolEffect::RemoveSelection(HitTarget::Node(NodeId::from("a"))),
5753                CanvasToolEffect::ToggleSelection(HitTarget::Shape(ShapeId::from("shape"))),
5754                CanvasToolEffect::AddSelection(HitTarget::Node(NodeId::from("missing"))),
5755            ])
5756            .unwrap();
5757
5758        assert!(editor.session.selection.nodes.is_empty());
5759        assert!(editor.session.selection.shapes.is_empty());
5760        assert_eq!(
5761            editor
5762                .session
5763                .selection
5764                .edges
5765                .iter()
5766                .cloned()
5767                .collect::<Vec<_>>(),
5768            vec![EdgeId::from("a-b")]
5769        );
5770    }
5771
5772    #[test]
5773    fn selection_effects_normalize_selected_ancestor_and_descendant() {
5774        let mut document = document_fixture()
5775            .shape(CanvasShape::new(
5776                "frame",
5777                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
5778            ))
5779            .node(CanvasNode::new(
5780                "child",
5781                point(px(20.0), px(20.0)),
5782                size(px(40.0), px(40.0)),
5783            ))
5784            .node(CanvasNode::new(
5785                "outside",
5786                point(px(260.0), px(20.0)),
5787                size(px(40.0), px(40.0)),
5788            ))
5789            .build();
5790        document
5791            .apply_transaction(CanvasTransaction::single(
5792                DocumentCommand::SetRecordParent {
5793                    child: CanvasRecordId::Node(NodeId::from("child")),
5794                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
5795                },
5796            ))
5797            .unwrap();
5798        let mut editor = CanvasEditor::new(document);
5799        let mut selection = CanvasSelection::default();
5800        selection.insert_shape(ShapeId::from("frame"));
5801        selection.insert_node(NodeId::from("child"));
5802        selection.insert_node(NodeId::from("outside"));
5803
5804        editor
5805            .apply_tool_effect(CanvasToolEffect::SetSelection(selection))
5806            .unwrap();
5807
5808        assert_eq!(
5809            editor
5810                .session
5811                .selection
5812                .shapes
5813                .iter()
5814                .cloned()
5815                .collect::<Vec<_>>(),
5816            vec![ShapeId::from("frame")]
5817        );
5818        assert_eq!(
5819            editor
5820                .session
5821                .selection
5822                .nodes
5823                .iter()
5824                .cloned()
5825                .collect::<Vec<_>>(),
5826            vec![NodeId::from("outside")]
5827        );
5828    }
5829
5830    #[test]
5831    fn public_selection_intents_normalize_redundant_descendants() {
5832        let mut document = document_fixture()
5833            .shape(CanvasShape::new(
5834                "frame",
5835                Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(200.0))),
5836            ))
5837            .node(CanvasNode::new(
5838                "child",
5839                point(px(20.0), px(20.0)),
5840                size(px(40.0), px(40.0)),
5841            ))
5842            .build();
5843        document
5844            .apply_transaction(CanvasTransaction::single(
5845                DocumentCommand::SetRecordParent {
5846                    child: CanvasRecordId::Node(NodeId::from("child")),
5847                    parent: CanvasRecordId::Shape(ShapeId::from("frame")),
5848                },
5849            ))
5850            .unwrap();
5851        let mut editor = CanvasEditor::new(document);
5852
5853        editor
5854            .apply_tool_intent(CanvasToolIntent::ReplaceSelection(HitTarget::Node(
5855                NodeId::from("child"),
5856            )))
5857            .unwrap();
5858        assert_eq!(
5859            editor
5860                .session
5861                .selection
5862                .nodes
5863                .iter()
5864                .cloned()
5865                .collect::<Vec<_>>(),
5866            vec![NodeId::from("child")]
5867        );
5868
5869        editor
5870            .apply_tool_intent(CanvasToolIntent::AddSelection(HitTarget::Shape(
5871                ShapeId::from("frame"),
5872            )))
5873            .unwrap();
5874
5875        assert!(editor.session.selection.nodes.is_empty());
5876        assert_eq!(
5877            editor
5878                .session
5879                .selection
5880                .shapes
5881                .iter()
5882                .cloned()
5883                .collect::<Vec<_>>(),
5884            vec![ShapeId::from("frame")]
5885        );
5886    }
5887
5888    #[test]
5889    fn selection_discards_removed_records_after_transaction() {
5890        let mut editor = CanvasEditor::default();
5891        editor
5892            .apply(DocumentCommand::InsertNode(CanvasNode::new(
5893                "a",
5894                point(px(0.0), px(0.0)),
5895                size(px(100.0), px(100.0)),
5896            )))
5897            .unwrap();
5898        editor.session.selection.nodes.insert(NodeId::from("a"));
5899
5900        editor
5901            .apply(DocumentCommand::RemoveNode(NodeId::from("a")))
5902            .unwrap();
5903
5904        assert!(editor.session.selection.is_empty());
5905    }
5906
5907    #[test]
5908    fn editor_keeps_spatial_index_in_sync_with_transactions() {
5909        let mut editor = CanvasEditor::default();
5910        editor
5911            .apply(DocumentCommand::InsertNode(CanvasNode::new(
5912                "a",
5913                point(px(0.0), px(0.0)),
5914                size(px(100.0), px(100.0)),
5915            )))
5916            .unwrap();
5917
5918        assert!(
5919            editor
5920                .runtime()
5921                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5922                .next()
5923                .is_some()
5924        );
5925
5926        assert!(editor.undo().unwrap());
5927        assert!(
5928            editor
5929                .runtime()
5930                .hit_test(point(px(10.0), px(10.0)), HitOptions::default())
5931                .next()
5932                .is_none()
5933        );
5934    }
5935
5936    #[test]
5937    fn editor_refreshes_runtime_geometry_with_installed_router() {
5938        let mut editor =
5939            CanvasEditor::new_with_router(connected_edge_document(), VerticalDetourRouter);
5940
5941        assert_eq!(
5942            editor
5943                .runtime()
5944                .edge_geometry(&EdgeId::from("a-b"))
5945                .unwrap()
5946                .path
5947                .document_points(),
5948            vec![
5949                point(px(5.0), px(5.0)),
5950                point(px(5.0), px(80.0)),
5951                point(px(25.0), px(5.0)),
5952            ]
5953        );
5954
5955        let mut target = editor.document().node(&NodeId::from("b")).unwrap().clone();
5956        target.position = point(px(40.0), px(0.0));
5957        editor.apply(DocumentCommand::UpdateNode(target)).unwrap();
5958
5959        assert_eq!(
5960            editor
5961                .runtime()
5962                .edge_geometry(&EdgeId::from("a-b"))
5963                .unwrap()
5964                .path
5965                .document_points(),
5966            vec![
5967                point(px(5.0), px(5.0)),
5968                point(px(5.0), px(80.0)),
5969                point(px(45.0), px(5.0)),
5970            ]
5971        );
5972    }
5973
5974    fn connected_edge_document() -> CanvasDocument {
5975        connected_pair_fixture().build()
5976    }
5977
5978    struct VerticalDetourRouter;
5979
5980    impl CanvasEdgeRouter for VerticalDetourRouter {
5981        fn route_edge(&self, request: CanvasRouteRequest<'_>) -> CanvasRoutePath {
5982            CanvasRoutePath::polyline([
5983                request.source,
5984                point(request.source.x, px(80.0)),
5985                request.target,
5986            ])
5987        }
5988    }
5989}