diff --git a/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.test.ts b/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.test.ts index 929e096d9f..c7fec2de42 100644 --- a/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.test.ts +++ b/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.test.ts @@ -112,6 +112,48 @@ describe('MouseGlowService', () => { surface.remove(); }); + it('only reads geometry for the selected surface', () => { + const surface = document.createElement('div'); + surface.className = 'workspace-card'; + surface.style.border = '1px solid black'; + surface.style.borderRadius = '8px'; + surface.style.background = 'black'; + const readSurfaceGeometry = vi.fn(() => ({ + bottom: 128, + height: 80, + left: 20, + right: 220, + top: 48, + width: 200, + x: 20, + y: 48, + toJSON: () => ({}), + })); + surface.getBoundingClientRect = readSurfaceGeometry; + const wrapper = document.createElement('div'); + const content = document.createElement('code'); + const readWrapperGeometry = vi.fn(); + const readContentGeometry = vi.fn(); + wrapper.getBoundingClientRect = readWrapperGeometry; + content.getBoundingClientRect = readContentGeometry; + wrapper.appendChild(content); + surface.appendChild(wrapper); + document.body.appendChild(surface); + service.initialize(); + + content.dispatchEvent(new MouseEvent('pointermove', { + bubbles: true, + clientX: 72, + clientY: 68, + })); + nextFrame?.(0); + + expect(readContentGeometry).not.toHaveBeenCalled(); + expect(readWrapperGeometry).not.toHaveBeenCalled(); + expect(readSurfaceGeometry).toHaveBeenCalledTimes(1); + surface.remove(); + }); + it('keeps the glow up until the frame resolves where the pointer went', () => { const surface = document.createElement('div'); surface.setAttribute('data-mouse-glow-surface', ''); @@ -249,6 +291,8 @@ describe('MouseGlowService', () => { const stackingHost = document.createElement('div'); stackingHost.style.position = 'relative'; stackingHost.style.zIndex = '1'; + const readStackingHostGeometry = vi.fn(); + stackingHost.getBoundingClientRect = readStackingHostGeometry; const surface = document.createElement('div'); surface.setAttribute('data-mouse-glow-surface', ''); surface.getBoundingClientRect = () => ({ @@ -295,7 +339,9 @@ describe('MouseGlowService', () => { const overlay = document.getElementById('bitfun-mouse-glow-overlay'); expect(overlay?.hasAttribute('data-active')).toBe(true); - expect(overlay?.parentElement).toBe(stackingHost); + expect(overlay?.parentElement).toBe(document.body); + expect(overlay?.hasAttribute('data-local-position')).toBe(false); + expect(readStackingHostGeometry).not.toHaveBeenCalled(); expect(Number(window.getComputedStyle(listbox).zIndex)).toBeGreaterThan(49); option.dispatchEvent(new MouseEvent('pointermove', { @@ -359,7 +405,7 @@ describe('MouseGlowService', () => { transformedHost.style.position = 'absolute'; transformedHost.style.zIndex = '100'; transformedHost.style.transform = 'translateX(-50%)'; - transformedHost.getBoundingClientRect = () => ({ + const readTransformedHostGeometry = vi.fn(() => ({ bottom: 700, height: 200, left: 100, @@ -369,7 +415,8 @@ describe('MouseGlowService', () => { x: 100, y: 500, toJSON: () => ({}), - }); + })); + transformedHost.getBoundingClientRect = readTransformedHostGeometry; const inputSurface = document.createElement('div'); inputSurface.setAttribute('data-mouse-glow-surface', ''); inputSurface.style.border = '1px solid black'; @@ -400,11 +447,12 @@ describe('MouseGlowService', () => { nextFrame?.(0); const overlay = document.getElementById('bitfun-mouse-glow-overlay'); - expect(overlay?.parentElement).toBe(transformedHost); - expect(overlay?.hasAttribute('data-local-position')).toBe(true); - expect(overlay?.style.transform).toBe('translate3d(20px, 40px, 0)'); + expect(overlay?.parentElement).toBe(document.body); + expect(overlay?.hasAttribute('data-local-position')).toBe(false); + expect(overlay?.style.transform).toBe('translate3d(120px, 540px, 0)'); expect(overlay?.style.getPropertyValue('--mouse-glow-local-x')).toBe('60px'); expect(overlay?.style.getPropertyValue('--mouse-glow-local-y')).toBe('20px'); + expect(readTransformedHostGeometry).not.toHaveBeenCalled(); transformedHost.remove(); }); diff --git a/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.ts b/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.ts index 0a8ee16129..5188bca00c 100644 --- a/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.ts +++ b/src/web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.ts @@ -279,7 +279,11 @@ export class MouseGlowService { overlayHost, style, ); - const overlayPosition = this.getOverlayPosition(geometry, overlayHost); + const overlayPosition = this.getOverlayPosition( + geometry, + overlayHost, + overlayHost === surface ? rect : null, + ); this.activeSurface = surface; overlay.toggleAttribute('data-divider', dividerGeometry !== null); overlay.toggleAttribute('data-local-position', overlayPosition.isLocal); @@ -319,6 +323,7 @@ export class MouseGlowService { private getOverlayPosition( geometry: OverlayGeometry, host: HTMLElement, + knownHostRect: DOMRect | null, ): { isLocal: boolean; left: number; top: number } { if (host === document.body) { return { isLocal: false, left: geometry.left, top: geometry.top }; @@ -329,7 +334,7 @@ export class MouseGlowService { return { isLocal: false, left: geometry.left, top: geometry.top }; } - const hostRect = host.getBoundingClientRect(); + const hostRect = knownHostRect ?? host.getBoundingClientRect(); const borderLeftWidth = parseFloat(hostStyle.borderLeftWidth) || 0; const borderTopWidth = parseFloat(hostStyle.borderTopWidth) || 0; return { @@ -408,11 +413,6 @@ export class MouseGlowService { return false; } - const rect = element.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) { - return false; - } - if (!this.isVisibleElement(style)) { return false; } @@ -441,7 +441,6 @@ export class MouseGlowService { return false; } - const rect = element.getBoundingClientRect(); if (!this.isVisibleElement(style)) { return false; } @@ -451,7 +450,7 @@ export class MouseGlowService { return true; } - return this.hasDividerSemantics(element) && this.isLineLikeGeometry(rect); + return this.hasDividerSemantics(element); } private getDividerGeometry( @@ -485,7 +484,7 @@ export class MouseGlowService { } } - if (this.hasDividerSemantics(element)) { + if (this.hasDividerSemantics(element) && this.isLineLikeGeometry(rect)) { return { height: Math.max(rect.height, 1), left: rect.left, @@ -557,9 +556,6 @@ export class MouseGlowService { if (this.isFloatingLayer(current)) { return current; } - if (this.createsStackingContext(current)) { - return current; - } current = current.parentElement; } return document.body; @@ -585,22 +581,6 @@ export class MouseGlowService { ); } - private createsStackingContext(element: HTMLElement): boolean { - const style = window.getComputedStyle(element); - const positionedWithZIndex = - style.position !== 'static' && style.zIndex !== 'auto'; - - return ( - positionedWithZIndex - || style.position === 'fixed' - || style.position === 'sticky' - || style.isolation === 'isolate' - || (style.opacity !== '' && style.opacity !== '1') - || (style.mixBlendMode !== '' && style.mixBlendMode !== 'normal') - || style.willChange.includes('opacity') - ); - } - private isVisibleElement(style: CSSStyleDeclaration): boolean { return ( style.display !== 'none'