Skip to content

Commit 696b625

Browse files
authored
Fix hover transfer bugs occurring with the color picker popover (#4146)
* Fix hover transfer bugs occurring with the color picker popover * Code review fix
1 parent 4d5dce9 commit 696b625

5 files changed

Lines changed: 25 additions & 23 deletions

File tree

editor/src/messages/color_picker/color_picker_message_handler.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,6 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
6565
self.allow_none = allow_none;
6666
self.disabled = disabled;
6767

68-
// Each `<ColorPicker>` Svelte instance maintains its own local layout state, but the Rust `LayoutMessageHandler` keeps a single shared layout per target. When a new picker instance opens after a previous one closed, the new instance's layout starts empty and a diff from the previously-shared state would not apply. Destroying the stored layouts here forces the next `SendLayout` to send the full layout instead of a diff.
69-
responses.add(LayoutMessage::DestroyLayout {
70-
layout_target: LayoutTarget::ColorPickerPickersAndGradient,
71-
});
72-
responses.add(LayoutMessage::DestroyLayout {
73-
layout_target: LayoutTarget::ColorPickerDetails,
74-
});
75-
7668
match initial_value {
7769
FillChoice::None => {
7870
self.set_new_hsva(0., 0., 0., 1., true);
@@ -99,10 +91,6 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
9991
self.send_layouts(responses);
10092
}
10193
ColorPickerMessage::Close => {
102-
self.gradient = None;
103-
self.active_marker_index = None;
104-
self.active_marker_is_midpoint = false;
105-
10694
responses.add(DocumentMessage::EndTransaction);
10795
}
10896
ColorPickerMessage::VisualUpdate { update } => {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
55
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
66
import WidgetLayout from "/src/components/widgets/WidgetLayout.svelte";
7-
import type { ColorPickerStore } from "/src/stores/color-picker";
7+
import type { ColorPickerCallbacks, ColorPickerStore } from "/src/stores/color-picker";
88
import type { EditorWrapper, FillChoice, MenuDirection } from "/wrapper/pkg/graphite_wasm_wrapper";
99
1010
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
@@ -27,23 +27,27 @@
2727
// Open/close lifecycle: when `open` flips, register/clear the global callbacks (so events route to *this* instance)
2828
// and tell the Rust handler to (re)initialize its state from the current `colorOrGradient`.
2929
let lastOpen = false;
30+
// Identity used by `clearCallbacks` to skip stale clears when another picker has already taken over the store's callbacks
31+
let installedCallbacks: ColorPickerCallbacks | undefined;
3032
$: handleOpenChange(open);
3133
3234
function handleOpenChange(isOpen: boolean) {
3335
if (isOpen && !lastOpen) {
34-
colorPickerStore.setCallbacks({
36+
installedCallbacks = {
3537
onColorChanged: (value) => dispatch("colorOrGradient", value),
3638
onStartTransaction: () => dispatch("startHistoryTransaction"),
3739
onCommitTransaction: () => dispatch("commitHistoryTransaction"),
38-
});
40+
};
41+
colorPickerStore.setCallbacks(installedCallbacks);
3942
editor.openColorPicker(colorOrGradient, allowNone, disabled);
4043
// Auto-select the hex color code text input. Deferred so the layout has time to render after the picker opens.
4144
setTimeout(() => {
4245
const hexInput = self?.div()?.querySelector(".text-input input");
4346
if (hexInput instanceof HTMLInputElement) hexInput.select();
4447
}, 0);
4548
} else if (!isOpen && lastOpen) {
46-
colorPickerStore.clearCallbacks();
49+
if (installedCallbacks) colorPickerStore.clearCallbacks(installedCallbacks);
50+
installedCallbacks = undefined;
4751
editor.closeColorPicker();
4852
}
4953
lastOpen = isOpen;
@@ -55,7 +59,8 @@
5559
5660
onDestroy(() => {
5761
if (!lastOpen) return;
58-
colorPickerStore.clearCallbacks();
62+
if (installedCallbacks) colorPickerStore.clearCallbacks(installedCallbacks);
63+
installedCallbacks = undefined;
5964
editor.closeColorPicker();
6065
});
6166
</script>

frontend/src/components/layout/FloatingMenu.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@
336336
337337
// HOVER TRANSFER
338338
// Transfer from this open floating menu to a sibling floating menu if the pointer hovers to a valid neighboring floating menu spawner
339-
hoverTransfer(self, ownSpawner, targetSpawner);
339+
if (strayCloses) hoverTransfer(self, ownSpawner, targetSpawner);
340340
341341
// POINTER STRAY
342342
// Close the floating menu if the pointer has strayed far enough from its bounds (and it's not hovering over its own spawner)

frontend/src/components/widgets/inputs/TextInput.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@
4040
// The `unFocus()` call in `onTextChangeCanceled()` causes itself to be run again, so this if statement skips a second run
4141
if (!editing) return;
4242
43+
// Capture before `onTextChangeCanceled` blurs the input
44+
const currentValue = self?.getValue();
45+
4346
onTextChangeCanceled();
4447
45-
// TODO: Find a less hacky way to do this
46-
if (self) dispatch("commitText", self.getValue());
48+
// Only commit on a real edit, so a blur fired when the focused input is removed from the DOM (e.g., from a picker closing
49+
// during hover transfer) doesn't round-trip the original value back to the backend and overwrite concurrent state.
50+
if (self && currentValue !== undefined && currentValue !== value) {
51+
dispatch("commitText", currentValue);
52+
}
4753
4854
// Required if value is not changed by the parent component upon update:value event
4955
self?.setInputElementValue(self.getValue());

frontend/src/stores/color-picker.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const { subscribe, update } = store;
3535
export type ColorPickerStore = {
3636
subscribe: typeof subscribe;
3737
setCallbacks: (callbacks: ColorPickerCallbacks) => void;
38-
clearCallbacks: () => void;
38+
// Identity-checked so a hover-transfer race where another picker's `setCallbacks` already replaced these doesn't clobber the new picker's callbacks
39+
clearCallbacks: (expected: ColorPickerCallbacks) => void;
3940
setDragging: (dragging: boolean) => void;
4041
};
4142

@@ -88,10 +89,12 @@ export function createColorPickerStore(subscriptions: SubscriptionsRouter): Colo
8889
return state;
8990
});
9091
},
91-
clearCallbacks: () => {
92+
clearCallbacks: (expected: ColorPickerCallbacks) => {
9293
update((state) => {
93-
state.callbacks = {};
9494
state.isDragging = false;
95+
if (state.callbacks === expected) {
96+
state.callbacks = {};
97+
}
9598
return state;
9699
});
97100
},

0 commit comments

Comments
 (0)