Skip to main content

canvas_core/
state.rs

1//! Canvas state management.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{InputEvent, Scene};
6
7/// Connection status to the AI/MCP.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum ConnectionStatus {
11    /// Fully connected and operational.
12    Connected,
13    /// Attempting to connect.
14    Connecting,
15    /// Disconnected but can operate offline.
16    Offline,
17    /// Connection error.
18    Error,
19}
20
21/// Interaction mode for direct manipulation.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum InteractionMode {
25    /// Default mode - select and manipulate elements.
26    Select,
27    /// Pan/zoom the canvas.
28    Pan,
29    /// Drawing mode (for annotations).
30    Draw,
31    /// Voice command active.
32    Voice,
33}
34
35/// The complete canvas state.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CanvasState {
38    /// The scene graph.
39    pub scene: Scene,
40    /// Connection status to AI/MCP.
41    pub connection: ConnectionStatus,
42    /// Current interaction mode.
43    pub mode: InteractionMode,
44    /// Pending events to sync when reconnected.
45    pending_sync: Vec<InputEvent>,
46    /// Whether there are unsaved local changes.
47    pub has_local_changes: bool,
48}
49
50impl CanvasState {
51    /// Create a new canvas state with the given viewport size.
52    #[must_use]
53    pub fn new(width: f32, height: f32) -> Self {
54        Self {
55            scene: Scene::new(width, height),
56            connection: ConnectionStatus::Connecting,
57            mode: InteractionMode::Select,
58            pending_sync: Vec::new(),
59            has_local_changes: false,
60        }
61    }
62
63    /// Process an input event.
64    pub fn process_event(&mut self, event: &InputEvent) {
65        // If offline, queue for later sync
66        if self.connection == ConnectionStatus::Offline {
67            self.pending_sync.push(event.clone());
68        }
69
70        match event {
71            InputEvent::Touch(touch) => {
72                // Find element at touch point
73                if let Some(primary) = touch.primary_touch() {
74                    if let Some(element_id) = self.scene.element_at(primary.x, primary.y) {
75                        tracing::debug!("Touch on element: {element_id}");
76                    }
77                }
78            }
79            InputEvent::Gesture(gesture) => {
80                tracing::debug!("Gesture: {:?}", gesture);
81            }
82            InputEvent::Voice(voice) => {
83                tracing::debug!("Voice command: {}", voice.transcript);
84            }
85            _ => {}
86        }
87
88        self.has_local_changes = true;
89    }
90
91    /// Set connection status.
92    pub fn set_connection(&mut self, status: ConnectionStatus) {
93        let was_offline = self.connection == ConnectionStatus::Offline;
94        self.connection = status;
95
96        // If we just reconnected, we might want to sync pending events
97        if was_offline && status == ConnectionStatus::Connected {
98            tracing::info!(
99                "Reconnected with {} pending events to sync",
100                self.pending_sync.len()
101            );
102        }
103    }
104
105    /// Get pending events to sync.
106    #[must_use]
107    pub fn pending_events(&self) -> &[InputEvent] {
108        &self.pending_sync
109    }
110
111    /// Clear pending events after successful sync.
112    pub fn clear_pending(&mut self) {
113        self.pending_sync.clear();
114    }
115
116    /// Check if connected to an AI backend.
117    #[must_use]
118    pub fn is_connected(&self) -> bool {
119        self.connection == ConnectionStatus::Connected
120    }
121
122    /// Get the current connection status.
123    #[must_use]
124    pub fn connection_status(&self) -> ConnectionStatus {
125        self.connection
126    }
127
128    /// Check if we can perform interactive operations.
129    #[must_use]
130    pub fn can_interact(&self) -> bool {
131        // Graceful degradation: allow some operations offline
132        match self.connection {
133            ConnectionStatus::Connected => true,
134            ConnectionStatus::Offline => {
135                // Allow view, pan, zoom, select locally
136                matches!(self.mode, InteractionMode::Select | InteractionMode::Pan)
137            }
138            ConnectionStatus::Connecting | ConnectionStatus::Error => false,
139        }
140    }
141}
142
143impl Default for CanvasState {
144    fn default() -> Self {
145        Self::new(800.0, 600.0)
146    }
147}