diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index 8d7adeb73a..439207683d 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -354,6 +354,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::X, aggregate: AlignAggregate::Min, + align_to_artboard: false, } .into() }) @@ -365,6 +366,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::X, aggregate: AlignAggregate::Center, + align_to_artboard: false, } .into() }) @@ -376,6 +378,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::X, aggregate: AlignAggregate::Max, + align_to_artboard: false, } .into() }) @@ -389,6 +392,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::Y, aggregate: AlignAggregate::Min, + align_to_artboard: false, } .into() }) @@ -400,6 +404,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::Y, aggregate: AlignAggregate::Center, + align_to_artboard: false, } .into() }) @@ -411,6 +416,7 @@ impl LayoutHolder for MenuBarMessageHandler { DocumentMessage::AlignSelectedLayers { axis: AlignAxis::Y, aggregate: AlignAggregate::Max, + align_to_artboard: false, } .into() }) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index babe195b5f..cc38744d68 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -42,6 +42,7 @@ pub enum DocumentMessage { AlignSelectedLayers { axis: AlignAxis, aggregate: AlignAggregate, + align_to_artboard: bool, }, RemoveArtboards, ClearLayersPanel, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 68b79a8d7f..ec41bc0bfd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -273,12 +273,17 @@ impl MessageHandler> for DocumentMes let mut graph_operation_message_handler = GraphOperationMessageHandler {}; graph_operation_message_handler.process_message(message, responses, context); } - DocumentMessage::AlignSelectedLayers { axis, aggregate } => { + DocumentMessage::AlignSelectedLayers { axis, aggregate, align_to_artboard } => { let axis = match axis { AlignAxis::X => DVec2::X, AlignAxis::Y => DVec2::Y, }; - let Some(combined_box) = self.network_interface.selected_layers_artwork_bounding_box_viewport() else { + let combined_box = if align_to_artboard { + self.selected_layers_shared_artboard_bounding_box_viewport() + } else { + self.network_interface.selected_layers_artwork_bounding_box_viewport() + }; + let Some(combined_box) = combined_box else { return; }; @@ -1513,11 +1518,38 @@ impl MessageHandler> for DocumentMes } impl DocumentMessageHandler { +<<<<<<< HEAD /// Translates a viewport mouse position to a document-space transform, or uses the viewport center if no mouse position is given. fn document_transform_from_mouse(&self, mouse: Option<(f64, f64)>, viewport: &ViewportMessageHandler) -> DAffine2 { let viewport_pos: DVec2 = mouse.map_or_else(|| viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into()); let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_pos - viewport.offset().into_dvec2())) +======= + fn selected_layers_shared_artboard_bounding_box_viewport(&self) -> Option<[DVec2; 2]> { + let selected_nodes = self.network_interface.selected_nodes(); + let mut selected_layers = selected_nodes.selected_unlocked_layers(&self.network_interface).peekable(); + selected_layers.peek()?; + + let mut shared_artboard = None; + + for layer in selected_layers { + let layer_artboard = if self.network_interface.is_artboard(&layer.to_node(), &[]) { + Some(layer) + } else { + layer + .ancestors(self.metadata()) + .find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.network_interface.is_artboard(&ancestor.to_node(), &[])) + }; + + match (shared_artboard, layer_artboard) { + (None, Some(artboard)) => shared_artboard = Some(artboard), + (Some(shared_artboard), Some(artboard)) if shared_artboard == artboard => {} + _ => return None, + } + } + + shared_artboard.and_then(|artboard| self.metadata().bounding_box_viewport(artboard)) +>>>>>>> 6525a73f6 (Add 'Align to Artboard' feature (issue #1720)) } /// Runs an intersection test with all layers and a viewport space quad diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index b8c6751565..326644b781 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -110,6 +110,9 @@ pub enum SelectToolMessage { SelectOptions { options: SelectOptionsUpdate, }, + SetAlignToArtboard { + align_to_artboard: bool, + }, SetPivot { position: ReferencePoint, }, @@ -159,7 +162,7 @@ impl SelectTool { .widget_instance() } - fn alignment_widgets(&self, disabled: bool) -> impl Iterator + use<> { + fn alignment_widgets(&self, disabled: bool, align_to_artboard: bool) -> impl Iterator + use<> { [AlignAxis::X, AlignAxis::Y] .into_iter() .flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)]) @@ -174,12 +177,18 @@ impl SelectTool { }; IconButton::new(icon, 24) .tooltip_label(label) - .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()) + .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate, align_to_artboard }.into()) .disabled(disabled) .widget_instance() }) } + fn should_refresh_align_to_artboard(&mut self) -> bool { + let align_to_artboard_changed = self.tool_data.align_to_artboard_changed; + self.tool_data.align_to_artboard_changed = false; + align_to_artboard_changed + } + fn flip_widgets(&self, disabled: bool) -> impl Iterator + use<> { [(FlipAxis::X, "FlipHorizontal", "Flip Horizontal"), (FlipAxis::Y, "FlipVertical", "Flip Vertical")] .into_iter() @@ -252,9 +261,33 @@ impl LayoutHolder for SelectTool { } // Align - let disabled = self.tool_data.selected_layers_count < 2; + let align_to_artboard = self.tool_data.align_to_artboard; + let disabled = self.tool_data.selected_layers_count == 0 || (!align_to_artboard && self.tool_data.selected_layers_count < 2); + let align_to_artboard_checkbox_id = CheckboxId::new(); widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); - widgets.extend(self.alignment_widgets(disabled)); + widgets.extend(self.alignment_widgets(disabled, align_to_artboard)); + widgets.push( + PopoverButton::new() + .icon("AlignVerticalCenter") + .tooltip_label("Alignment Options") + .tooltip_description("Change how alignment uses the selection bounds or artboard bounds.") + .popover_min_width(Some(190)) + .popover_layout(Layout(vec![LayoutGroup::row(vec![ + CheckboxInput::new(align_to_artboard) + .for_label(align_to_artboard_checkbox_id) + .tooltip_label("To Artboard") + .tooltip_description("Align selected layers to their shared artboard instead of the selection bounds.") + .on_update(|input: &CheckboxInput| SelectToolMessage::SetAlignToArtboard { align_to_artboard: input.checked }.into()) + .widget_instance(), + TextLabel::new("To Artboard") + .tooltip_label("To Artboard") + .tooltip_description("Align selected layers to their shared artboard instead of the selection bounds.") + .for_checkbox(align_to_artboard_checkbox_id) + .widget_instance(), + ])])) + .disabled(self.tool_data.selected_layers_count == 0) + .widget_instance(), + ); // Flip let disabled = self.tool_data.selected_layers_count == 0; @@ -312,7 +345,7 @@ impl<'a> MessageHandler> for Sele self.fsm_state.process_event(message, &mut self.tool_data, context, &(), responses, false); - if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_pivot { + if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.should_refresh_align_to_artboard() || self.tool_data.selected_layers_changed || redraw_reference_pivot { // Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool) self.send_layout(responses, LayoutTarget::ToolOptions); self.tool_data.selected_layers_changed = false; @@ -406,6 +439,8 @@ struct SelectToolData { snap_candidates: Vec, auto_panning: AutoPanning, drag_start_center: ViewportPosition, + align_to_artboard: bool, + align_to_artboard_changed: bool, } impl SelectToolData { @@ -611,6 +646,11 @@ impl Fsm for SelectToolFsmState { let ToolMessage::Select(event) = event else { return self }; match (self, event) { + (_, SelectToolMessage::SetAlignToArtboard { align_to_artboard }) => { + tool_data.align_to_artboard = align_to_artboard; + tool_data.align_to_artboard_changed = true; + self + } (_, SelectToolMessage::Overlays { context: mut overlay_context }) => { tool_data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context);