-
Notifications
You must be signed in to change notification settings - Fork 323
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into wip/sb/keyboard-shortcuts-settings
- Loading branch information
Showing
18 changed files
with
810 additions
and
95 deletions.
There are no files selected for viewing
184 changes: 184 additions & 0 deletions
184
app/ide-desktop/lib/dashboard/src/components/SelectionBrush.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
/** @file A selection brush to indicate the area being selected by the mouse drag action. */ | ||
import * as React from 'react' | ||
|
||
import * as reactDom from 'react-dom' | ||
|
||
import * as animationHooks from '#/hooks/animationHooks' | ||
|
||
import * as modalProvider from '#/providers/ModalProvider' | ||
|
||
import type * as geometry from '#/utilities/geometry' | ||
|
||
// ====================== | ||
// === SelectionBrush === | ||
// ====================== | ||
|
||
/** Props for a {@link SelectionBrush}. */ | ||
export interface SelectionBrushProps { | ||
readonly onDrag: (rectangle: geometry.DetailedRectangle, event: MouseEvent) => void | ||
readonly onDragEnd: (event: MouseEvent) => void | ||
readonly onDragCancel: () => void | ||
} | ||
|
||
/** A selection brush to indicate the area being selected by the mouse drag action. */ | ||
export default function SelectionBrush(props: SelectionBrushProps) { | ||
const { onDrag, onDragEnd, onDragCancel } = props | ||
const { modalRef } = modalProvider.useModalRef() | ||
const isMouseDownRef = React.useRef(false) | ||
const didMoveWhileDraggingRef = React.useRef(false) | ||
const onDragRef = React.useRef(onDrag) | ||
const onDragEndRef = React.useRef(onDragEnd) | ||
const onDragCancelRef = React.useRef(onDragCancel) | ||
const lastMouseEvent = React.useRef<MouseEvent | null>(null) | ||
const [anchor, setAnchor] = React.useState<geometry.Coordinate2D | null>(null) | ||
// This will be `null` if `anchor` is `null`. | ||
const [position, setPosition] = React.useState<geometry.Coordinate2D | null>(null) | ||
const [lastSetAnchor, setLastSetAnchor] = React.useState<geometry.Coordinate2D | null>(null) | ||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
const anchorAnimFactor = animationHooks.useApproach(anchor != null ? 1 : 0, 60) | ||
const hidden = | ||
anchor == null || | ||
position == null || | ||
(anchor.left === position.left && anchor.top === position.top) | ||
|
||
React.useEffect(() => { | ||
onDragRef.current = onDrag | ||
}, [onDrag]) | ||
|
||
React.useEffect(() => { | ||
onDragEndRef.current = onDragEnd | ||
}, [onDragEnd]) | ||
|
||
React.useEffect(() => { | ||
onDragCancelRef.current = onDragCancel | ||
}, [onDragCancel]) | ||
|
||
React.useEffect(() => { | ||
if (anchor != null) { | ||
anchorAnimFactor.skip() | ||
} | ||
}, [anchorAnimFactor, anchor]) | ||
|
||
React.useEffect(() => { | ||
const onMouseDown = (event: MouseEvent) => { | ||
if ( | ||
modalRef.current == null && | ||
!(event.target instanceof HTMLInputElement) && | ||
!(event.target instanceof HTMLTextAreaElement) && | ||
(!(event.target instanceof HTMLElement) || !event.target.isContentEditable) && | ||
!(event.target instanceof HTMLButtonElement) && | ||
!(event.target instanceof HTMLAnchorElement) | ||
) { | ||
isMouseDownRef.current = true | ||
didMoveWhileDraggingRef.current = false | ||
lastMouseEvent.current = event | ||
const newAnchor = { left: event.pageX, top: event.pageY } | ||
setAnchor(newAnchor) | ||
setLastSetAnchor(newAnchor) | ||
setPosition(newAnchor) | ||
} | ||
} | ||
const onMouseUp = (event: MouseEvent) => { | ||
if (didMoveWhileDraggingRef.current) { | ||
onDragEndRef.current(event) | ||
} | ||
// The `setTimeout` is required, otherwise the values are changed before the `onClick` handler | ||
// is executed. | ||
window.setTimeout(() => { | ||
isMouseDownRef.current = false | ||
didMoveWhileDraggingRef.current = false | ||
}) | ||
setAnchor(null) | ||
} | ||
const onMouseMove = (event: MouseEvent) => { | ||
if (!(event.buttons & 1)) { | ||
isMouseDownRef.current = false | ||
} | ||
if (isMouseDownRef.current) { | ||
// Left click is being held. | ||
didMoveWhileDraggingRef.current = true | ||
lastMouseEvent.current = event | ||
setPosition({ left: event.pageX, top: event.pageY }) | ||
} | ||
} | ||
const onClick = (event: MouseEvent) => { | ||
if (isMouseDownRef.current && didMoveWhileDraggingRef.current) { | ||
event.stopImmediatePropagation() | ||
} | ||
} | ||
const onDragStart = () => { | ||
if (isMouseDownRef.current) { | ||
isMouseDownRef.current = false | ||
onDragCancelRef.current() | ||
setAnchor(null) | ||
} | ||
} | ||
document.addEventListener('mousedown', onMouseDown) | ||
document.addEventListener('mouseup', onMouseUp) | ||
document.addEventListener('dragstart', onDragStart, { capture: true }) | ||
document.addEventListener('mousemove', onMouseMove) | ||
document.addEventListener('click', onClick) | ||
return () => { | ||
document.removeEventListener('mousedown', onMouseDown) | ||
document.removeEventListener('mouseup', onMouseUp) | ||
document.removeEventListener('dragstart', onDragStart, { capture: true }) | ||
document.removeEventListener('mousemove', onMouseMove) | ||
document.removeEventListener('click', onClick) | ||
} | ||
}, [/* should never change */ modalRef]) | ||
|
||
const rectangle = React.useMemo(() => { | ||
if (position != null && lastSetAnchor != null) { | ||
const start: geometry.Coordinate2D = { | ||
left: | ||
position.left * (1 - anchorAnimFactor.value) + | ||
lastSetAnchor.left * anchorAnimFactor.value, | ||
top: | ||
position.top * (1 - anchorAnimFactor.value) + lastSetAnchor.top * anchorAnimFactor.value, | ||
} | ||
return { | ||
left: Math.min(position.left, start.left), | ||
top: Math.min(position.top, start.top), | ||
right: Math.max(position.left, start.left), | ||
bottom: Math.max(position.top, start.top), | ||
width: Math.abs(position.left - start.left), | ||
height: Math.abs(position.top - start.top), | ||
signedWidth: position.left - start.left, | ||
signedHeight: position.top - start.top, | ||
} | ||
} else { | ||
return null | ||
} | ||
}, [anchorAnimFactor.value, lastSetAnchor, position]) | ||
|
||
const selectionRectangle = React.useMemo(() => (hidden ? null : rectangle), [hidden, rectangle]) | ||
|
||
React.useEffect(() => { | ||
if (selectionRectangle != null && lastMouseEvent.current != null) { | ||
onDrag(selectionRectangle, lastMouseEvent.current) | ||
} | ||
// `onChange` is a callback, not a dependency. | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [selectionRectangle]) | ||
|
||
const brushStyle = | ||
rectangle == null | ||
? {} | ||
: { | ||
left: `${rectangle.left}px`, | ||
top: `${rectangle.top}px`, | ||
width: `${rectangle.width}px`, | ||
height: `${rectangle.height}px`, | ||
} | ||
|
||
return reactDom.createPortal( | ||
<div | ||
className={`fixed bg-selection-brush pointer-events-none box-content rounded-lg border-transparent z-1 transition-border-margin duration-100 ${ | ||
hidden ? 'border-0 m-0' : 'border-6 -m-1.5' | ||
}`} | ||
style={brushStyle} | ||
/>, | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
document.getElementById('enso-dashboard')! | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.