Skip to main content

fission_core/input/
canvas.rs

1use fission_ir::{CanvasSelectionPolicy, CanvasTarget, CanvasTargetKind, WidgetId};
2use fission_layout::{LayoutPoint, LayoutRect, LayoutSnapshot};
3use serde::{Deserialize, Serialize};
4
5use crate::event::PointerKind;
6use crate::input::viewport::ViewportStateMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum CanvasInteractionPhase {
10    Start,
11    Update,
12    End,
13    Activate,
14    Cancel,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum CanvasInteractionKind {
19    SelectNode { node_id: u128 },
20    MoveNode { node_id: u128 },
21    ResizeNode { node_id: u128, handle: u8 },
22    SelectEdge { edge_id: u128 },
23    Marquee,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct CanvasInteraction {
28    pub canvas_id: WidgetId,
29    pub target_id: WidgetId,
30    pub kind: CanvasInteractionKind,
31    pub selection_policy: CanvasSelectionPolicy,
32    pub phase: CanvasInteractionPhase,
33    pub input_kind: PointerKind,
34    /// Modifier bitmask (Shift=1, Alt=2, Ctrl=4, Super=8).
35    pub modifiers: u8,
36    pub screen_point: LayoutPoint,
37    pub world_point: LayoutPoint,
38    pub screen_delta: LayoutPoint,
39    pub world_delta: LayoutPoint,
40    pub bounds_before: Option<LayoutRect>,
41    pub bounds_after: Option<LayoutRect>,
42    pub marquee: Option<LayoutRect>,
43}
44
45pub(crate) fn canvas_interaction(
46    target_id: WidgetId,
47    target: &CanvasTarget,
48    phase: CanvasInteractionPhase,
49    point: LayoutPoint,
50    delta: LayoutPoint,
51    drag_start: Option<LayoutPoint>,
52    layout: &LayoutSnapshot,
53    viewports: &ViewportStateMap,
54    input_kind: PointerKind,
55    modifiers: u8,
56) -> CanvasInteraction {
57    let canvas_id = WidgetId::from_u128(target.canvas_id);
58    let viewer_rect = layout
59        .get_node_rect(canvas_id)
60        .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
61    let transform = viewports.transform(canvas_id).unwrap_or_default();
62    let local = [point.x - viewer_rect.x(), point.y - viewer_rect.y()];
63    let world = transform.screen_to_world(local);
64    let world_point = LayoutPoint::new(world[0], world[1]);
65    let world_delta = LayoutPoint::new(delta.x / transform.scale, delta.y / transform.scale);
66    let (kind, bounds_before, bounds_after, marquee) = match &target.kind {
67        CanvasTargetKind::Node { node_id, bounds } => {
68            let before = rect(*bounds);
69            let mut after = before;
70            if matches!(phase, CanvasInteractionPhase::Update) {
71                after.origin.x += world_delta.x;
72                after.origin.y += world_delta.y;
73                after.origin.x = snap(after.origin.x, target);
74                after.origin.y = snap(after.origin.y, target);
75            }
76            (
77                if matches!(phase, CanvasInteractionPhase::Activate) {
78                    CanvasInteractionKind::SelectNode { node_id: *node_id }
79                } else {
80                    CanvasInteractionKind::MoveNode { node_id: *node_id }
81                },
82                Some(before),
83                Some(after),
84                None,
85            )
86        }
87        CanvasTargetKind::ResizeHandle {
88            node_id,
89            handle,
90            bounds,
91        } => {
92            let before = rect(*bounds);
93            let after = resize(before, *handle, world_delta, target);
94            (
95                CanvasInteractionKind::ResizeNode {
96                    node_id: *node_id,
97                    handle: *handle,
98                },
99                Some(before),
100                Some(after),
101                None,
102            )
103        }
104        CanvasTargetKind::Edge { edge_id, .. } => (
105            CanvasInteractionKind::SelectEdge { edge_id: *edge_id },
106            None,
107            None,
108            None,
109        ),
110        CanvasTargetKind::Marquee => {
111            let marquee = drag_start.map(|start| {
112                let start_local = [start.x - viewer_rect.x(), start.y - viewer_rect.y()];
113                let start_world = transform.screen_to_world(start_local);
114                normalized_rect(
115                    LayoutPoint::new(start_world[0], start_world[1]),
116                    world_point,
117                )
118            });
119            (CanvasInteractionKind::Marquee, None, None, marquee)
120        }
121    };
122    CanvasInteraction {
123        canvas_id,
124        target_id,
125        kind,
126        selection_policy: target.selection_policy,
127        phase,
128        input_kind,
129        modifiers,
130        screen_point: point,
131        world_point,
132        screen_delta: delta,
133        world_delta,
134        bounds_before,
135        bounds_after,
136        marquee,
137    }
138}
139
140fn rect(bounds: [f32; 4]) -> LayoutRect {
141    LayoutRect::new(bounds[0], bounds[1], bounds[2], bounds[3])
142}
143
144fn snap(value: f32, target: &CanvasTarget) -> f32 {
145    let Some(spacing) = target.snap_spacing.filter(|spacing| *spacing > 0.0) else {
146        return value;
147    };
148    let candidate = (value / spacing).round() * spacing;
149    if target.snap_threshold <= 0.0 || (candidate - value).abs() <= target.snap_threshold {
150        candidate
151    } else {
152        value
153    }
154}
155
156fn resize(before: LayoutRect, handle: u8, delta: LayoutPoint, target: &CanvasTarget) -> LayoutRect {
157    let mut left = before.x();
158    let mut top = before.y();
159    let mut right = before.right();
160    let mut bottom = before.bottom();
161    if matches!(handle, 0 | 6 | 7) {
162        left = snap(left + delta.x, target).min(right - 1.0);
163    }
164    if matches!(handle, 2 | 3 | 4) {
165        right = snap(right + delta.x, target).max(left + 1.0);
166    }
167    if matches!(handle, 0 | 1 | 2) {
168        top = snap(top + delta.y, target).min(bottom - 1.0);
169    }
170    if matches!(handle, 4 | 5 | 6) {
171        bottom = snap(bottom + delta.y, target).max(top + 1.0);
172    }
173    LayoutRect::new(left, top, right - left, bottom - top)
174}
175
176fn normalized_rect(first: LayoutPoint, second: LayoutPoint) -> LayoutRect {
177    let left = first.x.min(second.x);
178    let top = first.y.min(second.y);
179    LayoutRect::new(
180        left,
181        top,
182        (first.x - second.x).abs(),
183        (first.y - second.y).abs(),
184    )
185}