use crate::camera::Camera;
use crate::shapes::{Group, Shape, ShapeId, ShapeTrait};
use crate::tools::{ToolKind, ToolManager};
use crate::widget::{WidgetManager, WidgetState, EditingKind};
use kurbo::{Point, Rect};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
const MAX_UNDO_HISTORY: usize = 50;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DocumentSnapshot {
shapes: HashMap<ShapeId, Shape>,
z_order: Vec<ShapeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasDocument {
pub id: String,
pub name: String,
pub shapes: HashMap<ShapeId, Shape>,
pub z_order: Vec<ShapeId>,
#[serde(skip)]
undo_stack: Vec<DocumentSnapshot>,
#[serde(skip)]
redo_stack: Vec<DocumentSnapshot>,
}
impl Default for CanvasDocument {
fn default() -> Self {
Self::new()
}
}
impl CanvasDocument {
pub fn new() -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: "Untitled".to_string(),
shapes: HashMap::new(),
z_order: Vec::new(),
undo_stack: Vec::new(),
redo_stack: Vec::new(),
}
}
fn snapshot(&self) -> DocumentSnapshot {
DocumentSnapshot {
shapes: self.shapes.clone(),
z_order: self.z_order.clone(),
}
}
pub fn push_undo(&mut self) {
let snapshot = self.snapshot();
self.undo_stack.push(snapshot);
self.redo_stack.clear();
if self.undo_stack.len() > MAX_UNDO_HISTORY {
self.undo_stack.remove(0);
}
}
pub fn undo(&mut self) -> bool {
if let Some(snapshot) = self.undo_stack.pop() {
let current = self.snapshot();
self.redo_stack.push(current);
self.shapes = snapshot.shapes;
self.z_order = snapshot.z_order;
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(snapshot) = self.redo_stack.pop() {
let current = self.snapshot();
self.undo_stack.push(current);
self.shapes = snapshot.shapes;
self.z_order = snapshot.z_order;
true
} else {
false
}
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn add_shape(&mut self, shape: Shape) {
let id = shape.id();
self.z_order.push(id);
self.shapes.insert(id, shape);
}
pub fn remove_shape(&mut self, id: ShapeId) -> Option<Shape> {
self.z_order.retain(|&shape_id| shape_id != id);
self.shapes.remove(&id)
}
pub fn clear(&mut self) {
self.shapes.clear();
self.z_order.clear();
}
pub fn get_shape(&self, id: ShapeId) -> Option<&Shape> {
self.shapes.get(&id)
}
pub fn get_shape_mut(&mut self, id: ShapeId) -> Option<&mut Shape> {
self.shapes.get_mut(&id)
}
pub fn shapes_ordered(&self) -> impl Iterator<Item = &Shape> {
self.z_order.iter().filter_map(|id| self.shapes.get(id))
}
pub fn bring_to_front(&mut self, id: ShapeId) {
self.z_order.retain(|&shape_id| shape_id != id);
self.z_order.push(id);
}
pub fn send_to_back(&mut self, id: ShapeId) {
self.z_order.retain(|&shape_id| shape_id != id);
self.z_order.insert(0, id);
}
pub fn bring_forward(&mut self, id: ShapeId) -> bool {
if let Some(pos) = self.z_order.iter().position(|&shape_id| shape_id == id) {
if pos < self.z_order.len() - 1 {
self.z_order.swap(pos, pos + 1);
return true;
}
}
false
}
pub fn send_backward(&mut self, id: ShapeId) -> bool {
if let Some(pos) = self.z_order.iter().position(|&shape_id| shape_id == id) {
if pos > 0 {
self.z_order.swap(pos, pos - 1);
return true;
}
}
false
}
pub fn bounds(&self) -> Option<Rect> {
let mut result: Option<Rect> = None;
for shape in self.shapes.values() {
let bounds = shape.bounds();
result = Some(match result {
Some(r) => r.union(bounds),
None => bounds,
});
}
result
}
pub fn shapes_at_point(&self, point: Point, tolerance: f64) -> Vec<ShapeId> {
self.z_order
.iter()
.rev()
.filter_map(|&id| {
self.shapes
.get(&id)
.filter(|s| s.hit_test(point, tolerance))
.map(|_| id)
})
.collect()
}
pub fn shapes_in_rect(&self, rect: Rect) -> Vec<ShapeId> {
self.z_order
.iter()
.filter_map(|&id| {
self.shapes
.get(&id)
.filter(|s| {
let bounds = s.bounds();
rect.intersect(bounds).area() > 0.0
})
.map(|_| id)
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.shapes.is_empty()
}
pub fn len(&self) -> usize {
self.shapes.len()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn export_selection(&self, selection: &[ShapeId]) -> Self {
let mut doc = Self::new();
for &id in selection {
if let Some(shape) = self.shapes.get(&id) {
doc.add_shape(shape.clone());
}
}
doc
}
pub fn group_shapes(&mut self, shape_ids: &[ShapeId]) -> Option<ShapeId> {
if shape_ids.len() < 2 {
return None;
}
let mut shapes_to_group: Vec<(usize, Shape)> = Vec::new();
for (idx, &zid) in self.z_order.iter().enumerate() {
if shape_ids.contains(&zid) {
if let Some(shape) = self.shapes.get(&zid) {
shapes_to_group.push((idx, shape.clone()));
}
}
}
if shapes_to_group.len() < 2 {
return None;
}
let max_z_idx = shapes_to_group.iter().map(|(idx, _)| *idx).max().unwrap();
let children: Vec<Shape> = shapes_to_group.into_iter().map(|(_, s)| s).collect();
let group = Group::new(children);
let group_id = group.id();
for &id in shape_ids {
self.shapes.remove(&id);
self.z_order.retain(|&zid| zid != id);
}
self.shapes.insert(group_id, Shape::Group(group));
let insert_pos = max_z_idx.saturating_sub(shape_ids.len() - 1).min(self.z_order.len());
self.z_order.insert(insert_pos, group_id);
Some(group_id)
}
pub fn ungroup_shape(&mut self, group_id: ShapeId) -> Option<Vec<ShapeId>> {
let group = match self.shapes.get(&group_id) {
Some(Shape::Group(g)) => g.clone(),
_ => return None,
};
let z_pos = self.z_order.iter().position(|&id| id == group_id)?;
self.shapes.remove(&group_id);
self.z_order.retain(|&id| id != group_id);
let children = group.ungroup();
let child_ids: Vec<ShapeId> = children.iter().map(|s| s.id()).collect();
for (i, child) in children.into_iter().enumerate() {
let child_id = child.id();
self.shapes.insert(child_id, child);
self.z_order.insert(z_pos + i, child_id);
}
Some(child_ids)
}
}
#[derive(Debug, Clone)]
pub struct Canvas {
pub document: CanvasDocument,
pub camera: Camera,
pub tool_manager: ToolManager,
pub selection: Vec<ShapeId>,
pub viewport_size: kurbo::Size,
pub widgets: WidgetManager,
}
impl Default for Canvas {
fn default() -> Self {
Self::new()
}
}
impl Canvas {
pub fn new() -> Self {
Self {
document: CanvasDocument::new(),
camera: Camera::new(),
tool_manager: ToolManager::new(),
selection: Vec::new(),
viewport_size: kurbo::Size::new(800.0, 600.0),
widgets: WidgetManager::new(),
}
}
pub fn with_document(document: CanvasDocument) -> Self {
Self {
document,
camera: Camera::new(),
tool_manager: ToolManager::new(),
selection: Vec::new(),
viewport_size: kurbo::Size::new(800.0, 600.0),
widgets: WidgetManager::new(),
}
}
pub fn set_viewport_size(&mut self, width: f64, height: f64) {
self.viewport_size = kurbo::Size::new(width, height);
}
pub fn select(&mut self, id: ShapeId) {
self.clear_selection();
self.add_to_selection(id);
}
pub fn add_to_selection(&mut self, id: ShapeId) {
if !self.selection.contains(&id) {
self.selection.push(id);
}
self.widgets.add_to_selection(id);
}
pub fn clear_selection(&mut self) {
self.selection.clear();
self.widgets.clear_selection();
}
pub fn is_selected(&self, id: ShapeId) -> bool {
self.widgets.is_selected(id)
}
pub fn enter_text_editing(&mut self, id: ShapeId) {
self.widgets.enter_editing(id, EditingKind::Text);
}
pub fn exit_text_editing(&mut self) {
self.widgets.exit_editing();
}
pub fn editing_shape(&self) -> Option<ShapeId> {
self.widgets.focused()
}
pub fn is_editing(&self, id: ShapeId) -> bool {
self.widgets.is_editing_shape(id)
}
pub fn widget_state(&self, id: ShapeId) -> WidgetState {
self.widgets.state(id)
}
pub fn set_tool(&mut self, tool: ToolKind) {
self.tool_manager.set_tool(tool);
}
pub fn fit_to_content(&mut self) {
if let Some(bounds) = self.document.bounds() {
self.camera.fit_to_bounds(bounds, self.viewport_size, 50.0);
}
}
pub fn delete_selected(&mut self) {
for id in self.selection.drain(..).collect::<Vec<_>>() {
self.document.remove_shape(id);
self.widgets.remove(id);
}
}
pub fn remove_shape(&mut self, id: ShapeId) {
self.selection.retain(|&s| s != id);
self.document.remove_shape(id);
self.widgets.remove(id);
}
pub fn group_selected(&mut self) -> Option<ShapeId> {
if self.selection.len() < 2 {
return None;
}
let shape_ids: Vec<ShapeId> = self.selection.clone();
self.document.push_undo();
if let Some(group_id) = self.document.group_shapes(&shape_ids) {
for &id in &shape_ids {
self.widgets.remove(id);
}
self.selection.clear();
self.selection.push(group_id);
self.widgets.add_to_selection(group_id);
Some(group_id)
} else {
None
}
}
pub fn ungroup_selected(&mut self) -> Vec<ShapeId> {
let mut all_children = Vec::new();
let groups: Vec<ShapeId> = self.selection
.iter()
.filter(|&&id| {
self.document.get_shape(id)
.map(|s| s.is_group())
.unwrap_or(false)
})
.copied()
.collect();
if groups.is_empty() {
return all_children;
}
self.document.push_undo();
for group_id in groups {
self.widgets.remove(group_id);
self.selection.retain(|&id| id != group_id);
if let Some(children) = self.document.ungroup_shape(group_id) {
for &child_id in &children {
if !self.selection.contains(&child_id) {
self.selection.push(child_id);
}
self.widgets.add_to_selection(child_id);
}
all_children.extend(children);
}
}
all_children
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes::{Rectangle, ShapeTrait};
#[test]
fn test_document_creation() {
let doc = CanvasDocument::new();
assert!(doc.is_empty());
}
#[test]
fn test_add_shape() {
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
doc.add_shape(Shape::Rectangle(rect));
assert_eq!(doc.len(), 1);
assert!(doc.get_shape(id).is_some());
}
#[test]
fn test_remove_shape() {
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
doc.add_shape(Shape::Rectangle(rect));
let removed = doc.remove_shape(id);
assert!(removed.is_some());
assert!(doc.is_empty());
}
#[test]
fn test_z_order() {
let mut doc = CanvasDocument::new();
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let rect2 = Rectangle::new(Point::new(50.0, 50.0), 100.0, 100.0);
let id1 = rect1.id();
let id2 = rect2.id();
doc.add_shape(Shape::Rectangle(rect1));
doc.add_shape(Shape::Rectangle(rect2));
assert_eq!(doc.z_order, vec![id1, id2]);
doc.bring_to_front(id1);
assert_eq!(doc.z_order, vec![id2, id1]);
doc.send_to_back(id1);
assert_eq!(doc.z_order, vec![id1, id2]);
}
#[test]
fn test_shapes_at_point() {
let mut doc = CanvasDocument::new();
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let rect2 = Rectangle::new(Point::new(50.0, 50.0), 100.0, 100.0);
let id1 = rect1.id();
let id2 = rect2.id();
doc.add_shape(Shape::Rectangle(rect1));
doc.add_shape(Shape::Rectangle(rect2));
let hits = doc.shapes_at_point(Point::new(75.0, 75.0), 0.0);
assert_eq!(hits.len(), 2);
assert_eq!(hits[0], id2);
assert_eq!(hits[1], id1);
let hits = doc.shapes_at_point(Point::new(25.0, 25.0), 0.0);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0], id1);
}
#[test]
fn test_canvas_selection() {
let mut canvas = Canvas::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
canvas.document.add_shape(Shape::Rectangle(rect));
assert!(!canvas.is_selected(id));
canvas.select(id);
assert!(canvas.is_selected(id));
canvas.clear_selection();
assert!(!canvas.is_selected(id));
}
#[test]
fn test_delete_selected() {
let mut canvas = Canvas::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
canvas.document.add_shape(Shape::Rectangle(rect));
canvas.select(id);
canvas.delete_selected();
assert!(canvas.document.is_empty());
assert!(canvas.selection.is_empty());
}
#[test]
fn test_undo_add_shape() {
let mut doc = CanvasDocument::new();
doc.push_undo();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
doc.add_shape(Shape::Rectangle(rect));
assert_eq!(doc.len(), 1);
assert!(doc.can_undo());
assert!(doc.undo());
assert!(doc.is_empty());
assert!(doc.can_redo());
assert!(doc.redo());
assert_eq!(doc.len(), 1);
assert!(doc.get_shape(id).is_some());
}
#[test]
fn test_undo_remove_shape() {
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let id = rect.id();
doc.add_shape(Shape::Rectangle(rect));
doc.push_undo();
doc.remove_shape(id);
assert!(doc.is_empty());
assert!(doc.undo());
assert_eq!(doc.len(), 1);
assert!(doc.get_shape(id).is_some());
}
#[test]
fn test_undo_clears_redo() {
let mut doc = CanvasDocument::new();
doc.push_undo();
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
doc.add_shape(Shape::Rectangle(rect1));
assert!(doc.undo());
assert!(doc.can_redo());
doc.push_undo();
let rect2 = Rectangle::new(Point::new(50.0, 50.0), 100.0, 100.0);
doc.add_shape(Shape::Rectangle(rect2));
assert!(!doc.can_redo());
}
#[test]
fn test_undo_empty_stack() {
let mut doc = CanvasDocument::new();
assert!(!doc.can_undo());
assert!(!doc.undo());
assert!(!doc.can_redo());
assert!(!doc.redo());
}
}