mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-21 00:59:29 +00:00
Improve touch handling (#1098)
* Improve touch handling * Document touch event changes
This commit is contained in:
parent
e1305819bd
commit
b5b7345308
11 changed files with 155 additions and 36 deletions
|
|
@ -3,6 +3,7 @@
|
|||
### Features
|
||||
|
||||
- Support `.ani` cursors in skins [#1035](https://github.com/captbaritone/webamp/pull/1035), [blog post](https://jordaneldredge.com/blog/rendering-animated-ani-cursors-in-the-browser/).
|
||||
- Improved support for mobile/touch screen interaction: volume, balance and position sliders, marquee, playlist and window resizing. [#1098](https://github.com/captbaritone/webamp/pull/1098)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -91,9 +91,7 @@ export function scrollNTracks(n: number): Thunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function scrollPlaylistByDelta(
|
||||
e: React.WheelEvent<HTMLDivElement>
|
||||
): Thunk {
|
||||
export function scrollPlaylistByDelta(e: WheelEvent): Thunk {
|
||||
e.preventDefault();
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function Balance({ style, className, id }: Props) {
|
|||
max="100"
|
||||
step="1"
|
||||
value={balance}
|
||||
style={style}
|
||||
style={{ ...style, touchAction: "none" }}
|
||||
onChange={(e) => setBalance(Number(e.target.value))}
|
||||
onMouseDown={() => setFocus("balance")}
|
||||
onMouseUp={unsetFocus}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import CharacterString from "../CharacterString";
|
|||
import * as Actions from "../../actionCreators";
|
||||
import * as Selectors from "../../selectors";
|
||||
import { useTypedSelector, useActionCreator } from "../../hooks";
|
||||
import * as Utils from "../../utils";
|
||||
|
||||
const SEPARATOR = " *** ";
|
||||
|
||||
|
|
@ -82,8 +83,8 @@ function useDragX() {
|
|||
return;
|
||||
}
|
||||
const xStart = mouseDownX;
|
||||
const handleMouseMove = (ee: MouseEvent) => {
|
||||
const diff = ee.clientX - xStart;
|
||||
const handleMouseMove = (ee: MouseEvent | TouchEvent) => {
|
||||
const diff = Utils.getX(ee) - xStart;
|
||||
setDragOffset(-diff);
|
||||
};
|
||||
|
||||
|
|
@ -94,20 +95,26 @@ function useDragX() {
|
|||
return;
|
||||
}
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseUp", handleMouseUp);
|
||||
document.removeEventListener("touchmove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener("touchend", handleMouseUp);
|
||||
setMouseDownX(null);
|
||||
cleanedUp = true;
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("touchmove", handleMouseMove);
|
||||
document.addEventListener("touseup", handleMouseUp);
|
||||
document.addEventListener("touchend", handleMouseUp);
|
||||
|
||||
return handleMouseUp;
|
||||
}, [mouseDownX]);
|
||||
|
||||
const handleMouseDown = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
setMouseDownX(e.clientX);
|
||||
(
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
): void => {
|
||||
setMouseDownX(Utils.getX(e));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
@ -131,6 +138,7 @@ const Marquee = React.memo(() => {
|
|||
id="marquee"
|
||||
className="text"
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleMouseDown}
|
||||
title="Song Title"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ const Position = memo(() => {
|
|||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
style={{ touchAction: "none" }}
|
||||
value={displayedPosition}
|
||||
onInput={setPosition}
|
||||
onChange={
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ import {
|
|||
interface Props {
|
||||
id: number;
|
||||
index: number;
|
||||
handleMoveClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleMoveClick: (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +53,27 @@ function TrackCell({ children, handleMoveClick, index, id }: Props) {
|
|||
[dispatch, handleMoveClick, index, selected]
|
||||
);
|
||||
|
||||
const handleTouchStart = useCallback(
|
||||
(e) => {
|
||||
if (!selected) {
|
||||
dispatch({ type: CLICKED_TRACK, index });
|
||||
}
|
||||
handleMoveClick(e);
|
||||
|
||||
// There's no touch equivalent of onDoubleClick, so we fake one:
|
||||
function handleSecondTap() {
|
||||
playTrackNow(id);
|
||||
}
|
||||
e.target.addEventListener("touchstart", handleSecondTap);
|
||||
setTimeout(() => {
|
||||
// Technically we might be unmounted here, but that's fine since you
|
||||
// can't tap an unmounted element and we will clean up eventually.
|
||||
e.target.removeEventListener("touchstart", handleSecondTap);
|
||||
}, 250);
|
||||
},
|
||||
[dispatch, handleMoveClick, id, index, playTrackNow, selected]
|
||||
);
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
backgroundColor: selected ? skinPlaylistStyle.selectedbg : undefined,
|
||||
color: current ? skinPlaylistStyle.current : undefined,
|
||||
|
|
@ -61,6 +84,7 @@ function TrackCell({ children, handleMoveClick, index, id }: Props) {
|
|||
style={style}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={onMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onDoubleClick={() => playTrackNow(id)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import { getTimeStr } from "../../utils";
|
||||
import * as Selectors from "../../selectors";
|
||||
import { TRACK_HEIGHT } from "../../constants";
|
||||
import * as Actions from "../../actionCreators";
|
||||
import TrackCell from "./TrackCell";
|
||||
import TrackTitle from "./TrackTitle";
|
||||
import { useTypedSelector, useActionCreator } from "../../hooks";
|
||||
import * as Utils from "../../utils";
|
||||
|
||||
function getNumberLength(number: number): number {
|
||||
return number.toString().length;
|
||||
|
|
@ -22,13 +22,15 @@ function TrackList() {
|
|||
const dragSelected = useActionCreator(Actions.dragSelected);
|
||||
const scrollPlaylistByDelta = useActionCreator(Actions.scrollPlaylistByDelta);
|
||||
|
||||
const [node, setNode] = useState<Element | null>(null);
|
||||
const [node, setNode] = useState<HTMLElement | null>(null);
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [mouseStartY, setMouseStartY] = useState<number | null>(null);
|
||||
|
||||
const _handleMoveClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const _handleMoveClick = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
setMoving(true);
|
||||
setMouseStartY(e.clientY);
|
||||
setMouseStartY(Utils.getY(e));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -37,8 +39,9 @@ function TrackList() {
|
|||
}
|
||||
const { top, bottom, left, right } = node.getBoundingClientRect();
|
||||
let lastDiff = 0;
|
||||
const handleMouseMove = (ee: MouseEvent) => {
|
||||
const { clientY: y, clientX: x } = ee;
|
||||
const handleMouseMove = (ee: MouseEvent | TouchEvent) => {
|
||||
const x = Utils.getX(ee);
|
||||
const y = Utils.getY(ee);
|
||||
if (y < top || y > bottom || x < left || x > right) {
|
||||
// Mouse is outside the track list
|
||||
return;
|
||||
|
|
@ -54,9 +57,13 @@ function TrackList() {
|
|||
const handleMouseUp = () => setMoving(false);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("touchend", handleMouseUp);
|
||||
window.addEventListener("touchmove", handleMouseMove);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("touchmove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
// I'm not 100% sure how well this would work if it rebound mid drag, so
|
||||
// we'll just pretend it's okay that we have stale values in there.
|
||||
|
|
@ -81,13 +88,29 @@ function TrackList() {
|
|||
const maxTrackNumberLength = getNumberLength(numberOfTracks);
|
||||
const paddedTrackNumForIndex = (i: number) =>
|
||||
(i + 1 + offset).toString().padStart(maxTrackNumberLength, "\u00A0");
|
||||
|
||||
useEffect(() => {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chome changed wheel events to be passive be default. We need active (so
|
||||
// we can prevent default) and React does not have a way to control this, so
|
||||
// we must bind our own events.
|
||||
//
|
||||
// https://github.com/facebook/react/issues/14856#issuecomment-806052402
|
||||
node.addEventListener("wheel", scrollPlaylistByDelta, { passive: false });
|
||||
|
||||
return () => {
|
||||
node.removeEventListener("wheel", scrollPlaylistByDelta);
|
||||
};
|
||||
}, [node, scrollPlaylistByDelta]);
|
||||
return (
|
||||
<div
|
||||
ref={setNode}
|
||||
className="playlist-tracks"
|
||||
style={{ height: "100%" }}
|
||||
onClick={selectZero}
|
||||
onWheel={scrollPlaylistByDelta}
|
||||
>
|
||||
<div className="playlist-track-titles">
|
||||
{_renderTracks((id, i) => (
|
||||
|
|
@ -95,7 +118,7 @@ function TrackList() {
|
|||
))}
|
||||
</div>
|
||||
<div className="playlist-track-durations">
|
||||
{_renderTracks((id) => getTimeStr(tracks[id].duration))}
|
||||
{_renderTracks((id) => Utils.getTimeStr(tracks[id].duration))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
WINDOW_RESIZE_SEGMENT_WIDTH,
|
||||
WINDOW_RESIZE_SEGMENT_HEIGHT,
|
||||
} from "../constants";
|
||||
import * as Utils from "../utils";
|
||||
|
||||
type Size = [number, number];
|
||||
|
||||
|
|
@ -23,9 +24,9 @@ function ResizeTarget(props: Props) {
|
|||
return;
|
||||
}
|
||||
const [width, height] = currentSize;
|
||||
const handleMove = (ee: MouseEvent) => {
|
||||
const x = ee.clientX - mouseStart.x;
|
||||
const y = ee.clientY - mouseStart.y;
|
||||
const handleMove = (ee: MouseEvent | TouchEvent) => {
|
||||
const x = Utils.getX(ee) - mouseStart.x;
|
||||
const y = Utils.getY(ee) - mouseStart.y;
|
||||
|
||||
const newWidth = Math.max(
|
||||
0,
|
||||
|
|
@ -42,28 +43,38 @@ function ResizeTarget(props: Props) {
|
|||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
window.addEventListener("touchmove", handleMove);
|
||||
|
||||
const handleMouseUp = () => setMouseDown(false);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
window.addEventListener("touchend", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("touchmove", handleMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
// We pruposefully close over the props from when the mouse went down
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseStart, mouseDown]);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Prevent dragging from highlighting text.
|
||||
e.preventDefault();
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
setMouseStart({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
x: Utils.getX(e),
|
||||
y: Utils.getY(e),
|
||||
});
|
||||
setMouseDown(true);
|
||||
};
|
||||
|
||||
return <div onMouseDown={handleMouseDown} {...passThroughProps} />;
|
||||
return (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleMouseDown}
|
||||
{...passThroughProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default memo(ResizeTarget);
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ export default function Volume({ id, style, className }: Props) {
|
|||
max="100"
|
||||
step="1"
|
||||
value={volume}
|
||||
style={style}
|
||||
style={{ ...style, touchAction: "none" }}
|
||||
className={className}
|
||||
onChange={(e) => setVolume(Number(e.target.value))}
|
||||
onMouseDown={() => setFocus("volume")}
|
||||
onTouchStart={() => {
|
||||
setFocus("volume");
|
||||
}}
|
||||
onMouseUp={unsetFocus}
|
||||
onTouchEnd={unsetFocus}
|
||||
title="Volume Bar"
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as Selectors from "../selectors";
|
|||
import * as Actions from "../actionCreators";
|
||||
import { WindowInfo, WindowId, Box, Point } from "../types";
|
||||
import { useTypedSelector, useActionCreator } from "../hooks";
|
||||
import * as Utils from "../utils";
|
||||
|
||||
const abuts = (a: Box, b: Box) => {
|
||||
// TODO: This is kinda a hack. They should really be touching, not just within snapping distance.
|
||||
|
|
@ -26,7 +27,10 @@ type DraggingState = {
|
|||
|
||||
function useHandleMouseDown(propsWindows: {
|
||||
[windowId: string]: ReactNode;
|
||||
}): (key: WindowId, e: React.MouseEvent<HTMLDivElement>) => void {
|
||||
}): (
|
||||
key: WindowId,
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => void {
|
||||
const windowsInfo = useTypedSelector(Selectors.getWindowsInfo);
|
||||
const getWindowHidden = useTypedSelector(Selectors.getWindowHidden);
|
||||
const browserWindowSize = useTypedSelector(Selectors.getBrowserWindowSize);
|
||||
|
|
@ -41,10 +45,10 @@ function useHandleMouseDown(propsWindows: {
|
|||
return;
|
||||
}
|
||||
const { boundingBox, moving, stationary, mouseStart } = draggingState;
|
||||
const handleMouseMove = (ee: MouseEvent) => {
|
||||
const handleMouseMove = (ee: MouseEvent | TouchEvent) => {
|
||||
const proposedDiff = {
|
||||
x: ee.clientX - mouseStart.x,
|
||||
y: ee.clientY - mouseStart.y,
|
||||
x: Utils.getX(ee) - mouseStart.x,
|
||||
y: Utils.getY(ee) - mouseStart.y,
|
||||
};
|
||||
|
||||
const proposedWindows = moving.map((node) => ({
|
||||
|
|
@ -86,21 +90,32 @@ function useHandleMouseDown(propsWindows: {
|
|||
}
|
||||
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("touchend", handleMouseUp);
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove, { passive: false });
|
||||
window.addEventListener("touchmove", handleMouseMove, { passive: false });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("touchmove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
}, [browserWindowSize, draggingState, updateWindowPositions]);
|
||||
|
||||
// Mouse down handler
|
||||
return useCallback(
|
||||
(key: WindowId, e: React.MouseEvent<HTMLDivElement>) => {
|
||||
(
|
||||
key: WindowId,
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!(e.target as HTMLElement).classList.contains("draggable")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = Utils.getX(e);
|
||||
const y = Utils.getY(e);
|
||||
|
||||
if (getWindowHidden(key)) {
|
||||
// The user may be clicking on full screen Milkdrop.
|
||||
return;
|
||||
|
|
@ -124,7 +139,7 @@ function useHandleMouseDown(propsWindows: {
|
|||
const stationary = windows.filter((w) => !movingSet.has(w));
|
||||
const moving = Array.from(movingSet);
|
||||
|
||||
const mouseStart = { x: e.clientX, y: e.clientY };
|
||||
const mouseStart = { x, y };
|
||||
|
||||
const boundingBox = SnapUtils.boundingBox(moving);
|
||||
setDraggingState({ boundingBox, moving, stationary, mouseStart });
|
||||
|
|
@ -164,11 +179,15 @@ export default function WindowManager({ windows: propsWindows }: Props) {
|
|||
onMouseDown={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
handleMouseDown(w.key, e);
|
||||
}}
|
||||
onTouchStart={(e: React.TouchEvent<HTMLDivElement>) => {
|
||||
handleMouseDown(w.key, e);
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
transform: `translate(${w.x}px, ${w.y}px)`,
|
||||
touchAction: "none",
|
||||
}}
|
||||
>
|
||||
{propsWindows[w.key]}
|
||||
|
|
|
|||
|
|
@ -399,6 +399,36 @@ export function getScreenSize(): { width: number; height: number } {
|
|||
};
|
||||
}
|
||||
|
||||
type PosEvent =
|
||||
| MouseEvent
|
||||
| TouchEvent
|
||||
| React.MouseEvent<HTMLElement>
|
||||
| React.TouchEvent<HTMLElement>;
|
||||
|
||||
function getPos(e: PosEvent): { clientX: number; clientY: number } {
|
||||
switch (e.type) {
|
||||
case "touchstart":
|
||||
case "touchmove": {
|
||||
const touch = (e as TouchEvent).targetTouches[0];
|
||||
return touch;
|
||||
}
|
||||
case "mousedown":
|
||||
case "mousemove": {
|
||||
return e as MouseEvent;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected event type: ${e.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getX(e: PosEvent) {
|
||||
return getPos(e).clientX;
|
||||
}
|
||||
|
||||
export function getY(e: PosEvent) {
|
||||
return getPos(e).clientY;
|
||||
}
|
||||
|
||||
export function weakMapMemoize<T extends object, R>(
|
||||
func: (value: T) => R
|
||||
): (value: T) => R {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue