Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"markdown-it": "^14.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable": "^4.0.2",
"react-sidebar": "^3.0.2",
"react-toastify": "^11.0.3"
},
Expand Down
10 changes: 6 additions & 4 deletions src/css/Layout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
top: 0;
width: inherit;
height: inherit;
}

:global(.react-resizable) {
width: 0 !important;
height: 0 !important;
}
.handleEast,
.handleSouth,
.handleSouthEast {
touch-action: none;
user-select: none;
}

.handleEast {
Expand Down
56 changes: 6 additions & 50 deletions src/ui/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import * as React from "react";
import { ResizableBox } from "react-resizable";
import { type LayoutStateRef } from "../../livesplit-core";
import { type WebRenderer } from "../../livesplit-core/livesplit_core";
import AutoRefresh from "../../util/AutoRefresh";
import { type UrlCache } from "../../util/UrlCache";
import { type GeneralSettings } from "../views/MainSettings";

import classes from "../../css/Layout.module.css";
import { LayoutResizeHandles } from "./LayoutResizeHandles";

export function Layout({
getState,
Expand Down Expand Up @@ -70,53 +68,11 @@ export function Layout({
}}
/>
{allowResize && (
<div className={classes.resizableLayout}>
<ResizableBox
axis="x"
width={width}
height={height}
minConstraints={[100, 40]}
handle={
<div
onClick={(e) => e.stopPropagation()}
className={classes.handleEast}
/>
}
onResize={(_event, data) =>
onResize(data.size.width, data.size.height)
}
/>
<ResizableBox
axis="y"
width={width}
height={height}
minConstraints={[100, 40]}
handle={
<div
onClick={(e) => e.stopPropagation()}
className={classes.handleSouth}
/>
}
onResize={(_event, data) =>
onResize(data.size.width, data.size.height)
}
/>
<ResizableBox
axis="both"
width={width}
height={height}
minConstraints={[100, 40]}
handle={
<div
onClick={(e) => e.stopPropagation()}
className={classes.handleSouthEast}
/>
}
onResize={(_event, data) =>
onResize(data.size.width, data.size.height)
}
/>
</div>
<LayoutResizeHandles
width={width}
height={height}
onResize={onResize}
/>
)}
</div>
</AutoRefresh>
Expand Down
126 changes: 126 additions & 0 deletions src/ui/components/LayoutResizeHandles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from "react";

import classes from "../../css/Layout.module.css";

const MINIMUM_WIDTH = 100;
const MINIMUM_HEIGHT = 40;

type ResizeAxis = "horizontal" | "vertical" | "both";

export function LayoutResizeHandles({
width,
height,
onResize,
}: {
width: number;
height: number;
onResize: (width: number, height: number) => void;
}) {
const stopListening = React.useRef<(() => void) | null>(null);

React.useEffect(
() => () => {
stopListening.current?.();
},
[],
);

const startResize =
(axis: ResizeAxis) => (event: React.PointerEvent<HTMLDivElement>) => {
// Ignore secondary pointers and non-primary mouse buttons. Pointer
// events cover mouse, touch, and pen input without needing separate
// compatibility paths for each input type.
if (!event.isPrimary || event.button !== 0) {
return;
}

const pointerId = event.pointerId;
const startX = event.clientX;
const startY = event.clientY;
let lastWidth = width;
let lastHeight = height;
const ownerDocument = event.currentTarget.ownerDocument;
const ownerWindow = ownerDocument.defaultView;

// Listen on the document while dragging so the hit area can remain
// deliberately narrow without losing movement as soon as the
// pointer leaves it. The cleanup ref also handles the component
// being unmounted or the window losing focus mid-drag.
function resize(pointerEvent: PointerEvent) {
if (pointerEvent.pointerId !== pointerId) {
return;
}

const newWidth =
axis === "vertical"
? width
: Math.max(
MINIMUM_WIDTH,
width + pointerEvent.clientX - startX,
);
const newHeight =
axis === "horizontal"
? height
: Math.max(
MINIMUM_HEIGHT,
height + pointerEvent.clientY - startY,
);

if (newWidth !== lastWidth || newHeight !== lastHeight) {
lastWidth = newWidth;
lastHeight = newHeight;
onResize(newWidth, newHeight);
}
pointerEvent.preventDefault();
}

function stopResize(pointerEvent: PointerEvent) {
if (pointerEvent.pointerId === pointerId) {
cleanup();
}
}

function cleanup() {
ownerDocument.removeEventListener("pointermove", resize);
ownerDocument.removeEventListener("pointerup", stopResize);
ownerDocument.removeEventListener("pointercancel", stopResize);
ownerWindow?.removeEventListener("blur", cleanup);
stopListening.current = null;
}

stopListening.current?.();
stopListening.current = cleanup;
ownerDocument.addEventListener("pointermove", resize, {
passive: false,
});
ownerDocument.addEventListener("pointerup", stopResize);
ownerDocument.addEventListener("pointercancel", stopResize);
ownerWindow?.addEventListener("blur", cleanup);
event.preventDefault();
};

const handleProps = {
"aria-hidden": true,
onClick: (event: React.MouseEvent) => event.stopPropagation(),
} as const;

return (
<div className={classes.resizableLayout}>
<div
{...handleProps}
className={classes.handleEast}
onPointerDown={startResize("horizontal")}
/>
<div
{...handleProps}
className={classes.handleSouth}
onPointerDown={startResize("vertical")}
/>
<div
{...handleProps}
className={classes.handleSouthEast}
onPointerDown={startResize("both")}
/>
</div>
);
}
Loading