import { on , off , getAtomBounds , Rect } from './utils' ; import type { MathfieldPrivate } from './mathfield-private' ; import { requestUpdate } from './render' ; import { Offset } from '../public/mathfield' ; import { Atom } from '../core/atom-class' ; import { acceptCommandSuggestion } from './autocomplete' ; import { selectGroup } from '../editor-model/commands-select' ; let gLastTap = null ; let gTapCount = 0 ; function isTouchEvent ( evt ) { return globalThis . TouchEvent !== undefined && evt instanceof TouchEvent ; } export function onPointerDown ( mathfield , evt ) { mathfield . _atomBoundsCache = new Map < string , Rect > ( ) ; const that = mathfield ; let anchor ; let trackingPointer = false ; let trackingWords = false ; let dirty = 'none' ; if ( evt instanceof PointerEvent && evt . buttons !== 1 && evt . buttons !== 0 ) { return ; } let scrollLeft = false ; let scrollRight = false ; const anchorX = isTouchEvent ( evt ) ? evt . touches [ 0 ] . clientX : evt . clientX ; const anchorY = isTouchEvent ( evt ) ? evt . touches [ 0 ] . clientY : evt . clientY ; const anchorTime = Date . now ( ) ; const field = that . field ! ; const scrollInterval = setInterval ( ( ) => { if ( scrollLeft ) { field . scroll ( { top : 0 , left : field . scrollLeft - 16 } ) ; } else if ( scrollRight ) { field . scroll ( { top : 0 , left : field . scrollLeft + 16 } ) ; } } , 32 ) ; function endPointerTracking ( evt ) { if ( window . PointerEvent ) { off ( field , 'pointermove' , onPointerMove ) ; off ( field , 'pointerup pointercancel' , endPointerTracking as EventListener ) ; if ( evt instanceof PointerEvent ) { field . releasePointerCapture ( evt . pointerId ) ; } } else { off ( field , 'touchmove' , onPointerMove ) ; off ( field , 'touchcancel touchend' , endPointerTracking as EventListener ) ; off ( window , 'mousemove' , onPointerMove ) ; off ( window , 'mouseup blur' , endPointerTracking as EventListener ) ; } trackingPointer = false ; clearInterval ( scrollInterval ) ; mathfield . element ! . classList . remove ( 'tracking' ) ; if ( evt ) { evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } } function onPointerMove ( evt ) { if ( ! that . hasFocus ( ) ) { endPointerTracking ( null ) ; return ; } const x = isTouchEvent ( evt ) ? evt . touches [ 0 ] . clientX : evt . clientX ; const y = isTouchEvent ( evt ) ? evt . touches [ 0 ] . clientY : evt . clientY ; const hysteresis = isTouchEvent ( evt ) || evt . pointerType === 'touch' ? 20 : 5 ; if ( Date . now ( ) < anchorTime + 500 && Math . abs ( anchorX - x ) < hysteresis && Math . abs ( anchorY - y ) < hysteresis ) { evt . preventDefault ( ) ; evt . stopPropagation ( ) ; return ; } const fieldBounds = field . getBoundingClientRect ( ) ; scrollRight = x > fieldBounds . right ; scrollLeft = x < fieldBounds . left ; let actualAnchor = anchor ; if ( evt instanceof PointerEvent ) { if ( ! evt . isPrimary ) { actualAnchor = offsetFromPoint ( that , evt . clientX , evt . clientY , { bias : 0 , } ) ; } } else if ( evt . touches && evt . touches . length === 2 ) { actualAnchor = offsetFromPoint ( that , evt . touches [ 1 ] . clientX , evt . touches [ 1 ] . clientY , { bias : 0 } ) ; } const focus = offsetFromPoint ( that , x , y , { bias : x <= anchorX ? ( x === anchorX ? 0 : - 1 ) : + 1 , } ) ; if ( trackingWords ) { } if ( actualAnchor >= 0 && focus >= 0 ) { that . model . extendSelectionTo ( actualAnchor , focus ) ; requestUpdate ( mathfield ) ; } evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } if ( gLastTap && Math . abs ( gLastTap . x - anchorX ) < 5 && Math . abs ( gLastTap . y - anchorY ) < 5 && Date . now ( ) < gLastTap . time + 500 ) { gTapCount += 1 ; gLastTap . time = anchorTime ; } else { gLastTap = { x : anchorX , y : anchorY , time : anchorTime , } ; gTapCount = 1 ; } const bounds = field . getBoundingClientRect ( ) ; if ( anchorX >= bounds . left && anchorX <= bounds . right && anchorY >= bounds . top && anchorY <= bounds . bottom ) { if ( ! mathfield . hasFocus ( ) ) { dirty = 'all' ; mathfield . keyboardDelegate ! . focus ( ) ; } mathfield . resetKeystrokeBuffer ( ) ; mathfield . smartModeSuppressed = false ; anchor = offsetFromPoint ( mathfield , anchorX , anchorY , { bias : 0 , } ) ; if ( anchor >= 0 ) { mathfield . element ! . classList . add ( 'tracking' ) ; if ( evt . shiftKey ) { const wasCollapsed = mathfield . model . selectionIsCollapsed ; mathfield . model . extendSelectionTo ( mathfield . model . anchor , anchor ) ; if ( acceptCommandSuggestion ( mathfield . model ) || wasCollapsed ) { dirty = 'all' ; } else { dirty = 'selection' ; } } else if ( mathfield . model . at ( anchor ) . type === 'placeholder' ) { mathfield . model . setSelection ( anchor - 1 , anchor ) ; dirty = 'selection' ; } else if ( mathfield . model . at ( anchor ) . rightSibling ?. type === 'placeholder' ) { mathfield . model . setSelection ( anchor , anchor + 1 ) ; dirty = 'selection' ; } else { mathfield . model . position = anchor ; if ( acceptCommandSuggestion ( mathfield . model ) ) { dirty = 'all' ; } else { dirty = 'selection' ; } } mathfield . style = { } ; if ( evt . detail === 3 || gTapCount > 2 ) { endPointerTracking ( evt ) ; if ( evt . detail === 3 || gTapCount === 3 ) { mathfield . model . selection = { ranges : [ [ 0 , mathfield . model . lastOffset ] ] , } ; dirty = 'all' ; } } else if ( ! trackingPointer ) { trackingPointer = true ; if ( window . PointerEvent ) { on ( field , 'pointermove' , onPointerMove ) ; on ( field , 'pointerup pointercancel' , endPointerTracking as EventListener ) ; if ( evt instanceof PointerEvent ) { field . setPointerCapture ( evt . pointerId ) ; } } else { on ( window , 'blur' , endPointerTracking as EventListener ) ; if ( isTouchEvent ( evt ) && evt . touches ) { on ( evt . target ! , 'touchmove' , onPointerMove ) ; on ( evt . target ! , 'touchcancel touchend' , endPointerTracking as EventListener ) ; } else { on ( window , 'mousemove' , onPointerMove ) ; on ( window , 'mouseup' , endPointerTracking as EventListener ) ; } } if ( evt . detail === 2 || gTapCount === 2 ) { trackingWords = true ; selectGroup ( mathfield . model ) ; dirty = 'all' ; } } } } else { gLastTap = null ; } if ( dirty !== 'none' ) { if ( mathfield . model . selectionIsCollapsed ) dirty = 'all' ; requestUpdate ( mathfield ) ; } evt . preventDefault ( ) ; } function distance ( x , y , r ) { const dx = x - ( r . left + r . right ) / 2 ; const dy = y - ( r . top + r . bottom ) / 2 ; return dx * dx + dy * dy ; } function nearestAtomFromPointRecursive ( mathfield , cache , atom , x , y ) { if ( ! atom . id ) return [ Infinity , null ] ; if ( cache . has ( atom . id ) ) return cache . get ( atom . id ) ! ; const bounds = getAtomBounds ( mathfield , atom ) ; if ( ! bounds ) return [ Infinity , null ] ; let result = [ atom . type === 'group' ? Infinity : distance ( x , y , bounds ) , atom , ] ; if ( ! atom . captureSelection && x >= bounds . left && x <= bounds . right && atom . hasChildren ) { for ( const child of atom . children ) { const r = nearestAtomFromPointRecursive ( mathfield , cache , child , x , y ) ; if ( r [ 0 ] < result [ 0 ] ) result = r ; } } if ( ! result [ 1 ] ) { result = [ distance ( x , y , bounds ) , atom ] ; } cache . set ( atom . id , result ) ; return result ; } function nearestAtomFromPoint ( mathfield , x , y ) { const [ , atom ] = nearestAtomFromPointRecursive ( mathfield , new Map ( ) , mathfield . model . root , x , y ) ! ; return atom ! ; } param options . bias export function offsetFromPoint ( mathfield , x , y , options ? ) { const bounds = mathfield . fieldContent ! . getBoundingClientRect ( ) ; if ( x > bounds . right || y > bounds . bottom + 8 ) { return mathfield . model . lastOffset ; } if ( x < bounds . left || y < bounds . top - 8 ) { return 0 ; } options = options ?? { } ; options . bias = options . bias ?? 0 ; let atom = nearestAtomFromPoint ( mathfield , x , y ) ; const parents = [ ] ; let parent = atom ; while ( parent ) { parents . unshift ( parent ) ; parent = parent . parent ! ; } for ( const x of parents ) { if ( x . captureSelection ) { atom = x ; break ; } } let result = mathfield . model . offsetOf ( atom ) ; if ( result < 0 ) return - 1 ; if ( atom . leftSibling ) { if ( options . bias === 0 && atom . type !== 'placeholder' ) { const bounds = getAtomBounds ( mathfield , atom ) ; if ( bounds && x < ( bounds . left + bounds . right ) / 2 ) { result = mathfield . model . offsetOf ( atom . leftSibling ) ; } } else if ( options . bias < 0 ) { result = mathfield . model . offsetOf ( atom . leftSibling ) ; } } return result ; }