import UI from '/browser/app/ui.js' import {relativePointerPosition} from '/browser-touchpad-math.js' const POINTER_SPEED=1.35 const MOVE_THRESHOLD=6 const LONG_PRESS_DELAY=560 const DOUBLE_TAP_DELAY=320 const SCROLL_STEP=18 let installedCanvas=null let touchSurface=null let pointer={x:0,y:0} let primaryTouch=null let origin=null let lastPoint=null let moved=false let longPressed=false let dragging=false let longPressTimer=null let lastTapAt=0 let secondTap=false let scrollY=null let scrollRemainder=0 function rfb(){return UI.rfb} function canvas(){return rfb()?._canvas||null} function bounds(){const target=canvas();return target?{target,rect:target.getBoundingClientRect()}:null} function syncPointer(){ const remote=rfb()?._mousePos const target=canvas() if(Number.isFinite(remote?.x)&&Number.isFinite(remote?.y)){pointer={x:remote.x,y:remote.y}} else if(target){pointer={x:target.width/2,y:target.height/2}} } function movePointer(dx,dy){ const current=rfb(),geometry=bounds() if(!current||!geometry||!geometry.rect.width||!geometry.rect.height)return const scaleX=geometry.target.width/geometry.rect.width const scaleY=geometry.target.height/geometry.rect.height pointer=relativePointerPosition(pointer,dx,dy,scaleX,scaleY,POINTER_SPEED,geometry.target.width,geometry.target.height) current._handleMouseMove(pointer.x,pointer.y) current._cursor?.move(geometry.rect.left+pointer.x/scaleX,geometry.rect.top+pointer.y/scaleY) } function click(mask=1){ const current=rfb() if(!current)return syncPointer() current._handleMouseButton(pointer.x,pointer.y,mask) current._handleMouseButton(pointer.x,pointer.y,0) } function beginDrag(){ if(dragging||!rfb())return dragging=true rfb()._handleMouseButton(pointer.x,pointer.y,1) } function endDrag(){ if(!dragging||!rfb())return rfb()._handleMouseButton(pointer.x,pointer.y,0) dragging=false } function clearLongPress(){if(longPressTimer!==null){clearTimeout(longPressTimer);longPressTimer=null}} function touchAverage(touches){let total=0;for(const touch of touches)total+=touch.clientY;return touches.length?total/touches.length:0} function wheel(direction){ const current=rfb() if(!current)return const mask=direction>0?8:16 current._handleMouseButton(pointer.x,pointer.y,mask) current._handleMouseButton(pointer.x,pointer.y,0) } function consume(event){event.preventDefault();event.stopImmediatePropagation()} function onTouchStart(event){ consume(event) canvas()?.focus() if(event.touches.length>=2){ clearLongPress();endDrag();primaryTouch=null;scrollY=touchAverage(event.touches);scrollRemainder=0;return } const touch=event.changedTouches[0] if(!touch)return syncPointer() primaryTouch=touch.identifier origin={x:touch.clientX,y:touch.clientY} lastPoint={...origin} moved=false longPressed=false secondTap=Date.now()-lastTapAt{longPressed=true;click(4);navigator.vibrate?.(25)},LONG_PRESS_DELAY) } function onTouchMove(event){ consume(event) if(event.touches.length>=2){ clearLongPress() const next=touchAverage(event.touches) if(scrollY!==null){ scrollRemainder+=next-scrollY while(Math.abs(scrollRemainder)>=SCROLL_STEP){const direction=Math.sign(scrollRemainder);wheel(direction);scrollRemainder-=direction*SCROLL_STEP} } scrollY=next return } const touch=[...event.touches].find(item=>item.identifier===primaryTouch) if(!touch||!lastPoint||!origin)return const total=Math.hypot(touch.clientX-origin.x,touch.clientY-origin.y) if(total>MOVE_THRESHOLD){moved=true;clearLongPress();if(secondTap)beginDrag()} movePointer(touch.clientX-lastPoint.x,touch.clientY-lastPoint.y) lastPoint={x:touch.clientX,y:touch.clientY} } function onTouchEnd(event){ consume(event) if(event.touches.length>=2){scrollY=touchAverage(event.touches);return} if(event.touches.length===1){ scrollY=null const touch=event.touches[0] primaryTouch=touch.identifier;origin={x:touch.clientX,y:touch.clientY};lastPoint={...origin};return } clearLongPress();endDrag() if(!moved&&!longPressed){click(1);lastTapAt=Date.now()}else if(moved){lastTapAt=0} primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false } function onTouchCancel(event){ consume(event);clearLongPress();endDrag() primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false;moved=false } function installTouchSurface(target){ if(!touchSurface){ touchSurface=document.createElement('div') touchSurface.id='codexpi-relative-touch-surface' touchSurface.setAttribute('aria-label','Pavé tactile relatif') // noVNC draws its software cursor at z-index 65535. Stay immediately // below it so touches hit this surface while the pointer remains visible. Object.assign(touchSurface.style,{position:'fixed',zIndex:'65534',background:'transparent',touchAction:'none',userSelect:'none'}) touchSurface.addEventListener('touchstart',onTouchStart,{capture:true,passive:false}) touchSurface.addEventListener('touchmove',onTouchMove,{capture:true,passive:false}) touchSurface.addEventListener('touchend',onTouchEnd,{capture:true,passive:false}) touchSurface.addEventListener('touchcancel',onTouchCancel,{capture:true,passive:false}) document.body.appendChild(touchSurface) } const rect=target.getBoundingClientRect() Object.assign(touchSurface.style,{left:`${rect.left}px`,top:`${rect.top}px`,width:`${rect.width}px`,height:`${rect.height}px`,display:rect.width&&rect.height?'block':'none'}) } function install(){ const target=canvas() if(!target){if(touchSurface)touchSurface.style.display='none';return} target.style.touchAction='none' target.style.userSelect='none' installTouchSurface(target) if(target!==installedCanvas){ installedCanvas=target syncPointer() } } window.codexpiTouchpad={click,install} window.setInterval(install,500) install()