1use super::model::{
2 CanvasPaintInteractionState, CanvasPaintModel, CanvasPaintOptions, CanvasPaintTheme,
3};
4use super::style::{parse_color, positive_pixels};
5use crate::tool::RECONNECT_HANDLE_VIEW_SIZE;
6use crate::{
7 CanvasConnectionEndpointRole, CanvasDefaultEdgeRouter, CanvasEdge, CanvasEdgeRoute,
8 CanvasEdgeRouteKind, CanvasEdgeRouter, CanvasEndpoint, CanvasGeometryFacts, CanvasKindLabel,
9 CanvasRecordId, CanvasRecordScopeOptions, CanvasResolvedSelectionScope, CanvasRoutePath,
10 CanvasRouteRequest, CanvasRouteSegment, CanvasSelection, CanvasSnapAxis, CanvasSnapGuide,
11 CanvasTransformHandle, CanvasTransformTarget, CanvasViewport, EdgeId, HitOptions, HitTarget,
12 canvas_transform_handles, connection_hit_options, resolve_selection_scope,
13};
14use open_gpui::{
15 Bounds, Hsla, Pixels, Point, SharedString, TextRun, Window, WrappedLine, px, size,
16};
17
18const CONNECTION_TARGET_FEEDBACK_VIEW_SIZE: Pixels = px(18.0);
19const RECONNECT_HANDLE_VISUAL_SIZE: Pixels = px(11.0);
20
21#[derive(Clone, Debug, PartialEq)]
22pub struct CanvasPaintFrame {
23 pub visible_document_bounds: Bounds<Pixels>,
24 pub records: Vec<CanvasPaintRecord>,
25 pub interaction: CanvasPaintInteractionFrame,
26}
27
28impl CanvasPaintFrame {
29 pub fn widget_overlay_frame(
30 &self,
31 options: CanvasWidgetOverlayOptions,
32 ) -> CanvasWidgetOverlayFrame {
33 collect_widget_overlay_frame(self, options)
34 }
35
36 pub fn scene_frame(&self) -> CanvasSceneFrame {
37 CanvasSceneFrame::from_paint_frame(self)
38 }
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct CanvasSceneFrame {
43 pub visible_document_bounds: Bounds<Pixels>,
44 record_groups: Vec<CanvasSceneRecordGroup>,
45 edge_items: Vec<CanvasSceneLayerItem>,
46 tool_chrome: CanvasPaintInteractionFrame,
47}
48
49impl CanvasSceneFrame {
50 pub fn from_paint_frame(frame: &CanvasPaintFrame) -> Self {
51 let mut record_groups = Vec::new();
52 let mut edge_items = Vec::new();
53
54 for (ordinal, record) in frame.records.iter().enumerate() {
55 match record.target {
56 HitTarget::Edge(_) => edge_items.push(CanvasSceneLayerItem::from_record(
57 record,
58 ordinal,
59 CanvasSceneLayerPhase::EdgeBehindNodes,
60 )),
61 _ => record_groups.push(CanvasSceneRecordGroup::from_record(record, ordinal)),
62 }
63 }
64
65 record_groups.sort_by_key(scene_record_group_sort_key);
66 edge_items.sort_by(|left, right| {
67 left.z_index
68 .cmp(&right.z_index)
69 .then_with(|| left.ordinal.cmp(&right.ordinal))
70 });
71
72 Self {
73 visible_document_bounds: frame.visible_document_bounds,
74 record_groups,
75 edge_items,
76 tool_chrome: frame.interaction.clone(),
77 }
78 }
79
80 pub fn record_groups(&self) -> &[CanvasSceneRecordGroup] {
81 &self.record_groups
82 }
83
84 pub fn edge_items(&self) -> &[CanvasSceneLayerItem] {
85 &self.edge_items
86 }
87
88 pub fn tool_chrome(&self) -> &CanvasPaintInteractionFrame {
89 &self.tool_chrome
90 }
91
92 pub fn ordered_layer_items(&self) -> Vec<CanvasSceneLayerItem> {
93 let mut items = Vec::with_capacity(
94 self.edge_items.len()
95 + self
96 .record_groups
97 .iter()
98 .map(|group| group.phases.len())
99 .sum::<usize>(),
100 );
101 items.extend(self.edge_items.iter().cloned());
102 for group in &self.record_groups {
103 items.extend(group.layer_items());
104 }
105 items
106 }
107}
108
109fn scene_record_group_sort_key(group: &CanvasSceneRecordGroup) -> (u8, i32, usize) {
110 (
111 u8::from(group.selected || group.structurally_selected),
112 group.z_index,
113 group.ordinal,
114 )
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub struct CanvasSceneRecordGroup {
119 pub target: HitTarget,
120 pub document_bounds: Bounds<Pixels>,
121 pub view_bounds: Bounds<Pixels>,
122 pub z_index: i32,
123 pub ordinal: usize,
124 pub record_index: usize,
125 pub hidden: bool,
126 pub locked: bool,
127 pub hovered: bool,
128 pub selected: bool,
129 pub structurally_selected: bool,
130 phases: Vec<CanvasSceneLayerPhase>,
131}
132
133impl CanvasSceneRecordGroup {
134 fn from_record(record: &CanvasPaintRecord, ordinal: usize) -> Self {
135 Self {
136 target: record.target.clone(),
137 document_bounds: record.document_bounds,
138 view_bounds: record.view_bounds,
139 z_index: record.z_index,
140 ordinal,
141 record_index: ordinal,
142 hidden: record.hidden,
143 locked: record.locked,
144 hovered: record.hovered,
145 selected: record.selected,
146 structurally_selected: record.structurally_selected,
147 phases: scene_record_phases(&record.target).to_vec(),
148 }
149 }
150
151 pub fn phases(&self) -> &[CanvasSceneLayerPhase] {
152 &self.phases
153 }
154
155 pub fn has_phase(&self, phase: CanvasSceneLayerPhase) -> bool {
156 self.phases.contains(&phase)
157 }
158
159 pub fn layer_items(&self) -> impl Iterator<Item = CanvasSceneLayerItem> + '_ {
160 self.phases
161 .iter()
162 .copied()
163 .map(|phase| CanvasSceneLayerItem {
164 phase,
165 target: self.target.clone(),
166 document_bounds: self.document_bounds,
167 view_bounds: self.view_bounds,
168 z_index: self.z_index,
169 ordinal: self.ordinal,
170 record_index: self.record_index,
171 })
172 }
173}
174
175#[derive(Clone, Debug, PartialEq)]
176pub struct CanvasSceneLayerItem {
177 pub phase: CanvasSceneLayerPhase,
178 pub target: HitTarget,
179 pub document_bounds: Bounds<Pixels>,
180 pub view_bounds: Bounds<Pixels>,
181 pub z_index: i32,
182 pub ordinal: usize,
183 pub record_index: usize,
184}
185
186impl CanvasSceneLayerItem {
187 fn from_record(
188 record: &CanvasPaintRecord,
189 ordinal: usize,
190 phase: CanvasSceneLayerPhase,
191 ) -> Self {
192 Self {
193 phase,
194 target: record.target.clone(),
195 document_bounds: record.document_bounds,
196 view_bounds: record.view_bounds,
197 z_index: record.z_index,
198 ordinal,
199 record_index: ordinal,
200 }
201 }
202}
203
204#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
205pub enum CanvasSceneLayerPhase {
206 DocumentUnderlay,
207 EdgeBehindNodes,
208 RecordBody,
209 RecordWidget,
210 RecordChrome,
211 EdgeAboveNodes,
212 ToolChrome,
213 HostPortal,
214}
215
216fn scene_record_phases(target: &HitTarget) -> &'static [CanvasSceneLayerPhase] {
217 match target {
218 HitTarget::Node(_) => &[
219 CanvasSceneLayerPhase::RecordBody,
220 CanvasSceneLayerPhase::RecordWidget,
221 CanvasSceneLayerPhase::RecordChrome,
222 ],
223 HitTarget::Shape(_) => &[
224 CanvasSceneLayerPhase::RecordBody,
225 CanvasSceneLayerPhase::RecordChrome,
226 ],
227 HitTarget::Handle { .. } => &[CanvasSceneLayerPhase::RecordChrome],
228 HitTarget::Edge(_) => &[CanvasSceneLayerPhase::EdgeBehindNodes],
229 }
230}
231
232#[derive(Debug)]
233pub struct CanvasPreparedPaintFrame {
234 pub(super) frame: CanvasPaintFrame,
235 pub(super) labels: Vec<CanvasPreparedPaintLabel>,
236 pub(super) label_indices: Vec<Option<usize>>,
237}
238
239impl CanvasPreparedPaintFrame {
240 pub fn frame(&self) -> &CanvasPaintFrame {
241 &self.frame
242 }
243
244 pub fn into_frame(self) -> CanvasPaintFrame {
245 self.frame
246 }
247
248 pub fn prepared_label_count(&self) -> usize {
249 self.labels.len()
250 }
251
252 pub fn record_count(&self) -> usize {
253 self.frame.records.len()
254 }
255
256 pub fn has_prepared_label(&self, record_index: usize) -> bool {
257 self.label_indices
258 .get(record_index)
259 .is_some_and(Option::is_some)
260 }
261
262 pub fn widget_overlay_frame(
263 &self,
264 options: CanvasWidgetOverlayOptions,
265 ) -> CanvasWidgetOverlayFrame {
266 self.frame.widget_overlay_frame(options)
267 }
268}
269
270#[derive(Debug)]
271pub(super) struct CanvasPreparedPaintLabel {
272 pub(super) view_bounds: Bounds<Pixels>,
273 pub(super) lines: Vec<WrappedLine>,
274 pub(super) text_height: Pixels,
275}
276
277#[derive(Clone, Debug, PartialEq)]
278pub struct CanvasPaintRecord {
279 pub target: HitTarget,
280 pub document_bounds: Bounds<Pixels>,
281 pub view_bounds: Bounds<Pixels>,
282 pub label: Option<CanvasPaintLabel>,
283 pub edge_geometry: Option<CanvasPaintEdgeGeometry>,
284 pub z_index: i32,
285 pub hidden: bool,
286 pub locked: bool,
287 pub hovered: bool,
288 pub selected: bool,
289 pub structurally_selected: bool,
290}
291
292#[derive(Clone, Debug, PartialEq)]
293pub struct CanvasPaintEdgeGeometry {
294 pub view_path: CanvasRoutePath,
295 pub visual_state: CanvasPaintWireVisualState,
296}
297
298#[derive(Clone, Copy, Debug, Eq, PartialEq)]
299pub enum CanvasPaintWireVisualState {
300 Committed,
301 Hovered,
302 Selected,
303 SelectedHovered,
304 PreviewFree,
305 PreviewValidTarget,
306 PreviewInvalidTarget,
307}
308
309#[derive(Clone, Debug, PartialEq)]
310pub struct CanvasPaintLabel {
311 pub text: String,
312 pub document_bounds: Bounds<Pixels>,
313 pub view_bounds: Bounds<Pixels>,
314 pub color: Option<Hsla>,
315}
316
317#[derive(Clone, Debug, Default, PartialEq)]
318pub struct CanvasPaintInteractionFrame {
319 pub selection_bounds: Option<Bounds<Pixels>>,
320 pub structural_selection_bounds: Option<Bounds<Pixels>>,
321 pub connection_preview: Option<CanvasPaintConnectionPreview>,
322 pub reconnect_handles: Vec<CanvasPaintReconnectHandle>,
323 pub transform_handles: Vec<CanvasPaintTransformHandle>,
324 pub snap_guides: Vec<CanvasPaintSnapGuide>,
325}
326
327#[derive(Clone, Debug, PartialEq)]
328pub struct CanvasPaintConnectionPreview {
329 pub source_view_position: Point<Pixels>,
330 pub target_view_position: Point<Pixels>,
331 pub edge_geometry: CanvasPaintEdgeGeometry,
332 pub route_kind: CanvasEdgeRouteKind,
333 pub visual_state: CanvasPaintWireVisualState,
334 pub target_feedback: CanvasPaintConnectionTargetFeedback,
335}
336
337#[derive(Clone, Copy, Debug, Eq, PartialEq)]
338pub enum CanvasPaintConnectionTargetState {
339 Free,
340 Valid,
341 Invalid,
342}
343
344#[derive(Clone, Debug, PartialEq)]
345pub struct CanvasPaintConnectionTargetFeedback {
346 pub role: CanvasConnectionEndpointRole,
347 pub state: CanvasPaintConnectionTargetState,
348 pub view_bounds: Bounds<Pixels>,
349}
350
351#[derive(Clone, Copy, Debug, Eq, PartialEq)]
352pub enum CanvasPaintReconnectEndpoint {
353 Source,
354 Target,
355}
356
357#[derive(Clone, Debug, PartialEq)]
358pub struct CanvasPaintReconnectHandle {
359 pub edge_id: EdgeId,
360 pub endpoint: CanvasPaintReconnectEndpoint,
361 pub shape: CanvasPaintReconnectHandleShape,
362 pub view_bounds: Bounds<Pixels>,
363 pub hit_bounds: Bounds<Pixels>,
364 pub visual_bounds: Bounds<Pixels>,
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
368pub enum CanvasPaintReconnectHandleShape {
369 SourcePlug,
370 TargetSocket,
371}
372
373#[derive(Clone, Debug, PartialEq)]
374pub struct CanvasPaintTransformHandle {
375 pub target: CanvasTransformTarget,
376 pub handle: crate::CanvasResizeHandle,
377 pub view_bounds: Bounds<Pixels>,
378}
379
380#[derive(Clone, Debug, PartialEq)]
381pub struct CanvasPaintSnapGuide {
382 pub axis: CanvasSnapAxis,
383 pub view_start: Point<Pixels>,
384 pub view_end: Point<Pixels>,
385}
386
387#[derive(Clone, Debug, Default, PartialEq)]
388pub struct CanvasWidgetOverlayFrame {
389 pub placements: Vec<CanvasWidgetOverlayPlacement>,
390}
391
392impl CanvasWidgetOverlayFrame {
393 pub fn is_empty(&self) -> bool {
394 self.placements.is_empty()
395 }
396
397 pub fn len(&self) -> usize {
398 self.placements.len()
399 }
400}
401
402#[derive(Clone, Debug, PartialEq)]
403pub struct CanvasWidgetOverlayPlacement {
404 pub target: HitTarget,
405 pub document_bounds: Bounds<Pixels>,
406 pub view_bounds: Bounds<Pixels>,
407 pub z_index: i32,
408 pub hit_priority: CanvasWidgetOverlayHitPriority,
409}
410
411#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
412pub struct CanvasWidgetOverlayOptions {
413 pub include_selected_nodes: bool,
414 pub include_selected_shapes: bool,
415 pub include_hidden: bool,
416 pub include_locked: bool,
417 pub hit_priority: CanvasWidgetOverlayHitPriority,
418}
419
420impl CanvasWidgetOverlayOptions {
421 pub fn selected_nodes() -> Self {
422 Self {
423 include_selected_nodes: true,
424 ..Self::default()
425 }
426 }
427
428 pub fn selected_records() -> Self {
429 Self {
430 include_selected_nodes: true,
431 include_selected_shapes: true,
432 ..Self::default()
433 }
434 }
435
436 pub fn with_locked(mut self, include_locked: bool) -> Self {
437 self.include_locked = include_locked;
438 self
439 }
440
441 pub fn with_hidden(mut self, include_hidden: bool) -> Self {
442 self.include_hidden = include_hidden;
443 self
444 }
445
446 pub fn with_hit_priority(mut self, hit_priority: CanvasWidgetOverlayHitPriority) -> Self {
447 self.hit_priority = hit_priority;
448 self
449 }
450}
451
452#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
453pub enum CanvasWidgetOverlayHitPriority {
454 CanvasFirst,
455 #[default]
456 WidgetFirst,
457}
458
459pub fn collect_visible_records(
460 model: &CanvasPaintModel,
461 canvas_bounds: Bounds<Pixels>,
462 options: CanvasPaintOptions,
463) -> CanvasPaintFrame {
464 let mut visible_document_bounds = model
465 .viewport
466 .view_bounds_to_document(Bounds::new(Point::default(), canvas_bounds.size));
467 if options.cull_margin > Pixels::ZERO {
468 visible_document_bounds = visible_document_bounds.dilate(options.cull_margin);
469 }
470
471 let hit_options = HitOptions {
472 include_hidden: options.include_hidden,
473 include_locked: true,
474 include_handles: options.include_handles,
475 margin: Pixels::ZERO,
476 };
477 let selection_scope = options.include_interaction_feedback.then(|| {
478 resolve_selection_scope(
479 model.document.as_ref(),
480 model.interaction.selection(),
481 CanvasRecordScopeOptions::structural_with_internal_edges(),
482 )
483 });
484 let records = model
485 .runtime
486 .query_with_options(visible_document_bounds, hit_options)
487 .map(|record| {
488 let target = record.target.clone();
489 let selected = options.include_interaction_feedback
490 && selection_scope.as_ref().is_some_and(|scope| {
491 target_is_selected(&record.target, scope.normalized_selection())
492 });
493 let structurally_selected =
494 target_is_structurally_selected(&record.target, selection_scope.as_ref());
495 let hovered = options.include_interaction_feedback
496 && model
497 .interaction
498 .hovered_target()
499 .is_some_and(|hovered| hovered == &target);
500 let edge_geometry = paint_edge_geometry(
501 model,
502 &target,
503 committed_wire_visual_state(selected || structurally_selected, hovered),
504 );
505 CanvasPaintRecord {
506 label: paint_record_label(model, &target, record.bounds),
507 target,
508 document_bounds: record.bounds,
509 view_bounds: model.viewport.document_bounds_to_view(record.bounds),
510 edge_geometry,
511 z_index: record.z_index,
512 hidden: record.hidden,
513 locked: record.locked,
514 hovered,
515 selected,
516 structurally_selected,
517 }
518 })
519 .collect();
520
521 CanvasPaintFrame {
522 visible_document_bounds,
523 records,
524 interaction: if options.include_interaction_feedback {
525 interaction_frame(model, options, selection_scope.as_ref())
526 } else {
527 CanvasPaintInteractionFrame::default()
528 },
529 }
530}
531
532pub fn collect_widget_overlay_frame(
533 frame: &CanvasPaintFrame,
534 options: CanvasWidgetOverlayOptions,
535) -> CanvasWidgetOverlayFrame {
536 let placements = frame
537 .records
538 .iter()
539 .filter(|record| record_requests_widget_overlay(record, options))
540 .map(|record| CanvasWidgetOverlayPlacement {
541 target: record.target.clone(),
542 document_bounds: record.document_bounds,
543 view_bounds: record.view_bounds,
544 z_index: record.z_index,
545 hit_priority: options.hit_priority,
546 })
547 .collect();
548
549 CanvasWidgetOverlayFrame { placements }
550}
551
552pub fn prepaint_canvas_frame(
553 model: &CanvasPaintModel,
554 canvas_bounds: Bounds<Pixels>,
555 options: CanvasPaintOptions,
556 theme: CanvasPaintTheme,
557 window: &mut Window,
558) -> CanvasPreparedPaintFrame {
559 let frame = collect_visible_records(model, canvas_bounds, options);
560 prepare_canvas_frame(frame, theme, window)
561}
562
563pub fn prepare_canvas_frame(
564 frame: CanvasPaintFrame,
565 theme: CanvasPaintTheme,
566 window: &mut Window,
567) -> CanvasPreparedPaintFrame {
568 let mut labels = Vec::new();
569 let label_indices = frame
570 .records
571 .iter()
572 .map(|record| {
573 record.label.as_ref().and_then(|label| {
574 let prepared = prepare_label(label, theme, window)?;
575 let index = labels.len();
576 labels.push(prepared);
577 Some(index)
578 })
579 })
580 .collect();
581
582 CanvasPreparedPaintFrame {
583 frame,
584 labels,
585 label_indices,
586 }
587}
588
589fn interaction_frame(
590 model: &CanvasPaintModel,
591 options: CanvasPaintOptions,
592 selection_scope: Option<&crate::CanvasResolvedSelectionScope>,
593) -> CanvasPaintInteractionFrame {
594 match model.interaction.state() {
595 CanvasPaintInteractionState::Selecting { origin, current } => CanvasPaintInteractionFrame {
596 selection_bounds: Some(
597 model
598 .viewport
599 .document_bounds_to_view(bounds_from_points(*origin, *current)),
600 ),
601 structural_selection_bounds: None,
602 connection_preview: None,
603 reconnect_handles: Vec::new(),
604 transform_handles: Vec::new(),
605 snap_guides: Vec::new(),
606 },
607 CanvasPaintInteractionState::Connecting { source, current } => {
608 CanvasPaintInteractionFrame {
609 selection_bounds: None,
610 structural_selection_bounds: structural_selection_bounds(model, selection_scope),
611 connection_preview: connection_preview(
612 model,
613 source,
614 *current,
615 options.connection_preview_route.edge_route(),
616 ),
617 reconnect_handles: Vec::new(),
618 transform_handles: Vec::new(),
619 snap_guides: Vec::new(),
620 }
621 }
622 CanvasPaintInteractionState::Reconnecting {
623 edge_id,
624 endpoint,
625 fixed,
626 current,
627 } => CanvasPaintInteractionFrame {
628 selection_bounds: None,
629 structural_selection_bounds: structural_selection_bounds(model, selection_scope),
630 connection_preview: reconnect_preview(
631 model,
632 edge_id,
633 *endpoint,
634 fixed,
635 *current,
636 options.connection_preview_route.edge_route(),
637 ),
638 reconnect_handles: Vec::new(),
639 transform_handles: Vec::new(),
640 snap_guides: Vec::new(),
641 },
642 CanvasPaintInteractionState::Transforming { snap_guides } => CanvasPaintInteractionFrame {
643 selection_bounds: None,
644 structural_selection_bounds: structural_selection_bounds(model, selection_scope),
645 connection_preview: None,
646 reconnect_handles: reconnect_handles_for_model(model),
647 transform_handles: transform_handles_for_model(model),
648 snap_guides: paint_snap_guides(model, snap_guides),
649 },
650 CanvasPaintInteractionState::Idle => CanvasPaintInteractionFrame {
651 selection_bounds: None,
652 structural_selection_bounds: structural_selection_bounds(model, selection_scope),
653 connection_preview: None,
654 reconnect_handles: reconnect_handles_for_model(model),
655 transform_handles: transform_handles_for_model(model),
656 snap_guides: Vec::new(),
657 },
658 }
659}
660
661fn structural_selection_bounds(
662 model: &CanvasPaintModel,
663 selection_scope: Option<&crate::CanvasResolvedSelectionScope>,
664) -> Option<Bounds<Pixels>> {
665 let selection_scope = selection_scope?;
666 let facts = CanvasGeometryFacts::with_kind_registry(
667 model.document.as_ref(),
668 model.kind_registry.as_ref(),
669 );
670 let structural_bounds =
671 facts.node_shape_bounds_for_records(selection_scope.paint_structural_records())?;
672 if Some(structural_bounds) == facts.selected_bounds(model.interaction.selection()) {
673 return None;
674 }
675
676 Some(model.viewport.document_bounds_to_view(structural_bounds))
677}
678
679fn paint_snap_guides(
680 model: &CanvasPaintModel,
681 guides: &[CanvasSnapGuide],
682) -> Vec<CanvasPaintSnapGuide> {
683 guides
684 .iter()
685 .map(|guide| CanvasPaintSnapGuide {
686 axis: guide.axis,
687 view_start: model.viewport.document_to_view(guide.document_start),
688 view_end: model.viewport.document_to_view(guide.document_end),
689 })
690 .collect()
691}
692
693fn transform_handles_for_model(model: &CanvasPaintModel) -> Vec<CanvasPaintTransformHandle> {
694 canvas_transform_handles(
695 model.document.as_ref(),
696 model.interaction.selection(),
697 model.viewport,
698 Some(model.kind_registry.as_ref()),
699 )
700 .into_iter()
701 .map(|handle: CanvasTransformHandle| CanvasPaintTransformHandle {
702 target: handle.target,
703 handle: handle.handle,
704 view_bounds: model
705 .viewport
706 .document_bounds_to_view(handle.document_bounds),
707 })
708 .collect()
709}
710
711fn reconnect_handles_for_model(model: &CanvasPaintModel) -> Vec<CanvasPaintReconnectHandle> {
712 let facts = CanvasGeometryFacts::with_kind_registry(
713 model.document.as_ref(),
714 model.kind_registry.as_ref(),
715 );
716 model
717 .interaction
718 .selection()
719 .selected_edges()
720 .filter_map(|edge_id| {
721 let edge = model.document.edge(edge_id)?;
722 let source = facts.endpoint_position(&edge.source).ok()?;
723 let target = facts.endpoint_position(&edge.target).ok()?;
724 Some([
725 reconnect_handle(
726 edge_id.clone(),
727 CanvasPaintReconnectEndpoint::Source,
728 model.viewport.document_to_view(source),
729 ),
730 reconnect_handle(
731 edge_id.clone(),
732 CanvasPaintReconnectEndpoint::Target,
733 model.viewport.document_to_view(target),
734 ),
735 ])
736 })
737 .flatten()
738 .collect()
739}
740
741fn reconnect_handle(
742 edge_id: EdgeId,
743 endpoint: CanvasPaintReconnectEndpoint,
744 center: Point<Pixels>,
745) -> CanvasPaintReconnectHandle {
746 let hit_bounds = Bounds::centered_at(
747 center,
748 size(RECONNECT_HANDLE_VIEW_SIZE, RECONNECT_HANDLE_VIEW_SIZE),
749 );
750 let visual_size = RECONNECT_HANDLE_VISUAL_SIZE
751 .min(hit_bounds.size.width)
752 .min(hit_bounds.size.height);
753 CanvasPaintReconnectHandle {
754 edge_id,
755 endpoint,
756 shape: reconnect_handle_shape(endpoint),
757 view_bounds: hit_bounds,
758 hit_bounds,
759 visual_bounds: Bounds::centered_at(center, size(visual_size, visual_size)),
760 }
761}
762
763fn reconnect_handle_shape(
764 endpoint: CanvasPaintReconnectEndpoint,
765) -> CanvasPaintReconnectHandleShape {
766 match endpoint {
767 CanvasPaintReconnectEndpoint::Source => CanvasPaintReconnectHandleShape::SourcePlug,
768 CanvasPaintReconnectEndpoint::Target => CanvasPaintReconnectHandleShape::TargetSocket,
769 }
770}
771
772fn connection_preview(
773 model: &CanvasPaintModel,
774 source: &CanvasEndpoint,
775 current: Point<Pixels>,
776 route: CanvasEdgeRoute,
777) -> Option<CanvasPaintConnectionPreview> {
778 let facts = CanvasGeometryFacts::with_kind_registry(
779 model.document.as_ref(),
780 model.kind_registry.as_ref(),
781 );
782 let source = facts.endpoint_position(source).ok()?;
783 let target = connection_preview_endpoint_target(
784 model,
785 CanvasConnectionEndpointRole::Target,
786 source,
787 current,
788 );
789 Some(connection_preview_frame(
790 model,
791 source,
792 target.document_position,
793 route,
794 target.feedback,
795 ))
796}
797
798fn reconnect_preview(
799 model: &CanvasPaintModel,
800 edge_id: &EdgeId,
801 endpoint: CanvasConnectionEndpointRole,
802 fixed: &CanvasEndpoint,
803 current: Point<Pixels>,
804 fallback_route: CanvasEdgeRoute,
805) -> Option<CanvasPaintConnectionPreview> {
806 let facts = CanvasGeometryFacts::with_kind_registry(
807 model.document.as_ref(),
808 model.kind_registry.as_ref(),
809 );
810 let fixed = facts.endpoint_position(fixed).ok()?;
811 let moving = connection_preview_endpoint_target(model, endpoint, fixed, current);
812 let route = model
813 .document
814 .edge(edge_id)
815 .map(|edge| edge.route.clone())
816 .unwrap_or(fallback_route);
817 Some(match endpoint {
818 CanvasConnectionEndpointRole::Source => connection_preview_frame(
819 model,
820 moving.document_position,
821 fixed,
822 route,
823 moving.feedback,
824 ),
825 CanvasConnectionEndpointRole::Target => connection_preview_frame(
826 model,
827 fixed,
828 moving.document_position,
829 route,
830 moving.feedback,
831 ),
832 })
833}
834
835fn connection_preview_frame(
836 model: &CanvasPaintModel,
837 source: Point<Pixels>,
838 target: Point<Pixels>,
839 route: CanvasEdgeRoute,
840 target_feedback: CanvasPaintConnectionTargetFeedback,
841) -> CanvasPaintConnectionPreview {
842 let route_kind = route.kind.clone();
843 let visual_state = preview_wire_visual_state(target_feedback.state);
844 let mut edge = CanvasEdge::new(
845 "__connection_preview",
846 CanvasEndpoint::new("__connection_preview_source", None::<&str>),
847 CanvasEndpoint::new("__connection_preview_target", None::<&str>),
848 );
849 edge.route = route;
850 let path = CanvasDefaultEdgeRouter.route_edge(CanvasRouteRequest {
851 edge: &edge,
852 source,
853 target,
854 });
855 CanvasPaintConnectionPreview {
856 source_view_position: model.viewport.document_to_view(source),
857 target_view_position: model.viewport.document_to_view(target),
858 edge_geometry: CanvasPaintEdgeGeometry {
859 view_path: route_path_to_view(&path, model.viewport),
860 visual_state,
861 },
862 route_kind,
863 visual_state,
864 target_feedback,
865 }
866}
867
868fn preview_wire_visual_state(
869 state: CanvasPaintConnectionTargetState,
870) -> CanvasPaintWireVisualState {
871 match state {
872 CanvasPaintConnectionTargetState::Free => CanvasPaintWireVisualState::PreviewFree,
873 CanvasPaintConnectionTargetState::Valid => CanvasPaintWireVisualState::PreviewValidTarget,
874 CanvasPaintConnectionTargetState::Invalid => {
875 CanvasPaintWireVisualState::PreviewInvalidTarget
876 }
877 }
878}
879
880#[derive(Clone, Debug, PartialEq)]
881struct CanvasConnectionPreviewEndpointTarget {
882 document_position: Point<Pixels>,
883 feedback: CanvasPaintConnectionTargetFeedback,
884}
885
886fn connection_preview_endpoint_target(
887 model: &CanvasPaintModel,
888 role: CanvasConnectionEndpointRole,
889 fixed: Point<Pixels>,
890 current: Point<Pixels>,
891) -> CanvasConnectionPreviewEndpointTarget {
892 let facts = CanvasGeometryFacts::with_kind_registry(
893 model.document.as_ref(),
894 model.kind_registry.as_ref(),
895 );
896 let records = model
897 .runtime
898 .precise_hit_test_with_facts(facts, current, connection_hit_options())
899 .collect::<Vec<_>>();
900
901 for record in records {
902 match &record.target {
903 HitTarget::Handle { node_id, handle_id } => {
904 let Some(node) = model.document.node(node_id) else {
905 continue;
906 };
907 let Some(handle) = node.handle(Some(handle_id)) else {
908 continue;
909 };
910 let endpoint = CanvasEndpoint {
911 node_id: node_id.clone(),
912 handle_id: Some(handle_id.clone()),
913 };
914 let position = facts.endpoint_position(&endpoint).unwrap_or(current);
915 let state = if handle.is_pickable_connection_endpoint(role) && position != fixed {
916 CanvasPaintConnectionTargetState::Valid
917 } else {
918 CanvasPaintConnectionTargetState::Invalid
919 };
920 return CanvasConnectionPreviewEndpointTarget {
921 document_position: if state == CanvasPaintConnectionTargetState::Valid {
922 position
923 } else {
924 current
925 },
926 feedback: connection_target_feedback(model, role, state, position),
927 };
928 }
929 HitTarget::Node(node_id) => {
930 let endpoint = CanvasEndpoint {
931 node_id: node_id.clone(),
932 handle_id: None,
933 };
934 let position = facts.endpoint_position(&endpoint).unwrap_or(current);
935 let state = if position != fixed {
936 CanvasPaintConnectionTargetState::Valid
937 } else {
938 CanvasPaintConnectionTargetState::Invalid
939 };
940 return CanvasConnectionPreviewEndpointTarget {
941 document_position: if state == CanvasPaintConnectionTargetState::Valid {
942 position
943 } else {
944 current
945 },
946 feedback: connection_target_feedback(model, role, state, position),
947 };
948 }
949 HitTarget::Edge(_) | HitTarget::Shape(_) => {}
950 }
951 }
952
953 CanvasConnectionPreviewEndpointTarget {
954 document_position: current,
955 feedback: connection_target_feedback(
956 model,
957 role,
958 CanvasPaintConnectionTargetState::Free,
959 current,
960 ),
961 }
962}
963
964fn connection_target_feedback(
965 model: &CanvasPaintModel,
966 role: CanvasConnectionEndpointRole,
967 state: CanvasPaintConnectionTargetState,
968 document_position: Point<Pixels>,
969) -> CanvasPaintConnectionTargetFeedback {
970 CanvasPaintConnectionTargetFeedback {
971 role,
972 state,
973 view_bounds: Bounds::centered_at(
974 model.viewport.document_to_view(document_position),
975 size(
976 CONNECTION_TARGET_FEEDBACK_VIEW_SIZE,
977 CONNECTION_TARGET_FEEDBACK_VIEW_SIZE,
978 ),
979 ),
980 }
981}
982
983fn target_is_selected(target: &HitTarget, selection: &CanvasSelection) -> bool {
984 selection.contains_target(target)
985}
986
987fn committed_wire_visual_state(selected: bool, hovered: bool) -> CanvasPaintWireVisualState {
988 match (selected, hovered) {
989 (true, true) => CanvasPaintWireVisualState::SelectedHovered,
990 (true, false) => CanvasPaintWireVisualState::Selected,
991 (false, true) => CanvasPaintWireVisualState::Hovered,
992 (false, false) => CanvasPaintWireVisualState::Committed,
993 }
994}
995
996fn target_is_structurally_selected(
997 target: &HitTarget,
998 scope: Option<&CanvasResolvedSelectionScope>,
999) -> bool {
1000 let Some(scope) = scope else {
1001 return false;
1002 };
1003 let Some(record_id) = record_id_for_target(target) else {
1004 return false;
1005 };
1006
1007 scope.contains_paint_structural_record(&record_id)
1008}
1009
1010fn record_id_for_target(target: &HitTarget) -> Option<CanvasRecordId> {
1011 match target {
1012 HitTarget::Node(id) => Some(CanvasRecordId::Node(id.clone())),
1013 HitTarget::Edge(id) => Some(CanvasRecordId::Edge(id.clone())),
1014 HitTarget::Shape(id) => Some(CanvasRecordId::Shape(id.clone())),
1015 HitTarget::Handle { .. } => None,
1016 }
1017}
1018
1019fn record_requests_widget_overlay(
1020 record: &CanvasPaintRecord,
1021 options: CanvasWidgetOverlayOptions,
1022) -> bool {
1023 if !record.selected
1024 || (record.hidden && !options.include_hidden)
1025 || (record.locked && !options.include_locked)
1026 {
1027 return false;
1028 }
1029
1030 match &record.target {
1031 HitTarget::Node(_) => options.include_selected_nodes,
1032 HitTarget::Shape(_) => options.include_selected_shapes,
1033 HitTarget::Edge(_) | HitTarget::Handle { .. } => false,
1034 }
1035}
1036
1037fn bounds_from_points(a: Point<Pixels>, b: Point<Pixels>) -> Bounds<Pixels> {
1038 Bounds::from_corners(
1039 Point::new(a.x.min(b.x), a.y.min(b.y)),
1040 Point::new(a.x.max(b.x), a.y.max(b.y)),
1041 )
1042}
1043
1044fn prepare_label(
1045 label: &CanvasPaintLabel,
1046 theme: CanvasPaintTheme,
1047 window: &mut Window,
1048) -> Option<CanvasPreparedPaintLabel> {
1049 let text = label.text.trim();
1050 if text.is_empty()
1051 || label.view_bounds.size.width <= Pixels::ZERO
1052 || label.view_bounds.size.height <= Pixels::ZERO
1053 || !positive_pixels(theme.label_font_size)
1054 || !positive_pixels(theme.label_line_height)
1055 {
1056 return None;
1057 }
1058
1059 let mut text_style = window.text_style();
1060 text_style.color = label.color.unwrap_or(theme.label_color);
1061 let run = TextRun {
1062 len: text.len(),
1063 ..text_style.to_run(text.len())
1064 };
1065
1066 let Some(lines) = window
1067 .text_system()
1068 .shape_text(
1069 SharedString::new(text),
1070 theme.label_font_size,
1071 &[run],
1072 Some(label.view_bounds.size.width),
1073 label_line_clamp(theme, label.view_bounds),
1074 )
1075 .ok()
1076 else {
1077 return None;
1078 };
1079
1080 let text_height = lines.iter().fold(Pixels::ZERO, |height, line| {
1081 height + line.size(theme.label_line_height).height
1082 });
1083
1084 Some(CanvasPreparedPaintLabel {
1085 view_bounds: label.view_bounds,
1086 lines: lines.into_iter().collect(),
1087 text_height,
1088 })
1089}
1090
1091pub(super) fn label_line_clamp(theme: CanvasPaintTheme, bounds: Bounds<Pixels>) -> Option<usize> {
1092 let max_lines_by_height =
1093 (bounds.size.height.as_f32() / theme.label_line_height.as_f32()).floor() as usize;
1094 let max_lines = max_lines_by_height.max(1);
1095 Some(
1096 theme
1097 .label_line_clamp
1098 .map_or(max_lines, |clamp| clamp.max(1).min(max_lines)),
1099 )
1100}
1101
1102fn paint_record_label(
1103 model: &CanvasPaintModel,
1104 target: &HitTarget,
1105 document_bounds: Bounds<Pixels>,
1106) -> Option<CanvasPaintLabel> {
1107 let label = match target {
1108 HitTarget::Node(id) => model
1109 .document
1110 .node(id)
1111 .and_then(|node| model.kind_registry.node_label(node)),
1112 HitTarget::Shape(id) => model
1113 .document
1114 .shape(id)
1115 .and_then(|shape| model.kind_registry.shape_label(shape)),
1116 HitTarget::Edge(_) | HitTarget::Handle { .. } => None,
1117 }?;
1118
1119 Some(resolve_paint_label(model, label, document_bounds))
1120}
1121
1122fn resolve_paint_label(
1123 model: &CanvasPaintModel,
1124 label: CanvasKindLabel,
1125 document_bounds: Bounds<Pixels>,
1126) -> CanvasPaintLabel {
1127 let document_bounds = label_document_bounds(document_bounds, label.inset);
1128 CanvasPaintLabel {
1129 text: label.text,
1130 document_bounds,
1131 view_bounds: model.viewport.document_bounds_to_view(document_bounds),
1132 color: label.color.as_deref().and_then(parse_color),
1133 }
1134}
1135
1136fn label_document_bounds(bounds: Bounds<Pixels>, inset: Pixels) -> Bounds<Pixels> {
1137 if !inset.as_f32().is_finite() || inset <= Pixels::ZERO {
1138 return bounds;
1139 }
1140
1141 let max_inset = bounds.size.width.min(bounds.size.height) * 0.5;
1142 bounds.inset(inset.min(max_inset))
1143}
1144
1145fn paint_edge_geometry(
1146 model: &CanvasPaintModel,
1147 target: &HitTarget,
1148 visual_state: CanvasPaintWireVisualState,
1149) -> Option<CanvasPaintEdgeGeometry> {
1150 let HitTarget::Edge(id) = target else {
1151 return None;
1152 };
1153 let geometry = model.runtime.edge_geometry(id)?;
1154 Some(CanvasPaintEdgeGeometry {
1155 view_path: route_path_to_view(&geometry.path, model.viewport),
1156 visual_state,
1157 })
1158}
1159
1160fn route_path_to_view(path: &CanvasRoutePath, viewport: CanvasViewport) -> CanvasRoutePath {
1161 CanvasRoutePath::new(path.segments.iter().map(|segment| match segment {
1162 CanvasRouteSegment::Line { from, to } => CanvasRouteSegment::Line {
1163 from: viewport.document_to_view(*from),
1164 to: viewport.document_to_view(*to),
1165 },
1166 CanvasRouteSegment::CubicBezier {
1167 from,
1168 control_1,
1169 control_2,
1170 to,
1171 } => CanvasRouteSegment::CubicBezier {
1172 from: viewport.document_to_view(*from),
1173 control_1: viewport.document_to_view(*control_1),
1174 control_2: viewport.document_to_view(*control_2),
1175 to: viewport.document_to_view(*to),
1176 },
1177 }))
1178}