Files
kandev/apps/web/lib/state/dockview-pinned-enforce.ts
T

108 lines
4.7 KiB
TypeScript

/**
* Pinned-column target enforcement primitives.
*
* Lives in the state layer (next to `dockview-store`) so the store can
* call enforcement without crossing into the component layer. Sash-drag
* handlers in `components/task/dockview-layout-setup.ts` toggle the
* `sashDragging` flag via `setSashDragging` and consume `enforcePinnedTargets`
* for the reactive `onDidLayoutChange` callback - both directions now
* import from this module, breaking the previous
* state-imports-component-imports-state cycle.
*
* Dockview's splitview rebalances proportionally on any `api.layout` call,
* which would otherwise grow pinned columns past their initial defaults on
* container expansion and shrink them on container contraction. We treat
* sidebar/right as having a *target width* (stored in
* `layout-manager/pinned-targets.ts`) that is updated only by explicit user
* actions (drag, initial layout, restore from saved); after every
* layout-change event we force the live columns back to their targets via
* `sv.resizeView`.
*/
import type { DockviewApi } from "dockview-react";
import { getRootSplitview, getPinnedTarget } from "./layout-manager";
import { createDebugLogger, IS_DEBUG } from "@/lib/debug/log";
const debugWidths = createDebugLogger("dockview:widths");
/** Enforcement-in-progress guard to prevent infinite loops when our own
* `sv.resizeView` triggers `onDidLayoutChange`. */
let enforcing = false;
/** True while the user is actively dragging a `.dv-sash`. We pause target
* enforcement during the drag so the in-progress resize doesn't get
* reverted to the previous target on every intermediate layout change. */
let sashDragging = false;
/** Setter for the sash-drag flag. Sash mousedown/mouseup handlers in
* `components/task/dockview-layout-setup.ts` call this to gate enforcement
* during user drags. Reading from outside isn't useful because the value
* flips synchronously with the drag - call sites that need the state
* check it indirectly via `enforcePinnedTargets`'s short-circuit. */
export function setSashDragging(v: boolean): void {
sashDragging = v;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function restoreColumnToTarget(sv: any, idx: number, target: number | undefined): void {
if (target === undefined) return;
const cur = sv.getViewSize(idx);
if (Math.abs(cur - target) <= 1) return;
if (IS_DEBUG) {
debugWidths(`enforce-restore idx=${idx} cur=${Math.round(cur)} target=${Math.round(target)}`);
}
try {
sv.resizeView(idx, target);
} catch {
/* dockview rejects unreachable sizes — ignore */
}
}
/** Visibility/maximize state needed by `enforcePinnedTargets`. Passed in by
* callers (instead of read via `useDockviewStore.getState()`) so this module
* doesn't depend on the store and the import graph stays one-way. */
export type EnforcePinnedTargetsCtx = {
sidebarVisible: boolean;
rightPanelsVisible: boolean;
/** Non-null when we are in (or restoring) a maximized-group state; skip
* enforcement so we don't fight the 2-column maximize overlay. */
maximized: boolean;
};
/**
* Snap the pinned columns back to their recorded targets.
*
* Two call modes:
* - Reactive: wired in `setupSashDragCapToggle` via `onDidLayoutChange`.
* The subscription pre-checks `isRestoringLayout` and skips during
* restore so this function does not fight an in-progress programmatic
* layout.
* - Proactive: called synchronously by programmatic layout paths
* (build-default, env-switch, preset/custom-layout apply, sidebar/right
* toggle, maximize/exit) *inside* the post-`api.layout` rAF, before
* `isRestoringLayout` flips false. Dockview's proportional rebalance
* after `api.layout` can grow pinned columns up to their loose
* `setConstraints.maximumWidth`; without this synchronous snap the
* correction would only happen on the next user-triggered layout-change
* event, producing a visible jerk after env prep settles.
*
* The `enforcing` re-entry guard remains so the `sv.resizeView` call we emit
* does not feed back through the layout-change subscription. `sashDragging`
* still gates the whole thing - mid-drag, we want dockview to follow the
* user's mouse, not snap to the previous target.
*/
export function enforcePinnedTargets(api: DockviewApi, ctx: EnforcePinnedTargetsCtx): void {
if (enforcing || sashDragging) return;
if (api.hasMaximizedGroup() || ctx.maximized) return;
const sv = getRootSplitview(api);
if (!sv || sv.length < 2) return;
enforcing = true;
try {
if (ctx.sidebarVisible) restoreColumnToTarget(sv, 0, getPinnedTarget("sidebar"));
if (ctx.rightPanelsVisible) {
restoreColumnToTarget(sv, sv.length - 1, getPinnedTarget("right"));
}
} finally {
enforcing = false;
}
}