1package webkit
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// WebViewFactory creates WebView instances for the application.
13// It supports creating regular WebViews (optionally via pool) and
14// related WebViews that share session/cookies with a parent (for popups).
15type WebViewFactory struct {
16 wkCtx *WebKitContext
17 settings *SettingsManager
18 pool *WebViewPool
19 injector *ContentInjector
20 router *MessageRouter
21 filterApplier FilterApplier // Optional content filter applier
22
23 // Background color for WebViews (eliminates white flash)
24 bgR, bgG, bgB, bgA float32
25 bgMu sync.RWMutex
26}
27
28// NewWebViewFactory creates a new WebViewFactory.
29// The pool parameter is optional; if nil, WebViews are created directly.
30func NewWebViewFactory(
31 wkCtx *WebKitContext,
32 settings *SettingsManager,
33 pool *WebViewPool,
34 injector *ContentInjector,
35 router *MessageRouter,
36) *WebViewFactory {
37 return &WebViewFactory{
38 wkCtx: wkCtx,
39 settings: settings,
40 pool: pool,
41 injector: injector,
42 router: router,
43 }
44}
45
46// SetFilterApplier sets the content filter applier.
47// Filters will be applied to all newly created WebViews.
48func (f *WebViewFactory) SetFilterApplier(applier FilterApplier) {
49 f.filterApplier = applier
50 // Also propagate to the pool if present
51 if f.pool != nil {
52 f.pool.SetFilterApplier(applier)
53 }
54}
55
56// SetBackgroundColor sets the background color for newly created WebViews.
57// This color is shown before content is painted, eliminating white flash.
58func (f *WebViewFactory) SetBackgroundColor(r, g, b, a float32) {
59 f.bgMu.Lock()
60 f.bgR, f.bgG, f.bgB, f.bgA = r, g, b, a
61 f.bgMu.Unlock()
62 // Also propagate to the pool if present
63 if f.pool != nil {
64 f.pool.SetBackgroundColor(r, g, b, a)
65 }
66}
67
68// Create creates a new WebView instance.
69// If a pool is configured, it will try to acquire from the pool first.
70func (f *WebViewFactory) Create(ctx context.Context) (*WebView, error) {
71 log := logging.FromContext(ctx)
72
73 // Try pool first if available
74 if f.pool != nil {
75 wv, err := f.pool.Acquire(ctx)
76 if err == nil && wv != nil {
77 log.Debug().Uint64("id", uint64(wv.ID())).Msg("acquired webview from pool")
78 return wv, nil
79 }
80 log.Debug().Err(err).Msg("pool acquire failed, creating directly")
81 }
82
83 // Create directly
84 return f.createDirect(ctx)
85}
86
87// CreateRelated creates a WebView that shares session/cookies with the parent.
88// This is required for popup windows to maintain authentication state (OAuth).
89// Related WebViews bypass the pool since they must be linked to a specific parent.
90func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebViewID) (*WebView, error) {
91 log := logging.FromContext(ctx)
92
93 // Look up parent WebView
94 parent := LookupWebView(parentID)
95 if parent == nil {
96 return nil, fmt.Errorf("parent webview %d not found", parentID)
97 }
98 if parent.IsDestroyed() {
99 return nil, fmt.Errorf("parent webview %d is destroyed", parentID)
100 }
101
102 // Create related WebView
103 wv, err := NewWebViewWithRelated(ctx, parent, f.settings)
104 if err != nil {
105 return nil, fmt.Errorf("create related webview: %w", err)
106 }
107
108 // Set background color to match theme (eliminates white flash)
109 f.bgMu.RLock()
110 r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA
111 f.bgMu.RUnlock()
112 if a > 0 {
113 wv.SetBackgroundColor(r, g, b, a)
114 }
115
116 // Keep hidden until content is painted
117 wv.inner.SetVisible(false)
118
119 // Attach frontend (scripts, message handler)
120 if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
121 log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to related webview")
122 }
123
124 // Apply content filters if configured
125 if f.filterApplier != nil {
126 f.filterApplier.ApplyTo(ctx, wv.ucm)
127 }
128
129 log.Debug().
130 Uint64("id", uint64(wv.ID())).
131 Uint64("parent_id", uint64(parentID)).
132 Msg("created related webview for popup")
133
134 return wv, nil
135}
136
137// createDirect creates a WebView without using the pool.
138func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) {
139 log := logging.FromContext(ctx)
140
141 wv, err := NewWebView(ctx, f.wkCtx, f.settings)
142 if err != nil {
143 return nil, err
144 }
145
146 // Set background color to match theme (eliminates white flash)
147 f.bgMu.RLock()
148 r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA
149 f.bgMu.RUnlock()
150 if a > 0 {
151 wv.SetBackgroundColor(r, g, b, a)
152 }
153
154 // Keep hidden until content is painted
155 wv.inner.SetVisible(false)
156
157 // Attach frontend
158 if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
159 log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to webview")
160 }
161
162 // Apply content filters if configured
163 if f.filterApplier != nil {
164 f.filterApplier.ApplyTo(ctx, wv.ucm)
165 }
166
167 log.Debug().Uint64("id", uint64(wv.ID())).Msg("created webview directly")
168 return wv, nil
169}
170
171// Release returns a WebView to the pool if available, otherwise destroys it.
172// Related WebViews (popups) should be destroyed directly, not released to pool.
173func (f *WebViewFactory) Release(ctx context.Context, wv *WebView) {
174 if wv == nil || wv.IsDestroyed() {
175 return
176 }
177
178 if f.pool != nil {
179 f.pool.Release(ctx, wv)
180 } else {
181 wv.Destroy()
182 }
183}