Pointer lock when mouse-dragging to scroll.
This commit is contained in:
		| @@ -126,8 +126,9 @@ let lastScrollRange = { startVW: null, endVW: null } | |||||||
| let pendingRebuild = false | let pendingRebuild = false | ||||||
| // Week label column drag scrolling state (no momentum) | // Week label column drag scrolling state (no momentum) | ||||||
| const isWeekColDragging = ref(false) | const isWeekColDragging = ref(false) | ||||||
| let weekColDragStartY = 0 |  | ||||||
| let weekColDragStartScroll = 0 | let weekColDragStartScroll = 0 | ||||||
|  | let weekColAccum = 0 | ||||||
|  | let weekColPointerLocked = false | ||||||
|  |  | ||||||
| function scheduleRebuild(reason) { | function scheduleRebuild(reason) { | ||||||
|   if (pendingRebuild) return |   if (pendingRebuild) return | ||||||
| @@ -519,8 +520,9 @@ function handleWeekColMouseDown(e) { | |||||||
|   if (!rect) return |   if (!rect) return | ||||||
|   if (e.clientX < rect.left || e.clientX > rect.right) return |   if (e.clientX < rect.left || e.clientX > rect.right) return | ||||||
|   isWeekColDragging.value = true |   isWeekColDragging.value = true | ||||||
|   weekColDragStartY = e.clientY |  | ||||||
|   weekColDragStartScroll = viewport.value.scrollTop |   weekColDragStartScroll = viewport.value.scrollTop | ||||||
|  |   weekColAccum = 0 | ||||||
|  |   if (viewport.value.requestPointerLock) viewport.value.requestPointerLock() | ||||||
|   window.addEventListener('mousemove', handleWeekColMouseMove, { passive: false }) |   window.addEventListener('mousemove', handleWeekColMouseMove, { passive: false }) | ||||||
|   window.addEventListener('mouseup', handleWeekColMouseUp, { passive: false }) |   window.addEventListener('mouseup', handleWeekColMouseUp, { passive: false }) | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
| @@ -529,9 +531,13 @@ function handleWeekColMouseDown(e) { | |||||||
|  |  | ||||||
| function handleWeekColMouseMove(e) { | function handleWeekColMouseMove(e) { | ||||||
|   if (!isWeekColDragging.value || !viewport.value) return |   if (!isWeekColDragging.value || !viewport.value) return | ||||||
|   const dy = e.clientY - weekColDragStartY |   const dy = weekColPointerLocked ? e.movementY : e.clientY // movementY if locked | ||||||
|   // Natural: drag down moves view to earlier content (scroll up) |   weekColAccum += dy | ||||||
|   viewport.value.scrollTop = Math.max(0, weekColDragStartScroll - dy) |   let desired = weekColDragStartScroll - weekColAccum | ||||||
|  |   if (desired < 0) desired = 0 | ||||||
|  |   const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value) | ||||||
|  |   if (desired > maxScroll) desired = maxScroll | ||||||
|  |   viewport.value.scrollTop = desired | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -542,9 +548,17 @@ function handleWeekColMouseUp(e) { | |||||||
|   isWeekColDragging.value = false |   isWeekColDragging.value = false | ||||||
|   window.removeEventListener('mousemove', handleWeekColMouseMove) |   window.removeEventListener('mousemove', handleWeekColMouseMove) | ||||||
|   window.removeEventListener('mouseup', handleWeekColMouseUp) |   window.removeEventListener('mouseup', handleWeekColMouseUp) | ||||||
|  |   if (weekColPointerLocked && document.exitPointerLock) document.exitPointerLock() | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function handlePointerLockChange() { | ||||||
|  |   weekColPointerLocked = document.pointerLockElement === viewport.value | ||||||
|  |   if (!weekColPointerLocked && isWeekColDragging.value) { | ||||||
|  |     handleWeekColMouseUp(new MouseEvent('mouseup')) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| const onScroll = () => { | const onScroll = () => { | ||||||
|   if (viewport.value) scrollTop.value = viewport.value.scrollTop |   if (viewport.value) scrollTop.value = viewport.value.scrollTop | ||||||
|   scheduleRebuild('scroll') |   scheduleRebuild('scroll') | ||||||
| @@ -567,6 +581,7 @@ onMounted(() => { | |||||||
|   // Capture mousedown in viewport to allow dragging via week label column |   // Capture mousedown in viewport to allow dragging via week label column | ||||||
|   viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true) |   viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true) | ||||||
|   } |   } | ||||||
|  |   document.addEventListener('pointerlockchange', handlePointerLockChange) | ||||||
|  |  | ||||||
|   const timer = setInterval(() => { |   const timer = setInterval(() => { | ||||||
|     calendarStore.updateCurrentDate() |     calendarStore.updateCurrentDate() | ||||||
| @@ -598,6 +613,7 @@ onBeforeUnmount(() => { | |||||||
|       rowProbeObserver.disconnect() |       rowProbeObserver.disconnect() | ||||||
|     } catch (e) {} |     } catch (e) {} | ||||||
|   } |   } | ||||||
|  |   document.removeEventListener('pointerlockchange', handlePointerLockChange) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const handleDayMouseDown = (d) => { | const handleDayMouseDown = (d) => { | ||||||
|   | |||||||
| @@ -21,9 +21,10 @@ const jogwheelContent = ref(null) | |||||||
| const syncLock = ref(null) | const syncLock = ref(null) | ||||||
| // Drag state (no momentum, 1:1 mapping) | // Drag state (no momentum, 1:1 mapping) | ||||||
| const isDragging = ref(false) | const isDragging = ref(false) | ||||||
| let dragStartY = 0 |  | ||||||
| let mainStartScroll = 0 | let mainStartScroll = 0 | ||||||
| let dragScale = 1 // mainScrollPixels per mouse pixel | let dragScale = 1 // mainScrollPixels per mouse pixel | ||||||
|  | let accumDelta = 0 | ||||||
|  | let pointerLocked = false | ||||||
|  |  | ||||||
| // Jogwheel content height is 1/10th of main calendar | // Jogwheel content height is 1/10th of main calendar | ||||||
| const jogwheelHeight = computed(() => { | const jogwheelHeight = computed(() => { | ||||||
| @@ -38,8 +39,8 @@ const handleJogwheelScroll = () => { | |||||||
| function onDragMouseDown(e) { | function onDragMouseDown(e) { | ||||||
|   if (e.button !== 0) return |   if (e.button !== 0) return | ||||||
|   isDragging.value = true |   isDragging.value = true | ||||||
|   dragStartY = e.clientY |  | ||||||
|   mainStartScroll = props.scrollTop |   mainStartScroll = props.scrollTop | ||||||
|  |   accumDelta = 0 | ||||||
|   // Precompute scale between jogwheel scrollable range and main scrollable range |   // Precompute scale between jogwheel scrollable range and main scrollable range | ||||||
|   const mainScrollable = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight) |   const mainScrollable = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight) | ||||||
|   let jogScrollable = 0 |   let jogScrollable = 0 | ||||||
| @@ -48,6 +49,10 @@ function onDragMouseDown(e) { | |||||||
|   } |   } | ||||||
|   dragScale = jogScrollable > 0 ? mainScrollable / jogScrollable : 1 |   dragScale = jogScrollable > 0 ? mainScrollable / jogScrollable : 1 | ||||||
|   if (!isFinite(dragScale) || dragScale <= 0) dragScale = 1 |   if (!isFinite(dragScale) || dragScale <= 0) dragScale = 1 | ||||||
|  |   // Attempt pointer lock for relative movement | ||||||
|  |   if (jogwheelViewport.value && jogwheelViewport.value.requestPointerLock) { | ||||||
|  |     jogwheelViewport.value.requestPointerLock() | ||||||
|  |   } | ||||||
|   window.addEventListener('mousemove', onDragMouseMove, { passive: false }) |   window.addEventListener('mousemove', onDragMouseMove, { passive: false }) | ||||||
|   window.addEventListener('mouseup', onDragMouseUp, { passive: false }) |   window.addEventListener('mouseup', onDragMouseUp, { passive: false }) | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
| @@ -55,9 +60,9 @@ function onDragMouseDown(e) { | |||||||
|  |  | ||||||
| function onDragMouseMove(e) { | function onDragMouseMove(e) { | ||||||
|   if (!isDragging.value) return |   if (!isDragging.value) return | ||||||
|   const dy = e.clientY - dragStartY |   const dy = pointerLocked ? e.movementY : e.clientY // movementY only valid in pointer lock | ||||||
|   // Natural content drag (drag down => scrollTop decreases) |   accumDelta += dy | ||||||
|   let desired = mainStartScroll - dy * dragScale |   let desired = mainStartScroll - accumDelta * dragScale | ||||||
|   if (desired < 0) desired = 0 |   if (desired < 0) desired = 0 | ||||||
|   const maxScroll = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight) |   const maxScroll = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight) | ||||||
|   if (desired > maxScroll) desired = maxScroll |   if (desired > maxScroll) desired = maxScroll | ||||||
| @@ -70,13 +75,23 @@ function onDragMouseUp(e) { | |||||||
|   isDragging.value = false |   isDragging.value = false | ||||||
|   window.removeEventListener('mousemove', onDragMouseMove) |   window.removeEventListener('mousemove', onDragMouseMove) | ||||||
|   window.removeEventListener('mouseup', onDragMouseUp) |   window.removeEventListener('mouseup', onDragMouseUp) | ||||||
|  |   if (pointerLocked && document.exitPointerLock) document.exitPointerLock() | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function handlePointerLockChange() { | ||||||
|  |   pointerLocked = document.pointerLockElement === jogwheelViewport.value | ||||||
|  |   if (!pointerLocked && isDragging.value) { | ||||||
|  |     // Pointer lock lost (Esc) -> end drag gracefully | ||||||
|  |     onDragMouseUp(new MouseEvent('mouseup')) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   if (jogwheelViewport.value) { |   if (jogwheelViewport.value) { | ||||||
|     jogwheelViewport.value.addEventListener('mousedown', onDragMouseDown) |     jogwheelViewport.value.addEventListener('mousedown', onDragMouseDown) | ||||||
|   } |   } | ||||||
|  |   document.addEventListener('pointerlockchange', handlePointerLockChange) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| onBeforeUnmount(() => { | onBeforeUnmount(() => { | ||||||
| @@ -85,6 +100,7 @@ onBeforeUnmount(() => { | |||||||
|   } |   } | ||||||
|   window.removeEventListener('mousemove', onDragMouseMove) |   window.removeEventListener('mousemove', onDragMouseMove) | ||||||
|   window.removeEventListener('mouseup', onDragMouseUp) |   window.removeEventListener('mouseup', onDragMouseUp) | ||||||
|  |   document.removeEventListener('pointerlockchange', handlePointerLockChange) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const syncFromJogwheel = () => { | const syncFromJogwheel = () => { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko