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