Add an in-viewport color picker to the Gradient tool when double-clicking a color stop (#3834)

* Hide batched blocked debug print messages

* Implement the color picker on double-clicking stops

* Code review
This commit is contained in:
Keavon Chambers
2026-02-25 21:03:12 -08:00
committed by GitHub
parent 82cf8eb369
commit e62771845f
10 changed files with 264 additions and 46 deletions
+8 -3
View File
@@ -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>], 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 => {
@@ -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<Option<FrontendGraphOutput>>,
@@ -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<ToolMessage, &mut ToolActionMessageContext<'a>> for GradientTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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);
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;
}
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);
}
}
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;
}
_ => {
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
let has_gradient = has_gradient_on_selected_layers(context.document);
if has_gradient != self.data.has_selected_gradient {
self.data.has_selected_gradient = has_gradient;
responses.add(ToolMessage::RefreshToolOptions);
}
return;
};
match options {
GradientOptionsUpdate::Type(gradient_type) => {
self.options.gradient_type = gradient_type;
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));
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<usize>,
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<Message>) {
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