mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 10:18:12 +08:00
Add a selection mode to the spectrum widget so only the gradient editor supports having a highlighted stop (#4533)
* Add a selection mode to the spectrum widget so only the gradient editor keeps a highlighted stop * Record the drag mode for every drag and require selection for reordering so a non-selecting widget cannot drag the wrong stop
This commit is contained in:
@@ -529,6 +529,7 @@ impl ColorPickerMessageHandler {
|
|||||||
.allow_insert(!self.disabled)
|
.allow_insert(!self.disabled)
|
||||||
.allow_delete(!self.disabled)
|
.allow_delete(!self.disabled)
|
||||||
.allow_reorder(true)
|
.allow_reorder(true)
|
||||||
|
.allow_select(true)
|
||||||
.disabled(self.disabled)
|
.disabled(self.disabled)
|
||||||
.on_update(|update: &SpectrumInputUpdate| ColorPickerMessage::GradientUpdate { update: update.clone() }.into())
|
.on_update(|update: &SpectrumInputUpdate| ColorPickerMessage::GradientUpdate { update: update.clone() }.into())
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
|
|||||||
@@ -668,9 +668,13 @@ pub struct SpectrumInput {
|
|||||||
/// Whether right-click or pressing Delete removes a marker. The handler still has the final say on whether the deletion goes through (e.g., enforcing a minimum count).
|
/// Whether right-click or pressing Delete removes a marker. The handler still has the final say on whether the deletion goes through (e.g., enforcing a minimum count).
|
||||||
#[serde(rename = "allowDelete")]
|
#[serde(rename = "allowDelete")]
|
||||||
pub allow_delete: bool,
|
pub allow_delete: bool,
|
||||||
/// Whether dragging a marker past another reorders them. If false, the dragged marker is clamped between its neighbors.
|
/// Whether dragging a marker past another reorders them, which also needs `allow_select`. Otherwise the dragged marker is clamped between its neighbors.
|
||||||
#[serde(rename = "allowReorder")]
|
#[serde(rename = "allowReorder")]
|
||||||
pub allow_reorder: bool,
|
pub allow_reorder: bool,
|
||||||
|
/// Whether clicking a marker selects it, keeping it highlighted and reported as the active marker until another is chosen,
|
||||||
|
/// as a gradient editor needs for the stop being edited. Otherwise the highlight only follows the pointer and the drag.
|
||||||
|
#[serde(rename = "allowSelect")]
|
||||||
|
pub allow_select: bool,
|
||||||
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
|
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
|
||||||
pub narrow: bool,
|
pub narrow: bool,
|
||||||
/// Plain range-slider mode, for a number beside its number input: a flat 4px track is drawn in place of the gradient, so `track` is never shown.
|
/// Plain range-slider mode, for a number beside its number input: a flat 4px track is drawn in place of the gradient, so `track` is never shown.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
export let allowInsert = true;
|
export let allowInsert = true;
|
||||||
export let allowDelete = true;
|
export let allowDelete = true;
|
||||||
export let allowReorder = true;
|
export let allowReorder = true;
|
||||||
|
export let allowSelect = false;
|
||||||
export let narrow = false;
|
export let narrow = false;
|
||||||
export let rangeSlider = false;
|
export let rangeSlider = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
@@ -49,17 +50,40 @@
|
|||||||
// Set when a key-triggered reconcile inserts/removes the frozen copy, so the next pointer move skips emitting a `MoveMarker`
|
// Set when a key-triggered reconcile inserts/removes the frozen copy, so the next pointer move skips emitting a `MoveMarker`
|
||||||
// that would otherwise race the structural change before Rust has reported the dragged marker's new index.
|
// that would otherwise race the structural change before Rust has reported the dragged marker's new index.
|
||||||
let skipNextMove = false;
|
let skipNextMove = false;
|
||||||
|
// The hovered marker, highlighted ahead of the drag.
|
||||||
|
let hoverRun: [number, number] | undefined = undefined;
|
||||||
|
// The marker being dragged, or the left marker of the interval when a midpoint is dragged, and which of the two it is.
|
||||||
|
// Where selecting is allowed, these follow the selection, which Rust renumbers across structural changes.
|
||||||
|
let dragIndex: number | undefined = undefined;
|
||||||
|
let dragIsMidpoint = false;
|
||||||
|
$: if (allowSelect) {
|
||||||
|
dragIndex = activeMarkerIndex;
|
||||||
|
dragIsMidpoint = activeMarkerIsMidpoint;
|
||||||
|
}
|
||||||
|
// The marker highlighted: the one hovered, else the one being dragged (or selected, where selecting is allowed).
|
||||||
|
let highlightedRun: [number, number] | undefined;
|
||||||
|
$: highlightedRun = hoverRun !== undefined ? hoverRun : typeof dragIndex === "number" && !dragIsMidpoint ? [dragIndex, dragIndex] : undefined;
|
||||||
|
|
||||||
function emit(intent: SpectrumInputUpdate) {
|
function emit(intent: SpectrumInputUpdate) {
|
||||||
dispatch("update", intent);
|
dispatch("update", intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActive(index: number | undefined, isMidpoint: boolean) {
|
function setActive(index: number | undefined, isMidpoint: boolean) {
|
||||||
|
dragIndex = index;
|
||||||
|
dragIsMidpoint = isMidpoint;
|
||||||
|
if (!allowSelect) return;
|
||||||
|
|
||||||
activeMarkerIndex = index;
|
activeMarkerIndex = index;
|
||||||
activeMarkerIsMidpoint = isMidpoint;
|
activeMarkerIsMidpoint = isMidpoint;
|
||||||
emit({ ActiveMarker: { activeMarkerIndex: index, activeMarkerIsMidpoint: isMidpoint } });
|
emit({ ActiveMarker: { activeMarkerIndex: index, activeMarkerIsMidpoint: isMidpoint } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hovering highlights what a drag would carry, except where selecting is allowed and hover keeps its lighter tint
|
||||||
|
function markerPointerEnter(index: number) {
|
||||||
|
if (allowSelect) return;
|
||||||
|
hoverRun = [index, index];
|
||||||
|
}
|
||||||
|
|
||||||
function pointerPosition(e: MouseEvent, clamp = true): number | undefined {
|
function pointerPosition(e: MouseEvent, clamp = true): number | undefined {
|
||||||
const rect = markerTrackElement?.div()?.getBoundingClientRect();
|
const rect = markerTrackElement?.div()?.getBoundingClientRect();
|
||||||
if (!rect) return undefined;
|
if (!rect) return undefined;
|
||||||
@@ -173,6 +197,8 @@
|
|||||||
duplicateRequested = false;
|
duplicateRequested = false;
|
||||||
duplicateActive = false;
|
duplicateActive = false;
|
||||||
// Don't dispatch an `ActiveMarker` here. The Rust handler already updates the active marker in response to `InsertMarker` and a duplicate `ActiveMarker` would race the layout update.
|
// Don't dispatch an `ActiveMarker` here. The Rust handler already updates the active marker in response to `InsertMarker` and a duplicate `ActiveMarker` would race the layout update.
|
||||||
|
dragIndex = insertIndex;
|
||||||
|
dragIsMidpoint = false;
|
||||||
activeMarkerIndex = insertIndex;
|
activeMarkerIndex = insertIndex;
|
||||||
activeMarkerIsMidpoint = false;
|
activeMarkerIsMidpoint = false;
|
||||||
addEvents();
|
addEvents();
|
||||||
@@ -208,7 +234,7 @@
|
|||||||
let bestDistance = DUPLICATE_POSITION_EPSILON;
|
let bestDistance = DUPLICATE_POSITION_EPSILON;
|
||||||
|
|
||||||
markers.forEach((marker, index) => {
|
markers.forEach((marker, index) => {
|
||||||
if (index === activeMarkerIndex) return;
|
if (index === dragIndex) return;
|
||||||
|
|
||||||
const distance = Math.abs(marker.position - startPosition);
|
const distance = Math.abs(marker.position - startPosition);
|
||||||
if (distance < bestDistance) {
|
if (distance < bestDistance) {
|
||||||
@@ -223,14 +249,14 @@
|
|||||||
// Bring the materialized duplicate state in line with whether Alt is currently held, inserting or removing the frozen copy.
|
// Bring the materialized duplicate state in line with whether Alt is currently held, inserting or removing the frozen copy.
|
||||||
// Returns whether a structural change was emitted, so callers can skip the next move that would race it.
|
// Returns whether a structural change was emitted, so callers can skip the next move that would race it.
|
||||||
function reconcileDuplicate(): boolean {
|
function reconcileDuplicate(): boolean {
|
||||||
if (!allowInsert || activeMarkerIndex === undefined || activeMarkerIsMidpoint) return false;
|
if (!allowInsert || dragIndex === undefined || dragIsMidpoint) return false;
|
||||||
|
|
||||||
if (duplicateRequested && !duplicateActive) {
|
if (duplicateRequested && !duplicateActive) {
|
||||||
// Drop a frozen copy at the drag's start position. The dragged marker stays active and becomes the duplicate being moved.
|
// Drop a frozen copy at the drag's start position. The dragged marker stays active and becomes the duplicate being moved.
|
||||||
|
|
||||||
if (dragRestorePosition === undefined) return false;
|
if (dragRestorePosition === undefined) return false;
|
||||||
|
|
||||||
emit({ InsertDuplicate: { index: activeMarkerIndex, position: dragRestorePosition } });
|
emit({ InsertDuplicate: { index: dragIndex, position: dragRestorePosition } });
|
||||||
duplicateActive = true;
|
duplicateActive = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -250,7 +276,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function moveActiveMarker(e: PointerEvent) {
|
function moveActiveMarker(e: PointerEvent) {
|
||||||
if (disabled || activeMarkerIndex === undefined) return;
|
if (disabled || dragIndex === undefined) return;
|
||||||
if (e.buttons === 0) {
|
if (e.buttons === 0) {
|
||||||
endDrag();
|
endDrag();
|
||||||
return;
|
return;
|
||||||
@@ -271,15 +297,16 @@
|
|||||||
|
|
||||||
let position = pointerPosition(e);
|
let position = pointerPosition(e);
|
||||||
if (position === undefined) return;
|
if (position === undefined) return;
|
||||||
if (!allowReorder) position = clampToNeighbors(activeMarkerIndex, position);
|
// Without selection nothing reports the dragged marker's new index after a reorder, so it stays between its neighbors
|
||||||
|
if (!allowReorder || !allowSelect) position = clampToNeighbors(dragIndex, position);
|
||||||
|
|
||||||
dragMoved = true;
|
dragMoved = true;
|
||||||
if (!dragInsertedMarker) dispatch("dragging", true);
|
if (!dragInsertedMarker) dispatch("dragging", true);
|
||||||
emit({ MoveMarker: { index: activeMarkerIndex, position } });
|
emit({ MoveMarker: { index: dragIndex, position } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveActiveMidpoint(e: PointerEvent) {
|
function moveActiveMidpoint(e: PointerEvent) {
|
||||||
if (disabled || activeMarkerIndex === undefined) return;
|
if (disabled || dragIndex === undefined) return;
|
||||||
if (e.buttons === 0) {
|
if (e.buttons === 0) {
|
||||||
endDrag();
|
endDrag();
|
||||||
return;
|
return;
|
||||||
@@ -287,12 +314,12 @@
|
|||||||
|
|
||||||
// The wrapped interval's diamond (cyclic only) belongs to the last marker and spans through the 1|0 boundary to the first.
|
// The wrapped interval's diamond (cyclic only) belongs to the last marker and spans through the 1|0 boundary to the first.
|
||||||
// Its pointer ratio stays unclamped so overdragging past the strip's right or left edge keeps tracking after the 1|0 wrap.
|
// Its pointer ratio stays unclamped so overdragging past the strip's right or left edge keeps tracking after the 1|0 wrap.
|
||||||
const isWrappedInterval = trackCyclic && activeMarkerIndex === markers.length - 1;
|
const isWrappedInterval = trackCyclic && dragIndex === markers.length - 1;
|
||||||
const absolute = pointerPosition(e, !isWrappedInterval);
|
const absolute = pointerPosition(e, !isWrappedInterval);
|
||||||
if (absolute === undefined) return;
|
if (absolute === undefined) return;
|
||||||
|
|
||||||
const left = markers[activeMarkerIndex]?.position;
|
const left = markers[dragIndex]?.position;
|
||||||
const right = isWrappedInterval ? markers[0].position + 1 : markers[activeMarkerIndex + 1]?.position;
|
const right = isWrappedInterval ? markers[0].position + 1 : markers[dragIndex + 1]?.position;
|
||||||
if (left === undefined || right === undefined) return;
|
if (left === undefined || right === undefined) return;
|
||||||
const range = right - left;
|
const range = right - left;
|
||||||
if (range <= 0) return;
|
if (range <= 0) return;
|
||||||
@@ -310,13 +337,13 @@
|
|||||||
|
|
||||||
dragMoved = true;
|
dragMoved = true;
|
||||||
dispatch("dragging", true);
|
dispatch("dragging", true);
|
||||||
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
emit({ MoveMidpoint: { index: dragIndex, position: local / range } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function abortDrag() {
|
function abortDrag() {
|
||||||
if (disabled || activeMarkerIndex === undefined) return;
|
if (disabled || dragIndex === undefined) return;
|
||||||
|
|
||||||
const dragged = activeMarkerIndex;
|
const dragged = dragIndex;
|
||||||
const anchor = duplicateActive ? findDuplicateAnchorIndex() : undefined;
|
const anchor = duplicateActive ? findDuplicateAnchorIndex() : undefined;
|
||||||
|
|
||||||
if (dragInsertedMarker) {
|
if (dragInsertedMarker) {
|
||||||
@@ -327,7 +354,7 @@
|
|||||||
emit({ DeleteMarker: { index: dragged } });
|
emit({ DeleteMarker: { index: dragged } });
|
||||||
} else if (dragRestorePosition !== undefined) {
|
} else if (dragRestorePosition !== undefined) {
|
||||||
// Plain drag: return the marker (or midpoint) to where it began.
|
// Plain drag: return the marker (or midpoint) to where it began.
|
||||||
if (activeMarkerIsMidpoint) emit({ MoveMidpoint: { index: dragged, position: dragRestorePosition } });
|
if (dragIsMidpoint) emit({ MoveMidpoint: { index: dragged, position: dragRestorePosition } });
|
||||||
else emit({ MoveMarker: { index: dragged, position: dragRestorePosition } });
|
else emit({ MoveMarker: { index: dragged, position: dragRestorePosition } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,11 +376,16 @@
|
|||||||
duplicateRequested = false;
|
duplicateRequested = false;
|
||||||
duplicateActive = false;
|
duplicateActive = false;
|
||||||
skipNextMove = false;
|
skipNextMove = false;
|
||||||
|
// Without selection nothing stays active once the drag ends
|
||||||
|
if (!allowSelect) {
|
||||||
|
dragIndex = undefined;
|
||||||
|
dragIsMidpoint = false;
|
||||||
|
}
|
||||||
dispatch("dragging", false);
|
dispatch("dragging", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerMove(e: PointerEvent) {
|
function onPointerMove(e: PointerEvent) {
|
||||||
if (activeMarkerIsMidpoint) moveActiveMidpoint(e);
|
if (dragIsMidpoint) moveActiveMidpoint(e);
|
||||||
else moveActiveMarker(e);
|
else moveActiveMarker(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +409,7 @@
|
|||||||
// Pressing Alt mid-drag duplicates the marker, leaving a frozen copy where the drag began. Reconcile immediately for instant
|
// Pressing Alt mid-drag duplicates the marker, leaving a frozen copy where the drag began. Reconcile immediately for instant
|
||||||
// feedback, and arm a skip so the next pointer move doesn't race the just-emitted structural change. Only when dragging an
|
// feedback, and arm a skip so the next pointer move doesn't race the just-emitted structural change. Only when dragging an
|
||||||
// existing stop (not one being created by this drag).
|
// existing stop (not one being created by this drag).
|
||||||
if (e.key === "Alt" && allowInsert && !activeMarkerIsMidpoint && !dragInsertedMarker && !duplicateRequested) {
|
if (e.key === "Alt" && allowInsert && !dragIsMidpoint && !dragInsertedMarker && !duplicateRequested) {
|
||||||
duplicateRequested = true;
|
duplicateRequested = true;
|
||||||
if (reconcileDuplicate()) skipNextMove = true;
|
if (reconcileDuplicate()) skipNextMove = true;
|
||||||
}
|
}
|
||||||
@@ -479,9 +511,11 @@
|
|||||||
{#if marker.position >= 0 && marker.position <= 1}
|
{#if marker.position >= 0 && marker.position <= 1}
|
||||||
<svg
|
<svg
|
||||||
class="marker"
|
class="marker"
|
||||||
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
class:active={highlightedRun !== undefined && index >= highlightedRun[0] && index <= highlightedRun[1]}
|
||||||
style:--marker-position={marker.position}
|
style:--marker-position={marker.position}
|
||||||
style:--marker-color={marker.handleColorCSS}
|
style:--marker-color={marker.handleColorCSS}
|
||||||
|
on:pointerenter={() => markerPointerEnter(index)}
|
||||||
|
on:pointerleave={() => (hoverRun = undefined)}
|
||||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||||
on:dblclick={() => markerDoubleClick(index)}
|
on:dblclick={() => markerDoubleClick(index)}
|
||||||
data-gradient-marker
|
data-gradient-marker
|
||||||
|
|||||||
Reference in New Issue
Block a user