1package cef
2
3import (
4 "sync"
5
6 purecef "github.com/bnema/purego-cef/cef"
7
8 "github.com/bnema/dumber/internal/application/port"
9)
10
11// Compile-time interface check.
12var _ port.FindController = (*cefFindController)(nil)
13
14// cefFindController implements port.FindController using CEF's BrowserHost.Find API.
15type cefFindController struct {
16 mu sync.RWMutex
17 host purecef.BrowserHost
18 searchTxt string
19 matchCase int32 // cached from most recent Search call
20
21 // counting tracks whether the current search is a CountMatches request
22 // so that handleFindResult dispatches to the right callbacks.
23 counting bool
24
25 // Signal callbacks keyed by unique IDs.
26 nextSignalID uint
27 onFound map[uint]func(matchCount uint)
28 onFailed map[uint]func()
29 onCounted map[uint]func(matchCount uint)
30}
31
32func newFindController() *cefFindController {
33 return &cefFindController{
34 onFound: make(map[uint]func(matchCount uint)),
35 onFailed: make(map[uint]func()),
36 onCounted: make(map[uint]func(matchCount uint)),
37 }
38}
39
40func (fc *cefFindController) setHost(host purecef.BrowserHost) {
41 fc.mu.Lock()
42 fc.host = host
43 fc.mu.Unlock()
44}
45
46func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint) {
47 fc.mu.Lock()
48 fc.searchTxt = text
49 fc.counting = false
50 mc := matchCaseFromOpts(opts)
51 fc.matchCase = mc
52 host := fc.host
53 fc.mu.Unlock()
54
55 if host == nil || text == "" {
56 return
57 }
58 host.Find(text, 1, mc, 0) // forward=1, findnext=0 (new search)
59}
60
61// CountMatches starts a search whose results are dispatched to onCounted callbacks.
62func (fc *cefFindController) CountMatches(text string, opts port.FindOptions, _ uint) {
63 fc.mu.Lock()
64 fc.searchTxt = text
65 fc.counting = true
66 mc := matchCaseFromOpts(opts)
67 fc.matchCase = mc
68 host := fc.host
69 fc.mu.Unlock()
70
71 if host == nil || text == "" {
72 return
73 }
74 host.Find(text, 1, mc, 0)
75}
76
77// SearchNext continues the search in the forward direction.
78func (fc *cefFindController) SearchNext() {
79 fc.mu.RLock()
80 host := fc.host
81 text := fc.searchTxt
82 mc := fc.matchCase
83 fc.mu.RUnlock()
84
85 if host == nil || text == "" {
86 return
87 }
88 host.Find(text, 1, mc, 1) // forward=1, findnext=1
89}
90
91// SearchPrevious continues the search in the backward direction.
92func (fc *cefFindController) SearchPrevious() {
93 fc.mu.RLock()
94 host := fc.host
95 text := fc.searchTxt
96 mc := fc.matchCase
97 fc.mu.RUnlock()
98
99 if host == nil || text == "" {
100 return
101 }
102 host.Find(text, 0, mc, 1) // forward=0, findnext=1
103}
104
105func (fc *cefFindController) SearchFinish() {
106 fc.mu.Lock()
107 host := fc.host
108 fc.searchTxt = ""
109 fc.mu.Unlock()
110
111 if host != nil {
112 host.StopFinding(1)
113 }
114}
115
116func (fc *cefFindController) GetSearchText() string {
117 fc.mu.RLock()
118 defer fc.mu.RUnlock()
119 return fc.searchTxt
120}
121
122// OnFoundText registers a callback fired when matches are found.
123func (fc *cefFindController) OnFoundText(callback func(matchCount uint)) uint {
124 if callback == nil {
125 return 0
126 }
127 fc.mu.Lock()
128 defer fc.mu.Unlock()
129 fc.nextSignalID++
130 id := fc.nextSignalID
131 fc.onFound[id] = callback
132 return id
133}
134
135// OnFailedToFindText registers a callback fired when no matches are found.
136func (fc *cefFindController) OnFailedToFindText(callback func()) uint {
137 if callback == nil {
138 return 0
139 }
140 fc.mu.Lock()
141 defer fc.mu.Unlock()
142 fc.nextSignalID++
143 id := fc.nextSignalID
144 fc.onFailed[id] = callback
145 return id
146}
147
148// OnCountedMatches registers a callback fired with the final match count.
149func (fc *cefFindController) OnCountedMatches(callback func(matchCount uint)) uint {
150 if callback == nil {
151 return 0
152 }
153 fc.mu.Lock()
154 defer fc.mu.Unlock()
155 fc.nextSignalID++
156 id := fc.nextSignalID
157 fc.onCounted[id] = callback
158 return id
159}
160
161// DisconnectSignal removes a previously registered callback.
162func (fc *cefFindController) DisconnectSignal(id uint) {
163 if id == 0 {
164 return
165 }
166 fc.mu.Lock()
167 defer fc.mu.Unlock()
168 delete(fc.onFound, id)
169 delete(fc.onFailed, id)
170 delete(fc.onCounted, id)
171}
172
173// handleFindResult is called by the FindHandler when CEF reports find results.
174func (fc *cefFindController) handleFindResult(_, count, _, finalUpdate int32) {
175 fc.mu.RLock()
176 counting := fc.counting
177
178 // Snapshot callback maps so we can invoke them outside the lock.
179 var foundCBs []func(matchCount uint)
180 var failedCBs []func()
181 var countedCBs []func(matchCount uint)
182
183 if count > 0 && !counting {
184 foundCBs = make([]func(matchCount uint), 0, len(fc.onFound))
185 for _, cb := range fc.onFound {
186 foundCBs = append(foundCBs, cb)
187 }
188 }
189 if count == 0 && finalUpdate != 0 && !counting {
190 failedCBs = make([]func(), 0, len(fc.onFailed))
191 for _, cb := range fc.onFailed {
192 failedCBs = append(failedCBs, cb)
193 }
194 }
195 if finalUpdate != 0 && counting {
196 countedCBs = make([]func(matchCount uint), 0, len(fc.onCounted))
197 for _, cb := range fc.onCounted {
198 countedCBs = append(countedCBs, cb)
199 }
200 }
201 fc.mu.RUnlock()
202
203 var safeCount uint
204 if count > 0 {
205 safeCount = uint(count)
206 }
207 for _, cb := range foundCBs {
208 cb(safeCount)
209 }
210 for _, cb := range failedCBs {
211 cb()
212 }
213 for _, cb := range countedCBs {
214 cb(safeCount)
215 }
216}
217
218// matchCaseFromOpts converts FindOptions to CEF's matchcase parameter.
219// CEF: matchcase=1 means case-sensitive, matchcase=0 means case-insensitive.
220func matchCaseFromOpts(opts port.FindOptions) int32 {
221 if opts.CaseInsensitive {
222 return 0
223 }
224 return 1
225}