@@ -19,6 +19,18 @@ class DragCoordinator extends Manager {
1919 * @protected
2020 */
2121 singleton : true ,
22+ /**
23+ * Minimum time a native popup must remain over a remote dashboard target before
24+ * geometry-only reintegration can commit.
25+ * @member {Number} nativeWindowDropDwellMs=450
26+ */
27+ nativeWindowDropDwellMs : 450 ,
28+ /**
29+ * Quiescence delay after the last native window-position update before committing
30+ * a geometry-only drop. The browser has no mouseup during OS-titlebar drags.
31+ * @member {Number} nativeWindowDropSettleMs=250
32+ */
33+ nativeWindowDropSettleMs : 250 ,
2234 /**
2335 * @member {Map} sortZones=new Map()
2436 * @protected
@@ -32,6 +44,187 @@ class DragCoordinator extends Manager {
3244 */
3345 activeTargetZone = null
3446
47+ /**
48+ * @member {Map<String,Object>} nativeWindowDropCandidates=new Map()
49+ * @protected
50+ */
51+ nativeWindowDropCandidates = new Map ( )
52+
53+ /**
54+ * @summary Clears a pending geometry-only native window-drop candidate.
55+ *
56+ * Clears a pending geometry-only native window-drop candidate.
57+ * @param {String } windowId
58+ */
59+ clearNativeWindowDropCandidate ( windowId ) {
60+ let candidate = this . nativeWindowDropCandidates . get ( windowId ) ;
61+
62+ if ( candidate ) {
63+ clearTimeout ( candidate . timeoutId ) ;
64+ this . nativeWindowDropCandidates . delete ( windowId )
65+ }
66+ }
67+
68+ /**
69+ * @summary Commits an inferred native-titlebar popup drop into the remote dashboard path.
70+ *
71+ * Commits a conservative geometry-only native titlebar drop into the existing remote
72+ * dashboard drop path. The popup is closed only after dwell/settle intent has been inferred.
73+ * @param {String } windowId
74+ * @param {Object } candidate
75+ * @returns {Promise<void> }
76+ */
77+ async commitNativeWindowDrop ( windowId , candidate ) {
78+ let me = this ,
79+ current = me . nativeWindowDropCandidates . get ( windowId ) ;
80+
81+ if ( ! current || current !== candidate ) {
82+ return
83+ }
84+
85+ me . nativeWindowDropCandidates . delete ( windowId ) ;
86+
87+ let {
88+ draggedItem,
89+ localX,
90+ localY,
91+ offsetX,
92+ offsetY,
93+ proxyRect,
94+ sourceSortZone,
95+ targetSortZone,
96+ widgetName
97+ } = candidate ;
98+
99+ if ( sourceSortZone . getNativeWindowDrag ?. ( windowId ) ?. draggedItem !== draggedItem ) {
100+ return
101+ }
102+
103+ if ( ! targetSortZone . acceptsRemoteDrag ( localX , localY ) ) {
104+ return
105+ }
106+
107+ await sourceSortZone . suspendWindowDrag ( widgetName ) ;
108+
109+ await targetSortZone . onRemoteDragMove ( {
110+ draggedItem,
111+ localX,
112+ localY,
113+ offsetX,
114+ offsetY,
115+ proxyRect
116+ } ) ;
117+
118+ await targetSortZone . onRemoteDrop ( draggedItem ) ;
119+ sourceSortZone . onRemoteDropOut ( draggedItem ) ;
120+
121+ if ( me . activeTargetZone === targetSortZone ) {
122+ me . activeTargetZone = null
123+ }
124+ }
125+
126+ /**
127+ * @summary Finds the terminal detached dashboard item represented by a native popup window.
128+ *
129+ * Finds the terminal detached dashboard item represented by a native popup window.
130+ * @param {String } windowId
131+ * @returns {Object|null }
132+ */
133+ getNativeWindowDragSource ( windowId ) {
134+ let me = this ;
135+
136+ for ( const group of me . sortZones . values ( ) ) {
137+ for ( const sortZone of group . values ( ) ) {
138+ let drag = sortZone . getNativeWindowDrag ?. ( windowId ) ;
139+
140+ if ( drag ) {
141+ return {
142+ ...drag ,
143+ sourceSortZone : sortZone
144+ }
145+ }
146+ }
147+ }
148+
149+ return null
150+ }
151+
152+ /**
153+ * @summary Resolves a window at a point while excluding invalid native-titlebar targets.
154+ *
155+ * Resolves the first window at a global point while ignoring window ids that
156+ * cannot be valid native-titlebar drop targets, especially the popup being moved.
157+ * @param {Number } x
158+ * @param {Number } y
159+ * @param {Set<String> } excludedWindowIds
160+ * @returns {String|null }
161+ */
162+ getWindowAtExcept ( x , y , excludedWindowIds ) {
163+ let item = Window . items ?. find ( item => ! excludedWindowIds . has ( item . id ) &&
164+ item . outerRect ?. intersects ( { bottom : y , right : x , x, y} ) ) ;
165+
166+ return item ? item . id : null
167+ }
168+
169+ /**
170+ * @summary Resolves native popup geometry to a remote dashboard drop candidate.
171+ *
172+ * Resolves a native popup's current geometry to a remote dashboard drop candidate.
173+ * @param {Object } data
174+ * @param {String } data.windowId
175+ * @param {Object } sourceDrag
176+ * @returns {Object|null }
177+ */
178+ getNativeWindowDropCandidate ( data , sourceDrag ) {
179+ let me = this ,
180+ { sourceSortZone} = sourceDrag ,
181+ popupWindow = Window . get ( data . windowId ) ,
182+ popupRect = popupWindow ?. innerRect ,
183+ { sortGroup} = sourceSortZone ,
184+ targetWindowId , targetSortZone , targetWindow , localX , localY , width , height ;
185+
186+ if ( ! popupRect || ! sortGroup ) {
187+ return null
188+ }
189+
190+ localX = popupRect . x + popupRect . width / 2 ;
191+ localY = popupRect . y + popupRect . height / 2 ;
192+
193+ targetWindowId = me . getWindowAtExcept ( localX , localY , new Set ( [ data . windowId , sourceSortZone . windowId ] ) ) ;
194+
195+ if ( ! targetWindowId ) {
196+ return null
197+ }
198+
199+ targetSortZone = me . sortZones . get ( sortGroup ) ?. get ( targetWindowId ) ;
200+ targetWindow = Window . get ( targetWindowId ) ;
201+
202+ if ( ! targetSortZone || ! targetWindow ?. innerRect ) {
203+ return null
204+ }
205+
206+ localX = localX - targetWindow . innerRect . x ;
207+ localY = localY - targetWindow . innerRect . y ;
208+
209+ if ( ! targetSortZone . acceptsRemoteDrag ( localX , localY ) ) {
210+ return null
211+ }
212+
213+ width = popupRect . width ;
214+ height = popupRect . height ;
215+
216+ return {
217+ ...sourceDrag ,
218+ localX,
219+ localY,
220+ offsetX : width / 2 ,
221+ offsetY : height / 2 ,
222+ proxyRect : new Rectangle ( localX - width / 2 , localY - height / 2 , width , height ) ,
223+ targetSortZone,
224+ targetWindowId
225+ }
226+ }
227+
35228 /**
36229 * @param {Neo.draggable.container.SortZone } sourceSortZone
37230 * @param {Neo.component.Base } draggedItem
@@ -114,6 +307,57 @@ class DragCoordinator extends Manager {
114307 me . handleVoid ( sourceSortZone , draggedItem , proxyRect )
115308 }
116309
310+ /**
311+ * @summary Handles geometry updates for native OS-titlebar popup reintegration.
312+ *
313+ * Consumes high-frequency window geometry updates for native OS-titlebar popup drags,
314+ * where the browser does not emit pointer move/up events. A terminal detached popup
315+ * reintegrates only after it remains over a remote dashboard target long enough to
316+ * satisfy the settle/dwell intent contract.
317+ * @param {Object } data
318+ * @param {String } data.windowId
319+ */
320+ onWindowPositionChange ( data ) {
321+ let me = this ,
322+ { windowId} = data ,
323+ sourceDrag = me . getNativeWindowDragSource ( windowId ) ,
324+ candidate , current , dwellRemaining , firstSeenAt , delay , now ;
325+
326+ if ( ! sourceDrag ) {
327+ me . clearNativeWindowDropCandidate ( windowId ) ;
328+ return
329+ }
330+
331+ candidate = me . getNativeWindowDropCandidate ( data , sourceDrag ) ;
332+
333+ if ( ! candidate ) {
334+ me . clearNativeWindowDropCandidate ( windowId ) ;
335+ return
336+ }
337+
338+ now = Date . now ( ) ;
339+ current = me . nativeWindowDropCandidates . get ( windowId ) ;
340+
341+ firstSeenAt = current ?. targetSortZone === candidate . targetSortZone &&
342+ current ?. draggedItem === candidate . draggedItem
343+ ? current . firstSeenAt
344+ : now ;
345+
346+ me . clearNativeWindowDropCandidate ( windowId ) ;
347+
348+ dwellRemaining = Math . max ( 0 , me . nativeWindowDropDwellMs - ( now - firstSeenAt ) ) ;
349+ delay = Math . max ( me . nativeWindowDropSettleMs , dwellRemaining ) ;
350+
351+ candidate . firstSeenAt = firstSeenAt ;
352+ candidate . timeoutId = setTimeout ( ( ) => {
353+ me . commitNativeWindowDrop ( windowId , candidate ) . catch ( error => {
354+ ( Neo . logError || console . error ) ( 'Native window drop failed' , error )
355+ } )
356+ } , delay ) ;
357+
358+ me . nativeWindowDropCandidates . set ( windowId , candidate )
359+ }
360+
117361 /**
118362 * Finalizes a dashboard window drag by either dropping into the active target
119363 * or leaving the source popup as the terminal drop state.
@@ -168,6 +412,12 @@ class DragCoordinator extends Manager {
168412 me . sortZones . delete ( sortGroup )
169413 }
170414 }
415+
416+ for ( const [ windowId , candidate ] of me . nativeWindowDropCandidates . entries ( ) ) {
417+ if ( candidate . sourceSortZone === sortZone || candidate . targetSortZone === sortZone ) {
418+ me . clearNativeWindowDropCandidate ( windowId )
419+ }
420+ }
171421 }
172422
173423 /**
0 commit comments