From 8117ddcdb3f9ded70da8e36578ecf19afb866884 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 25 Feb 2026 00:48:21 -0800 Subject: [PATCH] Add Gradient tool control bar buttons, Reverse Stops and Reverse Direction (#3830) * Add Gradient tool control bar buttons, Reverse Stops and Reverse Direction * Consolidate reused gradient line updating code --- .../tool/tool_messages/gradient_tool.rs | 169 +++++++++++++----- 1 file changed, 126 insertions(+), 43 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 209295ab..3b47651a 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -44,6 +44,8 @@ pub enum GradientToolMessage { #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum GradientOptionsUpdate { Type(GradientType), + ReverseStops, + ReverseDirection, } impl ToolMetadata for GradientTool { @@ -63,52 +65,27 @@ impl<'a> MessageHandler> for Grad fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { let ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) = message else { self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false); + + let has_gradient = has_gradient_on_selected_layers(context.document); + if has_gradient != self.data.has_selected_gradient { + self.data.has_selected_gradient = has_gradient; + responses.add(ToolMessage::RefreshToolOptions); + } + return; }; match options { GradientOptionsUpdate::Type(gradient_type) => { self.options.gradient_type = gradient_type; - let selected_layers: Vec<_> = context - .document - .network_interface - .selected_nodes() - .selected_visible_layers(&context.document.network_interface) - .collect(); - - let mut transaction_started = false; - for layer in selected_layers { - if NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) { - continue; - } - - if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) - && gradient.gradient_type != gradient_type - { - if !transaction_started { - responses.add(DocumentMessage::StartTransaction); - transaction_started = true; - } - gradient.gradient_type = gradient_type; - responses.add(GraphOperationMessage::FillSet { - layer, - fill: Fill::Gradient(gradient), - }); - } - } - - if transaction_started { - responses.add(DocumentMessage::AddTransaction); - } - if let Some(selected_gradient) = &mut self.data.selected_gradient - && let Some(layer) = selected_gradient.layer - && !NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) - { - selected_gradient.gradient.gradient_type = gradient_type; - } + apply_gradient_update(&mut self.data, context, responses, |g| g.gradient_type != gradient_type, |g| g.gradient_type = gradient_type); responses.add(ToolMessage::UpdateHints); - responses.add(PropertiesPanelMessage::Refresh); responses.add(ToolMessage::UpdateCursor); - responses.add(ToolMessage::RefreshToolOptions); + } + GradientOptionsUpdate::ReverseStops => { + apply_gradient_update(&mut self.data, context, responses, |_| true, |g| g.stops = g.stops.reversed()); + } + GradientOptionsUpdate::ReverseDirection => { + apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end)); } } } @@ -142,7 +119,52 @@ impl LayoutHolder for GradientTool { .selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32)) .widget_instance(); - Layout(vec![LayoutGroup::Row { widgets: vec![gradient_type] }]) + let reverse_stops = IconButton::new("Reverse", 24) + .tooltip_label("Reverse Stops") + .tooltip_description("Reverse the gradient color stops.") + .disabled(!self.data.has_selected_gradient) + .on_update(|_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::ReverseStops, + } + .into() + }) + .widget_instance(); + + let mut widgets = vec![gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]; + + if self.options.gradient_type == GradientType::Radial { + let orientation = self + .data + .selected_gradient + .as_ref() + .map(|selected_gradient| { + let (start, end) = (selected_gradient.gradient.start, selected_gradient.gradient.end); + if (end.x - start.x).abs() > f64::EPSILON * 1e6 { + end.x > start.x + } else { + (start.x + start.y) < (end.x + end.y) + } + }) + .unwrap_or(true); + + let reverse_direction = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) + .tooltip_label("Reverse Direction") + .tooltip_description("Reverse which end the gradient radiates from.") + .disabled(!self.data.has_selected_gradient) + .on_update(|_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::ReverseDirection, + } + .into() + }) + .widget_instance(); + + widgets.push(Separator::new(SeparatorStyle::Related).widget_instance()); + widgets.push(reverse_direction); + } + + Layout(vec![LayoutGroup::Row { widgets }]) } } @@ -492,6 +514,7 @@ struct GradientToolData { auto_panning: AutoPanning, auto_pan_shift: DVec2, gradient_angle: f64, + has_selected_gradient: bool, } impl Fsm for GradientToolFsmState { @@ -993,9 +1016,12 @@ impl Fsm for GradientToolFsmState { } // Initialize `gradient_angle` from the existing gradient so Ctrl (lock angle) works from the first mouse move - if let Some(sg) = &tool_data.selected_gradient { - let (vp_start, vp_end) = (sg.transform.transform_point2(sg.gradient.start), sg.transform.transform_point2(sg.gradient.end)); - let delta = match sg.dragging { + if let Some(selected_gradient) = &tool_data.selected_gradient { + let (vp_start, vp_end) = ( + selected_gradient.transform.transform_point2(selected_gradient.gradient.start), + selected_gradient.transform.transform_point2(selected_gradient.gradient.end), + ); + let delta = match selected_gradient.dragging { // When dragging End, the fixed point is start and the mouse begins at end GradientDragTarget::End => vp_start - vp_end, // When dragging Start, the fixed point is end and the mouse begins at start @@ -1318,6 +1344,63 @@ fn compute_selected_target(tool_data: &GradientToolData) -> GradientSelectedTarg } } +fn apply_gradient_update( + data: &mut GradientToolData, + context: &mut ToolActionMessageContext, + responses: &mut VecDeque, + condition: impl Fn(&Gradient) -> bool, + update: impl Fn(&mut Gradient), +) { + let selected_layers: Vec<_> = context + .document + .network_interface + .selected_nodes() + .selected_visible_layers(&context.document.network_interface) + .collect(); + + let mut transaction_started = false; + for layer in selected_layers { + if NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) { + continue; + } + + if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) + && condition(&gradient) + { + if !transaction_started { + responses.add(DocumentMessage::StartTransaction); + transaction_started = true; + } + update(&mut gradient); + responses.add(GraphOperationMessage::FillSet { + layer, + fill: Fill::Gradient(gradient), + }); + } + } + + if transaction_started { + responses.add(DocumentMessage::AddTransaction); + } + if let Some(selected_gradient) = &mut data.selected_gradient + && let Some(layer) = selected_gradient.layer + && !NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) + { + update(&mut selected_gradient.gradient); + } + responses.add(PropertiesPanelMessage::Refresh); + data.has_selected_gradient = has_gradient_on_selected_layers(context.document); + responses.add(ToolMessage::RefreshToolOptions); +} + +fn has_gradient_on_selected_layers(document: &DocumentMessageHandler) -> bool { + document + .network_interface + .selected_nodes() + .selected_visible_layers(&document.network_interface) + .any(|layer| get_gradient(layer, &document.network_interface).is_some()) +} + #[inline(always)] fn midpoint_is_resettable(value: f64) -> bool { (value - 0.5).abs() >= f64::EPSILON * 1000.