mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 06:08:11 +08:00
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:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy, createEventDispatcher } from "svelte";
|
||||
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
@@ -40,7 +40,7 @@
|
||||
["Magenta", "#ff00ff", "#696969"],
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
|
||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
|
||||
export let colorOrGradient: FillChoice;
|
||||
@@ -109,10 +109,11 @@
|
||||
return new Color({ h, s, v, a });
|
||||
}
|
||||
|
||||
function watchOpen(open: boolean) {
|
||||
async function watchOpen(open: boolean) {
|
||||
if (open) {
|
||||
setTimeout(() => hexCodeInputWidget?.focus(), 0);
|
||||
} else {
|
||||
|
||||
await tick();
|
||||
setOldHSVA(hue, saturation, value, alpha, isNone);
|
||||
}
|
||||
}
|
||||
@@ -198,6 +199,7 @@
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (draggingPickerTrack) dispatch("commitHistoryTransaction");
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
@@ -413,6 +415,10 @@
|
||||
setOldHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
|
||||
}
|
||||
|
||||
export function div(): HTMLDivElement | undefined {
|
||||
return self?.div();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
removeEvents();
|
||||
});
|
||||
@@ -705,6 +711,7 @@
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-picker {
|
||||
--widget-height: 24px;
|
||||
--picker-size: 256px;
|
||||
--picker-circle-radius: 6px;
|
||||
|
||||
|
||||
@@ -199,7 +199,8 @@
|
||||
}
|
||||
|
||||
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
|
||||
if (!inParentFloatingMenu) {
|
||||
const noPosition = Boolean(floatingMenuContainer.closest("[data-floating-menu-no-position]"));
|
||||
if (!inParentFloatingMenu && !noPosition) {
|
||||
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
|
||||
// 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
|
||||
let tailOffset = 0;
|
||||
@@ -322,7 +323,7 @@
|
||||
|
||||
// POINTER STRAY
|
||||
// Close the floating menu if the pointer has strayed far enough from its bounds (and it's not hovering over its own spawner)
|
||||
const notHoveringOverOwnSpawner = ownSpawner !== targetSpawner;
|
||||
const notHoveringOverOwnSpawner = ownSpawner !== targetSpawner || (ownSpawner === undefined && targetSpawner === undefined);
|
||||
if (strayCloses && notHoveringOverOwnSpawner && isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) {
|
||||
// TODO: Extend this rectangle bounds check to all submenu bounds up the DOM tree since currently submenus disappear
|
||||
// TODO: with zero stray distance if the cursor is further than the stray distance from only the top-level menu
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import {
|
||||
type MenuDirection,
|
||||
type MouseCursorIcon,
|
||||
type XY,
|
||||
Color,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
@@ -14,6 +16,7 @@
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateGradientStopColorPickerPosition,
|
||||
UpdateMouseCursor,
|
||||
isWidgetSpanRow,
|
||||
} from "@graphite/messages";
|
||||
@@ -24,6 +27,7 @@
|
||||
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
@@ -35,6 +39,7 @@
|
||||
let rulerHorizontal: RulerInput | undefined;
|
||||
let rulerVertical: RulerInput | undefined;
|
||||
let viewport: HTMLDivElement | undefined;
|
||||
let gradientStopPicker: ColorPicker | undefined;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
@@ -75,6 +80,10 @@
|
||||
let cursorEyedropperPreviewColorPrimary = "";
|
||||
let cursorEyedropperPreviewColorSecondary = "";
|
||||
|
||||
// Gradient stop color picker
|
||||
let gradientStopPickerColor: Color | undefined = undefined;
|
||||
let gradientStopPickerPosition: { x: number; y: number } | undefined = undefined;
|
||||
|
||||
// Canvas dimensions
|
||||
let canvasWidth: number | undefined = undefined;
|
||||
let canvasHeight: number | undefined = undefined;
|
||||
@@ -406,6 +415,19 @@
|
||||
// which provides pixel-perfect physical dimensions via devicePixelContentBoxSize
|
||||
}
|
||||
|
||||
function gradientStopPickerDirection(position: XY | undefined, viewport: HTMLDivElement | undefined): MenuDirection {
|
||||
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;
|
||||
const roomBelow = position.y + picker.offsetHeight - viewport.clientHeight;
|
||||
|
||||
// Prefer bottom if there's room
|
||||
if (roomBelow <= 0) return "Bottom";
|
||||
// Otherwise choose the direction with more room
|
||||
return roomRight > roomBelow ? "Bottom" : "Right";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Not compatible with Safari:
|
||||
// <https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#browser_compatibility>
|
||||
@@ -441,6 +463,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Gradient stop color picker
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGradientStopColorPickerPosition, (data) => {
|
||||
gradientStopPickerColor = data.color;
|
||||
gradientStopPickerPosition = { x: data.x, y: data.y };
|
||||
});
|
||||
|
||||
// Update scrollbars and rulers
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (data) => {
|
||||
await tick();
|
||||
@@ -564,6 +592,34 @@
|
||||
y={cursorTop}
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
style:left={gradientStopPickerPosition ? `${gradientStopPickerPosition?.x}px` : undefined}
|
||||
style:top={gradientStopPickerPosition ? `${gradientStopPickerPosition?.y}px` : undefined}
|
||||
style:position="absolute"
|
||||
data-floating-menu-no-position
|
||||
>
|
||||
<div data-floating-menu-spawner></div>
|
||||
<ColorPicker
|
||||
direction={gradientStopPickerDirection(gradientStopPickerPosition, viewport)}
|
||||
open={Boolean(gradientStopPickerPosition && gradientStopPickerColor)}
|
||||
on:open={({ detail }) => {
|
||||
if (!detail) {
|
||||
editor.handle.closeGradientStopColorPicker();
|
||||
gradientStopPickerPosition = undefined;
|
||||
gradientStopPickerColor = undefined;
|
||||
}
|
||||
}}
|
||||
colorOrGradient={gradientStopPickerColor || new Color()}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
if (detail instanceof Color) {
|
||||
editor.handle.updateGradientStopColor(detail.red, detail.green, detail.blue, detail.alpha);
|
||||
}
|
||||
}}
|
||||
on:startHistoryTransaction={() => editor.handle.startGradientStopColorTransaction()}
|
||||
on:commitHistoryTransaction={() => editor.handle.commitGradientStopColorTransaction()}
|
||||
bind:this={gradientStopPicker}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class:viewport={!$appWindow.viewportHolePunch}
|
||||
class:viewport-transparent={$appWindow.viewportHolePunch}
|
||||
|
||||
@@ -32,11 +32,7 @@
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
<!-- {#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if} -->
|
||||
</button>
|
||||
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
{disabled}
|
||||
|
||||
Reference in New Issue
Block a user