Skip to main content

open_gpui_canvas/gpui/
model.rs

1use crate::session::ToolState;
2use crate::{
3    CanvasConnectionEndpointRole, CanvasDefaultEdgeRouter, CanvasDocument, CanvasEdgeRoute,
4    CanvasEdgeRouteKind, CanvasEdgeRouter, CanvasEditor, CanvasEndpoint, CanvasKindRegistry,
5    CanvasRuntime, CanvasSelection, CanvasSnapGuide, CanvasViewport, HitTarget,
6};
7use open_gpui::{Hsla, Pixels, TextAlign, px, rgb};
8use std::sync::Arc;
9
10#[derive(Clone, Debug, PartialEq)]
11pub(crate) enum CanvasPaintInteractionState {
12    Idle,
13    Selecting {
14        origin: open_gpui::Point<Pixels>,
15        current: open_gpui::Point<Pixels>,
16    },
17    Connecting {
18        source: CanvasEndpoint,
19        current: open_gpui::Point<Pixels>,
20    },
21    Reconnecting {
22        edge_id: crate::EdgeId,
23        endpoint: CanvasConnectionEndpointRole,
24        fixed: CanvasEndpoint,
25        current: open_gpui::Point<Pixels>,
26    },
27    Transforming {
28        snap_guides: Vec<CanvasSnapGuide>,
29    },
30}
31
32#[derive(Clone, Debug)]
33pub struct CanvasPaintModel {
34    pub(super) document: Arc<CanvasDocument>,
35    pub(super) runtime: Arc<CanvasRuntime>,
36    pub(super) kind_registry: Arc<CanvasKindRegistry>,
37    pub(super) viewport: CanvasViewport,
38    pub(super) interaction: CanvasPaintInteraction,
39}
40
41impl CanvasPaintModel {
42    pub fn new(document: CanvasDocument, viewport: CanvasViewport) -> Self {
43        Self::new_with_router(document, viewport, &CanvasDefaultEdgeRouter)
44    }
45
46    pub fn new_with_kind_registry(
47        document: CanvasDocument,
48        viewport: CanvasViewport,
49        kind_registry: CanvasKindRegistry,
50    ) -> Self {
51        Self::new_with_router_and_kind_registry(
52            document,
53            viewport,
54            &CanvasDefaultEdgeRouter,
55            kind_registry,
56        )
57    }
58
59    pub fn new_with_router<R>(
60        document: CanvasDocument,
61        viewport: CanvasViewport,
62        router: &R,
63    ) -> Self
64    where
65        R: CanvasEdgeRouter + ?Sized,
66    {
67        Self::new_with_router_and_kind_registry(
68            document,
69            viewport,
70            router,
71            CanvasKindRegistry::open(),
72        )
73    }
74
75    pub fn new_with_router_and_kind_registry<R>(
76        document: CanvasDocument,
77        viewport: CanvasViewport,
78        router: &R,
79        kind_registry: CanvasKindRegistry,
80    ) -> Self
81    where
82        R: CanvasEdgeRouter + ?Sized,
83    {
84        let runtime =
85            CanvasRuntime::rebuild_with_router_and_kind_registry(&document, router, &kind_registry);
86        Self {
87            document: Arc::new(document),
88            runtime: Arc::new(runtime),
89            kind_registry: Arc::new(kind_registry),
90            viewport,
91            interaction: CanvasPaintInteraction::default(),
92        }
93    }
94
95    pub fn document(&self) -> &CanvasDocument {
96        self.document.as_ref()
97    }
98
99    pub fn runtime(&self) -> &CanvasRuntime {
100        self.runtime.as_ref()
101    }
102
103    pub fn kind_registry(&self) -> &CanvasKindRegistry {
104        self.kind_registry.as_ref()
105    }
106
107    pub fn viewport(&self) -> CanvasViewport {
108        self.viewport
109    }
110
111    pub fn interaction(&self) -> &CanvasPaintInteraction {
112        &self.interaction
113    }
114
115    pub fn with_interaction(mut self, interaction: CanvasPaintInteraction) -> Self {
116        self.interaction = interaction;
117        self
118    }
119
120    pub fn with_selection(mut self, selection: CanvasSelection) -> Self {
121        self.interaction = self.interaction.with_selection(selection);
122        self
123    }
124}
125
126impl From<&CanvasEditor> for CanvasPaintModel {
127    fn from(editor: &CanvasEditor) -> Self {
128        let session = editor.session_snapshot();
129        Self {
130            document: editor.document_snapshot(),
131            runtime: editor.runtime_snapshot(),
132            kind_registry: editor.kind_registry_snapshot(),
133            viewport: session.viewport,
134            interaction: CanvasPaintInteraction::new(session.selection)
135                .with_internal_tool_state(session.state),
136        }
137    }
138}
139
140#[derive(Clone, Debug, PartialEq)]
141pub struct CanvasPaintInteraction {
142    selection: CanvasSelection,
143    state: CanvasPaintInteractionState,
144    hovered_target: Option<HitTarget>,
145}
146
147impl CanvasPaintInteraction {
148    pub fn new(selection: CanvasSelection) -> Self {
149        Self {
150            selection,
151            state: CanvasPaintInteractionState::Idle,
152            hovered_target: None,
153        }
154    }
155
156    pub fn selection(&self) -> &CanvasSelection {
157        &self.selection
158    }
159
160    pub(crate) fn state(&self) -> &CanvasPaintInteractionState {
161        &self.state
162    }
163
164    pub fn hovered_target(&self) -> Option<&HitTarget> {
165        self.hovered_target.as_ref()
166    }
167
168    pub fn with_selection(mut self, selection: CanvasSelection) -> Self {
169        self.selection = selection;
170        self
171    }
172
173    pub fn with_hovered_target(mut self, target: Option<HitTarget>) -> Self {
174        self.hovered_target = target;
175        self
176    }
177
178    pub(crate) fn with_internal_tool_state(mut self, state: ToolState) -> Self {
179        self.state = CanvasPaintInteractionState::from_tool_state(state);
180        self
181    }
182}
183
184impl Default for CanvasPaintInteraction {
185    fn default() -> Self {
186        Self {
187            selection: CanvasSelection::default(),
188            state: CanvasPaintInteractionState::Idle,
189            hovered_target: None,
190        }
191    }
192}
193
194impl CanvasPaintInteractionState {
195    pub(crate) fn from_tool_state(state: ToolState) -> Self {
196        match state {
197            ToolState::Idle
198            | ToolState::Pointing { .. }
199            | ToolState::PendingTranslation { .. }
200            | ToolState::Panning { .. } => Self::Idle,
201            ToolState::Selecting {
202                origin, current, ..
203            } => Self::Selecting { origin, current },
204            ToolState::Connecting { source, current } => Self::Connecting { source, current },
205            ToolState::Reconnecting {
206                edge_id,
207                endpoint,
208                fixed,
209                current,
210                ..
211            } => Self::Reconnecting {
212                edge_id,
213                endpoint,
214                fixed,
215                current,
216            },
217            ToolState::Translating { snap_guides, .. }
218            | ToolState::Resizing { snap_guides, .. } => Self::Transforming { snap_guides },
219        }
220    }
221}
222
223#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
224pub enum CanvasConnectionPreviewRoute {
225    #[default]
226    Straight,
227    Polyline,
228    Orthogonal,
229    CubicBezier,
230}
231
232impl CanvasConnectionPreviewRoute {
233    pub(crate) fn edge_route(self) -> CanvasEdgeRoute {
234        match self {
235            Self::Straight => CanvasEdgeRoute::straight(),
236            Self::Polyline => CanvasEdgeRoute::polyline(std::iter::empty()),
237            Self::Orthogonal => CanvasEdgeRoute::orthogonal(),
238            Self::CubicBezier => CanvasEdgeRoute::new(CanvasEdgeRouteKind::CUBIC_BEZIER),
239        }
240    }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq)]
244pub struct CanvasPaintOptions {
245    pub include_hidden: bool,
246    pub include_handles: bool,
247    pub include_interaction_feedback: bool,
248    pub cull_margin: Pixels,
249    pub connection_preview_route: CanvasConnectionPreviewRoute,
250}
251
252impl Default for CanvasPaintOptions {
253    fn default() -> Self {
254        Self {
255            include_hidden: false,
256            include_handles: false,
257            include_interaction_feedback: true,
258            cull_margin: Pixels::ZERO,
259            connection_preview_route: CanvasConnectionPreviewRoute::default(),
260        }
261    }
262}
263
264#[derive(Clone, Copy, Debug, PartialEq)]
265pub struct CanvasPaintTheme {
266    pub background: Option<Hsla>,
267    pub node_fill: Hsla,
268    pub node_stroke: Hsla,
269    pub node_stroke_width: Pixels,
270    pub node_corner_radius: Pixels,
271    pub shape_fill: Hsla,
272    pub shape_stroke: Hsla,
273    pub shape_stroke_width: Pixels,
274    pub edge_stroke: Hsla,
275    pub edge_stroke_width: Pixels,
276    pub handle_fill: Hsla,
277    pub handle_stroke: Hsla,
278    pub handle_stroke_width: Pixels,
279    pub handle_corner_radius: Pixels,
280    pub selection_fill: Hsla,
281    pub selection_stroke: Hsla,
282    pub selection_stroke_width: Pixels,
283    pub selection_corner_radius: Pixels,
284    pub selection_bounds_fill: Hsla,
285    pub selection_bounds_stroke: Hsla,
286    pub selection_bounds_stroke_width: Pixels,
287    pub connection_preview_stroke: Hsla,
288    pub connection_preview_stroke_width: Pixels,
289    pub connection_free_target_fill: Hsla,
290    pub connection_free_target_stroke: Hsla,
291    pub connection_valid_target_fill: Hsla,
292    pub connection_valid_target_stroke: Hsla,
293    pub connection_invalid_target_fill: Hsla,
294    pub connection_invalid_target_stroke: Hsla,
295    pub snap_guide_stroke: Hsla,
296    pub snap_guide_stroke_width: Pixels,
297    pub label_color: Hsla,
298    pub label_font_size: Pixels,
299    pub label_line_height: Pixels,
300    pub label_line_clamp: Option<usize>,
301    pub label_text_align: TextAlign,
302}
303
304impl Default for CanvasPaintTheme {
305    fn default() -> Self {
306        Self {
307            background: None,
308            node_fill: Hsla::from(rgb(0xffffff)),
309            node_stroke: Hsla::from(rgb(0xd0d7de)),
310            node_stroke_width: px(1.0),
311            node_corner_radius: px(6.0),
312            shape_fill: Hsla::from(rgb(0xf6f8fa)),
313            shape_stroke: Hsla::from(rgb(0xd0d7de)),
314            shape_stroke_width: px(1.0),
315            edge_stroke: Hsla::from(rgb(0x57606a)),
316            edge_stroke_width: px(2.0),
317            handle_fill: Hsla::from(rgb(0x0969da)),
318            handle_stroke: Hsla::from(rgb(0xffffff)),
319            handle_stroke_width: px(1.0),
320            handle_corner_radius: px(6.0),
321            selection_fill: Hsla::from(rgb(0x0969da)).alpha(0.08),
322            selection_stroke: Hsla::from(rgb(0x0969da)),
323            selection_stroke_width: px(2.0),
324            selection_corner_radius: px(7.0),
325            selection_bounds_fill: Hsla::from(rgb(0x0969da)).alpha(0.08),
326            selection_bounds_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
327            selection_bounds_stroke_width: px(1.0),
328            connection_preview_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
329            connection_preview_stroke_width: px(2.0),
330            connection_free_target_fill: Hsla::from(rgb(0x0969da)).alpha(0.12),
331            connection_free_target_stroke: Hsla::from(rgb(0x0969da)).alpha(0.7),
332            connection_valid_target_fill: Hsla::from(rgb(0x1a7f37)).alpha(0.14),
333            connection_valid_target_stroke: Hsla::from(rgb(0x1a7f37)).alpha(0.9),
334            connection_invalid_target_fill: Hsla::from(rgb(0xcf222e)).alpha(0.12),
335            connection_invalid_target_stroke: Hsla::from(rgb(0xcf222e)).alpha(0.9),
336            snap_guide_stroke: Hsla::from(rgb(0xbf8700)).alpha(0.9),
337            snap_guide_stroke_width: px(1.0),
338            label_color: Hsla::from(rgb(0x24292f)),
339            label_font_size: px(14.0),
340            label_line_height: px(18.0),
341            label_line_clamp: Some(3),
342            label_text_align: TextAlign::Center,
343        }
344    }
345}