From 5a2ae10bb29ffff67362c02ad0901ca820df051c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 25 Feb 2026 16:49:29 -0800 Subject: [PATCH 1/3] Hide batched blocked debug print messages --- editor/src/dispatcher.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 05d3a8e93e..c973d58589 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -359,10 +359,15 @@ impl Dispatcher { /// with a discriminant or the entire payload (depending on settings) fn log_message(&self, message: &Message, queues: &[VecDeque], message_logging_verbosity: MessageLoggingVerbosity) { let discriminant = MessageDiscriminant::from(message); - let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name)); - let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false }; + let is_blocked = + |discriminant| DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name)); + let is_empty_batched = if let Message::Batched { messages } = message { + messages.iter().all(|message| is_blocked(MessageDiscriminant::from(message))) + } else { + false + }; - if !is_blocked && !is_empty_batched { + if !is_blocked(discriminant) && !is_empty_batched { match message_logging_verbosity { MessageLoggingVerbosity::Off => {} MessageLoggingVerbosity::Names => { From 9973bc7016193c99ffbd6af868a83ab13be985e0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 25 Feb 2026 19:23:05 -0800 Subject: [PATCH 2/3] Implement the color picker on double-clicking stops --- .../src/messages/frontend/frontend_message.rs | 5 + .../tool/tool_messages/gradient_tool.rs | 167 +++++++++++++++--- .../floating-menus/ColorPicker.svelte | 15 +- .../src/components/layout/FloatingMenu.svelte | 5 +- .../src/components/panels/Document.svelte | 56 ++++++ .../widgets/inputs/ColorInput.svelte | 6 +- frontend/src/io-managers/input.ts | 3 +- frontend/src/messages.ts | 10 ++ frontend/wasm/src/editor_api.rs | 28 +++ 9 files changed, 254 insertions(+), 41 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index bf1535c9ad..293b9a77f9 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -57,6 +57,11 @@ pub enum FrontendMessage { transform: [f64; 6], }, DisplayRemoveEditableTextbox, + UpdateGradientStopColorPickerPosition { + color: Color, + x: f64, + y: f64, + }, // Send prefix: Send global, static data to the frontend that is never updated SendUIMetadata { diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 3b47651aa3..fc3bad458a 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; +use graphene_std::raster::color::Color; use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; #[derive(Default, ExtractField)] @@ -38,6 +39,10 @@ pub enum GradientToolMessage { PointerMove { constrain_axis: Key, lock_angle: Key }, PointerOutsideViewport { constrain_axis: Key, lock_angle: Key }, PointerUp, + StartTransactionForColorStop, + CommitTransactionForColorStop, + CloseStopColorPicker, + UpdateStopColor { color: Color }, UpdateOptions { options: GradientOptionsUpdate }, } @@ -63,29 +68,59 @@ impl ToolMetadata for GradientTool { #[message_handler_data] impl<'a> MessageHandler> for GradientTool { 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); + match message { + ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) => match options { + GradientOptionsUpdate::Type(gradient_type) => { + self.options.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(ToolMessage::UpdateCursor); + } + 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)); + } + }, + ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => { + if self.data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + } + responses.add(DocumentMessage::StartTransaction); + self.data.color_picker_transaction_open = true; } - - return; - }; - match options { - GradientOptionsUpdate::Type(gradient_type) => { - self.options.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(ToolMessage::UpdateCursor); + ToolMessage::Gradient(GradientToolMessage::CommitTransactionForColorStop) => { + if self.data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + self.data.color_picker_transaction_open = false; + } + } + ToolMessage::Gradient(GradientToolMessage::UpdateStopColor { color }) => { + if let Some(stop_index) = self.data.color_picker_editing_color_stop + && let Some(selected_gradient) = &mut self.data.selected_gradient + && stop_index < selected_gradient.gradient.stops.color.len() + { + selected_gradient.gradient.stops.color[stop_index] = color; + selected_gradient.render_gradient(responses); + responses.add(PropertiesPanelMessage::Refresh); + } } - GradientOptionsUpdate::ReverseStops => { - apply_gradient_update(&mut self.data, context, responses, |_| true, |g| g.stops = g.stops.reversed()); + ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => { + if self.data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + self.data.color_picker_transaction_open = false; + } + self.data.color_picker_editing_color_stop = None; } - GradientOptionsUpdate::ReverseDirection => { - apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end)); + _ => { + 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); + } } } } @@ -515,6 +550,8 @@ struct GradientToolData { auto_pan_shift: DVec2, gradient_angle: f64, has_selected_gradient: bool, + color_picker_editing_color_stop: Option, + color_picker_transaction_open: bool, } impl Fsm for GradientToolFsmState { @@ -723,9 +760,31 @@ impl Fsm for GradientToolFsmState { let snap_data = SnapData::new(document, input, viewport); tool_data.snap_manager.draw_overlays(snap_data, &mut overlay_context); + // Update color picker position if active (keeps it anchored to the stop during pan/zoom) + if let Some(stop_index) = tool_data.color_picker_editing_color_stop + && let Some(selected_gradient) = tool_data.selected_gradient.as_ref() + && let Some(layer) = selected_gradient.layer + { + let transform = gradient_space_transform(layer, document); + let gradient = &selected_gradient.gradient; + if stop_index < gradient.stops.position.len() { + let color = gradient.stops.color[stop_index].to_gamma_srgb(); + let position = gradient.stops.position[stop_index]; + let DVec2 { x, y } = transform.transform_point2(gradient.start.lerp(gradient.end, position)); + responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, x, y }); + } + } + self } (GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => { + if tool_data.color_picker_editing_color_stop.is_some() { + if tool_data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + tool_data.color_picker_transaction_open = false; + } + tool_data.color_picker_editing_color_stop = None; + } tool_data.selected_gradient = None; GradientToolFsmState::Ready { hovering: GradientHoverTarget::None, @@ -737,11 +796,45 @@ impl Fsm for GradientToolFsmState { let drag_start_viewport = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start); if input.mouse.position.distance(drag_start_viewport) <= DRAG_THRESHOLD && let Some(selected_gradient) = &mut tool_data.selected_gradient - && let GradientDragTarget::Midpoint(index) = selected_gradient.dragging { - selected_gradient.gradient.stops.midpoint[index] = 0.5; - selected_gradient.render_gradient(responses); - responses.add(PropertiesPanelMessage::Refresh); + match selected_gradient.dragging { + GradientDragTarget::Midpoint(index) => { + selected_gradient.gradient.stops.midpoint[index] = 0.5; + selected_gradient.render_gradient(responses); + responses.add(PropertiesPanelMessage::Refresh); + } + GradientDragTarget::Start | GradientDragTarget::End | GradientDragTarget::Stop(_) => { + // Find the stop index from the drag target + let stop_index = match selected_gradient.dragging { + GradientDragTarget::Stop(i) => Some(i), + GradientDragTarget::Start => selected_gradient.gradient.stops.position.iter().position(|&p| p.abs() < f64::EPSILON * 1000.), + GradientDragTarget::End => selected_gradient.gradient.stops.position.iter().position(|&p| (1. - p).abs() < f64::EPSILON * 1000.), + _ => None, + }; + if let Some(stop_index) = stop_index + && stop_index < selected_gradient.gradient.stops.color.len() + { + // Dismiss any existing color picker first + if tool_data.color_picker_editing_color_stop.is_some() && tool_data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + tool_data.color_picker_transaction_open = false; + } + + let stop_pos = selected_gradient.gradient.stops.position[stop_index]; + let viewport_pos = selected_gradient + .transform + .transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos)); + let color = selected_gradient.gradient.stops.color[stop_index].to_gamma_srgb(); + tool_data.color_picker_editing_color_stop = Some(stop_index); + responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { + color, + x: viewport_pos.x, + y: viewport_pos.y, + }); + } + } + _ => {} + } } self } @@ -1178,15 +1271,21 @@ impl Fsm for GradientToolFsmState { tool_data.selected_gradient = None; responses.add(OverlaysMessage::Draw); + dismiss_color_stop_color_picker(tool_data, responses); + + GradientToolFsmState::Ready { + hovering: GradientHoverTarget::None, + selected: GradientSelectedTarget::None, + } + } + (_, GradientToolMessage::Abort) => { + dismiss_color_stop_color_picker(tool_data, responses); + GradientToolFsmState::Ready { hovering: GradientHoverTarget::None, selected: GradientSelectedTarget::None, } } - (_, GradientToolMessage::Abort) => GradientToolFsmState::Ready { - hovering: GradientHoverTarget::None, - selected: GradientSelectedTarget::None, - }, _ => self, } } @@ -1273,6 +1372,16 @@ impl Fsm for GradientToolFsmState { } } +fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque) { + if tool_data.color_picker_editing_color_stop.is_some() { + if tool_data.color_picker_transaction_open { + responses.add(DocumentMessage::EndTransaction); + tool_data.color_picker_transaction_open = false; + } + tool_data.color_picker_editing_color_stop = None; + } +} + fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> GradientHoverTarget { let stop_tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2); @@ -1380,7 +1489,7 @@ fn apply_gradient_update( } if transaction_started { - responses.add(DocumentMessage::AddTransaction); + responses.add(DocumentMessage::EndTransaction); } if let Some(selected_gradient) = &mut data.selected_gradient && let Some(layer) = selected_gradient.layer diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 91caa98b2b..a9b6972fc3 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -1,5 +1,5 @@ - + Color) + readonly color!: Color; + + readonly x!: number; + + readonly y!: number; +} + export class UpdateDocumentLayerDetails extends JsMessage { @Type(() => LayerPanelEntry) readonly data!: LayerPanelEntry; @@ -1713,6 +1722,7 @@ export const messageMakers: Record = { UpdateEyedropperSamplingState, UpdateFullscreen, UpdateGraphFadeArtwork, + UpdateGradientStopColorPickerPosition, UpdateGraphViewOverlay, UpdateImportReorderIndex, UpdateImportsExports, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index e0353b1b65..0472a283ab 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -650,6 +650,34 @@ impl EditorHandle { Ok(()) } + /// Update the color of the currently-edited gradient stop + #[wasm_bindgen(js_name = updateGradientStopColor)] + pub fn update_gradient_stop_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> { + let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) else { + return Err(Error::new("Invalid color").into()); + }; + self.dispatch(GradientToolMessage::UpdateStopColor { color: color.to_linear_srgb() }); + Ok(()) + } + + /// Start a new undo transaction for gradient stop color editing + #[wasm_bindgen(js_name = startGradientStopColorTransaction)] + pub fn start_gradient_stop_color_transaction(&self) { + self.dispatch(GradientToolMessage::StartTransactionForColorStop); + } + + /// Commit the current gradient stop color transaction (called on pointer-up after each drag/click) + #[wasm_bindgen(js_name = commitGradientStopColorTransaction)] + pub fn commit_gradient_stop_color_transaction(&self) { + self.dispatch(GradientToolMessage::CommitTransactionForColorStop); + } + + /// Close the gradient stop color picker and commit any pending transaction + #[wasm_bindgen(js_name = closeGradientStopColorPicker)] + pub fn close_gradient_stop_color_picker(&self) { + self.dispatch(GradientToolMessage::CloseStopColorPicker); + } + #[wasm_bindgen(js_name = clipLayer)] pub fn clip_layer(&self, id: u64) { let id = NodeId(id); From 18a0724ba451efca1c7538f67ad550157ff7eeb5 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 25 Feb 2026 20:07:29 -0800 Subject: [PATCH 3/3] Code review --- editor/src/dispatcher.rs | 4 ++-- editor/src/messages/frontend/frontend_message.rs | 10 +++++----- frontend/src/components/panels/Document.svelte | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index c973d58589..976f931016 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -361,13 +361,13 @@ impl Dispatcher { let discriminant = MessageDiscriminant::from(message); let is_blocked = |discriminant| DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name)); - let is_empty_batched = if let Message::Batched { messages } = message { + let is_batch_all_blocked = if let Message::Batched { messages } = message { messages.iter().all(|message| is_blocked(MessageDiscriminant::from(message))) } else { false }; - if !is_blocked(discriminant) && !is_empty_batched { + if !is_blocked(discriminant) && !is_batch_all_blocked { match message_logging_verbosity { MessageLoggingVerbosity::Off => {} MessageLoggingVerbosity::Names => { diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 293b9a77f9..2160834136 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -57,11 +57,6 @@ pub enum FrontendMessage { transform: [f64; 6], }, DisplayRemoveEditableTextbox, - UpdateGradientStopColorPickerPosition { - color: Color, - x: f64, - y: f64, - }, // Send prefix: Send global, static data to the frontend that is never updated SendUIMetadata { @@ -156,6 +151,11 @@ pub enum FrontendMessage { #[serde(rename = "documentId")] document_id: DocumentId, }, + UpdateGradientStopColorPickerPosition { + color: Color, + x: f64, + y: f64, + }, UpdateImportsExports { /// If the primary import is not visible, then it is None. imports: Vec>, diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 9be7c3bcfe..d8453997f8 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -416,7 +416,7 @@ } function gradientStopPickerDirection(position: XY | undefined, viewport: HTMLDivElement | undefined): MenuDirection { - const picker = (gradientStopPicker?.div()?.querySelector("[data-floating-menu-content") || undefined) as HTMLElement | undefined; + const picker = (gradientStopPicker?.div()?.querySelector("[data-floating-menu-content]") || undefined) as HTMLElement | undefined; if (!picker || !position || !viewport) return "Bottom"; const roomRight = position.x + picker.offsetWidth - viewport.clientWidth;