fix: restore stacked pane callbacks
Rewire stacked-pane activation handlers when rebuilding a session so restored stack title bars remain clickable.
- commit
- 2f5a6d256fbaeabeff94aa46e1879ebf3383360e
- author
- brice · · 2026-04-15 09:53 UTC
- parent
- f439770
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +93 -0
M internal/ui/app.go +4 -0
| @@ -1692,6 +1692,10 @@ func (a *App) restoreSession(ctx context.Context, sessionID entity.SessionID) er | |||
| 1692 | 1692 | tabBar := a.mainWindow.TabBar() | |
| 1693 | 1693 | for _, tab := range a.tabs.Tabs { | |
| 1694 | 1694 | a.createWorkspaceViewWithoutAttach(ctx, tab) | |
| 1695 | + | wsView := a.workspaceViews[tab.ID] | |
| 1696 | + | if a.wsCoord != nil && wsView != nil { | |
| 1697 | + | a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView) | |
| 1698 | + | } | |
| 1695 | 1699 | if tabBar != nil { | |
| 1696 | 1700 | tabBar.AddTab(tab) | |
| 1697 | 1701 | } | |
| @@ -1692,6 +1692,10 @@ func (a *App) restoreSession(ctx context.Context, sessionID entity.SessionID) er | |||||
| 1692 | tabBar := a.mainWindow.TabBar() | 1692 | tabBar := a.mainWindow.TabBar() | ||
| 1693 | for _, tab := range a.tabs.Tabs { | 1693 | for _, tab := range a.tabs.Tabs { | ||
| 1694 | a.createWorkspaceViewWithoutAttach(ctx, tab) | 1694 | a.createWorkspaceViewWithoutAttach(ctx, tab) | ||
| 1695 | + | wsView := a.workspaceViews[tab.ID] | |||
| 1696 | + | if a.wsCoord != nil && wsView != nil { | |||
| 1697 | + | a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView) | |||
| 1698 | + | } | |||
| 1695 | if tabBar != nil { | 1699 | if tabBar != nil { | ||
| 1696 | tabBar.AddTab(tab) | 1700 | tabBar.AddTab(tab) | ||
| 1697 | } | 1701 | } | ||
M internal/ui/app_browser_launch_test.go +89 -0
| @@ -162,6 +162,20 @@ func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool { | |||
| 162 | 162 | return mw.TabBar().Box().GetVisible() | |
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | + | func stackedViewOnActivateIsNil(t *testing.T, sv *layout.StackedView) bool { | |
| 166 | + | t.Helper() | |
| 167 | + | if sv == nil { | |
| 168 | + | return true | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | rv := reflect.ValueOf(sv).Elem() | |
| 172 | + | fv := rv.FieldByName("onActivate") | |
| 173 | + | if !fv.IsValid() { | |
| 174 | + | t.Fatalf("StackedView missing onActivate field") | |
| 175 | + | } | |
| 176 | + | return reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().IsNil() | |
| 177 | + | } | |
| 178 | + | ||
| 165 | 179 | func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) { | |
| 166 | 180 | t.Helper() | |
| 167 | 181 | ||
| @@ -329,6 +343,81 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi | |||
| 329 | 343 | } | |
| 330 | 344 | } | |
| 331 | 345 | ||
| 346 | + | func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) { | |
| 347 | + | EnsureAdwaitaInitialized() | |
| 348 | + | appID := AppID | |
| 349 | + | gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue) | |
| 350 | + | if gtkApp == nil { | |
| 351 | + | t.Fatal("gtk application creation failed") | |
| 352 | + | } | |
| 353 | + | defer gtkApp.Unref() | |
| 354 | + | ||
| 355 | + | mainWindow, err := window.New(context.Background(), gtkApp, "top") | |
| 356 | + | if err != nil { | |
| 357 | + | t.Fatalf("window creation failed: %v", err) | |
| 358 | + | } | |
| 359 | + | defer mainWindow.Destroy() | |
| 360 | + | ||
| 361 | + | stackedSessionID := entity.SessionID("session-stacked-restore") | |
| 362 | + | stackedTabs := entity.NewTabList() | |
| 363 | + | stackedTabs.Add(entity.NewTab(entity.TabID("restored-tab"), entity.WorkspaceID("restored-workspace"), entity.NewPane(entity.PaneID("restored-pane-1")))) | |
| 364 | + | stackedTabs.Tabs[0].Workspace = &entity.Workspace{ | |
| 365 | + | ID: entity.WorkspaceID("restored-workspace"), | |
| 366 | + | Root: &entity.PaneNode{ | |
| 367 | + | ID: "restored-stack-root", | |
| 368 | + | IsStacked: true, | |
| 369 | + | Children: []*entity.PaneNode{ | |
| 370 | + | {ID: "restored-child-1", Pane: entity.NewPane(entity.PaneID("restored-pane-1"))}, | |
| 371 | + | {ID: "restored-child-2", Pane: entity.NewPane(entity.PaneID("restored-pane-2"))}, | |
| 372 | + | }, | |
| 373 | + | ActiveStackIndex: 0, | |
| 374 | + | }, | |
| 375 | + | ActivePaneID: entity.PaneID("restored-pane-1"), | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | app := &App{ | |
| 379 | + | deps: &Dependencies{ | |
| 380 | + | Config: &config.Config{}, | |
| 381 | + | SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)}, | |
| 382 | + | }, | |
| 383 | + | mainWindow: mainWindow, | |
| 384 | + | widgetFactory: layout.NewGtkWidgetFactory(), | |
| 385 | + | contentCoord: &contentcoord.Coordinator{}, | |
| 386 | + | wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}), | |
| 387 | + | tabs: entity.NewTabList(), | |
| 388 | + | browserWindows: map[string]*browserWindow{}, | |
| 389 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{}, | |
| 390 | + | windowForTab: map[entity.TabID]*browserWindow{}, | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | if err := app.restoreSession(context.Background(), stackedSessionID); err != nil { | |
| 394 | + | t.Fatalf("restoreSession returned error: %v", err) | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | restoredTab := app.tabs.ActiveTab() | |
| 398 | + | if restoredTab == nil { | |
| 399 | + | t.Fatal("restored active tab missing") | |
| 400 | + | } | |
| 401 | + | wv := app.workspaceViews[restoredTab.ID] | |
| 402 | + | if wv == nil { | |
| 403 | + | t.Fatal("restored workspace view missing") | |
| 404 | + | } | |
| 405 | + | tr := wv.TreeRenderer() | |
| 406 | + | if tr == nil { | |
| 407 | + | t.Fatal("restored tree renderer missing") | |
| 408 | + | } | |
| 409 | + | if restoredTab.Workspace == nil || restoredTab.Workspace.Root == nil || len(restoredTab.Workspace.Root.Children) == 0 { | |
| 410 | + | t.Fatal("restored stacked workspace missing children") | |
| 411 | + | } | |
| 412 | + | stackedView := tr.GetStackedViewForPane(string(restoredTab.Workspace.Root.Children[0].Pane.ID)) | |
| 413 | + | if stackedView == nil { | |
| 414 | + | t.Fatal("restored stacked view missing") | |
| 415 | + | } | |
| 416 | + | if stackedViewOnActivateIsNil(t, stackedView) { | |
| 417 | + | t.Fatal("restored stacked view onActivate callback is nil") | |
| 418 | + | } | |
| 419 | + | } | |
| 420 | + | ||
| 332 | 421 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | |
| 333 | 422 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | |
| 334 | 423 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | |
| @@ -162,6 +162,20 @@ func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool { | |||||
| 162 | return mw.TabBar().Box().GetVisible() | 162 | return mw.TabBar().Box().GetVisible() | ||
| 163 | } | 163 | } | ||
| 164 | 164 | ||||
| 165 | + | func stackedViewOnActivateIsNil(t *testing.T, sv *layout.StackedView) bool { | |||
| 166 | + | t.Helper() | |||
| 167 | + | if sv == nil { | |||
| 168 | + | return true | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | rv := reflect.ValueOf(sv).Elem() | |||
| 172 | + | fv := rv.FieldByName("onActivate") | |||
| 173 | + | if !fv.IsValid() { | |||
| 174 | + | t.Fatalf("StackedView missing onActivate field") | |||
| 175 | + | } | |||
| 176 | + | return reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().IsNil() | |||
| 177 | + | } | |||
| 178 | + | ||||
| 165 | func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) { | 179 | func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) { | ||
| 166 | t.Helper() | 180 | t.Helper() | ||
| 167 | 181 | ||||
| @@ -329,6 +343,81 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi | |||||
| 329 | } | 343 | } | ||
| 330 | } | 344 | } | ||
| 331 | 345 | ||||
| 346 | + | func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) { | |||
| 347 | + | EnsureAdwaitaInitialized() | |||
| 348 | + | appID := AppID | |||
| 349 | + | gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue) | |||
| 350 | + | if gtkApp == nil { | |||
| 351 | + | t.Fatal("gtk application creation failed") | |||
| 352 | + | } | |||
| 353 | + | defer gtkApp.Unref() | |||
| 354 | + | ||||
| 355 | + | mainWindow, err := window.New(context.Background(), gtkApp, "top") | |||
| 356 | + | if err != nil { | |||
| 357 | + | t.Fatalf("window creation failed: %v", err) | |||
| 358 | + | } | |||
| 359 | + | defer mainWindow.Destroy() | |||
| 360 | + | ||||
| 361 | + | stackedSessionID := entity.SessionID("session-stacked-restore") | |||
| 362 | + | stackedTabs := entity.NewTabList() | |||
| 363 | + | stackedTabs.Add(entity.NewTab(entity.TabID("restored-tab"), entity.WorkspaceID("restored-workspace"), entity.NewPane(entity.PaneID("restored-pane-1")))) | |||
| 364 | + | stackedTabs.Tabs[0].Workspace = &entity.Workspace{ | |||
| 365 | + | ID: entity.WorkspaceID("restored-workspace"), | |||
| 366 | + | Root: &entity.PaneNode{ | |||
| 367 | + | ID: "restored-stack-root", | |||
| 368 | + | IsStacked: true, | |||
| 369 | + | Children: []*entity.PaneNode{ | |||
| 370 | + | {ID: "restored-child-1", Pane: entity.NewPane(entity.PaneID("restored-pane-1"))}, | |||
| 371 | + | {ID: "restored-child-2", Pane: entity.NewPane(entity.PaneID("restored-pane-2"))}, | |||
| 372 | + | }, | |||
| 373 | + | ActiveStackIndex: 0, | |||
| 374 | + | }, | |||
| 375 | + | ActivePaneID: entity.PaneID("restored-pane-1"), | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | app := &App{ | |||
| 379 | + | deps: &Dependencies{ | |||
| 380 | + | Config: &config.Config{}, | |||
| 381 | + | SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)}, | |||
| 382 | + | }, | |||
| 383 | + | mainWindow: mainWindow, | |||
| 384 | + | widgetFactory: layout.NewGtkWidgetFactory(), | |||
| 385 | + | contentCoord: &contentcoord.Coordinator{}, | |||
| 386 | + | wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}), | |||
| 387 | + | tabs: entity.NewTabList(), | |||
| 388 | + | browserWindows: map[string]*browserWindow{}, | |||
| 389 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{}, | |||
| 390 | + | windowForTab: map[entity.TabID]*browserWindow{}, | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | if err := app.restoreSession(context.Background(), stackedSessionID); err != nil { | |||
| 394 | + | t.Fatalf("restoreSession returned error: %v", err) | |||
| 395 | + | } | |||
| 396 | + | ||||
| 397 | + | restoredTab := app.tabs.ActiveTab() | |||
| 398 | + | if restoredTab == nil { | |||
| 399 | + | t.Fatal("restored active tab missing") | |||
| 400 | + | } | |||
| 401 | + | wv := app.workspaceViews[restoredTab.ID] | |||
| 402 | + | if wv == nil { | |||
| 403 | + | t.Fatal("restored workspace view missing") | |||
| 404 | + | } | |||
| 405 | + | tr := wv.TreeRenderer() | |||
| 406 | + | if tr == nil { | |||
| 407 | + | t.Fatal("restored tree renderer missing") | |||
| 408 | + | } | |||
| 409 | + | if restoredTab.Workspace == nil || restoredTab.Workspace.Root == nil || len(restoredTab.Workspace.Root.Children) == 0 { | |||
| 410 | + | t.Fatal("restored stacked workspace missing children") | |||
| 411 | + | } | |||
| 412 | + | stackedView := tr.GetStackedViewForPane(string(restoredTab.Workspace.Root.Children[0].Pane.ID)) | |||
| 413 | + | if stackedView == nil { | |||
| 414 | + | t.Fatal("restored stacked view missing") | |||
| 415 | + | } | |||
| 416 | + | if stackedViewOnActivateIsNil(t, stackedView) { | |||
| 417 | + | t.Fatal("restored stacked view onActivate callback is nil") | |||
| 418 | + | } | |||
| 419 | + | } | |||
| 420 | + | ||||
| 332 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | 421 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | ||
| 333 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | 422 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | ||
| 334 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | 423 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | ||