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 // OnNativeHostAbort returns true when fallback adopted the WebView. A false
146 // result leaves destruction responsibility with the native host.
147 OnNativeHostAbort func(context.Context, port.WebView) bool
148}
149
150// GetBehavior returns the appropriate behavior based on popup type and config.
151func GetBehavior(popupType PopupType, cfg *entity.BrowsingContextConfig) entity.PopupBehavior {
152 if cfg == nil {
153 return entity.PopupBehaviorSplit // Default
154 }
155
156 switch popupType {
157 case PopupTypeTab:
158 // Tab-like popups (_blank) use blank_target_behavior
159 switch cfg.BlankTargetBehavior {
160 case "split":
161 return entity.PopupBehaviorSplit
162 case "stacked":
163 return entity.PopupBehaviorStacked
164 case "tabbed":
165 return entity.PopupBehaviorTabbed
166 default:
167 return entity.PopupBehaviorStacked // Default for _blank
168 }
169 case PopupTypePopup:
170 // JS popups use the main behavior setting
171 return cfg.Behavior
172 default:
173 return cfg.Behavior
174 }
175}
176
177// SetPopupConfig configures popup handling.
178func (c *Coordinator) SetPopupConfig(
179 factory port.WebViewFactory,
180 popupConfig *entity.BrowsingContextConfig,
181 generateID func() string,
182) {
183 c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
184}
185
186func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
187 c.ensurePopupManager().updatePopupConfig(popupConfig)
188}
189
190func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
191 c.ensurePopupManager().setWindowIDResolver(fn)
192}
193
194func (c *Coordinator) SetPopupSourceHostResolver(fn func(entity.PaneID) dto.SourceHostKind) {
195 c.ensurePopupManager().setSourceHostResolver(fn)
196}
197
198func (c *Coordinator) SetOnOpenBrowserWindow(fn func(context.Context, BrowserWindowInput) (BrowserWindowResult, error)) {
199 c.ensurePopupManager().setOnOpenBrowserWindow(fn)
200}
201
202func (c *Coordinator) SetOnStagePopup(fn func(context.Context, StagePopupInput) (PopupStagingHost, error)) {
203 c.ensurePopupManager().setOnStagePopup(fn)
204}
205
206func (c *Coordinator) ClearPopupNamedContextsForWindow(windowID string) {
207 pm := c.ensurePopupManager()
208 pm.cancelDeferredPopupsForWindow(windowID)
209 pm.clearReusableNamedPopupsForWindow(windowID)
210}
211
212// ShutdownPopups releases unresolved staged popups before the browser engine exits.
213func (c *Coordinator) ShutdownPopups() {
214 c.ensurePopupManager().cancelAllDeferredPopups()
215}
216
217// SetOnInsertPopup sets the callback to insert popups into the workspace.
218func (c *Coordinator) SetOnInsertPopup(fn func(ctx context.Context, input InsertPopupInput) error) {
219 c.ensurePopupManager().setOnInsertPopup(fn)
220}
221
222// SetOnClosePane sets the callback to close a pane when its popup closes.
223func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.PaneID) error) {
224 c.ensurePopupManager().setOnClosePane(fn)
225}
226
227func (c *Coordinator) SetOnOpenNativePopup(fn func(ctx context.Context, input NativePopupInput) error) {
228 c.ensurePopupManager().setOnOpenNativePopup(fn)
229}
230
231// buildPopupCreateHandler returns the OnCreate callback for a WebView.
232// Returns nil if popup handling is not configured.
233func (c *Coordinator) buildPopupCreateHandler(
234 ctx context.Context, paneID entity.PaneID, wv port.WebView,
235) func(port.PopupRequest) port.WebView {
236 if wv == nil {
237 return nil
238 }
239
240 log := logging.FromContext(ctx)
241 log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
242
243 return func(req port.PopupRequest) port.WebView {
244 return c.handlePopupCreate(ctx, paneID, wv, req)
245 }
246}
247
248// handlePopupCreate handles a popup request from the current WebView.
249// Returns a WebView if popup handling is allowed, nil to block.
250//
251// IMPORTANT: The WebView MUST be added to a GtkWindow hierarchy BEFORE this
252// signal handler returns for engines that require native popup lifecycles.
253// The WebView stays hidden until ready-to-show signal is received.
254func (c *Coordinator) handlePopupCreate(
255 ctx context.Context,
256 parentPaneID entity.PaneID,
257 parentWV port.WebView,
258 req port.PopupRequest,
259) port.WebView {
260 return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
261}
262
263// handleLinkMiddleClick handles middle-click / Ctrl+click on links.
264// Opens the link in a new pane using blank_target_behavior config.
265func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
266 return c.ensurePopupManager().handleLinkMiddleClick(ctx, c.popupHooks(), parentPaneID, uri)
267}