1package content
2
3import (
4 "context"
5 "strings"
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/adapter"
11 "github.com/bnema/dumber/internal/ui/layout"
12 "github.com/bnema/puregotk/v4/gdk"
13 "github.com/bnema/puregotk/v4/glib"
14)
15
16// GetTitle returns the current title for a pane.
17func (c *Coordinator) GetTitle(paneID entity.PaneID) string {
18 c.titleMu.RLock()
19 defer c.titleMu.RUnlock()
20 return c.paneTitles[paneID]
21}
22
23// onTitleChanged updates title tracking when a WebView's title changes.
24func (c *Coordinator) onTitleChanged(ctx context.Context, paneID entity.PaneID, title string) {
25 log := logging.FromContext(ctx)
26
27 // Update title map
28 c.titleMu.Lock()
29 c.paneTitles[paneID] = title
30 c.titleMu.Unlock()
31
32 // Update domain model and check if this is the active pane
33 isActivePaneTitle := false
34 if c.getActiveWS == nil {
35 return
36 }
37 ws, wsView := c.getActiveWS()
38 if ws != nil {
39 paneNode := ws.FindPane(paneID)
40 if paneNode != nil && paneNode.Pane != nil {
41 paneNode.Pane.Title = title
42 }
43 // Check if this pane is the active one
44 if ws.ActivePaneID == paneID {
45 isActivePaneTitle = true
46 }
47 }
48
49 // Update StackedView title bar if this pane is in a stack
50 if wsView != nil {
51 tr := wsView.TreeRenderer()
52 if tr != nil {
53 stackedView := tr.GetStackedViewForPane(string(paneID))
54 if stackedView != nil {
55 c.updateStackedPaneTitle(ctx, stackedView, paneID, title)
56 }
57 }
58 }
59
60 // Notify history persistence (get URL from WebView)
61 if c.onTitleUpdated != nil {
62 if wv := c.getWebViewLocked(paneID); wv != nil {
63 currentURI := wv.URI()
64 if currentURI != "" && title != "" {
65 c.onTitleUpdated(ctx, paneID, currentURI, title)
66 }
67 }
68 }
69
70 // Notify window title update if this is the active pane
71 if isActivePaneTitle && c.onWindowTitleChanged != nil {
72 c.onWindowTitleChanged(title)
73 }
74
75 log.Debug().
76 Str("pane_id", string(paneID)).
77 Str("title", title).
78 Msg("pane title updated")
79}
80
81// updateStackedPaneTitle updates the title of a pane in a StackedView.
82func (c *Coordinator) updateStackedPaneTitle(
83 ctx context.Context,
84 sv *layout.StackedView,
85 paneID entity.PaneID,
86 title string,
87) {
88 log := logging.FromContext(ctx)
89
90 // Find the pane's index directly in the StackedView
91 index := sv.FindPaneIndex(string(paneID))
92 if index < 0 {
93 log.Debug().
94 Str("pane_id", string(paneID)).
95 Msg("pane not found in StackedView for title update")
96 return
97 }
98
99 if err := sv.UpdateTitle(index, title); err != nil {
100 log.Warn().Err(err).Int("index", index).Msg("failed to update stacked pane title")
101 }
102}
103
104// syncStackedTitle updates the stacked title bar for a pane if it's in a stack.
105// Called from onLoadCommitted to keep titles in sync during navigation.
106func (c *Coordinator) syncStackedTitle(ctx context.Context, paneID entity.PaneID, title string) {
107 if c.getActiveWS == nil {
108 return
109 }
110 _, wsView := c.getActiveWS()
111 if wsView == nil {
112 return
113 }
114 tr := wsView.TreeRenderer()
115 if tr == nil {
116 return
117 }
118 if sv := tr.GetStackedViewForPane(string(paneID)); sv != nil {
119 c.updateStackedPaneTitle(ctx, sv, paneID, title)
120 }
121}
122
123// onFaviconChanged updates favicon tracking when a WebView's favicon changes.
124func (c *Coordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, emittingWV port.WebView, favicon *gdk.Texture) {
125 log := logging.FromContext(ctx)
126
127 // Verify this WebView is still bound to the expected pane
128 currentWV := c.getWebViewLocked(paneID)
129 if currentWV == nil || currentWV != emittingWV {
130 log.Debug().
131 Str("pane_id", string(paneID)).
132 Msg("ignoring favicon change from unbound webview")
133 return
134 }
135
136 uri := emittingWV.URI()
137
138 // Update favicon cache with domain key (handles cross-domain redirects)
139 if c.faviconAdapter != nil && favicon != nil && uri != "" {
140 c.navOriginMu.RLock()
141 originURL := c.navOrigins[paneID]
142 c.navOriginMu.RUnlock()
143 c.faviconAdapter.StoreFromWebKitWithOrigin(ctx, uri, originURL, favicon)
144 }
145
146 // Update StackedView favicon if this pane is in a stack
147 c.updateStackedFaviconForPane(ctx, paneID, favicon)
148
149 log.Debug().
150 Str("pane_id", string(paneID)).
151 Str("uri", uri).
152 Bool("has_favicon", favicon != nil).
153 Msg("pane favicon updated")
154}
155
156// updateStackedPaneFavicon updates the favicon of a pane in a StackedView.
157func (c *Coordinator) updateStackedPaneFavicon(
158 ctx context.Context,
159 sv *layout.StackedView,
160 paneID entity.PaneID,
161 favicon *gdk.Texture,
162) {
163 log := logging.FromContext(ctx)
164
165 // Find the pane's index directly in the StackedView
166 index := sv.FindPaneIndex(string(paneID))
167 if index < 0 {
168 log.Debug().
169 Str("pane_id", string(paneID)).
170 Msg("pane not found in StackedView for favicon update")
171 return
172 }
173
174 if err := sv.UpdateFaviconTexture(index, favicon); err != nil {
175 log.Warn().Err(err).Int("index", index).Msg("failed to update stacked pane favicon")
176 }
177}
178
179// updateStackedFaviconForPane updates the stacked title bar favicon for a pane.
180func (c *Coordinator) updateStackedFaviconForPane(ctx context.Context, paneID entity.PaneID, texture *gdk.Texture) {
181 if c.getActiveWS == nil {
182 return
183 }
184 _, wsView := c.getActiveWS()
185 if wsView == nil {
186 return
187 }
188 tr := wsView.TreeRenderer()
189 if tr == nil {
190 return
191 }
192 if sv := tr.GetStackedViewForPane(string(paneID)); sv != nil {
193 c.updateStackedPaneFavicon(ctx, sv, paneID, texture)
194 }
195}
196
197// resolveCommittedFavicon ensures the stacked title bar has a favicon after navigation commits.
198// First checks if WebKit already has a favicon for the page (common for subpath URLs).
199// Falls back to the full GetOrFetch pipeline (cache + WebKit DB + DuckDuckGo API).
200// This closes the gap where title bars relied solely on the notify::favicon signal.
201func (c *Coordinator) resolveCommittedFavicon(ctx context.Context, paneID entity.PaneID, wv port.WebView) {
202 if c.faviconAdapter == nil || wv == nil {
203 return
204 }
205
206 uri := wv.URI()
207 if uri == "" || strings.HasPrefix(uri, "about:") || strings.HasPrefix(uri, "dumb://") {
208 return
209 }
210
211 log := logging.FromContext(ctx)
212
213 // Check if we already have a texture cached for this domain
214 if texture := c.faviconAdapter.GetTextureByURL(uri); texture != nil {
215 // Already cached - just ensure the stacked view is updated
216 c.updateStackedFaviconForPane(ctx, paneID, texture)
217 return
218 }
219
220 // Check if the WebView already has a favicon for this page
221 if icon := wv.Favicon(); icon != nil {
222 if gdkIcon, ok := icon.(*gdk.Texture); ok {
223 log.Debug().Str("pane_id", string(paneID)).Str("uri", uri).Msg("using existing favicon for committed page")
224 // Store and update through the normal path
225 c.navOriginMu.RLock()
226 originURL := c.navOrigins[paneID]
227 c.navOriginMu.RUnlock()
228 c.faviconAdapter.StoreFromWebKitWithOrigin(ctx, uri, originURL, gdkIcon)
229 c.updateStackedFaviconForPane(ctx, paneID, gdkIcon)
230 return
231 }
232 }
233
234 // Fall back to GetOrFetch (checks service cache, engine DB, then DuckDuckGo API)
235 // Capture current generation to guard against stale callbacks
236 gen := wv.Generation()
237 capturedURI := uri
238 c.faviconAdapter.GetOrFetch(ctx, uri, func(texture *gdk.Texture) {
239 // Skip nil results — a nil means "couldn't resolve", not "no favicon".
240 // Without this guard, a late nil callback can overwrite a good favicon
241 // that was already set by an earlier onFaviconChanged signal.
242 if texture == nil {
243 return
244 }
245 // Verify WebView is still bound to pane and hasn't been reused
246 if wv.Generation() != gen {
247 return
248 }
249 currentWV := c.getWebViewLocked(paneID)
250 if currentWV != wv {
251 return
252 }
253 // Verify the WebView hasn't navigated away since we started the fetch
254 if currentWV.URI() != capturedURI {
255 return
256 }
257 c.updateStackedFaviconForPane(ctx, paneID, texture)
258 })
259}
260
261// FaviconAdapter returns the favicon adapter for external use (e.g., omnibox).
262func (c *Coordinator) FaviconAdapter() *adapter.FaviconAdapter {
263 return c.faviconAdapter
264}
265
266// SetNavigationOrigin records the original URL before navigation starts.
267// This allows caching favicons under both original and final domains
268// when cross-domain redirects occur (e.g., google.fr → google.com).
269func (c *Coordinator) SetNavigationOrigin(paneID entity.PaneID, uri string) {
270 c.navOriginMu.Lock()
271 c.navOrigins[paneID] = uri
272 c.navOriginMu.Unlock()
273}
274
275// PreloadCachedFavicon checks the favicon cache and updates the stacked pane
276// title bar immediately if a cached favicon exists for the URL.
277// This provides instant favicon display without waiting for WebKit.
278func (c *Coordinator) PreloadCachedFavicon(ctx context.Context, paneID entity.PaneID, uri string) {
279 if c.faviconAdapter == nil || uri == "" {
280 return
281 }
282 select {
283 case <-ctx.Done():
284 return
285 default:
286 }
287
288 // Check memory and disk cache (no external fetch)
289 texture := c.faviconAdapter.PreloadFromCache(uri)
290 select {
291 case <-ctx.Done():
292 return
293 default:
294 }
295
296 // Update stacked pane favicon on GTK main loop.
297 cb := glib.SourceFunc(func(_ uintptr) bool {
298 select {
299 case <-ctx.Done():
300 return false
301 default:
302 }
303 if c.getActiveWS == nil {
304 return false
305 }
306 // Only update if we found a cached texture.
307 // If nil, let resolveCommittedFavicon handle it after load commits.
308 if texture == nil {
309 return false
310 }
311 _, wsView := c.getActiveWS()
312 if wsView == nil {
313 return false
314 }
315 tr := wsView.TreeRenderer()
316 if tr == nil {
317 return false
318 }
319 stackedView := tr.GetStackedViewForPane(string(paneID))
320 if stackedView == nil {
321 return false
322 }
323 c.updateStackedPaneFavicon(ctx, stackedView, paneID, texture)
324 return false
325 })
326 glib.IdleAdd(&cb, 0)
327}