1package content
2
3import (
4 "context"
5 "fmt"
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 "github.com/bnema/dumber/internal/ui/component"
11 "github.com/bnema/dumber/internal/ui/input"
12 "github.com/bnema/dumber/internal/ui/layout"
13)
14
15// EnsureWebView acquires or reuses a WebView for the given pane.
16func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
17 log := logging.FromContext(ctx)
18
19 if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
20 log.Debug().
21 Str("pane_id", string(paneID)).
22 Uint64("webview_id", uint64(wv.ID())).
23 Msg("webview reused for pane")
24 return wv, nil
25 }
26
27 if c.pool == nil {
28 return nil, fmt.Errorf("webview pool not configured")
29 }
30
31 wv, err := c.pool.Acquire(ctx)
32 if err != nil {
33 return nil, err
34 }
35
36 // setWebViewLocked atomically resets presentation state for the acquired
37 // WebView, including a pooled instance previously revealed in another pane.
38 c.setWebViewLocked(paneID, wv)
39 c.setupWebViewCallbacks(ctx, paneID, wv)
40
41 log.Debug().
42 Str("pane_id", string(paneID)).
43 Uint64("webview_id", uint64(wv.ID())).
44 Msg("webview acquired for pane")
45 return wv, nil
46}
47
48// ReleaseWebView returns the WebView for a pane to the pool.
49func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) {
50 log := logging.FromContext(ctx)
51 c.ensurePopupManager().cancelDeferredPopupsForParent(paneID)
52 c.clearExternalPrompt(paneID)
53
54 // deleteWebViewLocked clears reveal state before returning, even when a
55 // previous cleanup already removed this pane's mapping.
56 wv := c.deleteWebViewLocked(paneID)
57 if wv == nil {
58 return
59 }
60 c.ensurePopupManager().clearReusableNamedPopupByPaneID(paneID)
61 c.ensurePopupManager().clearReusableNamedPopupByWebViewID(wv.ID())
62 c.clearPendingAppearance(paneID)
63
64 // CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing),
65 // we must release the inhibition before destroying the webview.
66 // Otherwise the D-Bus inhibit request stays active forever.
67 if c.idleInhibitor != nil {
68 if wv.IsFullscreen() {
69 log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)")
70 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
71 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)")
72 }
73 }
74 if wv.IsPlayingAudio() {
75 log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)")
76 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
77 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)")
78 }
79 }
80 }
81
82 // Clean up title tracking
83 c.titleMu.Lock()
84 delete(c.paneTitles, paneID)
85 c.titleMu.Unlock()
86
87 // Clean up navigation origin tracking
88 c.navOriginMu.Lock()
89 delete(c.navOrigins, paneID)
90 c.navOriginMu.Unlock()
91
92 if c.pool != nil {
93 c.pool.Release(wv)
94 } else {
95 wv.Destroy()
96 }
97
98 log.Debug().Str("pane_id", string(paneID)).Msg("webview released")
99}
100
101// AttachToWorkspace ensures each pane in the workspace has a WebView widget attached.
102func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) {
103 log := logging.FromContext(ctx)
104
105 if ws == nil || wsView == nil || c.widgetFactory == nil {
106 return
107 }
108
109 for _, pane := range ws.AllPanes() {
110 if pane == nil {
111 continue
112 }
113
114 wv, err := c.EnsureWebView(ctx, pane.ID)
115 if err != nil {
116 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane")
117 continue
118 }
119
120 // Load the pane's URI if set and different from current
121 if pane.URI != "" && pane.URI != wv.URI() {
122 if err := wv.LoadURI(ctx, pane.URI); err != nil {
123 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI")
124 }
125 }
126
127 widget := c.WrapWidget(ctx, wv)
128 if widget == nil {
129 continue
130 }
131
132 log.Debug().
133 Str("pane_id", string(pane.ID)).
134 Uint64("webview_id", uint64(wv.ID())).
135 Int("wrapped_widget_alloc_width", widget.GetAllocatedWidth()).
136 Int("wrapped_widget_alloc_height", widget.GetAllocatedHeight()).
137 Msg("wrapped webview widget prepared for pane")
138
139 if err := wsView.AttachWebViewWidget(pane.ID, widget, c.webViewRevealed(pane.ID, wv)); err != nil {
140 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
141 continue
142 }
143 }
144}
145
146// WrapWidget converts a WebView to a layout.Widget for embedding.
147// It also attaches gesture handlers for mouse button navigation.
148func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
149 log := logging.FromContext(ctx)
150
151 if wv == nil || c.widgetFactory == nil {
152 log.Debug().Msg("cannot wrap nil webview or factory")
153 return nil
154 }
155
156 // Use NativeWidgetProvider interface for GTK embedding (engine-agnostic)
157 nwp, ok := wv.(port.NativeWidgetProvider)
158 if !ok {
159 log.Debug().Msg("webview does not support widget embedding")
160 return nil
161 }
162
163 ptr := nwp.NativeWidget()
164 if ptr == 0 {
165 return nil
166 }
167
168 widget := c.widgetFactory.WrapNativeWidget(ptr)
169
170 // Attach gesture handler for mouse button 8/9 navigation
171 if widget != nil {
172 gestureHandler := input.NewGestureHandler(ctx)
173 // Pass WebView directly to preserve user gesture context (like Epiphany)
174 if nav, ok := wv.(input.DirectNavigator); ok {
175 gestureHandler.SetNavigator(nav)
176 }
177 // Keep callback as fallback
178 if c.gestureActionHandler != nil {
179 gestureHandler.SetOnAction(c.gestureActionHandler)
180 }
181 gestureHandler.AttachTo(widget.GtkWidget())
182 log.Debug().Msg("gesture handler attached to webview with direct navigator")
183 }
184
185 return widget
186}
187
188// ActiveWebView returns the WebView for the active pane.
189func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
190 log := logging.FromContext(ctx)
191
192 if paneID, ok := c.activePaneOverrideID(); ok {
193 wv := c.getWebViewLocked(paneID)
194 if wv != nil {
195 return wv
196 }
197 log.Debug().Str("pane_id", string(paneID)).Msg("active pane override has no webview, falling back")
198 }
199 if c.getActiveWS == nil {
200 log.Debug().Msg("no active workspace resolver")
201 return nil
202 }
203
204 ws, _ := c.getActiveWS()
205 if ws == nil {
206 log.Debug().Msg("no active workspace")
207 return nil
208 }
209
210 pane := ws.ActivePane()
211 if pane == nil || pane.Pane == nil {
212 log.Debug().Msg("no active pane")
213 return nil
214 }
215
216 return c.getWebViewLocked(pane.Pane.ID)
217}
218
219// GetWebView returns the WebView for a specific pane.
220func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
221 return c.getWebViewLocked(paneID)
222}
223
224// RegisterPopupWebView registers a popup WebView that was created externally.
225// This is used when popup tabs are created and the WebView needs to be tracked.
226func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
227 if wv != nil && paneID != "" {
228 c.setWebViewLocked(paneID, wv)
229 }
230}
231
232// SyncWebViewViewport requests an explicit viewport resync when the engine
233// supports it. This is useful after widget attachment, reparenting, visibility
234// changes, or layout promotion paths that may not emit a size change.
235func (c *Coordinator) SyncWebViewViewport(ctx context.Context, paneID entity.PaneID, reason string) {
236 if c == nil || paneID == "" {
237 return
238 }
239 wv := c.getWebViewLocked(paneID)
240 if wv == nil {
241 return
242 }
243 syncer, ok := wv.(port.ViewportSyncCapable)
244 if !ok {
245 return
246 }
247 syncer.SyncViewport(ctx, reason)
248}