diff --git a/CLAUDE.md b/CLAUDE.md index f471b663..c75b2fd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -628,6 +628,22 @@ native ``, no Acrylic or Accent opacity slider. Spec and plan: Explorer's MEDIUM row is the tree's default row (`rowLook()` in `treePrefs`). The `marquee` e2e measures both; a drag in a test is taken by the row's NAME, since a locator's centre is blank space. + **IN THE EXPLORER THE WHOLE ROW DRAGS, THE ARROW STAYS, AND THE BOX STARTS ONLY OFF THE ROWS** + (#320; owner, 2026-10-07: "when you left click drag an item dont switch the cursor to the hand, + keep it the normal cursor ... the whole row should let me left click drag ... that drag should + only be from empty spaces either under or beside the file row"). This narrows #257 for the + Explorer's list (the tree keeps its rule): a press ANYWHERE on a file row (any cell, any gap) + is its click and drag, never a sweep; the sweep starts under the last row or BESIDE the rows. + For "beside" a row is as wide as its columns (`width: fit-content`), Name grows only to + `--browse-name-max` (440px, Large 520px; search's Path to `--browse-path-max`) instead of the + rest of the width, and a `--browse-gutter` (32px) on the right is always blank, so even a + narrow list has a place to start one; the header keeps the same grid and gutter, so its labels + stay over the row's cells, and its LAST cell's box still runs on to the header's right edge + (`--browse-col-edge-end`; the 2026-10-04 "no strip a hover cannot fill" rule). + A click beside the rows clears the marks like the space under them. During an internal drag + every element's cursor is pinned to `default` (`internalFileDrag.ts`; it was `grabbing`). + The `marquee` e2e measures the gutter and the header, sweeps from beside, drags from the Size + and Date cells (label up, no rectangle, `default` cursor under the pointer, on html and body). **MARKING IS NOT PICKING** (#263; owner, 2026-10-03: "when you multiselect like this it picks a file so here this drag starts one of the videos ... same is the case if i ctrl select it shouldnt start or preview anything"). In the Explorer a sweep and a Ctrl or Shift click call diff --git a/package-lock.json b/package-lock.json index 85792dcf..beb8ed81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prism", - "version": "0.95.1", + "version": "0.95.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prism", - "version": "0.95.1", + "version": "0.95.2", "license": "MIT", "dependencies": { "@codemirror/commands": "^6.11.0", diff --git a/package.json b/package.json index c182ff68..8f884829 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prism", - "version": "0.95.1", + "version": "0.95.2", "description": "A fast, universal media viewer for Windows.", "author": "Max", "license": "MIT", diff --git a/src/renderer/src/components/browse/BrowseList.tsx b/src/renderer/src/components/browse/BrowseList.tsx index f84ad867..ce953d53 100644 --- a/src/renderer/src/components/browse/BrowseList.tsx +++ b/src/renderer/src/components/browse/BrowseList.tsx @@ -21,7 +21,7 @@ import { DRAG_MIME, setDrag } from '../../lib/dragDrop' import { FolderIcon, KindIcon, SweepBand, iconColour } from '../TreeRows' import { OverlayScrollbar } from './OverlayScrollbar' import { explorerHeadVars, explorerRow, useExplorerSize } from '../../lib/explorerSize' -import { bandBox, nearestRow, onRowOwnPart, rowsInBand } from '../../lib/marquee' +import { bandBox, nearestRow, rowsInBand } from '../../lib/marquee' import { useSweep } from '../../hooks/useSweep' import { BrowseIcon } from './BrowseIcon' import { useFolderDrop } from './useFolderDrop' @@ -186,12 +186,19 @@ export function BrowseList(props: Props): JSX.Element { rowAt(first + index) ) /** - * THE SWEEP (#257). From the list's blank space (under the rows, or a row - * to the right of its name, the other columns included) a drag draws the - * rectangle and marks every row it touches, live. A press on a row's icon - * or name is still the file's own drag, so a file still drags out to other - * apps. Rows are found by index, never by element: only the rows in view - * exist, and a row scrolled away under the rectangle is still in it. + * THE SWEEP (#257). From the list's blank space a drag draws the rectangle + * and marks every row it touches, live. Rows are found by index, never by + * element: only the rows in view exist, and a row scrolled away under the + * rectangle is still in it. + * + * THE WHOLE ROW IS THE FILE'S (#320; owner, 2026-10-07: "its not possible + * to pick up items unless you left click drag when hovering over the file + * name. the whole row should let me left click drag ... that drag should + * only be from empty spaces either under or beside the file row"). Until + * then a row's Type, Size and Date cells swept. Now a press anywhere on a + * row drags it, and the sweep starts only off the rows: under the last one, + * or beside them, since a row ends where its last column does and a gutter + * is always left on the right (browse.css), File Explorer's Details view. */ const [sweeping, setSweeping] = useState<{ paths: string[]; add: boolean } | null>(null) const sweepAdd = useRef(false) @@ -249,16 +256,10 @@ export function BrowseList(props: Props): JSX.Element { }) const onListPointerDown = (e: ReactPointerEvent): void => { if (e.button !== 0 || props.loading || props.message || !props.onSweep) return - const el = e.target as HTMLElement - const row = el.closest('.browse-row') - if (row) { - // The row up to the end of its name is the file's: it drags it, as - // before. The other columns are blank space and sweep. - const name = row.querySelectorAll('.browse-name > svg, .browse-name-text') - if (onRowOwnPart(e.clientX, [...name].map((n) => n.getBoundingClientRect()))) return - } + // A file's row, every cell of it and the gaps between: its own drag. + if ((e.target as HTMLElement).closest('.browse-row[data-browse-path]')) return sweepAdd.current = e.ctrlKey - sweep.begin(e, row) + sweep.begin(e) } /** What reads as marked: the sweep in progress (plus, for Ctrl, what was * marked before it), else the selection FolderBrowser holds. */ @@ -546,7 +547,10 @@ export function BrowseList(props: Props): JSX.Element { // the preview plays (owner, 2026-10-03: "i should have to click the // video or the pause icon"). A right press on an unmarked row too. onClick={(e) => { - if (e.target === e.currentTarget) props.onSelect(null, true) + // The space beside the rows (#320) is the row layer's own box. + const at = e.target as HTMLElement + if (at === e.currentTarget || at.matches('.browse-row-space, .browse-row-layer')) + props.onSelect(null, true) }} onContextMenu={(e) => { // The empty space's own menu, inside an archive (#300). A row's @@ -562,14 +566,9 @@ export function BrowseList(props: Props): JSX.Element { {props.message} ) : ( -
{ - if (e.target === e.currentTarget) props.onSelect(null, true) - }} - > +
void): void { pressed = false cancelAnimationFrame(frame) delete document.body.dataset.internalFileDrag + delete document.documentElement.dataset.internalFileDrag setDrag(null) // A release over a file must not also select/open the drop destination. suppressClick = true @@ -129,9 +130,13 @@ export function useInternalFileDrag(stepTab: (delta: number) => void): void { const label = paths.length === 1 ? (paths[0].split(/[\\/]/).pop() ?? 'Item') : `${paths.length} items` badge.textContent = label + // THE ARROW STAYS (#320; owner, 2026-10-07: "when you left click drag an + // item dont switch the cursor to the hand, keep it the normal cursor"). + // It was `grabbing`. Pinned to the arrow for the whole drag, so nothing + // under the pointer (a link, a text field, a splitter) changes it either. const cursorStyle = document.createElement('style') cursorStyle.textContent = - '[data-internal-file-drag], [data-internal-file-drag] * { cursor: grabbing !important; }' + 'html[data-internal-file-drag], html[data-internal-file-drag] * { cursor: default !important; }' document.head.append(cursorStyle) Object.assign(badge.style, { position: 'fixed', @@ -165,6 +170,7 @@ export function useInternalFileDrag(stepTab: (delta: number) => void): void { pin: !!pin } document.body.dataset.internalFileDrag = 'true' + document.documentElement.dataset.internalFileDrag = 'true' hover() frame = requestAnimationFrame(tick) } diff --git a/tools/e2e/run.mjs b/tools/e2e/run.mjs index f296637b..2fa22cda 100644 --- a/tools/e2e/run.mjs +++ b/tools/e2e/run.mjs @@ -7819,13 +7819,83 @@ async function marqueeScenario(fixtures) { !(await win.evaluate(() => !!document.querySelector('[data-browse-preview]')?.getClientRects().length)), 'and opened no preview pane (#263)' ) + // THE ROWS END WHERE THE COLUMNS DO (#320): blank space beside them, a + // gutter at the least, and the header's cells still over the row's. + const geo = await win.evaluate(() => { + const list = document.querySelector('[data-testid="browse-list"]')?.getBoundingClientRect() + const row = document.querySelector('[data-testid="browse-list"] [data-browse-index="0"]') + const r = row?.getBoundingClientRect() + const cell = row?.querySelector('.browse-column-modified')?.getBoundingClientRect() + const head = document.querySelector('.browse-columns .browse-column-modified')?.getBoundingClientRect() + const name = row?.querySelector('.browse-column-name')?.getBoundingClientRect() + const headName = document.querySelector('.browse-columns .browse-column-name')?.getBoundingClientRect() + return list && r && cell && head && name && headName + ? { listRight: list.right, rowRight: r.right, cellL: cell.left, headL: head.left, nameL: name.left, headNameL: headName.left } + : null + }) + ok(!!geo && geo.listRight - geo.rowRight >= 30, `a row stops short of the list's edge, leaving blank space beside it (${geo && Math.round(geo.listRight - geo.rowRight)}px)`) + // A header cell reaches out by half the gap (its whole box is the cell), so + // its LABEL starts where the row's cell does: the label's left, by padding. + const headLabel = await win.evaluate(() => { + const b = document.querySelector('.browse-columns .browse-column-modified') + const pad = b ? parseFloat(getComputedStyle(b).paddingLeft) : 0 + return b ? b.getBoundingClientRect().left + pad : null + }) + ok(!!geo && Math.abs(headLabel - geo.cellL) <= 1, `the Date modified header still sits over its column (${headLabel} and ${geo?.cellL})`) + // From the blank space BESIDE the rows: a sweep, which marks what it covers. + { + const g1 = await rowAt(1).boundingBox() + const g3 = await rowAt(3).boundingBox() + const beside = g1.x + g1.width + 14 + const mid = await sweep({ x: beside, y: g1.y + g1.height / 2 }, { x: beside - 60, y: g3.y + g3.height / 2 }, { + mid: async () => { + await win.screenshot({ path: join(SHOTS, 'marquee-beside.png') }) + return { band: await band('[data-testid="browse-list"]') } + } + }) + ok(mid.band === 1, `a press beside the rows draws the rectangle (${mid.band})`) + ex = await exMarked() + ok(ex.sort().join() === 'a2.txt,a3.txt,a4.txt', `and marks the rows it covered (${ex})`) + } + // THE WHOLE ROW DRAGS (#320): a press on the Size or the Date cell picks + // the file up, the label follows, no rectangle, and the arrow stays. + for (const cellName of ['size', 'modified']) { + const cell = await rowAt(5).locator(`.browse-column-${cellName}`).boundingBox() + const at = { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 } + await win.mouse.move(at.x, at.y) + await win.mouse.down() + await win.mouse.move(at.x - 30, at.y + 70, { steps: 10 }) + await sleep(200) + const mid = await win.evaluate( + ({ x, y }) => ({ + badge: document.querySelector('[data-file-drag-badge]')?.textContent ?? null, + under: getComputedStyle(document.elementFromPoint(x, y) ?? document.body).cursor, + html: getComputedStyle(document.documentElement).cursor, + body: getComputedStyle(document.body).cursor + }), + { x: at.x - 30, y: at.y + 70 } + ) + const midBand = await band('[data-testid="browse-list"]') + if (cellName === 'size') await win.screenshot({ path: join(SHOTS, 'row-drag-size.png') }) + await win.keyboard.press('Escape') + await win.mouse.up() + await sleep(300) + ok(midBand === 0, `a press on a row's ${cellName} cell never draws the rectangle`) + ok(/a6\.txt/.test(mid.badge ?? ''), `it picks the file up, its label following (${mid.badge})`) + ok( + mid.under === 'default' && mid.html === 'default' && mid.body === 'default', + `and the cursor stays the arrow (${mid.under}, ${mid.html}, ${mid.body})` + ) + } + ok(!(await win.evaluate(() => document.querySelector('[data-file-drag-badge]'))), 'Escape put the file down') let f0 = await rowAt(0).boundingBox() - let exBlank = f0.x + f0.width - 30 - // A plain click on a row's blank space, without moving, is still a click. - await win.mouse.click(exBlank, f0.y + f0.height / 2) + let exBlank = f0.x + f0.width + 14 + // A plain click on a row's Date cell, without moving, is still a click. + const dateCell = await rowAt(0).locator('.browse-column-modified').boundingBox() + await win.mouse.click(dateCell.x + dateCell.width / 2, dateCell.y + dateCell.height / 2) await sleep(250) ex = await exMarked() - ok(ex.join() === 'a1.txt', `a plain click on a row's blank space selects that row alone (${ex})`) + ok(ex.join() === 'a1.txt', `a plain click on a row's Date cell selects that row alone (${ex})`) // That click previewed the file, and the pane beside the list made it // narrower: every point after this is measured again. await sleep(400) @@ -7834,8 +7904,8 @@ async function marqueeScenario(fixtures) { const f3 = await rowAt(3).boundingBox() const f4 = await rowAt(4).boundingBox() const f7 = await rowAt(7).boundingBox() - exBlank = f0.x + f0.width - 30 - // Ctrl adds: a4..a5 swept from a row's blank space. + exBlank = f0.x + f0.width + 14 + // Ctrl adds: a4..a5 swept from the space beside the rows. await sweep({ x: exBlank, y: f3.y + f3.height / 2 }, { x: exBlank - 10, y: f4.y + f4.height / 2 }, { ctrl: true }) ex = await exMarked() ok(ex.sort().join() === 'a1.txt,a4.txt,a5.txt', `Ctrl+sweep adds to what was marked (${ex})`) @@ -7975,7 +8045,7 @@ async function marqueeQuietScenario(fixtures) { let box = await list.boundingBox() let r0 = await rowAt(0).boundingBox() let r3 = await rowAt(3).boundingBox() - let blank = r0.x + r0.width - 30 + let blank = r0.x + r0.width + 14 await sweep({ x: box.x + box.width * 0.6, y: r3.y + r3.height + 40 }, { x: blank, y: r0.y + r0.height / 2 + 4 }) let ex = await exMarked() ok(ex.sort().join() === 'v1.mp4,v2.mp4,v3.mp4,v4.mp4', `a sweep marks the four films (${ex})`) @@ -8016,7 +8086,7 @@ async function marqueeQuietScenario(fixtures) { box = await list.boundingBox() r0 = await rowAt(0).boundingBox() r3 = await rowAt(3).boundingBox() - blank = r0.x + r0.width - 30 + blank = r0.x + r0.width + 14 await sweep({ x: box.x + box.width * 0.6, y: r3.y + r3.height + 40 }, { x: blank, y: (await rowAt(1).boundingBox()).y + 4 }) await sleep(600) let f = await films() @@ -9696,7 +9766,8 @@ async function downloadsDateScenario(fixtures) { // THE SWEEP across a divider marks rows only. const rowsTop = await win.locator(`${list} [data-browse-path$="${order[0]}"]`).boundingBox() const rowsBottom = await win.locator(`${list} [data-browse-path$="${order[3]}"]`).boundingBox() - const x = rowsTop.x + rowsTop.width - 30 + // From the blank space beside the rows: a row's own cells drag it (#320). + const x = rowsTop.x + rowsTop.width + 12 await win.mouse.move(x, rowsTop.y + rowsTop.height / 2) await win.mouse.down() await win.mouse.move(x - 10, rowsTop.y + 20, { steps: 3 }) @@ -9814,14 +9885,17 @@ async function columnHeadersScenario(fixtures) { labelX: t ? t.left : null } }) - return { left: hr.left, right: hr.right, top: hr.top, inner, cells } + // The rows end where the columns do (#320), but the last cell's box runs + // on to the header's edge and past it (clipped), so a hover fills it all. + const row = document.querySelector('[data-testid="browse-list"] .browse-row[data-browse-path]') + return { left: hr.left, right: hr.right, rowRight: row?.getBoundingClientRect().right ?? null, top: hr.top, inner, cells } }, head) const tiles = (g) => { if (!g || !g.cells.length) return false const near = (a, b) => Math.abs(a - b) <= 0.6 return ( near(g.cells[0].left, g.left) && - near(g.cells[g.cells.length - 1].right, g.right) && + g.cells[g.cells.length - 1].right >= g.right - 0.6 && g.cells.every((c, i) => i === 0 || near(c.left, g.cells[i - 1].right)) && g.cells.every((c) => near(c.h, g.inner) && near(c.top, g.top)) ) @@ -9850,6 +9924,7 @@ async function columnHeadersScenario(fixtures) { ok(await intoFolder(), 'the Explorer shows the folder of three') await away() const g = await geometry() + ok(!!g && g.rowRight !== null && g.right - g.rowRight >= 30, `the rows end short of the header's edge (#320; ${g && Math.round(g.right - g.rowRight)}px)`) ok(tiles(g), `the cells tile the header edge to edge, each its full height (${say(g)}; header ${g?.left}-${g?.right}, ${g?.inner}px)`) const labels = Object.fromEntries(g.cells.map((c) => [c.key, c.label])) ok(