diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 78ebe16..7855d0d 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1778,6 +1778,11 @@ onBeforeUnmount(() => {
"
class="phone-resolution-wrapper phone-resolution-wrapper--primary"
>
+
{
it('uses layout zoom so the fixed-resolution phone stays sharply rasterized', () => {
expect(source).toContain('phone-resolution-canvas--primary')
+ expect(source).toMatch(
+ /phone-resolution-wrapper--primary[\s\S]*?id="phone-home-drag-portal"[\s\S]*?phone-resolution-canvas--primary/,
+ )
+ expect(source.match(/id="phone-home-drag-portal"/g)).toHaveLength(1)
expect(source).toContain("'--phone-rendered-height'")
expect(source).toContain("'--phone-rendered-width'")
expect(mainCss).toMatch(
diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css
index 1eb36f7..b1b735a 100644
--- a/frontend/src/assets/main.css
+++ b/frontend/src/assets/main.css
@@ -295,6 +295,18 @@ button {
height: var(--phone-rendered-height, 844px);
}
+.phone-home-drag-portal {
+ position: absolute;
+ z-index: 70;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.phone-home-drag-portal * {
+ pointer-events: none !important;
+}
+
.phone-resolution-canvas {
position: absolute;
top: 0;
@@ -2622,8 +2634,30 @@ button {
.springboard--widget-dragging .springboard-widget-page-scroll {
overflow: visible;
}
-.springboard--home-dragging .springboard-page--apps {
- overflow: visible;
+.home-drag-layer {
+ position: absolute;
+ overflow: hidden;
+ pointer-events: none;
+}
+.home-drag-layer * {
+ pointer-events: none !important;
+}
+.home-drag-position {
+ position: absolute;
+ top: 0;
+ left: 0;
+ will-change: transform;
+}
+.home-drag-ghost {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ opacity: 1;
+ transition: none !important;
+ animation: none !important;
+ transform-origin: 0 0;
+ will-change: transform;
}
.app-grid {
display: grid;
@@ -2658,6 +2692,9 @@ button {
transition: transform var(--springboard-page-duration)
var(--springboard-page-easing);
}
+.app-icon-item--drag-source {
+ opacity: 0;
+}
.app-icon-remove {
position: absolute;
z-index: 4;
diff --git a/frontend/src/components/AppIcon.vue b/frontend/src/components/AppIcon.vue
index 230fb3a..d3b447c 100644
--- a/frontend/src/components/AppIcon.vue
+++ b/frontend/src/components/AppIcon.vue
@@ -30,11 +30,13 @@ const props = withDefaults(
app: PhoneAppDefinition
compact?: boolean
editMode?: boolean
+ externalDragVisual?: boolean
showLabel?: boolean
}>(),
{
compact: false,
editMode: false,
+ externalDragVisual: false,
showLabel: true,
},
)
@@ -63,14 +65,14 @@ let dragStartPage = 0
let dragPageWidth = 0
let dragPageMetrics: SpringboardDragMetrics | null = null
const dragStyle = computed(() =>
- isDragging.value
+ isDragging.value && !props.externalDragVisual
? {
transform: `translate3d(${springboardPageDragCompensation(dragStartPage, phone.currentPage, dragPageWidth)}px, 0, 0)`,
}
: undefined,
)
const dragPointerStyle = computed(() =>
- isDragging.value
+ isDragging.value && !props.externalDragVisual
? {
transform: `translate3d(${dragOffset.value.x}px, ${dragOffset.value.y}px, 0)`,
}
@@ -300,6 +302,7 @@ onBeforeUnmount(() => {
class="app-icon-item"
:class="{
'app-icon-item--compact': compact,
+ 'app-icon-item--drag-source': isDragging && externalDragVisual,
'app-icon-item--dragging': isDragging,
'app-icon-item--editing': editMode,
}"
diff --git a/frontend/src/components/HomeFolderIcon.vue b/frontend/src/components/HomeFolderIcon.vue
index bb25b93..552262c 100644
--- a/frontend/src/components/HomeFolderIcon.vue
+++ b/frontend/src/components/HomeFolderIcon.vue
@@ -22,11 +22,13 @@ const props = withDefaults(
apps: PhoneAppDefinition[]
defaultName: string
editMode?: boolean
+ externalDragVisual?: boolean
folder: HomeFolder
showLabel?: boolean
}>(),
{
editMode: false,
+ externalDragVisual: false,
showLabel: true,
},
)
@@ -55,14 +57,14 @@ let stopPointerSession: (() => void) | null = null
const folderName = computed(() => props.folder.name || props.defaultName)
const dragStyle = computed(() =>
- isDragging.value
+ isDragging.value && !props.externalDragVisual
? {
transform: `translate3d(${springboardPageDragCompensation(dragStartPage, phone.currentPage, dragPageWidth)}px, 0, 0)`,
}
: undefined,
)
const dragPointerStyle = computed(() =>
- isDragging.value
+ isDragging.value && !props.externalDragVisual
? {
transform: `translate3d(${dragOffset.value.x}px, ${dragOffset.value.y}px, 0)`,
}
@@ -232,6 +234,7 @@ onBeforeUnmount(() => {
{
- expect(viewSource).not.toContain('source.cloneNode(true)')
- expect(viewSource).not.toContain('
')
- expect(viewSource).not.toContain(':external-drag-visual')
+ it('renders the home drag visual through the unzoomed phone portal', () => {
+ expect(viewSource).toContain('source.cloneNode(true)')
expect(viewSource).toContain(
- "'springboard--home-dragging': draggingHomeApp !== null",
+ '',
)
- expect(viewSource).toContain('draggedElement?.getBoundingClientRect()')
- expect(appIconSource).not.toContain('externalDragVisual')
- expect(folderIconSource).not.toContain('externalDragVisual')
- expect(mainCss).not.toContain('.home-drag-layer')
- expect(mainCss).not.toContain('.home-drag-ghost')
- expect(mainCss).not.toContain('.app-icon-item--drag-source')
+ expect(viewSource).toContain(
+ '',
+ )
+ expect(viewSource).toContain('readPhoneViewportGeometry')
+ expect(viewSource).toMatch(/const sourceBounds = \w+\.rect\(source\)/)
+ expect(viewSource).toMatch(/const clipBounds = \w+\.rect\(\w+\)/)
+ expect(viewSource).not.toContain('homeDragLocalPoint')
+ expect(viewSource).not.toContain('springboardViewportToLocal')
+ expect(viewSource).toContain("position.className = 'home-drag-position'")
+ expect(viewSource).toMatch(
+ /homeDragGrip = \{\s*x: event\.clientX - sourceBounds\.left,\s*y: event\.clientY - sourceBounds\.top,/,
+ )
+ expect(viewSource).toMatch(
+ /ghost\.style\.transform = `scale\(\$\{\w+\.scaleX\}, \$\{\w+\.scaleY\}\)`/,
+ )
+ expect(
+ viewSource.match(/:external-drag-visual="homeDragVisualActive"/g),
+ ).toHaveLength(4)
+ expect(viewSource).toContain('updateHomeDragGhost(event)')
+ expect(viewSource).toContain('const dropGhost = homeDragGhost')
+ expect(viewSource).toContain('const dropOrigin = homeDragPreviewBounds')
+ expect(viewSource).not.toContain('dropGhost?.getBoundingClientRect()')
+ expect(viewSource).toContain('clearHomeDragGhost(dropGhost)')
+ expect(viewSource).not.toContain('draggedElement?.getBoundingClientRect()')
+ expect(viewSource).not.toContain('springboard--home-dragging')
+ expect(appIconSource).toContain('externalDragVisual?: boolean')
+ expect(folderIconSource).toContain('externalDragVisual?: boolean')
+ expect(appIconSource).toContain('app-icon-item--drag-source')
+ expect(folderIconSource).toContain('app-icon-item--drag-source')
expect(mainCss).toMatch(
- /\.springboard--home-dragging \.springboard-page--apps\s*\{[^}]*overflow:\s*visible;/s,
+ /\.phone-home-drag-portal\s*\{[^}]*pointer-events:\s*none;/s,
)
+ expect(mainCss).toContain('.home-drag-position')
+ expect(mainCss).toContain('.home-drag-ghost')
+ expect(mainCss).toContain('.app-icon-item--drag-source')
expect(mainCss).toMatch(/\.springboard\s*\{[\s\S]*?overflow:\s*hidden;/)
expect(mainCss).toMatch(
/\.springboard-page--apps\s*\{[^}]*overflow:\s*hidden;/s,
)
})
+ it('cleans up the external drag visual on every terminal path', () => {
+ expect(
+ viewSource.match(
+ /@dragstart="startHomeDrag\('(grid|dock)', [^,]+, \$event\)"/g,
+ ),
+ ).toHaveLength(4)
+ expect(viewSource.match(/@dragcancel="stopHomeDrag"/g)).toHaveLength(4)
+ expect(viewSource).toContain(
+ 'if (expectedGhost && homeDragGhost !== expectedGhost) return',
+ )
+ expect(viewSource).toMatch(
+ /if \(!dragged\) \{\s*clearHomeDragGhost\(\)\s*return/,
+ )
+ expect(viewSource).toMatch(
+ /animateHomeItemDrop\(\s*draggedItem,\s*dropArea,\s*dropIndex,\s*dropOrigin,?\s*\)\.finally\(/,
+ )
+ expect(viewSource).toMatch(
+ /function stopHomeDrag[\s\S]*?clearHomeDragGhost\(\)/,
+ )
+ expect(viewSource).toMatch(
+ /if \(folderId\) \{[\s\S]*?clearHomeDragGhost\(\)/,
+ )
+ expect(viewSource).toMatch(
+ /onBeforeUnmount\(\(\) => \{[\s\S]*?clearHomeDragGhost\(\)/,
+ )
+ })
+
it('targets the active visual page and lets grid or dock drags turn pages', () => {
expect(viewSource).toContain(':data-home-page="page.page"')
expect(viewSource).toContain(':data-home-target-offset="cell.targetOffset"')
expect(viewSource).toContain('nearestGridDropTarget')
expect(viewSource).toContain('moveHomeAppToGridPage')
expect(viewSource).not.toContain("draggingHomeApp.value?.area === 'grid'")
- expect(viewSource).toContain('event.clientX < pageBounds.left')
- expect(viewSource).toContain('event.clientY > pageBounds.bottom')
+ expect(viewSource).toContain('event.clientX < springboardBounds.left')
+ expect(viewSource).toContain('event.clientY > springboardBounds.bottom')
+ expect(viewSource).toContain('function homeDragViewportRect(')
+ expect(viewSource).toContain('if (geometry) return geometry.rect(element)')
+ expect(viewSource).toContain('homeDragViewportRect(pageElement, geometry)')
+ expect(viewSource).toContain('homeDragViewportRect(springboard, geometry)')
+ expect(viewSource).toContain('homeDragViewportRect(slot, geometry)')
+ expect(viewSource).toContain('homeDragViewportRect(dock, geometry)')
+ expect(viewSource).toMatch(
+ /homeDragViewportRect\(\s*appElement,\s*readPhoneViewportGeometry\(appElement\),?\s*\)/,
+ )
+ expect(viewSource).toContain(
+ 'springboardBounds.left + (bounds.left - pageBounds.left)',
+ )
expect(viewSource).toContain('nearestSpringboardRectIndex')
expect(viewSource).toContain("queueEdgePageTurn(lastHomePointer, 'app')")
expect(viewSource).toContain("edgePageLocked = dragType === 'widget'")
diff --git a/frontend/src/views/SpringboardView.vue b/frontend/src/views/SpringboardView.vue
index 683ac1a..9bb3598 100644
--- a/frontend/src/views/SpringboardView.vue
+++ b/frontend/src/views/SpringboardView.vue
@@ -41,6 +41,11 @@ import {
type HomeItem,
} from '@/utils/homeLayout'
import type { ReorderDirection } from '@/utils/keyboard'
+import {
+ readPhoneViewportGeometry,
+ type PhoneViewportGeometry,
+ type PhoneViewportRect,
+} from '@/utils/phoneViewportGeometry'
import { layoutSpringboardHomePages } from '@/utils/springboardLayout'
import {
maximumRenderedWidgetPage,
@@ -106,6 +111,8 @@ const draggingHomeApp = ref<{
area: HomeArea
index: number
} | null>(null)
+const homeDragLayer = ref(null)
+const homeDragVisualActive = ref(false)
const temporaryHomePage = ref(null)
const openedFolderId = ref(null)
const folderDraggingOutside = ref(false)
@@ -125,6 +132,11 @@ let edgePageDirection = 0
let edgePageLocked = false
let folderHoverTimer: number | undefined
let lastHomePointer: { clientX: number; clientY: number } | null = null
+let homeDragGhost: HTMLElement | null = null
+let homeDragGrip: { x: number; y: number } | null = null
+let homeDragGeometry: PhoneViewportGeometry | null = null
+let homeDragClipBounds: PhoneViewportRect | null = null
+let homeDragPreviewBounds: PhoneViewportRect | null = null
const installedApps = computed(() =>
PHONE_APPS.filter((app) => appStore.isInstalled(app.id)),
@@ -453,7 +465,7 @@ function startWidgetDrag(id: string, event: PointerEvent): void {
).find((candidate) => candidate.dataset.widgetId === id)
if (!widget) return
pageTransitioning.value = false
- const bounds = widget.getBoundingClientRect()
+ const bounds = homeDragViewportRect(widget)
draggingWidgetId.value = id
widgetDragGrip.value = {
x: event.clientX - bounds.left,
@@ -474,7 +486,7 @@ function updateWidgetDragPreview(event: {
)
const pageElement = grid?.closest('.springboard-page')
if (!grid || !pageElement) return
- const renderedGridBounds = grid.getBoundingClientRect()
+ const renderedGridBounds = homeDragViewportRect(grid)
const gridStyle = getComputedStyle(grid)
const scaleX =
grid.offsetWidth > 0 ? renderedGridBounds.width / grid.offsetWidth : 1
@@ -529,7 +541,7 @@ function clearTemporaryHomePage(keepCurrentPage = false): void {
function resolveHomeEdgeTurn(event: { clientX: number }) {
const springboard = document.querySelector('.springboard')
if (!springboard) return null
- const bounds = springboard.getBoundingClientRect()
+ const bounds = homeDragViewportRect(springboard)
const renderedLastPage = appPages.value.length
return resolveSpringboardHomeEdgeTurn(
event.clientX,
@@ -548,7 +560,7 @@ function queueEdgePageTurn(
): void {
const springboard = document.querySelector('.springboard')
if (!springboard) return
- const bounds = springboard.getBoundingClientRect()
+ const bounds = homeDragViewportRect(springboard)
const turn =
dragType === 'app'
? resolveHomeEdgeTurn(event)
@@ -733,10 +745,166 @@ async function saveWidgetConfig(
if (configured) changePage(configured.page)
}
-function startHomeDrag(area: HomeArea, index: number): void {
+function homeDragViewportRect(
+ element: Element,
+ geometry = homeDragGeometry ?? readPhoneViewportGeometry(element),
+): PhoneViewportRect {
+ if (geometry) return geometry.rect(element)
+ const bounds = element.getBoundingClientRect()
+ return {
+ bottom: bounds.bottom,
+ height: bounds.height,
+ left: bounds.left,
+ right: bounds.right,
+ top: bounds.top,
+ width: bounds.width,
+ }
+}
+
+function viewportRectAt(
+ left: number,
+ top: number,
+ width: number,
+ height: number,
+): PhoneViewportRect {
+ return {
+ bottom: top + height,
+ height,
+ left,
+ right: left + width,
+ top,
+ width,
+ }
+}
+
+function clearHomeDragGhost(expectedGhost?: HTMLElement | null): void {
+ if (expectedGhost && homeDragGhost !== expectedGhost) return
+ homeDragGhost?.remove()
+ homeDragGhost = null
+ homeDragGrip = null
+ homeDragGeometry = null
+ homeDragClipBounds = null
+ homeDragPreviewBounds = null
+ homeDragVisualActive.value = false
+ const layer = homeDragLayer.value
+ layer?.style.removeProperty('left')
+ layer?.style.removeProperty('top')
+ layer?.style.removeProperty('width')
+ layer?.style.removeProperty('height')
+}
+
+function updateHomeDragGhost(event: {
+ clientX: number
+ clientY: number
+}): void {
+ const ghost = homeDragGhost
+ const grip = homeDragGrip
+ const clipBounds = homeDragClipBounds
+ const previewBounds = homeDragPreviewBounds
+ if (!ghost || !grip || !clipBounds || !previewBounds) return
+
+ const left = event.clientX - grip.x
+ const top = event.clientY - grip.y
+ ghost.style.transform = `translate3d(${left - clipBounds.left}px, ${top - clipBounds.top}px, 0)`
+ homeDragPreviewBounds = viewportRectAt(
+ left,
+ top,
+ previewBounds.width,
+ previewBounds.height,
+ )
+}
+
+function createHomeDragGhost(event: PointerEvent): void {
+ clearHomeDragGhost()
+ const eventTarget =
+ event.currentTarget instanceof Element
+ ? event.currentTarget
+ : event.target instanceof Element
+ ? event.target
+ : null
+ const source = eventTarget?.closest('.app-icon-item')
+ const layer = homeDragLayer.value
+ const springboard = source?.closest('.springboard')
+ const portal = layer?.closest('.phone-home-drag-portal')
+ const geometry = readPhoneViewportGeometry(source ?? null)
+ if (!source || !layer || !springboard || !portal || !geometry) return
+
+ const portalBounds = portal.getBoundingClientRect()
+ const clipBounds = geometry.rect(springboard)
+ const sourceBounds = geometry.rect(source)
+ if (
+ clipBounds.width <= 0 ||
+ clipBounds.height <= 0 ||
+ sourceBounds.width <= 0 ||
+ sourceBounds.height <= 0
+ ) {
+ return
+ }
+
+ layer.style.left = `${clipBounds.left - portalBounds.left}px`
+ layer.style.top = `${clipBounds.top - portalBounds.top}px`
+ layer.style.width = `${clipBounds.width}px`
+ layer.style.height = `${clipBounds.height}px`
+
+ const ghost = source.cloneNode(true) as HTMLElement
+ ghost.classList.remove(
+ 'app-icon-item--dragging',
+ 'app-icon-item--editing',
+ 'app-icon-item--drag-source',
+ 'app-icon-item--drop-settling',
+ 'home-folder-item--dragging',
+ 'home-item--folder-hover',
+ )
+ ghost.classList.add('home-drag-ghost')
+ ghost.removeAttribute('style')
+ ghost
+ .querySelector('.app-icon-button')
+ ?.style.removeProperty('transform')
+ ghost.setAttribute('aria-hidden', 'true')
+ ghost.querySelector('.app-icon-remove')?.remove()
+ for (const element of [ghost, ...Array.from(ghost.querySelectorAll('*'))]) {
+ element.removeAttribute('id')
+ for (const attribute of element.getAttributeNames()) {
+ if (attribute.startsWith('data-home-')) element.removeAttribute(attribute)
+ }
+ }
+ for (const control of ghost.querySelectorAll(
+ 'a, button, input, select, textarea, [tabindex]',
+ )) {
+ control.tabIndex = -1
+ }
+
+ const position = document.createElement('div')
+ position.className = 'home-drag-position'
+ position.style.width = `${sourceBounds.width}px`
+ position.style.height = `${sourceBounds.height}px`
+ ghost.style.width = `${source.offsetWidth}px`
+ ghost.style.height = `${source.offsetHeight}px`
+ ghost.style.transform = `scale(${geometry.scaleX}, ${geometry.scaleY})`
+ position.appendChild(ghost)
+
+ homeDragGhost = position
+ homeDragGrip = {
+ x: event.clientX - sourceBounds.left,
+ y: event.clientY - sourceBounds.top,
+ }
+ homeDragGeometry = geometry
+ homeDragClipBounds = clipBounds
+ homeDragPreviewBounds = sourceBounds
+ layer.appendChild(position)
+ homeDragVisualActive.value = true
+ updateHomeDragGhost(event)
+}
+
+function startHomeDrag(
+ area: HomeArea,
+ index: number,
+ event: PointerEvent,
+): void {
clearTemporaryHomePage()
lastHomePointer = null
draggingHomeApp.value = { area, index }
+ createHomeDragGhost(event)
}
function clearFolderHover(): void {
@@ -754,10 +922,40 @@ function queueFolderHover(event: PointerEvent): void {
return
}
- const targetElement = document
- .elementsFromPoint(event.clientX, event.clientY)
- .map((element) => element.closest('[data-home-index]'))
- .find((element) => element && !element.closest('.app-icon-item--dragging'))
+ const targetElement = Array.from(
+ document.querySelectorAll('[data-home-area][data-home-index]'),
+ )
+ .filter((element) => {
+ const area = element.dataset.homeArea as HomeArea | undefined
+ const index = Number(element.dataset.homeIndex)
+ if (
+ (area !== 'grid' && area !== 'dock') ||
+ !Number.isInteger(index) ||
+ (area === dragged.area && index === dragged.index)
+ ) {
+ return false
+ }
+ if (
+ area === 'grid' &&
+ Number(
+ element.closest('[data-home-page]')?.dataset.homePage,
+ ) !== phone.currentPage
+ ) {
+ return false
+ }
+ const bounds = homeDragViewportRect(element)
+ return (
+ event.clientX >= bounds.left &&
+ event.clientX <= bounds.right &&
+ event.clientY >= bounds.top &&
+ event.clientY <= bounds.bottom
+ )
+ })
+ .sort((left, right) => {
+ const leftIsDock = left.dataset.homeArea === 'dock'
+ const rightIsDock = right.dataset.homeArea === 'dock'
+ return Number(rightIsDock) - Number(leftIsDock)
+ })[0]
const area = targetElement?.dataset.homeArea as HomeArea | undefined
const targetIndex = Number(targetElement?.dataset.homeIndex)
if (
@@ -822,6 +1020,7 @@ function queueFolderHover(event: PointerEvent): void {
if (folderId) {
draggingHomeApp.value = null
lastHomePointer = null
+ clearHomeDragGhost()
clearTemporaryHomePage()
openedFolderId.value = folderId
}
@@ -832,6 +1031,7 @@ function queueFolderHover(event: PointerEvent): void {
function moveHomeDrag(event: PointerEvent): void {
if (!draggingHomeApp.value) return
lastHomePointer = { clientX: event.clientX, clientY: event.clientY }
+ updateHomeDragGhost(event)
if (resolveHomeEdgeTurn(event)) {
clearFolderHover()
queueEdgePageTurn(event, 'app')
@@ -845,7 +1045,7 @@ async function animateHomeItemDrop(
item: HomeItem,
area: HomeArea,
index: number,
- from: DOMRect,
+ from: PhoneViewportRect,
): Promise {
await nextTick()
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
@@ -861,7 +1061,10 @@ async function animateHomeItemDrop(
)
if (!appElement) return
- const target = appElement.getBoundingClientRect()
+ const target = homeDragViewportRect(
+ appElement,
+ readPhoneViewportGeometry(appElement),
+ )
const { x: offsetX, y: offsetY } = springboardViewportDeltaToLocal(
from.left - target.left,
from.top - target.top,
@@ -924,13 +1127,16 @@ function nearestGridDropTarget(event: {
`.springboard-page--apps[data-home-page="${page}"] .app-grid`,
)
const pageElement = grid?.closest('.springboard-page')
- if (!grid || !pageElement) return null
- const pageBounds = pageElement.getBoundingClientRect()
+ const springboard = grid?.closest('.springboard')
+ if (!grid || !pageElement || !springboard) return null
+ const geometry = homeDragGeometry ?? readPhoneViewportGeometry(springboard)
+ const pageBounds = homeDragViewportRect(pageElement, geometry)
+ const springboardBounds = homeDragViewportRect(springboard, geometry)
if (
- event.clientX < pageBounds.left ||
- event.clientX > pageBounds.right ||
- event.clientY < pageBounds.top ||
- event.clientY > pageBounds.bottom
+ event.clientX < springboardBounds.left ||
+ event.clientX > springboardBounds.right ||
+ event.clientY < springboardBounds.top ||
+ event.clientY > springboardBounds.bottom
) {
return null
}
@@ -940,7 +1146,15 @@ function nearestGridDropTarget(event: {
const closestIndex = nearestSpringboardRectIndex(
event.clientX,
event.clientY,
- slots.map((slot) => slot.getBoundingClientRect()),
+ slots.map((slot) => {
+ const bounds = homeDragViewportRect(slot, geometry)
+ return {
+ height: bounds.height,
+ left: springboardBounds.left + (bounds.left - pageBounds.left),
+ top: springboardBounds.top + (bounds.top - pageBounds.top),
+ width: bounds.width,
+ }
+ }),
)
const closest = closestIndex === null ? null : slots[closestIndex]
const targetOffset = Number(closest?.dataset.homeTargetOffset)
@@ -953,7 +1167,8 @@ function nearestDockDropTarget(event: {
}): HTMLElement | null {
const dock = document.querySelector('.app-dock')
if (!dock) return null
- const dockBounds = dock.getBoundingClientRect()
+ const geometry = homeDragGeometry ?? readPhoneViewportGeometry(dock)
+ const dockBounds = homeDragViewportRect(dock, geometry)
if (
event.clientX < dockBounds.left ||
event.clientX > dockBounds.right ||
@@ -970,7 +1185,7 @@ function nearestDockDropTarget(event: {
const closestIndex = nearestSpringboardRectIndex(
event.clientX,
event.clientY,
- slots.map((slot) => slot.getBoundingClientRect()),
+ slots.map((slot) => homeDragViewportRect(slot, geometry)),
)
return closestIndex === null ? null : slots[closestIndex]
}
@@ -979,12 +1194,14 @@ function finishHomeDrag(event: PointerEvent): void {
clearEdgePageTurn()
clearFolderHover()
const dragged = draggingHomeApp.value
- if (!dragged) return
+ if (!dragged) {
+ clearHomeDragGhost()
+ return
+ }
+ updateHomeDragGhost(event)
const draggedItem = appStore.homeLayout[dragged.area][dragged.index]
- const draggedElement = document.querySelector(
- `[data-home-area="${dragged.area}"][data-home-index="${dragged.index}"]`,
- )
- const dropOrigin = draggedElement?.getBoundingClientRect()
+ const dropGhost = homeDragGhost
+ const dropOrigin = homeDragPreviewBounds ? { ...homeDragPreviewBounds } : null
const dockTarget = nearestDockDropTarget(event)
const gridTarget = dockTarget ? null : nearestGridDropTarget(event)
let dropArea = dragged.area
@@ -1022,7 +1239,14 @@ function finishHomeDrag(event: PointerEvent): void {
lastHomePointer = null
clearTemporaryHomePage(moved && gridTarget !== null)
if (moved && draggedItem && dropOrigin) {
- void animateHomeItemDrop(draggedItem, dropArea, dropIndex, dropOrigin)
+ void animateHomeItemDrop(
+ draggedItem,
+ dropArea,
+ dropIndex,
+ dropOrigin,
+ ).finally(() => clearHomeDragGhost(dropGhost))
+ } else {
+ clearHomeDragGhost()
}
}
@@ -1031,6 +1255,7 @@ function stopHomeDrag(): void {
clearFolderHover()
draggingHomeApp.value = null
lastHomePointer = null
+ clearHomeDragGhost()
clearTemporaryHomePage()
}
@@ -1043,7 +1268,9 @@ function reorderHomeApp(
const appElement = document.querySelector(
`[data-home-area="${area}"][data-home-index="${sourceIndex}"]`,
)
- const moveOrigin = appElement?.getBoundingClientRect()
+ const moveOrigin = appElement
+ ? homeDragViewportRect(appElement, readPhoneViewportGeometry(appElement))
+ : null
const targetIndex = homeKeyboardTarget(
appStore.homeLayout,
area,
@@ -1131,7 +1358,10 @@ function extractOpenedFolderApp(
`.home-folder-panel [data-folder-app-index="${sourceIndex}"]`,
)
?.closest('.app-icon-item')
- const sourceBounds = sourceElement?.getBoundingClientRect()
+ const geometry = readPhoneViewportGeometry(sourceElement ?? null)
+ const sourceBounds = sourceElement
+ ? homeDragViewportRect(sourceElement, geometry)
+ : null
const candidates = Array.from(
document.querySelectorAll('[data-home-index][data-home-area]'),
).filter((element) => {
@@ -1145,8 +1375,8 @@ function extractOpenedFolderApp(
})
const target = candidates.reduce((closest, candidate) => {
if (!closest) return candidate
- const bounds = candidate.getBoundingClientRect()
- const closestBounds = closest.getBoundingClientRect()
+ const bounds = homeDragViewportRect(candidate, geometry)
+ const closestBounds = homeDragViewportRect(closest, geometry)
const distance = Math.hypot(
event.clientX - (bounds.left + bounds.width / 2),
event.clientY - (bounds.top + bounds.height / 2),
@@ -1212,6 +1442,7 @@ onBeforeUnmount(() => {
pendingWidgetPointer = null
lastWidgetPointer = null
lastHomePointer = null
+ clearHomeDragGhost()
temporaryHomePage.value = null
draggingHomeApp.value = null
draggingWidgetId.value = null
@@ -1227,7 +1458,6 @@ onBeforeUnmount(() => {
'springboard--dragging': dragging,
'springboard--editing': editMode,
'springboard--folder-open': folderOverlayVisible,
- 'springboard--home-dragging': draggingHomeApp !== null,
'springboard--widget-dragging': draggingWidgetId !== null,
},
]"
@@ -1303,6 +1533,7 @@ onBeforeUnmount(() => {
:data-home-index="cell.sourceIndex"
:data-home-target-offset="cell.targetOffset"
:edit-mode="editMode"
+ :external-drag-visual="homeDragVisualActive"
:class="{
'home-item--folder-hover':
folderHoverTarget === `grid:${cell.sourceIndex}`,
@@ -1310,7 +1541,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
- @dragstart="startHomeDrag('grid', cell.sourceIndex)"
+ @dragstart="startHomeDrag('grid', cell.sourceIndex, $event)"
@edit="enterEditMode"
@remove="removeHomeApp(cell.app.id)"
@reorder="reorderHomeApp('grid', cell.sourceIndex, $event)"
@@ -1325,6 +1556,7 @@ onBeforeUnmount(() => {
:data-home-index="cell.sourceIndex"
:data-home-target-offset="cell.targetOffset"
:edit-mode="editMode"
+ :external-drag-visual="homeDragVisualActive"
:folder="cell.folder"
:class="{
'home-item--folder-hover':
@@ -1333,7 +1565,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
- @dragstart="startHomeDrag('grid', cell.sourceIndex)"
+ @dragstart="startHomeDrag('grid', cell.sourceIndex, $event)"
@edit="enterEditMode"
@open="openFolder(cell.folder.id)"
@reorder="reorderHomeApp('grid', cell.sourceIndex, $event)"
@@ -1484,6 +1716,10 @@ onBeforeUnmount(() => {
+
+
+
+
{
:data-home-item-key="`app-${slot.app.id}`"
:data-home-index="slot.index"
:edit-mode="editMode"
+ :external-drag-visual="homeDragVisualActive"
:show-label="false"
:class="{
'home-item--folder-hover':
@@ -1538,7 +1775,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
- @dragstart="startHomeDrag('dock', slot.index)"
+ @dragstart="startHomeDrag('dock', slot.index, $event)"
@edit="enterEditMode"
@remove="removeHomeApp(slot.app.id)"
@reorder="reorderHomeApp('dock', slot.index, $event)"
@@ -1552,6 +1789,7 @@ onBeforeUnmount(() => {
:data-home-item-key="slot.folder.id"
:data-home-index="slot.index"
:edit-mode="editMode"
+ :external-drag-visual="homeDragVisualActive"
:folder="slot.folder"
:show-label="false"
:class="{
@@ -1561,7 +1799,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
- @dragstart="startHomeDrag('dock', slot.index)"
+ @dragstart="startHomeDrag('dock', slot.index, $event)"
@edit="enterEditMode"
@open="openFolder(slot.folder.id)"
@reorder="reorderHomeApp('dock', slot.index, $event)"