From f9e8f17f2e7a009dabbace58e6cbcb8568e75bab Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 29 Jan 2026 09:45:46 +0000 Subject: [PATCH 01/15] Gradient --- .../messages/input_mapper/input_mappings.rs | 1 - .../document/overlays/utility_types_native.rs | 21 ++++ .../document/overlays/utility_types_web.rs | 30 +++++ .../tool/tool_messages/gradient_tool.rs | 106 ++++++++++++++++-- 4 files changed, 145 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 44ce4eecf3..e11cd28c6f 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -178,7 +178,6 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(MouseLeft); action_dispatch=GradientToolMessage::PointerDown), entry!(PointerMove; refresh_keys=[Shift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: Shift }), entry!(KeyUp(MouseLeft); action_dispatch=GradientToolMessage::PointerUp), - entry!(DoubleClick(MouseButton::Left); action_dispatch=GradientToolMessage::InsertStop), entry!(KeyDown(Delete); action_dispatch=GradientToolMessage::DeleteStop), entry!(KeyDown(Backspace); action_dispatch=GradientToolMessage::DeleteStop), entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort), diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index d23f077592..1c21f612f0 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -273,6 +273,10 @@ impl OverlayContext { self.internal().manipulator_anchor(position, selected, color); } + pub fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.internal().gradient_handle(position, selected, color); + } + pub fn resize_handle(&mut self, position: DVec2, rotation: f64) { self.internal().resize_handle(position, rotation); } @@ -582,6 +586,23 @@ impl OverlayContextInternal { self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); } + fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + let transform = self.get_transform(); + let position = position.round() - DVec2::splat(0.5); + + let (radius_offset, stroke_width) = if selected { (1.0, 3.0) } else { (0.0, 1.0) }; + let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + radius_offset; + + let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); + + let circle = kurbo::Circle::new((position.x, position.y), radius); + self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); + + let black_circle = kurbo::Circle::new((position.x, position.y), radius + stroke_width / 2.0); + self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color("#000000"), None, &black_circle); + self.scene.stroke(&kurbo::Stroke::new(stroke_width), transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle); + } + fn resize_handle(&mut self, position: DVec2, rotation: f64) { let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]); self.quad(quad, None, Some(COLOR_OVERLAY_WHITE)); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index f3545a901e..fcd83bb685 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -468,6 +468,36 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } + pub fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.start_dpi_aware_transform(); + + let position = position.round() - DVec2::splat(0.5); + + let (radius_offset, stroke_width) = if selected { (1.0, 3.0) } else { (0.0, 1.0) }; + let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; + + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + + let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); + self.render_context.set_fill_style_str(fill); + self.render_context.fill(); + + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius + stroke_width / 2., 0., TAU).expect("Failed to draw the circle"); + self.render_context.set_line_width(1.0); + self.render_context.set_stroke_style_str("#000000"); + self.render_context.stroke(); + + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + self.render_context.set_line_width(stroke_width); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_WHITE); + self.render_context.stroke(); + + self.end_dpi_aware_transform(); + } + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { self.start_dpi_aware_transform(); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index fa5b3f38a4..469cb9d9ff 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD}; +use crate::consts::{COLOR_OVERLAY_BLUE, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD}; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; @@ -266,6 +266,7 @@ impl Fsm for GradientToolFsmState { match (self, event) { (_, GradientToolMessage::Overlays { context: mut overlay_context }) => { let selected = tool_data.selected_gradient.as_ref(); + let mouse = input.mouse.position; for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; @@ -274,19 +275,63 @@ impl Fsm for GradientToolFsmState { .filter(|selected| selected.layer.is_some_and(|selected_layer| selected_layer == layer)) .map(|selected| selected.dragging); + let gradient = if let Some(selected_gradient) = selected.filter(|s| s.layer == Some(layer)) { + &selected_gradient.gradient + } else { + &gradient + }; + let Gradient { start, end, stops, .. } = gradient; - let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); + let (start, end) = (transform.transform_point2(*start), transform.transform_point2(*end)); + + fn color_to_hex(color: graphene_std::Color) -> String { + if color.a() > 0.999 { + format!("#{:02X}{:02X}{:02X}", (color.r() * 255.) as u8, (color.g() * 255.) as u8, (color.b() * 255.) as u8) + } else { + color.to_rgba_hex_srgb() + } + } + + let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)); + let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)); overlay_context.line(start, end, None, None); - overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None); - overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None); + overlay_context.gradient_handle(start, dragging == Some(GradientDragTarget::Start), start_hex.as_deref()); + overlay_context.gradient_handle(end, dragging == Some(GradientDragTarget::End), end_hex.as_deref()); - for (index, (position, _)) in stops.into_iter().enumerate() { + for (index, (position, color)) in stops.clone().into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } + overlay_context.gradient_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), Some(&color_to_hex(color))); + } - overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), None); + // Use the selection threshold to check if the mouse is close enough to the gradient line to insert a new stop + let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); + let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + if distance.abs() < SELECTION_THRESHOLD * 3. && (0. ..=1.).contains(&projection) { + let mut near_stop = false; + // Check if the mouse is close to an existing stop + for (position, _) in stops { + let stop_pos = start.lerp(end, *position); + if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { + near_stop = true; + break; + } + } + // Check if close to start or end + if start.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) || end.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { + near_stop = true; + } + + if !near_stop { + if let Some(dir) = (end - start).try_normalize() { + let perp = dir.perp(); + let point = start.lerp(end, projection); + overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), Some(1.)); + } + } } } @@ -419,6 +464,27 @@ impl Fsm for GradientToolFsmState { }) } } + + // Insert stop if clicking on line + if !dragging { + let (start, end) = (transform.transform_point2(gradient.start), transform.transform_point2(gradient.end)); + let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); + let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + if distance.abs() < SELECTION_THRESHOLD && (0. ..=1.).contains(&projection) { + if let Some(index) = gradient.clone().insert_stop(mouse, transform) { + responses.add(DocumentMessage::AddTransaction); + let mut new_gradient = gradient.clone(); + new_gradient.insert_stop(mouse, transform); + + let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document); + selected_gradient.dragging = GradientDragTarget::Step(index); + selected_gradient.render_gradient(responses); + tool_data.selected_gradient = Some(selected_gradient); + dragging = true; + } + } + } } let gradient_state = if dragging { @@ -454,7 +520,9 @@ impl Fsm for GradientToolFsmState { GradientToolFsmState::Ready } }; - responses.add(DocumentMessage::StartTransaction); + if gradient_state == GradientToolFsmState::Drawing { + responses.add(DocumentMessage::StartTransaction); + } gradient_state } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { @@ -506,6 +574,11 @@ impl Fsm for GradientToolFsmState { GradientToolFsmState::Ready } + (GradientToolFsmState::Ready, GradientToolMessage::PointerMove { .. }) => { + responses.add(OverlaysMessage::Draw); + GradientToolFsmState::Ready + } + (GradientToolFsmState::Drawing, GradientToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); tool_data.snap_manager.cleanup(responses); @@ -693,7 +766,7 @@ mod test_gradient { } #[tokio::test] - async fn double_click_insert_stop() { + async fn single_click_insert_stop() { let mut editor = EditorTestUtils::create(); editor.new_document().await; @@ -707,7 +780,9 @@ mod test_gradient { assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len()); editor.select_tool(ToolType::Gradient).await; - editor.double_click(DVec2::new(50., 0.)).await; + editor.move_mouse(50., 0., ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(50., 0., ModifierKeys::empty()).await; + editor.left_mouseup(50., 0., ModifierKeys::empty()).await; // Check that a new stop has been added let (updated_gradient, _) = get_gradient(&mut editor).await; @@ -800,7 +875,9 @@ mod test_gradient { editor.select_tool(ToolType::Gradient).await; // Add a middle stop at 50% - editor.double_click(DVec2::new(50., 0.)).await; + editor.move_mouse(50., 0., ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(50., 0., ModifierKeys::empty()).await; + editor.left_mouseup(50., 0., ModifierKeys::empty()).await; let (initial_gradient, _) = get_gradient(&mut editor).await; assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len()); @@ -875,8 +952,13 @@ mod test_gradient { editor.select_tool(ToolType::Gradient).await; // Add two middle stops - editor.double_click(DVec2::new(25., 0.)).await; - editor.double_click(DVec2::new(75., 0.)).await; + editor.move_mouse(25., 0., ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(25., 0., ModifierKeys::empty()).await; + editor.left_mouseup(25., 0., ModifierKeys::empty()).await; + + editor.move_mouse(75., 0., ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(75., 0., ModifierKeys::empty()).await; + editor.left_mouseup(75., 0., ModifierKeys::empty()).await; let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len()); From fad2fdf60ef3610809543f360032ec1b63b5f477 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 29 Jan 2026 09:52:20 +0000 Subject: [PATCH 02/15] improvement --- .../document/overlays/utility_types_web.rs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index fcd83bb685..b582c5750c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -476,24 +476,25 @@ impl OverlayContext { let (radius_offset, stroke_width) = if selected { (1.0, 3.0) } else { (0.0, 1.0) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + { + let stroke_circle = |radius: f64, width: f64, color: &str| { + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + self.render_context.set_line_width(width); + self.render_context.set_stroke_style_str(color); + self.render_context.stroke(); + }; - let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); - self.render_context.set_fill_style_str(fill); - self.render_context.fill(); + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); + self.render_context.set_fill_style_str(fill); + self.render_context.fill(); - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius + stroke_width / 2., 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_line_width(1.0); - self.render_context.set_stroke_style_str("#000000"); - self.render_context.stroke(); + stroke_circle(radius + stroke_width / 2., 1.0, "#000000"); - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_line_width(stroke_width); - self.render_context.set_stroke_style_str(COLOR_OVERLAY_WHITE); - self.render_context.stroke(); + stroke_circle(radius, stroke_width, COLOR_OVERLAY_WHITE); + } self.end_dpi_aware_transform(); } From ad20a2a3e4c9d20121a08b975bb56160bfe1f5b8 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 29 Jan 2026 09:56:18 +0000 Subject: [PATCH 03/15] removed unnecessary code --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 469cb9d9ff..030b02a10f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -306,13 +306,11 @@ impl Fsm for GradientToolFsmState { overlay_context.gradient_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), Some(&color_to_hex(color))); } - // Use the selection threshold to check if the mouse is close enough to the gradient line to insert a new stop let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); if distance.abs() < SELECTION_THRESHOLD * 3. && (0. ..=1.).contains(&projection) { let mut near_stop = false; - // Check if the mouse is close to an existing stop for (position, _) in stops { let stop_pos = start.lerp(end, *position); if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { @@ -320,7 +318,6 @@ impl Fsm for GradientToolFsmState { break; } } - // Check if close to start or end if start.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) || end.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { near_stop = true; } From 58e9c60a5a5bbda52289fce460fb8e327fe218b0 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 29 Jan 2026 10:52:09 +0000 Subject: [PATCH 04/15] corrected error --- editor/src/test_utils.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 8c963e0b57..1a796407f8 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -210,6 +210,18 @@ impl EditorTestUtils { .await; } + pub async fn left_mouseup(&mut self, x: f64, y: f64, modifier_keys: ModifierKeys) { + self.mouseup( + EditorMouseState { + editor_position: (x, y).into(), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + modifier_keys, + ) + .await; + } + pub async fn input(&mut self, message: InputPreprocessorMessage) { self.handle_message(Message::InputPreprocessor(message)).await; } From 6cc10a917044f1ea142f32f98d0cafa19b7a6467 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 30 Jan 2026 20:55:58 +0000 Subject: [PATCH 05/15] Partical changes --- .../tool/tool_messages/gradient_tool.rs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 030b02a10f..948a33d1e6 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_BLUE, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD}; +use crate::consts::{COLOR_OVERLAY_BLUE, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD}; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; @@ -25,6 +25,7 @@ pub enum GradientToolMessage { // Standard messages Abort, Overlays { context: OverlayContext }, + SelectionChanged, // Tool-specific messages DeleteStop, @@ -228,6 +229,7 @@ impl ToolTransition for GradientTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(GradientToolMessage::Abort.into()), + selection_changed: Some(GradientToolMessage::SelectionChanged.into()), overlay_provider: Some(|context| GradientToolMessage::Overlays { context }.into()), ..Default::default() } @@ -309,7 +311,7 @@ impl Fsm for GradientToolFsmState { let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); - if distance.abs() < SELECTION_THRESHOLD * 3. && (0. ..=1.).contains(&projection) { + if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { let mut near_stop = false; for (position, _) in stops { let stop_pos = start.lerp(end, *position); @@ -334,6 +336,10 @@ impl Fsm for GradientToolFsmState { self } + (GradientToolFsmState::Ready, GradientToolMessage::SelectionChanged) => { + tool_data.selected_gradient = None; + self + } (GradientToolFsmState::Ready, GradientToolMessage::DeleteStop) => { let Some(selected_gradient) = &mut tool_data.selected_gradient else { return self; @@ -344,7 +350,7 @@ impl Fsm for GradientToolFsmState { return self; } - responses.add(DocumentMessage::AddTransaction); + responses.add(DocumentMessage::StartTransaction); // Remove the selected point match selected_gradient.dragging { @@ -367,6 +373,7 @@ impl Fsm for GradientToolFsmState { fill: Fill::Solid(selected_gradient.gradient.stops[0].1), }); } + responses.add(DocumentMessage::CommitTransaction); return self; } @@ -388,6 +395,7 @@ impl Fsm for GradientToolFsmState { // Render the new gradient selected_gradient.render_gradient(responses); + responses.add(DocumentMessage::CommitTransaction); self } @@ -406,7 +414,7 @@ impl Fsm for GradientToolFsmState { if distance < (SELECTION_THRESHOLD * 2.) { // Try and insert the new stop if let Some(index) = gradient.insert_stop(mouse, transform) { - responses.add(DocumentMessage::AddTransaction); + responses.add(DocumentMessage::StartTransaction); let mut selected_gradient = SelectedGradient::new(gradient, layer, document); @@ -417,7 +425,7 @@ impl Fsm for GradientToolFsmState { selected_gradient.render_gradient(responses); tool_data.selected_gradient = Some(selected_gradient); - + responses.add(DocumentMessage::CommitTransaction); break; } } @@ -431,6 +439,7 @@ impl Fsm for GradientToolFsmState { let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); let mut dragging = false; + let mut transaction_started = false; for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; let transform = gradient_space_transform(layer, document); @@ -468,9 +477,10 @@ impl Fsm for GradientToolFsmState { let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); - if distance.abs() < SELECTION_THRESHOLD && (0. ..=1.).contains(&projection) { + if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { if let Some(index) = gradient.clone().insert_stop(mouse, transform) { - responses.add(DocumentMessage::AddTransaction); + responses.add(DocumentMessage::StartTransaction); + transaction_started = true; let mut new_gradient = gradient.clone(); new_gradient.insert_stop(mouse, transform); @@ -517,9 +527,11 @@ impl Fsm for GradientToolFsmState { GradientToolFsmState::Ready } }; - if gradient_state == GradientToolFsmState::Drawing { + + if gradient_state == GradientToolFsmState::Drawing && !transaction_started { responses.add(DocumentMessage::StartTransaction); } + gradient_state } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { From 4f4815b68b9f792fa24e397d6f97ae48e6f50661 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 31 Jan 2026 08:11:07 +0000 Subject: [PATCH 06/15] Improveded --- .../document/document_message_handler.rs | 2 ++ .../messages/tool/tool_messages/gradient_tool.rs | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 330fe16abc..6514da4aaf 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -965,6 +965,7 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::DocumentHistoryForward); responses.add(ToolMessage::Redo); responses.add(OverlaysMessage::Draw); + responses.add(EventMessage::SelectionChanged); } DocumentMessage::RenameDocument { new_name } => { self.name = new_name.clone(); @@ -1432,6 +1433,7 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::DocumentHistoryBackward); responses.add(OverlaysMessage::Draw); responses.add(ToolMessage::Undo); + responses.add(EventMessage::SelectionChanged); } DocumentMessage::UngroupSelectedLayers => { if !self.selection_network_path.is_empty() { diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 948a33d1e6..f5077af935 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -144,6 +144,7 @@ struct SelectedGradient { transform: DAffine2, gradient: Gradient, dragging: GradientDragTarget, + offset: DVec2, } impl SelectedGradient { @@ -154,6 +155,7 @@ impl SelectedGradient { transform, gradient, dragging: GradientDragTarget::End, + offset: DVec2::ZERO, } } @@ -163,6 +165,7 @@ impl SelectedGradient { } pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool, gradient_type: GradientType) { + mouse += self.offset; self.gradient.gradient_type = gradient_type; if snap_rotate && matches!(self.dragging, GradientDragTarget::End | GradientDragTarget::Start) { @@ -266,7 +269,7 @@ impl Fsm for GradientToolFsmState { let ToolMessage::Gradient(event) = event else { return self }; match (self, event) { - (_, GradientToolMessage::Overlays { context: mut overlay_context }) => { + (_state, GradientToolMessage::Overlays { context: mut overlay_context }) => { let selected = tool_data.selected_gradient.as_ref(); let mouse = input.mouse.position; @@ -421,6 +424,8 @@ impl Fsm for GradientToolFsmState { // Select the new point selected_gradient.dragging = GradientDragTarget::Step(index); + selected_gradient.offset = DVec2::ZERO; + // Update the layer fill selected_gradient.render_gradient(responses); @@ -453,6 +458,7 @@ impl Fsm for GradientToolFsmState { transform, gradient: gradient.clone(), dragging: GradientDragTarget::Step(index), + offset: pos - mouse, }) } } @@ -467,6 +473,7 @@ impl Fsm for GradientToolFsmState { transform, gradient: gradient.clone(), dragging: dragging_target, + offset: pos - mouse, }) } } @@ -486,6 +493,8 @@ impl Fsm for GradientToolFsmState { let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document); selected_gradient.dragging = GradientDragTarget::Step(index); + // No offset when inserting a new stop, it should be exactly under the mouse + selected_gradient.offset = DVec2::ZERO; selected_gradient.render_gradient(responses); tool_data.selected_gradient = Some(selected_gradient); dragging = true; @@ -547,6 +556,8 @@ impl Fsm for GradientToolFsmState { ]; tool_data.auto_panning.setup_by_mouse_position(input, viewport, &messages, responses); + responses.add(OverlaysMessage::Draw); + GradientToolFsmState::Drawing } (GradientToolFsmState::Drawing, GradientToolMessage::PointerOutsideViewport { .. }) => { @@ -570,7 +581,7 @@ impl Fsm for GradientToolFsmState { state } (GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => { - input.mouse.finish_transaction(tool_data.drag_start, responses); + responses.add(DocumentMessage::EndTransaction); tool_data.snap_manager.cleanup(responses); let was_dragging = tool_data.selected_gradient.is_some(); From 192f4734fec066a0ec13146a393075208bedc6e9 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 31 Jan 2026 08:28:37 +0000 Subject: [PATCH 07/15] remove from advertised actions --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f5077af935..31ee3ed67c 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -84,7 +84,6 @@ impl<'a> MessageHandler> for Grad PointerUp, PointerMove, Abort, - InsertStop, DeleteStop, ); } From 33ea7d249ddd6cd3cbfcd21eaa15929c1d0c20f3 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 31 Jan 2026 09:24:39 +0000 Subject: [PATCH 08/15] Mousedown --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 31ee3ed67c..5d372886db 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -540,6 +540,8 @@ impl Fsm for GradientToolFsmState { responses.add(DocumentMessage::StartTransaction); } + responses.add(OverlaysMessage::Draw); + gradient_state } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { From 7593a747ac184d2ac27ae435468248259a197c3d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 31 Jan 2026 18:40:28 -0800 Subject: [PATCH 09/15] Partial code review --- editor/src/consts.rs | 1 + .../document/overlays/utility_types_native.rs | 36 +++++++++--------- .../document/overlays/utility_types_web.rs | 38 +++++++++---------- .../tool/tool_messages/gradient_tool.rs | 6 +-- 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index cd13946d0d..5c820ff3e6 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -153,6 +153,7 @@ pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_GRAY: &str = "#cccccc"; pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40"; pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; +pub const COLOR_OVERLAY_BLACK: &str = "#000000"; pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; // DOCUMENT diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 1c21f612f0..809d3452bf 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1,7 +1,7 @@ use crate::consts::{ - ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, - COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, - PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, + ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLACK, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, + COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, + MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, }; use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -273,8 +273,8 @@ impl OverlayContext { self.internal().manipulator_anchor(position, selected, color); } - pub fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { - self.internal().gradient_handle(position, selected, color); + pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.internal().gradient_color_stop(position, selected, color); } pub fn resize_handle(&mut self, position: DVec2, rotation: f64) { @@ -586,11 +586,11 @@ impl OverlayContextInternal { self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); } - fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); - let (radius_offset, stroke_width) = if selected { (1.0, 3.0) } else { (0.0, 1.0) }; + let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + radius_offset; let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); @@ -598,8 +598,8 @@ impl OverlayContextInternal { let circle = kurbo::Circle::new((position.x, position.y), radius); self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); - let black_circle = kurbo::Circle::new((position.x, position.y), radius + stroke_width / 2.0); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color("#000000"), None, &black_circle); + let black_circle = kurbo::Circle::new((position.x, position.y), radius + stroke_width / 2.); + self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(COLOR_OVERLAY_BLACK), None, &black_circle); self.scene.stroke(&kurbo::Stroke::new(stroke_width), transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle); } @@ -852,7 +852,7 @@ impl OverlayContextInternal { path.move_to(kurbo::Point::new(x, y)); path.line_to(kurbo::Point::new(start1_x, start1_y)); - let arc1 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start1, FRAC_PI_2, 0.0); + let arc1 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start1, FRAC_PI_2, 0.); arc1.to_cubic_beziers(0.1, |p1, p2, p| { path.curve_to(p1, p2, p); }); @@ -864,7 +864,7 @@ impl OverlayContextInternal { path.move_to(kurbo::Point::new(x, y)); path.line_to(kurbo::Point::new(start2_x, start2_y)); - let arc2 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start2, FRAC_PI_2, 0.0); + let arc2 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start2, FRAC_PI_2, 0.); arc2.to_cubic_beziers(0.1, |p1, p2, p| { path.curve_to(p1, p2, p); }); @@ -911,7 +911,7 @@ impl OverlayContextInternal { let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); - self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); + self.scene.stroke(&kurbo::Stroke::new(4.), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { @@ -919,7 +919,7 @@ impl OverlayContextInternal { let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); - self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); + self.scene.stroke(&kurbo::Stroke::new(4.), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); } fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) { @@ -1054,16 +1054,16 @@ impl OverlayContextInternal { fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { // Use the proper text-to-path system for accurate text rendering - const FONT_SIZE: f64 = 12.0; + const FONT_SIZE: f64 = 12.; // Create typesetting configuration let typesetting = TypesettingConfig { font_size: FONT_SIZE, line_height_ratio: 1.2, - character_spacing: 0.0, + character_spacing: 0., max_width: None, max_height: None, - tilt: 0.0, + tilt: 0., align: TextAlign::Left, // We'll handle alignment manually via pivot }; @@ -1078,7 +1078,7 @@ impl OverlayContextInternal { let text_width = text_size.x; let text_height = text_size.y; // Create a rect from the size (assuming text starts at origin) - let text_bounds = kurbo::Rect::new(0.0, 0.0, text_width, text_height); + let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height); // Convert text to vector paths for rendering let text_table = text_context.to_path(text, &font, &GLOBAL_FONT_CACHE, typesetting, false); @@ -1087,7 +1087,7 @@ impl OverlayContextInternal { let mut position = DVec2::ZERO; match pivot[0] { Pivot::Start => position.x = padding, - Pivot::Middle => position.x = -text_width / 2.0, + Pivot::Middle => position.x = -text_width / 2., Pivot::End => position.x = -padding - text_width, } match pivot[1] { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index b582c5750c..9ad817a491 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -1,8 +1,8 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ - ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, - COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, - PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, + ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLACK, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, + COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, + MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, }; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::Message; @@ -468,33 +468,31 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } - pub fn gradient_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { self.start_dpi_aware_transform(); let position = position.round() - DVec2::splat(0.5); - let (radius_offset, stroke_width) = if selected { (1.0, 3.0) } else { (0.0, 1.0) }; + let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - { - let stroke_circle = |radius: f64, width: f64, color: &str| { - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_line_width(width); - self.render_context.set_stroke_style_str(color); - self.render_context.stroke(); - }; - + let stroke_circle = |radius: f64, width: f64, color: &str| { self.render_context.begin_path(); self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); - self.render_context.set_fill_style_str(fill); - self.render_context.fill(); + self.render_context.set_line_width(width); + self.render_context.set_stroke_style_str(color); + self.render_context.stroke(); + }; - stroke_circle(radius + stroke_width / 2., 1.0, "#000000"); + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); + self.render_context.set_fill_style_str(fill); + self.render_context.fill(); - stroke_circle(radius, stroke_width, COLOR_OVERLAY_WHITE); - } + stroke_circle(radius + stroke_width / 2., 1., COLOR_OVERLAY_BLACK); + + stroke_circle(radius, stroke_width, COLOR_OVERLAY_WHITE); self.end_dpi_aware_transform(); } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 5d372886db..afb0f166b4 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -300,14 +300,14 @@ impl Fsm for GradientToolFsmState { let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)); overlay_context.line(start, end, None, None); - overlay_context.gradient_handle(start, dragging == Some(GradientDragTarget::Start), start_hex.as_deref()); - overlay_context.gradient_handle(end, dragging == Some(GradientDragTarget::End), end_hex.as_deref()); + overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), start_hex.as_deref()); + overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), end_hex.as_deref()); for (index, (position, color)) in stops.clone().into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.gradient_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), Some(&color_to_hex(color))); + overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), Some(&color_to_hex(color))); } let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); From 92f7c9a74599d509466a492d31149577d02af17d Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 04:12:28 +0000 Subject: [PATCH 10/15] changes as per recommendation --- .../document/overlays/utility_types_native.rs | 30 ++++++++++------- .../document/overlays/utility_types_web.rs | 32 ++++++++++--------- .../tool/tool_messages/gradient_tool.rs | 10 +++--- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 809d3452bf..0dfb08e2f7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -273,7 +273,7 @@ impl OverlayContext { self.internal().manipulator_anchor(position, selected, color); } - pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) { self.internal().gradient_color_stop(position, selected, color); } @@ -586,21 +586,27 @@ impl OverlayContextInternal { self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); } - fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; - let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + radius_offset; - - let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); - - let circle = kurbo::Circle::new((position.x, position.y), radius); - self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); - - let black_circle = kurbo::Circle::new((position.x, position.y), radius + stroke_width / 2.); - self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(COLOR_OVERLAY_BLACK), None, &black_circle); - self.scene.stroke(&kurbo::Stroke::new(stroke_width), transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle); + let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; + + let mut draw_circle = |radius: f64, width: Option, color: &str| { + let circle = kurbo::Circle::new((position.x, position.y), radius); + if let Some(width) = width { + self.scene.stroke(&kurbo::Stroke::new(width), transform, Self::parse_color(color), None, &circle); + } else { + self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color), None, &circle); + } + }; + // Fill + draw_circle(radius, None, color); + // Stroke (inner) + draw_circle(radius + stroke_width / 2., Some(1.), COLOR_OVERLAY_BLACK); + // Stroke (outer) + draw_circle(radius, Some(stroke_width), COLOR_OVERLAY_WHITE); } fn resize_handle(&mut self, position: DVec2, rotation: f64) { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index 9ad817a491..f47334659c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -468,7 +468,7 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } - pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) { self.start_dpi_aware_transform(); let position = position.round() - DVec2::splat(0.5); @@ -476,23 +476,25 @@ impl OverlayContext { let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - let stroke_circle = |radius: f64, width: f64, color: &str| { + let mut draw_circle = |radius: f64, width: Option, color: &str| { self.render_context.begin_path(); self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_line_width(width); - self.render_context.set_stroke_style_str(color); - self.render_context.stroke(); - }; - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - let fill = color.unwrap_or(if selected { COLOR_OVERLAY_WHITE } else { COLOR_OVERLAY_BLUE }); - self.render_context.set_fill_style_str(fill); - self.render_context.fill(); - - stroke_circle(radius + stroke_width / 2., 1., COLOR_OVERLAY_BLACK); - - stroke_circle(radius, stroke_width, COLOR_OVERLAY_WHITE); + if let Some(width) = width { + self.render_context.set_line_width(width); + self.render_context.set_stroke_style_str(color); + self.render_context.stroke(); + } else { + self.render_context.set_fill_style_str(color); + self.render_context.fill(); + } + }; + // Fill + draw_circle(radius, None, color); + // Stroke (inner) + draw_circle(radius + stroke_width / 2., Some(1.), COLOR_OVERLAY_BLACK); + // Stroke (outer) + draw_circle(radius, Some(stroke_width), COLOR_OVERLAY_WHITE); self.end_dpi_aware_transform(); } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index afb0f166b4..5997e7aeb4 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -296,18 +296,18 @@ impl Fsm for GradientToolFsmState { } } - let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)); - let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)); + let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); + let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); overlay_context.line(start, end, None, None); - overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), start_hex.as_deref()); - overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), end_hex.as_deref()); + overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), &start_hex); + overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), &end_hex); for (index, (position, color)) in stops.clone().into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), Some(&color_to_hex(color))); + overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(color)); } let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); From 23f37010358ee8b0d75f045ea0cb8cd9534cc000 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 04:52:07 +0000 Subject: [PATCH 11/15] corrected error --- .../portfolio/document/overlays/utility_types_native.rs | 2 +- .../messages/portfolio/document/overlays/utility_types_web.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 0dfb08e2f7..d0d9486529 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -593,7 +593,7 @@ impl OverlayContextInternal { let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - let mut draw_circle = |radius: f64, width: Option, color: &str| { + let draw_circle = |radius: f64, width: Option, color: &str| { let circle = kurbo::Circle::new((position.x, position.y), radius); if let Some(width) = width { self.scene.stroke(&kurbo::Stroke::new(width), transform, Self::parse_color(color), None, &circle); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index f47334659c..4fbaa5878d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -476,7 +476,7 @@ impl OverlayContext { let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - let mut draw_circle = |radius: f64, width: Option, color: &str| { + let draw_circle = |radius: f64, width: Option, color: &str| { self.render_context.begin_path(); self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); From e565fa03b1e0b7e3c06eac194b20236073ef9145 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 05:02:02 +0000 Subject: [PATCH 12/15] corrected error -2 --- .../portfolio/document/overlays/utility_types_native.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index d0d9486529..0dfb08e2f7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -593,7 +593,7 @@ impl OverlayContextInternal { let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) }; let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset; - let draw_circle = |radius: f64, width: Option, color: &str| { + let mut draw_circle = |radius: f64, width: Option, color: &str| { let circle = kurbo::Circle::new((position.x, position.y), radius); if let Some(width) = width { self.scene.stroke(&kurbo::Stroke::new(width), transform, Self::parse_color(color), None, &circle); From 176e90a6d0e9a8ee0cd6e7821915f7b77d232820 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 10:59:48 +0000 Subject: [PATCH 13/15] changes as per recommendation --- .../tool/tool_messages/gradient_tool.rs | 156 +++++++++++------- 1 file changed, 97 insertions(+), 59 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 5997e7aeb4..b33e72e8de 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_BLUE, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD}; +use crate::consts::{COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD}; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; @@ -111,13 +111,18 @@ impl LayoutHolder for GradientTool { } } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] enum GradientToolFsmState { - #[default] - Ready, + Ready { hover_insertion: bool }, Drawing, } +impl Default for GradientToolFsmState { + fn default() -> Self { + Self::Ready { hover_insertion: false } + } +} + /// Computes the transform from gradient space to viewport space (where gradient space is 0..1) fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { let bounds = document.metadata().nonzero_bounding_box(layer); @@ -134,6 +139,7 @@ pub enum GradientDragTarget { #[default] End, Step(usize), + New, } /// Contains information about the selected gradient handle @@ -143,7 +149,28 @@ struct SelectedGradient { transform: DAffine2, gradient: Gradient, dragging: GradientDragTarget, - offset: DVec2, + initial_gradient: Gradient, +} + +fn calculate_insertion(start: DVec2, end: DVec2, stops: &[(f64, graphene_std::Color)], mouse: DVec2) -> Option { + let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); + let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { + for (position, _) in stops { + let stop_pos = start.lerp(end, *position); + if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { + return None; + } + } + if start.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) || end.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { + return None; + } + + return Some(projection); + } + + None } impl SelectedGradient { @@ -152,23 +179,23 @@ impl SelectedGradient { Self { layer: Some(layer), transform, - gradient, + gradient: gradient.clone(), dragging: GradientDragTarget::End, - offset: DVec2::ZERO, + initial_gradient: gradient, } } - pub fn with_gradient_start(mut self, start: DVec2) -> Self { - self.gradient.start = self.transform.inverse().transform_point2(start); - self - } + pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool, gradient_type: GradientType, drag_start: DVec2) { + if mouse.distance(drag_start) < DRAG_THRESHOLD { + self.gradient = self.initial_gradient.clone(); + self.render_gradient(responses); + return; + } - pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool, gradient_type: GradientType) { - mouse += self.offset; self.gradient.gradient_type = gradient_type; - if snap_rotate && matches!(self.dragging, GradientDragTarget::End | GradientDragTarget::Start) { - let point = if self.dragging == GradientDragTarget::Start { + if snap_rotate && matches!(self.dragging, GradientDragTarget::End | GradientDragTarget::Start | GradientDragTarget::New) { + let point = if matches!(self.dragging, GradientDragTarget::Start | GradientDragTarget::New) { self.transform.transform_point2(self.gradient.end) } else { self.transform.transform_point2(self.gradient.start) @@ -191,6 +218,10 @@ impl SelectedGradient { match self.dragging { GradientDragTarget::Start => self.gradient.start = transformed_mouse, GradientDragTarget::End => self.gradient.end = transformed_mouse, + GradientDragTarget::New => { + self.gradient.start = self.transform.inverse().transform_point2(drag_start); + self.gradient.end = transformed_mouse; + } GradientDragTarget::Step(s) => { let (start, end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end)); @@ -310,39 +341,22 @@ impl Fsm for GradientToolFsmState { overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(color)); } - let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); - let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); - - if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { - let mut near_stop = false; - for (position, _) in stops { - let stop_pos = start.lerp(end, *position); - if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { - near_stop = true; - break; - } - } - if start.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) || end.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { - near_stop = true; - } - - if !near_stop { - if let Some(dir) = (end - start).try_normalize() { - let perp = dir.perp(); - let point = start.lerp(end, projection); - overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), Some(1.)); - } + if let Some(projection) = calculate_insertion(start, end, stops, mouse) { + if let Some(dir) = (end - start).try_normalize() { + let perp = dir.perp(); + let point = start.lerp(end, projection); + overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), Some(1.)); } } } self } - (GradientToolFsmState::Ready, GradientToolMessage::SelectionChanged) => { + (GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => { tool_data.selected_gradient = None; self } - (GradientToolFsmState::Ready, GradientToolMessage::DeleteStop) => { + (GradientToolFsmState::Ready { .. }, GradientToolMessage::DeleteStop) => { let Some(selected_gradient) = &mut tool_data.selected_gradient else { return self; }; @@ -365,6 +379,7 @@ impl Fsm for GradientToolFsmState { GradientDragTarget::Step(index) => { selected_gradient.gradient.stops.remove(index); } + GradientDragTarget::New => {} }; // The gradient has only one point and so should become a fill @@ -423,8 +438,6 @@ impl Fsm for GradientToolFsmState { // Select the new point selected_gradient.dragging = GradientDragTarget::Step(index); - selected_gradient.offset = DVec2::ZERO; - // Update the layer fill selected_gradient.render_gradient(responses); @@ -437,7 +450,7 @@ impl Fsm for GradientToolFsmState { self } - (GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => { + (GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerDown) => { let mouse = input.mouse.position; tool_data.drag_start = mouse; let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); @@ -457,7 +470,7 @@ impl Fsm for GradientToolFsmState { transform, gradient: gradient.clone(), dragging: GradientDragTarget::Step(index), - offset: pos - mouse, + initial_gradient: gradient.clone(), }) } } @@ -472,7 +485,7 @@ impl Fsm for GradientToolFsmState { transform, gradient: gradient.clone(), dragging: dragging_target, - offset: pos - mouse, + initial_gradient: gradient.clone(), }) } } @@ -493,7 +506,6 @@ impl Fsm for GradientToolFsmState { let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document); selected_gradient.dragging = GradientDragTarget::Step(index); // No offset when inserting a new stop, it should be exactly under the mouse - selected_gradient.offset = DVec2::ZERO; selected_gradient.render_gradient(responses); tool_data.selected_gradient = Some(selected_gradient); dragging = true; @@ -511,7 +523,7 @@ impl Fsm for GradientToolFsmState { if let Some(layer) = selected_layer { // Add check for raster layer if NodeGraphLayer::is_raster_layer(layer, &mut document.network_interface) { - return GradientToolFsmState::Ready; + return GradientToolFsmState::Ready { hover_insertion: false }; } if !document.network_interface.selected_nodes().selected_layers_contains(layer, document.metadata()) { let nodes = vec![layer.to_node()]; @@ -526,13 +538,14 @@ impl Fsm for GradientToolFsmState { // Generate a new gradient Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type) }; - let selected_gradient = SelectedGradient::new(gradient, layer, document).with_gradient_start(input.mouse.position); + let mut selected_gradient = SelectedGradient::new(gradient, layer, document); + selected_gradient.dragging = GradientDragTarget::New; tool_data.selected_gradient = Some(selected_gradient); GradientToolFsmState::Drawing } else { - GradientToolFsmState::Ready + GradientToolFsmState::Ready { hover_insertion: false } } }; @@ -547,7 +560,13 @@ impl Fsm for GradientToolFsmState { (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { if let Some(selected_gradient) = &mut tool_data.selected_gradient { let mouse = input.mouse.position; // tool_data.snap_manager.snap_position(responses, document, input.mouse.position); - selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type); + selected_gradient.update_gradient( + mouse, + responses, + input.keyboard.get(constrain_axis as usize), + selected_gradient.gradient.gradient_type, + tool_data.drag_start, + ); } // Auto-panning @@ -592,32 +611,51 @@ impl Fsm for GradientToolFsmState { { tool_data.selected_gradient = Some(SelectedGradient::new(gradient, selected_layer, document)); } - GradientToolFsmState::Ready + GradientToolFsmState::Ready { hover_insertion: false } } - (GradientToolFsmState::Ready, GradientToolMessage::PointerMove { .. }) => { + (GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerMove { .. }) => { + let mut hover_insertion = false; + let mouse = input.mouse.position; + + for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { + let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; + let transform = gradient_space_transform(layer, document); + let start = transform.transform_point2(gradient.start); + let end = transform.transform_point2(gradient.end); + + if calculate_insertion(start, end, &gradient.stops, mouse).is_some() { + hover_insertion = true; + break; + } + } + responses.add(OverlaysMessage::Draw); - GradientToolFsmState::Ready + GradientToolFsmState::Ready { hover_insertion } } (GradientToolFsmState::Drawing, GradientToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); tool_data.snap_manager.cleanup(responses); + tool_data.selected_gradient = None; responses.add(OverlaysMessage::Draw); - GradientToolFsmState::Ready + GradientToolFsmState::Ready { hover_insertion: false } } - (_, GradientToolMessage::Abort) => GradientToolFsmState::Ready, + (_, GradientToolMessage::Abort) => GradientToolFsmState::Ready { hover_insertion: false }, _ => self, } } fn update_hints(&self, responses: &mut VecDeque) { let hint_data = match self { - GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Gradient"), - HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), - ])]), + GradientToolFsmState::Ready { hover_insertion } => { + let mut hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Gradient"), HintInfo::keys([Key::Shift], "15° Increments").prepend_plus()]; + if *hover_insertion { + hints.insert(0, HintInfo::mouse(MouseMotion::Lmb, "Insert Color Stop")); + } + HintData(vec![HintGroup(hints)]) + } GradientToolFsmState::Drawing => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), @@ -787,7 +825,7 @@ mod test_gradient { } #[tokio::test] - async fn single_click_insert_stop() { + async fn click_to_insert_stop() { let mut editor = EditorTestUtils::create(); editor.new_document().await; From 3609297d9a0638b22dc5730166f490a774a693ca Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 12:36:44 +0000 Subject: [PATCH 14/15] error corrected --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index b33e72e8de..442d1cbc96 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -195,8 +195,10 @@ impl SelectedGradient { self.gradient.gradient_type = gradient_type; if snap_rotate && matches!(self.dragging, GradientDragTarget::End | GradientDragTarget::Start | GradientDragTarget::New) { - let point = if matches!(self.dragging, GradientDragTarget::Start | GradientDragTarget::New) { + let point = if self.dragging == GradientDragTarget::Start { self.transform.transform_point2(self.gradient.end) + } else if self.dragging == GradientDragTarget::New { + drag_start } else { self.transform.transform_point2(self.gradient.start) }; From d51e10099355422907d8cca6693782eb617e4ef2 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 1 Feb 2026 19:21:48 +0000 Subject: [PATCH 15/15] changes as suggested --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 442d1cbc96..9026f9e023 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -322,11 +322,7 @@ impl Fsm for GradientToolFsmState { let (start, end) = (transform.transform_point2(*start), transform.transform_point2(*end)); fn color_to_hex(color: graphene_std::Color) -> String { - if color.a() > 0.999 { - format!("#{:02X}{:02X}{:02X}", (color.r() * 255.) as u8, (color.g() * 255.) as u8, (color.b() * 255.) as u8) - } else { - color.to_rgba_hex_srgb() - } + color.with_alpha(1.).to_rgba_hex_srgb() } let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));