mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +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_delete(!self.disabled)
|
||||
.allow_reorder(true)
|
||||
.allow_select(true)
|
||||
.disabled(self.disabled)
|
||||
.on_update(|update: &SpectrumInputUpdate| ColorPickerMessage::GradientUpdate { update: update.clone() }.into())
|
||||
.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).
|
||||
#[serde(rename = "allowDelete")]
|
||||
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")]
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
export let allowInsert = true;
|
||||
export let allowDelete = true;
|
||||
export let allowReorder = true;
|
||||
export let allowSelect = false;
|
||||
export let narrow = false;
|
||||
export let rangeSlider = 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`
|
||||
// that would otherwise race the structural change before Rust has reported the dragged marker's new index.
|
||||
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) {
|
||||
dispatch("update", intent);
|
||||
}
|
||||
|
||||
function setActive(index: number | undefined, isMidpoint: boolean) {
|
||||
dragIndex = index;
|
||||
dragIsMidpoint = isMidpoint;
|
||||
if (!allowSelect) return;
|
||||
|
||||
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 {
|
||||
const rect = markerTrackElement?.div()?.getBoundingClientRect();
|
||||
if (!rect) return undefined;
|
||||
@@ -173,6 +197,8 @@
|
||||
duplicateRequested = 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.
|
||||
dragIndex = insertIndex;
|
||||
dragIsMidpoint = false;
|
||||
activeMarkerIndex = insertIndex;
|
||||
activeMarkerIsMidpoint = false;
|
||||
addEvents();
|
||||
@@ -208,7 +234,7 @@
|
||||
let bestDistance = DUPLICATE_POSITION_EPSILON;
|
||||
|
||||
markers.forEach((marker, index) => {
|
||||
if (index === activeMarkerIndex) return;
|
||||
if (index === dragIndex) return;
|
||||
|
||||
const distance = Math.abs(marker.position - startPosition);
|
||||
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.
|
||||
// Returns whether a structural change was emitted, so callers can skip the next move that would race it.
|
||||
function reconcileDuplicate(): boolean {
|
||||
if (!allowInsert || activeMarkerIndex === undefined || activeMarkerIsMidpoint) return false;
|
||||
if (!allowInsert || dragIndex === undefined || dragIsMidpoint) return false;
|
||||
|
||||
if (duplicateRequested && !duplicateActive) {
|
||||
// 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;
|
||||
|
||||
emit({ InsertDuplicate: { index: activeMarkerIndex, position: dragRestorePosition } });
|
||||
emit({ InsertDuplicate: { index: dragIndex, position: dragRestorePosition } });
|
||||
duplicateActive = true;
|
||||
|
||||
return true;
|
||||
@@ -250,7 +276,7 @@
|
||||
}
|
||||
|
||||
function moveActiveMarker(e: PointerEvent) {
|
||||
if (disabled || activeMarkerIndex === undefined) return;
|
||||
if (disabled || dragIndex === undefined) return;
|
||||
if (e.buttons === 0) {
|
||||
endDrag();
|
||||
return;
|
||||
@@ -271,15 +297,16 @@
|
||||
|
||||
let position = pointerPosition(e);
|
||||
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;
|
||||
if (!dragInsertedMarker) dispatch("dragging", true);
|
||||
emit({ MoveMarker: { index: activeMarkerIndex, position } });
|
||||
emit({ MoveMarker: { index: dragIndex, position } });
|
||||
}
|
||||
|
||||
function moveActiveMidpoint(e: PointerEvent) {
|
||||
if (disabled || activeMarkerIndex === undefined) return;
|
||||
if (disabled || dragIndex === undefined) return;
|
||||
if (e.buttons === 0) {
|
||||
endDrag();
|
||||
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.
|
||||
// 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);
|
||||
if (absolute === undefined) return;
|
||||
|
||||
const left = markers[activeMarkerIndex]?.position;
|
||||
const right = isWrappedInterval ? markers[0].position + 1 : markers[activeMarkerIndex + 1]?.position;
|
||||
const left = markers[dragIndex]?.position;
|
||||
const right = isWrappedInterval ? markers[0].position + 1 : markers[dragIndex + 1]?.position;
|
||||
if (left === undefined || right === undefined) return;
|
||||
const range = right - left;
|
||||
if (range <= 0) return;
|
||||
@@ -310,13 +337,13 @@
|
||||
|
||||
dragMoved = true;
|
||||
dispatch("dragging", true);
|
||||
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
||||
emit({ MoveMidpoint: { index: dragIndex, position: local / range } });
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
if (disabled || activeMarkerIndex === undefined) return;
|
||||
if (disabled || dragIndex === undefined) return;
|
||||
|
||||
const dragged = activeMarkerIndex;
|
||||
const dragged = dragIndex;
|
||||
const anchor = duplicateActive ? findDuplicateAnchorIndex() : undefined;
|
||||
|
||||
if (dragInsertedMarker) {
|
||||
@@ -327,7 +354,7 @@
|
||||
emit({ DeleteMarker: { index: dragged } });
|
||||
} else if (dragRestorePosition !== undefined) {
|
||||
// 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 } });
|
||||
}
|
||||
|
||||
@@ -349,11 +376,16 @@
|
||||
duplicateRequested = false;
|
||||
duplicateActive = false;
|
||||
skipNextMove = false;
|
||||
// Without selection nothing stays active once the drag ends
|
||||
if (!allowSelect) {
|
||||
dragIndex = undefined;
|
||||
dragIsMidpoint = false;
|
||||
}
|
||||
dispatch("dragging", false);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (activeMarkerIsMidpoint) moveActiveMidpoint(e);
|
||||
if (dragIsMidpoint) moveActiveMidpoint(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
|
||||
// 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).
|
||||
if (e.key === "Alt" && allowInsert && !activeMarkerIsMidpoint && !dragInsertedMarker && !duplicateRequested) {
|
||||
if (e.key === "Alt" && allowInsert && !dragIsMidpoint && !dragInsertedMarker && !duplicateRequested) {
|
||||
duplicateRequested = true;
|
||||
if (reconcileDuplicate()) skipNextMove = true;
|
||||
}
|
||||
@@ -479,9 +511,11 @@
|
||||
{#if marker.position >= 0 && marker.position <= 1}
|
||||
<svg
|
||||
class="marker"
|
||||
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
||||
class:active={highlightedRun !== undefined && index >= highlightedRun[0] && index <= highlightedRun[1]}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.handleColorCSS}
|
||||
on:pointerenter={() => markerPointerEnter(index)}
|
||||
on:pointerleave={() => (hoverRun = undefined)}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
on:dblclick={() => markerDoubleClick(index)}
|
||||
data-gradient-marker
|
||||
|
||||
Reference in New Issue
Block a user