drafftink_core/widget/
manager.rs

1//! Widget manager for tracking UI state of shapes.
2
3use std::collections::{HashMap, HashSet};
4use kurbo::Point;
5use crate::shapes::{Shape, ShapeId, ShapeTrait};
6use super::state::{WidgetState, EditingKind};
7use super::handles::{Handle, HandleKind, HandleShape};
8
9/// Manages UI state for all shapes in the document.
10///
11/// This separates UI concerns (selection, editing, hover) from
12/// the pure shape data, making the system easier to reason about.
13#[derive(Debug, Clone)]
14pub struct WidgetManager {
15    /// UI state for each shape.
16    states: HashMap<ShapeId, WidgetState>,
17    /// Currently selected shapes (subset of states with Selected/Editing).
18    selected: HashSet<ShapeId>,
19    /// Shape that has keyboard focus (for text editing).
20    focused: Option<ShapeId>,
21    /// Shape currently being hovered.
22    hovered: Option<ShapeId>,
23}
24
25impl WidgetManager {
26    /// Create a new widget manager.
27    pub fn new() -> Self {
28        Self {
29            states: HashMap::new(),
30            selected: HashSet::new(),
31            focused: None,
32            hovered: None,
33        }
34    }
35
36    /// Get the state of a shape.
37    pub fn state(&self, id: ShapeId) -> WidgetState {
38        self.states.get(&id).cloned().unwrap_or_default()
39    }
40
41    /// Set the state of a shape.
42    pub fn set_state(&mut self, id: ShapeId, state: WidgetState) {
43        // Update selected set
44        if state.is_selected() {
45            self.selected.insert(id);
46        } else {
47            self.selected.remove(&id);
48        }
49
50        // Update focused
51        if state.is_editing() {
52            self.focused = Some(id);
53        } else if self.focused == Some(id) {
54            self.focused = None;
55        }
56
57        self.states.insert(id, state);
58    }
59
60    /// Check if a shape is selected.
61    pub fn is_selected(&self, id: ShapeId) -> bool {
62        self.selected.contains(&id)
63    }
64
65    /// Get all selected shape IDs.
66    pub fn selected(&self) -> &HashSet<ShapeId> {
67        &self.selected
68    }
69
70    /// Get the focused shape ID (if any).
71    pub fn focused(&self) -> Option<ShapeId> {
72        self.focused
73    }
74
75    /// Get the hovered shape ID (if any).
76    pub fn hovered(&self) -> Option<ShapeId> {
77        self.hovered
78    }
79
80    /// Set the hovered shape.
81    pub fn set_hovered(&mut self, id: Option<ShapeId>) {
82        // Clear old hover state
83        if let Some(old_id) = self.hovered {
84            if Some(old_id) != id {
85                if let Some(state) = self.states.get(&old_id) {
86                    if *state == WidgetState::Hovered {
87                        self.states.insert(old_id, WidgetState::Normal);
88                    }
89                }
90            }
91        }
92
93        // Set new hover state
94        if let Some(new_id) = id {
95            let current = self.state(new_id);
96            if current == WidgetState::Normal {
97                self.states.insert(new_id, WidgetState::Hovered);
98            }
99        }
100
101        self.hovered = id;
102    }
103
104    /// Select a single shape (clears other selections).
105    pub fn select(&mut self, id: ShapeId) {
106        self.clear_selection();
107        self.add_to_selection(id);
108    }
109
110    /// Add a shape to the selection.
111    pub fn add_to_selection(&mut self, id: ShapeId) {
112        self.set_state(id, WidgetState::Selected);
113    }
114
115    /// Remove a shape from the selection.
116    pub fn deselect(&mut self, id: ShapeId) {
117        if self.selected.contains(&id) {
118            self.set_state(id, WidgetState::Normal);
119        }
120    }
121
122    /// Clear all selections.
123    pub fn clear_selection(&mut self) {
124        let selected: Vec<_> = self.selected.iter().copied().collect();
125        for id in selected {
126            self.set_state(id, WidgetState::Normal);
127        }
128        self.selected.clear();
129    }
130
131    /// Enter editing mode for a shape.
132    pub fn enter_editing(&mut self, id: ShapeId, kind: EditingKind) {
133        // Exit any current editing
134        if let Some(old_id) = self.focused {
135            if old_id != id {
136                self.exit_editing();
137            }
138        }
139        
140        self.set_state(id, WidgetState::Editing(kind));
141    }
142
143    /// Exit editing mode.
144    pub fn exit_editing(&mut self) {
145        if let Some(id) = self.focused {
146            self.set_state(id, WidgetState::Selected);
147        }
148    }
149
150    /// Check if currently in editing mode.
151    pub fn is_editing(&self) -> bool {
152        self.focused.is_some()
153    }
154
155    /// Check if a specific shape is being edited.
156    pub fn is_editing_shape(&self, id: ShapeId) -> bool {
157        self.focused == Some(id)
158    }
159
160    /// Remove state for a deleted shape.
161    pub fn remove(&mut self, id: ShapeId) {
162        self.states.remove(&id);
163        self.selected.remove(&id);
164        if self.focused == Some(id) {
165            self.focused = None;
166        }
167        if self.hovered == Some(id) {
168            self.hovered = None;
169        }
170    }
171
172    /// Get handles for a shape based on its type and state.
173    pub fn get_handles(&self, shape: &Shape) -> Vec<Handle> {
174        let id = shape.id();
175        if !self.is_selected(id) {
176            return vec![];
177        }
178
179        get_shape_handles(shape)
180    }
181}
182
183impl Default for WidgetManager {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189/// Get the manipulation handles for a shape.
190pub fn get_shape_handles(shape: &Shape) -> Vec<Handle> {
191    match shape {
192        Shape::Rectangle(r) => {
193            let bounds = r.bounds();
194            vec![
195                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
196                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
197                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
198                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
199            ]
200        }
201        Shape::Ellipse(e) => {
202            let bounds = e.bounds();
203            vec![
204                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
205                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
206                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
207                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
208            ]
209        }
210        Shape::Line(l) => {
211            vec![
212                Handle::new(HandleKind::Start, l.start).with_shape(HandleShape::Circle),
213                Handle::new(HandleKind::End, l.end).with_shape(HandleShape::Circle),
214            ]
215        }
216        Shape::Arrow(a) => {
217            vec![
218                Handle::new(HandleKind::Start, a.start).with_shape(HandleShape::Circle),
219                Handle::new(HandleKind::End, a.end).with_shape(HandleShape::Circle),
220            ]
221        }
222        Shape::Freehand(f) => {
223            let bounds = f.bounds();
224            vec![
225                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
226                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
227                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
228                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
229            ]
230        }
231        Shape::Text(t) => {
232            let bounds = t.bounds();
233            vec![
234                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
235                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
236                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
237                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
238            ]
239        }
240        Shape::Group(g) => {
241            // Groups use bounding box corners
242            let bounds = g.bounds();
243            vec![
244                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
245                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
246                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
247                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
248            ]
249        }
250        Shape::Image(img) => {
251            // Images use bounding box corners for resizing
252            let bounds = img.bounds();
253            vec![
254                Handle::new(HandleKind::TopLeft, Point::new(bounds.x0, bounds.y0)),
255                Handle::new(HandleKind::TopRight, Point::new(bounds.x1, bounds.y0)),
256                Handle::new(HandleKind::BottomLeft, Point::new(bounds.x0, bounds.y1)),
257                Handle::new(HandleKind::BottomRight, Point::new(bounds.x1, bounds.y1)),
258            ]
259        }
260    }
261}
262
263/// Hit test handles for a shape.
264#[allow(dead_code)]
265pub fn hit_test_handle(shape: &Shape, point: Point, tolerance: f64) -> Option<HandleKind> {
266    let handles = get_shape_handles(shape);
267    for handle in handles {
268        let dx = point.x - handle.position.x;
269        let dy = point.y - handle.position.y;
270        if dx * dx + dy * dy <= tolerance * tolerance {
271            return Some(handle.kind);
272        }
273    }
274    None
275}