Improve scrollbar-mouse interaction (#667)

* fix scrollbar detection on high devicePixelRatio devices

* don't create a new element on pointerdown over a scrollbar

* Return scrollbars from renderScene and use it in isOverScrollBars

* remove unneeded setState

* show default cursor when hovering or dragging a scrollbar

* disable scrollbars when in multielement mode

Co-authored-by: David Luzar <luzar.david@gmail.com>
pull/864/head
lissitz 5 years ago committed by GitHub
parent 8e0206cc1e
commit e920c078b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -112,6 +112,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
import { copyToAppClipboard, getClipboardContent } from "./clipboard"; import { copyToAppClipboard, getClipboardContent } from "./clipboard";
import { normalizeScroll } from "./scene/data"; import { normalizeScroll } from "./scene/data";
import { getCenter, getDistance } from "./gesture"; import { getCenter, getDistance } from "./gesture";
import { ScrollBars } from "./scene/types";
import { createUndoAction, createRedoAction } from "./actions/actionHistory"; import { createUndoAction, createRedoAction } from "./actions/actionHistory";
let { elements } = createScene(); let { elements } = createScene();
@ -214,6 +215,8 @@ let cursorX = 0;
let cursorY = 0; let cursorY = 0;
let isHoldingSpace: boolean = false; let isHoldingSpace: boolean = false;
let isPanning: boolean = false; let isPanning: boolean = false;
let isDraggingScrollBar: boolean = false;
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -1159,12 +1162,9 @@ export class App extends React.Component<any, AppState> {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar, isOverVerticalScrollBar,
} = isOverScrollBars( } = isOverScrollBars(
elements, currentScrollBars,
event.clientX / window.devicePixelRatio, event.clientX,
event.clientY / window.devicePixelRatio, event.clientY,
canvasWidth / window.devicePixelRatio,
canvasHeight / window.devicePixelRatio,
this.state,
); );
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
@ -1172,6 +1172,60 @@ export class App extends React.Component<any, AppState> {
this.state, this.state,
this.canvas, 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 originX = x;
const originY = y; const originY = y;
@ -1373,14 +1427,6 @@ export class App extends React.Component<any, AppState> {
this.setState({ multiElement: null, draggingElement: element }); this.setState({ multiElement: null, draggingElement: element });
} }
let lastX = x;
let lastY = y;
if (isOverHorizontalScrollBar || isOverVerticalScrollBar) {
lastX = event.clientX;
lastY = event.clientY;
}
let resizeArrowFn: let resizeArrowFn:
| (( | ((
element: ExcalidrawElement, element: ExcalidrawElement,
@ -2115,10 +2161,27 @@ export class App extends React.Component<any, AppState> {
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null; gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
} }
if (isHoldingSpace || isPanning) { if (isHoldingSpace || isPanning || isDraggingScrollBar) {
return; 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( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -2138,6 +2201,7 @@ export class App extends React.Component<any, AppState> {
return; return;
} }
const hasDeselectedButton = Boolean(event.buttons);
if ( if (
hasDeselectedButton || hasDeselectedButton ||
this.state.elementType !== "selection" this.state.elementType !== "selection"
@ -2146,7 +2210,7 @@ export class App extends React.Component<any, AppState> {
} }
const selectedElements = getSelectedElements(elements); const selectedElements = getSelectedElements(elements);
if (selectedElements.length === 1) { if (selectedElements.length === 1 && !isOverScrollBar) {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, elements,
{ x, y }, { x, y },
@ -2166,7 +2230,8 @@ export class App extends React.Component<any, AppState> {
y, y,
this.state.zoom, this.state.zoom,
); );
document.documentElement.style.cursor = hitElement ? "move" : ""; document.documentElement.style.cursor =
hitElement && !isOverScrollBar ? "move" : "";
}} }}
onPointerUp={this.removePointer} onPointerUp={this.removePointer}
onPointerLeave={this.removePointer} onPointerLeave={this.removePointer}
@ -2279,7 +2344,7 @@ export class App extends React.Component<any, AppState> {
}, 300); }, 300);
componentDidUpdate() { componentDidUpdate() {
const atLeastOneVisibleElement = renderScene( const { atLeastOneVisibleElement, scrollBars } = renderScene(
elements, elements,
this.state.selectionElement, this.state.selectionElement,
this.rc!, this.rc!,
@ -2294,6 +2359,9 @@ export class App extends React.Component<any, AppState> {
renderOptimizations: true, renderOptimizations: true,
}, },
); );
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0; const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) { if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside: scrolledOutside }); this.setState({ scrolledOutside: scrolledOutside });

@ -36,9 +36,9 @@ export function renderScene(
renderSelection?: boolean; renderSelection?: boolean;
renderOptimizations?: boolean; renderOptimizations?: boolean;
} = {}, } = {},
): boolean { ) {
if (!canvas) { if (!canvas) {
return false; return { atLeastOneVisibleElement: false };
} }
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
@ -196,9 +196,10 @@ export function renderScene(
} }
}); });
context.restore(); context.restore();
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
} }
return visibleElements.length > 0; return { atLeastOneVisibleElement: visibleElements.length > 0 };
} }
function isVisibleElement( function isVisibleElement(

@ -1,6 +1,7 @@
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element"; import { getCommonBounds } from "../element";
import { FlooredNumber } from "../types"; import { FlooredNumber } from "../types";
import { ScrollBars } from "./types";
const SCROLLBAR_MARGIN = 4; const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_WIDTH = 6;
@ -19,7 +20,7 @@ export function getScrollBars(
scrollY: FlooredNumber; scrollY: FlooredNumber;
zoom: number; zoom: number;
}, },
) { ): ScrollBars {
// This is the bounding box of all the elements // This is the bounding box of all the elements
const [ const [
elementsMinX, elementsMinX,
@ -83,28 +84,7 @@ export function getScrollBars(
}; };
} }
export function isOverScrollBars( export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) {
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,
});
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [ const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal, scrollBars.horizontal,
scrollBars.vertical, scrollBars.vertical,

@ -19,3 +19,18 @@ export interface Scene {
} }
export type ExportType = "png" | "clipboard" | "backend" | "svg"; 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;
};

Loading…
Cancel
Save