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
27 changes: 27 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,33 @@ export const App = (): React.ReactElement => {
}
}, [])

/*
* A file dropped anywhere but the files tree does nothing.
*
* The browser's own answer to a dropped file is to *navigate to it*, which
* here means the IDE is replaced by somebody's screen recording and every
* terminal on screen is gone -- the sessions survive, being tmux, but the
* page has to be loaded again. The tree accepts a drop on purpose (see
* `FilesPane`); this is what makes a near miss cost nothing instead.
*
* Taking `dragover` is what makes a drop *possible*, which reads backwards
* until you know the default: refusing the drag means the browser handles the
* drop itself, and handling it means we can decline to do anything.
*/
useEffect(() => {
const swallow = (event: DragEvent): void => {
if (event.dataTransfer?.types.includes('Files') !== true) return
event.preventDefault()
if (event.type === 'dragover') event.dataTransfer.dropEffect = 'none'
}
window.addEventListener('dragover', swallow)
window.addEventListener('drop', swallow)
return () => {
window.removeEventListener('dragover', swallow)
window.removeEventListener('drop', swallow)
}
}, [])

useEffect(() => {
const unbind = bindSocketToStore()
void refresh()
Expand Down
5 changes: 4 additions & 1 deletion web/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,10 @@ export const TopBar = ({
</button>
<button
className="topbar__signout"
onClick={() => void signOut()}
onClick={() => {
// The icon sits beside the machine's terminal, one slip away from it.
if (window.confirm('Sign out of this browser?')) void signOut()
}}
title="Sign out of this browser"
aria-label="Sign out"
>
Expand Down
19 changes: 18 additions & 1 deletion web/src/views/FilesPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1370,8 +1370,25 @@ export const FilesPane = ({
const at = rows.findIndex((row) => row.path === (cursor ?? files.path))
const here = at === -1 ? (rows.length > 0 ? 0 : -1) : at

/*
* The open file's row is kept in view by scrolling the tree, and only the
* tree. `scrollIntoView` scrolls every scroller above the row as well, and the
* row of windows is one of them: the click that opened the file had already
* started a smooth scroll to bring a window hanging off the edge over (see
* `revealTile`), and any scroll on that element -- even one to where it
* already is -- cancels it. Measured: opening a second file in a window
* hanging 385px off the right edge asked for 2779 -> 3176, and the row stayed
* at 2779. The first file opened worked only because the window grows then,
* and that reveal runs after this.
*/
useLayoutEffect(() => {
selectedRef.current?.scrollIntoView({ block: 'nearest' })
const row = selectedRef.current
const tree = row?.closest<HTMLElement>('.files__tree')
if (!row || !tree) return
const at = row.getBoundingClientRect()
const box = tree.getBoundingClientRect()
if (at.top < box.top) tree.scrollTop += at.top - box.top
else if (at.bottom > box.bottom) tree.scrollTop += at.bottom - box.bottom
}, [files.path])

/*
Expand Down
22 changes: 19 additions & 3 deletions web/src/views/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,8 @@ export const Overview = ({
const steering = useRef(false)
/* Where the last glide was sent, in px; NaN once a hand has taken the row. */
const sentTo = useRef(Number.NaN)
/* A request is waiting, a frame at a time, for the row to be wide enough. */
const waiting = useRef(false)
useEffect(() => {
if (target === undefined || width === 0 || scrollTo === null) return
if (answered.current === scrollTo.nonce) return
Expand Down Expand Up @@ -2008,16 +2010,22 @@ export const Overview = ({
* and a half at most, and not at all once a finger or a wheel has taken
* the row, or another request has sent it somewhere else.
*/
if (grid.scrollWidth - grid.clientWidth < aim - 2) {
waiting.current = grid.scrollWidth - grid.clientWidth < aim - 2
if (waiting.current) {
let frames = 0
const wait = (): void => {
if (sentTo.current !== aim) return
if (sentTo.current !== aim) {
waiting.current = false
return
}
if (grid.scrollWidth - grid.clientWidth >= aim - 2) {
waiting.current = false
steering.current = true
grid.scrollTo({ left: aim, behavior: 'smooth' })
return
}
if (++frames < 90) requestAnimationFrame(wait)
else waiting.current = false
}
requestAnimationFrame(wait)
}
Expand Down Expand Up @@ -2066,8 +2074,16 @@ export const Overview = ({
* request that is still waiting for the row to be wide enough -- see the
* request above -- and cancelling that wait left `alpha` lit one pixel
* past the screen's edge after a reload.
*
* Only while that wait is still running. `sentTo` outlives the glide --
* only a finger, a wheel or this clears it -- so on its own it went on
* saying "on its way" long after the row had arrived and been moved again,
* and a reveal that happened to want the same offset did nothing. Measured
* after a reload at 1600px: opening a file in a window hanging off the
* right edge wanted 3176, the reload's glide had been sent to 3176, and
* the row stayed at 2779.
*/
if (offset * pitch === sentTo.current) return
if (waiting.current && offset * pitch === sentTo.current) return
steering.current = true
sentTo.current = Number.NaN
grid.scrollTo({ left: offset * pitch, behavior: 'smooth' })
Expand Down
Loading