diff --git a/src/index.tsx b/src/index.tsx index 15515083..cd859e6a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -112,6 +112,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile"; import { copyToAppClipboard, getClipboardContent } from "./clipboard"; import { normalizeScroll } from "./scene/data"; import { getCenter, getDistance } from "./gesture"; +import { ScrollBars } from "./scene/types"; import { createUndoAction, createRedoAction } from "./actions/actionHistory"; let { elements } = createScene(); @@ -214,6 +215,8 @@ let cursorX = 0; let cursorY = 0; let isHoldingSpace: boolean = false; let isPanning: boolean = false; +let isDraggingScrollBar: boolean = false; +let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; interface LayerUIProps { actionManager: ActionManager; @@ -1159,12 +1162,9 @@ export class App extends React.Component { isOverHorizontalScrollBar, isOverVerticalScrollBar, } = isOverScrollBars( - elements, - event.clientX / window.devicePixelRatio, - event.clientY / window.devicePixelRatio, - canvasWidth / window.devicePixelRatio, - canvasHeight / window.devicePixelRatio, - this.state, + currentScrollBars, + event.clientX, + event.clientY, ); const { x, y } = viewportCoordsToSceneCoords( @@ -1172,6 +1172,60 @@ export class App extends React.Component { this.state, this.canvas, ); + let lastX = x; + let lastY = y; + + if ( + (isOverHorizontalScrollBar || isOverVerticalScrollBar) && + !this.state.multiElement + ) { + isDraggingScrollBar = true; + lastX = event.clientX; + lastY = event.clientY; + const onPointerMove = (event: PointerEvent) => { + const target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + + if (isOverHorizontalScrollBar) { + const x = event.clientX; + const dx = x - lastX; + this.setState({ + scrollX: normalizeScroll( + this.state.scrollX - dx / this.state.zoom, + ), + }); + lastX = x; + return; + } + + if (isOverVerticalScrollBar) { + const y = event.clientY; + const dy = y - lastY; + this.setState({ + scrollY: normalizeScroll( + this.state.scrollY - dy / this.state.zoom, + ), + }); + lastY = y; + } + }; + + const onPointerUp = () => { + isDraggingScrollBar = false; + setCursorForShape(this.state.elementType); + lastPointerUp = null; + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", onPointerUp); + }; + + lastPointerUp = onPointerUp; + + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); + return; + } const originX = x; const originY = y; @@ -1373,14 +1427,6 @@ export class App extends React.Component { this.setState({ multiElement: null, draggingElement: element }); } - let lastX = x; - let lastY = y; - - if (isOverHorizontalScrollBar || isOverVerticalScrollBar) { - lastX = event.clientX; - lastY = event.clientY; - } - let resizeArrowFn: | (( element: ExcalidrawElement, @@ -2115,10 +2161,27 @@ export class App extends React.Component { gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null; } - if (isHoldingSpace || isPanning) { + if (isHoldingSpace || isPanning || isDraggingScrollBar) { return; } - const hasDeselectedButton = Boolean(event.buttons); + + const { + isOverHorizontalScrollBar, + isOverVerticalScrollBar, + } = isOverScrollBars( + currentScrollBars, + event.clientX, + event.clientY, + ); + const isOverScrollBar = + isOverVerticalScrollBar || isOverHorizontalScrollBar; + if (!this.state.draggingElement && !this.state.multiElement) { + if (isOverScrollBar) { + resetCursor(); + } else { + setCursorForShape(this.state.elementType); + } + } const { x, y } = viewportCoordsToSceneCoords( event, @@ -2138,6 +2201,7 @@ export class App extends React.Component { return; } + const hasDeselectedButton = Boolean(event.buttons); if ( hasDeselectedButton || this.state.elementType !== "selection" @@ -2146,7 +2210,7 @@ export class App extends React.Component { } const selectedElements = getSelectedElements(elements); - if (selectedElements.length === 1) { + if (selectedElements.length === 1 && !isOverScrollBar) { const resizeElement = getElementWithResizeHandler( elements, { x, y }, @@ -2166,7 +2230,8 @@ export class App extends React.Component { y, this.state.zoom, ); - document.documentElement.style.cursor = hitElement ? "move" : ""; + document.documentElement.style.cursor = + hitElement && !isOverScrollBar ? "move" : ""; }} onPointerUp={this.removePointer} onPointerLeave={this.removePointer} @@ -2279,7 +2344,7 @@ export class App extends React.Component { }, 300); componentDidUpdate() { - const atLeastOneVisibleElement = renderScene( + const { atLeastOneVisibleElement, scrollBars } = renderScene( elements, this.state.selectionElement, this.rc!, @@ -2294,6 +2359,9 @@ export class App extends React.Component { renderOptimizations: true, }, ); + if (scrollBars) { + currentScrollBars = scrollBars; + } const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0; if (this.state.scrolledOutside !== scrolledOutside) { this.setState({ scrolledOutside: scrolledOutside }); diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 16047c8a..ac1b16bd 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -36,9 +36,9 @@ export function renderScene( renderSelection?: boolean; renderOptimizations?: boolean; } = {}, -): boolean { +) { if (!canvas) { - return false; + return { atLeastOneVisibleElement: false }; } const context = canvas.getContext("2d")!; @@ -196,9 +196,10 @@ export function renderScene( } }); context.restore(); + return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; } - return visibleElements.length > 0; + return { atLeastOneVisibleElement: visibleElements.length > 0 }; } function isVisibleElement( diff --git a/src/scene/scrollbars.ts b/src/scene/scrollbars.ts index 2528a1ea..6af637f3 100644 --- a/src/scene/scrollbars.ts +++ b/src/scene/scrollbars.ts @@ -1,6 +1,7 @@ import { ExcalidrawElement } from "../element/types"; import { getCommonBounds } from "../element"; import { FlooredNumber } from "../types"; +import { ScrollBars } from "./types"; const SCROLLBAR_MARGIN = 4; export const SCROLLBAR_WIDTH = 6; @@ -19,7 +20,7 @@ export function getScrollBars( scrollY: FlooredNumber; zoom: number; }, -) { +): ScrollBars { // This is the bounding box of all the elements const [ elementsMinX, @@ -83,28 +84,7 @@ export function getScrollBars( }; } -export function isOverScrollBars( - elements: readonly ExcalidrawElement[], - x: number, - y: number, - viewportWidth: number, - viewportHeight: number, - { - scrollX, - scrollY, - zoom, - }: { - scrollX: FlooredNumber; - scrollY: FlooredNumber; - zoom: number; - }, -) { - const scrollBars = getScrollBars(elements, viewportWidth, viewportHeight, { - scrollX, - scrollY, - zoom, - }); - +export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) { const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [ scrollBars.horizontal, scrollBars.vertical, diff --git a/src/scene/types.ts b/src/scene/types.ts index a1123e03..e744e6cb 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -19,3 +19,18 @@ export interface Scene { } export type ExportType = "png" | "clipboard" | "backend" | "svg"; + +export type ScrollBars = { + horizontal: { + x: number; + y: number; + width: number; + height: number; + } | null; + vertical: { + x: number; + y: number; + width: number; + height: number; + } | null; +};