From dca0868753bbe4b964702e2d63fa9f63ecc73eb8 Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Thu, 20 Aug 2026 19:14:00 +0200 Subject: [PATCH] FIX - stabilize homescreen dragging across scaling (#18) * FIX - stabilize homescreen dragging across scaling Render the home drag preview in an unzoomed viewport portal and normalize FiveM CEF DOM geometry before hit testing and drop settling. Cover Chrome 103, fractional scaling, 4K, ultrawide, and non-16:9 viewport behavior. * FIX - Implement LB guest app layout and lifecycle bridge Ensure custom LB apps are properly sized and visible by applying the required CSS contract on initialization and upon receiving the 'componentsLoaded' signal. * FIX - match uppercase script tags in LB bridge test --- frontend/src/App.vue | 5 + .../AppDevelopmentPreview.contract.test.ts | 4 + frontend/src/assets/main.css | 41 ++- frontend/src/components/AppIcon.vue | 7 +- frontend/src/components/HomeFolderIcon.vue | 7 +- frontend/src/utils/lbPhoneAppBridge.test.ts | 74 +++++ frontend/src/utils/lbPhoneAppBridge.ts | 29 +- .../src/utils/phoneViewportGeometry.test.ts | 174 ++++++++++ frontend/src/utils/phoneViewportGeometry.ts | 73 +++++ frontend/src/utils/springboardDrag.test.ts | 52 +++ frontend/src/utils/springboardDrag.ts | 6 +- .../views/SpringboardView.contract.test.ts | 91 +++++- frontend/src/views/SpringboardView.vue | 308 ++++++++++++++++-- 13 files changed, 814 insertions(+), 57 deletions(-) create mode 100644 frontend/src/utils/phoneViewportGeometry.test.ts create mode 100644 frontend/src/utils/phoneViewportGeometry.ts 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(() => {
', + { + appName: 'radio-app', + localStorage: {}, + resourceName: 'lb-radioapp', + settings: createLbPhoneHostSettings({ + deviceName: 'Main phone', + isDarkMode: true, + language: 'en', + preferences: DEFAULT_PHONE_PREFERENCES, + securityEnabled: false, + }), + ui: 'https://cfx-nui-lb-radioapp/ui/dist/index.html', + }, + ) + const runtime = /