1package cef
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sync"
8 "unsafe"
9
10 purecef "github.com/bnema/purego-cef/cef"
11
12 "github.com/bnema/dumber/internal/logging"
13)
14
15const (
16 rendererBridgeMessageName = "dumber.renderer_bridge"
17 rendererBridgeActionExplicitTextCopy = "explicit_text_copy"
18 rendererBridgeActionFocusSync = "focus_sync"
19 rendererBridgeActionEditableFocusChanged = "editable_focus_changed"
20 rendererBridgeActionPopupOpen = "popup_open"
21 rendererBridgeActionPopupNavigate = "popup_navigate"
22 rendererBridgeActionPopupClose = "popup_close"
23 rendererBridgeActionReady = "bridge_ready"
24 rendererBridgeExtensionName = "dumber.renderer_bridge"
25)
26
27var newRendererBridgeProcessMessage = purecef.ProcessMessageCreate
28
29type rendererBridgeProcessHandler struct {
30 registerOnce sync.Once
31 bridgeHandler purecef.V8Handler
32}
33
34func (h *rendererBridgeProcessHandler) ensureExtensionRegistered() {
35 if h == nil {
36 return
37 }
38 h.registerOnce.Do(func() {
39 if h.bridgeHandler == nil {
40 h.bridgeHandler = &rendererBridgeV8Handler{}
41 }
42 if result := purecef.RegisterExtension(rendererBridgeExtensionName, rendererBridgeExtensionJS, h.bridgeHandler); result != 1 {
43 logging.FromContext(context.Background()).Error().Int32("result", result).Msg("cef: failed to register renderer bridge extension")
44 }
45 })
46}
47
48func (h *rendererBridgeProcessHandler) OnWebKitInitialized() {
49 h.ensureExtensionRegistered()
50}
51
52func (*rendererBridgeProcessHandler) OnBrowserCreated(_ purecef.Browser, _ purecef.DictionaryValue) {}
53func (*rendererBridgeProcessHandler) OnBrowserDestroyed(_ purecef.Browser) {}
54func (*rendererBridgeProcessHandler) GetLoadHandler() purecef.LoadHandler { return nil }
55func (*rendererBridgeProcessHandler) OnContextCreated(_ purecef.Browser, _ purecef.Frame, _ purecef.V8Context) {
56}
57func (*rendererBridgeProcessHandler) OnContextReleased(_ purecef.Browser, _ purecef.Frame, _ purecef.V8Context) {
58}
59func (*rendererBridgeProcessHandler) OnUncaughtException(
60 _ purecef.Browser, _ purecef.Frame, _ purecef.V8Context, _ purecef.V8Exception, _ purecef.V8StackTrace,
61) {
62}
63func (*rendererBridgeProcessHandler) OnFocusedNodeChanged(_ purecef.Browser, frame purecef.Frame, node purecef.Domnode) {
64 if frame == nil {
65 return
66 }
67 message := newRendererBridgeProcessMessage(rendererBridgeMessageName)
68 if message == nil {
69 return
70 }
71 args := message.GetArgumentList()
72 if args == nil {
73 return
74 }
75 args.SetString(0, rendererBridgeActionEditableFocusChanged)
76 if node != nil && node.IsEditable() {
77 args.SetString(1, "1")
78 } else {
79 args.SetString(1, "0")
80 }
81 frame.SendProcessMessage(purecef.ProcessIDPidBrowser, message)
82}
83func (*rendererBridgeProcessHandler) OnProcessMessageReceived(
84 _ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage,
85) int32 {
86 return 0
87}
88
89type rendererBridgeV8Handler struct{}
90
91func (*rendererBridgeV8Handler) Execute(
92 name string, _ purecef.V8Value, arguments []purecef.V8Value, _ unsafe.Pointer, _ uintptr,
93) int32 {
94 if name != "Dispatch" || len(arguments) < 2 {
95 return 0
96 }
97 if arguments[0] == nil || arguments[1] == nil || !arguments[0].IsString() || !arguments[1].IsString() {
98 return 0
99 }
100 action := arguments[0].GetStringValue()
101 payload := arguments[1].GetStringValue()
102 if action == "" {
103 return 0
104 }
105
106 ctx := purecef.V8ContextGetCurrentContext()
107 if ctx == nil || !ctx.IsValid() {
108 return 0
109 }
110 frame := ctx.GetFrame()
111 if frame == nil {
112 return 0
113 }
114 message := purecef.ProcessMessageCreate(rendererBridgeMessageName)
115 if message == nil {
116 return 0
117 }
118 args := message.GetArgumentList()
119 if args == nil {
120 return 0
121 }
122 args.SetString(0, action)
123 args.SetString(1, payload)
124 frame.SendProcessMessage(purecef.ProcessIDPidBrowser, message)
125 return 1
126}
127
128func decodeRendererBridgeProcessMessage(message purecef.ProcessMessage) (string, string, bool) {
129 if message == nil || message.GetName() != rendererBridgeMessageName {
130 return "", "", false
131 }
132 args := message.GetArgumentList()
133 if args == nil || args.GetSize() < 2 {
134 return "", "", false
135 }
136 action := args.GetString(0)
137 payload := args.GetString(1)
138 if action == "" {
139 return "", "", false
140 }
141 return action, payload, true
142}
143
144type rendererBridgeExplicitTextCopyPayload struct {
145 Text string `json:"text"`
146 Action string `json:"action"`
147}
148
149type rendererBridgePopupOpenPayload struct {
150 ProxyID string `json:"proxy_id"`
151 URL string `json:"url"`
152 FrameName string `json:"frame_name"`
153 UserGesture bool `json:"user_gesture"`
154 NoJavaScriptAccess bool `json:"no_javascript_access"`
155}
156
157type rendererBridgePopupNavigatePayload struct {
158 ProxyID string `json:"proxy_id"`
159 URL string `json:"url"`
160}
161
162type rendererBridgePopupClosePayload struct {
163 ProxyID string `json:"proxy_id"`
164}
165
166func decodeRendererBridgeExplicitTextCopyPayload(payload []byte) (rendererBridgeExplicitTextCopyPayload, error) {
167 var req rendererBridgeExplicitTextCopyPayload
168 if len(payload) == 0 {
169 return req, fmt.Errorf("empty payload")
170 }
171 if err := json.Unmarshal(payload, &req); err != nil {
172 return req, err
173 }
174 if req.Action == "" {
175 req.Action = "copy"
176 }
177 if req.Text == "" {
178 return req, fmt.Errorf("missing text")
179 }
180 return req, nil
181}
182
183func decodeRendererBridgePopupPayload[T any](payload []byte, proxyID func(T) string) (T, error) {
184 var req T
185 if len(payload) == 0 {
186 return req, fmt.Errorf("empty payload")
187 }
188 if err := json.Unmarshal(payload, &req); err != nil {
189 return req, err
190 }
191 if proxyID(req) == "" {
192 return req, fmt.Errorf("missing proxy_id")
193 }
194 return req, nil
195}
196
197func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) {
198 return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupOpenPayload) string {
199 return req.ProxyID
200 })
201}
202
203func decodeRendererBridgePopupNavigatePayload(payload []byte) (rendererBridgePopupNavigatePayload, error) {
204 return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupNavigatePayload) string {
205 return req.ProxyID
206 })
207}
208
209func decodeRendererBridgePopupClosePayload(payload []byte) (rendererBridgePopupClosePayload, error) {
210 return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupClosePayload) string {
211 return req.ProxyID
212 })
213}
214
215func (e *Engine) handleEditableFocusBridge(browser purecef.Browser) {
216 logger := logging.FromContext(e.currentContext())
217 task := purecef.NewTask(cefTaskFunc(func() {
218 if browser == nil {
219 logger.Debug().Msg("cef: editable focus sync skipped — browser nil")
220 return
221 }
222 host := browser.GetHost()
223 if host == nil {
224 logger.Debug().Msg("cef: editable focus sync skipped — host nil")
225 return
226 }
227 syncWindowlessBrowserFocus(host)
228 logger.Debug().Msg("cef: editable focus synchronized")
229 }))
230 if result := purecef.PostTask(purecef.ThreadIDTidUi, task); result != 1 {
231 logger.Warn().Int32("result", result).Msg("cef: failed to post editable focus sync to UI thread")
232 }
233}