diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 2e5fbab416..f00c0a4cdb 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -30,7 +30,6 @@ impl Dispatcher { | Message::Frontend(FrontendMessage::UpdateScrollbars { .. }) | Message::Frontend(FrontendMessage::SetCanvasZoom { .. }) | Message::Frontend(FrontendMessage::SetCanvasRotation { .. }) - | Message::Documents(DocumentsMessage::Document(DocumentMessage::DispatchOperation { .. })) ) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove")) { log::trace!("Message: {:?}", message); @@ -152,7 +151,7 @@ mod test { let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1]; - editor.handle_message(DocumentMessage::SelectLayers(vec![vec![shape_id]])).unwrap(); + editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]])).unwrap(); editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); @@ -212,7 +211,7 @@ mod test { }) .unwrap(); - editor.handle_message(DocumentMessage::SelectLayers(vec![vec![folder_id]])).unwrap(); + editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![folder_id]])).unwrap(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); @@ -276,7 +275,7 @@ mod test { let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX]; let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX]; - editor.handle_message(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap(); + editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap(); editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap(); editor.draw_rect(0., 800., 12., 200.); @@ -310,7 +309,7 @@ mod test { let verify_order = |handler: &mut DocumentMessageHandler| (handler.all_layers_sorted(), handler.non_selected_layers_sorted(), handler.selected_layers_sorted()); - editor.handle_message(DocumentMessage::SelectLayers(vec![vec![0], vec![2]])).unwrap(); + editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![0], vec![2]])).unwrap(); editor.handle_message(DocumentMessage::ReorderSelectedLayers(1)).unwrap(); let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut()); diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 700b21979c..b39d1d529a 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -66,8 +66,10 @@ pub enum DocumentMessage { #[child] Movement(MovementMessage), DispatchOperation(Box), - SelectLayers(Vec>), + SetSelectedLayers(Vec>), + AddSelectedLayers(Vec>), SelectAllLayers, + SelectionChanged, DeselectAllLayers, DeleteLayer(Vec), DeleteSelectedLayers, @@ -125,9 +127,17 @@ impl DocumentMessageHandler { self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false); } fn select_layer(&mut self, path: &[LayerId]) -> Option { + if self.document.layer(path).ok()?.overlay { + return None; + } self.layer_data(path).selected = true; + let data = self.layer_panel_entry(path.to_vec()).ok()?; // TODO: Add deduplication - (!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten() + (!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), data }.into()) + } + pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> { + let paths = self.selected_layers().map(|vec| &vec[..]); + self.document.combined_viewport_bounding_box(paths) } pub fn layerdata(&self, path: &[LayerId]) -> &LayerData { self.layer_data.get(path).expect("Layerdata does not exist") @@ -247,6 +257,7 @@ impl DocumentMessageHandler { .iter() .zip(paths.iter().zip(data)) .rev() + .filter(|(layer, _)| !layer.overlay) .map(|(layer, (path, data))| { layer_panel_entry( &data, @@ -332,34 +343,41 @@ impl MessageHandler for DocumentMessageHand self.layer_data(&path).expanded ^= true; responses.extend(self.handle_folder_changed(path)); } + SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()), DeleteSelectedLayers => { for path in self.selected_layers().cloned() { responses.push_back(DocumentOperation::DeleteLayer { path }.into()) } + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } DuplicateSelectedLayers => { for path in self.selected_layers_sorted() { responses.push_back(DocumentOperation::DuplicateLayer { path }.into()) } } - SelectLayers(paths) => { + SetSelectedLayers(paths) => { self.clear_selection(); + responses.push_front(AddSelectedLayers(paths).into()); + } + AddSelectedLayers(paths) => { for path in paths { responses.extend(self.select_layer(&path)); } // TODO: Correctly update layer panel in clear_selection instead of here responses.extend(self.handle_folder_changed(Vec::new())); + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } SelectAllLayers => { - let all_layer_paths = self.layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::>(); - for path in all_layer_paths { - responses.extend(self.select_layer(&path)); - } + let all_layer_paths = self + .layer_data + .keys() + .filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay) + .cloned() + .collect::>(); + responses.push_back(SetSelectedLayers(all_layer_paths).into()); } DeselectAllLayers => { - self.clear_selection(); - let children = self.layer_panel(&[]).expect("The provided Path was not valid"); - responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into()); + responses.push_back(SetSelectedLayers(vec![]).into()); } Undo => { // this is a temporary fix and will be addressed by #123 @@ -378,7 +396,8 @@ impl MessageHandler for DocumentMessageHand DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path), DocumentResponse::DeletedLayer { path } => { self.layer_data.remove(&path); - None + + Some(SelectMessage::UpdateSelectionBoundingBox.into()) } DocumentResponse::LayerChanged { path } => Some( FrontendMessage::UpdateLayer { @@ -387,7 +406,7 @@ impl MessageHandler for DocumentMessageHand } .into(), ), - DocumentResponse::CreatedLayer { path } => self.select_layer(&path), + DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()), DocumentResponse::DocumentChanged => unreachable!(), }) .flatten(), @@ -434,6 +453,7 @@ impl MessageHandler for DocumentMessageHand }; responses.push_back(operation.into()); } + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } MoveSelectedLayersTo { path, insert_index } => { responses.push_back(DocumentsMessage::CopySelectedLayers.into()); @@ -488,6 +508,7 @@ impl MessageHandler for DocumentMessageHand .into(), ); } + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } } AlignSelectedLayers(axis, aggregate) => { @@ -520,6 +541,7 @@ impl MessageHandler for DocumentMessageHand .into(), ); } + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } } RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), diff --git a/editor/src/document/movement_handler.rs b/editor/src/document/movement_handler.rs index 79ac524a1f..5f892679eb 100644 --- a/editor/src/document/movement_handler.rs +++ b/editor/src/document/movement_handler.rs @@ -105,6 +105,7 @@ impl MessageHandler { if let Some([pos1, pos2]) = document.visible_layers_bounding_box() { diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 9c94f9b439..be9cd256bf 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -131,7 +131,7 @@ impl Default for Mapping { entry! {action=MovementMessage::DisableSnapping, key_up=KeyShift}, // Select entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove}, - entry! {action=SelectMessage::DragStart, key_down=Lmb}, + entry! {action=SelectMessage::DragStart{add_to_selection: KeyShift}, key_down=Lmb}, entry! {action=SelectMessage::DragStop, key_up=Lmb}, entry! {action=SelectMessage::Abort, key_down=Rmb}, entry! {action=SelectMessage::Abort, key_down=KeyEscape}, diff --git a/editor/src/input/keyboard.rs b/editor/src/input/keyboard.rs index bca81d3fff..704bbf7938 100644 --- a/editor/src/input/keyboard.rs +++ b/editor/src/input/keyboard.rs @@ -1,4 +1,5 @@ use crate::message_prelude::*; +use serde::{Deserialize, Serialize}; pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize; // Edit this to specify the storage type used @@ -12,7 +13,7 @@ const KEY_MASK_STORAGE_LENGTH: usize = (NUMBER_OF_KEYS + STORAGE_SIZE_BITS - 1) pub type KeyStates = BitVector; #[impl_message(Message, InputMapperMessage, KeyDown)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Key { UnknownKey, // MouseKeys diff --git a/editor/src/tool/tool_message_handler.rs b/editor/src/tool/tool_message_handler.rs index ed90623c11..5ebbcbdcd3 100644 --- a/editor/src/tool/tool_message_handler.rs +++ b/editor/src/tool/tool_message_handler.rs @@ -16,6 +16,7 @@ pub enum ToolMessage { SelectSecondaryColor(Color), SwapColors, ResetColors, + NoOp, SetToolOptions(ToolType, ToolOptions), #[child] Fill(FillMessage), @@ -59,16 +60,27 @@ impl MessageHandler update_working_colors(&self.tool_state.document_tool_data, responses); } SelectTool(tool) => { - let mut reset = |tool| match tool { - ToolType::Ellipse => responses.push_back(EllipseMessage::Abort.into()), - ToolType::Rectangle => responses.push_back(RectangleMessage::Abort.into()), - ToolType::Shape => responses.push_back(ShapeMessage::Abort.into()), - ToolType::Line => responses.push_back(LineMessage::Abort.into()), - ToolType::Pen => responses.push_back(PenMessage::Abort.into()), - _ => (), + let old_tool = self.tool_state.tool_data.active_tool_type; + let reset = |tool| match tool { + ToolType::Ellipse => EllipseMessage::Abort.into(), + ToolType::Rectangle => RectangleMessage::Abort.into(), + ToolType::Shape => ShapeMessage::Abort.into(), + ToolType::Line => LineMessage::Abort.into(), + ToolType::Pen => PenMessage::Abort.into(), + ToolType::Select => SelectMessage::Abort.into(), + _ => ToolMessage::NoOp, }; - reset(tool); - reset(self.tool_state.tool_data.active_tool_type); + let (new, old) = (reset(tool), reset(old_tool)); + let mut send_to_tool = |tool_type, message: ToolMessage| { + if let Some(tool) = self.tool_state.tool_data.tools.get_mut(&tool_type) { + tool.process_action(message, (document, &self.tool_state.document_tool_data, input), responses); + } + }; + send_to_tool(tool, new); + send_to_tool(old_tool, old); + if tool == ToolType::Select { + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + } self.tool_state.tool_data.active_tool_type = tool; responses.push_back(FrontendMessage::SetActiveTool { tool_name: tool.to_string() }.into()) @@ -88,22 +100,11 @@ impl MessageHandler self.tool_state.document_tool_data.tool_options.insert(tool_type, tool_options); } message => { - let tool_type = match message { - Fill(_) => ToolType::Fill, - Rectangle(_) => ToolType::Rectangle, - Ellipse(_) => ToolType::Ellipse, - Shape(_) => ToolType::Shape, - Line(_) => ToolType::Line, - Pen(_) => ToolType::Pen, - Select(_) => ToolType::Select, - Crop(_) => ToolType::Crop, - Eyedropper(_) => ToolType::Eyedropper, - Navigate(_) => ToolType::Navigate, - Path(_) => ToolType::Path, - _ => unreachable!(), - }; + let tool_type = message_to_tool_type(&message); if let Some(tool) = self.tool_state.tool_data.tools.get_mut(&tool_type) { - tool.process_action(message, (document, &self.tool_state.document_tool_data, input), responses); + if tool_type == self.tool_state.tool_data.active_tool_type { + tool.process_action(message, (document, &self.tool_state.document_tool_data, input), responses); + } } } } @@ -115,6 +116,24 @@ impl MessageHandler } } +fn message_to_tool_type(message: &ToolMessage) -> ToolType { + use ToolMessage::*; + match message { + Fill(_) => ToolType::Fill, + Rectangle(_) => ToolType::Rectangle, + Ellipse(_) => ToolType::Ellipse, + Shape(_) => ToolType::Shape, + Line(_) => ToolType::Line, + Pen(_) => ToolType::Pen, + Select(_) => ToolType::Select, + Crop(_) => ToolType::Crop, + Eyedropper(_) => ToolType::Eyedropper, + Navigate(_) => ToolType::Navigate, + Path(_) => ToolType::Path, + _ => unreachable!(), + } +} + fn update_working_colors(doc_data: &DocumentToolData, responses: &mut VecDeque) { responses.push_back( FrontendMessage::UpdateWorkingColors { diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index 2fcb36c9b7..f2a34a7ab3 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -8,6 +8,7 @@ use graphene::Quad; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; +use crate::input::keyboard::Key; use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{ @@ -25,10 +26,11 @@ pub struct Select { #[impl_message(Message, ToolMessage, Select)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] pub enum SelectMessage { - DragStart, + DragStart { add_to_selection: Key }, DragStop, MouseMove, Abort, + UpdateSelectionBoundingBox, Align(AlignAxis, AlignAggregate), FlipHorizontal, @@ -67,7 +69,8 @@ struct SelectToolData { drag_start: ViewportPosition, drag_current: ViewportPosition, layers_dragging: Vec>, // Paths and offsets - box_id: Option>, + drag_box_id: Option>, + bounding_box_id: Option>, } impl SelectToolData { @@ -86,6 +89,24 @@ impl SelectToolData { } } +fn add_boundnig_box(responses: &mut VecDeque) -> Vec { + let path = vec![generate_uuid()]; + responses.push_back( + Operation::AddBoundingBox { + path: path.clone(), + transform: DAffine2::ZERO.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())), + } + .into(), + ); + + path +} + +fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { + DAffine2::from_scale_angle_translation(pos2 - pos1, 0., pos1).to_cols_array() +} + impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; @@ -102,7 +123,21 @@ impl Fsm for SelectToolFsmState { use SelectToolFsmState::*; if let ToolMessage::Select(event) = event { match (self, event) { - (Ready, DragStart) => { + (_, UpdateSelectionBoundingBox) => { + let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) { + (None, Some(path)) => Operation::DeleteLayer { path }.into(), + (Some([pos1, pos2]), path) => { + let path = path.unwrap_or_else(|| add_boundnig_box(responses)); + data.bounding_box_id = Some(path.clone()); + let transform = transform_from_box(pos1, pos2); + Operation::SetLayerTransformInViewport { path, transform }.into() + } + (_, _) => Message::NoOp, + }; + responses.push_back(response); + self + } + (Ready, DragStart { add_to_selection }) => { data.drag_start = input.mouse.position; data.drag_current = input.mouse.position; let mut selected: Vec<_> = document.selected_layers().cloned().collect(); @@ -112,7 +147,7 @@ impl Fsm for SelectToolFsmState { if selected.is_empty() { if let Some(layer) = intersection.last() { selected.push(layer.clone()); - responses.push_back(DocumentMessage::SelectLayers(selected.clone()).into()); + responses.push_back(DocumentMessage::SetSelectedLayers(selected.clone()).into()); } } // If the user clicks on a layer that is in their current selection, go into the dragging mode. @@ -121,16 +156,10 @@ impl Fsm for SelectToolFsmState { data.layers_dragging = selected; Dragging } else { - responses.push_back(DocumentMessage::DeselectAllLayers.into()); - data.box_id = Some(vec![generate_uuid()]); - responses.push_back( - Operation::AddBoundingBox { - path: data.box_id.clone().unwrap(), - transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())), - } - .into(), - ); + if !input.keyboard.get(add_to_selection as usize) { + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + } + data.drag_box_id = Some(add_boundnig_box(responses)); DrawingBox } } @@ -144,17 +173,19 @@ impl Fsm for SelectToolFsmState { .into(), ); } + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); data.drag_current = input.mouse.position; Dragging } (DrawingBox, MouseMove) => { data.drag_current = input.mouse.position; - let start = data.drag_start; - let size = data.drag_current - start; + let half_pixel_offset = DVec2::new(0.5, 0.5); + let start = data.drag_start + half_pixel_offset; + let size = data.drag_current - start + half_pixel_offset; responses.push_back( Operation::SetLayerTransformInViewport { - path: data.box_id.clone().unwrap(), + path: data.drag_box_id.clone().unwrap(), transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(), } .into(), @@ -162,14 +193,22 @@ impl Fsm for SelectToolFsmState { DrawingBox } (Dragging, DragStop) => Ready, - (DrawingBox, Abort) => { - responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into()); - Ready - } (DrawingBox, DragStop) => { let quad = data.selection_quad(); - responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into()); - responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into()); + responses.push_back(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into()); + responses.push_back( + Operation::DeleteLayer { + path: data.drag_box_id.take().unwrap(), + } + .into(), + ); + data.drag_box_id = None; + Ready + } + (_, Abort) => { + let mut delete = |path: &mut Option>| path.take().map(|path| responses.push_back(Operation::DeleteLayer { path }.into())); + delete(&mut data.drag_box_id); + delete(&mut data.bounding_box_id); Ready } (_, Align(axis, aggregate)) => { diff --git a/frontend/wasm/src/document.rs b/frontend/wasm/src/document.rs index eba56529e6..62e98b6008 100644 --- a/frontend/wasm/src/document.rs +++ b/frontend/wasm/src/document.rs @@ -109,7 +109,7 @@ pub fn close_all_documents_with_confirmation() -> Result<(), JsValue> { #[wasm_bindgen] pub fn bounds_of_viewports(bounds_of_viewports: &[f64]) -> Result<(), JsValue> { let chunked: Vec<_> = bounds_of_viewports.chunks(4).map(ViewportBounds::from_slice).collect(); - let ev = InputPreprocessorMessage::BoundsOfViewports((chunked).into()); + let ev = InputPreprocessorMessage::BoundsOfViewports(chunked); EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) } @@ -324,7 +324,7 @@ pub fn translate_canvas_by_fraction(delta_x: f64, delta_y: f64) -> Result<(), Js pub fn select_layers(paths: Vec) -> Result<(), JsValue> { let paths = paths.split(|id| *id == LayerId::MAX).map(|path| path.to_vec()).collect(); EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectLayers(paths))) + .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SetSelectedLayers(paths))) .map_err(convert_error) } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index de93fc3749..582ebad51c 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -33,7 +33,7 @@ fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> } impl Document { - pub fn with_content(serialized_content: &String) -> Result { + pub fn with_content(serialized_content: &str) -> Result { serde_json::from_str(serialized_content).map_err(|e| DocumentError::InvalidFile(e.to_string())) } @@ -289,8 +289,10 @@ impl Document { Operation::AddBoundingBox { path, transform, style } => { let mut rect = Shape::rectangle(*style); rect.render_index = -1; - self.set_layer(path, Layer::new(LayerDataType::Shape(rect), *transform), -1)?; - Some(vec![DocumentResponse::DocumentChanged]) + let mut layer = Layer::new(LayerDataType::Shape(rect), *transform); + layer.overlay = true; + self.set_layer(path, layer, -1)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } Operation::AddShape { path, diff --git a/graphene/src/layers/mod.rs b/graphene/src/layers/mod.rs index a154240483..88e89d219e 100644 --- a/graphene/src/layers/mod.rs +++ b/graphene/src/layers/mod.rs @@ -83,6 +83,7 @@ pub struct Layer { pub cache_dirty: bool, pub blend_mode: BlendMode, pub opacity: f64, + pub overlay: bool, } impl Layer { @@ -97,6 +98,7 @@ impl Layer { cache_dirty: true, blend_mode: BlendMode::Normal, opacity: 1., + overlay: false, } } @@ -128,7 +130,7 @@ impl Layer { } pub fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { - if !self.visible { + if !self.visible || self.overlay { return; } let transformed_quad = self.transform.inverse() * quad; @@ -166,6 +168,7 @@ impl Clone for Layer { cache_dirty: true, blend_mode: self.blend_mode, opacity: self.opacity, + overlay: self.overlay, } } }