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