diff --git a/frontend/src/components/CommandPalette.css b/frontend/src/components/CommandPalette.css index 8df5128..c8e493a 100644 --- a/frontend/src/components/CommandPalette.css +++ b/frontend/src/components/CommandPalette.css @@ -21,6 +21,22 @@ .command-palette-search-icon { color: #71717a; flex-shrink: 0; + /* Explicit drag handle for the panel. Wails native window drag does not + support NSPanel yet, so dragging is implemented in JS (mousedown/move + -> spotlight:drag events) and applied via SetPosition on the backend. + The grab cursor makes the handle discoverable. */ + cursor: grab; +} + +/* SVG child elements (circle/line) have zero clientWidth/clientHeight, which + breaks offset-based hit checks. Route all pointer events to the svg root + so the drag handle behaves consistently. */ +.command-palette-search-icon * { + pointer-events: none; +} + +.command-palette-search-icon:active { + cursor: grabbing; } .command-palette-input { @@ -59,7 +75,7 @@ } .command-palette-results { - height: 264px; /* Results area: 80% of original (332 * 0.8) */ + height: auto; /* Grows with the window; the JS backend drives window height */ overflow: hidden; background: #18181b; } @@ -73,7 +89,7 @@ .command-palette-list { overflow-y: auto; - max-height: 252px; /* 80% of original (320 * 0.8) */ + max-height: calc(100vh - 49px); /* Scroll within the window; 49px = search box */ } .command-palette-item { diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index be4fc3e..b1ef2f5 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -212,6 +212,49 @@ export function CommandPalette() { }); const inputRef = useRef(null); const listRef = useRef(null); + const dragRef = useRef(null); // { startX, startY } in screen coords + + // Custom panel dragging: Wails native window drag doesn't support NSPanel, + // so we track the pointer in JS and let the backend move the window. + const handleDragMouseDown = useCallback((e) => { + if (e.button !== 0) return; + dragRef.current = { startX: e.screenX, startY: e.screenY }; + try { + Events.Emit('spotlight:drag:start'); + } catch (err) { + console.error('Failed to emit spotlight:drag:start', err); + } + }, []); + + useEffect(() => { + const onMouseMove = (e) => { + if (!dragRef.current) return; + const { startX, startY } = dragRef.current; + try { + Events.Emit('spotlight:drag', { + dx: e.screenX - startX, + dy: e.screenY - startY, + }); + } catch (err) { + console.error('Failed to emit spotlight:drag', err); + } + }; + const onMouseUp = () => { + if (!dragRef.current) return; + dragRef.current = null; + try { + Events.Emit('spotlight:drag:end'); + } catch (err) { + console.error('Failed to emit spotlight:drag:end', err); + } + }; + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, []); // Calculate fuzzy match score const fuzzyScore = (target, query) => { @@ -251,7 +294,7 @@ export function CommandPalette() { // Listen for command palette opened event useEffect(() => { - const unsubscribe = window.runtime?.EventsOn?.('command-palette:opened', () => { + const unsubscribe = window.runtime?.EventsOn?.('spotlight:opened', () => { setSearchQuery(''); setSelectedIndex(0); setTimeout(() => inputRef.current?.focus(), 100); @@ -261,6 +304,20 @@ export function CommandPalette() { }; }, []); + // Resize the spotlight window to fit the visible results (Spotlight-like + // dynamic height). The backend listens for spotlight:resize and resizes + // the NSPanel while keeping it centered. + useEffect(() => { + const itemCount = commands.length; + const shown = Math.min(itemCount, 12); + const height = Math.max(100, Math.min(600, 49 + shown * 37 + 2)); + try { + Events.Emit('spotlight:resize', height); + } catch (err) { + console.error('Failed to emit spotlight:resize', err); + } + }, [commands.length]); + // Save recent command const saveRecentCommand = useCallback((commandId) => { setRecentCommands((prev) => { @@ -323,9 +380,9 @@ export function CommandPalette() { e.preventDefault(); console.log('[CommandPalette] Escape pressed, closing...'); try { - Events.Emit('command-palette:close'); + Events.Emit('spotlight:close'); } catch (err) { - console.error('Failed to emit command-palette:close', err); + console.error('Failed to emit spotlight:close', err); } } }, @@ -343,7 +400,11 @@ export function CommandPalette() { return (
- + 0 { + spotlightWindow.SetSize(640, int(h)) + } + }) + + // Custom panel dragging. Wails native window drag (startDrag) does not + // support NSPanel yet, so the frontend tracks the pointer and we move the + // window here with SetPosition. + var dragOriginX, dragOriginY int + app.Event.On("spotlight:drag:start", func(_ *application.CustomEvent) { + dragOriginX, dragOriginY = spotlightWindow.Position() + }) + app.Event.On("spotlight:drag", func(event *application.CustomEvent) { + m, ok := event.Data.(map[string]interface{}) + if !ok { + return + } + dx, _ := m["dx"].(float64) + dy, _ := m["dy"].(float64) + spotlightWindow.SetPosition(dragOriginX+int(dx), dragOriginY+int(dy)) + }) + app.Event.On("spotlight:drag:end", func(_ *application.CustomEvent) { + x, y := spotlightWindow.Position() + if err := settingsManager.SetSpotlightPosition(x, y); err != nil { + log.Printf("[Spotlight] Failed to save drag position: %v", err) + } }) app.Event.On("window:toggle", func(_ *application.CustomEvent) { @@ -250,11 +311,6 @@ func main() { } }) - app.Event.On("app:quit", func(_ *application.CustomEvent) { - log.Printf("[Spotlight] App quit requested via spotlight") - app.Quit() - }) - // Setup system tray systray := app.SystemTray.New() @@ -296,6 +352,20 @@ func main() { } } +// positionSpotlight keeps the spotlight panel horizontally centered while its +// top edge stays at a fixed height below the top of the screen, so resizing +// makes it grow downward instead of re-centering (macOS Spotlight behaviour). +func positionSpotlight(w *application.WebviewWindow) { + screen, err := w.GetScreen() + if err != nil || screen == nil { + w.Center() + return + } + x := screen.Bounds.X + (screen.Bounds.Width-640)/2 + y := screen.Bounds.Y + int(float64(screen.Bounds.Height)*0.15) + w.SetPosition(x, y) +} + func GinMiddleware(ginEngine *gin.Engine) application.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/service/spotlight.go b/service/spotlight.go index bc04cd2..3568fa0 100644 --- a/service/spotlight.go +++ b/service/spotlight.go @@ -54,18 +54,3 @@ func (s *SpotlightService) Toggle() { s.Show() } } - -// IsVisible returns whether the spotlight window is visible -func (s *SpotlightService) IsVisible() bool { - if s.window == nil { - return false - } - return s.window.IsVisible() -} - -// Close closes the spotlight window -func (s *SpotlightService) Close() { - if s.window != nil { - s.window.Close() - } -} diff --git a/service/spotlight_test.go b/service/spotlight_test.go index a3bff3b..5946370 100644 --- a/service/spotlight_test.go +++ b/service/spotlight_test.go @@ -21,12 +21,8 @@ func TestSpotlightService_Operations(t *testing.T) { { name: "Toggle with nil window", test: func(t *testing.T, s *SpotlightService) { - // Initially not visible - assert.False(t, s.IsVisible()) - // Toggle should not panic with nil window s.Toggle() - assert.False(t, s.IsVisible()) }, }, { @@ -34,7 +30,6 @@ func TestSpotlightService_Operations(t *testing.T) { test: func(t *testing.T, s *SpotlightService) { // Should not panic with nil window s.Show() - assert.False(t, s.IsVisible()) }, }, { @@ -42,14 +37,6 @@ func TestSpotlightService_Operations(t *testing.T) { test: func(t *testing.T, s *SpotlightService) { // Should not panic with nil window s.Hide() - assert.False(t, s.IsVisible()) - }, - }, - { - name: "IsVisible with nil window", - test: func(t *testing.T, s *SpotlightService) { - // Should return false with nil window - assert.False(t, s.IsVisible()) }, }, }