1//go:build !webkit_cgo
2
3package webkit
4
5import "sync"
6
7// Orientation mirrors GtkOrientation in stub builds.
8type Orientation int
9
10const (
11 OrientationHorizontal Orientation = 0
12 OrientationVertical Orientation = 1
13)
14
15// WidgetBounds mirrors the CGO struct for stub builds.
16type WidgetBounds struct {
17 X float64
18 Y float64
19 Width float64
20 Height float64
21}
22
23// WidgetAllocation represents a widget's allocation (position and size) in stub builds
24type WidgetAllocation struct {
25 X int
26 Y int
27 Width int
28 Height int
29}
30
31type widgetStub struct {
32 startChild uintptr
33 endChild uintptr
34 bounds WidgetBounds
35 hasBounds bool
36 hover map[uintptr]func()
37 // GTK4 lifecycle simulation
38 parent uintptr
39 isDestroyed bool
40 refCount uint
41 widgetType string // Track widget type for IsPaned, etc.
42 // GTK focus management simulation
43 hasFocus bool
44 canFocus bool
45 focusedChild uintptr
46}
47
48var (
49 widgetMu sync.Mutex
50 widgetState = map[uintptr]*widgetStub{}
51 nextWidgetID uintptr = 1
52 nextHoverToken uintptr = 1
53 // GTK focus management simulation
54 globalFocusedWidget uintptr
55 focusWarningEnabled bool = true
56 gtkTestInitOnce sync.Once
57)
58
59func newWidgetHandle() uintptr {
60 widgetMu.Lock()
61 defer widgetMu.Unlock()
62 id := nextWidgetID
63 nextWidgetID++
64 widgetState[id] = &widgetStub{
65 refCount: 1, // GTK widgets start with ref count 1
66 canFocus: true, // Most widgets can receive focus
67 }
68 return id
69}
70
71// NewTestWidget returns a unique widget handle for tests.
72func NewTestWidget() uintptr { return newWidgetHandle() }
73
74// SetWidgetBoundsForTesting assigns bounds for the widget in stub builds.
75func SetWidgetBoundsForTesting(widget uintptr, bounds WidgetBounds) {
76 widgetMu.Lock()
77 defer widgetMu.Unlock()
78 stub, ok := widgetState[widget]
79 if !ok {
80 stub = &widgetStub{}
81 widgetState[widget] = stub
82 }
83 stub.bounds = bounds
84 stub.hasBounds = true
85}
86
87// ResetWidgetStubsForTesting clears widget stub state for deterministic tests.
88func ResetWidgetStubsForTesting() {
89 widgetMu.Lock()
90 defer widgetMu.Unlock()
91 widgetState = map[uintptr]*widgetStub{}
92 nextWidgetID = 1
93 nextHoverToken = 1
94}
95
96func NewPaned(orientation Orientation) uintptr {
97 widgetMu.Lock()
98 defer widgetMu.Unlock()
99 id := nextWidgetID
100 nextWidgetID++
101 widgetState[id] = &widgetStub{
102 refCount: 1,
103 canFocus: true,
104 widgetType: "paned", // Mark as paned widget
105 }
106 return id
107}
108
109func PanedSetStartChild(paned uintptr, child uintptr) {
110 widgetMu.Lock()
111 defer widgetMu.Unlock()
112
113 // GTK4 validation: paned must be a valid widget
114 if paned == 0 {
115 panic("GTK-CRITICAL simulation: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed - paned is NULL")
116 }
117
118 stub, ok := widgetState[paned]
119 if !ok || stub.isDestroyed {
120 panic("GTK-CRITICAL simulation: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed - paned is invalid")
121 }
122
123 // GTK4 validation: if child is not 0, it must be a valid widget
124 if child != 0 {
125 childStub, childExists := widgetState[child]
126 if !childExists || childStub.isDestroyed {
127 panic("GTK-CRITICAL simulation: gtk_widget_insert_before: assertion 'GTK_IS_WIDGET (widget)' failed - child is invalid")
128 }
129 }
130
131 // GTK4 behavior: setting child to 0 automatically unparents the old child
132 if stub.startChild != 0 && child == 0 {
133 if oldChild, exists := widgetState[stub.startChild]; exists {
134 oldChild.parent = 0 // Automatically unparent
135 }
136 }
137 // Set new child and establish parent relationship
138 stub.startChild = child
139 if child != 0 {
140 if childStub, exists := widgetState[child]; exists {
141 // Simulate the exact GTK focus management issue from production
142 // When reparenting a widget with focus hierarchy to a new parent,
143 // GTK's focus management calls gtk_paned_set_focus_child with (nil)
144 if focusWarningEnabled && (childStub.hasFocus || childStub.focusedChild != 0) {
145 panic("GTK-WARNING simulation: Error finding last focus widget of GtkPaned, gtk_paned_set_focus_child was called on widget (nil) which is not child")
146 }
147 childStub.parent = paned
148 }
149 }
150}
151
152func PanedSetEndChild(paned uintptr, child uintptr) {
153 widgetMu.Lock()
154 defer widgetMu.Unlock()
155
156 // GTK4 validation: paned must be a valid widget
157 if paned == 0 {
158 panic("GTK-CRITICAL simulation: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed - paned is NULL")
159 }
160
161 stub, ok := widgetState[paned]
162 if !ok || stub.isDestroyed {
163 panic("GTK-CRITICAL simulation: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed - paned is invalid")
164 }
165
166 // GTK4 validation: if child is not 0, it must be a valid widget
167 if child != 0 {
168 childStub, childExists := widgetState[child]
169 if !childExists || childStub.isDestroyed {
170 panic("GTK-CRITICAL simulation: gtk_widget_insert_before: assertion 'GTK_IS_WIDGET (widget)' failed - child is invalid")
171 }
172 }
173
174 // GTK4 behavior: setting child to 0 automatically unparents the old child
175 if stub.endChild != 0 && child == 0 {
176 if oldChild, exists := widgetState[stub.endChild]; exists {
177 oldChild.parent = 0 // Automatically unparent
178 }
179 }
180 // Set new child and establish parent relationship
181 stub.endChild = child
182 if child != 0 {
183 if childStub, exists := widgetState[child]; exists {
184 // Simulate the exact GTK focus management issue from production
185 // When reparenting a widget with focus hierarchy to a new parent,
186 // GTK's focus management calls gtk_paned_set_focus_child with (nil)
187 if focusWarningEnabled && (childStub.hasFocus || childStub.focusedChild != 0) {
188 panic("GTK-WARNING simulation: Error finding last focus widget of GtkPaned, gtk_paned_set_focus_child was called on widget (nil) which is not child")
189 }
190 childStub.parent = paned
191 }
192 }
193}
194
195func PanedSetResizeStart(paned uintptr, resize bool) {}
196
197func PanedSetResizeEnd(paned uintptr, resize bool) {}
198
199// PanedSetPosition is a no-op in stub builds.
200func PanedSetPosition(paned uintptr, pos int) {}
201
202// PanedGetPosition gets the current position of the divider in a GtkPaned (stub)
203func PanedGetPosition(paned uintptr) int { return 0 }
204
205func WidgetUnparent(widget uintptr) {
206 widgetMu.Lock()
207 defer widgetMu.Unlock()
208 if stub, ok := widgetState[widget]; ok {
209 // GTK4 CRITICAL simulation: unparenting widget with no parent is illegal
210 if stub.parent == 0 {
211 // In real GTK4, this would cause: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed
212 // We'll simulate this by marking the widget as destroyed
213 stub.isDestroyed = true
214 panic("GTK-CRITICAL simulation: gtk_widget_unparent called on widget with no parent")
215 }
216 // Remove from parent's child list
217 if parentStub, exists := widgetState[stub.parent]; exists {
218 if parentStub.startChild == widget {
219 parentStub.startChild = 0
220 }
221 if parentStub.endChild == widget {
222 parentStub.endChild = 0
223 }
224 }
225 stub.parent = 0
226 }
227}
228
229func WidgetSetHExpand(widget uintptr, expand bool) {}
230
231func WidgetSetVExpand(widget uintptr, expand bool) {}
232
233func WidgetGetParent(widget uintptr) uintptr {
234 widgetMu.Lock()
235 defer widgetMu.Unlock()
236 if stub, ok := widgetState[widget]; ok && !stub.isDestroyed {
237 return stub.parent
238 }
239 return 0
240}
241
242func WidgetShow(widget uintptr) {}
243
244func WidgetGrabFocus(widget uintptr) {
245 widgetMu.Lock()
246 defer widgetMu.Unlock()
247
248 if widget == 0 {
249 return
250 }
251
252 stub, ok := widgetState[widget]
253 if !ok || stub.isDestroyed || !stub.canFocus {
254 return
255 }
256
257 // Clear previous focus
258 if globalFocusedWidget != 0 {
259 if prevStub, exists := widgetState[globalFocusedWidget]; exists {
260 prevStub.hasFocus = false
261 }
262 }
263
264 // Set new focus
265 stub.hasFocus = true
266 globalFocusedWidget = widget
267
268 // Simulate focus in parent hierarchy - each parent should point to its direct focused child
269 currentChild := widget
270 parent := stub.parent
271 for parent != 0 {
272 if parentStub, exists := widgetState[parent]; exists {
273 parentStub.focusedChild = currentChild // Direct child that contains focus
274 currentChild = parent // Move up the hierarchy
275 parent = parentStub.parent
276 } else {
277 break
278 }
279 }
280}
281
282// WidgetSetFocusChild sets or clears the focus child of a widget.
283// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
284func WidgetSetFocusChild(widget uintptr, child uintptr) {
285 widgetMu.Lock()
286 defer widgetMu.Unlock()
287
288 if widget == 0 {
289 return
290 }
291
292 stub, ok := widgetState[widget]
293 if !ok || stub.isDestroyed {
294 return
295 }
296
297 stub.focusedChild = child
298}
299
300func WidgetRef(widget uintptr) bool {
301 widgetMu.Lock()
302 defer widgetMu.Unlock()
303 _, ok := widgetState[widget]
304 return ok
305}
306
307func WidgetUnref(widget uintptr) {}
308
309func WidgetQueueAllocate(widget uintptr) {}
310
311func WidgetRealizeInContainer(widget uintptr) {}
312
313func WidgetSetSizeRequest(widget uintptr, width, height int) {}
314
315func WidgetResetSizeRequest(widget uintptr) {}
316
317func WidgetQueueResize(widget uintptr) {}
318
319func WidgetHookDestroy(widget uintptr) {}
320
321func WidgetIsValid(widget uintptr) bool {
322 widgetMu.Lock()
323 defer widgetMu.Unlock()
324 if widget == 0 {
325 return false
326 }
327 if stub, ok := widgetState[widget]; ok {
328 return !stub.isDestroyed
329 }
330 return false
331}
332
333// IsPaned checks if a widget is a GtkPaned container.
334func IsPaned(widget uintptr) bool {
335 widgetMu.Lock()
336 defer widgetMu.Unlock()
337 if widget == 0 {
338 return false
339 }
340 if stub, ok := widgetState[widget]; ok && !stub.isDestroyed {
341 return stub.widgetType == "paned"
342 }
343 return false
344}
345
346// IsBox checks if a widget is a GtkBox container.
347func IsBox(widget uintptr) bool {
348 widgetMu.Lock()
349 defer widgetMu.Unlock()
350 if widget == 0 {
351 return false
352 }
353 if stub, ok := widgetState[widget]; ok && !stub.isDestroyed {
354 return stub.widgetType == "box"
355 }
356 return false
357}
358
359func WidgetRefCount(widget uintptr) uint {
360 if WidgetIsValid(widget) {
361 return 1
362 }
363 return 0
364}
365
366func WidgetAddHoverHandler(widget uintptr, fn func()) uintptr {
367 if widget == 0 || fn == nil {
368 return 0
369 }
370 widgetMu.Lock()
371 defer widgetMu.Unlock()
372 stub, ok := widgetState[widget]
373 if !ok {
374 stub = &widgetStub{}
375 widgetState[widget] = stub
376 }
377 if stub.hover == nil {
378 stub.hover = make(map[uintptr]func())
379 }
380 token := nextHoverToken
381 nextHoverToken++
382 stub.hover[token] = fn
383 return token
384}
385
386func WidgetRemoveHoverHandler(widget uintptr, token uintptr) {
387 widgetMu.Lock()
388 defer widgetMu.Unlock()
389 if stub, ok := widgetState[widget]; ok && stub.hover != nil {
390 delete(stub.hover, token)
391 }
392}
393
394func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
395 widgetMu.Lock()
396 defer widgetMu.Unlock()
397 stub, ok := widgetState[widget]
398 if !ok || !stub.hasBounds {
399 return WidgetBounds{}, false
400 }
401 return stub.bounds, true
402}
403
404// WidgetWaitForDraw is a no-op validation stub in this build.
405// It does NOT provide any synchronization or guarantee that widget draw operations are complete.
406// In the stub, it only validates the widget state and does not wait for any operations.
407// In real GTK builds, this would wait for all pending draw operations to complete.
408func WidgetWaitForDraw(widget uintptr) {
409 gtkTestInitOnce.Do(func() {})
410 widgetMu.Lock()
411 defer widgetMu.Unlock()
412
413 // Validate widget is still valid after any recent operations
414 if widget != 0 {
415 stub, ok := widgetState[widget]
416 if !ok || stub.isDestroyed {
417 panic("GTK-CRITICAL simulation: gtk_test_widget_wait_for_draw: widget is invalid")
418 }
419 }
420}
421
422// IdleAdd simulates GTK's idle callback system - DEPRECATED, use WidgetWaitForDraw instead
423func IdleAdd(fn func() bool) uintptr {
424 if fn != nil {
425 fn()
426 }
427 return 0
428}
429
430// IdleRemove cancels a simulated idle callback (no-op in stub builds).
431func IdleRemove(handle uintptr) {
432 _ = handle
433}
434
435func AddCSSProvider(css string) {}
436
437func WidgetAddCSSClass(widget uintptr, class string) {}
438
439func WidgetRemoveCSSClass(widget uintptr, class string) {}
440
441func WidgetHasCSSClass(widget uintptr, class string) bool {
442 // In stub mode, we don't track CSS classes
443 return false
444}
445
446// Widget margin functions for creating visual borders (stub implementations)
447
448func WidgetSetMargin(widget uintptr, margin int) {}
449
450func WidgetSetMarginTop(widget uintptr, margin int) {}
451
452func WidgetSetMarginBottom(widget uintptr, margin int) {}
453
454func WidgetSetMarginStart(widget uintptr, margin int) {}
455
456func WidgetSetMarginEnd(widget uintptr, margin int) {}
457
458func WidgetQueueDraw(widget uintptr) {
459 // Stub implementation - no-op
460}
461
462// Focus controller functions for GTK4 EventControllerFocus (stub implementations)
463func WidgetAddFocusController(widget uintptr, onEnter, onLeave func()) uintptr {
464 // Stub implementation - return a unique token
465 token := nextHoverToken
466 nextHoverToken++
467 return token
468}
469
470func WidgetRemoveFocusController(widget uintptr, token uintptr) {
471 // Stub implementation - no-op
472}
473
474// SimulateFocusForTesting gives focus to a widget for test scenarios
475func SimulateFocusForTesting(widget uintptr) {
476 WidgetGrabFocus(widget)
477}
478
479// GtkBox functions for stacked panes (stub implementations)
480
481func NewBox(orientation Orientation, spacing int) uintptr {
482 widgetMu.Lock()
483 defer widgetMu.Unlock()
484 id := nextWidgetID
485 nextWidgetID++
486 widgetState[id] = &widgetStub{
487 refCount: 1,
488 canFocus: true,
489 widgetType: "box", // Mark as box widget
490 }
491 return id
492}
493
494func BoxAppend(box uintptr, child uintptr) {
495 widgetMu.Lock()
496 defer widgetMu.Unlock()
497
498 if box == 0 || child == 0 {
499 return
500 }
501
502 boxStub, ok := widgetState[box]
503 if !ok || boxStub.isDestroyed {
504 panic("GTK-CRITICAL simulation: gtk_box_append: box is invalid")
505 }
506
507 childStub, ok := widgetState[child]
508 if !ok || childStub.isDestroyed {
509 panic("GTK-CRITICAL simulation: gtk_box_append: child is invalid")
510 }
511
512 if childStub.parent != 0 && childStub.parent != box {
513 panic("GTK-CRITICAL simulation: gtk_box_append: child already has a different parent")
514 }
515 childStub.parent = box
516}
517
518func BoxPrepend(box uintptr, child uintptr) {
519 BoxAppend(box, child)
520}
521
522func BoxRemove(box uintptr, child uintptr) {
523 widgetMu.Lock()
524 defer widgetMu.Unlock()
525
526 if box == 0 || child == 0 {
527 return
528 }
529
530 childStub, ok := widgetState[child]
531 if !ok || childStub.isDestroyed {
532 panic("GTK-CRITICAL simulation: gtk_box_remove: child is invalid")
533 }
534
535 if childStub.parent != box {
536 panic("GTK-CRITICAL simulation: gtk_box_remove: child is not parented to the specified box")
537 }
538
539 childStub.parent = 0
540}
541
542func BoxInsertChildAfter(box uintptr, child uintptr, sibling uintptr) {
543 BoxAppend(box, child)
544}
545
546// Widget visibility functions for stacked panes (stub implementations)
547
548func WidgetSetVisible(widget uintptr, visible bool) {}
549
550func WidgetGetVisible(widget uintptr) bool { return true }
551
552func WidgetHide(widget uintptr) {}
553
554// Label functions for title bars (stub implementations)
555
556func NewLabel(text string) uintptr {
557 return newWidgetHandle()
558}
559
560func LabelSetText(label uintptr, text string) {}
561
562func LabelGetText(label uintptr) string { return "" }
563
564// EllipsizeMode represents PangoEllipsizeMode values in stub builds.
565type EllipsizeMode int
566
567const (
568 EllipsizeNone EllipsizeMode = 0
569 EllipsizeStart EllipsizeMode = 1
570 EllipsizeMiddle EllipsizeMode = 2
571 EllipsizeEnd EllipsizeMode = 3
572)
573
574func LabelSetEllipsize(label uintptr, mode EllipsizeMode) {}
575
576func LabelSetMaxWidthChars(label uintptr, nChars int) {}
577
578// SetTestContainer is a testing helper to set the container field for WebView in stub builds
579func (w *WebView) SetTestContainer(container uintptr) {
580 w.container = container
581}
582
583// WidgetGetAllocation returns a stub allocation for the widget
584func WidgetGetAllocation(widget uintptr) WidgetAllocation {
585 widgetMu.Lock()
586 defer widgetMu.Unlock()
587
588 // Return stub allocation based on widget bounds if available
589 if stub, ok := widgetState[widget]; ok && stub.hasBounds {
590 return WidgetAllocation{
591 X: int(stub.bounds.X),
592 Y: int(stub.bounds.Y),
593 Width: int(stub.bounds.Width),
594 Height: int(stub.bounds.Height),
595 }
596 }
597
598 // Default stub allocation for testing
599 return WidgetAllocation{
600 X: 0,
601 Y: 0,
602 Width: 400,
603 Height: 300,
604 }
605}