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 "github.com/bnema/puregotk/v4/gtk"
14)
15
16// EnsureWebView acquires or reuses a WebView for the given pane.
17func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
18 log := logging.FromContext(ctx)
19
20 if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
21 return wv, nil
22 }
23
24 if c.pool == nil {
25 return nil, fmt.Errorf("webview pool not configured")
26 }
27
28 // Mark tab_created on first webview (first tab)
29 if c.webViewCount() == 0 {
30 logging.Trace().Mark("tab_created")
31 }
32
33 wv, err := c.pool.Acquire(ctx)
34 if err != nil {
35 return nil, err
36 }
37 logging.Trace().Mark("webview_acquired")
38
39 c.setWebViewLocked(paneID, wv)
40 c.setupWebViewCallbacks(ctx, paneID, wv)
41
42 log.Debug().Str("pane_id", string(paneID)).Msg("webview acquired for pane")
43 return wv, nil
44}
45
46// ReleaseWebView returns the WebView for a pane to the pool.
47func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) {
48 log := logging.FromContext(ctx)
49
50 wv := c.deleteWebViewLocked(paneID)
51 if wv == nil {
52 return
53 }
54 c.clearPendingAppearance(paneID)
55
56 // CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing),
57 // we must release the inhibition before destroying the webview.
58 // Otherwise the D-Bus inhibit request stays active forever.
59 if c.idleInhibitor != nil {
60 if wv.IsFullscreen() {
61 log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)")
62 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
63 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)")
64 }
65 }
66 if wv.IsPlayingAudio() {
67 log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)")
68 if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
69 log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)")
70 }
71 }
72 }
73
74 // Clean up title tracking
75 c.titleMu.Lock()
76 delete(c.paneTitles, paneID)
77 c.titleMu.Unlock()
78
79 // Clean up navigation origin tracking
80 c.navOriginMu.Lock()
81 delete(c.navOrigins, paneID)
82 c.navOriginMu.Unlock()
83
84 if c.pool != nil {
85 c.pool.Release(wv)
86 } else {
87 wv.Destroy()
88 }
89
90 log.Debug().Str("pane_id", string(paneID)).Msg("webview released")
91}
92
93// AttachToWorkspace ensures each pane in the workspace has a WebView widget attached.
94func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) {
95 log := logging.FromContext(ctx)
96
97 if ws == nil || wsView == nil || c.widgetFactory == nil {
98 return
99 }
100
101 for _, pane := range ws.AllPanes() {
102 if pane == nil {
103 continue
104 }
105
106 wv, err := c.EnsureWebView(ctx, pane.ID)
107 if err != nil {
108 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane")
109 continue
110 }
111
112 // Load the pane's URI if set and different from current
113 if pane.URI != "" && pane.URI != wv.URI() {
114 if err := wv.LoadURI(ctx, pane.URI); err != nil {
115 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI")
116 }
117 }
118
119 widget := c.WrapWidget(ctx, wv)
120 if widget == nil {
121 continue
122 }
123
124 if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil {
125 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
126 }
127 logging.Trace().Mark("webview_attached")
128 }
129}
130
131// WrapWidget converts a WebView to a layout.Widget for embedding.
132// It also attaches gesture handlers for mouse button navigation.
133func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
134 log := logging.FromContext(ctx)
135
136 if wv == nil || c.widgetFactory == nil {
137 log.Debug().Msg("cannot wrap nil webview or factory")
138 return nil
139 }
140
141 // Use NativeWidgetProvider interface for GTK embedding (engine-agnostic)
142 nwp, ok := wv.(port.NativeWidgetProvider)
143 if !ok {
144 log.Debug().Msg("webview does not support widget embedding")
145 return nil
146 }
147
148 ptr := nwp.NativeWidget()
149 if ptr == 0 {
150 return nil
151 }
152
153 gtkWidget := >k.Widget{}
154 gtkWidget.Ptr = ptr
155 widget := c.widgetFactory.WrapWidget(gtkWidget)
156
157 // Attach gesture handler for mouse button 8/9 navigation
158 if widget != nil {
159 gestureHandler := input.NewGestureHandler(ctx)
160 // Pass WebView directly to preserve user gesture context (like Epiphany)
161 if nav, ok := wv.(input.DirectNavigator); ok {
162 gestureHandler.SetNavigator(nav)
163 }
164 // Keep callback as fallback
165 if c.gestureActionHandler != nil {
166 gestureHandler.SetOnAction(c.gestureActionHandler)
167 }
168 gestureHandler.AttachTo(widget.GtkWidget())
169 log.Debug().Msg("gesture handler attached to webview with direct navigator")
170 }
171
172 return widget
173}
174
175// ActiveWebView returns the WebView for the active pane.
176func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
177 log := logging.FromContext(ctx)
178
179 if paneID, ok := c.activePaneOverrideID(); ok {
180 wv := c.getWebViewLocked(paneID)
181 if wv != nil {
182 return wv
183 }
184 log.Debug().Str("pane_id", string(paneID)).Msg("active pane override has no webview, falling back")
185 }
186 if c.getActiveWS == nil {
187 log.Debug().Msg("no active workspace resolver")
188 return nil
189 }
190
191 ws, _ := c.getActiveWS()
192 if ws == nil {
193 log.Debug().Msg("no active workspace")
194 return nil
195 }
196
197 pane := ws.ActivePane()
198 if pane == nil || pane.Pane == nil {
199 log.Debug().Msg("no active pane")
200 return nil
201 }
202
203 return c.getWebViewLocked(pane.Pane.ID)
204}
205
206// GetWebView returns the WebView for a specific pane.
207func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
208 return c.getWebViewLocked(paneID)
209}
210
211// RegisterPopupWebView registers a popup WebView that was created externally.
212// This is used when popup tabs are created and the WebView needs to be tracked.
213func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
214 if wv != nil && paneID != "" {
215 c.setWebViewLocked(paneID, wv)
216 }
217}