Skip to main content

open_gpui_canvas/gpui/
frame.rs

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}