v0.11.0 dc50343

workspace_widgets_stub.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close