fix(ui): restore floating focus and release on close
- commit
- 482d01454c8fd999274639c58618d5cdc343a8e4
- author
- brice · · 2026-02-09 10:42 UTC
- parent
- 48c09fe
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +146 -41
M internal/ui/app.go +118 -37
| @@ -151,6 +151,7 @@ type floatingWorkspaceSession struct { | |||
| 151 | 151 | webView *webkit.WebView | |
| 152 | 152 | overlay layout.OverlayWidget | |
| 153 | 153 | widget layout.Widget | |
| 154 | + | focusWidget layout.Widget | |
| 154 | 155 | omnibox *component.Omnibox | |
| 155 | 156 | omniboxWidget layout.Widget | |
| 156 | 157 | resizeWatcherActive bool | |
| @@ -618,7 +619,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 618 | 619 | a.keyboardHandler = input.NewKeyboardHandler(ctx, a.deps.Config) | |
| 619 | 620 | a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error { | |
| 620 | 621 | if action == input.ActionClosePane { | |
| 621 | - | if a.closeActiveFloatingPane(ctx) { | |
| 622 | + | if a.closeAndReleaseActiveFloatingPane(ctx) { | |
| 622 | 623 | return nil | |
| 623 | 624 | } | |
| 624 | 625 | } | |
| @@ -2182,21 +2183,33 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe | |||
| 2182 | 2183 | return x, y, width, height, true | |
| 2183 | 2184 | } | |
| 2184 | 2185 | ||
| 2186 | + | //nolint:gosec // GtkOverlay get-child-position provides a valid GtkAllocation pointer owned by GTK. | |
| 2185 | 2187 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | |
| 2186 | 2188 | if allocationPtr == nil { | |
| 2187 | 2189 | return false | |
| 2188 | 2190 | } | |
| 2189 | 2191 | ||
| 2192 | + | const ( | |
| 2193 | + | gtkAllocationOffsetX = uintptr(0) | |
| 2194 | + | gtkAllocationOffsetY = uintptr(4) | |
| 2195 | + | gtkAllocationOffsetWidth = uintptr(8) | |
| 2196 | + | gtkAllocationOffsetHeight = uintptr(12) | |
| 2197 | + | ) | |
| 2198 | + | ||
| 2190 | 2199 | // GtkAllocation/GdkRectangle is four 32-bit signed integers in C. | |
| 2191 | 2200 | base := unsafe.Pointer(allocationPtr) | |
| 2192 | - | *(*int32)(base) = int32(x) | |
| 2193 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(4))) = int32(y) | |
| 2194 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(8))) = int32(width) | |
| 2195 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(12))) = int32(height) | |
| 2201 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetX)) = int32(x) | |
| 2202 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetY)) = int32(y) | |
| 2203 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetWidth)) = int32(width) | |
| 2204 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetHeight)) = int32(height) | |
| 2196 | 2205 | return true | |
| 2197 | 2206 | } | |
| 2198 | 2207 | ||
| 2199 | - | func (a *App) floatingAllocationForWidget(tabID entity.TabID, widgetPtr uintptr, overlayWidth, overlayHeight int) (x, y, width, height int, ok bool) { | |
| 2208 | + | func (a *App) floatingAllocationForWidget( | |
| 2209 | + | tabID entity.TabID, | |
| 2210 | + | widgetPtr uintptr, | |
| 2211 | + | overlayWidth, overlayHeight int, | |
| 2212 | + | ) (x, y, width, height int, ok bool) { | |
| 2200 | 2213 | for key, session := range a.floatingSessions { | |
| 2201 | 2214 | if key.tabID != tabID || session == nil || session.widget == nil { | |
| 2202 | 2215 | continue | |
| @@ -2273,6 +2286,9 @@ func (a *App) ensureFloatingSession( | |||
| 2273 | 2286 | } | |
| 2274 | 2287 | if session.paneView != nil { | |
| 2275 | 2288 | wsView.RegisterPaneView(session.paneID, session.paneView) | |
| 2289 | + | if session.focusWidget == nil { | |
| 2290 | + | session.focusWidget = session.paneView.WebViewWidget() | |
| 2291 | + | } | |
| 2276 | 2292 | } | |
| 2277 | 2293 | return session, nil | |
| 2278 | 2294 | } | |
| @@ -2334,12 +2350,13 @@ func (a *App) ensureFloatingSession( | |||
| 2334 | 2350 | }) | |
| 2335 | 2351 | ||
| 2336 | 2352 | session := &floatingWorkspaceSession{ | |
| 2337 | - | paneID: paneID, | |
| 2338 | - | pane: floatingPane, | |
| 2339 | - | paneView: pv, | |
| 2340 | - | webView: wv, | |
| 2341 | - | overlay: pvOverlay, | |
| 2342 | - | widget: pvOverlay, | |
| 2353 | + | paneID: paneID, | |
| 2354 | + | pane: floatingPane, | |
| 2355 | + | paneView: pv, | |
| 2356 | + | webView: wv, | |
| 2357 | + | overlay: pvOverlay, | |
| 2358 | + | widget: pvOverlay, | |
| 2359 | + | focusWidget: webViewWidget, | |
| 2343 | 2360 | } | |
| 2344 | 2361 | a.floatingSessions[key] = session | |
| 2345 | 2362 | return session, nil | |
| @@ -2363,41 +2380,66 @@ func (a *App) releaseFloatingSessionsForTab(ctx context.Context, tabID entity.Ta | |||
| 2363 | 2380 | } | |
| 2364 | 2381 | ||
| 2365 | 2382 | for key, session := range sessions { | |
| 2366 | - | a.stopFloatingResizeWatcher(session) | |
| 2367 | - | if session.pane != nil { | |
| 2368 | - | session.pane.Hide(ctx) | |
| 2369 | - | } | |
| 2370 | - | a.hideFloatingOmnibox(ctx, session) | |
| 2371 | - | if wsView := a.workspaceViews[tabID]; wsView != nil { | |
| 2372 | - | if session.widget != nil { | |
| 2373 | - | wsView.RemoveWorkspaceOverlayWidget(session.widget) | |
| 2374 | - | } | |
| 2375 | - | if session.paneID != "" { | |
| 2376 | - | wsView.UnregisterPaneView(session.paneID) | |
| 2377 | - | } | |
| 2378 | - | } | |
| 2379 | - | if a.contentCoord != nil && session.paneID != "" { | |
| 2380 | - | a.contentCoord.ReleaseWebView(ctx, session.paneID) | |
| 2381 | - | } | |
| 2382 | - | delete(a.floatingSessions, key) | |
| 2383 | + | a.releaseFloatingSession(ctx, key, session) | |
| 2383 | 2384 | } | |
| 2384 | 2385 | a.syncFloatingFocus() | |
| 2385 | 2386 | } | |
| 2386 | 2387 | ||
| 2387 | - | func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) { | |
| 2388 | + | func (a *App) activeFloatingSessionEntry() (floatingSessionKey, *floatingWorkspaceSession, bool) { | |
| 2388 | 2389 | activeTab := a.tabs.ActiveTab() | |
| 2389 | 2390 | if activeTab == nil { | |
| 2390 | - | return nil, "" | |
| 2391 | + | return floatingSessionKey{}, nil, false | |
| 2391 | 2392 | } | |
| 2392 | 2393 | for key, session := range a.floatingSessions { | |
| 2393 | 2394 | if key.tabID != activeTab.ID || session == nil || session.pane == nil { | |
| 2394 | 2395 | continue | |
| 2395 | 2396 | } | |
| 2396 | 2397 | if session.pane.IsVisible() { | |
| 2397 | - | return session, activeTab.ID | |
| 2398 | + | return key, session, true | |
| 2398 | 2399 | } | |
| 2399 | 2400 | } | |
| 2400 | - | return nil, activeTab.ID | |
| 2401 | + | return floatingSessionKey{tabID: activeTab.ID}, nil, false | |
| 2402 | + | } | |
| 2403 | + | ||
| 2404 | + | func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) { | |
| 2405 | + | key, session, ok := a.activeFloatingSessionEntry() | |
| 2406 | + | if !ok { | |
| 2407 | + | return nil, key.tabID | |
| 2408 | + | } | |
| 2409 | + | return session, key.tabID | |
| 2410 | + | } | |
| 2411 | + | ||
| 2412 | + | func (a *App) releaseFloatingSession(ctx context.Context, key floatingSessionKey, session *floatingWorkspaceSession) { | |
| 2413 | + | if session == nil { | |
| 2414 | + | return | |
| 2415 | + | } | |
| 2416 | + | ||
| 2417 | + | a.stopFloatingResizeWatcher(session) | |
| 2418 | + | if session.pane != nil { | |
| 2419 | + | session.pane.Hide(ctx) | |
| 2420 | + | } | |
| 2421 | + | a.hideFloatingOmnibox(ctx, session) | |
| 2422 | + | if wsView := a.workspaceViews[key.tabID]; wsView != nil { | |
| 2423 | + | if session.widget != nil { | |
| 2424 | + | wsView.RemoveWorkspaceOverlayWidget(session.widget) | |
| 2425 | + | } | |
| 2426 | + | if session.paneID != "" { | |
| 2427 | + | wsView.UnregisterPaneView(session.paneID) | |
| 2428 | + | } | |
| 2429 | + | } | |
| 2430 | + | if a.contentCoord != nil && session.paneID != "" { | |
| 2431 | + | a.contentCoord.ReleaseWebView(ctx, session.paneID) | |
| 2432 | + | } | |
| 2433 | + | delete(a.floatingSessions, key) | |
| 2434 | + | ||
| 2435 | + | session.appliedWidth = 0 | |
| 2436 | + | session.appliedHeight = 0 | |
| 2437 | + | session.focusWidget = nil | |
| 2438 | + | session.paneView = nil | |
| 2439 | + | session.widget = nil | |
| 2440 | + | session.overlay = nil | |
| 2441 | + | session.webView = nil | |
| 2442 | + | session.pane = nil | |
| 2401 | 2443 | } | |
| 2402 | 2444 | ||
| 2403 | 2445 | func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) { | |
| @@ -2504,6 +2546,33 @@ func (a *App) hideFloatingOmnibox(ctx context.Context, session *floatingWorkspac | |||
| 2504 | 2546 | session.omniboxWidget = nil | |
| 2505 | 2547 | } | |
| 2506 | 2548 | ||
| 2549 | + | func floatingFocusWidget(session *floatingWorkspaceSession) layout.Widget { | |
| 2550 | + | if session == nil { | |
| 2551 | + | return nil | |
| 2552 | + | } | |
| 2553 | + | if session.focusWidget != nil { | |
| 2554 | + | return session.focusWidget | |
| 2555 | + | } | |
| 2556 | + | if session.paneView != nil { | |
| 2557 | + | return session.paneView.WebViewWidget() | |
| 2558 | + | } | |
| 2559 | + | return nil | |
| 2560 | + | } | |
| 2561 | + | ||
| 2562 | + | func focusFloatingSessionContent(session *floatingWorkspaceSession) { | |
| 2563 | + | if session == nil || session.pane == nil { | |
| 2564 | + | return | |
| 2565 | + | } | |
| 2566 | + | if !session.pane.IsVisible() || session.pane.IsOmniboxVisible() { | |
| 2567 | + | return | |
| 2568 | + | } | |
| 2569 | + | w := floatingFocusWidget(session) | |
| 2570 | + | if w == nil { | |
| 2571 | + | return | |
| 2572 | + | } | |
| 2573 | + | w.GrabFocus() | |
| 2574 | + | } | |
| 2575 | + | ||
| 2507 | 2576 | func (a *App) resizeFloatingWidget(session *floatingWorkspaceSession) { | |
| 2508 | 2577 | if session == nil || session.pane == nil { | |
| 2509 | 2578 | return | |
| @@ -2608,6 +2677,16 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool { | |||
| 2608 | 2677 | return true | |
| 2609 | 2678 | } | |
| 2610 | 2679 | ||
| 2680 | + | func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool { | |
| 2681 | + | key, session, ok := a.activeFloatingSessionEntry() | |
| 2682 | + | if !ok { | |
| 2683 | + | return false | |
| 2684 | + | } | |
| 2685 | + | a.releaseFloatingSession(ctx, key, session) | |
| 2686 | + | a.syncFloatingFocus() | |
| 2687 | + | return true | |
| 2688 | + | } | |
| 2689 | + | ||
| 2611 | 2690 | // ToggleFloatingPane toggles the active workspace floating pane visibility. | |
| 2612 | 2691 | func (a *App) ToggleFloatingPane(ctx context.Context) error { | |
| 2613 | 2692 | activeTab := a.tabs.ActiveTab() | |
| @@ -2660,18 +2739,20 @@ func (a *App) openFloatingPaneSession(ctx context.Context, sessionID string, url | |||
| 2660 | 2739 | return err | |
| 2661 | 2740 | } | |
| 2662 | 2741 | } | |
| 2663 | - | a.hideFloatingOmnibox(ctx, session) | |
| 2664 | 2742 | } else { | |
| 2665 | 2743 | if err := session.pane.ShowToggle(ctx); err != nil { | |
| 2666 | 2744 | return err | |
| 2667 | 2745 | } | |
| 2668 | - | if session.pane.IsOmniboxVisible() { | |
| 2669 | - | a.showFloatingOmnibox(ctx, session) | |
| 2670 | - | } | |
| 2671 | 2746 | } | |
| 2672 | 2747 | ||
| 2673 | 2748 | a.resizeFloatingWidget(session) | |
| 2674 | 2749 | setFloatingWidgetShown(session.widget, session.pane.IsVisible()) | |
| 2750 | + | if session.pane.IsOmniboxVisible() { | |
| 2751 | + | a.showFloatingOmnibox(ctx, session) | |
| 2752 | + | } else { | |
| 2753 | + | a.hideFloatingOmnibox(ctx, session) | |
| 2754 | + | focusFloatingSessionContent(session) | |
| 2755 | + | } | |
| 2675 | 2756 | a.startFloatingResizeWatcher(session) | |
| 2676 | 2757 | a.syncFloatingFocus() | |
| 2677 | 2758 | ||
| @@ -151,6 +151,7 @@ type floatingWorkspaceSession struct { | |||||
| 151 | webView *webkit.WebView | 151 | webView *webkit.WebView | ||
| 152 | overlay layout.OverlayWidget | 152 | overlay layout.OverlayWidget | ||
| 153 | widget layout.Widget | 153 | widget layout.Widget | ||
| 154 | + | focusWidget layout.Widget | |||
| 154 | omnibox *component.Omnibox | 155 | omnibox *component.Omnibox | ||
| 155 | omniboxWidget layout.Widget | 156 | omniboxWidget layout.Widget | ||
| 156 | resizeWatcherActive bool | 157 | resizeWatcherActive bool | ||
| @@ -618,7 +619,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 618 | a.keyboardHandler = input.NewKeyboardHandler(ctx, a.deps.Config) | 619 | a.keyboardHandler = input.NewKeyboardHandler(ctx, a.deps.Config) | ||
| 619 | a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error { | 620 | a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error { | ||
| 620 | if action == input.ActionClosePane { | 621 | if action == input.ActionClosePane { | ||
| 621 | - | if a.closeActiveFloatingPane(ctx) { | 622 | + | if a.closeAndReleaseActiveFloatingPane(ctx) { |
| 622 | return nil | 623 | return nil | ||
| 623 | } | 624 | } | ||
| 624 | } | 625 | } | ||
| @@ -2182,21 +2183,33 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe | |||||
| 2182 | return x, y, width, height, true | 2183 | return x, y, width, height, true | ||
| 2183 | } | 2184 | } | ||
| 2184 | 2185 | ||||
| 2186 | + | //nolint:gosec // GtkOverlay get-child-position provides a valid GtkAllocation pointer owned by GTK. | |||
| 2185 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | 2187 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | ||
| 2186 | if allocationPtr == nil { | 2188 | if allocationPtr == nil { | ||
| 2187 | return false | 2189 | return false | ||
| 2188 | } | 2190 | } | ||
| 2189 | 2191 | ||||
| 2192 | + | const ( | |||
| 2193 | + | gtkAllocationOffsetX = uintptr(0) | |||
| 2194 | + | gtkAllocationOffsetY = uintptr(4) | |||
| 2195 | + | gtkAllocationOffsetWidth = uintptr(8) | |||
| 2196 | + | gtkAllocationOffsetHeight = uintptr(12) | |||
| 2197 | + | ) | |||
| 2198 | + | ||||
| 2190 | // GtkAllocation/GdkRectangle is four 32-bit signed integers in C. | 2199 | // GtkAllocation/GdkRectangle is four 32-bit signed integers in C. | ||
| 2191 | base := unsafe.Pointer(allocationPtr) | 2200 | base := unsafe.Pointer(allocationPtr) | ||
| 2192 | - | *(*int32)(base) = int32(x) | 2201 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetX)) = int32(x) |
| 2193 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(4))) = int32(y) | 2202 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetY)) = int32(y) |
| 2194 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(8))) = int32(width) | 2203 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetWidth)) = int32(width) |
| 2195 | - | *(*int32)(unsafe.Pointer(uintptr(base) + uintptr(12))) = int32(height) | 2204 | + | *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetHeight)) = int32(height) |
| 2196 | return true | 2205 | return true | ||
| 2197 | } | 2206 | } | ||
| 2198 | 2207 | ||||
| 2199 | - | func (a *App) floatingAllocationForWidget(tabID entity.TabID, widgetPtr uintptr, overlayWidth, overlayHeight int) (x, y, width, height int, ok bool) { | 2208 | + | func (a *App) floatingAllocationForWidget( |
| 2209 | + | tabID entity.TabID, | |||
| 2210 | + | widgetPtr uintptr, | |||
| 2211 | + | overlayWidth, overlayHeight int, | |||
| 2212 | + | ) (x, y, width, height int, ok bool) { | |||
| 2200 | for key, session := range a.floatingSessions { | 2213 | for key, session := range a.floatingSessions { | ||
| 2201 | if key.tabID != tabID || session == nil || session.widget == nil { | 2214 | if key.tabID != tabID || session == nil || session.widget == nil { | ||
| 2202 | continue | 2215 | continue | ||
| @@ -2273,6 +2286,9 @@ func (a *App) ensureFloatingSession( | |||||
| 2273 | } | 2286 | } | ||
| 2274 | if session.paneView != nil { | 2287 | if session.paneView != nil { | ||
| 2275 | wsView.RegisterPaneView(session.paneID, session.paneView) | 2288 | wsView.RegisterPaneView(session.paneID, session.paneView) | ||
| 2289 | + | if session.focusWidget == nil { | |||
| 2290 | + | session.focusWidget = session.paneView.WebViewWidget() | |||
| 2291 | + | } | |||
| 2276 | } | 2292 | } | ||
| 2277 | return session, nil | 2293 | return session, nil | ||
| 2278 | } | 2294 | } | ||
| @@ -2334,12 +2350,13 @@ func (a *App) ensureFloatingSession( | |||||
| 2334 | }) | 2350 | }) | ||
| 2335 | 2351 | ||||
| 2336 | session := &floatingWorkspaceSession{ | 2352 | session := &floatingWorkspaceSession{ | ||
| 2337 | - | paneID: paneID, | 2353 | + | paneID: paneID, |
| 2338 | - | pane: floatingPane, | 2354 | + | pane: floatingPane, |
| 2339 | - | paneView: pv, | 2355 | + | paneView: pv, |
| 2340 | - | webView: wv, | 2356 | + | webView: wv, |
| 2341 | - | overlay: pvOverlay, | 2357 | + | overlay: pvOverlay, |
| 2342 | - | widget: pvOverlay, | 2358 | + | widget: pvOverlay, |
| 2359 | + | focusWidget: webViewWidget, | |||
| 2343 | } | 2360 | } | ||
| 2344 | a.floatingSessions[key] = session | 2361 | a.floatingSessions[key] = session | ||
| 2345 | return session, nil | 2362 | return session, nil | ||
| @@ -2363,41 +2380,66 @@ func (a *App) releaseFloatingSessionsForTab(ctx context.Context, tabID entity.Ta | |||||
| 2363 | } | 2380 | } | ||
| 2364 | 2381 | ||||
| 2365 | for key, session := range sessions { | 2382 | for key, session := range sessions { | ||
| 2366 | - | a.stopFloatingResizeWatcher(session) | 2383 | + | a.releaseFloatingSession(ctx, key, session) |
| 2367 | - | if session.pane != nil { | |||
| 2368 | - | session.pane.Hide(ctx) | |||
| 2369 | - | } | |||
| 2370 | - | a.hideFloatingOmnibox(ctx, session) | |||
| 2371 | - | if wsView := a.workspaceViews[tabID]; wsView != nil { | |||
| 2372 | - | if session.widget != nil { | |||
| 2373 | - | wsView.RemoveWorkspaceOverlayWidget(session.widget) | |||
| 2374 | - | } | |||
| 2375 | - | if session.paneID != "" { | |||
| 2376 | - | wsView.UnregisterPaneView(session.paneID) | |||
| 2377 | - | } | |||
| 2378 | - | } | |||
| 2379 | - | if a.contentCoord != nil && session.paneID != "" { | |||
| 2380 | - | a.contentCoord.ReleaseWebView(ctx, session.paneID) | |||
| 2381 | - | } | |||
| 2382 | - | delete(a.floatingSessions, key) | |||
| 2383 | } | 2384 | } | ||
| 2384 | a.syncFloatingFocus() | 2385 | a.syncFloatingFocus() | ||
| 2385 | } | 2386 | } | ||
| 2386 | 2387 | ||||
| 2387 | - | func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) { | 2388 | + | func (a *App) activeFloatingSessionEntry() (floatingSessionKey, *floatingWorkspaceSession, bool) { |
| 2388 | activeTab := a.tabs.ActiveTab() | 2389 | activeTab := a.tabs.ActiveTab() | ||
| 2389 | if activeTab == nil { | 2390 | if activeTab == nil { | ||
| 2390 | - | return nil, "" | 2391 | + | return floatingSessionKey{}, nil, false |
| 2391 | } | 2392 | } | ||
| 2392 | for key, session := range a.floatingSessions { | 2393 | for key, session := range a.floatingSessions { | ||
| 2393 | if key.tabID != activeTab.ID || session == nil || session.pane == nil { | 2394 | if key.tabID != activeTab.ID || session == nil || session.pane == nil { | ||
| 2394 | continue | 2395 | continue | ||
| 2395 | } | 2396 | } | ||
| 2396 | if session.pane.IsVisible() { | 2397 | if session.pane.IsVisible() { | ||
| 2397 | - | return session, activeTab.ID | 2398 | + | return key, session, true |
| 2398 | } | 2399 | } | ||
| 2399 | } | 2400 | } | ||
| 2400 | - | return nil, activeTab.ID | 2401 | + | return floatingSessionKey{tabID: activeTab.ID}, nil, false |
| 2402 | + | } | |||
| 2403 | + | ||||
| 2404 | + | func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) { | |||
| 2405 | + | key, session, ok := a.activeFloatingSessionEntry() | |||
| 2406 | + | if !ok { | |||
| 2407 | + | return nil, key.tabID | |||
| 2408 | + | } | |||
| 2409 | + | return session, key.tabID | |||
| 2410 | + | } | |||
| 2411 | + | ||||
| 2412 | + | func (a *App) releaseFloatingSession(ctx context.Context, key floatingSessionKey, session *floatingWorkspaceSession) { | |||
| 2413 | + | if session == nil { | |||
| 2414 | + | return | |||
| 2415 | + | } | |||
| 2416 | + | ||||
| 2417 | + | a.stopFloatingResizeWatcher(session) | |||
| 2418 | + | if session.pane != nil { | |||
| 2419 | + | session.pane.Hide(ctx) | |||
| 2420 | + | } | |||
| 2421 | + | a.hideFloatingOmnibox(ctx, session) | |||
| 2422 | + | if wsView := a.workspaceViews[key.tabID]; wsView != nil { | |||
| 2423 | + | if session.widget != nil { | |||
| 2424 | + | wsView.RemoveWorkspaceOverlayWidget(session.widget) | |||
| 2425 | + | } | |||
| 2426 | + | if session.paneID != "" { | |||
| 2427 | + | wsView.UnregisterPaneView(session.paneID) | |||
| 2428 | + | } | |||
| 2429 | + | } | |||
| 2430 | + | if a.contentCoord != nil && session.paneID != "" { | |||
| 2431 | + | a.contentCoord.ReleaseWebView(ctx, session.paneID) | |||
| 2432 | + | } | |||
| 2433 | + | delete(a.floatingSessions, key) | |||
| 2434 | + | ||||
| 2435 | + | session.appliedWidth = 0 | |||
| 2436 | + | session.appliedHeight = 0 | |||
| 2437 | + | session.focusWidget = nil | |||
| 2438 | + | session.paneView = nil | |||
| 2439 | + | session.widget = nil | |||
| 2440 | + | session.overlay = nil | |||
| 2441 | + | session.webView = nil | |||
| 2442 | + | session.pane = nil | |||
| 2401 | } | 2443 | } | ||
| 2402 | 2444 | ||||
| 2403 | func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) { | 2445 | func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) { | ||
| @@ -2504,6 +2546,33 @@ func (a *App) hideFloatingOmnibox(ctx context.Context, session *floatingWorkspac | |||||
| 2504 | session.omniboxWidget = nil | 2546 | session.omniboxWidget = nil | ||
| 2505 | } | 2547 | } | ||
| 2506 | 2548 | ||||
| 2549 | + | func floatingFocusWidget(session *floatingWorkspaceSession) layout.Widget { | |||
| 2550 | + | if session == nil { | |||
| 2551 | + | return nil | |||
| 2552 | + | } | |||
| 2553 | + | if session.focusWidget != nil { | |||
| 2554 | + | return session.focusWidget | |||
| 2555 | + | } | |||
| 2556 | + | if session.paneView != nil { | |||
| 2557 | + | return session.paneView.WebViewWidget() | |||
| 2558 | + | } | |||
| 2559 | + | return nil | |||
| 2560 | + | } | |||
| 2561 | + | ||||
| 2562 | + | func focusFloatingSessionContent(session *floatingWorkspaceSession) { | |||
| 2563 | + | if session == nil || session.pane == nil { | |||
| 2564 | + | return | |||
| 2565 | + | } | |||
| 2566 | + | if !session.pane.IsVisible() || session.pane.IsOmniboxVisible() { | |||
| 2567 | + | return | |||
| 2568 | + | } | |||
| 2569 | + | w := floatingFocusWidget(session) | |||
| 2570 | + | if w == nil { | |||
| 2571 | + | return | |||
| 2572 | + | } | |||
| 2573 | + | w.GrabFocus() | |||
| 2574 | + | } | |||
| 2575 | + | ||||
| 2507 | func (a *App) resizeFloatingWidget(session *floatingWorkspaceSession) { | 2576 | func (a *App) resizeFloatingWidget(session *floatingWorkspaceSession) { | ||
| 2508 | if session == nil || session.pane == nil { | 2577 | if session == nil || session.pane == nil { | ||
| 2509 | return | 2578 | return | ||
| @@ -2608,6 +2677,16 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool { | |||||
| 2608 | return true | 2677 | return true | ||
| 2609 | } | 2678 | } | ||
| 2610 | 2679 | ||||
| 2680 | + | func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool { | |||
| 2681 | + | key, session, ok := a.activeFloatingSessionEntry() | |||
| 2682 | + | if !ok { | |||
| 2683 | + | return false | |||
| 2684 | + | } | |||
| 2685 | + | a.releaseFloatingSession(ctx, key, session) | |||
| 2686 | + | a.syncFloatingFocus() | |||
| 2687 | + | return true | |||
| 2688 | + | } | |||
| 2689 | + | ||||
| 2611 | // ToggleFloatingPane toggles the active workspace floating pane visibility. | 2690 | // ToggleFloatingPane toggles the active workspace floating pane visibility. | ||
| 2612 | func (a *App) ToggleFloatingPane(ctx context.Context) error { | 2691 | func (a *App) ToggleFloatingPane(ctx context.Context) error { | ||
| 2613 | activeTab := a.tabs.ActiveTab() | 2692 | activeTab := a.tabs.ActiveTab() | ||
| @@ -2660,18 +2739,20 @@ func (a *App) openFloatingPaneSession(ctx context.Context, sessionID string, url | |||||
| 2660 | return err | 2739 | return err | ||
| 2661 | } | 2740 | } | ||
| 2662 | } | 2741 | } | ||
| 2663 | - | a.hideFloatingOmnibox(ctx, session) | |||
| 2664 | } else { | 2742 | } else { | ||
| 2665 | if err := session.pane.ShowToggle(ctx); err != nil { | 2743 | if err := session.pane.ShowToggle(ctx); err != nil { | ||
| 2666 | return err | 2744 | return err | ||
| 2667 | } | 2745 | } | ||
| 2668 | - | if session.pane.IsOmniboxVisible() { | |||
| 2669 | - | a.showFloatingOmnibox(ctx, session) | |||
| 2670 | - | } | |||
| 2671 | } | 2746 | } | ||
| 2672 | 2747 | ||||
| 2673 | a.resizeFloatingWidget(session) | 2748 | a.resizeFloatingWidget(session) | ||
| 2674 | setFloatingWidgetShown(session.widget, session.pane.IsVisible()) | 2749 | setFloatingWidgetShown(session.widget, session.pane.IsVisible()) | ||
| 2750 | + | if session.pane.IsOmniboxVisible() { | |||
| 2751 | + | a.showFloatingOmnibox(ctx, session) | |||
| 2752 | + | } else { | |||
| 2753 | + | a.hideFloatingOmnibox(ctx, session) | |||
| 2754 | + | focusFloatingSessionContent(session) | |||
| 2755 | + | } | |||
| 2675 | a.startFloatingResizeWatcher(session) | 2756 | a.startFloatingResizeWatcher(session) | ||
| 2676 | a.syncFloatingFocus() | 2757 | a.syncFloatingFocus() | ||
| 2677 | 2758 | ||||
M internal/ui/floating_pane_test.go +28 -4
| @@ -53,6 +53,28 @@ func TestHideFloatingWidget_RemovesVisibleClassAndDisablesInteraction(t *testing | |||
| 53 | 53 | hideFloatingWidget(widget) | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | + | func TestFocusFloatingSessionContent_GrabsWebViewFocusWhenOmniboxHidden(t *testing.T) { | |
| 57 | + | session := newFloatingPaneSession(entity.TabID("tab-1"), "profile:google") | |
| 58 | + | session.pane.Show() | |
| 59 | + | ||
| 60 | + | widget := layoutmocks.NewMockWidget(t) | |
| 61 | + | widget.EXPECT().GrabFocus().Return(true).Once() | |
| 62 | + | session.focusWidget = widget | |
| 63 | + | ||
| 64 | + | focusFloatingSessionContent(session) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func TestFocusFloatingSessionContent_SkipsWhenOmniboxVisible(t *testing.T) { | |
| 68 | + | session := newFloatingPaneSession(entity.TabID("tab-1"), floatingSessionIDDefault) | |
| 69 | + | require.NoError(t, session.pane.ShowToggle(context.Background())) | |
| 70 | + | require.True(t, session.pane.IsOmniboxVisible()) | |
| 71 | + | ||
| 72 | + | widget := layoutmocks.NewMockWidget(t) | |
| 73 | + | session.focusWidget = widget | |
| 74 | + | ||
| 75 | + | focusFloatingSessionContent(session) | |
| 76 | + | } | |
| 77 | + | ||
| 56 | 78 | func TestFloatingAllocationRect_CentersRequestedSize(t *testing.T) { | |
| 57 | 79 | x, y, width, height, ok := floatingAllocationRect(1000, 700, 820, 504) | |
| 58 | 80 | ||
| @@ -273,15 +295,17 @@ func TestFloatingPane_DefaultSessionStillUsesAboutBlank(t *testing.T) { | |||
| 273 | 295 | func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) { | |
| 274 | 296 | app, tabID, _ := newFloatingPaneTestApp(t) | |
| 275 | 297 | session := newFloatingPaneSession(tabID, "profile:gmail") | |
| 276 | - | app.floatingSessions[floatingSessionKey{tabID: tabID, sessionID: "profile:gmail"}] = session | |
| 298 | + | key := floatingSessionKey{tabID: tabID, sessionID: "profile:gmail"} | |
| 299 | + | app.floatingSessions[key] = session | |
| 277 | 300 | ||
| 278 | 301 | require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", "https://mail.google.com")) | |
| 279 | 302 | ||
| 280 | - | handled := app.closeActiveFloatingPane(context.Background()) | |
| 303 | + | handled := app.closeAndReleaseActiveFloatingPane(context.Background()) | |
| 281 | 304 | assert.True(t, handled) | |
| 282 | - | assert.False(t, session.pane.IsVisible()) | |
| 305 | + | _, stillExists := app.floatingSessions[key] | |
| 306 | + | assert.False(t, stillExists) | |
| 283 | 307 | ||
| 284 | - | handled = app.closeActiveFloatingPane(context.Background()) | |
| 308 | + | handled = app.closeAndReleaseActiveFloatingPane(context.Background()) | |
| 285 | 309 | assert.False(t, handled) | |
| 286 | 310 | } | |
| 287 | 311 | ||
| @@ -53,6 +53,28 @@ func TestHideFloatingWidget_RemovesVisibleClassAndDisablesInteraction(t *testing | |||||
| 53 | hideFloatingWidget(widget) | 53 | hideFloatingWidget(widget) | ||
| 54 | } | 54 | } | ||
| 55 | 55 | ||||
| 56 | + | func TestFocusFloatingSessionContent_GrabsWebViewFocusWhenOmniboxHidden(t *testing.T) { | |||
| 57 | + | session := newFloatingPaneSession(entity.TabID("tab-1"), "profile:google") | |||
| 58 | + | session.pane.Show() | |||
| 59 | + | ||||
| 60 | + | widget := layoutmocks.NewMockWidget(t) | |||
| 61 | + | widget.EXPECT().GrabFocus().Return(true).Once() | |||
| 62 | + | session.focusWidget = widget | |||
| 63 | + | ||||
| 64 | + | focusFloatingSessionContent(session) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestFocusFloatingSessionContent_SkipsWhenOmniboxVisible(t *testing.T) { | |||
| 68 | + | session := newFloatingPaneSession(entity.TabID("tab-1"), floatingSessionIDDefault) | |||
| 69 | + | require.NoError(t, session.pane.ShowToggle(context.Background())) | |||
| 70 | + | require.True(t, session.pane.IsOmniboxVisible()) | |||
| 71 | + | ||||
| 72 | + | widget := layoutmocks.NewMockWidget(t) | |||
| 73 | + | session.focusWidget = widget | |||
| 74 | + | ||||
| 75 | + | focusFloatingSessionContent(session) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 56 | func TestFloatingAllocationRect_CentersRequestedSize(t *testing.T) { | 78 | func TestFloatingAllocationRect_CentersRequestedSize(t *testing.T) { | ||
| 57 | x, y, width, height, ok := floatingAllocationRect(1000, 700, 820, 504) | 79 | x, y, width, height, ok := floatingAllocationRect(1000, 700, 820, 504) | ||
| 58 | 80 | ||||
| @@ -273,15 +295,17 @@ func TestFloatingPane_DefaultSessionStillUsesAboutBlank(t *testing.T) { | |||||
| 273 | func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) { | 295 | func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) { | ||
| 274 | app, tabID, _ := newFloatingPaneTestApp(t) | 296 | app, tabID, _ := newFloatingPaneTestApp(t) | ||
| 275 | session := newFloatingPaneSession(tabID, "profile:gmail") | 297 | session := newFloatingPaneSession(tabID, "profile:gmail") | ||
| 276 | - | app.floatingSessions[floatingSessionKey{tabID: tabID, sessionID: "profile:gmail"}] = session | 298 | + | key := floatingSessionKey{tabID: tabID, sessionID: "profile:gmail"} |
| 299 | + | app.floatingSessions[key] = session | |||
| 277 | 300 | ||||
| 278 | require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", "https://mail.google.com")) | 301 | require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", "https://mail.google.com")) | ||
| 279 | 302 | ||||
| 280 | - | handled := app.closeActiveFloatingPane(context.Background()) | 303 | + | handled := app.closeAndReleaseActiveFloatingPane(context.Background()) |
| 281 | assert.True(t, handled) | 304 | assert.True(t, handled) | ||
| 282 | - | assert.False(t, session.pane.IsVisible()) | 305 | + | _, stillExists := app.floatingSessions[key] |
| 306 | + | assert.False(t, stillExists) | |||
| 283 | 307 | ||||
| 284 | - | handled = app.closeActiveFloatingPane(context.Background()) | 308 | + | handled = app.closeAndReleaseActiveFloatingPane(context.Background()) |
| 285 | assert.False(t, handled) | 309 | assert.False(t, handled) | ||
| 286 | } | 310 | } | ||
| 287 | 311 | ||||