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