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(®istry),
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 ¬e
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}