diff --git a/apps/web/src/layout/SurfaceHost.tsx b/apps/web/src/layout/SurfaceHost.tsx index 2bce918138dfa..0291ba144a90c 100644 --- a/apps/web/src/layout/SurfaceHost.tsx +++ b/apps/web/src/layout/SurfaceHost.tsx @@ -185,12 +185,16 @@ const NO_GHOSTS: ReadonlyMap = new Map(); * Pane edges land on whole pixels, so borders and text stay crisp at any split. * Both panes round the same boundary, so the gap between them stays exact. */ -function slotStyle(slot: LayoutPaneSlot): React.CSSProperties { +function slotStyle(slot: LayoutPaneSlot, stripPx?: number): React.CSSProperties { const edge = (has: boolean) => (has ? HALF_PANE_GAP : "0px"); const at = (fraction: number, offset: string) => `round(calc(${fraction * 100}% + ${offset}), 1px)`; - const left = at(slot.rect.left, edge(slot.hasLeft)); - const right = at(slot.rect.left + slot.rect.width, `-1 * ${edge(slot.hasRight)}`); + const across = (fraction: number, offset: string) => + stripPx === undefined + ? at(fraction, offset) + : `round(calc(${fraction * stripPx}px + ${offset}), 1px)`; + const left = across(slot.rect.left, edge(slot.hasLeft)); + const right = across(slot.rect.left + slot.rect.width, `-1 * ${edge(slot.hasRight)}`); const top = at(slot.rect.top, edge(slot.hasAbove)); const bottom = at(slot.rect.top + slot.rect.height, `-1 * ${edge(slot.hasBelow)}`); return { @@ -1056,6 +1060,7 @@ export function LayoutSurfaceHost({ chats: chatTabsById, launchers: launcherTabsById, placed: slotByTabId, + stripPx, motionKey: paneMotionKey, framed: paneCount > 1 || maximizedTabId !== null, }); @@ -1068,6 +1073,7 @@ export function LayoutSurfaceHost({ chats: chatTabsById, launchers: launcherTabsById, placed: slotByTabId, + stripPx, motionKey: paneMotionKey, framed: paneCount > 1 || maximizedTabId !== null, }); @@ -1080,8 +1086,9 @@ export function LayoutSurfaceHost({ seen.motionKey === paneMotionKey ) { const closed: [string, ClosingTab][] = []; - for (const [tabId, placed] of seen.placed) { + for (const [tabId, was] of seen.placed) { if (presentTabIds.has(tabId) || ghosts.has(tabId)) continue; + const placed = { slot: was.slot, style: slotStyle(was.slot, seen.stripPx) }; const chat = seen.chats.get(tabId); const launcher = seen.launchers.get(tabId); if (chat?.tab.content.kind === "chat") { diff --git a/apps/web/src/motion/paneMotion.browser.test.ts b/apps/web/src/motion/paneMotion.browser.test.ts new file mode 100644 index 0000000000000..b6f77ce99552e --- /dev/null +++ b/apps/web/src/motion/paneMotion.browser.test.ts @@ -0,0 +1,16 @@ +import { assert, describe, it } from "vite-plus/test"; + +import { findDonor } from "./paneMotion"; + +const column = (x: number, width: number) => ({ x, y: 0, width, height: 880 }); + +describe("findDonor", () => { + it("takes only a neighbour across the gutter, never one with panes between", () => { + const closed = column(872, 416); + const far = { box: column(0, 442), other: column(0, 1288) }; + const beside = { box: column(450, 414), other: column(450, 838) }; + + assert.isNull(findDonor(closed, [far])); + assert.strictEqual(findDonor(closed, [far, beside])?.neighbour, beside); + }); +}); diff --git a/apps/web/src/motion/paneMotion.ts b/apps/web/src/motion/paneMotion.ts index 155c6b6520df7..8cf36e8354f12 100644 --- a/apps/web/src/motion/paneMotion.ts +++ b/apps/web/src/motion/paneMotion.ts @@ -38,6 +38,8 @@ const translate = (dx: number, dy: number): Keyframe => ({ /** How far a new pane slides out from under its donor, in px. */ const OPEN_TRAVEL_PX = 48; +const ADJACENT_GAP_PX = 16; + /** The Panel animations setting's default, which plays niri's springs as they are. */ const DEFAULT_PANEL_MS = 150; @@ -123,7 +125,7 @@ interface Neighbour { * donor. One that changed size along that side wins; then left, above, * right, below, the nearest on each side. */ -function findDonor( +export function findDonor( box: Box, neighbours: Iterable, ): { neighbour: Neighbour; side: Side; gap: number } | null { @@ -145,7 +147,7 @@ function findDonor( ); } for (const [side, gap] of gaps) { - if (gap < -1) continue; + if (gap < -1 || gap > ADJACENT_GAP_PX) continue; const rank = (candidate: { neighbour: Neighbour; side: Side }) => (candidate.neighbour.other && resized(candidate.side, candidate.neighbour.box, candidate.neighbour.other)