refactor(ui): extract app window lifecycle helpers
- commit
- ec8c3ee6aefd4a40484572ed97980b5731fdc8c1
- author
- brice · · 2026-06-25 12:54 UTC
- parent
- c031fea
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +298 -258
M internal/ui/app.go +0 -258
| @@ -446,78 +446,6 @@ func (a *App) prewarmWebViewPoolAsync(ctx context.Context) { | |||
| 446 | 446 | pool.PrewarmAsync(ctx, 0) | |
| 447 | 447 | } | |
| 448 | 448 | ||
| 449 | - | func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) { | |
| 450 | - | log := logging.FromContext(ctx) | |
| 451 | - | log.Debug(). | |
| 452 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 453 | - | Bool("using_factory", a.browserWindowFactory != nil). | |
| 454 | - | Int("window_count_before", len(a.browserWindows)). | |
| 455 | - | Msg("ui: create browser window started") | |
| 456 | - | ||
| 457 | - | if a.browserWindowFactory != nil { | |
| 458 | - | created, err := a.browserWindowFactory(ctx, initialURL) | |
| 459 | - | if err != nil { | |
| 460 | - | log.Warn().Err(err). | |
| 461 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 462 | - | Msg("ui: browser window factory failed") | |
| 463 | - | return nil, err | |
| 464 | - | } | |
| 465 | - | created.ensureTabs() | |
| 466 | - | a.wireBrowserWindowActivationTracking(created) | |
| 467 | - | created.initChrome(ctx, a) | |
| 468 | - | return created, nil | |
| 469 | - | } | |
| 470 | - | ||
| 471 | - | runtimeCfg := a.runtimeConfigSnapshot().UI | |
| 472 | - | mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition) | |
| 473 | - | if err != nil { | |
| 474 | - | log.Warn().Err(err). | |
| 475 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 476 | - | Msg("ui: GTK browser window shell creation failed") | |
| 477 | - | return nil, err | |
| 478 | - | } | |
| 479 | - | browserWindow := &browserWindow{ | |
| 480 | - | id: a.generateWindowID(), | |
| 481 | - | initialURL: initialURL, | |
| 482 | - | tabs: entity.NewTabList(), | |
| 483 | - | mainWindow: mainWindow, | |
| 484 | - | } | |
| 485 | - | a.wireBrowserWindowActivationTracking(browserWindow) | |
| 486 | - | if a.mainWindow == nil { | |
| 487 | - | a.mainWindow = mainWindow | |
| 488 | - | } | |
| 489 | - | ||
| 490 | - | closeRequestCb := func(_ gtk.Window) bool { | |
| 491 | - | log.Info().Msg("browser window close requested") | |
| 492 | - | a.removeBrowserWindow(browserWindow.id) | |
| 493 | - | return false | |
| 494 | - | } | |
| 495 | - | mainWindow.Window().ConnectCloseRequest(&closeRequestCb) | |
| 496 | - | ||
| 497 | - | a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg) | |
| 498 | - | a.wireBrowserWindowPermissionIndicator(browserWindow) | |
| 499 | - | if a.kbDispatcher != nil { | |
| 500 | - | a.initBrowserWindowInput(ctx, browserWindow) | |
| 501 | - | } | |
| 502 | - | if a.tabCoord != nil { | |
| 503 | - | a.wireBrowserWindowTabBar(ctx, browserWindow) | |
| 504 | - | } | |
| 505 | - | browserWindow.initChrome(ctx, a) | |
| 506 | - | ||
| 507 | - | // Apply GTK CSS styling from theme manager. | |
| 508 | - | if a.deps == nil || a.deps.Theme == nil { | |
| 509 | - | return browserWindow, nil | |
| 510 | - | } | |
| 511 | - | if display := mainWindow.Window().GetDisplay(); display != nil { | |
| 512 | - | a.deps.Theme.ApplyToDisplay(ctx, display) | |
| 513 | - | } | |
| 514 | - | log.Debug(). | |
| 515 | - | Str("window_id", browserWindow.id). | |
| 516 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 517 | - | Msg("ui: create browser window completed") | |
| 518 | - | return browserWindow, nil | |
| 519 | - | } | |
| 520 | - | ||
| 521 | 449 | func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) { | |
| 522 | 450 | // Create permission popup and dialog presenter | |
| 523 | 451 | if a.deps != nil && a.deps.PermissionUC != nil { | |
| @@ -830,192 +758,6 @@ func (a *App) closeBrowserLaunchRelayListener() { | |||
| 830 | 758 | a.browserLaunchRelayCloser = nil | |
| 831 | 759 | } | |
| 832 | 760 | ||
| 833 | - | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |
| 834 | - | log := logging.FromContext(ctx) | |
| 835 | - | log.Debug(). | |
| 836 | - | Str("url_host", logging.SafeURLHost(url)). | |
| 837 | - | Int("window_count_before", len(a.browserWindows)). | |
| 838 | - | Bool("has_tab_coord", a.tabCoord != nil). | |
| 839 | - | Bool("has_tabs_uc", a.tabsUC != nil). | |
| 840 | - | Msg("ui: open fresh window started") | |
| 841 | - | ||
| 842 | - | created, err := a.createBrowserWindow(ctx, url) | |
| 843 | - | if err != nil { | |
| 844 | - | log.Warn().Err(err). | |
| 845 | - | Str("url_host", logging.SafeURLHost(url)). | |
| 846 | - | Int("window_count_after", len(a.browserWindows)). | |
| 847 | - | Msg("ui: open fresh window could not create browser window") | |
| 848 | - | return err | |
| 849 | - | } | |
| 850 | - | log.Debug(). | |
| 851 | - | Str("window_id", created.id). | |
| 852 | - | Str("url_host", logging.SafeURLHost(url)). | |
| 853 | - | Msg("ui: open fresh window created browser window shell") | |
| 854 | - | a.registerBrowserWindow(created) | |
| 855 | - | log.Debug(). | |
| 856 | - | Str("window_id", created.id). | |
| 857 | - | Int("window_count_after_register", len(a.browserWindows)). | |
| 858 | - | Msg("ui: open fresh window registered browser window") | |
| 859 | - | ||
| 860 | - | if len(a.browserWindows) == 1 { | |
| 861 | - | log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window") | |
| 862 | - | a.activateBrowserWindow(created) | |
| 863 | - | return nil | |
| 864 | - | } | |
| 865 | - | ||
| 866 | - | var openErr error | |
| 867 | - | path := "tabs_uc" | |
| 868 | - | if a.tabCoord != nil { | |
| 869 | - | path = "tab_coord" | |
| 870 | - | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator") | |
| 871 | - | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) | |
| 872 | - | } else if a.tabsUC == nil { | |
| 873 | - | log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case") | |
| 874 | - | a.activateBrowserWindow(created) | |
| 875 | - | return nil | |
| 876 | - | } else { | |
| 877 | - | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case") | |
| 878 | - | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |
| 879 | - | } | |
| 880 | - | if openErr != nil { | |
| 881 | - | log.Warn().Err(openErr). | |
| 882 | - | Str("window_id", created.id). | |
| 883 | - | Str("path", path). | |
| 884 | - | Int("window_count_after", len(a.browserWindows)). | |
| 885 | - | Msg("ui: open fresh window initial tab creation failed") | |
| 886 | - | return openErr | |
| 887 | - | } | |
| 888 | - | log.Debug(). | |
| 889 | - | Str("window_id", created.id). | |
| 890 | - | Str("path", path). | |
| 891 | - | Msg("ui: activating fresh browser window after tab creation") | |
| 892 | - | a.activateBrowserWindow(created) | |
| 893 | - | return nil | |
| 894 | - | } | |
| 895 | - | ||
| 896 | - | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |
| 897 | - | log := logging.FromContext(ctx) | |
| 898 | - | previousTarget := a.tabCoord.CurrentTarget() | |
| 899 | - | target := a.ensureTabTargetForBrowserWindow(created) | |
| 900 | - | a.tabCoord.SetCurrentTarget(target) | |
| 901 | - | defer func() { | |
| 902 | - | a.tabCoord.SetCurrentTarget(previousTarget) | |
| 903 | - | }() | |
| 904 | - | ||
| 905 | - | log.Debug(). | |
| 906 | - | Str("window_id", created.id). | |
| 907 | - | Str("url_host", logging.SafeURLHost(url)). | |
| 908 | - | Msg("ui: tab coordinator create started for fresh window") | |
| 909 | - | createdTab, createErr := a.tabCoord.Create(ctx, target, url) | |
| 910 | - | if createErr != nil { | |
| 911 | - | log.Warn().Err(createErr). | |
| 912 | - | Str("window_id", created.id). | |
| 913 | - | Msg("ui: tab coordinator create failed for fresh window; rolling back window") | |
| 914 | - | a.removeBrowserWindow(created.id) | |
| 915 | - | if created.mainWindow != nil { | |
| 916 | - | created.mainWindow.Destroy() | |
| 917 | - | } | |
| 918 | - | return createErr | |
| 919 | - | } | |
| 920 | - | log.Debug(). | |
| 921 | - | Str("window_id", created.id). | |
| 922 | - | Str("tab_id", string(createdTab.ID)). | |
| 923 | - | Msg("ui: tab coordinator create completed for fresh window") | |
| 924 | - | if a.workspaceViews[createdTab.ID] == nil { | |
| 925 | - | log.Warn(). | |
| 926 | - | Str("window_id", created.id). | |
| 927 | - | Str("tab_id", string(createdTab.ID)). | |
| 928 | - | Msg("ui: fresh window tab has no workspace view; rolling back window") | |
| 929 | - | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |
| 930 | - | createdTabs.Remove(createdTab.ID) | |
| 931 | - | } | |
| 932 | - | if created.mainWindow != nil { | |
| 933 | - | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |
| 934 | - | tabBar.RemoveTab(createdTab.ID) | |
| 935 | - | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |
| 936 | - | tabBar.SetActive(activeTab.ID) | |
| 937 | - | } | |
| 938 | - | } | |
| 939 | - | } | |
| 940 | - | a.removeBrowserWindow(created.id) | |
| 941 | - | if created.mainWindow != nil { | |
| 942 | - | created.mainWindow.Destroy() | |
| 943 | - | } | |
| 944 | - | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |
| 945 | - | } | |
| 946 | - | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 947 | - | // Tab was already created in the per-window TabList via tabCoord.Create; | |
| 948 | - | // active state is managed by TabList.SetActive. | |
| 949 | - | if created.mainWindow != nil { | |
| 950 | - | created.mainWindow.Show() | |
| 951 | - | } | |
| 952 | - | return nil | |
| 953 | - | } | |
| 954 | - | ||
| 955 | - | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |
| 956 | - | log := logging.FromContext(ctx) | |
| 957 | - | log.Debug(). | |
| 958 | - | Str("window_id", created.id). | |
| 959 | - | Str("url_host", logging.SafeURLHost(url)). | |
| 960 | - | Msg("ui: tabs use case create started for fresh window") | |
| 961 | - | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 962 | - | TabList: a.tabs, | |
| 963 | - | InitialURL: url, | |
| 964 | - | }) | |
| 965 | - | if err != nil { | |
| 966 | - | log.Warn().Err(err). | |
| 967 | - | Str("window_id", created.id). | |
| 968 | - | Msg("ui: tabs use case create failed for fresh window; rolling back window") | |
| 969 | - | a.removeBrowserWindow(created.id) | |
| 970 | - | if created.mainWindow != nil { | |
| 971 | - | created.mainWindow.Destroy() | |
| 972 | - | } | |
| 973 | - | return err | |
| 974 | - | } | |
| 975 | - | log.Debug(). | |
| 976 | - | Str("window_id", created.id). | |
| 977 | - | Str("tab_id", string(output.Tab.ID)). | |
| 978 | - | Msg("ui: tabs use case create completed for fresh window") | |
| 979 | - | a.setBrowserWindowForTab(output.Tab.ID, created) | |
| 980 | - | // Mirror the tabsUC-created tab into the new browser window's tab list. | |
| 981 | - | // This path uses the tabs usecase directly instead of tabCoord.Create, | |
| 982 | - | // so the per-window TabList must be populated explicitly. | |
| 983 | - | a.ensureTabListForBrowserWindow(created).Add(output.Tab) | |
| 984 | - | if a.widgetFactory != nil { | |
| 985 | - | a.createWorkspaceView(ctx, output.Tab) | |
| 986 | - | a.switchWorkspaceView(ctx, output.Tab.ID) | |
| 987 | - | } | |
| 988 | - | if a.workspaceViews[output.Tab.ID] == nil { | |
| 989 | - | log.Warn(). | |
| 990 | - | Str("window_id", created.id). | |
| 991 | - | Str("tab_id", string(output.Tab.ID)). | |
| 992 | - | Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window") | |
| 993 | - | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |
| 994 | - | createdTabs.Remove(output.Tab.ID) | |
| 995 | - | } | |
| 996 | - | if a.tabs != nil { | |
| 997 | - | a.tabs.Remove(output.Tab.ID) | |
| 998 | - | } | |
| 999 | - | if created.mainWindow != nil { | |
| 1000 | - | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |
| 1001 | - | tabBar.RemoveTab(output.Tab.ID) | |
| 1002 | - | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |
| 1003 | - | tabBar.SetActive(activeTab.ID) | |
| 1004 | - | } | |
| 1005 | - | } | |
| 1006 | - | } | |
| 1007 | - | a.removeBrowserWindow(created.id) | |
| 1008 | - | if created.mainWindow != nil { | |
| 1009 | - | created.mainWindow.Destroy() | |
| 1010 | - | } | |
| 1011 | - | return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID) | |
| 1012 | - | } | |
| 1013 | - | if created.mainWindow != nil { | |
| 1014 | - | created.mainWindow.Show() | |
| 1015 | - | } | |
| 1016 | - | return nil | |
| 1017 | - | } | |
| 1018 | - | ||
| 1019 | 761 | func (a *App) initialWindowURL() string { | |
| 1020 | 762 | if a.deps != nil { | |
| 1021 | 763 | return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL) | |
| @@ -446,78 +446,6 @@ func (a *App) prewarmWebViewPoolAsync(ctx context.Context) { | |||||
| 446 | pool.PrewarmAsync(ctx, 0) | 446 | pool.PrewarmAsync(ctx, 0) | ||
| 447 | } | 447 | } | ||
| 448 | 448 | ||||
| 449 | - | func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) { | |||
| 450 | - | log := logging.FromContext(ctx) | |||
| 451 | - | log.Debug(). | |||
| 452 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 453 | - | Bool("using_factory", a.browserWindowFactory != nil). | |||
| 454 | - | Int("window_count_before", len(a.browserWindows)). | |||
| 455 | - | Msg("ui: create browser window started") | |||
| 456 | - | ||||
| 457 | - | if a.browserWindowFactory != nil { | |||
| 458 | - | created, err := a.browserWindowFactory(ctx, initialURL) | |||
| 459 | - | if err != nil { | |||
| 460 | - | log.Warn().Err(err). | |||
| 461 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 462 | - | Msg("ui: browser window factory failed") | |||
| 463 | - | return nil, err | |||
| 464 | - | } | |||
| 465 | - | created.ensureTabs() | |||
| 466 | - | a.wireBrowserWindowActivationTracking(created) | |||
| 467 | - | created.initChrome(ctx, a) | |||
| 468 | - | return created, nil | |||
| 469 | - | } | |||
| 470 | - | ||||
| 471 | - | runtimeCfg := a.runtimeConfigSnapshot().UI | |||
| 472 | - | mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition) | |||
| 473 | - | if err != nil { | |||
| 474 | - | log.Warn().Err(err). | |||
| 475 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 476 | - | Msg("ui: GTK browser window shell creation failed") | |||
| 477 | - | return nil, err | |||
| 478 | - | } | |||
| 479 | - | browserWindow := &browserWindow{ | |||
| 480 | - | id: a.generateWindowID(), | |||
| 481 | - | initialURL: initialURL, | |||
| 482 | - | tabs: entity.NewTabList(), | |||
| 483 | - | mainWindow: mainWindow, | |||
| 484 | - | } | |||
| 485 | - | a.wireBrowserWindowActivationTracking(browserWindow) | |||
| 486 | - | if a.mainWindow == nil { | |||
| 487 | - | a.mainWindow = mainWindow | |||
| 488 | - | } | |||
| 489 | - | ||||
| 490 | - | closeRequestCb := func(_ gtk.Window) bool { | |||
| 491 | - | log.Info().Msg("browser window close requested") | |||
| 492 | - | a.removeBrowserWindow(browserWindow.id) | |||
| 493 | - | return false | |||
| 494 | - | } | |||
| 495 | - | mainWindow.Window().ConnectCloseRequest(&closeRequestCb) | |||
| 496 | - | ||||
| 497 | - | a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg) | |||
| 498 | - | a.wireBrowserWindowPermissionIndicator(browserWindow) | |||
| 499 | - | if a.kbDispatcher != nil { | |||
| 500 | - | a.initBrowserWindowInput(ctx, browserWindow) | |||
| 501 | - | } | |||
| 502 | - | if a.tabCoord != nil { | |||
| 503 | - | a.wireBrowserWindowTabBar(ctx, browserWindow) | |||
| 504 | - | } | |||
| 505 | - | browserWindow.initChrome(ctx, a) | |||
| 506 | - | ||||
| 507 | - | // Apply GTK CSS styling from theme manager. | |||
| 508 | - | if a.deps == nil || a.deps.Theme == nil { | |||
| 509 | - | return browserWindow, nil | |||
| 510 | - | } | |||
| 511 | - | if display := mainWindow.Window().GetDisplay(); display != nil { | |||
| 512 | - | a.deps.Theme.ApplyToDisplay(ctx, display) | |||
| 513 | - | } | |||
| 514 | - | log.Debug(). | |||
| 515 | - | Str("window_id", browserWindow.id). | |||
| 516 | - | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 517 | - | Msg("ui: create browser window completed") | |||
| 518 | - | return browserWindow, nil | |||
| 519 | - | } | |||
| 520 | - | ||||
| 521 | func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) { | 449 | func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) { | ||
| 522 | // Create permission popup and dialog presenter | 450 | // Create permission popup and dialog presenter | ||
| 523 | if a.deps != nil && a.deps.PermissionUC != nil { | 451 | if a.deps != nil && a.deps.PermissionUC != nil { | ||
| @@ -830,192 +758,6 @@ func (a *App) closeBrowserLaunchRelayListener() { | |||||
| 830 | a.browserLaunchRelayCloser = nil | 758 | a.browserLaunchRelayCloser = nil | ||
| 831 | } | 759 | } | ||
| 832 | 760 | ||||
| 833 | - | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 834 | - | log := logging.FromContext(ctx) | |||
| 835 | - | log.Debug(). | |||
| 836 | - | Str("url_host", logging.SafeURLHost(url)). | |||
| 837 | - | Int("window_count_before", len(a.browserWindows)). | |||
| 838 | - | Bool("has_tab_coord", a.tabCoord != nil). | |||
| 839 | - | Bool("has_tabs_uc", a.tabsUC != nil). | |||
| 840 | - | Msg("ui: open fresh window started") | |||
| 841 | - | ||||
| 842 | - | created, err := a.createBrowserWindow(ctx, url) | |||
| 843 | - | if err != nil { | |||
| 844 | - | log.Warn().Err(err). | |||
| 845 | - | Str("url_host", logging.SafeURLHost(url)). | |||
| 846 | - | Int("window_count_after", len(a.browserWindows)). | |||
| 847 | - | Msg("ui: open fresh window could not create browser window") | |||
| 848 | - | return err | |||
| 849 | - | } | |||
| 850 | - | log.Debug(). | |||
| 851 | - | Str("window_id", created.id). | |||
| 852 | - | Str("url_host", logging.SafeURLHost(url)). | |||
| 853 | - | Msg("ui: open fresh window created browser window shell") | |||
| 854 | - | a.registerBrowserWindow(created) | |||
| 855 | - | log.Debug(). | |||
| 856 | - | Str("window_id", created.id). | |||
| 857 | - | Int("window_count_after_register", len(a.browserWindows)). | |||
| 858 | - | Msg("ui: open fresh window registered browser window") | |||
| 859 | - | ||||
| 860 | - | if len(a.browserWindows) == 1 { | |||
| 861 | - | log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window") | |||
| 862 | - | a.activateBrowserWindow(created) | |||
| 863 | - | return nil | |||
| 864 | - | } | |||
| 865 | - | ||||
| 866 | - | var openErr error | |||
| 867 | - | path := "tabs_uc" | |||
| 868 | - | if a.tabCoord != nil { | |||
| 869 | - | path = "tab_coord" | |||
| 870 | - | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator") | |||
| 871 | - | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) | |||
| 872 | - | } else if a.tabsUC == nil { | |||
| 873 | - | log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case") | |||
| 874 | - | a.activateBrowserWindow(created) | |||
| 875 | - | return nil | |||
| 876 | - | } else { | |||
| 877 | - | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case") | |||
| 878 | - | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |||
| 879 | - | } | |||
| 880 | - | if openErr != nil { | |||
| 881 | - | log.Warn().Err(openErr). | |||
| 882 | - | Str("window_id", created.id). | |||
| 883 | - | Str("path", path). | |||
| 884 | - | Int("window_count_after", len(a.browserWindows)). | |||
| 885 | - | Msg("ui: open fresh window initial tab creation failed") | |||
| 886 | - | return openErr | |||
| 887 | - | } | |||
| 888 | - | log.Debug(). | |||
| 889 | - | Str("window_id", created.id). | |||
| 890 | - | Str("path", path). | |||
| 891 | - | Msg("ui: activating fresh browser window after tab creation") | |||
| 892 | - | a.activateBrowserWindow(created) | |||
| 893 | - | return nil | |||
| 894 | - | } | |||
| 895 | - | ||||
| 896 | - | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |||
| 897 | - | log := logging.FromContext(ctx) | |||
| 898 | - | previousTarget := a.tabCoord.CurrentTarget() | |||
| 899 | - | target := a.ensureTabTargetForBrowserWindow(created) | |||
| 900 | - | a.tabCoord.SetCurrentTarget(target) | |||
| 901 | - | defer func() { | |||
| 902 | - | a.tabCoord.SetCurrentTarget(previousTarget) | |||
| 903 | - | }() | |||
| 904 | - | ||||
| 905 | - | log.Debug(). | |||
| 906 | - | Str("window_id", created.id). | |||
| 907 | - | Str("url_host", logging.SafeURLHost(url)). | |||
| 908 | - | Msg("ui: tab coordinator create started for fresh window") | |||
| 909 | - | createdTab, createErr := a.tabCoord.Create(ctx, target, url) | |||
| 910 | - | if createErr != nil { | |||
| 911 | - | log.Warn().Err(createErr). | |||
| 912 | - | Str("window_id", created.id). | |||
| 913 | - | Msg("ui: tab coordinator create failed for fresh window; rolling back window") | |||
| 914 | - | a.removeBrowserWindow(created.id) | |||
| 915 | - | if created.mainWindow != nil { | |||
| 916 | - | created.mainWindow.Destroy() | |||
| 917 | - | } | |||
| 918 | - | return createErr | |||
| 919 | - | } | |||
| 920 | - | log.Debug(). | |||
| 921 | - | Str("window_id", created.id). | |||
| 922 | - | Str("tab_id", string(createdTab.ID)). | |||
| 923 | - | Msg("ui: tab coordinator create completed for fresh window") | |||
| 924 | - | if a.workspaceViews[createdTab.ID] == nil { | |||
| 925 | - | log.Warn(). | |||
| 926 | - | Str("window_id", created.id). | |||
| 927 | - | Str("tab_id", string(createdTab.ID)). | |||
| 928 | - | Msg("ui: fresh window tab has no workspace view; rolling back window") | |||
| 929 | - | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |||
| 930 | - | createdTabs.Remove(createdTab.ID) | |||
| 931 | - | } | |||
| 932 | - | if created.mainWindow != nil { | |||
| 933 | - | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |||
| 934 | - | tabBar.RemoveTab(createdTab.ID) | |||
| 935 | - | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |||
| 936 | - | tabBar.SetActive(activeTab.ID) | |||
| 937 | - | } | |||
| 938 | - | } | |||
| 939 | - | } | |||
| 940 | - | a.removeBrowserWindow(created.id) | |||
| 941 | - | if created.mainWindow != nil { | |||
| 942 | - | created.mainWindow.Destroy() | |||
| 943 | - | } | |||
| 944 | - | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |||
| 945 | - | } | |||
| 946 | - | a.setBrowserWindowForTab(createdTab.ID, created) | |||
| 947 | - | // Tab was already created in the per-window TabList via tabCoord.Create; | |||
| 948 | - | // active state is managed by TabList.SetActive. | |||
| 949 | - | if created.mainWindow != nil { | |||
| 950 | - | created.mainWindow.Show() | |||
| 951 | - | } | |||
| 952 | - | return nil | |||
| 953 | - | } | |||
| 954 | - | ||||
| 955 | - | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |||
| 956 | - | log := logging.FromContext(ctx) | |||
| 957 | - | log.Debug(). | |||
| 958 | - | Str("window_id", created.id). | |||
| 959 | - | Str("url_host", logging.SafeURLHost(url)). | |||
| 960 | - | Msg("ui: tabs use case create started for fresh window") | |||
| 961 | - | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |||
| 962 | - | TabList: a.tabs, | |||
| 963 | - | InitialURL: url, | |||
| 964 | - | }) | |||
| 965 | - | if err != nil { | |||
| 966 | - | log.Warn().Err(err). | |||
| 967 | - | Str("window_id", created.id). | |||
| 968 | - | Msg("ui: tabs use case create failed for fresh window; rolling back window") | |||
| 969 | - | a.removeBrowserWindow(created.id) | |||
| 970 | - | if created.mainWindow != nil { | |||
| 971 | - | created.mainWindow.Destroy() | |||
| 972 | - | } | |||
| 973 | - | return err | |||
| 974 | - | } | |||
| 975 | - | log.Debug(). | |||
| 976 | - | Str("window_id", created.id). | |||
| 977 | - | Str("tab_id", string(output.Tab.ID)). | |||
| 978 | - | Msg("ui: tabs use case create completed for fresh window") | |||
| 979 | - | a.setBrowserWindowForTab(output.Tab.ID, created) | |||
| 980 | - | // Mirror the tabsUC-created tab into the new browser window's tab list. | |||
| 981 | - | // This path uses the tabs usecase directly instead of tabCoord.Create, | |||
| 982 | - | // so the per-window TabList must be populated explicitly. | |||
| 983 | - | a.ensureTabListForBrowserWindow(created).Add(output.Tab) | |||
| 984 | - | if a.widgetFactory != nil { | |||
| 985 | - | a.createWorkspaceView(ctx, output.Tab) | |||
| 986 | - | a.switchWorkspaceView(ctx, output.Tab.ID) | |||
| 987 | - | } | |||
| 988 | - | if a.workspaceViews[output.Tab.ID] == nil { | |||
| 989 | - | log.Warn(). | |||
| 990 | - | Str("window_id", created.id). | |||
| 991 | - | Str("tab_id", string(output.Tab.ID)). | |||
| 992 | - | Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window") | |||
| 993 | - | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |||
| 994 | - | createdTabs.Remove(output.Tab.ID) | |||
| 995 | - | } | |||
| 996 | - | if a.tabs != nil { | |||
| 997 | - | a.tabs.Remove(output.Tab.ID) | |||
| 998 | - | } | |||
| 999 | - | if created.mainWindow != nil { | |||
| 1000 | - | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |||
| 1001 | - | tabBar.RemoveTab(output.Tab.ID) | |||
| 1002 | - | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |||
| 1003 | - | tabBar.SetActive(activeTab.ID) | |||
| 1004 | - | } | |||
| 1005 | - | } | |||
| 1006 | - | } | |||
| 1007 | - | a.removeBrowserWindow(created.id) | |||
| 1008 | - | if created.mainWindow != nil { | |||
| 1009 | - | created.mainWindow.Destroy() | |||
| 1010 | - | } | |||
| 1011 | - | return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID) | |||
| 1012 | - | } | |||
| 1013 | - | if created.mainWindow != nil { | |||
| 1014 | - | created.mainWindow.Show() | |||
| 1015 | - | } | |||
| 1016 | - | return nil | |||
| 1017 | - | } | |||
| 1018 | - | ||||
| 1019 | func (a *App) initialWindowURL() string { | 761 | func (a *App) initialWindowURL() string { | ||
| 1020 | if a.deps != nil { | 762 | if a.deps != nil { | ||
| 1021 | return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL) | 763 | return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL) | ||
A internal/ui/app_window_lifecycle.go +270 -0
| @@ -0,0 +1,270 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | "github.com/bnema/dumber/internal/ui/window" | |
| 11 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) { | |
| 15 | + | log := logging.FromContext(ctx) | |
| 16 | + | log.Debug(). | |
| 17 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 18 | + | Bool("using_factory", a.browserWindowFactory != nil). | |
| 19 | + | Int("window_count_before", len(a.browserWindows)). | |
| 20 | + | Msg("ui: create browser window started") | |
| 21 | + | ||
| 22 | + | if a.browserWindowFactory != nil { | |
| 23 | + | created, err := a.browserWindowFactory(ctx, initialURL) | |
| 24 | + | if err != nil { | |
| 25 | + | log.Warn().Err(err). | |
| 26 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 27 | + | Msg("ui: browser window factory failed") | |
| 28 | + | return nil, err | |
| 29 | + | } | |
| 30 | + | created.ensureTabs() | |
| 31 | + | a.wireBrowserWindowActivationTracking(created) | |
| 32 | + | created.initChrome(ctx, a) | |
| 33 | + | return created, nil | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | runtimeCfg := a.runtimeConfigSnapshot().UI | |
| 37 | + | mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition) | |
| 38 | + | if err != nil { | |
| 39 | + | log.Warn().Err(err). | |
| 40 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 41 | + | Msg("ui: GTK browser window shell creation failed") | |
| 42 | + | return nil, err | |
| 43 | + | } | |
| 44 | + | browserWindow := &browserWindow{ | |
| 45 | + | id: a.generateWindowID(), | |
| 46 | + | initialURL: initialURL, | |
| 47 | + | tabs: entity.NewTabList(), | |
| 48 | + | mainWindow: mainWindow, | |
| 49 | + | } | |
| 50 | + | a.wireBrowserWindowActivationTracking(browserWindow) | |
| 51 | + | if a.mainWindow == nil { | |
| 52 | + | a.mainWindow = mainWindow | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | closeRequestCb := func(_ gtk.Window) bool { | |
| 56 | + | log.Info().Msg("browser window close requested") | |
| 57 | + | a.removeBrowserWindow(browserWindow.id) | |
| 58 | + | return false | |
| 59 | + | } | |
| 60 | + | mainWindow.Window().ConnectCloseRequest(&closeRequestCb) | |
| 61 | + | ||
| 62 | + | a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg) | |
| 63 | + | a.wireBrowserWindowPermissionIndicator(browserWindow) | |
| 64 | + | if a.kbDispatcher != nil { | |
| 65 | + | a.initBrowserWindowInput(ctx, browserWindow) | |
| 66 | + | } | |
| 67 | + | if a.tabCoord != nil { | |
| 68 | + | a.wireBrowserWindowTabBar(ctx, browserWindow) | |
| 69 | + | } | |
| 70 | + | browserWindow.initChrome(ctx, a) | |
| 71 | + | ||
| 72 | + | // Apply GTK CSS styling from theme manager. | |
| 73 | + | if a.deps == nil || a.deps.Theme == nil { | |
| 74 | + | return browserWindow, nil | |
| 75 | + | } | |
| 76 | + | if display := mainWindow.Window().GetDisplay(); display != nil { | |
| 77 | + | a.deps.Theme.ApplyToDisplay(ctx, display) | |
| 78 | + | } | |
| 79 | + | log.Debug(). | |
| 80 | + | Str("window_id", browserWindow.id). | |
| 81 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |
| 82 | + | Msg("ui: create browser window completed") | |
| 83 | + | return browserWindow, nil | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |
| 87 | + | log := logging.FromContext(ctx) | |
| 88 | + | log.Debug(). | |
| 89 | + | Str("url_host", logging.SafeURLHost(url)). | |
| 90 | + | Int("window_count_before", len(a.browserWindows)). | |
| 91 | + | Bool("has_tab_coord", a.tabCoord != nil). | |
| 92 | + | Bool("has_tabs_uc", a.tabsUC != nil). | |
| 93 | + | Msg("ui: open fresh window started") | |
| 94 | + | ||
| 95 | + | created, err := a.createBrowserWindow(ctx, url) | |
| 96 | + | if err != nil { | |
| 97 | + | log.Warn().Err(err). | |
| 98 | + | Str("url_host", logging.SafeURLHost(url)). | |
| 99 | + | Int("window_count_after", len(a.browserWindows)). | |
| 100 | + | Msg("ui: open fresh window could not create browser window") | |
| 101 | + | return err | |
| 102 | + | } | |
| 103 | + | log.Debug(). | |
| 104 | + | Str("window_id", created.id). | |
| 105 | + | Str("url_host", logging.SafeURLHost(url)). | |
| 106 | + | Msg("ui: open fresh window created browser window shell") | |
| 107 | + | a.registerBrowserWindow(created) | |
| 108 | + | log.Debug(). | |
| 109 | + | Str("window_id", created.id). | |
| 110 | + | Int("window_count_after_register", len(a.browserWindows)). | |
| 111 | + | Msg("ui: open fresh window registered browser window") | |
| 112 | + | ||
| 113 | + | if len(a.browserWindows) == 1 { | |
| 114 | + | log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window") | |
| 115 | + | a.activateBrowserWindow(created) | |
| 116 | + | return nil | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | var openErr error | |
| 120 | + | path := "tabs_uc" | |
| 121 | + | if a.tabCoord != nil { | |
| 122 | + | path = "tab_coord" | |
| 123 | + | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator") | |
| 124 | + | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) | |
| 125 | + | } else if a.tabsUC == nil { | |
| 126 | + | log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case") | |
| 127 | + | a.activateBrowserWindow(created) | |
| 128 | + | return nil | |
| 129 | + | } else { | |
| 130 | + | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case") | |
| 131 | + | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |
| 132 | + | } | |
| 133 | + | if openErr != nil { | |
| 134 | + | log.Warn().Err(openErr). | |
| 135 | + | Str("window_id", created.id). | |
| 136 | + | Str("path", path). | |
| 137 | + | Int("window_count_after", len(a.browserWindows)). | |
| 138 | + | Msg("ui: open fresh window initial tab creation failed") | |
| 139 | + | return openErr | |
| 140 | + | } | |
| 141 | + | log.Debug(). | |
| 142 | + | Str("window_id", created.id). | |
| 143 | + | Str("path", path). | |
| 144 | + | Msg("ui: activating fresh browser window after tab creation") | |
| 145 | + | a.activateBrowserWindow(created) | |
| 146 | + | return nil | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |
| 150 | + | log := logging.FromContext(ctx) | |
| 151 | + | previousTarget := a.tabCoord.CurrentTarget() | |
| 152 | + | target := a.ensureTabTargetForBrowserWindow(created) | |
| 153 | + | a.tabCoord.SetCurrentTarget(target) | |
| 154 | + | defer func() { | |
| 155 | + | a.tabCoord.SetCurrentTarget(previousTarget) | |
| 156 | + | }() | |
| 157 | + | ||
| 158 | + | log.Debug(). | |
| 159 | + | Str("window_id", created.id). | |
| 160 | + | Str("url_host", logging.SafeURLHost(url)). | |
| 161 | + | Msg("ui: tab coordinator create started for fresh window") | |
| 162 | + | createdTab, createErr := a.tabCoord.Create(ctx, target, url) | |
| 163 | + | if createErr != nil { | |
| 164 | + | log.Warn().Err(createErr). | |
| 165 | + | Str("window_id", created.id). | |
| 166 | + | Msg("ui: tab coordinator create failed for fresh window; rolling back window") | |
| 167 | + | a.removeBrowserWindow(created.id) | |
| 168 | + | if created.mainWindow != nil { | |
| 169 | + | created.mainWindow.Destroy() | |
| 170 | + | } | |
| 171 | + | return createErr | |
| 172 | + | } | |
| 173 | + | log.Debug(). | |
| 174 | + | Str("window_id", created.id). | |
| 175 | + | Str("tab_id", string(createdTab.ID)). | |
| 176 | + | Msg("ui: tab coordinator create completed for fresh window") | |
| 177 | + | if a.workspaceViews[createdTab.ID] == nil { | |
| 178 | + | log.Warn(). | |
| 179 | + | Str("window_id", created.id). | |
| 180 | + | Str("tab_id", string(createdTab.ID)). | |
| 181 | + | Msg("ui: fresh window tab has no workspace view; rolling back window") | |
| 182 | + | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |
| 183 | + | createdTabs.Remove(createdTab.ID) | |
| 184 | + | } | |
| 185 | + | if created.mainWindow != nil { | |
| 186 | + | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |
| 187 | + | tabBar.RemoveTab(createdTab.ID) | |
| 188 | + | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |
| 189 | + | tabBar.SetActive(activeTab.ID) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | a.removeBrowserWindow(created.id) | |
| 194 | + | if created.mainWindow != nil { | |
| 195 | + | created.mainWindow.Destroy() | |
| 196 | + | } | |
| 197 | + | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |
| 198 | + | } | |
| 199 | + | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 200 | + | // Tab was already created in the per-window TabList via tabCoord.Create; | |
| 201 | + | // active state is managed by TabList.SetActive. | |
| 202 | + | if created.mainWindow != nil { | |
| 203 | + | created.mainWindow.Show() | |
| 204 | + | } | |
| 205 | + | return nil | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |
| 209 | + | log := logging.FromContext(ctx) | |
| 210 | + | log.Debug(). | |
| 211 | + | Str("window_id", created.id). | |
| 212 | + | Str("url_host", logging.SafeURLHost(url)). | |
| 213 | + | Msg("ui: tabs use case create started for fresh window") | |
| 214 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 215 | + | TabList: a.tabs, | |
| 216 | + | InitialURL: url, | |
| 217 | + | }) | |
| 218 | + | if err != nil { | |
| 219 | + | log.Warn().Err(err). | |
| 220 | + | Str("window_id", created.id). | |
| 221 | + | Msg("ui: tabs use case create failed for fresh window; rolling back window") | |
| 222 | + | a.removeBrowserWindow(created.id) | |
| 223 | + | if created.mainWindow != nil { | |
| 224 | + | created.mainWindow.Destroy() | |
| 225 | + | } | |
| 226 | + | return err | |
| 227 | + | } | |
| 228 | + | log.Debug(). | |
| 229 | + | Str("window_id", created.id). | |
| 230 | + | Str("tab_id", string(output.Tab.ID)). | |
| 231 | + | Msg("ui: tabs use case create completed for fresh window") | |
| 232 | + | a.setBrowserWindowForTab(output.Tab.ID, created) | |
| 233 | + | // Mirror the tabsUC-created tab into the new browser window's tab list. | |
| 234 | + | // This path uses the tabs usecase directly instead of tabCoord.Create, | |
| 235 | + | // so the per-window TabList must be populated explicitly. | |
| 236 | + | a.ensureTabListForBrowserWindow(created).Add(output.Tab) | |
| 237 | + | if a.widgetFactory != nil { | |
| 238 | + | a.createWorkspaceView(ctx, output.Tab) | |
| 239 | + | a.switchWorkspaceView(ctx, output.Tab.ID) | |
| 240 | + | } | |
| 241 | + | if a.workspaceViews[output.Tab.ID] == nil { | |
| 242 | + | log.Warn(). | |
| 243 | + | Str("window_id", created.id). | |
| 244 | + | Str("tab_id", string(output.Tab.ID)). | |
| 245 | + | Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window") | |
| 246 | + | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |
| 247 | + | createdTabs.Remove(output.Tab.ID) | |
| 248 | + | } | |
| 249 | + | if a.tabs != nil { | |
| 250 | + | a.tabs.Remove(output.Tab.ID) | |
| 251 | + | } | |
| 252 | + | if created.mainWindow != nil { | |
| 253 | + | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |
| 254 | + | tabBar.RemoveTab(output.Tab.ID) | |
| 255 | + | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |
| 256 | + | tabBar.SetActive(activeTab.ID) | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | a.removeBrowserWindow(created.id) | |
| 261 | + | if created.mainWindow != nil { | |
| 262 | + | created.mainWindow.Destroy() | |
| 263 | + | } | |
| 264 | + | return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID) | |
| 265 | + | } | |
| 266 | + | if created.mainWindow != nil { | |
| 267 | + | created.mainWindow.Show() | |
| 268 | + | } | |
| 269 | + | return nil | |
| 270 | + | } | |
| @@ -0,0 +1,270 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 8 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |||
| 10 | + | "github.com/bnema/dumber/internal/ui/window" | |||
| 11 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) { | |||
| 15 | + | log := logging.FromContext(ctx) | |||
| 16 | + | log.Debug(). | |||
| 17 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 18 | + | Bool("using_factory", a.browserWindowFactory != nil). | |||
| 19 | + | Int("window_count_before", len(a.browserWindows)). | |||
| 20 | + | Msg("ui: create browser window started") | |||
| 21 | + | ||||
| 22 | + | if a.browserWindowFactory != nil { | |||
| 23 | + | created, err := a.browserWindowFactory(ctx, initialURL) | |||
| 24 | + | if err != nil { | |||
| 25 | + | log.Warn().Err(err). | |||
| 26 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 27 | + | Msg("ui: browser window factory failed") | |||
| 28 | + | return nil, err | |||
| 29 | + | } | |||
| 30 | + | created.ensureTabs() | |||
| 31 | + | a.wireBrowserWindowActivationTracking(created) | |||
| 32 | + | created.initChrome(ctx, a) | |||
| 33 | + | return created, nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | runtimeCfg := a.runtimeConfigSnapshot().UI | |||
| 37 | + | mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition) | |||
| 38 | + | if err != nil { | |||
| 39 | + | log.Warn().Err(err). | |||
| 40 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 41 | + | Msg("ui: GTK browser window shell creation failed") | |||
| 42 | + | return nil, err | |||
| 43 | + | } | |||
| 44 | + | browserWindow := &browserWindow{ | |||
| 45 | + | id: a.generateWindowID(), | |||
| 46 | + | initialURL: initialURL, | |||
| 47 | + | tabs: entity.NewTabList(), | |||
| 48 | + | mainWindow: mainWindow, | |||
| 49 | + | } | |||
| 50 | + | a.wireBrowserWindowActivationTracking(browserWindow) | |||
| 51 | + | if a.mainWindow == nil { | |||
| 52 | + | a.mainWindow = mainWindow | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | closeRequestCb := func(_ gtk.Window) bool { | |||
| 56 | + | log.Info().Msg("browser window close requested") | |||
| 57 | + | a.removeBrowserWindow(browserWindow.id) | |||
| 58 | + | return false | |||
| 59 | + | } | |||
| 60 | + | mainWindow.Window().ConnectCloseRequest(&closeRequestCb) | |||
| 61 | + | ||||
| 62 | + | a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg) | |||
| 63 | + | a.wireBrowserWindowPermissionIndicator(browserWindow) | |||
| 64 | + | if a.kbDispatcher != nil { | |||
| 65 | + | a.initBrowserWindowInput(ctx, browserWindow) | |||
| 66 | + | } | |||
| 67 | + | if a.tabCoord != nil { | |||
| 68 | + | a.wireBrowserWindowTabBar(ctx, browserWindow) | |||
| 69 | + | } | |||
| 70 | + | browserWindow.initChrome(ctx, a) | |||
| 71 | + | ||||
| 72 | + | // Apply GTK CSS styling from theme manager. | |||
| 73 | + | if a.deps == nil || a.deps.Theme == nil { | |||
| 74 | + | return browserWindow, nil | |||
| 75 | + | } | |||
| 76 | + | if display := mainWindow.Window().GetDisplay(); display != nil { | |||
| 77 | + | a.deps.Theme.ApplyToDisplay(ctx, display) | |||
| 78 | + | } | |||
| 79 | + | log.Debug(). | |||
| 80 | + | Str("window_id", browserWindow.id). | |||
| 81 | + | Str("url_host", logging.SafeURLHost(initialURL)). | |||
| 82 | + | Msg("ui: create browser window completed") | |||
| 83 | + | return browserWindow, nil | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 87 | + | log := logging.FromContext(ctx) | |||
| 88 | + | log.Debug(). | |||
| 89 | + | Str("url_host", logging.SafeURLHost(url)). | |||
| 90 | + | Int("window_count_before", len(a.browserWindows)). | |||
| 91 | + | Bool("has_tab_coord", a.tabCoord != nil). | |||
| 92 | + | Bool("has_tabs_uc", a.tabsUC != nil). | |||
| 93 | + | Msg("ui: open fresh window started") | |||
| 94 | + | ||||
| 95 | + | created, err := a.createBrowserWindow(ctx, url) | |||
| 96 | + | if err != nil { | |||
| 97 | + | log.Warn().Err(err). | |||
| 98 | + | Str("url_host", logging.SafeURLHost(url)). | |||
| 99 | + | Int("window_count_after", len(a.browserWindows)). | |||
| 100 | + | Msg("ui: open fresh window could not create browser window") | |||
| 101 | + | return err | |||
| 102 | + | } | |||
| 103 | + | log.Debug(). | |||
| 104 | + | Str("window_id", created.id). | |||
| 105 | + | Str("url_host", logging.SafeURLHost(url)). | |||
| 106 | + | Msg("ui: open fresh window created browser window shell") | |||
| 107 | + | a.registerBrowserWindow(created) | |||
| 108 | + | log.Debug(). | |||
| 109 | + | Str("window_id", created.id). | |||
| 110 | + | Int("window_count_after_register", len(a.browserWindows)). | |||
| 111 | + | Msg("ui: open fresh window registered browser window") | |||
| 112 | + | ||||
| 113 | + | if len(a.browserWindows) == 1 { | |||
| 114 | + | log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window") | |||
| 115 | + | a.activateBrowserWindow(created) | |||
| 116 | + | return nil | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | var openErr error | |||
| 120 | + | path := "tabs_uc" | |||
| 121 | + | if a.tabCoord != nil { | |||
| 122 | + | path = "tab_coord" | |||
| 123 | + | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator") | |||
| 124 | + | openErr = a.openFreshWindowWithTabCoord(ctx, url, created) | |||
| 125 | + | } else if a.tabsUC == nil { | |||
| 126 | + | log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case") | |||
| 127 | + | a.activateBrowserWindow(created) | |||
| 128 | + | return nil | |||
| 129 | + | } else { | |||
| 130 | + | log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case") | |||
| 131 | + | openErr = a.openFreshWindowWithTabsUC(ctx, url, created) | |||
| 132 | + | } | |||
| 133 | + | if openErr != nil { | |||
| 134 | + | log.Warn().Err(openErr). | |||
| 135 | + | Str("window_id", created.id). | |||
| 136 | + | Str("path", path). | |||
| 137 | + | Int("window_count_after", len(a.browserWindows)). | |||
| 138 | + | Msg("ui: open fresh window initial tab creation failed") | |||
| 139 | + | return openErr | |||
| 140 | + | } | |||
| 141 | + | log.Debug(). | |||
| 142 | + | Str("window_id", created.id). | |||
| 143 | + | Str("path", path). | |||
| 144 | + | Msg("ui: activating fresh browser window after tab creation") | |||
| 145 | + | a.activateBrowserWindow(created) | |||
| 146 | + | return nil | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |||
| 150 | + | log := logging.FromContext(ctx) | |||
| 151 | + | previousTarget := a.tabCoord.CurrentTarget() | |||
| 152 | + | target := a.ensureTabTargetForBrowserWindow(created) | |||
| 153 | + | a.tabCoord.SetCurrentTarget(target) | |||
| 154 | + | defer func() { | |||
| 155 | + | a.tabCoord.SetCurrentTarget(previousTarget) | |||
| 156 | + | }() | |||
| 157 | + | ||||
| 158 | + | log.Debug(). | |||
| 159 | + | Str("window_id", created.id). | |||
| 160 | + | Str("url_host", logging.SafeURLHost(url)). | |||
| 161 | + | Msg("ui: tab coordinator create started for fresh window") | |||
| 162 | + | createdTab, createErr := a.tabCoord.Create(ctx, target, url) | |||
| 163 | + | if createErr != nil { | |||
| 164 | + | log.Warn().Err(createErr). | |||
| 165 | + | Str("window_id", created.id). | |||
| 166 | + | Msg("ui: tab coordinator create failed for fresh window; rolling back window") | |||
| 167 | + | a.removeBrowserWindow(created.id) | |||
| 168 | + | if created.mainWindow != nil { | |||
| 169 | + | created.mainWindow.Destroy() | |||
| 170 | + | } | |||
| 171 | + | return createErr | |||
| 172 | + | } | |||
| 173 | + | log.Debug(). | |||
| 174 | + | Str("window_id", created.id). | |||
| 175 | + | Str("tab_id", string(createdTab.ID)). | |||
| 176 | + | Msg("ui: tab coordinator create completed for fresh window") | |||
| 177 | + | if a.workspaceViews[createdTab.ID] == nil { | |||
| 178 | + | log.Warn(). | |||
| 179 | + | Str("window_id", created.id). | |||
| 180 | + | Str("tab_id", string(createdTab.ID)). | |||
| 181 | + | Msg("ui: fresh window tab has no workspace view; rolling back window") | |||
| 182 | + | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |||
| 183 | + | createdTabs.Remove(createdTab.ID) | |||
| 184 | + | } | |||
| 185 | + | if created.mainWindow != nil { | |||
| 186 | + | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |||
| 187 | + | tabBar.RemoveTab(createdTab.ID) | |||
| 188 | + | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |||
| 189 | + | tabBar.SetActive(activeTab.ID) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | a.removeBrowserWindow(created.id) | |||
| 194 | + | if created.mainWindow != nil { | |||
| 195 | + | created.mainWindow.Destroy() | |||
| 196 | + | } | |||
| 197 | + | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |||
| 198 | + | } | |||
| 199 | + | a.setBrowserWindowForTab(createdTab.ID, created) | |||
| 200 | + | // Tab was already created in the per-window TabList via tabCoord.Create; | |||
| 201 | + | // active state is managed by TabList.SetActive. | |||
| 202 | + | if created.mainWindow != nil { | |||
| 203 | + | created.mainWindow.Show() | |||
| 204 | + | } | |||
| 205 | + | return nil | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |||
| 209 | + | log := logging.FromContext(ctx) | |||
| 210 | + | log.Debug(). | |||
| 211 | + | Str("window_id", created.id). | |||
| 212 | + | Str("url_host", logging.SafeURLHost(url)). | |||
| 213 | + | Msg("ui: tabs use case create started for fresh window") | |||
| 214 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |||
| 215 | + | TabList: a.tabs, | |||
| 216 | + | InitialURL: url, | |||
| 217 | + | }) | |||
| 218 | + | if err != nil { | |||
| 219 | + | log.Warn().Err(err). | |||
| 220 | + | Str("window_id", created.id). | |||
| 221 | + | Msg("ui: tabs use case create failed for fresh window; rolling back window") | |||
| 222 | + | a.removeBrowserWindow(created.id) | |||
| 223 | + | if created.mainWindow != nil { | |||
| 224 | + | created.mainWindow.Destroy() | |||
| 225 | + | } | |||
| 226 | + | return err | |||
| 227 | + | } | |||
| 228 | + | log.Debug(). | |||
| 229 | + | Str("window_id", created.id). | |||
| 230 | + | Str("tab_id", string(output.Tab.ID)). | |||
| 231 | + | Msg("ui: tabs use case create completed for fresh window") | |||
| 232 | + | a.setBrowserWindowForTab(output.Tab.ID, created) | |||
| 233 | + | // Mirror the tabsUC-created tab into the new browser window's tab list. | |||
| 234 | + | // This path uses the tabs usecase directly instead of tabCoord.Create, | |||
| 235 | + | // so the per-window TabList must be populated explicitly. | |||
| 236 | + | a.ensureTabListForBrowserWindow(created).Add(output.Tab) | |||
| 237 | + | if a.widgetFactory != nil { | |||
| 238 | + | a.createWorkspaceView(ctx, output.Tab) | |||
| 239 | + | a.switchWorkspaceView(ctx, output.Tab.ID) | |||
| 240 | + | } | |||
| 241 | + | if a.workspaceViews[output.Tab.ID] == nil { | |||
| 242 | + | log.Warn(). | |||
| 243 | + | Str("window_id", created.id). | |||
| 244 | + | Str("tab_id", string(output.Tab.ID)). | |||
| 245 | + | Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window") | |||
| 246 | + | if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil { | |||
| 247 | + | createdTabs.Remove(output.Tab.ID) | |||
| 248 | + | } | |||
| 249 | + | if a.tabs != nil { | |||
| 250 | + | a.tabs.Remove(output.Tab.ID) | |||
| 251 | + | } | |||
| 252 | + | if created.mainWindow != nil { | |||
| 253 | + | if tabBar := created.mainWindow.TabBar(); tabBar != nil { | |||
| 254 | + | tabBar.RemoveTab(output.Tab.ID) | |||
| 255 | + | if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil { | |||
| 256 | + | tabBar.SetActive(activeTab.ID) | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | a.removeBrowserWindow(created.id) | |||
| 261 | + | if created.mainWindow != nil { | |||
| 262 | + | created.mainWindow.Destroy() | |||
| 263 | + | } | |||
| 264 | + | return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID) | |||
| 265 | + | } | |||
| 266 | + | if created.mainWindow != nil { | |||
| 267 | + | created.mainWindow.Show() | |||
| 268 | + | } | |||
| 269 | + | return nil | |||
| 270 | + | } | |||
M internal/ui/browser_window_test.go +28 -0
| @@ -459,6 +459,34 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||
| 459 | 459 | } | |
| 460 | 460 | } | |
| 461 | 461 | ||
| 462 | + | func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) { | |
| 463 | + | app := &App{ | |
| 464 | + | tabs: entity.NewTabList(), | |
| 465 | + | tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }), | |
| 466 | + | browserWindows: make(map[string]*browserWindow), | |
| 467 | + | } | |
| 468 | + | app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult { | |
| 469 | + | fn() | |
| 470 | + | return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted} | |
| 471 | + | } | |
| 472 | + | app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) { | |
| 473 | + | return &browserWindow{id: "window-1", tabs: entity.NewTabList()}, nil | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |
| 477 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |
| 478 | + | } | |
| 479 | + | if got := len(app.browserWindows); got != 1 { | |
| 480 | + | t.Fatalf("browserWindows length = %d, want 1", got) | |
| 481 | + | } | |
| 482 | + | if app.lastFocusedWindowID != "window-1" { | |
| 483 | + | t.Fatalf("lastFocusedWindowID = %q, want window-1", app.lastFocusedWindowID) | |
| 484 | + | } | |
| 485 | + | if got := app.tabs.Count(); got != 0 { | |
| 486 | + | t.Fatalf("tabs count = %d, want 0; first shell should wait for createInitialTab", got) | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | ||
| 462 | 490 | func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) { | |
| 463 | 491 | app := &App{browserWindows: make(map[string]*browserWindow)} | |
| 464 | 492 | app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult { | |
| @@ -459,6 +459,34 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||||
| 459 | } | 459 | } | ||
| 460 | } | 460 | } | ||
| 461 | 461 | ||||
| 462 | + | func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) { | |||
| 463 | + | app := &App{ | |||
| 464 | + | tabs: entity.NewTabList(), | |||
| 465 | + | tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }), | |||
| 466 | + | browserWindows: make(map[string]*browserWindow), | |||
| 467 | + | } | |||
| 468 | + | app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult { | |||
| 469 | + | fn() | |||
| 470 | + | return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted} | |||
| 471 | + | } | |||
| 472 | + | app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) { | |||
| 473 | + | return &browserWindow{id: "window-1", tabs: entity.NewTabList()}, nil | |||
| 474 | + | } | |||
| 475 | + | ||||
| 476 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |||
| 477 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |||
| 478 | + | } | |||
| 479 | + | if got := len(app.browserWindows); got != 1 { | |||
| 480 | + | t.Fatalf("browserWindows length = %d, want 1", got) | |||
| 481 | + | } | |||
| 482 | + | if app.lastFocusedWindowID != "window-1" { | |||
| 483 | + | t.Fatalf("lastFocusedWindowID = %q, want window-1", app.lastFocusedWindowID) | |||
| 484 | + | } | |||
| 485 | + | if got := app.tabs.Count(); got != 0 { | |||
| 486 | + | t.Fatalf("tabs count = %d, want 0; first shell should wait for createInitialTab", got) | |||
| 487 | + | } | |||
| 488 | + | } | |||
| 489 | + | ||||
| 462 | func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) { | 490 | func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) { | ||
| 463 | app := &App{browserWindows: make(map[string]*browserWindow)} | 491 | app := &App{browserWindows: make(map[string]*browserWindow)} | ||
| 464 | app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult { | 492 | app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult { | ||