Move sidebar resize math into lib

The first press no longer pairs with an imaginary one at time zero.
This commit is contained in:
Rūdolfs Ošiņš
2026-10-01 18:20:25 +02:00
parent f0d4d678aa
commit 92e866f7b8
3 changed files with 117 additions and 39 deletions
+9 -39
View File
@@ -22,7 +22,6 @@
import {
commitSidebarWidth,
discardSidebarWidthPreview,
MIN_SIDEBAR_WIDTH,
previewSidebarWidth,
RAIL_WIDTH_REM,
renderedSidebarWidth,
@@ -33,6 +32,7 @@
sidebarWidth,
toggleSidebar,
} from "@app/lib/sidebar.svelte";
import { doublePressDetector, dragResult } from "@app/lib/sidebarResize";
import { updateChecker } from "@app/lib/updateChecker.svelte";
import useLocalStorage from "@app/lib/useLocalStorage.svelte";
import { isMac } from "@app/lib/utils";
@@ -92,14 +92,6 @@
const mini = $derived(collapsed.value);
// How far below the minimum width the drag has to go before it collapses,
// rather than the edge just sticking at the minimum.
const COLLAPSE_DRAG_SLACK_REM = 2;
// While collapsed, the rail follows the drag at a fraction of its distance so
// it feels attached to the pointer, instead of sitting still until the
// threshold and then jumping open.
const RAIL_STRETCH_FACTOR = 0.35;
let railStretch = $state(0);
// What `.slot` is animating towards.
@@ -176,29 +168,14 @@
// A second press in quick succession fits the sidebar. Detected here rather
// than with `dblclick`, because the pointerdown below is cancelled to stop a
// Tauri window drag, which suppresses the mouse events `dblclick` comes from.
const DOUBLE_PRESS_MS = 500;
const DOUBLE_PRESS_SLOP_PX = 6;
let lastPressAt = 0;
let lastPressX = 0;
function isDoublePress(e: PointerEvent): boolean {
const paired =
// A press that ended in a resize isn't half of a pair.
!lastPressResized &&
e.timeStamp - lastPressAt < DOUBLE_PRESS_MS &&
Math.abs(e.clientX - lastPressX) <= DOUBLE_PRESS_SLOP_PX;
// Cleared so a third press starts a new pair instead of fitting again.
lastPressAt = paired ? 0 : e.timeStamp;
lastPressX = e.clientX;
return paired;
}
const isDoublePress = doublePressDetector();
function onEdgePointerDown(e: PointerEvent) {
// Primary button only; a right-click here would otherwise start a drag.
if (e.button !== 0) return;
e.preventDefault();
if (isDoublePress(e)) {
if (isDoublePress(e, lastPressResized)) {
void fitSidebarToContent();
return;
}
@@ -223,26 +200,19 @@
if (Math.abs(ev.clientX - startX) <= DRAG_SLOP_PX) return;
dragged = true;
}
const width = startWidth + (ev.clientX - startX) / pxPerRem;
// One boundary serves both directions: below it the sidebar is collapsed,
// above it expanded, so a single drag can cross either way.
const collapse = width < MIN_SIDEBAR_WIDTH - COLLAPSE_DRAG_SLACK_REM;
if (collapsed.value !== collapse) {
collapsed.value = collapse;
const drag = dragResult(startWidth, ev.clientX - startX, pxPerRem);
if (collapsed.value !== drag.collapse) {
collapsed.value = drag.collapse;
snapWidth();
}
if (collapse) {
railStretch = Math.max(
0,
(width - RAIL_WIDTH_REM) * RAIL_STRETCH_FACTOR,
);
railStretch = drag.railStretch;
if (drag.collapse) {
// Anything previewed before the threshold was crossed is on the way to
// collapsing, not a width to remember.
discardSidebarWidthPreview();
} else {
railStretch = 0;
// Off storage; persisted once when the drag ends.
previewSidebarWidth(width);
previewSidebarWidth(drag.width);
}
};
+49
View File
@@ -0,0 +1,49 @@
import { MIN_SIDEBAR_WIDTH, RAIL_WIDTH_REM } from "@app/lib/sidebar.svelte";
// How far below the minimum width the drag has to go before it collapses,
// rather than the edge just sticking at the minimum.
const COLLAPSE_DRAG_SLACK_REM = 2;
// While collapsed, the rail follows the drag at a fraction of its distance so
// it feels attached to the pointer, instead of sitting still until the
// threshold and then jumping open.
const RAIL_STRETCH_FACTOR = 0.35;
// Where a drag of the sidebar edge leaves it. One boundary serves both
// directions: below it the sidebar is collapsed, above it expanded, so a
// single drag can cross either way.
export function dragResult(
startWidth: number,
deltaPx: number,
pxPerRem: number,
): { collapse: boolean; width: number; railStretch: number } {
const width = startWidth + deltaPx / pxPerRem;
const collapse = width < MIN_SIDEBAR_WIDTH - COLLAPSE_DRAG_SLACK_REM;
const railStretch = collapse
? Math.max(0, (width - RAIL_WIDTH_REM) * RAIL_STRETCH_FACTOR)
: 0;
return { collapse, width, railStretch };
}
const DOUBLE_PRESS_MS = 500;
const DOUBLE_PRESS_SLOP_PX = 6;
// A second press in quick succession, close to the first. A press that ended
// in a resize isn't half of a pair, and a pair is cleared so a third press
// starts a new one.
export function doublePressDetector() {
let lastPressAt = -Infinity;
let lastPressX = 0;
return (
press: { timeStamp: number; clientX: number },
lastPressResized: boolean,
): boolean => {
const paired =
!lastPressResized &&
press.timeStamp - lastPressAt < DOUBLE_PRESS_MS &&
Math.abs(press.clientX - lastPressX) <= DOUBLE_PRESS_SLOP_PX;
lastPressAt = paired ? -Infinity : press.timeStamp;
lastPressX = press.clientX;
return paired;
};
}
+59
View File
@@ -0,0 +1,59 @@
import { describe, expect, test } from "vitest";
import { doublePressDetector, dragResult } from "@app/lib/sidebarResize";
describe("dragResult", () => {
test("widens by the dragged distance", () => {
expect(dragResult(16, 32, 16)).toEqual({
collapse: false,
width: 18,
railStretch: 0,
});
});
test("collapses only past the slack below the minimum width", () => {
expect(dragResult(12, -2 * 16, 16).collapse).toBe(false);
expect(dragResult(12, -2 * 16 - 1, 16).collapse).toBe(true);
});
test("stretches the rail with a collapsed drag, never below its width", () => {
expect(dragResult(5, 2 * 16, 16)).toEqual({
collapse: true,
width: 7,
railStretch: 0.7,
});
expect(dragResult(5, -16, 16).railStretch).toBe(0);
});
});
describe("doublePressDetector", () => {
const press = (timeStamp: number, clientX = 100) => ({ timeStamp, clientX });
test("pairs two quick presses in the same place", () => {
const isDoublePress = doublePressDetector();
expect(isDoublePress(press(1000), false)).toBe(false);
expect(isDoublePress(press(1499, 106), false)).toBe(true);
});
test("a third press starts a new pair", () => {
const isDoublePress = doublePressDetector();
isDoublePress(press(1000), false);
isDoublePress(press(1100), false);
expect(isDoublePress(press(1200), false)).toBe(false);
expect(isDoublePress(press(1300), false)).toBe(true);
});
test.each([
["too late", press(1500), false],
["too far", press(1100, 107), false],
["after a resize", press(1100), true],
])("doesn't pair a press %s", (_, second, resized) => {
const isDoublePress = doublePressDetector();
isDoublePress(press(1000), false);
expect(isDoublePress(second, resized)).toBe(false);
});
test("the first press right after launch isn't a pair", () => {
expect(doublePressDetector()(press(10, 0), false)).toBe(false);
});
});