detached 482d014

fix(ui): restore floating focus and release on close

commit
482d01454c8fd999274639c58618d5cdc343a8e4
author
brice · · 2026-02-09 10:42 UTC
parent
48c09fe
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
151151 webView *webkit.WebView
152152 overlay layout.OverlayWidget
153153 widget layout.Widget
154+ focusWidget layout.Widget
154155 omnibox *component.Omnibox
155156 omniboxWidget layout.Widget
156157 resizeWatcherActive bool
@@ -618,7 +619,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
618619 a.keyboardHandler = input.NewKeyboardHandler(ctx, a.deps.Config)
619620 a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error {
620621 if action == input.ActionClosePane {
621- if a.closeActiveFloatingPane(ctx) {
622+ if a.closeAndReleaseActiveFloatingPane(ctx) {
622623 return nil
623624 }
624625 }
@@ -2182,21 +2183,33 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe
21822183 return x, y, width, height, true
21832184 }
21842185
2186+//nolint:gosec // GtkOverlay get-child-position provides a valid GtkAllocation pointer owned by GTK.
21852187 func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool {
21862188 if allocationPtr == nil {
21872189 return false
21882190 }
21892191
2192+ const (
2193+ gtkAllocationOffsetX = uintptr(0)
2194+ gtkAllocationOffsetY = uintptr(4)
2195+ gtkAllocationOffsetWidth = uintptr(8)
2196+ gtkAllocationOffsetHeight = uintptr(12)
2197+ )
2198+
21902199 // GtkAllocation/GdkRectangle is four 32-bit signed integers in C.
21912200 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)
21962205 return true
21972206 }
21982207
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) {
22002213 for key, session := range a.floatingSessions {
22012214 if key.tabID != tabID || session == nil || session.widget == nil {
22022215 continue
@@ -2273,6 +2286,9 @@ func (a *App) ensureFloatingSession(
22732286 }
22742287 if session.paneView != nil {
22752288 wsView.RegisterPaneView(session.paneID, session.paneView)
2289+ if session.focusWidget == nil {
2290+ session.focusWidget = session.paneView.WebViewWidget()
2291+ }
22762292 }
22772293 return session, nil
22782294 }
@@ -2334,12 +2350,13 @@ func (a *App) ensureFloatingSession(
23342350 })
23352351
23362352 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,
23432360 }
23442361 a.floatingSessions[key] = session
23452362 return session, nil
@@ -2363,41 +2380,66 @@ func (a *App) releaseFloatingSessionsForTab(ctx context.Context, tabID entity.Ta
23632380 }
23642381
23652382 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)
23832384 }
23842385 a.syncFloatingFocus()
23852386 }
23862387
2387-func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) {
2388+func (a *App) activeFloatingSessionEntry() (floatingSessionKey, *floatingWorkspaceSession, bool) {
23882389 activeTab := a.tabs.ActiveTab()
23892390 if activeTab == nil {
2390- return nil, ""
2391+ return floatingSessionKey{}, nil, false
23912392 }
23922393 for key, session := range a.floatingSessions {
23932394 if key.tabID != activeTab.ID || session == nil || session.pane == nil {
23942395 continue
23952396 }
23962397 if session.pane.IsVisible() {
2397- return session, activeTab.ID
2398+ return key, session, true
23982399 }
23992400 }
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
24012443 }
24022444
24032445 func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) {
@@ -2504,6 +2546,33 @@ func (a *App) hideFloatingOmnibox(ctx context.Context, session *floatingWorkspac
25042546 session.omniboxWidget = nil
25052547 }
25062548
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+
25072576 func (a *App) resizeFloatingWidget(session *floatingWorkspaceSession) {
25082577 if session == nil || session.pane == nil {
25092578 return
@@ -2608,6 +2677,16 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool {
26082677 return true
26092678 }
26102679
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+
26112690 // ToggleFloatingPane toggles the active workspace floating pane visibility.
26122691 func (a *App) ToggleFloatingPane(ctx context.Context) error {
26132692 activeTab := a.tabs.ActiveTab()
@@ -2660,18 +2739,20 @@ func (a *App) openFloatingPaneSession(ctx context.Context, sessionID string, url
26602739 return err
26612740 }
26622741 }
2663- a.hideFloatingOmnibox(ctx, session)
26642742 } else {
26652743 if err := session.pane.ShowToggle(ctx); err != nil {
26662744 return err
26672745 }
2668- if session.pane.IsOmniboxVisible() {
2669- a.showFloatingOmnibox(ctx, session)
2670- }
26712746 }
26722747
26732748 a.resizeFloatingWidget(session)
26742749 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+ }
26752756 a.startFloatingResizeWatcher(session)
26762757 a.syncFloatingFocus()
26772758
@@ -151,6 +151,7 @@ type floatingWorkspaceSession struct {
151 webView *webkit.WebView151 webView *webkit.WebView
152 overlay layout.OverlayWidget152 overlay layout.OverlayWidget
153 widget layout.Widget153 widget layout.Widget
154+ focusWidget layout.Widget
154 omnibox *component.Omnibox155 omnibox *component.Omnibox
155 omniboxWidget layout.Widget156 omniboxWidget layout.Widget
156 resizeWatcherActive bool157 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 nil623 return nil
623 }624 }
624 }625 }
@@ -2182,21 +2183,33 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe
2182 return x, y, width, height, true2183 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 false2189 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 true2205 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 continue2215 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, nil2293 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] = session2361 a.floatingSessions[key] = session
2345 return session, nil2362 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 continue2395 continue
2395 }2396 }
2396 if session.pane.IsVisible() {2397 if session.pane.IsVisible() {
2397- return session, activeTab.ID2398+ return key, session, true
2398 }2399 }
2399 }2400 }
2400- return nil, activeTab.ID2401+ 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 = nil2546 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 return2578 return
@@ -2608,6 +2677,16 @@ func (a *App) closeActiveFloatingPane(ctx context.Context) bool {
2608 return true2677 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 err2739 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 err2744 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
5353 hideFloatingWidget(widget)
5454 }
5555
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+
5678 func TestFloatingAllocationRect_CentersRequestedSize(t *testing.T) {
5779 x, y, width, height, ok := floatingAllocationRect(1000, 700, 820, 504)
5880
@@ -273,15 +295,17 @@ func TestFloatingPane_DefaultSessionStillUsesAboutBlank(t *testing.T) {
273295 func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) {
274296 app, tabID, _ := newFloatingPaneTestApp(t)
275297 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
277300
278301 require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", "https://mail.google.com"))
279302
280- handled := app.closeActiveFloatingPane(context.Background())
303+ handled := app.closeAndReleaseActiveFloatingPane(context.Background())
281304 assert.True(t, handled)
282- assert.False(t, session.pane.IsVisible())
305+ _, stillExists := app.floatingSessions[key]
306+ assert.False(t, stillExists)
283307
284- handled = app.closeActiveFloatingPane(context.Background())
308+ handled = app.closeAndReleaseActiveFloatingPane(context.Background())
285309 assert.False(t, handled)
286310 }
287311
@@ -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"}] = session298+ 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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close