1package ui
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/application/usecase"
8 "github.com/bnema/dumber/internal/domain/entity"
9 domainurl "github.com/bnema/dumber/internal/domain/url"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/window"
12 "github.com/bnema/puregotk/v4/gtk"
13)
14
15func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
16 log := logging.FromContext(ctx)
17 log.Debug().
18 Str("url_host", logging.SafeURLHost(initialURL)).
19 Bool("using_factory", a.browserWindowFactory != nil).
20 Int("window_count_before", len(a.browserWindows)).
21 Msg("ui: create browser window started")
22
23 if a.browserWindowFactory != nil {
24 created, err := a.browserWindowFactory(ctx, initialURL)
25 if err != nil {
26 log.Warn().Err(err).
27 Str("url_host", logging.SafeURLHost(initialURL)).
28 Msg("ui: browser window factory failed")
29 return nil, err
30 }
31 created.ensureTabs()
32 a.wireBrowserWindowActivationTracking(created)
33 created.initChrome(ctx, a)
34 return created, nil
35 }
36
37 runtimeCfg := a.runtimeConfigSnapshot().UI
38 mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
39 if err != nil {
40 log.Warn().Err(err).
41 Str("url_host", logging.SafeURLHost(initialURL)).
42 Msg("ui: GTK browser window shell creation failed")
43 return nil, err
44 }
45 browserWindow := &browserWindow{
46 id: a.generateWindowID(),
47 initialURL: initialURL,
48 tabs: entity.NewTabList(),
49 mainWindow: mainWindow,
50 }
51 a.wireBrowserWindowActivationTracking(browserWindow)
52 if a.mainWindow == nil {
53 a.mainWindow = mainWindow
54 }
55
56 closeRequestCb := func(_ gtk.Window) bool {
57 log.Info().Msg("browser window close requested")
58 a.removeBrowserWindow(browserWindow.id)
59 return false
60 }
61 mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
62
63 a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
64 a.wireBrowserWindowPermissionIndicator(browserWindow)
65 if a.kbDispatcher != nil {
66 a.initBrowserWindowInput(ctx, browserWindow)
67 }
68 if a.tabCoord != nil {
69 a.wireBrowserWindowTabBar(ctx, browserWindow)
70 }
71 browserWindow.initChrome(ctx, a)
72
73 // Apply GTK CSS styling from theme manager.
74 if a.deps == nil || a.deps.Theme == nil {
75 return browserWindow, nil
76 }
77 if display := mainWindow.Window().GetDisplay(); display != nil {
78 a.deps.Theme.ApplyToDisplay(ctx, display)
79 }
80 log.Debug().
81 Str("window_id", browserWindow.id).
82 Str("url_host", logging.SafeURLHost(initialURL)).
83 Msg("ui: create browser window completed")
84 return browserWindow, nil
85}
86
87func (a *App) openInitialBrowserWindowShell(ctx context.Context, initialURL string) error {
88 log := logging.FromContext(ctx)
89 created, err := a.createBrowserWindow(ctx, initialURL)
90 if err != nil {
91 log.Warn().Err(err).
92 Str("url_host", logging.SafeURLHost(initialURL)).
93 Int("window_count_after", len(a.browserWindows)).
94 Msg("ui: initial browser window shell creation failed")
95 return err
96 }
97 a.registerBrowserWindow(created)
98 log.Debug().
99 Str("window_id", created.id).
100 Str("url_host", logging.SafeURLHost(initialURL)).
101 Msg("ui: initial browser window shell registered")
102 a.activateBrowserWindow(created)
103 return nil
104}
105
106// OpenInstance opens a dedicated window for a logical instance, or focuses the
107// existing window and opens the requested URL there. All instances owned by an
108// App share the same engine and CEF request context.
109func (a *App) OpenInstance(ctx context.Context, name, url string) error {
110 if name == "" {
111 return fmt.Errorf("instance name must not be empty")
112 }
113 url = domainurl.Normalize(url)
114 return a.dispatchWindowURLWork(ctx, "ui.open_instance", url, func(ctx context.Context) error {
115 if a.instanceWindows == nil {
116 a.instanceWindows = make(map[string]string)
117 }
118 if windowID := a.instanceWindows[name]; windowID != "" {
119 if existing := a.browserWindows[windowID]; existing != nil {
120 a.activateBrowserWindow(existing)
121 return a.openExternalURLInWindowOnMainThread(ctx, url, existing)
122 }
123 delete(a.instanceWindows, name)
124 }
125
126 before := make(map[string]struct{}, len(a.browserWindows))
127 for id := range a.browserWindows {
128 before[id] = struct{}{}
129 }
130 if err := a.openFreshWindow(ctx, url); err != nil {
131 return err
132 }
133 for id := range a.browserWindows {
134 if _, existed := before[id]; !existed {
135 a.instanceWindows[name] = id
136 return nil
137 }
138 }
139 return fmt.Errorf("instance %q did not create a browser window", name)
140 })
141}
142
143func (a *App) openExternalURLInWindowOnMainThread(ctx context.Context, url string, target *browserWindow) error {
144 if target == nil {
145 return fmt.Errorf("target browser window is nil")
146 }
147 a.activateBrowserWindow(target)
148 return a.openExternalURLInFocusedWindow(ctx, url, entity.ExternalLinksConfig{
149 Behavior: entity.ExternalLinkBehaviorTabbed,
150 })
151}
152
153func (a *App) openFreshWindow(ctx context.Context, url string) error {
154 log := logging.FromContext(ctx)
155 log.Debug().
156 Str("url_host", logging.SafeURLHost(url)).
157 Int("window_count_before", len(a.browserWindows)).
158 Bool("has_tab_coord", a.tabCoord != nil).
159 Bool("has_tabs_uc", a.tabsUC != nil).
160 Msg("ui: open fresh window started")
161
162 created, err := a.createBrowserWindow(ctx, url)
163 if err != nil {
164 log.Warn().Err(err).
165 Str("url_host", logging.SafeURLHost(url)).
166 Int("window_count_after", len(a.browserWindows)).
167 Msg("ui: open fresh window could not create browser window")
168 return err
169 }
170 log.Debug().
171 Str("window_id", created.id).
172 Str("url_host", logging.SafeURLHost(url)).
173 Msg("ui: open fresh window created browser window shell")
174 a.registerBrowserWindow(created)
175 log.Debug().
176 Str("window_id", created.id).
177 Int("window_count_after_register", len(a.browserWindows)).
178 Msg("ui: open fresh window registered browser window")
179
180 var openErr error
181 path := "tabs_uc"
182 if a.tabCoord != nil {
183 path = "tab_coord"
184 log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
185 openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
186 } else if a.tabsUC == nil {
187 log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
188 a.activateBrowserWindow(created)
189 return nil
190 } else {
191 log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
192 openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
193 }
194 if openErr != nil {
195 log.Warn().Err(openErr).
196 Str("window_id", created.id).
197 Str("path", path).
198 Int("window_count_after", len(a.browserWindows)).
199 Msg("ui: open fresh window initial tab creation failed")
200 return openErr
201 }
202 log.Debug().
203 Str("window_id", created.id).
204 Str("path", path).
205 Msg("ui: activating fresh browser window after tab creation")
206 a.activateBrowserWindow(created)
207 return nil
208}
209
210func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
211 log := logging.FromContext(ctx)
212 previousTarget := a.tabCoord.CurrentTarget()
213 target := a.ensureTabTargetForBrowserWindow(created)
214 a.tabCoord.SetCurrentTarget(target)
215 defer func() {
216 a.tabCoord.SetCurrentTarget(previousTarget)
217 }()
218
219 log.Debug().
220 Str("window_id", created.id).
221 Str("url_host", logging.SafeURLHost(url)).
222 Msg("ui: tab coordinator create started for fresh window")
223 createdTab, createErr := a.tabCoord.Create(ctx, target, url)
224 if createErr != nil {
225 log.Warn().Err(createErr).
226 Str("window_id", created.id).
227 Msg("ui: tab coordinator create failed for fresh window; rolling back window")
228 a.removeBrowserWindow(created.id)
229 if created.mainWindow != nil {
230 created.mainWindow.Destroy()
231 }
232 return createErr
233 }
234 log.Debug().
235 Str("window_id", created.id).
236 Str("tab_id", string(createdTab.ID)).
237 Msg("ui: tab coordinator create completed for fresh window")
238 if a.workspaceViews[createdTab.ID] == nil {
239 log.Warn().
240 Str("window_id", created.id).
241 Str("tab_id", string(createdTab.ID)).
242 Msg("ui: fresh window tab has no workspace view; rolling back window")
243 if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
244 createdTabs.Remove(createdTab.ID)
245 }
246 if created.mainWindow != nil {
247 if tabBar := created.mainWindow.TabBar(); tabBar != nil {
248 tabBar.RemoveTab(createdTab.ID)
249 if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
250 tabBar.SetActive(activeTab.ID)
251 }
252 }
253 }
254 a.removeBrowserWindow(created.id)
255 if created.mainWindow != nil {
256 created.mainWindow.Destroy()
257 }
258 return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
259 }
260 a.setBrowserWindowForTab(createdTab.ID, created)
261 // Tab was already created in the per-window TabList via tabCoord.Create;
262 // active state is managed by TabList.SetActive.
263 if created.mainWindow != nil {
264 created.mainWindow.Show()
265 }
266 return nil
267}
268
269func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
270 log := logging.FromContext(ctx)
271 log.Debug().
272 Str("window_id", created.id).
273 Str("url_host", logging.SafeURLHost(url)).
274 Msg("ui: tabs use case create started for fresh window")
275 output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
276 TabList: a.tabs,
277 InitialURL: url,
278 })
279 if err != nil {
280 log.Warn().Err(err).
281 Str("window_id", created.id).
282 Msg("ui: tabs use case create failed for fresh window; rolling back window")
283 a.removeBrowserWindow(created.id)
284 if created.mainWindow != nil {
285 created.mainWindow.Destroy()
286 }
287 return err
288 }
289 log.Debug().
290 Str("window_id", created.id).
291 Str("tab_id", string(output.Tab.ID)).
292 Msg("ui: tabs use case create completed for fresh window")
293 a.setBrowserWindowForTab(output.Tab.ID, created)
294 // Mirror the tabsUC-created tab into the new browser window's tab list.
295 // This path uses the tabs usecase directly instead of tabCoord.Create,
296 // so the per-window TabList must be populated explicitly.
297 a.ensureTabListForBrowserWindow(created).Add(output.Tab)
298 if a.widgetFactory != nil || a.workspaceViewCreateOverride != nil {
299 a.createWorkspaceView(ctx, output.Tab)
300 a.switchWorkspaceView(ctx, output.Tab.ID)
301 }
302 if a.workspaceViews[output.Tab.ID] == nil {
303 log.Warn().
304 Str("window_id", created.id).
305 Str("tab_id", string(output.Tab.ID)).
306 Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
307 if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
308 createdTabs.Remove(output.Tab.ID)
309 }
310 if a.tabs != nil {
311 a.tabs.Remove(output.Tab.ID)
312 }
313 if created.mainWindow != nil {
314 if tabBar := created.mainWindow.TabBar(); tabBar != nil {
315 tabBar.RemoveTab(output.Tab.ID)
316 if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
317 tabBar.SetActive(activeTab.ID)
318 }
319 }
320 }
321 a.removeBrowserWindow(created.id)
322 if created.mainWindow != nil {
323 created.mainWindow.Destroy()
324 }
325 return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
326 }
327 if created.mainWindow != nil {
328 created.mainWindow.Show()
329 }
330 return nil
331}