Skip to content

Commit e627718

Browse files
authored
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
1 parent 82cf8eb commit e627718

File tree

10 files changed

+262
-44
lines changed

10 files changed

+262
-44
lines changed

editor/src/dispatcher.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,15 @@ impl Dispatcher {
359359
/// with a discriminant or the entire payload (depending on settings)
360360
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
361361
let discriminant = MessageDiscriminant::from(message);
362-
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));
363-
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
362+
let is_blocked =
363+
|discriminant| DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
364+
let is_batch_all_blocked = if let Message::Batched { messages } = message {
365+
messages.iter().all(|message| is_blocked(MessageDiscriminant::from(message)))
366+
} else {
367+
false
368+
};
364369

365-
if !is_blocked && !is_empty_batched {
370+
if !is_blocked(discriminant) && !is_batch_all_blocked {
366371
match message_logging_verbosity {
367372
MessageLoggingVerbosity::Off => {}
368373
MessageLoggingVerbosity::Names => {

editor/src/messages/frontend/frontend_message.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ pub enum FrontendMessage {
151151
#[serde(rename = "documentId")]
152152
document_id: DocumentId,
153153
},
154+
UpdateGradientStopColorPickerPosition {
155+
color: Color,
156+
x: f64,
157+
y: f64,
158+
},
154159
UpdateImportsExports {
155160
/// If the primary import is not visible, then it is None.
156161
imports: Vec<Option<FrontendGraphOutput>>,

editor/src/messages/tool/tool_messages/gradient_tool.rs

Lines changed: 138 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
88
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
99
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient};
1010
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
11+
use graphene_std::raster::color::Color;
1112
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType};
1213

1314
#[derive(Default, ExtractField)]
@@ -38,6 +39,10 @@ pub enum GradientToolMessage {
3839
PointerMove { constrain_axis: Key, lock_angle: Key },
3940
PointerOutsideViewport { constrain_axis: Key, lock_angle: Key },
4041
PointerUp,
42+
StartTransactionForColorStop,
43+
CommitTransactionForColorStop,
44+
CloseStopColorPicker,
45+
UpdateStopColor { color: Color },
4146
UpdateOptions { options: GradientOptionsUpdate },
4247
}
4348

@@ -63,29 +68,59 @@ impl ToolMetadata for GradientTool {
6368
#[message_handler_data]
6469
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for GradientTool {
6570
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
66-
let ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) = message else {
67-
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
68-
69-
let has_gradient = has_gradient_on_selected_layers(context.document);
70-
if has_gradient != self.data.has_selected_gradient {
71-
self.data.has_selected_gradient = has_gradient;
72-
responses.add(ToolMessage::RefreshToolOptions);
71+
match message {
72+
ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) => match options {
73+
GradientOptionsUpdate::Type(gradient_type) => {
74+
self.options.gradient_type = gradient_type;
75+
apply_gradient_update(&mut self.data, context, responses, |g| g.gradient_type != gradient_type, |g| g.gradient_type = gradient_type);
76+
responses.add(ToolMessage::UpdateHints);
77+
responses.add(ToolMessage::UpdateCursor);
78+
}
79+
GradientOptionsUpdate::ReverseStops => {
80+
apply_gradient_update(&mut self.data, context, responses, |_| true, |g| g.stops = g.stops.reversed());
81+
}
82+
GradientOptionsUpdate::ReverseDirection => {
83+
apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end));
84+
}
85+
},
86+
ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => {
87+
if self.data.color_picker_transaction_open {
88+
responses.add(DocumentMessage::EndTransaction);
89+
}
90+
responses.add(DocumentMessage::StartTransaction);
91+
self.data.color_picker_transaction_open = true;
7392
}
74-
75-
return;
76-
};
77-
match options {
78-
GradientOptionsUpdate::Type(gradient_type) => {
79-
self.options.gradient_type = gradient_type;
80-
apply_gradient_update(&mut self.data, context, responses, |g| g.gradient_type != gradient_type, |g| g.gradient_type = gradient_type);
81-
responses.add(ToolMessage::UpdateHints);
82-
responses.add(ToolMessage::UpdateCursor);
93+
ToolMessage::Gradient(GradientToolMessage::CommitTransactionForColorStop) => {
94+
if self.data.color_picker_transaction_open {
95+
responses.add(DocumentMessage::EndTransaction);
96+
self.data.color_picker_transaction_open = false;
97+
}
98+
}
99+
ToolMessage::Gradient(GradientToolMessage::UpdateStopColor { color }) => {
100+
if let Some(stop_index) = self.data.color_picker_editing_color_stop
101+
&& let Some(selected_gradient) = &mut self.data.selected_gradient
102+
&& stop_index < selected_gradient.gradient.stops.color.len()
103+
{
104+
selected_gradient.gradient.stops.color[stop_index] = color;
105+
selected_gradient.render_gradient(responses);
106+
responses.add(PropertiesPanelMessage::Refresh);
107+
}
83108
}
84-
GradientOptionsUpdate::ReverseStops => {
85-
apply_gradient_update(&mut self.data, context, responses, |_| true, |g| g.stops = g.stops.reversed());
109+
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
110+
if self.data.color_picker_transaction_open {
111+
responses.add(DocumentMessage::EndTransaction);
112+
self.data.color_picker_transaction_open = false;
113+
}
114+
self.data.color_picker_editing_color_stop = None;
86115
}
87-
GradientOptionsUpdate::ReverseDirection => {
88-
apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end));
116+
_ => {
117+
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
118+
119+
let has_gradient = has_gradient_on_selected_layers(context.document);
120+
if has_gradient != self.data.has_selected_gradient {
121+
self.data.has_selected_gradient = has_gradient;
122+
responses.add(ToolMessage::RefreshToolOptions);
123+
}
89124
}
90125
}
91126
}
@@ -515,6 +550,8 @@ struct GradientToolData {
515550
auto_pan_shift: DVec2,
516551
gradient_angle: f64,
517552
has_selected_gradient: bool,
553+
color_picker_editing_color_stop: Option<usize>,
554+
color_picker_transaction_open: bool,
518555
}
519556

520557
impl Fsm for GradientToolFsmState {
@@ -723,9 +760,31 @@ impl Fsm for GradientToolFsmState {
723760
let snap_data = SnapData::new(document, input, viewport);
724761
tool_data.snap_manager.draw_overlays(snap_data, &mut overlay_context);
725762

763+
// Update color picker position if active (keeps it anchored to the stop during pan/zoom)
764+
if let Some(stop_index) = tool_data.color_picker_editing_color_stop
765+
&& let Some(selected_gradient) = tool_data.selected_gradient.as_ref()
766+
&& let Some(layer) = selected_gradient.layer
767+
{
768+
let transform = gradient_space_transform(layer, document);
769+
let gradient = &selected_gradient.gradient;
770+
if stop_index < gradient.stops.position.len() {
771+
let color = gradient.stops.color[stop_index].to_gamma_srgb();
772+
let position = gradient.stops.position[stop_index];
773+
let DVec2 { x, y } = transform.transform_point2(gradient.start.lerp(gradient.end, position));
774+
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, x, y });
775+
}
776+
}
777+
726778
self
727779
}
728780
(GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => {
781+
if tool_data.color_picker_editing_color_stop.is_some() {
782+
if tool_data.color_picker_transaction_open {
783+
responses.add(DocumentMessage::EndTransaction);
784+
tool_data.color_picker_transaction_open = false;
785+
}
786+
tool_data.color_picker_editing_color_stop = None;
787+
}
729788
tool_data.selected_gradient = None;
730789
GradientToolFsmState::Ready {
731790
hovering: GradientHoverTarget::None,
@@ -737,11 +796,45 @@ impl Fsm for GradientToolFsmState {
737796
let drag_start_viewport = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start);
738797
if input.mouse.position.distance(drag_start_viewport) <= DRAG_THRESHOLD
739798
&& let Some(selected_gradient) = &mut tool_data.selected_gradient
740-
&& let GradientDragTarget::Midpoint(index) = selected_gradient.dragging
741799
{
742-
selected_gradient.gradient.stops.midpoint[index] = 0.5;
743-
selected_gradient.render_gradient(responses);
744-
responses.add(PropertiesPanelMessage::Refresh);
800+
match selected_gradient.dragging {
801+
GradientDragTarget::Midpoint(index) => {
802+
selected_gradient.gradient.stops.midpoint[index] = 0.5;
803+
selected_gradient.render_gradient(responses);
804+
responses.add(PropertiesPanelMessage::Refresh);
805+
}
806+
GradientDragTarget::Start | GradientDragTarget::End | GradientDragTarget::Stop(_) => {
807+
// Find the stop index from the drag target
808+
let stop_index = match selected_gradient.dragging {
809+
GradientDragTarget::Stop(i) => Some(i),
810+
GradientDragTarget::Start => selected_gradient.gradient.stops.position.iter().position(|&p| p.abs() < f64::EPSILON * 1000.),
811+
GradientDragTarget::End => selected_gradient.gradient.stops.position.iter().position(|&p| (1. - p).abs() < f64::EPSILON * 1000.),
812+
_ => None,
813+
};
814+
if let Some(stop_index) = stop_index
815+
&& stop_index < selected_gradient.gradient.stops.color.len()
816+
{
817+
// Dismiss any existing color picker first
818+
if tool_data.color_picker_editing_color_stop.is_some() && tool_data.color_picker_transaction_open {
819+
responses.add(DocumentMessage::EndTransaction);
820+
tool_data.color_picker_transaction_open = false;
821+
}
822+
823+
let stop_pos = selected_gradient.gradient.stops.position[stop_index];
824+
let viewport_pos = selected_gradient
825+
.transform
826+
.transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos));
827+
let color = selected_gradient.gradient.stops.color[stop_index].to_gamma_srgb();
828+
tool_data.color_picker_editing_color_stop = Some(stop_index);
829+
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition {
830+
color,
831+
x: viewport_pos.x,
832+
y: viewport_pos.y,
833+
});
834+
}
835+
}
836+
_ => {}
837+
}
745838
}
746839
self
747840
}
@@ -1178,15 +1271,21 @@ impl Fsm for GradientToolFsmState {
11781271
tool_data.selected_gradient = None;
11791272
responses.add(OverlaysMessage::Draw);
11801273

1274+
dismiss_color_stop_color_picker(tool_data, responses);
1275+
1276+
GradientToolFsmState::Ready {
1277+
hovering: GradientHoverTarget::None,
1278+
selected: GradientSelectedTarget::None,
1279+
}
1280+
}
1281+
(_, GradientToolMessage::Abort) => {
1282+
dismiss_color_stop_color_picker(tool_data, responses);
1283+
11811284
GradientToolFsmState::Ready {
11821285
hovering: GradientHoverTarget::None,
11831286
selected: GradientSelectedTarget::None,
11841287
}
11851288
}
1186-
(_, GradientToolMessage::Abort) => GradientToolFsmState::Ready {
1187-
hovering: GradientHoverTarget::None,
1188-
selected: GradientSelectedTarget::None,
1189-
},
11901289
_ => self,
11911290
}
11921291
}
@@ -1273,6 +1372,16 @@ impl Fsm for GradientToolFsmState {
12731372
}
12741373
}
12751374

1375+
fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
1376+
if tool_data.color_picker_editing_color_stop.is_some() {
1377+
if tool_data.color_picker_transaction_open {
1378+
responses.add(DocumentMessage::EndTransaction);
1379+
tool_data.color_picker_transaction_open = false;
1380+
}
1381+
tool_data.color_picker_editing_color_stop = None;
1382+
}
1383+
}
1384+
12761385
fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> GradientHoverTarget {
12771386
let stop_tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2);
12781387
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
@@ -1380,7 +1489,7 @@ fn apply_gradient_update(
13801489
}
13811490

13821491
if transaction_started {
1383-
responses.add(DocumentMessage::AddTransaction);
1492+
responses.add(DocumentMessage::EndTransaction);
13841493
}
13851494
if let Some(selected_gradient) = &mut data.selected_gradient
13861495
&& let Some(layer) = selected_gradient.layer

frontend/src/components/floating-menus/ColorPicker.svelte

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { getContext, onDestroy, createEventDispatcher } from "svelte";
2+
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
33
44
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
55
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
@@ -40,7 +40,7 @@
4040
["Magenta", "#ff00ff", "#696969"],
4141
];
4242
43-
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
43+
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
4444
const tooltip = getContext<TooltipState>("tooltip");
4545
4646
export let colorOrGradient: FillChoice;
@@ -109,10 +109,11 @@
109109
return new Color({ h, s, v, a });
110110
}
111111
112-
function watchOpen(open: boolean) {
112+
async function watchOpen(open: boolean) {
113113
if (open) {
114114
setTimeout(() => hexCodeInputWidget?.focus(), 0);
115-
} else {
115+
116+
await tick();
116117
setOldHSVA(hue, saturation, value, alpha, isNone);
117118
}
118119
}
@@ -198,6 +199,7 @@
198199
}
199200
200201
function onPointerUp() {
202+
if (draggingPickerTrack) dispatch("commitHistoryTransaction");
201203
removeEvents();
202204
}
203205
@@ -413,6 +415,10 @@
413415
setOldHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
414416
}
415417
418+
export function div(): HTMLDivElement | undefined {
419+
return self?.div();
420+
}
421+
416422
onDestroy(() => {
417423
removeEvents();
418424
});
@@ -705,6 +711,7 @@
705711

706712
<style lang="scss" global>
707713
.color-picker {
714+
--widget-height: 24px;
708715
--picker-size: 256px;
709716
--picker-circle-radius: 6px;
710717

frontend/src/components/layout/FloatingMenu.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@
199199
}
200200
201201
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
202-
if (!inParentFloatingMenu) {
202+
const noPosition = Boolean(floatingMenuContainer.closest("[data-floating-menu-no-position]"));
203+
if (!inParentFloatingMenu && !noPosition) {
203204
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
204205
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
205206
let tailOffset = 0;
@@ -322,7 +323,7 @@
322323
323324
// POINTER STRAY
324325
// Close the floating menu if the pointer has strayed far enough from its bounds (and it's not hovering over its own spawner)
325-
const notHoveringOverOwnSpawner = ownSpawner !== targetSpawner;
326+
const notHoveringOverOwnSpawner = ownSpawner !== targetSpawner || (ownSpawner === undefined && targetSpawner === undefined);
326327
if (strayCloses && notHoveringOverOwnSpawner && isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) {
327328
// TODO: Extend this rectangle bounds check to all submenu bounds up the DOM tree since currently submenus disappear
328329
// TODO: with zero stray distance if the cursor is further than the stray distance from only the top-level menu

0 commit comments

Comments
 (0)