1package content
2
3import (
4 "context"
5 "time"
6
7 "github.com/bnema/dumber/internal/application/dto"
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/logging"
11)
12
13// PopupType indicates whether the popup was triggered by a link or JavaScript.
14type PopupType int
15
16const (
17 // PopupTypeTab represents a popup from target="_blank" links.
18 // These are typically user-initiated and open like new tabs.
19 PopupTypeTab PopupType = iota
20 // PopupTypePopup represents a popup from window.open() JavaScript calls.
21 // These may have different placement behavior than tab-like popups.
22 PopupTypePopup
23)
24
25// String returns a human-readable name for the popup type.
26func (t PopupType) String() string {
27 switch t {
28 case PopupTypeTab:
29 return "tab"
30 case PopupTypePopup:
31 return "popup"
32 default:
33 return "unknown"
34 }
35}
36
37// DetectPopupType determines if this is a tab-like or JS popup based on frame name.
38func DetectPopupType(frameName string) PopupType {
39 // _blank is the standard indicator for "open in new tab/window"
40 if frameName == "_blank" {
41 return PopupTypeTab
42 }
43 // Empty or custom frame names indicate JS window.open()
44 return PopupTypePopup
45}
46
47// PendingPopup tracks a popup WebView that has been created but not yet
48// inserted into the workspace. This is used during the three-phase popup
49// lifecycle: create → ready-to-show → (insert into workspace).
50type PendingPopup struct {
51 // PaneID is the popup pane created during the create phase.
52 PaneID entity.PaneID
53
54 // WebView is the related WebView created in the "create" phase.
55 // It shares cookies/session with the parent for OAuth support.
56 WebView port.WebView
57
58 // ParentPaneID is the pane that spawned this popup.
59 ParentPaneID entity.PaneID
60
61 // ParentWebViewID is the WebView ID of the parent (for lookup).
62 ParentWebViewID port.WebViewID
63
64 // TargetURI is the initial URL the popup will load.
65 TargetURI string
66
67 // FrameName is the target frame name from the navigation action.
68 // "_blank" indicates a tab-like popup, other values indicate JS popups.
69 FrameName string
70
71 // IsUserGesture indicates if the popup was triggered by user action
72 // (click) vs script-initiated (e.g., popup after timeout).
73 IsUserGesture bool
74
75 // PopupType categorizes this as a tab-like or JS popup.
76 PopupType PopupType
77
78 // CreatedAt is when the popup was created (for timeout handling).
79 CreatedAt time.Time
80}
81
82// InsertPopupInput contains the data needed to insert a popup into the workspace.
83type InsertPopupInput struct {
84 // ParentPaneID is the pane that spawned this popup.
85 ParentPaneID entity.PaneID
86
87 // PopupPane is the pre-created pane entity for the popup.
88 // It should have IsRelated=true and ParentPaneID set.
89 PopupPane *entity.Pane
90
91 // WebView is the related WebView for the popup.
92 WebView port.WebView
93
94 // Behavior determines how the popup is inserted (split/stacked/tabbed).
95 Behavior entity.PopupBehavior
96
97 // Placement specifies direction for split behavior (right/left/top/bottom).
98 Placement string
99
100 // PopupType indicates if this is a tab-like or JS popup.
101 PopupType PopupType
102
103 // TargetURI is the URL to load in the popup.
104 TargetURI string
105}
106
107// NativePopupInput contains the data needed to host a native-required popup in
108// a dedicated top-level GTK window instead of the workspace.
109// BrowserWindowInput contains an engine-created related WebView to adopt into
110// a complete Dumber browser window. A successful callback transfers ownership.
111type BrowserWindowInput struct {
112 ParentPaneID entity.PaneID
113 ParentWebViewID port.WebViewID
114 PopupPane *entity.Pane
115 PopupWebView port.WebView
116 TargetURI string
117 Request port.PopupRequest
118 Ready bool
119}
120
121type BrowserWindowResult struct {
122 WindowID string
123}
124
125// PopupStagingHost temporarily owns a popup widget while late engine metadata
126// is unavailable. Detach transfers the widget without destroying it.
127type PopupStagingHost interface {
128 Detach() error
129 Destroy()
130}
131
132type StagePopupInput struct {
133 PopupWebView port.WebView
134}
135
136type NativePopupInput struct {
137 ParentPaneID entity.PaneID
138 ParentWebViewID port.WebViewID
139 ParentURIAtOpen string
140 PopupWebView port.WebView
141 TargetURI string
142 Request port.PopupRequest
143 ObserveOAuthAutoClose bool
144 AllowBrowserWindowFallback bool
145 // AlreadyReadyToShow is true when the popup's ready-to-show lifecycle
146 // event fired before the native host was attached (deferred feature
147 // resolution). The host must reveal it immediately instead of waiting.
148 AlreadyReadyToShow bool
149 // OnNativeHostAbort returns true when fallback adopted the WebView. A false
150 // result leaves destruction responsibility with the native host.
151 OnNativeHostAbort func(context.Context, port.WebView) bool
152}
153
154// GetBehavior returns the appropriate behavior based on popup type and config.
155func GetBehavior(popupType PopupType, cfg *entity.BrowsingContextConfig) entity.PopupBehavior {
156 if cfg == nil {
157 return entity.PopupBehaviorSplit // Default
158 }
159
160 switch popupType {
161 case PopupTypeTab:
162 // Tab-like popups (_blank) use blank_target_behavior
163 switch cfg.BlankTargetBehavior {
164 case "split":
165 return entity.PopupBehaviorSplit
166 case "stacked":
167 return entity.PopupBehaviorStacked
168 case "tabbed":
169 return entity.PopupBehaviorTabbed
170 default:
171 return entity.PopupBehaviorStacked // Default for _blank
172 }
173 case PopupTypePopup:
174 // JS popups use the main behavior setting
175 return cfg.Behavior
176 default:
177 return cfg.Behavior
178 }
179}
180
181// SetPopupConfig configures popup handling.
182func (c *Coordinator) SetPopupConfig(
183 factory port.WebViewFactory,
184 popupConfig *entity.BrowsingContextConfig,
185 generateID func() string,
186) {
187 c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
188}
189
190func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
191 c.ensurePopupManager().updatePopupConfig(popupConfig)
192}
193
194func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
195 c.ensurePopupManager().setWindowIDResolver(fn)
196}
197
198func (c *Coordinator) SetPopupSourceHostResolver(fn func(entity.PaneID) dto.SourceHostKind) {
199 c.ensurePopupManager().setSourceHostResolver(fn)
200}
201
202func (c *Coordinator) SetOnOpenBrowserWindow(fn func(context.Context, BrowserWindowInput) (BrowserWindowResult, error)) {
203 c.ensurePopupManager().setOnOpenBrowserWindow(fn)
204}
205
206func (c *Coordinator) SetOnStagePopup(fn func(context.Context, StagePopupInput) (PopupStagingHost, error)) {
207 c.ensurePopupManager().setOnStagePopup(fn)
208}
209
210func (c *Coordinator) ClearPopupNamedContextsForWindow(windowID string) {
211 pm := c.ensurePopupManager()
212 pm.cancelDeferredPopupsForWindow(windowID)
213 pm.clearReusableNamedPopupsForWindow(windowID)
214}
215
216// ShutdownPopups releases unresolved staged popups before the browser engine exits.
217func (c *Coordinator) ShutdownPopups() {
218 c.ensurePopupManager().cancelAllDeferredPopups()
219}
220
221// SetOnInsertPopup sets the callback to insert popups into the workspace.
222func (c *Coordinator) SetOnInsertPopup(fn func(ctx context.Context, input InsertPopupInput) error) {
223 c.ensurePopupManager().setOnInsertPopup(fn)
224}
225
226// SetOnClosePane sets the callback to close a pane when its popup closes.
227func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.PaneID) error) {
228 c.ensurePopupManager().setOnClosePane(fn)
229}
230
231func (c *Coordinator) SetOnOpenNativePopup(fn func(ctx context.Context, input NativePopupInput) error) {
232 c.ensurePopupManager().setOnOpenNativePopup(fn)
233}
234
235// buildPopupCreateHandler returns the OnCreate callback for a WebView.
236// Returns nil if popup handling is not configured.
237func (c *Coordinator) buildPopupCreateHandler(
238 ctx context.Context, paneID entity.PaneID, wv port.WebView,
239) func(port.PopupRequest) port.WebView {
240 if wv == nil {
241 return nil
242 }
243
244 log := logging.FromContext(ctx)
245 log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
246
247 return func(req port.PopupRequest) port.WebView {
248 return c.handlePopupCreate(ctx, paneID, wv, req)
249 }
250}
251
252// handlePopupCreate handles a popup request from the current WebView.
253// Returns a WebView if popup handling is allowed, nil to block.
254//
255// IMPORTANT: The WebView MUST be added to a GtkWindow hierarchy BEFORE this
256// signal handler returns for engines that require native popup lifecycles.
257// The WebView stays hidden until ready-to-show signal is received.
258func (c *Coordinator) handlePopupCreate(
259 ctx context.Context,
260 parentPaneID entity.PaneID,
261 parentWV port.WebView,
262 req port.PopupRequest,
263) port.WebView {
264 return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
265}
266
267// handleLinkMiddleClick handles middle-click / Ctrl+click on links.
268// Opens the link in a new pane using blank_target_behavior config.
269func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
270 return c.ensurePopupManager().handleLinkMiddleClick(ctx, c.popupHooks(), parentPaneID, uri)
271}