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