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