fix: resolve GTK test crash and focus management issues
**Critical Fixes:** - Remove gtk_test_widget_wait_for_draw from production (GTK4 violation) - Remove workspace_gtk_test_init and gtkTestInitOnce from CGO - Replace with WidgetQueueDraw (GTK4 recommended approach) **Focus Management:** - Fix focus order: call FocusManager.SetActivePane BEFORE updating currentlyFocused - Add fatal check verifyOnlyOneActivePaneOrPanic() to enforce single active pane rule - Add WidgetHasCSSClass() for active pane verification - Fix mouse hover focus not applying CSS changes **Navigation:** - Alt+Direction navigation now works correctly with proper focus transitions - Mouse hover navigation now works like Alt+Direction (same focus flow) - Single active pane rule enforced with panic on violation The crash was caused by using GTK test functions in production code. The focus issues were caused by incorrect operation order in hover/navigation handlers.
- commit
- fcf18d521cbdae96dab61974d72f56845c29469e
- author
- bamen · · 2025-09-26 05:32 UTC
- parent
- 2ef39d4
- signature
- unsigned
4 files · +78 -33
M internal/app/browser/focus_manager.go +40 -0
| @@ -106,13 +106,18 @@ func (fm *FocusManager) updateVisualState(oldPane, newPane *paneNode) { | |||
| 106 | 106 | // Remove active class from old pane | |
| 107 | 107 | if oldPane != nil && oldPane.container != 0 { | |
| 108 | 108 | webkit.WidgetRemoveCSSClass(oldPane.container, activePaneClass) | |
| 109 | + | webkit.WidgetQueueDraw(oldPane.container) | |
| 109 | 110 | } | |
| 110 | 111 | ||
| 111 | 112 | // Add active class to new pane | |
| 112 | 113 | if newPane.container != 0 { | |
| 113 | 114 | webkit.WidgetAddCSSClass(newPane.container, activePaneClass) | |
| 115 | + | webkit.WidgetQueueDraw(newPane.container) | |
| 114 | 116 | } | |
| 115 | 117 | ||
| 118 | + | // FATAL CHECK: Ensure only ONE pane has active class | |
| 119 | + | fm.verifyOnlyOneActivePaneOrPanic() | |
| 120 | + | ||
| 116 | 121 | // Handle stacked panes visibility | |
| 117 | 122 | fm.updateStackedPaneVisibility(newPane) | |
| 118 | 123 | } | |
| @@ -242,3 +247,38 @@ func (fm *FocusManager) SetActivePaneByView(view *webkit.WebView) { | |||
| 242 | 247 | log.Printf("[focus-manager] WARNING: SetActivePaneByView called with unknown view %p", view) | |
| 243 | 248 | } | |
| 244 | 249 | } | |
| 250 | + | ||
| 251 | + | // verifyOnlyOneActivePaneOrPanic performs a FATAL check to ensure only one pane is active | |
| 252 | + | // This is a fundamental rule that MUST NEVER be violated | |
| 253 | + | func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() { | |
| 254 | + | if fm == nil || fm.wm == nil { | |
| 255 | + | return | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | activePaneClass := "workspace-pane-active" | |
| 259 | + | activeCount := 0 | |
| 260 | + | var activePanes []uintptr | |
| 261 | + | ||
| 262 | + | // Check all leaf panes in the workspace | |
| 263 | + | leaves := fm.wm.collectLeaves() | |
| 264 | + | for _, leaf := range leaves { | |
| 265 | + | if leaf != nil && leaf.container != 0 { | |
| 266 | + | if webkit.WidgetHasCSSClass(leaf.container, activePaneClass) { | |
| 267 | + | activeCount++ | |
| 268 | + | activePanes = append(activePanes, leaf.container) | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // FATAL: Multiple active panes detected | |
| 274 | + | if activeCount > 1 { | |
| 275 | + | log.Fatalf("[focus-manager] FATAL: Multiple active panes detected! Count=%d, Containers=%v. Only ONE pane can be active at a time. This violates a fundamental rule.", activeCount, activePanes) | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | // FATAL: No active panes when there should be one | |
| 279 | + | if activeCount == 0 && len(leaves) > 0 { | |
| 280 | + | log.Fatalf("[focus-manager] FATAL: No active panes detected but %d panes exist. There should always be exactly ONE active pane.", len(leaves)) | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | log.Printf("[focus-manager] ✓ Single active pane rule verified: %d active out of %d total panes", activeCount, len(leaves)) | |
| 284 | + | } | |
| @@ -106,13 +106,18 @@ func (fm *FocusManager) updateVisualState(oldPane, newPane *paneNode) { | |||||
| 106 | // Remove active class from old pane | 106 | // Remove active class from old pane | ||
| 107 | if oldPane != nil && oldPane.container != 0 { | 107 | if oldPane != nil && oldPane.container != 0 { | ||
| 108 | webkit.WidgetRemoveCSSClass(oldPane.container, activePaneClass) | 108 | webkit.WidgetRemoveCSSClass(oldPane.container, activePaneClass) | ||
| 109 | + | webkit.WidgetQueueDraw(oldPane.container) | |||
| 109 | } | 110 | } | ||
| 110 | 111 | ||||
| 111 | // Add active class to new pane | 112 | // Add active class to new pane | ||
| 112 | if newPane.container != 0 { | 113 | if newPane.container != 0 { | ||
| 113 | webkit.WidgetAddCSSClass(newPane.container, activePaneClass) | 114 | webkit.WidgetAddCSSClass(newPane.container, activePaneClass) | ||
| 115 | + | webkit.WidgetQueueDraw(newPane.container) | |||
| 114 | } | 116 | } | ||
| 115 | 117 | ||||
| 118 | + | // FATAL CHECK: Ensure only ONE pane has active class | |||
| 119 | + | fm.verifyOnlyOneActivePaneOrPanic() | |||
| 120 | + | ||||
| 116 | // Handle stacked panes visibility | 121 | // Handle stacked panes visibility | ||
| 117 | fm.updateStackedPaneVisibility(newPane) | 122 | fm.updateStackedPaneVisibility(newPane) | ||
| 118 | } | 123 | } | ||
| @@ -242,3 +247,38 @@ func (fm *FocusManager) SetActivePaneByView(view *webkit.WebView) { | |||||
| 242 | log.Printf("[focus-manager] WARNING: SetActivePaneByView called with unknown view %p", view) | 247 | log.Printf("[focus-manager] WARNING: SetActivePaneByView called with unknown view %p", view) | ||
| 243 | } | 248 | } | ||
| 244 | } | 249 | } | ||
| 250 | + | ||||
| 251 | + | // verifyOnlyOneActivePaneOrPanic performs a FATAL check to ensure only one pane is active | |||
| 252 | + | // This is a fundamental rule that MUST NEVER be violated | |||
| 253 | + | func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() { | |||
| 254 | + | if fm == nil || fm.wm == nil { | |||
| 255 | + | return | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | activePaneClass := "workspace-pane-active" | |||
| 259 | + | activeCount := 0 | |||
| 260 | + | var activePanes []uintptr | |||
| 261 | + | ||||
| 262 | + | // Check all leaf panes in the workspace | |||
| 263 | + | leaves := fm.wm.collectLeaves() | |||
| 264 | + | for _, leaf := range leaves { | |||
| 265 | + | if leaf != nil && leaf.container != 0 { | |||
| 266 | + | if webkit.WidgetHasCSSClass(leaf.container, activePaneClass) { | |||
| 267 | + | activeCount++ | |||
| 268 | + | activePanes = append(activePanes, leaf.container) | |||
| 269 | + | } | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | // FATAL: Multiple active panes detected | |||
| 274 | + | if activeCount > 1 { | |||
| 275 | + | log.Fatalf("[focus-manager] FATAL: Multiple active panes detected! Count=%d, Containers=%v. Only ONE pane can be active at a time. This violates a fundamental rule.", activeCount, activePanes) | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | // FATAL: No active panes when there should be one | |||
| 279 | + | if activeCount == 0 && len(leaves) > 0 { | |||
| 280 | + | log.Fatalf("[focus-manager] FATAL: No active panes detected but %d panes exist. There should always be exactly ONE active pane.", len(leaves)) | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | log.Printf("[focus-manager] ✓ Single active pane rule verified: %d active out of %d total panes", activeCount, len(leaves)) | |||
| 284 | + | } | |||
M internal/app/browser/workspace_manager.go +6 -12
| @@ -610,8 +610,8 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) { | |||
| 610 | 610 | if wm == nil { | |
| 611 | 611 | return | |
| 612 | 612 | } | |
| 613 | - | wm.currentlyFocused = node | |
| 614 | 613 | wm.focusManager.SetActivePane(node) | |
| 614 | + | wm.currentlyFocused = node | |
| 615 | 615 | }) | |
| 616 | 616 | node.hoverToken = token | |
| 617 | 617 | if token == 0 { | |
| @@ -714,8 +714,8 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool { | |||
| 714 | 714 | ||
| 715 | 715 | // Focus the new active pane | |
| 716 | 716 | newActivePane := stackNode.stackedPanes[newIndex] | |
| 717 | - | wm.currentlyFocused = newActivePane | |
| 718 | 717 | wm.focusManager.SetActivePane(newActivePane) | |
| 718 | + | wm.currentlyFocused = newActivePane | |
| 719 | 719 | ||
| 720 | 720 | log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d", | |
| 721 | 721 | direction, currentIndex, newIndex, len(stackNode.stackedPanes)) | |
| @@ -730,8 +730,8 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||
| 730 | 730 | } | |
| 731 | 731 | ||
| 732 | 732 | if neighbor := wm.structuralNeighbor(wm.currentlyFocused, direction); neighbor != nil { | |
| 733 | - | wm.currentlyFocused = neighbor | |
| 734 | 733 | wm.focusManager.SetActivePane(neighbor) | |
| 734 | + | wm.currentlyFocused = neighbor | |
| 735 | 735 | return true | |
| 736 | 736 | } | |
| 737 | 737 | ||
| @@ -798,8 +798,8 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||
| 798 | 798 | } | |
| 799 | 799 | ||
| 800 | 800 | if best != nil { | |
| 801 | - | wm.currentlyFocused = best | |
| 802 | 801 | wm.focusManager.SetActivePane(best) | |
| 802 | + | wm.currentlyFocused = best | |
| 803 | 803 | return true | |
| 804 | 804 | } | |
| 805 | 805 | ||
| @@ -1148,9 +1148,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1148 | 1148 | } | |
| 1149 | 1149 | } | |
| 1150 | 1150 | ||
| 1151 | - | // Ensure widget operations are complete before re-parenting (GTK test pattern) | |
| 1152 | - | webkit.WidgetWaitForDraw(splitTargetContainer) | |
| 1153 | - | webkit.WidgetWaitForDraw(newContainer) | |
| 1151 | + | // GTK4 handles widget operations automatically - no need to force redraw | |
| 1154 | 1152 | ||
| 1155 | 1153 | // Add both containers to the new paned | |
| 1156 | 1154 | if existingFirst { | |
| @@ -1889,11 +1887,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1889 | 1887 | } | |
| 1890 | 1888 | ||
| 1891 | 1889 | // Now safely reparent the sibling to the grandparent | |
| 1892 | - | // Ensure GTK focus state is synchronized before reparenting to prevent | |
| 1893 | - | // GTK-WARNING: "Error finding last focus widget of GtkPaned, gtk_paned_set_focus_child | |
| 1894 | - | // was called on widget (nil) which is not child" | |
| 1895 | - | webkit.WidgetWaitForDraw(sibling.container) | |
| 1896 | - | webkit.WidgetWaitForDraw(grand.container) | |
| 1890 | + | // GTK4 automatically handles focus state synchronization during reparenting | |
| 1897 | 1891 | ||
| 1898 | 1892 | sibling.parent = grand | |
| 1899 | 1893 | if grand.container != 0 && !grand.isLeaf { | |
| @@ -610,8 +610,8 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) { | |||||
| 610 | if wm == nil { | 610 | if wm == nil { | ||
| 611 | return | 611 | return | ||
| 612 | } | 612 | } | ||
| 613 | - | wm.currentlyFocused = node | |||
| 614 | wm.focusManager.SetActivePane(node) | 613 | wm.focusManager.SetActivePane(node) | ||
| 614 | + | wm.currentlyFocused = node | |||
| 615 | }) | 615 | }) | ||
| 616 | node.hoverToken = token | 616 | node.hoverToken = token | ||
| 617 | if token == 0 { | 617 | if token == 0 { | ||
| @@ -714,8 +714,8 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool { | |||||
| 714 | 714 | ||||
| 715 | // Focus the new active pane | 715 | // Focus the new active pane | ||
| 716 | newActivePane := stackNode.stackedPanes[newIndex] | 716 | newActivePane := stackNode.stackedPanes[newIndex] | ||
| 717 | - | wm.currentlyFocused = newActivePane | |||
| 718 | wm.focusManager.SetActivePane(newActivePane) | 717 | wm.focusManager.SetActivePane(newActivePane) | ||
| 718 | + | wm.currentlyFocused = newActivePane | |||
| 719 | 719 | ||||
| 720 | log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d", | 720 | log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d", | ||
| 721 | direction, currentIndex, newIndex, len(stackNode.stackedPanes)) | 721 | direction, currentIndex, newIndex, len(stackNode.stackedPanes)) | ||
| @@ -730,8 +730,8 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||||
| 730 | } | 730 | } | ||
| 731 | 731 | ||||
| 732 | if neighbor := wm.structuralNeighbor(wm.currentlyFocused, direction); neighbor != nil { | 732 | if neighbor := wm.structuralNeighbor(wm.currentlyFocused, direction); neighbor != nil { | ||
| 733 | - | wm.currentlyFocused = neighbor | |||
| 734 | wm.focusManager.SetActivePane(neighbor) | 733 | wm.focusManager.SetActivePane(neighbor) | ||
| 734 | + | wm.currentlyFocused = neighbor | |||
| 735 | return true | 735 | return true | ||
| 736 | } | 736 | } | ||
| 737 | 737 | ||||
| @@ -798,8 +798,8 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||||
| 798 | } | 798 | } | ||
| 799 | 799 | ||||
| 800 | if best != nil { | 800 | if best != nil { | ||
| 801 | - | wm.currentlyFocused = best | |||
| 802 | wm.focusManager.SetActivePane(best) | 801 | wm.focusManager.SetActivePane(best) | ||
| 802 | + | wm.currentlyFocused = best | |||
| 803 | return true | 803 | return true | ||
| 804 | } | 804 | } | ||
| 805 | 805 | ||||
| @@ -1148,9 +1148,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||||
| 1148 | } | 1148 | } | ||
| 1149 | } | 1149 | } | ||
| 1150 | 1150 | ||||
| 1151 | - | // Ensure widget operations are complete before re-parenting (GTK test pattern) | 1151 | + | // GTK4 handles widget operations automatically - no need to force redraw |
| 1152 | - | webkit.WidgetWaitForDraw(splitTargetContainer) | |||
| 1153 | - | webkit.WidgetWaitForDraw(newContainer) | |||
| 1154 | 1152 | ||||
| 1155 | // Add both containers to the new paned | 1153 | // Add both containers to the new paned | ||
| 1156 | if existingFirst { | 1154 | if existingFirst { | ||
| @@ -1889,11 +1887,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||||
| 1889 | } | 1887 | } | ||
| 1890 | 1888 | ||||
| 1891 | // Now safely reparent the sibling to the grandparent | 1889 | // Now safely reparent the sibling to the grandparent | ||
| 1892 | - | // Ensure GTK focus state is synchronized before reparenting to prevent | 1890 | + | // GTK4 automatically handles focus state synchronization during reparenting |
| 1893 | - | // GTK-WARNING: "Error finding last focus widget of GtkPaned, gtk_paned_set_focus_child | |||
| 1894 | - | // was called on widget (nil) which is not child" | |||
| 1895 | - | webkit.WidgetWaitForDraw(sibling.container) | |||
| 1896 | - | webkit.WidgetWaitForDraw(grand.container) | |||
| 1897 | 1891 | ||||
| 1898 | sibling.parent = grand | 1892 | sibling.parent = grand | ||
| 1899 | if grand.container != 0 && !grand.isLeaf { | 1893 | if grand.container != 0 && !grand.isLeaf { | ||
M pkg/webkit/workspace_widgets_cgo.go +23 -21
| @@ -196,17 +196,7 @@ static void widget_show(GtkWidget* widget) { | |||
| 196 | 196 | gtk_widget_set_visible(widget, TRUE); | |
| 197 | 197 | } | |
| 198 | 198 | ||
| 199 | - | static void workspace_gtk_test_init(void) { | |
| 200 | - | // Create dummy argc/argv for GTK test initialization | |
| 201 | - | // GTK test framework requires valid argc/argv pointers, not NULL | |
| 202 | - | static int test_argc = 1; | |
| 203 | - | static char test_prog[] = "dumber-test"; | |
| 204 | - | static char* test_argv[] = { test_prog, NULL }; | |
| 205 | - | static char** test_argv_ptr = test_argv; | |
| 206 | - | ||
| 207 | - | // Initialize GTK test framework with proper arguments | |
| 208 | - | gtk_test_init(&test_argc, &test_argv_ptr, NULL); | |
| 209 | - | } | |
| 199 | + | // workspace_gtk_test_init removed - moved to test files only | |
| 210 | 200 | ||
| 211 | 201 | // Label helpers for title bars | |
| 212 | 202 | static GtkWidget* label_new(const char* text) { | |
| @@ -254,7 +244,7 @@ var ( | |||
| 254 | 244 | hoverCallbacks = make(map[uintptr]func()) | |
| 255 | 245 | hoverControllers = make(map[uintptr]uintptr) | |
| 256 | 246 | nextHoverID uintptr = 1 | |
| 257 | - | gtkTestInitOnce sync.Once | |
| 247 | + | // gtkTestInitOnce removed - no longer needed | |
| 258 | 248 | ) | |
| 259 | 249 | ||
| 260 | 250 | func widgetIsValid(widget uintptr) bool { | |
| @@ -763,17 +753,29 @@ func LabelSetMaxWidthChars(label uintptr, nChars int) { | |||
| 763 | 753 | C.label_set_max_width_chars((*C.GtkWidget)(unsafe.Pointer(label)), C.int(nChars)) | |
| 764 | 754 | } | |
| 765 | 755 | ||
| 766 | - | // WidgetWaitForDraw ensures widget operations are complete (GTK test pattern) | |
| 767 | - | // This replaces the problematic IdleAdd pattern with proper widget synchronization | |
| 756 | + | // WidgetWaitForDraw is deprecated - GTK4 handles widget operations automatically | |
| 757 | + | // This function is kept for compatibility but does nothing in production | |
| 768 | 758 | func WidgetWaitForDraw(widget uintptr) { | |
| 769 | - | if widget == 0 { | |
| 759 | + | // GTK4 documentation states: never force a redraw | |
| 760 | + | // Widget operations are handled automatically by the GTK4 main loop | |
| 761 | + | // This function is a no-op in production code | |
| 762 | + | } | |
| 763 | + | ||
| 764 | + | // WidgetQueueDraw safely queues a redraw operation (GTK4 recommended approach) | |
| 765 | + | func WidgetQueueDraw(widget uintptr) { | |
| 766 | + | if widget == 0 || !widgetIsValid(widget) { | |
| 770 | 767 | return | |
| 771 | 768 | } | |
| 772 | - | if !widgetIsValid(widget) { | |
| 773 | - | log.Panicf("gtk_test_widget_wait_for_draw: widget=%#x invalid", widget) | |
| 769 | + | C.gtk_widget_queue_draw((*C.GtkWidget)(unsafe.Pointer(widget))) | |
| 770 | + | } | |
| 771 | + | ||
| 772 | + | // WidgetHasCSSClass checks if a widget has a specific CSS class | |
| 773 | + | func WidgetHasCSSClass(widget uintptr, class string) bool { | |
| 774 | + | if widget == 0 || class == "" || !widgetIsValid(widget) { | |
| 775 | + | return false | |
| 774 | 776 | } | |
| 775 | - | gtkTestInitOnce.Do(func() { | |
| 776 | - | C.workspace_gtk_test_init() | |
| 777 | - | }) | |
| 778 | - | C.gtk_test_widget_wait_for_draw((*C.GtkWidget)(unsafe.Pointer(widget))) | |
| 777 | + | cstr := C.CString(class) | |
| 778 | + | defer C.free(unsafe.Pointer(cstr)) | |
| 779 | + | result := C.gtk_widget_has_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr) | |
| 780 | + | return result != 0 | |
| 779 | 781 | } | |
| @@ -196,17 +196,7 @@ static void widget_show(GtkWidget* widget) { | |||||
| 196 | gtk_widget_set_visible(widget, TRUE); | 196 | gtk_widget_set_visible(widget, TRUE); | ||
| 197 | } | 197 | } | ||
| 198 | 198 | ||||
| 199 | - | static void workspace_gtk_test_init(void) { | 199 | + | // workspace_gtk_test_init removed - moved to test files only |
| 200 | - | // Create dummy argc/argv for GTK test initialization | |||
| 201 | - | // GTK test framework requires valid argc/argv pointers, not NULL | |||
| 202 | - | static int test_argc = 1; | |||
| 203 | - | static char test_prog[] = "dumber-test"; | |||
| 204 | - | static char* test_argv[] = { test_prog, NULL }; | |||
| 205 | - | static char** test_argv_ptr = test_argv; | |||
| 206 | - | ||||
| 207 | - | // Initialize GTK test framework with proper arguments | |||
| 208 | - | gtk_test_init(&test_argc, &test_argv_ptr, NULL); | |||
| 209 | - | } | |||
| 210 | 200 | ||||
| 211 | // Label helpers for title bars | 201 | // Label helpers for title bars | ||
| 212 | static GtkWidget* label_new(const char* text) { | 202 | static GtkWidget* label_new(const char* text) { | ||
| @@ -254,7 +244,7 @@ var ( | |||||
| 254 | hoverCallbacks = make(map[uintptr]func()) | 244 | hoverCallbacks = make(map[uintptr]func()) | ||
| 255 | hoverControllers = make(map[uintptr]uintptr) | 245 | hoverControllers = make(map[uintptr]uintptr) | ||
| 256 | nextHoverID uintptr = 1 | 246 | nextHoverID uintptr = 1 | ||
| 257 | - | gtkTestInitOnce sync.Once | 247 | + | // gtkTestInitOnce removed - no longer needed |
| 258 | ) | 248 | ) | ||
| 259 | 249 | ||||
| 260 | func widgetIsValid(widget uintptr) bool { | 250 | func widgetIsValid(widget uintptr) bool { | ||
| @@ -763,17 +753,29 @@ func LabelSetMaxWidthChars(label uintptr, nChars int) { | |||||
| 763 | C.label_set_max_width_chars((*C.GtkWidget)(unsafe.Pointer(label)), C.int(nChars)) | 753 | C.label_set_max_width_chars((*C.GtkWidget)(unsafe.Pointer(label)), C.int(nChars)) | ||
| 764 | } | 754 | } | ||
| 765 | 755 | ||||
| 766 | - | // WidgetWaitForDraw ensures widget operations are complete (GTK test pattern) | 756 | + | // WidgetWaitForDraw is deprecated - GTK4 handles widget operations automatically |
| 767 | - | // This replaces the problematic IdleAdd pattern with proper widget synchronization | 757 | + | // This function is kept for compatibility but does nothing in production |
| 768 | func WidgetWaitForDraw(widget uintptr) { | 758 | func WidgetWaitForDraw(widget uintptr) { | ||
| 769 | - | if widget == 0 { | 759 | + | // GTK4 documentation states: never force a redraw |
| 760 | + | // Widget operations are handled automatically by the GTK4 main loop | |||
| 761 | + | // This function is a no-op in production code | |||
| 762 | + | } | |||
| 763 | + | ||||
| 764 | + | // WidgetQueueDraw safely queues a redraw operation (GTK4 recommended approach) | |||
| 765 | + | func WidgetQueueDraw(widget uintptr) { | |||
| 766 | + | if widget == 0 || !widgetIsValid(widget) { | |||
| 770 | return | 767 | return | ||
| 771 | } | 768 | } | ||
| 772 | - | if !widgetIsValid(widget) { | 769 | + | C.gtk_widget_queue_draw((*C.GtkWidget)(unsafe.Pointer(widget))) |
| 773 | - | log.Panicf("gtk_test_widget_wait_for_draw: widget=%#x invalid", widget) | 770 | + | } |
| 771 | + | ||||
| 772 | + | // WidgetHasCSSClass checks if a widget has a specific CSS class | |||
| 773 | + | func WidgetHasCSSClass(widget uintptr, class string) bool { | |||
| 774 | + | if widget == 0 || class == "" || !widgetIsValid(widget) { | |||
| 775 | + | return false | |||
| 774 | } | 776 | } | ||
| 775 | - | gtkTestInitOnce.Do(func() { | 777 | + | cstr := C.CString(class) |
| 776 | - | C.workspace_gtk_test_init() | 778 | + | defer C.free(unsafe.Pointer(cstr)) |
| 777 | - | }) | 779 | + | result := C.gtk_widget_has_css_class((*C.GtkWidget)(unsafe.Pointer(widget)), cstr) |
| 778 | - | C.gtk_test_widget_wait_for_draw((*C.GtkWidget)(unsafe.Pointer(widget))) | 780 | + | return result != 0 |
| 779 | } | 781 | } | ||
M pkg/webkit/workspace_widgets_stub.go +9 -0
| @@ -361,6 +361,15 @@ func WidgetAddCSSClass(widget uintptr, class string) {} | |||
| 361 | 361 | ||
| 362 | 362 | func WidgetRemoveCSSClass(widget uintptr, class string) {} | |
| 363 | 363 | ||
| 364 | + | func WidgetHasCSSClass(widget uintptr, class string) bool { | |
| 365 | + | // In stub mode, we don't track CSS classes | |
| 366 | + | return false | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | func WidgetQueueDraw(widget uintptr) { | |
| 370 | + | // Stub implementation - no-op | |
| 371 | + | } | |
| 372 | + | ||
| 364 | 373 | // SimulateFocusForTesting gives focus to a widget for test scenarios | |
| 365 | 374 | func SimulateFocusForTesting(widget uintptr) { | |
| 366 | 375 | WidgetGrabFocus(widget) | |
| @@ -361,6 +361,15 @@ func WidgetAddCSSClass(widget uintptr, class string) {} | |||||
| 361 | 361 | ||||
| 362 | func WidgetRemoveCSSClass(widget uintptr, class string) {} | 362 | func WidgetRemoveCSSClass(widget uintptr, class string) {} | ||
| 363 | 363 | ||||
| 364 | + | func WidgetHasCSSClass(widget uintptr, class string) bool { | |||
| 365 | + | // In stub mode, we don't track CSS classes | |||
| 366 | + | return false | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | func WidgetQueueDraw(widget uintptr) { | |||
| 370 | + | // Stub implementation - no-op | |||
| 371 | + | } | |||
| 372 | + | ||||
| 364 | // SimulateFocusForTesting gives focus to a widget for test scenarios | 373 | // SimulateFocusForTesting gives focus to a widget for test scenarios | ||
| 365 | func SimulateFocusForTesting(widget uintptr) { | 374 | func SimulateFocusForTesting(widget uintptr) { | ||
| 366 | WidgetGrabFocus(widget) | 375 | WidgetGrabFocus(widget) | ||