diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 05d3a8e93e..976f931016 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_batch_all_blocked = 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_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 bf1535c9ad..2160834136 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -151,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/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);