mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 13:48:12 +08:00
Polish and add aborting to several input widgets: no Esc closing parent menus; color picker axis align; repeat on arrow buttons (#2276)
* Remove color input outline; reduce antialiasing compositing artifacts in color widgets * Rename ColorButton to ColorInput * Add features and aborting to several other widgets - Prevent Esc from closing parent floating menus when aborting - Fix missing icon regression - Gutter resizing abort - Color picker aborts, Shift axis alignment, improve click/drag behavior for gradient spectrum - Scrollbar abort, repeat when held, fix directional arrows when viewport is zoomed - Number input abort, repeat when held * Move ColorInput into the inputs folder * Fix tiny logo
This commit is contained in:
@@ -3,38 +3,46 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
// Linear Interpolation
|
||||
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a;
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/io-managers/input";
|
||||
|
||||
// Convert the position of the handle (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
|
||||
const handleToTrack = (handleLen: number, handlePos: number): number => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
|
||||
const ARROW_CLICK_DISTANCE = 0.05;
|
||||
const ARROW_REPEAT_DISTANCE = 0.01;
|
||||
|
||||
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
// Convert the position of the thumb (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 thumb length gap of the possible thumb position each side so the end of the thumb doesn't go off the track.
|
||||
const lerp = (a: number, b: number, t: number): number => a * (1 - t) + b * t;
|
||||
const thumbToTrack = (thumbLength: number, thumbPosition: number): number => lerp(thumbLength / 2, 1 - thumbLength / 2, thumbPosition);
|
||||
|
||||
const dispatch = createEventDispatcher<{ handlePosition: number; pressTrack: number; pointerup: undefined }>();
|
||||
const pointerPosition = (e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
|
||||
const clamp01 = (value: number): number => Math.min(Math.max(value, 0), 1);
|
||||
|
||||
const dispatch = createEventDispatcher<{ trackShift: number; thumbPosition: number; thumbDragStart: undefined; thumbDragEnd: undefined; thumbDragAbort: undefined }>();
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let handlePosition = 0.5;
|
||||
export let handleLength = 0.5;
|
||||
export let thumbPosition = 0.5;
|
||||
export let thumbLength = 0.5;
|
||||
|
||||
let scrollTrack: HTMLDivElement | undefined;
|
||||
let dragging = false;
|
||||
let pointerPos = 0;
|
||||
let pressingTrack = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let pointerPositionLastFrame = 0;
|
||||
let thumbTop: string | undefined = undefined;
|
||||
let thumbBottom: string | undefined = undefined;
|
||||
let thumbLeft: string | undefined = undefined;
|
||||
let thumbRight: string | undefined = undefined;
|
||||
|
||||
$: start = handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: end = 1 - handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: start = thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: end = 1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: [thumbTop, thumbBottom, thumbLeft, thumbRight] = direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`];
|
||||
|
||||
function trackLength(): number | undefined {
|
||||
if (scrollTrack === undefined) return undefined;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - handleLength : scrollTrack.clientWidth;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - thumbLength : scrollTrack.clientWidth;
|
||||
}
|
||||
|
||||
function trackOffset(): number | undefined {
|
||||
@@ -42,72 +50,168 @@
|
||||
return direction === "Vertical" ? scrollTrack.getBoundingClientRect().top : scrollTrack.getBoundingClientRect().left;
|
||||
}
|
||||
|
||||
function clampHandlePosition(newPos: number) {
|
||||
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
|
||||
dispatch("handlePosition", clampedPosition);
|
||||
function dragThumb(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
dragging = true;
|
||||
dispatch("thumbDragStart");
|
||||
pointerPositionLastFrame = pointerPosition(e);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function updateHandlePosition(e: PointerEvent) {
|
||||
function pressArrow(direction: number) {
|
||||
const sendMove = () => {
|
||||
if (!pressingArrow) return;
|
||||
|
||||
const distance = afterInitialDelay ? ARROW_REPEAT_DISTANCE : ARROW_CLICK_DISTANCE;
|
||||
dispatch("trackShift", -direction * distance);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_RAPID_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
dispatch("thumbDragStart");
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function pressTrack(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
|
||||
const position = pointerPosition(direction, e);
|
||||
const sendMove = () => {
|
||||
if (!pressingTrack) return;
|
||||
|
||||
clampHandlePosition(handlePosition + (position - pointerPos) / (length * (1 - handleLength)));
|
||||
pointerPos = position;
|
||||
const oldPointer = thumbToTrack(thumbLength, thumbPosition) * length + offset;
|
||||
const newPointer = pointerPosition(e);
|
||||
|
||||
// Check if the thumb has reached the cursor position
|
||||
const proposedThumbPosition = (newPointer - offset) / length;
|
||||
if (proposedThumbPosition >= start && proposedThumbPosition <= 1 - end) {
|
||||
// End pressing the track
|
||||
pressingTrack = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
|
||||
// Begin dragging the thumb
|
||||
dragging = true;
|
||||
pointerPositionLastFrame = newPointer;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const move = newPointer - oldPointer < 0 ? 1 : -1;
|
||||
dispatch("trackShift", move);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
dispatch("thumbDragStart");
|
||||
pressingTrack = true;
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function grabHandle(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
dragging = true;
|
||||
pointerPos = pointerPosition(direction, e);
|
||||
function abortInteraction() {
|
||||
if (pressingTrack || pressingArrow) {
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
}
|
||||
|
||||
function grabArea(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
const length = trackLength();
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
function onPointerUp() {
|
||||
if (dragging) dispatch("thumbDragEnd");
|
||||
|
||||
const oldPointer = handleToTrack(handleLength, handlePosition) * length + offset;
|
||||
const pointerPos = pointerPosition(direction, e);
|
||||
dispatch("pressTrack", pointerPos - oldPointer);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUp() {
|
||||
dragging = false;
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function pointerMove(e: PointerEvent) {
|
||||
if (dragging) updateHandlePosition(e);
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (pressingTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressingArrow) {
|
||||
const target = e.target || undefined;
|
||||
if (!target || !(target instanceof Element)) return;
|
||||
if (!target?.closest?.("[data-scrollbar-arrow]")) {
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
const positionPositionThisFrame = pointerPosition(e);
|
||||
const dragDelta = positionPositionThisFrame - pointerPositionLastFrame;
|
||||
const movement = dragDelta / (length * (1 - thumbLength));
|
||||
const newThumbPosition = clamp01(thumbPosition + movement);
|
||||
dispatch("thumbPosition", newThumbPosition);
|
||||
|
||||
pointerPositionLastFrame = positionPositionThisFrame;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function changePosition(difference: number) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
clampHandlePosition(handlePosition + difference / length);
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortInteraction();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("pointerup", pointerUp);
|
||||
window.addEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") abortInteraction();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("pointerup", pointerUp);
|
||||
window.removeEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function addEvents() {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`} on:pointerup={() => dispatch("pointerup")}>
|
||||
<button class="arrow decrease" on:pointerdown={() => changePosition(-50)} tabindex="-1" />
|
||||
<div class="scroll-track" bind:this={scrollTrack} on:pointerdown={grabArea}>
|
||||
<div class="scroll-thumb" on:pointerdown={grabHandle} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
|
||||
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}>
|
||||
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
</div>
|
||||
<button class="arrow increase" on:click={() => changePosition(50)} tabindex="-1" />
|
||||
<button class="arrow increase" on:pointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
@@ -115,20 +219,66 @@
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
--arrow-color: var(--color-5-dullgray);
|
||||
flex: 0 0 auto;
|
||||
background: none;
|
||||
border: none;
|
||||
border-style: solid;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:hover {
|
||||
--arrow-color: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
--arrow-color: var(--color-c-brightgray);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical .arrow.decrease::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--arrow-color) transparent;
|
||||
}
|
||||
|
||||
&.vertical .arrow.increase::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--arrow-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.decrease::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--arrow-color) transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.increase::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--arrow-color);
|
||||
}
|
||||
|
||||
.scroll-track {
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.scroll-thumb {
|
||||
position: absolute;
|
||||
@@ -140,70 +290,6 @@
|
||||
background: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-click-area {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--color-5-dullgray) transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent var(--color-6-lowergray) transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent var(--color-c-brightgray) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--color-5-dullgray) transparent transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-6-lowergray) transparent transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: var(--color-c-brightgray) transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--color-5-dullgray) transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent var(--color-6-lowergray) transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent var(--color-c-brightgray) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--color-5-dullgray);
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent transparent var(--color-6-lowergray);
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent transparent var(--color-c-brightgray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user