1package coordinator
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/dumber/internal/ui/coordinator/content"
13)
14
15// OmniboxProvider provides access to omnibox operations.
16type OmniboxProvider interface {
17 ToggleOmnibox(ctx context.Context)
18 UpdateOmniboxZoom(factor float64)
19}
20
21// NavigationCoordinator handles URL navigation, history, and browser controls.
22type NavigationCoordinator struct {
23 contextProvider func() context.Context
24 navigateUC *usecase.NavigateUseCase
25 historyRecorder *usecase.HistoryRecorderUseCase
26 contentCoord *content.Coordinator
27 omniboxProvider OmniboxProvider
28}
29
30const faviconPreloadTimeout = 300 * time.Millisecond
31
32// NewNavigationCoordinator creates a new NavigationCoordinator.
33func NewNavigationCoordinator(
34 ctx context.Context,
35 navigateUC *usecase.NavigateUseCase,
36 contentCoord *content.Coordinator,
37) *NavigationCoordinator {
38 return NewNavigationCoordinatorWithHistoryRecorder(ctx, navigateUC, nil, contentCoord)
39}
40
41// NewNavigationCoordinatorWithHistoryRecorder creates a new NavigationCoordinator with history recording.
42func NewNavigationCoordinatorWithHistoryRecorder(
43 ctx context.Context,
44 navigateUC *usecase.NavigateUseCase,
45 historyRecorder *usecase.HistoryRecorderUseCase,
46 contentCoord *content.Coordinator,
47) *NavigationCoordinator {
48 log := logging.FromContext(ctx)
49 log.Debug().Msg("creating navigation coordinator")
50 callbackLogger := *log
51
52 return &NavigationCoordinator{
53 contextProvider: func() context.Context {
54 return logging.WithContext(context.Background(), callbackLogger)
55 },
56 navigateUC: navigateUC,
57 historyRecorder: historyRecorder,
58 contentCoord: contentCoord,
59 }
60}
61
62// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
63func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
64 c.omniboxProvider = provider
65}
66
67// requireWebView returns an error if wv is nil, preserving stable error text.
68func requireWebView(wv port.WebView) error {
69 if wv == nil {
70 return fmt.Errorf("no webview provided")
71 }
72 return nil
73}
74
75func (c *NavigationCoordinator) trackNavigationOrigin(ctx context.Context, paneID entity.PaneID, url string) {
76 if c.contentCoord == nil || paneID == "" {
77 return
78 }
79 c.contentCoord.SetNavigationOrigin(paneID, url)
80 go func() {
81 preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout)
82 defer cancelPreload()
83 c.contentCoord.PreloadCachedFavicon(preloadCtx, paneID, url)
84 }()
85}
86
87// NavigateWebView loads a URL in the specified pane using the provided WebView.
88// This is the explicit-target equivalent of Navigate: it uses NavigateUseCase if
89// available, falling back to direct LoadURI. Origin tracking via contentCoord is
90// performed when contentCoord and paneID are available.
91func (c *NavigationCoordinator) NavigateWebView(ctx context.Context, url string, paneID entity.PaneID, wv port.WebView) error {
92 log := logging.FromContext(ctx)
93
94 if err := requireWebView(wv); err != nil {
95 log.Warn().Str("url", url).Msg("NavigateWebView called with nil webview")
96 return err
97 }
98
99 paneIDStr := string(paneID)
100
101 if c.navigateUC != nil {
102 input := usecase.NavigateInput{
103 URL: url,
104 PaneID: paneIDStr,
105 WebView: wv,
106 }
107 output, err := c.navigateUC.Execute(ctx, input)
108 if err != nil {
109 log.Error().Err(err).Str("url", url).Str("pane_id", paneIDStr).Msg("navigation failed")
110 return err
111 }
112 c.trackNavigationOrigin(ctx, paneID, url)
113 log.Debug().
114 Str("url", url).
115 Str("pane_id", paneIDStr).
116 Float64("zoom", output.AppliedZoom).
117 Uint64("webview_id", uint64(wv.ID())).
118 Msg("navigation initiated via usecase")
119 return nil
120 }
121
122 log.Warn().Msg("navigateUC not available, using direct LoadURI")
123 if err := wv.LoadURI(ctx, url); err != nil {
124 log.Error().Err(err).Str("url", url).Msg("failed to navigate")
125 return err
126 }
127
128 c.trackNavigationOrigin(ctx, paneID, url)
129 log.Debug().Str("url", url).Uint64("webview_id", uint64(wv.ID())).Msg("navigated explicit webview (direct)")
130 return nil
131}
132
133// ReloadWebView reloads the page in the provided WebView, optionally bypassing cache.
134func (c *NavigationCoordinator) ReloadWebView(ctx context.Context, wv port.WebView, bypassCache bool) error {
135 log := logging.FromContext(ctx)
136
137 if err := requireWebView(wv); err != nil {
138 log.Debug().Msg("ReloadWebView called with nil webview")
139 return err
140 }
141
142 if c.navigateUC != nil {
143 return c.navigateUC.Reload(ctx, wv, bypassCache)
144 }
145
146 if bypassCache {
147 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("reloading explicit webview bypassing cache")
148 return wv.ReloadBypassCache(ctx)
149 }
150
151 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("reloading explicit webview")
152 return wv.Reload(ctx)
153}
154
155// StopWebView stops loading in the provided WebView.
156func (c *NavigationCoordinator) StopWebView(ctx context.Context, wv port.WebView) error {
157 log := logging.FromContext(ctx)
158
159 if err := requireWebView(wv); err != nil {
160 log.Debug().Msg("StopWebView called with nil webview")
161 return err
162 }
163
164 if c.navigateUC != nil {
165 return c.navigateUC.Stop(ctx, wv)
166 }
167
168 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("stopping explicit webview")
169 return wv.Stop(ctx)
170}
171
172// GoBackWebView navigates back in history for the provided WebView.
173// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
174func (c *NavigationCoordinator) GoBackWebView(ctx context.Context, wv port.WebView) error {
175 log := logging.FromContext(ctx)
176
177 if err := requireWebView(wv); err != nil {
178 log.Debug().Msg("GoBackWebView called with nil webview")
179 return err
180 }
181
182 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("going back in explicit webview")
183 return wv.GoBack(ctx)
184}
185
186// GoForwardWebView navigates forward in history for the provided WebView.
187// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
188func (c *NavigationCoordinator) GoForwardWebView(ctx context.Context, wv port.WebView) error {
189 log := logging.FromContext(ctx)
190
191 if err := requireWebView(wv); err != nil {
192 log.Debug().Msg("GoForwardWebView called with nil webview")
193 return err
194 }
195
196 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("going forward in explicit webview")
197 return wv.GoForward(ctx)
198}
199
200// OpenOmnibox toggles the omnibox visibility.
201func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
202 log := logging.FromContext(ctx)
203
204 if c.omniboxProvider == nil {
205 log.Error().Msg("omnibox provider not initialized")
206 return fmt.Errorf("omnibox provider not initialized")
207 }
208
209 log.Debug().Msg("toggling omnibox")
210 c.omniboxProvider.ToggleOmnibox(ctx)
211 return nil
212}
213
214// OpenDevToolsWebView opens the WebKit inspector for the provided WebView.
215func (c *NavigationCoordinator) OpenDevToolsWebView(ctx context.Context, wv port.WebView) error {
216 log := logging.FromContext(ctx)
217
218 if err := requireWebView(wv); err != nil {
219 log.Warn().Msg("OpenDevToolsWebView called with nil webview")
220 return err
221 }
222
223 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools")
224
225 if opener, ok := wv.(port.DevToolsOpener); ok {
226 opener.OpenDevTools()
227 return nil
228 }
229
230 return fmt.Errorf("webview does not support devtools")
231}
232
233// PrintWebView opens the print dialog for the provided WebView.
234func (c *NavigationCoordinator) PrintWebView(ctx context.Context, wv port.WebView) error {
235 log := logging.FromContext(ctx)
236
237 if err := requireWebView(wv); err != nil {
238 log.Warn().Msg("PrintWebView called with nil webview")
239 return err
240 }
241
242 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
243
244 if printer, ok := wv.(port.Printer); ok {
245 printer.PrintPage()
246 return nil
247 }
248
249 return fmt.Errorf("webview does not support printing")
250}
251
252// UpdateHistoryTitle updates the title of a history entry after page load.
253func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
254 if c.historyRecorder == nil {
255 return
256 }
257
258 c.historyRecorder.UpdateHistoryTitle(ctx, url, title)
259}
260
261// RecordHistory records a URL in history on page commit.
262func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity.PaneID, url string) {
263 if c.historyRecorder == nil {
264 return
265 }
266
267 c.historyRecorder.RecordHistory(ctx, string(paneID), url)
268}
269
270// ClearPaneHistory clears per-pane navigation history deduplication state.
271func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) {
272 if c.historyRecorder == nil {
273 return
274 }
275 c.historyRecorder.ClearPaneHistory(string(paneID))
276}
277
278// NotifyZoomChanged updates the omnibox zoom indicator.
279func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
280 if c.omniboxProvider != nil {
281 c.omniboxProvider.UpdateOmniboxZoom(factor)
282 }
283}