From fa58f66b384faf13746f8aa3e2bca266fdde3530 Mon Sep 17 00:00:00 2001 From: "burrito.space" Date: Thu, 12 Feb 2026 11:00:25 +0100 Subject: [PATCH] =?UTF-8?q?fix(drag):=20unified=20window=20drag=20system?= =?UTF-8?q?=20=E2=80=94=20faster,=20works=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the fragmented drag implementations (preload.js, app/drag.js, cmd/panel.js custom handler) with a single robust system in preload.js. Key improvements: - Reduced hold delay from 300ms to 150ms for much snappier feel - Window position fetched eagerly on mousedown (overlaps with hold timer) so drag starts instantly when timer fires (no async wait after delay) - Window ID cached at init time (fetched once, never changes) - Added e.buttons === 0 safety check during mousemove - Event listeners use capture phase for reliable event interception - Removed -webkit-app-region CSS checks (only data-no-drag matters now) Removed duplicate implementations: - cmd/panel.js: removed custom drag handler (preload handles it) - cmd/panel.html: removed -webkit-app-region: no-drag CSS (not needed) - app/drag.js: replaced with no-op stub (settings.html import compat) Architecture: preload.js is the single source of truth. Windows opt out via data-no-drag on body (e.g., peek://page has its own canvas drag). --- app/drag.js | 135 ++------------------------------------ extensions/cmd/panel.html | 12 ++-- extensions/cmd/panel.js | 111 ++----------------------------- notes/ux-rules.md | 25 +++++-- preload.js | 109 ++++++++++++++++++++---------- 5 files changed, 110 insertions(+), 282 deletions(-) diff --git a/app/drag.js b/app/drag.js index 96d41734..688183ec 100644 --- a/app/drag.js +++ b/app/drag.js @@ -1,136 +1,11 @@ // app/drag.js -// Click-and-hold window dragging utility - -const HOLD_DELAY = 300; // ms before drag starts -const MOVE_THRESHOLD = 5; // px - cancel hold if mouse moves more than this - -let isDragging = false; -let holdTimer = null; -let startMouse = null; -let startWindowPos = null; -let windowId = null; - -// Elements that should not trigger drag -const isInteractive = (el) => { - if (!el) return false; - const tag = el.tagName?.toLowerCase(); - if (['input', 'textarea', 'button', 'a', 'select', 'label'].includes(tag)) return true; - if (el.isContentEditable) return true; - if (el.hasAttribute('data-no-drag')) return true; - if (el.closest('[data-no-drag]')) return true; - // Check for -webkit-app-region: no-drag - try { - const style = getComputedStyle(el); - if (style.webkitAppRegion === 'no-drag') return true; - } catch (e) { - // Ignore errors from pseudo-elements - } - return false; -}; - -const cancelHold = () => { - if (holdTimer) { - clearTimeout(holdTimer); - holdTimer = null; - } -}; - -const endDrag = () => { - cancelHold(); - if (isDragging) { - isDragging = false; - document.body.style.cursor = ''; - document.body.classList.remove('is-dragging'); - } - startMouse = null; - startWindowPos = null; - windowId = null; -}; - -const onMouseDown = async (e) => { - // Only left click, not on interactive elements - if (e.button !== 0) return; - if (isInteractive(e.target)) return; - - startMouse = { x: e.screenX, y: e.screenY }; - - holdTimer = setTimeout(async () => { - try { - // Check if window is draggable (API option can disable this) - const draggableResult = await window.app.invoke('window-is-draggable'); - if (!draggableResult?.draggable) return; - - // Get window ID and position - windowId = await window.app.invoke('get-window-id'); - if (!windowId) return; - - const pos = await window.app.window.getPosition(); - if (!pos.success) return; - - startWindowPos = { x: pos.x, y: pos.y }; - isDragging = true; - document.body.style.cursor = 'grabbing'; - document.body.classList.add('is-dragging'); - } catch (err) { - console.error('Failed to start drag:', err); - } - }, HOLD_DELAY); -}; - -const onMouseMove = (e) => { - if (!startMouse) return; - - if (!isDragging) { - // Cancel hold if mouse moves too much before delay - const dx = Math.abs(e.screenX - startMouse.x); - const dy = Math.abs(e.screenY - startMouse.y); - if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) { - cancelHold(); - startMouse = null; - } - return; - } - - // Calculate and apply new position - const deltaX = e.screenX - startMouse.x; - const deltaY = e.screenY - startMouse.y; - const newX = startWindowPos.x + deltaX; - const newY = startWindowPos.y + deltaY; - - window.app.window.move(windowId, newX, newY); -}; - -const onMouseUp = () => { - endDrag(); -}; - -// Also end drag if window loses focus -const onBlur = () => { - endDrag(); -}; +// Window dragging is now handled by the unified drag system in preload.js. +// This module is kept as a no-op stub so existing imports (e.g., settings.html) +// don't break. The preload drag runs automatically in every window. /** - * Initialize click-and-hold window dragging - * Call this once when the page loads + * No-op: drag is handled by preload.js */ export function initWindowDrag() { - // Check if window.app is available - if (!window.app?.window?.move || !window.app?.invoke) { - console.warn('Window drag: app API not available'); - return; - } - - document.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - window.addEventListener('blur', onBlur); - - console.log('Window drag initialized'); -} - -// Auto-initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initWindowDrag); -} else { - initWindowDrag(); + // Preload handles window dragging for all windows. } diff --git a/extensions/cmd/panel.html b/extensions/cmd/panel.html index d1409424..12980e6a 100644 --- a/extensions/cmd/panel.html +++ b/extensions/cmd/panel.html @@ -63,7 +63,7 @@ font-weight: 500; padding: 0; box-sizing: border-box; - -webkit-app-region: no-drag; + z-index: 2; } @@ -111,7 +111,7 @@ padding: 10px; margin: 5px 10px; box-sizing: border-box; - -webkit-app-region: no-drag; + background-color: rgba(40, 44, 52, 0.95); border-radius: 8px; } @@ -196,7 +196,7 @@ cursor: pointer; font-size: 14px; padding: 2px 6px; - -webkit-app-region: no-drag; + } #chain-indicator #chain-cancel:hover { @@ -215,7 +215,7 @@ border-radius: 8px; max-height: 200px; overflow: auto; - -webkit-app-region: no-drag; + width: calc(100% - 20px); box-sizing: border-box; } @@ -379,7 +379,7 @@ cursor: pointer; font-size: 12px; padding: 2px 8px; - -webkit-app-region: no-drag; + } #execution-state .exec-cancel:hover { @@ -403,7 +403,7 @@ transition: all 0.15s ease; margin-right: 10px; flex-shrink: 0; - -webkit-app-region: no-drag; + user-select: none; } diff --git a/extensions/cmd/panel.js b/extensions/cmd/panel.js index 089bdfc7..96daf96d 100644 --- a/extensions/cmd/panel.js +++ b/extensions/cmd/panel.js @@ -297,113 +297,10 @@ function isCommandAvailable(cmd) { return cmd.modes.includes(currentMajorMode); } -// ===== Custom Drag Handler ===== -// Native -webkit-app-region: drag is unreliable on transparent frameless panel -// windows (especially macOS type:'panel' with modal blur). Use a custom drag -// handler that moves the window via IPC, similar to how page.js handles dragging. - -let dragState = null; // { windowId, startScreenX, startScreenY, startPosX, startPosY } - -/** - * Check if an element is interactive (should not trigger drag) - */ -function isDragInteractive(el) { - if (!el) return false; - const tag = el.tagName?.toLowerCase(); - if (['input', 'textarea', 'button', 'a', 'select', 'label'].includes(tag)) return true; - if (el.isContentEditable) return true; - if (el.hasAttribute('data-no-drag')) return true; - if (el.closest('[data-no-drag]')) return true; - return false; -} - -/** - * Initialize custom window drag from visible UI areas (center-wrapper, chain indicator, etc.) - * Drag starts immediately on mousedown (no hold delay) for quick repositioning. - */ -async function initDrag() { - let windowId = null; - - // Get window ID once at init - try { - windowId = await api.window.getWindowId(); - log('cmd:panel', 'Drag: got window ID', windowId); - } catch (err) { - log.error('cmd:panel', 'Drag: failed to get window ID', err); - return; - } - - // Drag starts on mousedown on any non-interactive visible element - document.addEventListener('mousedown', (e) => { - // Only left click, skip interactive elements - if (e.button !== 0) return; - if (isDragInteractive(e.target)) return; - // Skip if clicking on transparent body background (not on a visible element) - // Only allow drag from visible UI containers - const visibleContainer = e.target.closest('.center-wrapper, #chain-indicator, #execution-state'); - if (!visibleContainer) return; - - // Prevent default immediately (before async call) to avoid text selection - e.preventDefault(); - - // Capture screen position synchronously - const startScreenX = e.screenX; - const startScreenY = e.screenY; - - // Get window position async, then start drag - api.window.getPosition().then(pos => { - if (!pos.success) return; - - dragState = { - windowId, - startScreenX, - startScreenY, - startPosX: pos.x, - startPosY: pos.y - }; - - document.body.style.cursor = 'grabbing'; - }).catch(err => { - log.error('cmd:panel', 'Drag: failed to get position', err); - }); - }); - - document.addEventListener('mousemove', (e) => { - if (!dragState) return; - - // Safety: cancel if no button held - if (e.buttons === 0) { - dragState = null; - document.body.style.cursor = ''; - return; - } - - const dx = e.screenX - dragState.startScreenX; - const dy = e.screenY - dragState.startScreenY; - const newX = dragState.startPosX + dx; - const newY = dragState.startPosY + dy; - - api.window.move(dragState.windowId, newX, newY); - }); - - document.addEventListener('mouseup', () => { - if (dragState) { - dragState = null; - document.body.style.cursor = ''; - } - }); - - // Add cursor hint on the center-wrapper for discoverability - const centerWrapper = document.querySelector('.center-wrapper'); - if (centerWrapper) { - centerWrapper.style.cursor = 'grab'; - } - - log('cmd:panel', 'Custom drag handler initialized'); -} - -// Initialize drag handler -initDrag(); +// ===== Window Drag ===== +// Window dragging is handled by the preload's unified drag system. +// Hold the mouse button on any non-interactive area for ~150ms to start dragging. +// No custom drag handler needed here -- the preload covers all windows. // Window sizing constants const COLLAPSED_HEIGHT = 60; // Just the command bar diff --git a/notes/ux-rules.md b/notes/ux-rules.md index 2a671417..0e86e5ef 100644 --- a/notes/ux-rules.md +++ b/notes/ux-rules.md @@ -23,13 +23,18 @@ When multiple interactions could trigger from the same gesture, follow this prio ## Window Dragging Frameless windows can be dragged by click-and-hold anywhere on the window body. +Dragging is handled by a single unified system in `preload.js` that runs in every +window. Individual windows do NOT need their own drag code. ### Activation | Parameter | Value | Purpose | |-----------|-------|---------| -| Hold delay | 300ms | Time before drag activates | +| Hold delay | 150ms | Time before drag activates | | Movement threshold | 5px | Cancel if mouse moves before delay | +The window position is fetched eagerly on mousedown (overlapping with the hold +timer) so that dragging begins instantly when the timer fires. + ### Rules **1. Text selection wins over dragging** @@ -42,26 +47,34 @@ These never trigger drag: - ``, `