Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Add a "Reverse" command to the "Order" menu
Add a "Reverse" command to the bottom of this menu:

![Image](https://github.com/user-attachments/assets/9220a770-eb83-4273-bc00-cbd852abca39)

- If multiple noncontiguous ranges are present with the same direct parent (they're siblings in the nesting hierarchy), all the selected items should be swapped as part of the same list: `[1], [2], 3, 4, [5], [6]` should become `[6], [5], 3, 4, [2], [1]` (where `[]` indicates selection).
- If any selected layer has a selected ancestor, only the ancestor is considered selected and all children/descendants are ignored as part of the selection.
- If there are separate sets of selected sibling layers, and they aren't descendants of any selected layer, each set gets reversed independently as part of its own sibling set.
- Any selected layer without at least one other selected sibling is ignored.

So in summary, reversal happens for each set of selected siblings, but not those with selected ancestors.


Discussion / hints:

Fix patch:
diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs
index 315702db8b..8ae84271ea 100644
--- a/editor/src/messages/portfolio/document/document_message.rs
+++ b/editor/src/messages/portfolio/document/document_message.rs
@@ -114,6 +114,7 @@ pub enum DocumentMessage {
 	SelectedLayersLowerToBack,
 	SelectedLayersRaise,
 	SelectedLayersRaiseToFront,
+	SelectedLayersReverse,
 	SelectedLayersReorder {
 		relative_index_offset: isize,
 	},
diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs
index 57f9576151..efcb5201e6 100644
--- a/editor/src/messages/portfolio/document/document_message_handler.rs
+++ b/editor/src/messages/portfolio/document/document_message_handler.rs
@@ -948,6 +948,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
 			DocumentMessage::SelectedLayersRaiseToFront => {
 				responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN });
 			}
+			DocumentMessage::SelectedLayersReverse => {
+				self.selected_layers_reverse(responses);
+			}
 			DocumentMessage::SelectedLayersReorder { relative_index_offset } => {
 				self.selected_layers_reorder(relative_index_offset, responses);
 			}
@@ -2149,6 +2152,98 @@ impl DocumentMessageHandler {
 		});
 	}
 
+	pub fn selected_layers_reverse(&mut self, responses: &mut VecDeque<Message>) {
+		let selected_layers = self.network_interface.selected_nodes(&[]).unwrap();
+		let metadata = self.metadata();
+		let selected_layer_set = selected_layers.selected_layers(metadata).collect::<HashSet<_>>();
+
+		// Ignore those with selected ancestors
+		let mut top_level_layers = Vec::new();
+		for &layer in &selected_layer_set {
+			let mut is_top_level = true;
+			let mut current_layer = layer;
+
+			while let Some(parent) = current_layer.parent(metadata) {
+				if selected_layer_set.contains(&parent) {
+					is_top_level = false;
+					break;
+				}
+				current_layer = parent;
+			}
+
+			if is_top_level {
+				top_level_layers.push(layer);
+			}
+		}
+
+		// Group selected layers by their parent
+		let mut grouped_layers: HashMap<LayerNodeIdentifier, Vec<(usize, LayerNodeIdentifier)>> = HashMap::new();
+		for &layer in &top_level_layers {
+			if let Some(parent) = layer.parent(metadata) {
+				let index = parent.children(metadata).position(|child| child == layer).unwrap_or(usize::MAX);
+
+				grouped_layers.entry(parent).or_default().push((index, layer));
+			}
+		}
+
+		let mut modified = false;
+
+		// Process each group separately
+		for (parent, mut layers) in grouped_layers {
+			// Retrieve all children under the parent
+			let all_children = parent.children(metadata).collect::<Vec<_>>();
+
+			// Separate unselected layers with their original indices
+			let unselected_layers = all_children
+				.iter()
+				.enumerate()
+				.filter_map(|(index, &layer)| if !selected_layer_set.contains(&layer) { Some((index, layer)) } else { None })
+				.collect::<Vec<_>>();
+
+			layers.sort_by_key(|(index, _)| *index);
+
+			let reversed_layers = layers.iter().rev().map(|(_, layer)| *layer).collect::<Vec<_>>();
+			let selected_positions = layers.iter().map(|(index, _)| *index).collect::<Vec<_>>();
+			let selected_iter = reversed_layers.into_iter();
+			let mut merged_layers = vec![None; all_children.len()];
+
+			for (&original_index, new_layer) in selected_positions.iter().zip(selected_iter) {
+				merged_layers[original_index] = Some(new_layer);
+			}
+
+			// Place unselected layers at their original positions
+			for (index, layer) in unselected_layers {
+				if merged_layers[index].is_none() {
+					merged_layers[index] = Some(layer);
+				}
+			}
+
+			let final_layers = merged_layers.into_iter().flatten().collect::<Vec<_>>();
+			if final_layers.is_empty() {
+				continue;
+			}
+
+			if !modified {
+				responses.add(DocumentMessage::AddTransaction);
+			}
+
+			for (index, layer) in final_layers.iter().enumerate() {
+				responses.add(NodeGraphMessage::MoveLayerToStack {
+					layer: *layer,
+					parent,
+					insert_index: index,
+				});
+			}
+
+			modified = true;
+		}
+
+		if modified {
+			responses.add(NodeGraphMessage::RunDocumentGraph);
+			responses.add(NodeGraphMessage::SendGraph);
+		}
+	}
+
 	pub fn selected_layers_reorder(&mut self, relative_index_offset: isize, responses: &mut VecDeque<Message>) {
 		let selected_nodes = self.network_interface.selected_nodes(&[]).unwrap();
 		let mut selected_layers = selected_nodes.selected_layers(self.metadata());
diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
index fa241fd3aa..9ae11ee393 100644
--- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
+++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs
@@ -346,8 +346,7 @@ impl LayoutHolder for MenuBarMessageHandler {
 							vec![MenuBarEntry {
 								label: "Reverse".into(),
 								icon: Some("StackReverse".into()),
-								// shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersReverse),
-								action: MenuBarEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: Some(2271) }.into()),
+								action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersReverse.into()),
 								disabled: no_active_document || !has_selected_layers,
 								..MenuBarEntry::default()
 							}],


Test patch: