detached 9964d39

fix: resolve GTK4 widget lifecycle and CSS focus management issues

## Major Fixes

### GTK4 Widget Unparenting Segfaults
- Fixed double-unparenting crashes in closePane() function
- Removed manual webkit.WidgetUnparent() calls after GTK4 auto-unparenting
- GTK4 automatically handles unparenting when setting paned children to 0
- Eliminated "sibling widget became invalid after unparenting" errors

### CSS Focus Management for Stacked Panes
- Fixed CSS class removal to include ALL panes in stacks, not just active one
- Added removeActiveCSSFromAllPanes() method for comprehensive cleanup
- Enhanced CSS rules to apply active borders to ALL pane types
- Added debug logging for CSS class changes

### Improvements
- CSS borders now work correctly for simple panes (Ctrl+P+R navigation)
- Stacked pane arrow key navigation still needs work (next phase)
- Comprehensive debug logging for troubleshooting

Resolves segfaults during pane closing operations and improves focus visualization.
commit
9964d39b3a4832d0845eb1ad71d66aefd59f0627
author
bamen · · 2025-09-25 16:33 UTC
committer
bamen · · 2025-09-25 17:12 UTC
parent
e7e2362
signature
unsigned
2 files · +448 -221
M internal/app/browser/focus_manager.go +99 -24
@@ -109,19 +109,22 @@ func (fm *FocusManager) setGTKFocus(node *paneNode) {
109109 func (fm *FocusManager) updateVisualState(newPane *paneNode) {
110110 activePaneClass := "workspace-pane-active"
111111
112- // ROBUST: Remove active class from ALL leaf panes to prevent multiple active states
113- // This ensures we never have a race condition or missed cleanup
114- leaves := fm.wm.collectLeaves()
115- for _, leaf := range leaves {
116- if leaf != nil && leaf.container != 0 {
117- webkit.WidgetRemoveCSSClass(leaf.container, activePaneClass)
118- }
119- }
112+ log.Printf("[focus-manager] updateVisualState: setting active pane to %p", newPane)
113+
114+ // CRITICAL: For stacked panes, we need to remove active class from ALL panes in ALL stacks
115+ // collectLeaves() only returns the currently active pane in each stack, missing the others
116+ fm.removeActiveCSSFromAllPanes(activePaneClass)
120117
121118 // Add active class to new pane only
122- if newPane != nil && newPane.container != 0 {
123- webkit.WidgetAddCSSClass(newPane.container, activePaneClass)
124- webkit.WidgetQueueDraw(newPane.container)
119+ if newPane != nil && newPane.container != nil {
120+ newPane.container.Execute(func(containerPtr uintptr) error {
121+ log.Printf("[focus-manager] Adding CSS class '%s' to new active pane: container=%#x", activePaneClass, containerPtr)
122+ webkit.WidgetAddCSSClass(containerPtr, activePaneClass)
123+ webkit.WidgetQueueDraw(containerPtr)
124+ return nil
125+ })
126+ } else {
127+ log.Printf("[focus-manager] WARNING: newPane is nil or has no container, cannot add active CSS class")
125128 }
126129
127130 // FATAL CHECK: Ensure only ONE pane has active class
@@ -131,6 +134,63 @@ func (fm *FocusManager) updateVisualState(newPane *paneNode) {
131134 fm.updateStackedPaneVisibility(newPane)
132135 }
133136
137+// removeActiveCSSFromAllPanes removes the active CSS class from ALL panes, including stacked panes
138+// This is critical for stacked panes where collectLeaves() only returns the active pane
139+// removeActiveCSSFromAllPanes removes the active CSS class from ALL panes, including stacked panes
140+// This is critical for stacked panes where collectLeaves() only returns the active pane
141+func (fm *FocusManager) removeActiveCSSFromAllPanes(activePaneClass string) {
142+ if fm.wm == nil || fm.wm.root == nil {
143+ return
144+ }
145+
146+ log.Printf("[focus-manager] removeActiveCSSFromAllPanes: starting CSS cleanup for class '%s'", activePaneClass)
147+
148+ // Walk the entire tree and remove active class from ALL leaf panes
149+ // This includes both regular panes and ALL panes within stacks
150+ var walkAndRemove func(*paneNode, int)
151+ walkAndRemove = func(n *paneNode, depth int) {
152+ const maxDepth = 50
153+ if n == nil || depth > maxDepth {
154+ return
155+ }
156+
157+ if n.isLeaf {
158+ // Regular leaf pane - remove active class
159+ if n.container != nil {
160+ n.container.Execute(func(containerPtr uintptr) error {
161+ log.Printf("[focus-manager] Removing CSS class '%s' from regular pane: container=%#x", activePaneClass, containerPtr)
162+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
163+ return nil
164+ })
165+ }
166+ return
167+ }
168+
169+ if n.isStacked && len(n.stackedPanes) > 0 {
170+ // CRITICAL: For stacked panes, remove active class from ALL panes in the stack
171+ // not just the currently active one (which is what collectLeaves() returns)
172+ log.Printf("[focus-manager] Processing stacked pane: activeIndex=%d stackSize=%d", n.activeStackIndex, len(n.stackedPanes))
173+ for i, stackedPane := range n.stackedPanes {
174+ if stackedPane != nil && stackedPane.container != nil {
175+ stackedPane.container.Execute(func(containerPtr uintptr) error {
176+ log.Printf("[focus-manager] Removing CSS class '%s' from stacked pane[%d]: container=%#x", activePaneClass, i, containerPtr)
177+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
178+ return nil
179+ })
180+ }
181+ }
182+ return
183+ }
184+
185+ // Handle regular split nodes
186+ walkAndRemove(n.left, depth+1)
187+ walkAndRemove(n.right, depth+1)
188+ }
189+
190+ walkAndRemove(fm.wm.root, 0)
191+ log.Printf("[focus-manager] removeActiveCSSFromAllPanes: completed CSS cleanup")
192+}
193+
134194 // updateStackedPaneVisibility handles special case for stacked panes
135195 func (fm *FocusManager) updateStackedPaneVisibility(node *paneNode) {
136196 // Find if this node is part of a stack
@@ -164,19 +224,31 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
164224 // Hide all panes in stack
165225 for i, pane := range stackNode.stackedPanes {
166226 if i != activeIndex {
167- webkit.WidgetSetVisible(pane.container, false)
168- webkit.WidgetSetVisible(pane.titleBar, true)
169- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active")
170- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")
227+ pane.container.Execute(func(containerPtr uintptr) error {
228+ webkit.WidgetSetVisible(containerPtr, false)
229+ webkit.WidgetRemoveCSSClass(containerPtr, "stacked-pane-active")
230+ webkit.WidgetAddCSSClass(containerPtr, "stacked-pane-collapsed")
231+ return nil
232+ })
233+ pane.titleBar.Execute(func(titleBarPtr uintptr) error {
234+ webkit.WidgetSetVisible(titleBarPtr, true)
235+ return nil
236+ })
171237 }
172238 }
173239
174240 // Show active pane
175241 activePaneNode := stackNode.stackedPanes[activeIndex]
176- webkit.WidgetSetVisible(activePaneNode.container, true)
177- webkit.WidgetSetVisible(activePaneNode.titleBar, false)
178- webkit.WidgetAddCSSClass(activePaneNode.container, "stacked-pane-active")
179- webkit.WidgetRemoveCSSClass(activePaneNode.container, "stacked-pane-collapsed")
242+ activePaneNode.container.Execute(func(containerPtr uintptr) error {
243+ webkit.WidgetSetVisible(containerPtr, true)
244+ webkit.WidgetAddCSSClass(containerPtr, "stacked-pane-active")
245+ webkit.WidgetRemoveCSSClass(containerPtr, "stacked-pane-collapsed")
246+ return nil
247+ })
248+ activePaneNode.titleBar.Execute(func(titleBarPtr uintptr) error {
249+ webkit.WidgetSetVisible(titleBarPtr, false)
250+ return nil
251+ })
180252 }
181253
182254 // notifyJavaScript dispatches focus events to the JavaScript bridge
@@ -275,11 +347,14 @@ func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() {
275347 // Check all leaf panes in the workspace
276348 leaves := fm.wm.collectLeaves()
277349 for _, leaf := range leaves {
278- if leaf != nil && leaf.container != 0 {
279- if webkit.WidgetHasCSSClass(leaf.container, activePaneClass) {
280- activeCount++
281- activePanes = append(activePanes, leaf.container)
282- }
350+ if leaf != nil && leaf.container != nil {
351+ leaf.container.Execute(func(containerPtr uintptr) error {
352+ if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
353+ activeCount++
354+ activePanes = append(activePanes, containerPtr)
355+ }
356+ return nil
357+ })
283358 }
284359 }
285360
@@ -109,19 +109,22 @@ func (fm *FocusManager) setGTKFocus(node *paneNode) {
109 func (fm *FocusManager) updateVisualState(newPane *paneNode) {109 func (fm *FocusManager) updateVisualState(newPane *paneNode) {
110 activePaneClass := "workspace-pane-active"110 activePaneClass := "workspace-pane-active"
111 111
112- // ROBUST: Remove active class from ALL leaf panes to prevent multiple active states112+ log.Printf("[focus-manager] updateVisualState: setting active pane to %p", newPane)
113- // This ensures we never have a race condition or missed cleanup113+
114- leaves := fm.wm.collectLeaves()114+ // CRITICAL: For stacked panes, we need to remove active class from ALL panes in ALL stacks
115- for _, leaf := range leaves {115+ // collectLeaves() only returns the currently active pane in each stack, missing the others
116- if leaf != nil && leaf.container != 0 {116+ fm.removeActiveCSSFromAllPanes(activePaneClass)
117- webkit.WidgetRemoveCSSClass(leaf.container, activePaneClass)
118- }
119- }
120 117
121 // Add active class to new pane only118 // Add active class to new pane only
122- if newPane != nil && newPane.container != 0 {119+ if newPane != nil && newPane.container != nil {
123- webkit.WidgetAddCSSClass(newPane.container, activePaneClass)120+ newPane.container.Execute(func(containerPtr uintptr) error {
124- webkit.WidgetQueueDraw(newPane.container)121+ log.Printf("[focus-manager] Adding CSS class '%s' to new active pane: container=%#x", activePaneClass, containerPtr)
122+ webkit.WidgetAddCSSClass(containerPtr, activePaneClass)
123+ webkit.WidgetQueueDraw(containerPtr)
124+ return nil
125+ })
126+ } else {
127+ log.Printf("[focus-manager] WARNING: newPane is nil or has no container, cannot add active CSS class")
125 }128 }
126 129
127 // FATAL CHECK: Ensure only ONE pane has active class130 // FATAL CHECK: Ensure only ONE pane has active class
@@ -131,6 +134,63 @@ func (fm *FocusManager) updateVisualState(newPane *paneNode) {
131 fm.updateStackedPaneVisibility(newPane)134 fm.updateStackedPaneVisibility(newPane)
132 }135 }
133 136
137+// removeActiveCSSFromAllPanes removes the active CSS class from ALL panes, including stacked panes
138+// This is critical for stacked panes where collectLeaves() only returns the active pane
139+// removeActiveCSSFromAllPanes removes the active CSS class from ALL panes, including stacked panes
140+// This is critical for stacked panes where collectLeaves() only returns the active pane
141+func (fm *FocusManager) removeActiveCSSFromAllPanes(activePaneClass string) {
142+ if fm.wm == nil || fm.wm.root == nil {
143+ return
144+ }
145+
146+ log.Printf("[focus-manager] removeActiveCSSFromAllPanes: starting CSS cleanup for class '%s'", activePaneClass)
147+
148+ // Walk the entire tree and remove active class from ALL leaf panes
149+ // This includes both regular panes and ALL panes within stacks
150+ var walkAndRemove func(*paneNode, int)
151+ walkAndRemove = func(n *paneNode, depth int) {
152+ const maxDepth = 50
153+ if n == nil || depth > maxDepth {
154+ return
155+ }
156+
157+ if n.isLeaf {
158+ // Regular leaf pane - remove active class
159+ if n.container != nil {
160+ n.container.Execute(func(containerPtr uintptr) error {
161+ log.Printf("[focus-manager] Removing CSS class '%s' from regular pane: container=%#x", activePaneClass, containerPtr)
162+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
163+ return nil
164+ })
165+ }
166+ return
167+ }
168+
169+ if n.isStacked && len(n.stackedPanes) > 0 {
170+ // CRITICAL: For stacked panes, remove active class from ALL panes in the stack
171+ // not just the currently active one (which is what collectLeaves() returns)
172+ log.Printf("[focus-manager] Processing stacked pane: activeIndex=%d stackSize=%d", n.activeStackIndex, len(n.stackedPanes))
173+ for i, stackedPane := range n.stackedPanes {
174+ if stackedPane != nil && stackedPane.container != nil {
175+ stackedPane.container.Execute(func(containerPtr uintptr) error {
176+ log.Printf("[focus-manager] Removing CSS class '%s' from stacked pane[%d]: container=%#x", activePaneClass, i, containerPtr)
177+ webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass)
178+ return nil
179+ })
180+ }
181+ }
182+ return
183+ }
184+
185+ // Handle regular split nodes
186+ walkAndRemove(n.left, depth+1)
187+ walkAndRemove(n.right, depth+1)
188+ }
189+
190+ walkAndRemove(fm.wm.root, 0)
191+ log.Printf("[focus-manager] removeActiveCSSFromAllPanes: completed CSS cleanup")
192+}
193+
134 // updateStackedPaneVisibility handles special case for stacked panes194 // updateStackedPaneVisibility handles special case for stacked panes
135 func (fm *FocusManager) updateStackedPaneVisibility(node *paneNode) {195 func (fm *FocusManager) updateStackedPaneVisibility(node *paneNode) {
136 // Find if this node is part of a stack196 // Find if this node is part of a stack
@@ -164,19 +224,31 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
164 // Hide all panes in stack224 // Hide all panes in stack
165 for i, pane := range stackNode.stackedPanes {225 for i, pane := range stackNode.stackedPanes {
166 if i != activeIndex {226 if i != activeIndex {
167- webkit.WidgetSetVisible(pane.container, false)227+ pane.container.Execute(func(containerPtr uintptr) error {
168- webkit.WidgetSetVisible(pane.titleBar, true)228+ webkit.WidgetSetVisible(containerPtr, false)
169- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active")229+ webkit.WidgetRemoveCSSClass(containerPtr, "stacked-pane-active")
170- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")230+ webkit.WidgetAddCSSClass(containerPtr, "stacked-pane-collapsed")
231+ return nil
232+ })
233+ pane.titleBar.Execute(func(titleBarPtr uintptr) error {
234+ webkit.WidgetSetVisible(titleBarPtr, true)
235+ return nil
236+ })
171 }237 }
172 }238 }
173 239
174 // Show active pane240 // Show active pane
175 activePaneNode := stackNode.stackedPanes[activeIndex]241 activePaneNode := stackNode.stackedPanes[activeIndex]
176- webkit.WidgetSetVisible(activePaneNode.container, true)242+ activePaneNode.container.Execute(func(containerPtr uintptr) error {
177- webkit.WidgetSetVisible(activePaneNode.titleBar, false)243+ webkit.WidgetSetVisible(containerPtr, true)
178- webkit.WidgetAddCSSClass(activePaneNode.container, "stacked-pane-active")244+ webkit.WidgetAddCSSClass(containerPtr, "stacked-pane-active")
179- webkit.WidgetRemoveCSSClass(activePaneNode.container, "stacked-pane-collapsed")245+ webkit.WidgetRemoveCSSClass(containerPtr, "stacked-pane-collapsed")
246+ return nil
247+ })
248+ activePaneNode.titleBar.Execute(func(titleBarPtr uintptr) error {
249+ webkit.WidgetSetVisible(titleBarPtr, false)
250+ return nil
251+ })
180 }252 }
181 253
182 // notifyJavaScript dispatches focus events to the JavaScript bridge254 // notifyJavaScript dispatches focus events to the JavaScript bridge
@@ -275,11 +347,14 @@ func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() {
275 // Check all leaf panes in the workspace347 // Check all leaf panes in the workspace
276 leaves := fm.wm.collectLeaves()348 leaves := fm.wm.collectLeaves()
277 for _, leaf := range leaves {349 for _, leaf := range leaves {
278- if leaf != nil && leaf.container != 0 {350+ if leaf != nil && leaf.container != nil {
279- if webkit.WidgetHasCSSClass(leaf.container, activePaneClass) {351+ leaf.container.Execute(func(containerPtr uintptr) error {
280- activeCount++352+ if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
281- activePanes = append(activePanes, leaf.container)353+ activeCount++
282- }354+ activePanes = append(activePanes, containerPtr)
355+ }
356+ return nil
357+ })
283 }358 }
284 }359 }
285 360
M internal/app/browser/workspace_manager.go +349 -197
@@ -19,10 +19,13 @@ type paneNode struct {
1919 parent *paneNode
2020 left *paneNode
2121 right *paneNode
22- container uintptr // GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, stable WebView container for leaves
23- orientation webkit.Orientation
24- isLeaf bool
25- isPopup bool // Deprecated: use windowType instead
22+
23+ // Widget management with proper lifecycle tracking
24+ container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)
25+ orientation webkit.Orientation
26+ isLeaf bool
27+ isPopup bool // Deprecated: use windowType instead
28+
2629 // Window type tracking
2730 windowType webkit.WindowType // Tab or Popup
2831 windowFeatures *webkit.WindowFeatures // Features if popup
@@ -30,12 +33,13 @@ type paneNode struct {
3033 parentPane *paneNode // Parent for related views
3134 autoClose bool // Auto-close on OAuth success
3235 hoverToken uintptr
33- // Stacked panes support
36+
37+ // Stacked panes support with proper widget management
3438 isStacked bool // Whether this node contains stacked panes
3539 stackedPanes []*paneNode // List of stacked panes (if isStacked)
3640 activeStackIndex int // Index of currently visible pane in stack
37- titleBar uintptr // GtkBox for title bar (when collapsed)
38- stackWrapper uintptr // Internal GtkBox containing the actual stacked widgets (titles + webviews)
41+ titleBar *SafeWidget // GtkBox for title bar (when collapsed)
42+ stackWrapper *SafeWidget // Internal GtkBox containing the actual stacked widgets (titles + webviews)
3943 }
4044
4145 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -74,6 +78,10 @@ type WorkspaceManager struct {
7478
7579 // Current focused pane (Single Source of Truth for focus)
7680 currentlyFocused *paneNode
81+
82+ // Widget operation synchronization to prevent GTK race conditions
83+ widgetMutex sync.Mutex
84+ widgetRegistry *WidgetRegistry
7785 }
7886
7987 const (
@@ -92,6 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
92100 lastSplitMsg: make(map[*webkit.WebView]time.Time),
93101 lastExitMsg: make(map[*webkit.WebView]time.Time),
94102 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator
103+ widgetRegistry: NewWidgetRegistry(), // Initialize widget registry
95104 }
96105
97106 // Initialize specialized managers
@@ -120,10 +129,12 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
120129
121130 rootContainer := rootPane.webView.RootWidget()
122131 root := &paneNode{
123- pane: rootPane,
124- container: rootContainer,
125- isLeaf: true,
132+ pane: rootPane,
133+ isLeaf: true,
126134 }
135+ // Initialize widgets properly
136+ manager.initializePaneWidgets(root, rootContainer)
137+
127138 manager.root = root
128139 manager.mainPane = root
129140 manager.viewToNode[rootPane.webView] = root
@@ -485,6 +496,11 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
485496 border-color: %s;
486497 }
487498
499+ /* Active border for ALL workspace panes (including stacked panes) */
500+ .workspace-pane.workspace-pane-active {
501+ border-color: %s !important;
502+ }
503+
488504 /* Stacked panes styling */
489505 .stacked-pane-container {
490506 background-color: %s;
@@ -526,6 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
526542 inactiveBorderColor,
527543 styling.BorderRadius,
528544 styling.BorderColor,
545+ styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color
529546 // Additional parameters for stacked pane styles
530547 windowBackgroundColor, // stacked-pane-container background
531548 styling.BorderRadius, // stacked-pane-container border-radius
@@ -564,13 +581,16 @@ func (wm *WorkspaceManager) ensurePaneBaseClasses() {
564581
565582 leaves := wm.collectLeaves()
566583 for _, leaf := range leaves {
567- if leaf != nil && leaf.container != 0 {
568- webkit.WidgetAddCSSClass(leaf.container, basePaneClass)
569- if wm.hasMultiplePanes() {
570- webkit.WidgetAddCSSClass(leaf.container, multiPaneClass)
571- } else {
572- webkit.WidgetRemoveCSSClass(leaf.container, multiPaneClass)
573- }
584+ if leaf != nil && leaf.container != nil {
585+ leaf.container.Execute(func(containerPtr uintptr) error {
586+ webkit.WidgetAddCSSClass(containerPtr, basePaneClass)
587+ if wm.hasMultiplePanes() {
588+ webkit.WidgetAddCSSClass(containerPtr, multiPaneClass)
589+ } else {
590+ webkit.WidgetRemoveCSSClass(containerPtr, multiPaneClass)
591+ }
592+ return nil
593+ })
574594 }
575595 }
576596 }
@@ -601,20 +621,24 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) {
601621 if wm == nil || node == nil || !node.isLeaf {
602622 return
603623 }
604- if node.container == 0 || node.hoverToken != 0 {
624+ if node.container == nil || node.hoverToken != 0 {
605625 return
606626 }
607627
608- token := webkit.WidgetAddHoverHandler(node.container, func() {
609- if wm == nil {
610- return
611- }
612- wm.focusManager.SetActivePane(node)
613- wm.currentlyFocused = node
628+ var token uintptr
629+ node.container.Execute(func(containerPtr uintptr) error {
630+ token = webkit.WidgetAddHoverHandler(containerPtr, func() {
631+ if wm == nil {
632+ return
633+ }
634+ wm.focusManager.SetActivePane(node)
635+ wm.currentlyFocused = node
636+ })
637+ return nil
614638 })
615639 node.hoverToken = token
616640 if token == 0 {
617- log.Printf("[workspace] failed to attach hover handler: widget=%#x", node.container)
641+ log.Printf("[workspace] failed to attach hover handler")
618642 }
619643 }
620644
@@ -622,7 +646,10 @@ func (wm *WorkspaceManager) detachHover(node *paneNode) {
622646 if wm == nil || node == nil || node.hoverToken == 0 {
623647 return
624648 }
625- webkit.WidgetRemoveHoverHandler(node.container, node.hoverToken)
649+ node.container.Execute(func(containerPtr uintptr) error {
650+ webkit.WidgetRemoveHoverHandler(containerPtr, node.hoverToken)
651+ return nil
652+ })
626653 node.hoverToken = 0
627654 }
628655
@@ -724,7 +751,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
724751 const focusEpsilon = 1e-3
725752
726753 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
727- if wm.currentlyFocused == nil || !wm.currentlyFocused.isLeaf || wm.currentlyFocused.container == 0 {
754+ if wm.currentlyFocused == nil || !wm.currentlyFocused.isLeaf || wm.currentlyFocused.container == nil {
728755 return false
729756 }
730757
@@ -734,7 +761,12 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
734761 return true
735762 }
736763
737- currentBounds, ok := webkit.WidgetGetBounds(wm.currentlyFocused.container)
764+ var currentBounds webkit.WidgetBounds
765+ var ok bool
766+ wm.currentlyFocused.container.Execute(func(containerPtr uintptr) error {
767+ currentBounds, ok = webkit.WidgetGetBounds(containerPtr)
768+ return nil
769+ })
738770 if !ok {
739771 log.Printf("[workspace] unable to compute bounds for active pane")
740772 return false
@@ -749,10 +781,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
749781 var debugCandidates []string
750782
751783 for _, candidate := range leaves {
752- if candidate == nil || candidate == wm.currentlyFocused || candidate.container == 0 {
784+ if candidate == nil || candidate == wm.currentlyFocused || candidate.container == nil {
753785 continue
754786 }
755- bounds, ok := webkit.WidgetGetBounds(candidate.container)
787+ var bounds webkit.WidgetBounds
788+ var boundsOk bool
789+ candidate.container.Execute(func(containerPtr uintptr) error {
790+ bounds, boundsOk = webkit.WidgetGetBounds(containerPtr)
791+ return nil
792+ })
793+ ok := boundsOk
756794 if !ok {
757795 continue
758796 }
@@ -809,11 +847,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
809847 }
810848
811849 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {
812- if node == nil || node.container == 0 {
850+ if node == nil || node.container == nil {
813851 return nil
814852 }
815853
816- refBounds, ok := webkit.WidgetGetBounds(node.container)
854+ var refBounds webkit.WidgetBounds
855+ var ok bool
856+ node.container.Execute(func(containerPtr uintptr) error {
857+ refBounds, ok = webkit.WidgetGetBounds(containerPtr)
858+ return nil
859+ })
817860 if !ok {
818861 return nil
819862 }
@@ -858,10 +901,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6
858901 bestScore := math.MaxFloat64
859902 var best *paneNode
860903 for _, leaf := range leaves {
861- if leaf == nil || leaf.container == 0 {
904+ if leaf == nil || leaf.container == nil {
862905 continue
863906 }
864- bounds, ok := webkit.WidgetGetBounds(leaf.container)
907+ var bounds webkit.WidgetBounds
908+ var boundsOk bool
909+ leaf.container.Execute(func(containerPtr uintptr) error {
910+ bounds, boundsOk = webkit.WidgetGetBounds(containerPtr)
911+ return nil
912+ })
913+ ok := boundsOk
865914 if !ok {
866915 continue
867916 }
@@ -1026,18 +1075,18 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
10261075
10271076 // Determine what we're actually splitting
10281077 var splitTarget *paneNode
1029- var splitTargetContainer uintptr
1078+ var splitTargetContainer *SafeWidget
10301079
10311080 if stackContainer != nil {
10321081 // Case: splitting from inside a stack - we split around the entire stack
10331082 splitTarget = stackContainer
10341083 splitTargetContainer = stackContainer.container
1035- log.Printf("[workspace] splitting around stack: stackContainer=%p container=%#x", splitTarget, splitTargetContainer)
1084+ log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget)
10361085 } else {
10371086 // Case: normal split from a simple pane
10381087 splitTarget = target
10391088 splitTargetContainer = target.container
1040- log.Printf("[workspace] normal split from pane: target=%p container=%#x", splitTarget, splitTargetContainer)
1089+ log.Printf("[workspace] normal split from pane: target=%p", splitTarget)
10411090 }
10421091
10431092 newView, err := wm.createWebView()
@@ -1063,22 +1112,28 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
10631112 webkit.WidgetRealizeInContainer(newContainer)
10641113
10651114 // Use the determined split target container
1066- if splitTargetContainer == 0 {
1115+ if splitTargetContainer == nil {
10671116 return nil, errors.New("split target missing container")
10681117 }
10691118
10701119 // Only apply GTK operations if this is a simple widget (leaf pane)
10711120 // For stacked containers, we need to be more careful
10721121 if splitTarget.isLeaf {
1073- webkit.WidgetSetHExpand(splitTargetContainer, true)
1074- webkit.WidgetSetVExpand(splitTargetContainer, true)
1075- webkit.WidgetRealizeInContainer(splitTargetContainer)
1122+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1123+ webkit.WidgetSetHExpand(containerPtr, true)
1124+ webkit.WidgetSetVExpand(containerPtr, true)
1125+ webkit.WidgetRealizeInContainer(containerPtr)
1126+ return nil
1127+ })
10761128 } else if splitTarget.isStacked {
10771129 // Stack containers need the same setup as regular panes for proper splitting
1078- webkit.WidgetSetHExpand(splitTargetContainer, true)
1079- webkit.WidgetSetVExpand(splitTargetContainer, true)
1080- webkit.WidgetRealizeInContainer(splitTargetContainer)
1081- log.Printf("[workspace] configured stack wrapper for split: %#x", splitTargetContainer)
1130+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1131+ webkit.WidgetSetHExpand(containerPtr, true)
1132+ webkit.WidgetSetVExpand(containerPtr, true)
1133+ webkit.WidgetRealizeInContainer(containerPtr)
1134+ log.Printf("[workspace] configured stack wrapper for split: %#x", containerPtr)
1135+ return nil
1136+ })
10821137 }
10831138
10841139 orientation, existingFirst := mapDirection(direction)
@@ -1095,16 +1150,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
10951150 webkit.PanedSetResizeEnd(paned, true)
10961151
10971152 newLeaf := &paneNode{
1098- pane: newPane,
1099- container: newContainer,
1100- isLeaf: true,
1153+ pane: newPane,
1154+ isLeaf: true,
11011155 }
1156+ wm.initializePaneWidgets(newLeaf, newContainer)
1157+
11021158 split := &paneNode{
11031159 parent: splitTarget.parent,
1104- container: paned,
11051160 orientation: orientation,
11061161 isLeaf: false,
11071162 }
1163+ wm.initializePaneWidgets(split, paned)
11081164
11091165 parent := split.parent
11101166
@@ -1116,19 +1172,25 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
11161172 wm.window.SetChild(0)
11171173 }
11181174 // For root widgets, only unparent if they actually have a GTK parent
1119- if webkit.WidgetGetParent(splitTargetContainer) != 0 {
1120- webkit.WidgetUnparent(splitTargetContainer)
1121- }
1122- } else if parent.container != 0 {
1175+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1176+ if webkit.WidgetGetParent(containerPtr) != 0 {
1177+ webkit.WidgetUnparent(containerPtr)
1178+ }
1179+ return nil
1180+ })
1181+ } else if parent.container != nil {
11231182 // Split target has a parent paned - remove it (automatically unparents in GTK4)
11241183 log.Printf("[workspace] unparenting split target container=%#x from parent paned=%#x", splitTargetContainer, parent.container)
1125- if parent.left == splitTarget {
1126- webkit.PanedSetStartChild(parent.container, 0)
1127- } else if parent.right == splitTarget {
1128- webkit.PanedSetEndChild(parent.container, 0)
1129- }
1130- // GTK4 automatically unparents when we set paned child to 0 - no manual unparent needed
1131- webkit.WidgetQueueAllocate(parent.container)
1184+ parent.container.Execute(func(panedPtr uintptr) error {
1185+ if parent.left == splitTarget {
1186+ webkit.PanedSetStartChild(panedPtr, 0)
1187+ } else if parent.right == splitTarget {
1188+ webkit.PanedSetEndChild(panedPtr, 0)
1189+ }
1190+ // GTK4 automatically unparents when we set paned child to 0 - no manual unparent needed
1191+ webkit.WidgetQueueAllocate(panedPtr)
1192+ return nil
1193+ })
11321194 }
11331195
11341196 // Set up the tree structure first
@@ -1158,13 +1220,19 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
11581220
11591221 // Add both containers to the new paned
11601222 if existingFirst {
1161- webkit.PanedSetStartChild(paned, splitTargetContainer)
1162- webkit.PanedSetEndChild(paned, newContainer)
1163- log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer)
1223+ splitTargetContainer.Execute(func(splitPtr uintptr) error {
1224+ webkit.PanedSetStartChild(paned, splitPtr)
1225+ webkit.PanedSetEndChild(paned, newContainer)
1226+ log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitPtr, newContainer)
1227+ return nil
1228+ })
11641229 } else {
1165- webkit.PanedSetStartChild(paned, newContainer)
1166- webkit.PanedSetEndChild(paned, splitTargetContainer)
1167- log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer)
1230+ splitTargetContainer.Execute(func(splitPtr uintptr) error {
1231+ webkit.PanedSetStartChild(paned, newContainer)
1232+ webkit.PanedSetEndChild(paned, splitPtr)
1233+ log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitPtr)
1234+ return nil
1235+ })
11681236 }
11691237
11701238 // Attach the new paned to its parent
@@ -1175,14 +1243,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
11751243 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
11761244 }
11771245 } else {
1178- if parent.left == split {
1179- webkit.PanedSetStartChild(parent.container, paned)
1180- } else if parent.right == split {
1181- webkit.PanedSetEndChild(parent.container, paned)
1182- }
1183- webkit.WidgetQueueAllocate(parent.container)
1184- webkit.WidgetQueueAllocate(paned)
1185- log.Printf("[workspace] paned inserted into parent=%#x", parent.container)
1246+ parent.container.Execute(func(parentPtr uintptr) error {
1247+ if parent.left == split {
1248+ webkit.PanedSetStartChild(parentPtr, paned)
1249+ } else if parent.right == split {
1250+ webkit.PanedSetEndChild(parentPtr, paned)
1251+ }
1252+ webkit.WidgetQueueAllocate(parentPtr)
1253+ webkit.WidgetQueueAllocate(paned)
1254+ log.Printf("[workspace] paned inserted into parent=%#x", parentPtr)
1255+ return nil
1256+ })
11861257 }
11871258
11881259 webkit.WidgetShow(paned)
@@ -1278,19 +1349,25 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
12781349 return wm.stackedPaneManager.CloseStackedPane(node)
12791350 }
12801351
1281- if node.parent != nil && node.parent.container != 0 {
1282- if node.parent.left == node {
1283- webkit.PanedSetStartChild(node.parent.container, 0)
1284- // GTK4 automatically unparents when setting child to 0
1285- } else if node.parent.right == node {
1286- webkit.PanedSetEndChild(node.parent.container, 0)
1287- // GTK4 automatically unparents when setting child to 0
1288- }
1289- } else if node.container != 0 {
1352+ if node.parent != nil && node.parent.container != nil {
1353+ node.parent.container.Execute(func(panedPtr uintptr) error {
1354+ if node.parent.left == node {
1355+ webkit.PanedSetStartChild(panedPtr, 0)
1356+ // GTK4 automatically unparents when setting child to 0
1357+ } else if node.parent.right == node {
1358+ webkit.PanedSetEndChild(panedPtr, 0)
1359+ // GTK4 automatically unparents when setting child to 0
1360+ }
1361+ return nil
1362+ })
1363+ } else if node.container != nil {
12901364 // Only manually unparent if widget wasn't auto-unparented above
1291- if webkit.WidgetGetParent(node.container) != 0 {
1292- webkit.WidgetUnparent(node.container)
1293- }
1365+ node.container.Execute(func(containerPtr uintptr) error {
1366+ if webkit.WidgetGetParent(containerPtr) != 0 {
1367+ webkit.WidgetUnparent(containerPtr)
1368+ }
1369+ return nil
1370+ })
12941371 }
12951372
12961373 remaining := len(wm.app.panes)
@@ -1316,24 +1393,30 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
13161393 if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil {
13171394 if sibling.isPopup && !node.isPopup {
13181395 parentNode := node.parent
1319- parentContainer := uintptr(0)
1396+ var parentContainer *SafeWidget
13201397 if parentNode != nil {
13211398 parentContainer = parentNode.container
13221399 }
13231400
13241401 // Detach remaining GTK widgets before destroying the WebViews so GTK
13251402 // does not attempt to dispose still-parented children.
1326- if parentContainer != 0 {
1327- if parentNode.left == sibling {
1328- webkit.PanedSetStartChild(parentContainer, 0)
1329- } else if parentNode.right == sibling {
1330- webkit.PanedSetEndChild(parentContainer, 0)
1331- }
1403+ if parentContainer != nil {
1404+ parentContainer.Execute(func(panedPtr uintptr) error {
1405+ if parentNode.left == sibling {
1406+ webkit.PanedSetStartChild(panedPtr, 0)
1407+ } else if parentNode.right == sibling {
1408+ webkit.PanedSetEndChild(panedPtr, 0)
1409+ }
1410+ return nil
1411+ })
13321412 }
1333- if sibling.container != 0 {
1334- webkit.WidgetUnparent(sibling.container)
1413+ if sibling.container != nil {
1414+ sibling.container.Execute(func(containerPtr uintptr) error {
1415+ webkit.WidgetUnparent(containerPtr)
1416+ return nil
1417+ })
13351418 }
1336- if parentContainer != 0 && wm.window != nil {
1419+ if parentContainer != nil && wm.window != nil {
13371420 wm.window.SetChild(0)
13381421 }
13391422
@@ -1420,12 +1503,13 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
14201503 replacement.parent = nil
14211504
14221505 // Replace window child directly (GTK handles reparenting automatically)
1423- if wm.window != nil {
1424- wm.window.SetChild(replacement.container)
1425- if replacement.container != 0 {
1426- webkit.WidgetQueueAllocate(replacement.container)
1427- webkit.WidgetShow(replacement.container)
1428- }
1506+ if wm.window != nil && replacement.container != nil {
1507+ replacement.container.Execute(func(containerPtr uintptr) error {
1508+ wm.window.SetChild(containerPtr)
1509+ webkit.WidgetQueueAllocate(containerPtr)
1510+ webkit.WidgetShow(containerPtr)
1511+ return nil
1512+ })
14291513 }
14301514
14311515 // Focus a suitable pane
@@ -1474,12 +1558,6 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
14741558
14751559 log.Printf("[workspace] closing pane: target=%#x parent=%#x sibling=%#x remaining=%d", node.container, parent.container, sibling.container, remaining)
14761560
1477- // Find focus target before modifying the tree structure
1478- focusTarget := wm.leftmostLeaf(sibling)
1479- if focusTarget == nil {
1480- focusTarget = wm.leftmostLeaf(wm.root)
1481- }
1482-
14831561 // Clean up workspace tracking
14841562 node.pane.CleanupFromWorkspace(wm)
14851563
@@ -1487,97 +1565,71 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
14871565 wm.mainPane = nil
14881566 }
14891567
1490- // Clear current active if it's the node being closed and immediately set focus to target
1568+ // Clear current active if it's the node being closed
14911569 if wm.currentlyFocused == node {
1492- wm.currentlyFocused = focusTarget
1493- if focusTarget != nil {
1494- wm.focusManager.SetActivePane(focusTarget)
1495- }
1570+ wm.currentlyFocused = nil
14961571 }
14971572
14981573 grand := parent.parent
14991574 if grand == nil {
15001575 // Parent is the root node. Promote sibling to become the new root.
1501- // The sibling can be either a leaf (when only 2 panes total) or a branch (when more panes exist)
15021576 log.Printf("[workspace] promoting sibling to root: container=%#x, isLeaf=%v", sibling.container, sibling.isLeaf)
15031577
1504- // CRITICAL: Check widget validity before any GTK operations to prevent segfault
1505- if !webkit.WidgetIsValid(sibling.container) {
1506- log.Printf("[workspace] ERROR: sibling container %#x is not a valid GTK widget, skipping root promotion", sibling.container)
1507- return errors.New("sibling container is not a valid GTK widget")
1508- }
1509-
1578+ // Update tree structure first
15101579 wm.root = sibling
15111580 sibling.parent = nil
15121581
1513- // Unparent the sibling from the paned first, then set it as window child
1514- if wm.window != nil {
1515- // GTK requires explicit unparenting before reparenting
1516- if parent.container != 0 && webkit.WidgetIsValid(parent.container) {
1517- if parent.left == sibling {
1518- webkit.PanedSetStartChild(parent.container, 0)
1519- } else {
1520- webkit.PanedSetEndChild(parent.container, 0)
1521- }
1522- }
1523- // Now we can safely set it as window child (widget validity already checked above)
1524- wm.window.SetChild(sibling.container)
1525- webkit.WidgetQueueAllocate(sibling.container)
1582+ // For root promotion, GTK4 handles unparenting automatically when we set the new window child
1583+ if wm.window != nil && sibling.container != nil {
1584+ sibling.container.Execute(func(containerPtr uintptr) error {
1585+ wm.window.SetChild(containerPtr)
1586+ webkit.WidgetQueueAllocate(containerPtr)
1587+ webkit.WidgetShow(containerPtr)
1588+ log.Printf("[workspace] successfully promoted sibling to root: %#x", containerPtr)
1589+ return nil
1590+ })
15261591 }
15271592 } else {
15281593 // Parent has a grandparent, so promote sibling to take parent's place
15291594 log.Printf("[workspace] promoting sibling to parent's position: sibling=%#x grand=%#x", sibling.container, grand.container)
15301595
1531- // CRITICAL: Check widget validity before any GTK operations to prevent segfault
1532- if !webkit.WidgetIsValid(sibling.container) {
1533- log.Printf("[workspace] ERROR: sibling container %#x is not a valid GTK widget, skipping promotion", sibling.container)
1534- return errors.New("sibling container is not a valid GTK widget")
1535- }
1536- if !webkit.WidgetIsValid(grand.container) {
1537- log.Printf("[workspace] ERROR: grand container %#x is not a valid GTK widget, skipping promotion", grand.container)
1538- return errors.New("grand container is not a valid GTK widget")
1539- }
1540-
1541- // First unparent the sibling from its current parent to avoid GTK-CRITICAL errors
1542- if parent.container != 0 && !parent.isLeaf && webkit.WidgetIsValid(parent.container) {
1543- if parent.left == sibling {
1544- webkit.PanedSetStartChild(parent.container, 0)
1545- } else if parent.right == sibling {
1546- webkit.PanedSetEndChild(parent.container, 0)
1547- }
1548- }
1549-
1550- // Now safely reparent the sibling to the grandparent
1551- // GTK4 automatically handles focus state synchronization during reparenting
1552-
1596+ // Update tree structure
15531597 sibling.parent = grand
1554- if grand.container != 0 && !grand.isLeaf && webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {
1555- if grand.left == parent {
1556- grand.left = sibling
1557- webkit.PanedSetStartChild(grand.container, sibling.container)
1558- } else if grand.right == parent {
1559- grand.right = sibling
1560- webkit.PanedSetEndChild(grand.container, sibling.container)
1561- }
1562- webkit.WidgetQueueAllocate(grand.container)
1598+ if grand.left == parent {
1599+ grand.left = sibling
1600+ } else {
1601+ grand.right = sibling
15631602 }
1564- }
15651603
1566- // Keep sibling subtree visible
1567- if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {
1568- webkit.WidgetShow(sibling.container)
1604+ // Move sibling to grandparent's paned widget
1605+ // GTK4 handles unparenting automatically when we set the new paned child
1606+ grand.container.Execute(func(grandPtr uintptr) error {
1607+ sibling.container.Execute(func(siblingPtr uintptr) error {
1608+ if grand.left == sibling {
1609+ webkit.PanedSetStartChild(grandPtr, siblingPtr)
1610+ } else {
1611+ webkit.PanedSetEndChild(grandPtr, siblingPtr)
1612+ }
1613+ webkit.WidgetQueueAllocate(siblingPtr)
1614+ webkit.WidgetShow(siblingPtr)
1615+ log.Printf("[workspace] successfully promoted sibling to parent position: %#x", siblingPtr)
1616+ return nil
1617+ })
1618+ return nil
1619+ })
15691620 }
15701621
1571- // Clean up parent node references
1572- parent.left = nil
1573- parent.right = nil
1574- node.parent = nil
1575-
1576- // Focus handling was moved earlier to avoid state gaps
1622+ // Find a suitable focus target
1623+ focusTarget := wm.leftmostLeaf(sibling)
1624+ if focusTarget != nil {
1625+ wm.currentlyFocused = focusTarget
1626+ wm.focusManager.SetActivePane(focusTarget)
1627+ }
15771628
1578- // Destroy the webview and detach hover AFTER all hierarchy changes are complete
1629+ // Clean up the node being closed
15791630 ensureCleanup()
15801631 wm.detachHover(node)
1632+
15811633 switch {
15821634 case node.isPopup:
15831635 if err := node.pane.webView.Destroy(); err != nil {
@@ -1612,6 +1664,91 @@ func (wm *WorkspaceManager) leftmostLeaf(node *paneNode) *paneNode {
16121664 return node
16131665 }
16141666
1667+// validateWidgetsForReparenting validates that all widgets needed for reparenting are valid
1668+func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error {
1669+ // Validate sibling container (this is the most critical one)
1670+ if sibling.container == nil {
1671+ return errors.New("sibling container is nil")
1672+ }
1673+ if !sibling.container.IsValid() {
1674+ return fmt.Errorf("sibling container %s is not valid", sibling.container.String())
1675+ }
1676+
1677+ // Validate parent container if present
1678+ if parent != nil && parent.container != nil && !parent.container.IsValid() {
1679+ return fmt.Errorf("parent container %s is not valid", parent.container.String())
1680+ }
1681+
1682+ // Validate grandparent container if present
1683+ if grand != nil && grand.container != nil && !grand.container.IsValid() {
1684+ return fmt.Errorf("grandparent container %s is not valid", grand.container.String())
1685+ }
1686+
1687+ return nil
1688+}
1689+
1690+// safeWidgetOperation performs a widget operation with proper locking and validation
1691+func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error {
1692+ wm.widgetMutex.Lock()
1693+ defer wm.widgetMutex.Unlock()
1694+ return operation()
1695+}
1696+
1697+// registerWidget registers a widget with the registry and returns a SafeWidget
1698+func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget {
1699+ if ptr == 0 {
1700+ return nil
1701+ }
1702+ return wm.widgetRegistry.Register(ptr, typeInfo)
1703+}
1704+
1705+// initializePaneWidgets sets up SafeWidget wrappers for a paneNode
1706+func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr uintptr) {
1707+ // Register the main container
1708+ node.container = wm.registerWidget(containerPtr, "pane-container")
1709+
1710+ // Initialize other widget fields as nil (will be set when needed)
1711+ node.titleBar = nil
1712+ node.stackWrapper = nil
1713+}
1714+
1715+// Helper functions for safe widget operations
1716+func (node *paneNode) getContainerPtr() uintptr {
1717+ if node.container != nil {
1718+ return node.container.Ptr()
1719+ }
1720+ return 0
1721+}
1722+
1723+func (node *paneNode) getTitleBarPtr() uintptr {
1724+ if node.titleBar != nil {
1725+ return node.titleBar.Ptr()
1726+ }
1727+ return 0
1728+}
1729+
1730+func (node *paneNode) getStackWrapperPtr() uintptr {
1731+ if node.stackWrapper != nil {
1732+ return node.stackWrapper.Ptr()
1733+ }
1734+ return 0
1735+}
1736+
1737+// setContainer sets the SafeWidget container
1738+func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {
1739+ node.container = wm.registerWidget(ptr, typeInfo)
1740+}
1741+
1742+// setTitleBar sets the SafeWidget titleBar
1743+func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) {
1744+ node.titleBar = wm.registerWidget(ptr, "title-bar")
1745+}
1746+
1747+// setStackWrapper sets the SafeWidget stackWrapper
1748+func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {
1749+ node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper")
1750+}
1751+
16151752 // findReplacementRoot finds a suitable replacement when closing the current root pane
16161753 func (wm *WorkspaceManager) findReplacementRoot(excludeNode *paneNode) *paneNode {
16171754 if wm == nil || wm.root == nil {
@@ -2302,8 +2439,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
23022439 webkit.WidgetRealizeInContainer(webViewWidget)
23032440 }
23042441
2305- existingContainer := target.container
2306- if existingContainer == 0 {
2442+ if target.container == nil {
23072443 return errors.New("existing pane missing container")
23082444 }
23092445
@@ -2320,35 +2456,44 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
23202456 webkit.PanedSetResizeEnd(paned, true)
23212457
23222458 newLeaf := &paneNode{
2323- pane: newPane,
2324- container: newContainer,
2325- isLeaf: true,
2459+ pane: newPane,
2460+ isLeaf: true,
23262461 }
2462+ wm.initializePaneWidgets(newLeaf, newContainer)
2463+
23272464 split := &paneNode{
23282465 parent: target.parent,
2329- container: paned,
23302466 orientation: orientation,
23312467 isLeaf: false,
23322468 }
2469+ wm.initializePaneWidgets(split, paned)
23332470
23342471 parent := split.parent
23352472
23362473 // Detach existing container from its current GTK parent before inserting into new paned.
2474+ existingContainer := target.getContainerPtr()
23372475 if parent == nil {
23382476 // Target is the root - remove it from the window
23392477 log.Printf("[workspace] removing existing container=%#x from window", existingContainer)
23402478 if wm.window != nil {
23412479 wm.window.SetChild(0)
23422480 }
2343- } else if parent.container != 0 {
2481+ } else if parent.container != nil {
23442482 // Target has a parent paned - unparent it from there
2345- log.Printf("[workspace] unparenting existing container=%#x from parent paned=%#x", existingContainer, parent.container)
2346- if parent.left == target {
2347- webkit.PanedSetStartChild(parent.container, 0)
2348- } else if parent.right == target {
2349- webkit.PanedSetEndChild(parent.container, 0)
2483+ log.Printf("[workspace] unparenting existing container from parent paned")
2484+ err := parent.container.Execute(func(parentPtr uintptr) error {
2485+ log.Printf("[workspace] executing unparent: parentPtr=%#x", parentPtr)
2486+ if parent.left == target {
2487+ webkit.PanedSetStartChild(parentPtr, 0)
2488+ } else if parent.right == target {
2489+ webkit.PanedSetEndChild(parentPtr, 0)
2490+ }
2491+ webkit.WidgetQueueAllocate(parentPtr)
2492+ return nil
2493+ })
2494+ if err != nil {
2495+ log.Printf("[workspace] error during parent unparent: %v", err)
23502496 }
2351- webkit.WidgetQueueAllocate(parent.container)
23522497 }
23532498
23542499 // Add both containers to the new paned
@@ -2377,16 +2522,23 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
23772522 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
23782523 }
23792524 } else {
2380- if parent.left == target {
2381- parent.left = split
2382- webkit.PanedSetStartChild(parent.container, paned)
2383- } else if parent.right == target {
2384- parent.right = split
2385- webkit.PanedSetEndChild(parent.container, paned)
2525+ err := parent.container.Execute(func(parentPtr uintptr) error {
2526+ log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parentPtr, paned)
2527+ if parent.left == target {
2528+ parent.left = split
2529+ webkit.PanedSetStartChild(parentPtr, paned)
2530+ } else if parent.right == target {
2531+ parent.right = split
2532+ webkit.PanedSetEndChild(parentPtr, paned)
2533+ }
2534+ webkit.WidgetQueueAllocate(parentPtr)
2535+ webkit.WidgetQueueAllocate(paned)
2536+ log.Printf("[workspace] paned inserted into parent successfully")
2537+ return nil
2538+ })
2539+ if err != nil {
2540+ log.Printf("[workspace] error during parent reparent: %v", err)
23862541 }
2387- webkit.WidgetQueueAllocate(parent.container)
2388- webkit.WidgetQueueAllocate(paned)
2389- log.Printf("[workspace] paned inserted into parent=%#x", parent.container)
23902542 }
23912543
23922544 webkit.WidgetShow(paned)
@@ -19,10 +19,13 @@ type paneNode struct {
19 parent *paneNode19 parent *paneNode
20 left *paneNode20 left *paneNode
21 right *paneNode21 right *paneNode
22- container uintptr // GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, stable WebView container for leaves22+
23- orientation webkit.Orientation23+ // Widget management with proper lifecycle tracking
24- isLeaf bool24+ container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)
25- isPopup bool // Deprecated: use windowType instead25+ orientation webkit.Orientation
26+ isLeaf bool
27+ isPopup bool // Deprecated: use windowType instead
28+
26 // Window type tracking29 // Window type tracking
27 windowType webkit.WindowType // Tab or Popup30 windowType webkit.WindowType // Tab or Popup
28 windowFeatures *webkit.WindowFeatures // Features if popup31 windowFeatures *webkit.WindowFeatures // Features if popup
@@ -30,12 +33,13 @@ type paneNode struct {
30 parentPane *paneNode // Parent for related views33 parentPane *paneNode // Parent for related views
31 autoClose bool // Auto-close on OAuth success34 autoClose bool // Auto-close on OAuth success
32 hoverToken uintptr35 hoverToken uintptr
33- // Stacked panes support36+
37+ // Stacked panes support with proper widget management
34 isStacked bool // Whether this node contains stacked panes38 isStacked bool // Whether this node contains stacked panes
35 stackedPanes []*paneNode // List of stacked panes (if isStacked)39 stackedPanes []*paneNode // List of stacked panes (if isStacked)
36 activeStackIndex int // Index of currently visible pane in stack40 activeStackIndex int // Index of currently visible pane in stack
37- titleBar uintptr // GtkBox for title bar (when collapsed)41+ titleBar *SafeWidget // GtkBox for title bar (when collapsed)
38- stackWrapper uintptr // Internal GtkBox containing the actual stacked widgets (titles + webviews)42+ stackWrapper *SafeWidget // Internal GtkBox containing the actual stacked widgets (titles + webviews)
39 }43 }
40 44
41 // WorkspaceManager coordinates Zellij-style pane operations.45 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -74,6 +78,10 @@ type WorkspaceManager struct {
74 78
75 // Current focused pane (Single Source of Truth for focus)79 // Current focused pane (Single Source of Truth for focus)
76 currentlyFocused *paneNode80 currentlyFocused *paneNode
81+
82+ // Widget operation synchronization to prevent GTK race conditions
83+ widgetMutex sync.Mutex
84+ widgetRegistry *WidgetRegistry
77 }85 }
78 86
79 const (87 const (
@@ -92,6 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
92 lastSplitMsg: make(map[*webkit.WebView]time.Time),100 lastSplitMsg: make(map[*webkit.WebView]time.Time),
93 lastExitMsg: make(map[*webkit.WebView]time.Time),101 lastExitMsg: make(map[*webkit.WebView]time.Time),
94 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator102 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator
103+ widgetRegistry: NewWidgetRegistry(), // Initialize widget registry
95 }104 }
96 105
97 // Initialize specialized managers106 // Initialize specialized managers
@@ -120,10 +129,12 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
120 129
121 rootContainer := rootPane.webView.RootWidget()130 rootContainer := rootPane.webView.RootWidget()
122 root := &paneNode{131 root := &paneNode{
123- pane: rootPane,132+ pane: rootPane,
124- container: rootContainer,133+ isLeaf: true,
125- isLeaf: true,
126 }134 }
135+ // Initialize widgets properly
136+ manager.initializePaneWidgets(root, rootContainer)
137+
127 manager.root = root138 manager.root = root
128 manager.mainPane = root139 manager.mainPane = root
129 manager.viewToNode[rootPane.webView] = root140 manager.viewToNode[rootPane.webView] = root
@@ -485,6 +496,11 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
485 border-color: %s;496 border-color: %s;
486 }497 }
487 498
499+ /* Active border for ALL workspace panes (including stacked panes) */
500+ .workspace-pane.workspace-pane-active {
501+ border-color: %s !important;
502+ }
503+
488 /* Stacked panes styling */504 /* Stacked panes styling */
489 .stacked-pane-container {505 .stacked-pane-container {
490 background-color: %s;506 background-color: %s;
@@ -526,6 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
526 inactiveBorderColor,542 inactiveBorderColor,
527 styling.BorderRadius,543 styling.BorderRadius,
528 styling.BorderColor,544 styling.BorderColor,
545+ styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color
529 // Additional parameters for stacked pane styles546 // Additional parameters for stacked pane styles
530 windowBackgroundColor, // stacked-pane-container background547 windowBackgroundColor, // stacked-pane-container background
531 styling.BorderRadius, // stacked-pane-container border-radius548 styling.BorderRadius, // stacked-pane-container border-radius
@@ -564,13 +581,16 @@ func (wm *WorkspaceManager) ensurePaneBaseClasses() {
564 581
565 leaves := wm.collectLeaves()582 leaves := wm.collectLeaves()
566 for _, leaf := range leaves {583 for _, leaf := range leaves {
567- if leaf != nil && leaf.container != 0 {584+ if leaf != nil && leaf.container != nil {
568- webkit.WidgetAddCSSClass(leaf.container, basePaneClass)585+ leaf.container.Execute(func(containerPtr uintptr) error {
569- if wm.hasMultiplePanes() {586+ webkit.WidgetAddCSSClass(containerPtr, basePaneClass)
570- webkit.WidgetAddCSSClass(leaf.container, multiPaneClass)587+ if wm.hasMultiplePanes() {
571- } else {588+ webkit.WidgetAddCSSClass(containerPtr, multiPaneClass)
572- webkit.WidgetRemoveCSSClass(leaf.container, multiPaneClass)589+ } else {
573- }590+ webkit.WidgetRemoveCSSClass(containerPtr, multiPaneClass)
591+ }
592+ return nil
593+ })
574 }594 }
575 }595 }
576 }596 }
@@ -601,20 +621,24 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) {
601 if wm == nil || node == nil || !node.isLeaf {621 if wm == nil || node == nil || !node.isLeaf {
602 return622 return
603 }623 }
604- if node.container == 0 || node.hoverToken != 0 {624+ if node.container == nil || node.hoverToken != 0 {
605 return625 return
606 }626 }
607 627
608- token := webkit.WidgetAddHoverHandler(node.container, func() {628+ var token uintptr
609- if wm == nil {629+ node.container.Execute(func(containerPtr uintptr) error {
610- return630+ token = webkit.WidgetAddHoverHandler(containerPtr, func() {
611- }631+ if wm == nil {
612- wm.focusManager.SetActivePane(node)632+ return
613- wm.currentlyFocused = node633+ }
634+ wm.focusManager.SetActivePane(node)
635+ wm.currentlyFocused = node
636+ })
637+ return nil
614 })638 })
615 node.hoverToken = token639 node.hoverToken = token
616 if token == 0 {640 if token == 0 {
617- log.Printf("[workspace] failed to attach hover handler: widget=%#x", node.container)641+ log.Printf("[workspace] failed to attach hover handler")
618 }642 }
619 }643 }
620 644
@@ -622,7 +646,10 @@ func (wm *WorkspaceManager) detachHover(node *paneNode) {
622 if wm == nil || node == nil || node.hoverToken == 0 {646 if wm == nil || node == nil || node.hoverToken == 0 {
623 return647 return
624 }648 }
625- webkit.WidgetRemoveHoverHandler(node.container, node.hoverToken)649+ node.container.Execute(func(containerPtr uintptr) error {
650+ webkit.WidgetRemoveHoverHandler(containerPtr, node.hoverToken)
651+ return nil
652+ })
626 node.hoverToken = 0653 node.hoverToken = 0
627 }654 }
628 655
@@ -724,7 +751,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
724 const focusEpsilon = 1e-3751 const focusEpsilon = 1e-3
725 752
726 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {753 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
727- if wm.currentlyFocused == nil || !wm.currentlyFocused.isLeaf || wm.currentlyFocused.container == 0 {754+ if wm.currentlyFocused == nil || !wm.currentlyFocused.isLeaf || wm.currentlyFocused.container == nil {
728 return false755 return false
729 }756 }
730 757
@@ -734,7 +761,12 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
734 return true761 return true
735 }762 }
736 763
737- currentBounds, ok := webkit.WidgetGetBounds(wm.currentlyFocused.container)764+ var currentBounds webkit.WidgetBounds
765+ var ok bool
766+ wm.currentlyFocused.container.Execute(func(containerPtr uintptr) error {
767+ currentBounds, ok = webkit.WidgetGetBounds(containerPtr)
768+ return nil
769+ })
738 if !ok {770 if !ok {
739 log.Printf("[workspace] unable to compute bounds for active pane")771 log.Printf("[workspace] unable to compute bounds for active pane")
740 return false772 return false
@@ -749,10 +781,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
749 var debugCandidates []string781 var debugCandidates []string
750 782
751 for _, candidate := range leaves {783 for _, candidate := range leaves {
752- if candidate == nil || candidate == wm.currentlyFocused || candidate.container == 0 {784+ if candidate == nil || candidate == wm.currentlyFocused || candidate.container == nil {
753 continue785 continue
754 }786 }
755- bounds, ok := webkit.WidgetGetBounds(candidate.container)787+ var bounds webkit.WidgetBounds
788+ var boundsOk bool
789+ candidate.container.Execute(func(containerPtr uintptr) error {
790+ bounds, boundsOk = webkit.WidgetGetBounds(containerPtr)
791+ return nil
792+ })
793+ ok := boundsOk
756 if !ok {794 if !ok {
757 continue795 continue
758 }796 }
@@ -809,11 +847,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
809 }847 }
810 848
811 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {849 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {
812- if node == nil || node.container == 0 {850+ if node == nil || node.container == nil {
813 return nil851 return nil
814 }852 }
815 853
816- refBounds, ok := webkit.WidgetGetBounds(node.container)854+ var refBounds webkit.WidgetBounds
855+ var ok bool
856+ node.container.Execute(func(containerPtr uintptr) error {
857+ refBounds, ok = webkit.WidgetGetBounds(containerPtr)
858+ return nil
859+ })
817 if !ok {860 if !ok {
818 return nil861 return nil
819 }862 }
@@ -858,10 +901,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6
858 bestScore := math.MaxFloat64901 bestScore := math.MaxFloat64
859 var best *paneNode902 var best *paneNode
860 for _, leaf := range leaves {903 for _, leaf := range leaves {
861- if leaf == nil || leaf.container == 0 {904+ if leaf == nil || leaf.container == nil {
862 continue905 continue
863 }906 }
864- bounds, ok := webkit.WidgetGetBounds(leaf.container)907+ var bounds webkit.WidgetBounds
908+ var boundsOk bool
909+ leaf.container.Execute(func(containerPtr uintptr) error {
910+ bounds, boundsOk = webkit.WidgetGetBounds(containerPtr)
911+ return nil
912+ })
913+ ok := boundsOk
865 if !ok {914 if !ok {
866 continue915 continue
867 }916 }
@@ -1026,18 +1075,18 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1026 1075
1027 // Determine what we're actually splitting1076 // Determine what we're actually splitting
1028 var splitTarget *paneNode1077 var splitTarget *paneNode
1029- var splitTargetContainer uintptr1078+ var splitTargetContainer *SafeWidget
1030 1079
1031 if stackContainer != nil {1080 if stackContainer != nil {
1032 // Case: splitting from inside a stack - we split around the entire stack1081 // Case: splitting from inside a stack - we split around the entire stack
1033 splitTarget = stackContainer1082 splitTarget = stackContainer
1034 splitTargetContainer = stackContainer.container1083 splitTargetContainer = stackContainer.container
1035- log.Printf("[workspace] splitting around stack: stackContainer=%p container=%#x", splitTarget, splitTargetContainer)1084+ log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget)
1036 } else {1085 } else {
1037 // Case: normal split from a simple pane1086 // Case: normal split from a simple pane
1038 splitTarget = target1087 splitTarget = target
1039 splitTargetContainer = target.container1088 splitTargetContainer = target.container
1040- log.Printf("[workspace] normal split from pane: target=%p container=%#x", splitTarget, splitTargetContainer)1089+ log.Printf("[workspace] normal split from pane: target=%p", splitTarget)
1041 }1090 }
1042 1091
1043 newView, err := wm.createWebView()1092 newView, err := wm.createWebView()
@@ -1063,22 +1112,28 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1063 webkit.WidgetRealizeInContainer(newContainer)1112 webkit.WidgetRealizeInContainer(newContainer)
1064 1113
1065 // Use the determined split target container1114 // Use the determined split target container
1066- if splitTargetContainer == 0 {1115+ if splitTargetContainer == nil {
1067 return nil, errors.New("split target missing container")1116 return nil, errors.New("split target missing container")
1068 }1117 }
1069 1118
1070 // Only apply GTK operations if this is a simple widget (leaf pane)1119 // Only apply GTK operations if this is a simple widget (leaf pane)
1071 // For stacked containers, we need to be more careful1120 // For stacked containers, we need to be more careful
1072 if splitTarget.isLeaf {1121 if splitTarget.isLeaf {
1073- webkit.WidgetSetHExpand(splitTargetContainer, true)1122+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1074- webkit.WidgetSetVExpand(splitTargetContainer, true)1123+ webkit.WidgetSetHExpand(containerPtr, true)
1075- webkit.WidgetRealizeInContainer(splitTargetContainer)1124+ webkit.WidgetSetVExpand(containerPtr, true)
1125+ webkit.WidgetRealizeInContainer(containerPtr)
1126+ return nil
1127+ })
1076 } else if splitTarget.isStacked {1128 } else if splitTarget.isStacked {
1077 // Stack containers need the same setup as regular panes for proper splitting1129 // Stack containers need the same setup as regular panes for proper splitting
1078- webkit.WidgetSetHExpand(splitTargetContainer, true)1130+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1079- webkit.WidgetSetVExpand(splitTargetContainer, true)1131+ webkit.WidgetSetHExpand(containerPtr, true)
1080- webkit.WidgetRealizeInContainer(splitTargetContainer)1132+ webkit.WidgetSetVExpand(containerPtr, true)
1081- log.Printf("[workspace] configured stack wrapper for split: %#x", splitTargetContainer)1133+ webkit.WidgetRealizeInContainer(containerPtr)
1134+ log.Printf("[workspace] configured stack wrapper for split: %#x", containerPtr)
1135+ return nil
1136+ })
1082 }1137 }
1083 1138
1084 orientation, existingFirst := mapDirection(direction)1139 orientation, existingFirst := mapDirection(direction)
@@ -1095,16 +1150,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1095 webkit.PanedSetResizeEnd(paned, true)1150 webkit.PanedSetResizeEnd(paned, true)
1096 1151
1097 newLeaf := &paneNode{1152 newLeaf := &paneNode{
1098- pane: newPane,1153+ pane: newPane,
1099- container: newContainer,1154+ isLeaf: true,
1100- isLeaf: true,
1101 }1155 }
1156+ wm.initializePaneWidgets(newLeaf, newContainer)
1157+
1102 split := &paneNode{1158 split := &paneNode{
1103 parent: splitTarget.parent,1159 parent: splitTarget.parent,
1104- container: paned,
1105 orientation: orientation,1160 orientation: orientation,
1106 isLeaf: false,1161 isLeaf: false,
1107 }1162 }
1163+ wm.initializePaneWidgets(split, paned)
1108 1164
1109 parent := split.parent1165 parent := split.parent
1110 1166
@@ -1116,19 +1172,25 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1116 wm.window.SetChild(0)1172 wm.window.SetChild(0)
1117 }1173 }
1118 // For root widgets, only unparent if they actually have a GTK parent1174 // For root widgets, only unparent if they actually have a GTK parent
1119- if webkit.WidgetGetParent(splitTargetContainer) != 0 {1175+ splitTargetContainer.Execute(func(containerPtr uintptr) error {
1120- webkit.WidgetUnparent(splitTargetContainer)1176+ if webkit.WidgetGetParent(containerPtr) != 0 {
1121- }1177+ webkit.WidgetUnparent(containerPtr)
1122- } else if parent.container != 0 {1178+ }
1179+ return nil
1180+ })
1181+ } else if parent.container != nil {
1123 // Split target has a parent paned - remove it (automatically unparents in GTK4)1182 // Split target has a parent paned - remove it (automatically unparents in GTK4)
1124 log.Printf("[workspace] unparenting split target container=%#x from parent paned=%#x", splitTargetContainer, parent.container)1183 log.Printf("[workspace] unparenting split target container=%#x from parent paned=%#x", splitTargetContainer, parent.container)
1125- if parent.left == splitTarget {1184+ parent.container.Execute(func(panedPtr uintptr) error {
1126- webkit.PanedSetStartChild(parent.container, 0)1185+ if parent.left == splitTarget {
1127- } else if parent.right == splitTarget {1186+ webkit.PanedSetStartChild(panedPtr, 0)
1128- webkit.PanedSetEndChild(parent.container, 0)1187+ } else if parent.right == splitTarget {
1129- }1188+ webkit.PanedSetEndChild(panedPtr, 0)
1130- // GTK4 automatically unparents when we set paned child to 0 - no manual unparent needed1189+ }
1131- webkit.WidgetQueueAllocate(parent.container)1190+ // GTK4 automatically unparents when we set paned child to 0 - no manual unparent needed
1191+ webkit.WidgetQueueAllocate(panedPtr)
1192+ return nil
1193+ })
1132 }1194 }
1133 1195
1134 // Set up the tree structure first1196 // Set up the tree structure first
@@ -1158,13 +1220,19 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1158 1220
1159 // Add both containers to the new paned1221 // Add both containers to the new paned
1160 if existingFirst {1222 if existingFirst {
1161- webkit.PanedSetStartChild(paned, splitTargetContainer)1223+ splitTargetContainer.Execute(func(splitPtr uintptr) error {
1162- webkit.PanedSetEndChild(paned, newContainer)1224+ webkit.PanedSetStartChild(paned, splitPtr)
1163- log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer)1225+ webkit.PanedSetEndChild(paned, newContainer)
1226+ log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitPtr, newContainer)
1227+ return nil
1228+ })
1164 } else {1229 } else {
1165- webkit.PanedSetStartChild(paned, newContainer)1230+ splitTargetContainer.Execute(func(splitPtr uintptr) error {
1166- webkit.PanedSetEndChild(paned, splitTargetContainer)1231+ webkit.PanedSetStartChild(paned, newContainer)
1167- log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer)1232+ webkit.PanedSetEndChild(paned, splitPtr)
1233+ log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitPtr)
1234+ return nil
1235+ })
1168 }1236 }
1169 1237
1170 // Attach the new paned to its parent1238 // Attach the new paned to its parent
@@ -1175,14 +1243,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1175 log.Printf("[workspace] paned set as window child: paned=%#x", paned)1243 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
1176 }1244 }
1177 } else {1245 } else {
1178- if parent.left == split {1246+ parent.container.Execute(func(parentPtr uintptr) error {
1179- webkit.PanedSetStartChild(parent.container, paned)1247+ if parent.left == split {
1180- } else if parent.right == split {1248+ webkit.PanedSetStartChild(parentPtr, paned)
1181- webkit.PanedSetEndChild(parent.container, paned)1249+ } else if parent.right == split {
1182- }1250+ webkit.PanedSetEndChild(parentPtr, paned)
1183- webkit.WidgetQueueAllocate(parent.container)1251+ }
1184- webkit.WidgetQueueAllocate(paned)1252+ webkit.WidgetQueueAllocate(parentPtr)
1185- log.Printf("[workspace] paned inserted into parent=%#x", parent.container)1253+ webkit.WidgetQueueAllocate(paned)
1254+ log.Printf("[workspace] paned inserted into parent=%#x", parentPtr)
1255+ return nil
1256+ })
1186 }1257 }
1187 1258
1188 webkit.WidgetShow(paned)1259 webkit.WidgetShow(paned)
@@ -1278,19 +1349,25 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
1278 return wm.stackedPaneManager.CloseStackedPane(node)1349 return wm.stackedPaneManager.CloseStackedPane(node)
1279 }1350 }
1280 1351
1281- if node.parent != nil && node.parent.container != 0 {1352+ if node.parent != nil && node.parent.container != nil {
1282- if node.parent.left == node {1353+ node.parent.container.Execute(func(panedPtr uintptr) error {
1283- webkit.PanedSetStartChild(node.parent.container, 0)1354+ if node.parent.left == node {
1284- // GTK4 automatically unparents when setting child to 01355+ webkit.PanedSetStartChild(panedPtr, 0)
1285- } else if node.parent.right == node {1356+ // GTK4 automatically unparents when setting child to 0
1286- webkit.PanedSetEndChild(node.parent.container, 0)1357+ } else if node.parent.right == node {
1287- // GTK4 automatically unparents when setting child to 01358+ webkit.PanedSetEndChild(panedPtr, 0)
1288- }1359+ // GTK4 automatically unparents when setting child to 0
1289- } else if node.container != 0 {1360+ }
1361+ return nil
1362+ })
1363+ } else if node.container != nil {
1290 // Only manually unparent if widget wasn't auto-unparented above1364 // Only manually unparent if widget wasn't auto-unparented above
1291- if webkit.WidgetGetParent(node.container) != 0 {1365+ node.container.Execute(func(containerPtr uintptr) error {
1292- webkit.WidgetUnparent(node.container)1366+ if webkit.WidgetGetParent(containerPtr) != 0 {
1293- }1367+ webkit.WidgetUnparent(containerPtr)
1368+ }
1369+ return nil
1370+ })
1294 }1371 }
1295 1372
1296 remaining := len(wm.app.panes)1373 remaining := len(wm.app.panes)
@@ -1316,24 +1393,30 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
1316 if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil {1393 if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil {
1317 if sibling.isPopup && !node.isPopup {1394 if sibling.isPopup && !node.isPopup {
1318 parentNode := node.parent1395 parentNode := node.parent
1319- parentContainer := uintptr(0)1396+ var parentContainer *SafeWidget
1320 if parentNode != nil {1397 if parentNode != nil {
1321 parentContainer = parentNode.container1398 parentContainer = parentNode.container
1322 }1399 }
1323 1400
1324 // Detach remaining GTK widgets before destroying the WebViews so GTK1401 // Detach remaining GTK widgets before destroying the WebViews so GTK
1325 // does not attempt to dispose still-parented children.1402 // does not attempt to dispose still-parented children.
1326- if parentContainer != 0 {1403+ if parentContainer != nil {
1327- if parentNode.left == sibling {1404+ parentContainer.Execute(func(panedPtr uintptr) error {
1328- webkit.PanedSetStartChild(parentContainer, 0)1405+ if parentNode.left == sibling {
1329- } else if parentNode.right == sibling {1406+ webkit.PanedSetStartChild(panedPtr, 0)
1330- webkit.PanedSetEndChild(parentContainer, 0)1407+ } else if parentNode.right == sibling {
1331- }1408+ webkit.PanedSetEndChild(panedPtr, 0)
1409+ }
1410+ return nil
1411+ })
1332 }1412 }
1333- if sibling.container != 0 {1413+ if sibling.container != nil {
1334- webkit.WidgetUnparent(sibling.container)1414+ sibling.container.Execute(func(containerPtr uintptr) error {
1415+ webkit.WidgetUnparent(containerPtr)
1416+ return nil
1417+ })
1335 }1418 }
1336- if parentContainer != 0 && wm.window != nil {1419+ if parentContainer != nil && wm.window != nil {
1337 wm.window.SetChild(0)1420 wm.window.SetChild(0)
1338 }1421 }
1339 1422
@@ -1420,12 +1503,13 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
1420 replacement.parent = nil1503 replacement.parent = nil
1421 1504
1422 // Replace window child directly (GTK handles reparenting automatically)1505 // Replace window child directly (GTK handles reparenting automatically)
1423- if wm.window != nil {1506+ if wm.window != nil && replacement.container != nil {
1424- wm.window.SetChild(replacement.container)1507+ replacement.container.Execute(func(containerPtr uintptr) error {
1425- if replacement.container != 0 {1508+ wm.window.SetChild(containerPtr)
1426- webkit.WidgetQueueAllocate(replacement.container)1509+ webkit.WidgetQueueAllocate(containerPtr)
1427- webkit.WidgetShow(replacement.container)1510+ webkit.WidgetShow(containerPtr)
1428- }1511+ return nil
1512+ })
1429 }1513 }
1430 1514
1431 // Focus a suitable pane1515 // Focus a suitable pane
@@ -1474,12 +1558,6 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
1474 1558
1475 log.Printf("[workspace] closing pane: target=%#x parent=%#x sibling=%#x remaining=%d", node.container, parent.container, sibling.container, remaining)1559 log.Printf("[workspace] closing pane: target=%#x parent=%#x sibling=%#x remaining=%d", node.container, parent.container, sibling.container, remaining)
1476 1560
1477- // Find focus target before modifying the tree structure
1478- focusTarget := wm.leftmostLeaf(sibling)
1479- if focusTarget == nil {
1480- focusTarget = wm.leftmostLeaf(wm.root)
1481- }
1482-
1483 // Clean up workspace tracking1561 // Clean up workspace tracking
1484 node.pane.CleanupFromWorkspace(wm)1562 node.pane.CleanupFromWorkspace(wm)
1485 1563
@@ -1487,97 +1565,71 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
1487 wm.mainPane = nil1565 wm.mainPane = nil
1488 }1566 }
1489 1567
1490- // Clear current active if it's the node being closed and immediately set focus to target1568+ // Clear current active if it's the node being closed
1491 if wm.currentlyFocused == node {1569 if wm.currentlyFocused == node {
1492- wm.currentlyFocused = focusTarget1570+ wm.currentlyFocused = nil
1493- if focusTarget != nil {
1494- wm.focusManager.SetActivePane(focusTarget)
1495- }
1496 }1571 }
1497 1572
1498 grand := parent.parent1573 grand := parent.parent
1499 if grand == nil {1574 if grand == nil {
1500 // Parent is the root node. Promote sibling to become the new root.1575 // Parent is the root node. Promote sibling to become the new root.
1501- // The sibling can be either a leaf (when only 2 panes total) or a branch (when more panes exist)
1502 log.Printf("[workspace] promoting sibling to root: container=%#x, isLeaf=%v", sibling.container, sibling.isLeaf)1576 log.Printf("[workspace] promoting sibling to root: container=%#x, isLeaf=%v", sibling.container, sibling.isLeaf)
1503 1577
1504- // CRITICAL: Check widget validity before any GTK operations to prevent segfault1578+ // Update tree structure first
1505- if !webkit.WidgetIsValid(sibling.container) {
1506- log.Printf("[workspace] ERROR: sibling container %#x is not a valid GTK widget, skipping root promotion", sibling.container)
1507- return errors.New("sibling container is not a valid GTK widget")
1508- }
1509-
1510 wm.root = sibling1579 wm.root = sibling
1511 sibling.parent = nil1580 sibling.parent = nil
1512 1581
1513- // Unparent the sibling from the paned first, then set it as window child1582+ // For root promotion, GTK4 handles unparenting automatically when we set the new window child
1514- if wm.window != nil {1583+ if wm.window != nil && sibling.container != nil {
1515- // GTK requires explicit unparenting before reparenting1584+ sibling.container.Execute(func(containerPtr uintptr) error {
1516- if parent.container != 0 && webkit.WidgetIsValid(parent.container) {1585+ wm.window.SetChild(containerPtr)
1517- if parent.left == sibling {1586+ webkit.WidgetQueueAllocate(containerPtr)
1518- webkit.PanedSetStartChild(parent.container, 0)1587+ webkit.WidgetShow(containerPtr)
1519- } else {1588+ log.Printf("[workspace] successfully promoted sibling to root: %#x", containerPtr)
1520- webkit.PanedSetEndChild(parent.container, 0)1589+ return nil
1521- }1590+ })
1522- }
1523- // Now we can safely set it as window child (widget validity already checked above)
1524- wm.window.SetChild(sibling.container)
1525- webkit.WidgetQueueAllocate(sibling.container)
1526 }1591 }
1527 } else {1592 } else {
1528 // Parent has a grandparent, so promote sibling to take parent's place1593 // Parent has a grandparent, so promote sibling to take parent's place
1529 log.Printf("[workspace] promoting sibling to parent's position: sibling=%#x grand=%#x", sibling.container, grand.container)1594 log.Printf("[workspace] promoting sibling to parent's position: sibling=%#x grand=%#x", sibling.container, grand.container)
1530 1595
1531- // CRITICAL: Check widget validity before any GTK operations to prevent segfault1596+ // Update tree structure
1532- if !webkit.WidgetIsValid(sibling.container) {
1533- log.Printf("[workspace] ERROR: sibling container %#x is not a valid GTK widget, skipping promotion", sibling.container)
1534- return errors.New("sibling container is not a valid GTK widget")
1535- }
1536- if !webkit.WidgetIsValid(grand.container) {
1537- log.Printf("[workspace] ERROR: grand container %#x is not a valid GTK widget, skipping promotion", grand.container)
1538- return errors.New("grand container is not a valid GTK widget")
1539- }
1540-
1541- // First unparent the sibling from its current parent to avoid GTK-CRITICAL errors
1542- if parent.container != 0 && !parent.isLeaf && webkit.WidgetIsValid(parent.container) {
1543- if parent.left == sibling {
1544- webkit.PanedSetStartChild(parent.container, 0)
1545- } else if parent.right == sibling {
1546- webkit.PanedSetEndChild(parent.container, 0)
1547- }
1548- }
1549-
1550- // Now safely reparent the sibling to the grandparent
1551- // GTK4 automatically handles focus state synchronization during reparenting
1552-
1553 sibling.parent = grand1597 sibling.parent = grand
1554- if grand.container != 0 && !grand.isLeaf && webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {1598+ if grand.left == parent {
1555- if grand.left == parent {1599+ grand.left = sibling
1556- grand.left = sibling1600+ } else {
1557- webkit.PanedSetStartChild(grand.container, sibling.container)1601+ grand.right = sibling
1558- } else if grand.right == parent {
1559- grand.right = sibling
1560- webkit.PanedSetEndChild(grand.container, sibling.container)
1561- }
1562- webkit.WidgetQueueAllocate(grand.container)
1563 }1602 }
1564- }
1565 1603
1566- // Keep sibling subtree visible1604+ // Move sibling to grandparent's paned widget
1567- if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {1605+ // GTK4 handles unparenting automatically when we set the new paned child
1568- webkit.WidgetShow(sibling.container)1606+ grand.container.Execute(func(grandPtr uintptr) error {
1607+ sibling.container.Execute(func(siblingPtr uintptr) error {
1608+ if grand.left == sibling {
1609+ webkit.PanedSetStartChild(grandPtr, siblingPtr)
1610+ } else {
1611+ webkit.PanedSetEndChild(grandPtr, siblingPtr)
1612+ }
1613+ webkit.WidgetQueueAllocate(siblingPtr)
1614+ webkit.WidgetShow(siblingPtr)
1615+ log.Printf("[workspace] successfully promoted sibling to parent position: %#x", siblingPtr)
1616+ return nil
1617+ })
1618+ return nil
1619+ })
1569 }1620 }
1570 1621
1571- // Clean up parent node references1622+ // Find a suitable focus target
1572- parent.left = nil1623+ focusTarget := wm.leftmostLeaf(sibling)
1573- parent.right = nil1624+ if focusTarget != nil {
1574- node.parent = nil1625+ wm.currentlyFocused = focusTarget
1575-1626+ wm.focusManager.SetActivePane(focusTarget)
1576- // Focus handling was moved earlier to avoid state gaps1627+ }
1577 1628
1578- // Destroy the webview and detach hover AFTER all hierarchy changes are complete1629+ // Clean up the node being closed
1579 ensureCleanup()1630 ensureCleanup()
1580 wm.detachHover(node)1631 wm.detachHover(node)
1632+
1581 switch {1633 switch {
1582 case node.isPopup:1634 case node.isPopup:
1583 if err := node.pane.webView.Destroy(); err != nil {1635 if err := node.pane.webView.Destroy(); err != nil {
@@ -1612,6 +1664,91 @@ func (wm *WorkspaceManager) leftmostLeaf(node *paneNode) *paneNode {
1612 return node1664 return node
1613 }1665 }
1614 1666
1667+// validateWidgetsForReparenting validates that all widgets needed for reparenting are valid
1668+func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error {
1669+ // Validate sibling container (this is the most critical one)
1670+ if sibling.container == nil {
1671+ return errors.New("sibling container is nil")
1672+ }
1673+ if !sibling.container.IsValid() {
1674+ return fmt.Errorf("sibling container %s is not valid", sibling.container.String())
1675+ }
1676+
1677+ // Validate parent container if present
1678+ if parent != nil && parent.container != nil && !parent.container.IsValid() {
1679+ return fmt.Errorf("parent container %s is not valid", parent.container.String())
1680+ }
1681+
1682+ // Validate grandparent container if present
1683+ if grand != nil && grand.container != nil && !grand.container.IsValid() {
1684+ return fmt.Errorf("grandparent container %s is not valid", grand.container.String())
1685+ }
1686+
1687+ return nil
1688+}
1689+
1690+// safeWidgetOperation performs a widget operation with proper locking and validation
1691+func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error {
1692+ wm.widgetMutex.Lock()
1693+ defer wm.widgetMutex.Unlock()
1694+ return operation()
1695+}
1696+
1697+// registerWidget registers a widget with the registry and returns a SafeWidget
1698+func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget {
1699+ if ptr == 0 {
1700+ return nil
1701+ }
1702+ return wm.widgetRegistry.Register(ptr, typeInfo)
1703+}
1704+
1705+// initializePaneWidgets sets up SafeWidget wrappers for a paneNode
1706+func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr uintptr) {
1707+ // Register the main container
1708+ node.container = wm.registerWidget(containerPtr, "pane-container")
1709+
1710+ // Initialize other widget fields as nil (will be set when needed)
1711+ node.titleBar = nil
1712+ node.stackWrapper = nil
1713+}
1714+
1715+// Helper functions for safe widget operations
1716+func (node *paneNode) getContainerPtr() uintptr {
1717+ if node.container != nil {
1718+ return node.container.Ptr()
1719+ }
1720+ return 0
1721+}
1722+
1723+func (node *paneNode) getTitleBarPtr() uintptr {
1724+ if node.titleBar != nil {
1725+ return node.titleBar.Ptr()
1726+ }
1727+ return 0
1728+}
1729+
1730+func (node *paneNode) getStackWrapperPtr() uintptr {
1731+ if node.stackWrapper != nil {
1732+ return node.stackWrapper.Ptr()
1733+ }
1734+ return 0
1735+}
1736+
1737+// setContainer sets the SafeWidget container
1738+func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {
1739+ node.container = wm.registerWidget(ptr, typeInfo)
1740+}
1741+
1742+// setTitleBar sets the SafeWidget titleBar
1743+func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) {
1744+ node.titleBar = wm.registerWidget(ptr, "title-bar")
1745+}
1746+
1747+// setStackWrapper sets the SafeWidget stackWrapper
1748+func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {
1749+ node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper")
1750+}
1751+
1615 // findReplacementRoot finds a suitable replacement when closing the current root pane1752 // findReplacementRoot finds a suitable replacement when closing the current root pane
1616 func (wm *WorkspaceManager) findReplacementRoot(excludeNode *paneNode) *paneNode {1753 func (wm *WorkspaceManager) findReplacementRoot(excludeNode *paneNode) *paneNode {
1617 if wm == nil || wm.root == nil {1754 if wm == nil || wm.root == nil {
@@ -2302,8 +2439,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
2302 webkit.WidgetRealizeInContainer(webViewWidget)2439 webkit.WidgetRealizeInContainer(webViewWidget)
2303 }2440 }
2304 2441
2305- existingContainer := target.container2442+ if target.container == nil {
2306- if existingContainer == 0 {
2307 return errors.New("existing pane missing container")2443 return errors.New("existing pane missing container")
2308 }2444 }
2309 2445
@@ -2320,35 +2456,44 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
2320 webkit.PanedSetResizeEnd(paned, true)2456 webkit.PanedSetResizeEnd(paned, true)
2321 2457
2322 newLeaf := &paneNode{2458 newLeaf := &paneNode{
2323- pane: newPane,2459+ pane: newPane,
2324- container: newContainer,2460+ isLeaf: true,
2325- isLeaf: true,
2326 }2461 }
2462+ wm.initializePaneWidgets(newLeaf, newContainer)
2463+
2327 split := &paneNode{2464 split := &paneNode{
2328 parent: target.parent,2465 parent: target.parent,
2329- container: paned,
2330 orientation: orientation,2466 orientation: orientation,
2331 isLeaf: false,2467 isLeaf: false,
2332 }2468 }
2469+ wm.initializePaneWidgets(split, paned)
2333 2470
2334 parent := split.parent2471 parent := split.parent
2335 2472
2336 // Detach existing container from its current GTK parent before inserting into new paned.2473 // Detach existing container from its current GTK parent before inserting into new paned.
2474+ existingContainer := target.getContainerPtr()
2337 if parent == nil {2475 if parent == nil {
2338 // Target is the root - remove it from the window2476 // Target is the root - remove it from the window
2339 log.Printf("[workspace] removing existing container=%#x from window", existingContainer)2477 log.Printf("[workspace] removing existing container=%#x from window", existingContainer)
2340 if wm.window != nil {2478 if wm.window != nil {
2341 wm.window.SetChild(0)2479 wm.window.SetChild(0)
2342 }2480 }
2343- } else if parent.container != 0 {2481+ } else if parent.container != nil {
2344 // Target has a parent paned - unparent it from there2482 // Target has a parent paned - unparent it from there
2345- log.Printf("[workspace] unparenting existing container=%#x from parent paned=%#x", existingContainer, parent.container)2483+ log.Printf("[workspace] unparenting existing container from parent paned")
2346- if parent.left == target {2484+ err := parent.container.Execute(func(parentPtr uintptr) error {
2347- webkit.PanedSetStartChild(parent.container, 0)2485+ log.Printf("[workspace] executing unparent: parentPtr=%#x", parentPtr)
2348- } else if parent.right == target {2486+ if parent.left == target {
2349- webkit.PanedSetEndChild(parent.container, 0)2487+ webkit.PanedSetStartChild(parentPtr, 0)
2488+ } else if parent.right == target {
2489+ webkit.PanedSetEndChild(parentPtr, 0)
2490+ }
2491+ webkit.WidgetQueueAllocate(parentPtr)
2492+ return nil
2493+ })
2494+ if err != nil {
2495+ log.Printf("[workspace] error during parent unparent: %v", err)
2350 }2496 }
2351- webkit.WidgetQueueAllocate(parent.container)
2352 }2497 }
2353 2498
2354 // Add both containers to the new paned2499 // Add both containers to the new paned
@@ -2377,16 +2522,23 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
2377 log.Printf("[workspace] paned set as window child: paned=%#x", paned)2522 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
2378 }2523 }
2379 } else {2524 } else {
2380- if parent.left == target {2525+ err := parent.container.Execute(func(parentPtr uintptr) error {
2381- parent.left = split2526+ log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parentPtr, paned)
2382- webkit.PanedSetStartChild(parent.container, paned)2527+ if parent.left == target {
2383- } else if parent.right == target {2528+ parent.left = split
2384- parent.right = split2529+ webkit.PanedSetStartChild(parentPtr, paned)
2385- webkit.PanedSetEndChild(parent.container, paned)2530+ } else if parent.right == target {
2531+ parent.right = split
2532+ webkit.PanedSetEndChild(parentPtr, paned)
2533+ }
2534+ webkit.WidgetQueueAllocate(parentPtr)
2535+ webkit.WidgetQueueAllocate(paned)
2536+ log.Printf("[workspace] paned inserted into parent successfully")
2537+ return nil
2538+ })
2539+ if err != nil {
2540+ log.Printf("[workspace] error during parent reparent: %v", err)
2386 }2541 }
2387- webkit.WidgetQueueAllocate(parent.container)
2388- webkit.WidgetQueueAllocate(paned)
2389- log.Printf("[workspace] paned inserted into parent=%#x", parent.container)
2390 }2542 }
2391 2543
2392 webkit.WidgetShow(paned)2544 webkit.WidgetShow(paned)

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