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) { | |||
| 109 | 109 | func (fm *FocusManager) updateVisualState(newPane *paneNode) { | |
| 110 | 110 | activePaneClass := "workspace-pane-active" | |
| 111 | 111 | ||
| 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) | |
| 120 | 117 | ||
| 121 | 118 | // 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") | |
| 125 | 128 | } | |
| 126 | 129 | ||
| 127 | 130 | // FATAL CHECK: Ensure only ONE pane has active class | |
| @@ -131,6 +134,63 @@ func (fm *FocusManager) updateVisualState(newPane *paneNode) { | |||
| 131 | 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 | 194 | // updateStackedPaneVisibility handles special case for stacked panes | |
| 135 | 195 | func (fm *FocusManager) updateStackedPaneVisibility(node *paneNode) { | |
| 136 | 196 | // Find if this node is part of a stack | |
| @@ -164,19 +224,31 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) { | |||
| 164 | 224 | // Hide all panes in stack | |
| 165 | 225 | for i, pane := range stackNode.stackedPanes { | |
| 166 | 226 | 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 | + | }) | |
| 171 | 237 | } | |
| 172 | 238 | } | |
| 173 | 239 | ||
| 174 | 240 | // Show active pane | |
| 175 | 241 | 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 | + | }) | |
| 180 | 252 | } | |
| 181 | 253 | ||
| 182 | 254 | // notifyJavaScript dispatches focus events to the JavaScript bridge | |
| @@ -275,11 +347,14 @@ func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() { | |||
| 275 | 347 | // Check all leaf panes in the workspace | |
| 276 | 348 | leaves := fm.wm.collectLeaves() | |
| 277 | 349 | 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 | + | }) | |
| 283 | 358 | } | |
| 284 | 359 | } | |
| 285 | 360 | ||
| @@ -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 states | 112 | + | log.Printf("[focus-manager] updateVisualState: setting active pane to %p", newPane) |
| 113 | - | // This ensures we never have a race condition or missed cleanup | 113 | + | |
| 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 only | 118 | // 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 class | 130 | // 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 panes | 194 | // 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 stack | 196 | // Find if this node is part of a stack | ||
| @@ -164,19 +224,31 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) { | |||||
| 164 | // Hide all panes in stack | 224 | // 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 pane | 240 | // 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 bridge | 254 | // notifyJavaScript dispatches focus events to the JavaScript bridge | ||
| @@ -275,11 +347,14 @@ func (fm *FocusManager) verifyOnlyOneActivePaneOrPanic() { | |||||
| 275 | // Check all leaf panes in the workspace | 347 | // 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 { | |||
| 19 | 19 | parent *paneNode | |
| 20 | 20 | left *paneNode | |
| 21 | 21 | 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 | + | ||
| 26 | 29 | // Window type tracking | |
| 27 | 30 | windowType webkit.WindowType // Tab or Popup | |
| 28 | 31 | windowFeatures *webkit.WindowFeatures // Features if popup | |
| @@ -30,12 +33,13 @@ type paneNode struct { | |||
| 30 | 33 | parentPane *paneNode // Parent for related views | |
| 31 | 34 | autoClose bool // Auto-close on OAuth success | |
| 32 | 35 | hoverToken uintptr | |
| 33 | - | // Stacked panes support | |
| 36 | + | ||
| 37 | + | // Stacked panes support with proper widget management | |
| 34 | 38 | isStacked bool // Whether this node contains stacked panes | |
| 35 | 39 | stackedPanes []*paneNode // List of stacked panes (if isStacked) | |
| 36 | 40 | 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) | |
| 39 | 43 | } | |
| 40 | 44 | ||
| 41 | 45 | // WorkspaceManager coordinates Zellij-style pane operations. | |
| @@ -74,6 +78,10 @@ type WorkspaceManager struct { | |||
| 74 | 78 | ||
| 75 | 79 | // Current focused pane (Single Source of Truth for focus) | |
| 76 | 80 | currentlyFocused *paneNode | |
| 81 | + | ||
| 82 | + | // Widget operation synchronization to prevent GTK race conditions | |
| 83 | + | widgetMutex sync.Mutex | |
| 84 | + | widgetRegistry *WidgetRegistry | |
| 77 | 85 | } | |
| 78 | 86 | ||
| 79 | 87 | const ( | |
| @@ -92,6 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 92 | 100 | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |
| 93 | 101 | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 94 | 102 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | |
| 103 | + | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | |
| 95 | 104 | } | |
| 96 | 105 | ||
| 97 | 106 | // Initialize specialized managers | |
| @@ -120,10 +129,12 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 120 | 129 | ||
| 121 | 130 | rootContainer := rootPane.webView.RootWidget() | |
| 122 | 131 | root := &paneNode{ | |
| 123 | - | pane: rootPane, | |
| 124 | - | container: rootContainer, | |
| 125 | - | isLeaf: true, | |
| 132 | + | pane: rootPane, | |
| 133 | + | isLeaf: true, | |
| 126 | 134 | } | |
| 135 | + | // Initialize widgets properly | |
| 136 | + | manager.initializePaneWidgets(root, rootContainer) | |
| 137 | + | ||
| 127 | 138 | manager.root = root | |
| 128 | 139 | manager.mainPane = root | |
| 129 | 140 | manager.viewToNode[rootPane.webView] = root | |
| @@ -485,6 +496,11 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string { | |||
| 485 | 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 | 504 | /* Stacked panes styling */ | |
| 489 | 505 | .stacked-pane-container { | |
| 490 | 506 | background-color: %s; | |
| @@ -526,6 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string { | |||
| 526 | 542 | inactiveBorderColor, | |
| 527 | 543 | styling.BorderRadius, | |
| 528 | 544 | styling.BorderColor, | |
| 545 | + | styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color | |
| 529 | 546 | // Additional parameters for stacked pane styles | |
| 530 | 547 | windowBackgroundColor, // stacked-pane-container background | |
| 531 | 548 | styling.BorderRadius, // stacked-pane-container border-radius | |
| @@ -564,13 +581,16 @@ func (wm *WorkspaceManager) ensurePaneBaseClasses() { | |||
| 564 | 581 | ||
| 565 | 582 | leaves := wm.collectLeaves() | |
| 566 | 583 | 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 | + | }) | |
| 574 | 594 | } | |
| 575 | 595 | } | |
| 576 | 596 | } | |
| @@ -601,20 +621,24 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) { | |||
| 601 | 621 | if wm == nil || node == nil || !node.isLeaf { | |
| 602 | 622 | return | |
| 603 | 623 | } | |
| 604 | - | if node.container == 0 || node.hoverToken != 0 { | |
| 624 | + | if node.container == nil || node.hoverToken != 0 { | |
| 605 | 625 | return | |
| 606 | 626 | } | |
| 607 | 627 | ||
| 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 | |
| 614 | 638 | }) | |
| 615 | 639 | node.hoverToken = token | |
| 616 | 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 | 646 | if wm == nil || node == nil || node.hoverToken == 0 { | |
| 623 | 647 | 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 | 653 | node.hoverToken = 0 | |
| 627 | 654 | } | |
| 628 | 655 | ||
| @@ -724,7 +751,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool { | |||
| 724 | 751 | const focusEpsilon = 1e-3 | |
| 725 | 752 | ||
| 726 | 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 | 755 | return false | |
| 729 | 756 | } | |
| 730 | 757 | ||
| @@ -734,7 +761,12 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||
| 734 | 761 | 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 | 770 | if !ok { | |
| 739 | 771 | log.Printf("[workspace] unable to compute bounds for active pane") | |
| 740 | 772 | return false | |
| @@ -749,10 +781,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||
| 749 | 781 | var debugCandidates []string | |
| 750 | 782 | ||
| 751 | 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 | 785 | 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 | 794 | if !ok { | |
| 757 | 795 | continue | |
| 758 | 796 | } | |
| @@ -809,11 +847,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||
| 809 | 847 | } | |
| 810 | 848 | ||
| 811 | 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 | 851 | 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 | 860 | if !ok { | |
| 818 | 861 | return nil | |
| 819 | 862 | } | |
| @@ -858,10 +901,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6 | |||
| 858 | 901 | bestScore := math.MaxFloat64 | |
| 859 | 902 | var best *paneNode | |
| 860 | 903 | for _, leaf := range leaves { | |
| 861 | - | if leaf == nil || leaf.container == 0 { | |
| 904 | + | if leaf == nil || leaf.container == nil { | |
| 862 | 905 | 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 | 914 | if !ok { | |
| 866 | 915 | continue | |
| 867 | 916 | } | |
| @@ -1026,18 +1075,18 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1026 | 1075 | ||
| 1027 | 1076 | // Determine what we're actually splitting | |
| 1028 | 1077 | var splitTarget *paneNode | |
| 1029 | - | var splitTargetContainer uintptr | |
| 1078 | + | var splitTargetContainer *SafeWidget | |
| 1030 | 1079 | ||
| 1031 | 1080 | if stackContainer != nil { | |
| 1032 | 1081 | // Case: splitting from inside a stack - we split around the entire stack | |
| 1033 | 1082 | splitTarget = stackContainer | |
| 1034 | 1083 | 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 | 1085 | } else { | |
| 1037 | 1086 | // Case: normal split from a simple pane | |
| 1038 | 1087 | splitTarget = target | |
| 1039 | 1088 | 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 | 1092 | newView, err := wm.createWebView() | |
| @@ -1063,22 +1112,28 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1063 | 1112 | webkit.WidgetRealizeInContainer(newContainer) | |
| 1064 | 1113 | ||
| 1065 | 1114 | // Use the determined split target container | |
| 1066 | - | if splitTargetContainer == 0 { | |
| 1115 | + | if splitTargetContainer == nil { | |
| 1067 | 1116 | return nil, errors.New("split target missing container") | |
| 1068 | 1117 | } | |
| 1069 | 1118 | ||
| 1070 | 1119 | // Only apply GTK operations if this is a simple widget (leaf pane) | |
| 1071 | 1120 | // For stacked containers, we need to be more careful | |
| 1072 | 1121 | 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 | + | }) | |
| 1076 | 1128 | } else if splitTarget.isStacked { | |
| 1077 | 1129 | // 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 | + | }) | |
| 1082 | 1137 | } | |
| 1083 | 1138 | ||
| 1084 | 1139 | orientation, existingFirst := mapDirection(direction) | |
| @@ -1095,16 +1150,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1095 | 1150 | webkit.PanedSetResizeEnd(paned, true) | |
| 1096 | 1151 | ||
| 1097 | 1152 | newLeaf := &paneNode{ | |
| 1098 | - | pane: newPane, | |
| 1099 | - | container: newContainer, | |
| 1100 | - | isLeaf: true, | |
| 1153 | + | pane: newPane, | |
| 1154 | + | isLeaf: true, | |
| 1101 | 1155 | } | |
| 1156 | + | wm.initializePaneWidgets(newLeaf, newContainer) | |
| 1157 | + | ||
| 1102 | 1158 | split := &paneNode{ | |
| 1103 | 1159 | parent: splitTarget.parent, | |
| 1104 | - | container: paned, | |
| 1105 | 1160 | orientation: orientation, | |
| 1106 | 1161 | isLeaf: false, | |
| 1107 | 1162 | } | |
| 1163 | + | wm.initializePaneWidgets(split, paned) | |
| 1108 | 1164 | ||
| 1109 | 1165 | parent := split.parent | |
| 1110 | 1166 | ||
| @@ -1116,19 +1172,25 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1116 | 1172 | wm.window.SetChild(0) | |
| 1117 | 1173 | } | |
| 1118 | 1174 | // 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 { | |
| 1123 | 1182 | // Split target has a parent paned - remove it (automatically unparents in GTK4) | |
| 1124 | 1183 | 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 | + | }) | |
| 1132 | 1194 | } | |
| 1133 | 1195 | ||
| 1134 | 1196 | // Set up the tree structure first | |
| @@ -1158,13 +1220,19 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1158 | 1220 | ||
| 1159 | 1221 | // Add both containers to the new paned | |
| 1160 | 1222 | 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 | + | }) | |
| 1164 | 1229 | } 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 | + | }) | |
| 1168 | 1236 | } | |
| 1169 | 1237 | ||
| 1170 | 1238 | // Attach the new paned to its parent | |
| @@ -1175,14 +1243,17 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 1175 | 1243 | log.Printf("[workspace] paned set as window child: paned=%#x", paned) | |
| 1176 | 1244 | } | |
| 1177 | 1245 | } 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 | + | }) | |
| 1186 | 1257 | } | |
| 1187 | 1258 | ||
| 1188 | 1259 | webkit.WidgetShow(paned) | |
| @@ -1278,19 +1349,25 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1278 | 1349 | return wm.stackedPaneManager.CloseStackedPane(node) | |
| 1279 | 1350 | } | |
| 1280 | 1351 | ||
| 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 { | |
| 1290 | 1364 | // 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 | + | }) | |
| 1294 | 1371 | } | |
| 1295 | 1372 | ||
| 1296 | 1373 | remaining := len(wm.app.panes) | |
| @@ -1316,24 +1393,30 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1316 | 1393 | if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil { | |
| 1317 | 1394 | if sibling.isPopup && !node.isPopup { | |
| 1318 | 1395 | parentNode := node.parent | |
| 1319 | - | parentContainer := uintptr(0) | |
| 1396 | + | var parentContainer *SafeWidget | |
| 1320 | 1397 | if parentNode != nil { | |
| 1321 | 1398 | parentContainer = parentNode.container | |
| 1322 | 1399 | } | |
| 1323 | 1400 | ||
| 1324 | 1401 | // Detach remaining GTK widgets before destroying the WebViews so GTK | |
| 1325 | 1402 | // 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 | + | }) | |
| 1332 | 1412 | } | |
| 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 | + | }) | |
| 1335 | 1418 | } | |
| 1336 | - | if parentContainer != 0 && wm.window != nil { | |
| 1419 | + | if parentContainer != nil && wm.window != nil { | |
| 1337 | 1420 | wm.window.SetChild(0) | |
| 1338 | 1421 | } | |
| 1339 | 1422 | ||
| @@ -1420,12 +1503,13 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1420 | 1503 | replacement.parent = nil | |
| 1421 | 1504 | ||
| 1422 | 1505 | // 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 | + | }) | |
| 1429 | 1513 | } | |
| 1430 | 1514 | ||
| 1431 | 1515 | // Focus a suitable pane | |
| @@ -1474,12 +1558,6 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1474 | 1558 | ||
| 1475 | 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 | 1561 | // Clean up workspace tracking | |
| 1484 | 1562 | node.pane.CleanupFromWorkspace(wm) | |
| 1485 | 1563 | ||
| @@ -1487,97 +1565,71 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1487 | 1565 | wm.mainPane = nil | |
| 1488 | 1566 | } | |
| 1489 | 1567 | ||
| 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 | |
| 1491 | 1569 | if wm.currentlyFocused == node { | |
| 1492 | - | wm.currentlyFocused = focusTarget | |
| 1493 | - | if focusTarget != nil { | |
| 1494 | - | wm.focusManager.SetActivePane(focusTarget) | |
| 1495 | - | } | |
| 1570 | + | wm.currentlyFocused = nil | |
| 1496 | 1571 | } | |
| 1497 | 1572 | ||
| 1498 | 1573 | grand := parent.parent | |
| 1499 | 1574 | if grand == nil { | |
| 1500 | 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 | 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 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 | |
| 1510 | 1579 | wm.root = sibling | |
| 1511 | 1580 | sibling.parent = nil | |
| 1512 | 1581 | ||
| 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 | + | }) | |
| 1526 | 1591 | } | |
| 1527 | 1592 | } else { | |
| 1528 | 1593 | // Parent has a grandparent, so promote sibling to take parent's place | |
| 1529 | 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 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 | |
| 1553 | 1597 | 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 | |
| 1563 | 1602 | } | |
| 1564 | - | } | |
| 1565 | 1603 | ||
| 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 | + | }) | |
| 1569 | 1620 | } | |
| 1570 | 1621 | ||
| 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 | + | } | |
| 1577 | 1628 | ||
| 1578 | - | // Destroy the webview and detach hover AFTER all hierarchy changes are complete | |
| 1629 | + | // Clean up the node being closed | |
| 1579 | 1630 | ensureCleanup() | |
| 1580 | 1631 | wm.detachHover(node) | |
| 1632 | + | ||
| 1581 | 1633 | switch { | |
| 1582 | 1634 | case node.isPopup: | |
| 1583 | 1635 | if err := node.pane.webView.Destroy(); err != nil { | |
| @@ -1612,6 +1664,91 @@ func (wm *WorkspaceManager) leftmostLeaf(node *paneNode) *paneNode { | |||
| 1612 | 1664 | 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 | 1752 | // findReplacementRoot finds a suitable replacement when closing the current root pane | |
| 1616 | 1753 | func (wm *WorkspaceManager) findReplacementRoot(excludeNode *paneNode) *paneNode { | |
| 1617 | 1754 | if wm == nil || wm.root == nil { | |
| @@ -2302,8 +2439,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 2302 | 2439 | webkit.WidgetRealizeInContainer(webViewWidget) | |
| 2303 | 2440 | } | |
| 2304 | 2441 | ||
| 2305 | - | existingContainer := target.container | |
| 2306 | - | if existingContainer == 0 { | |
| 2442 | + | if target.container == nil { | |
| 2307 | 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 | 2456 | webkit.PanedSetResizeEnd(paned, true) | |
| 2321 | 2457 | ||
| 2322 | 2458 | newLeaf := &paneNode{ | |
| 2323 | - | pane: newPane, | |
| 2324 | - | container: newContainer, | |
| 2325 | - | isLeaf: true, | |
| 2459 | + | pane: newPane, | |
| 2460 | + | isLeaf: true, | |
| 2326 | 2461 | } | |
| 2462 | + | wm.initializePaneWidgets(newLeaf, newContainer) | |
| 2463 | + | ||
| 2327 | 2464 | split := &paneNode{ | |
| 2328 | 2465 | parent: target.parent, | |
| 2329 | - | container: paned, | |
| 2330 | 2466 | orientation: orientation, | |
| 2331 | 2467 | isLeaf: false, | |
| 2332 | 2468 | } | |
| 2469 | + | wm.initializePaneWidgets(split, paned) | |
| 2333 | 2470 | ||
| 2334 | 2471 | parent := split.parent | |
| 2335 | 2472 | ||
| 2336 | 2473 | // Detach existing container from its current GTK parent before inserting into new paned. | |
| 2474 | + | existingContainer := target.getContainerPtr() | |
| 2337 | 2475 | if parent == nil { | |
| 2338 | 2476 | // Target is the root - remove it from the window | |
| 2339 | 2477 | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | |
| 2340 | 2478 | if wm.window != nil { | |
| 2341 | 2479 | wm.window.SetChild(0) | |
| 2342 | 2480 | } | |
| 2343 | - | } else if parent.container != 0 { | |
| 2481 | + | } else if parent.container != nil { | |
| 2344 | 2482 | // 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) | |
| 2350 | 2496 | } | |
| 2351 | - | webkit.WidgetQueueAllocate(parent.container) | |
| 2352 | 2497 | } | |
| 2353 | 2498 | ||
| 2354 | 2499 | // Add both containers to the new paned | |
| @@ -2377,16 +2522,23 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 2377 | 2522 | log.Printf("[workspace] paned set as window child: paned=%#x", paned) | |
| 2378 | 2523 | } | |
| 2379 | 2524 | } 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) | |
| 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 | 2544 | webkit.WidgetShow(paned) | |
| @@ -19,10 +19,13 @@ type paneNode struct { | |||||
| 19 | parent *paneNode | 19 | parent *paneNode | ||
| 20 | left *paneNode | 20 | left *paneNode | ||
| 21 | right *paneNode | 21 | right *paneNode | ||
| 22 | - | container uintptr // GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, stable WebView container for leaves | 22 | + | |
| 23 | - | orientation webkit.Orientation | 23 | + | // Widget management with proper lifecycle tracking |
| 24 | - | isLeaf bool | 24 | + | container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves) |
| 25 | - | isPopup bool // Deprecated: use windowType instead | 25 | + | orientation webkit.Orientation |
| 26 | + | isLeaf bool | |||
| 27 | + | isPopup bool // Deprecated: use windowType instead | |||
| 28 | + | ||||
| 26 | // Window type tracking | 29 | // Window type tracking | ||
| 27 | windowType webkit.WindowType // Tab or Popup | 30 | windowType webkit.WindowType // Tab or Popup | ||
| 28 | windowFeatures *webkit.WindowFeatures // Features if popup | 31 | windowFeatures *webkit.WindowFeatures // Features if popup | ||
| @@ -30,12 +33,13 @@ type paneNode struct { | |||||
| 30 | parentPane *paneNode // Parent for related views | 33 | parentPane *paneNode // Parent for related views | ||
| 31 | autoClose bool // Auto-close on OAuth success | 34 | autoClose bool // Auto-close on OAuth success | ||
| 32 | hoverToken uintptr | 35 | hoverToken uintptr | ||
| 33 | - | // Stacked panes support | 36 | + | |
| 37 | + | // Stacked panes support with proper widget management | |||
| 34 | isStacked bool // Whether this node contains stacked panes | 38 | 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 stack | 40 | 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 *paneNode | 80 | 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 deduplicator | 102 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | ||
| 103 | + | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | |||
| 95 | } | 104 | } | ||
| 96 | 105 | ||||
| 97 | // Initialize specialized managers | 106 | // 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 = root | 138 | manager.root = root | ||
| 128 | manager.mainPane = root | 139 | manager.mainPane = root | ||
| 129 | manager.viewToNode[rootPane.webView] = root | 140 | 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 styles | 546 | // Additional parameters for stacked pane styles | ||
| 530 | windowBackgroundColor, // stacked-pane-container background | 547 | windowBackgroundColor, // stacked-pane-container background | ||
| 531 | styling.BorderRadius, // stacked-pane-container border-radius | 548 | 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 | return | 622 | return | ||
| 603 | } | 623 | } | ||
| 604 | - | if node.container == 0 || node.hoverToken != 0 { | 624 | + | if node.container == nil || node.hoverToken != 0 { |
| 605 | return | 625 | 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 | - | return | 630 | + | token = webkit.WidgetAddHoverHandler(containerPtr, func() { |
| 611 | - | } | 631 | + | if wm == nil { |
| 612 | - | wm.focusManager.SetActivePane(node) | 632 | + | return |
| 613 | - | wm.currentlyFocused = node | 633 | + | } |
| 634 | + | wm.focusManager.SetActivePane(node) | |||
| 635 | + | wm.currentlyFocused = node | |||
| 636 | + | }) | |||
| 637 | + | return nil | |||
| 614 | }) | 638 | }) | ||
| 615 | node.hoverToken = token | 639 | 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 | return | 647 | 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 = 0 | 653 | node.hoverToken = 0 | ||
| 627 | } | 654 | } | ||
| 628 | 655 | ||||
| @@ -724,7 +751,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool { | |||||
| 724 | const focusEpsilon = 1e-3 | 751 | 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 false | 755 | return false | ||
| 729 | } | 756 | } | ||
| 730 | 757 | ||||
| @@ -734,7 +761,12 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||||
| 734 | return true | 761 | 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 false | 772 | return false | ||
| @@ -749,10 +781,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool { | |||||
| 749 | var debugCandidates []string | 781 | 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 | continue | 785 | 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 | continue | 795 | 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 nil | 851 | 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 nil | 861 | return nil | ||
| 819 | } | 862 | } | ||
| @@ -858,10 +901,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6 | |||||
| 858 | bestScore := math.MaxFloat64 | 901 | bestScore := math.MaxFloat64 | ||
| 859 | var best *paneNode | 902 | 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 | continue | 905 | 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 | continue | 915 | continue | ||
| 867 | } | 916 | } | ||
| @@ -1026,18 +1075,18 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||||
| 1026 | 1075 | ||||
| 1027 | // Determine what we're actually splitting | 1076 | // Determine what we're actually splitting | ||
| 1028 | var splitTarget *paneNode | 1077 | var splitTarget *paneNode | ||
| 1029 | - | var splitTargetContainer uintptr | 1078 | + | 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 stack | 1081 | // Case: splitting from inside a stack - we split around the entire stack | ||
| 1033 | splitTarget = stackContainer | 1082 | splitTarget = stackContainer | ||
| 1034 | splitTargetContainer = stackContainer.container | 1083 | 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 pane | 1086 | // Case: normal split from a simple pane | ||
| 1038 | splitTarget = target | 1087 | splitTarget = target | ||
| 1039 | splitTargetContainer = target.container | 1088 | 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 container | 1114 | // 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 careful | 1120 | // 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 splitting | 1129 | // 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.parent | 1165 | 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 parent | 1174 | // 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 needed | 1189 | + | } |
| 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 first | 1196 | // 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 paned | 1221 | // 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 parent | 1238 | // 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 0 | 1355 | + | 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 0 | 1358 | + | 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 above | 1364 | // 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.parent | 1395 | parentNode := node.parent | ||
| 1319 | - | parentContainer := uintptr(0) | 1396 | + | var parentContainer *SafeWidget |
| 1320 | if parentNode != nil { | 1397 | if parentNode != nil { | ||
| 1321 | parentContainer = parentNode.container | 1398 | parentContainer = parentNode.container | ||
| 1322 | } | 1399 | } | ||
| 1323 | 1400 | ||||
| 1324 | // Detach remaining GTK widgets before destroying the WebViews so GTK | 1401 | // 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 = nil | 1503 | 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 pane | 1515 | // 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 tracking | 1561 | // 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 = nil | 1565 | wm.mainPane = nil | ||
| 1488 | } | 1566 | } | ||
| 1489 | 1567 | ||||
| 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 |
| 1491 | if wm.currentlyFocused == node { | 1569 | if wm.currentlyFocused == node { | ||
| 1492 | - | wm.currentlyFocused = focusTarget | 1570 | + | wm.currentlyFocused = nil |
| 1493 | - | if focusTarget != nil { | |||
| 1494 | - | wm.focusManager.SetActivePane(focusTarget) | |||
| 1495 | - | } | |||
| 1496 | } | 1571 | } | ||
| 1497 | 1572 | ||||
| 1498 | grand := parent.parent | 1573 | 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 segfault | 1578 | + | // 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 = sibling | 1579 | wm.root = sibling | ||
| 1511 | sibling.parent = nil | 1580 | sibling.parent = nil | ||
| 1512 | 1581 | ||||
| 1513 | - | // Unparent the sibling from the paned first, then set it as window child | 1582 | + | // 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 reparenting | 1584 | + | 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 place | 1593 | // 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 segfault | 1596 | + | // 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 = grand | 1597 | 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 = sibling | 1600 | + | } 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 visible | 1604 | + | // 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 references | 1622 | + | // Find a suitable focus target |
| 1572 | - | parent.left = nil | 1623 | + | focusTarget := wm.leftmostLeaf(sibling) |
| 1573 | - | parent.right = nil | 1624 | + | if focusTarget != nil { |
| 1574 | - | node.parent = nil | 1625 | + | wm.currentlyFocused = focusTarget |
| 1575 | - | 1626 | + | wm.focusManager.SetActivePane(focusTarget) | |
| 1576 | - | // Focus handling was moved earlier to avoid state gaps | 1627 | + | } |
| 1577 | 1628 | ||||
| 1578 | - | // Destroy the webview and detach hover AFTER all hierarchy changes are complete | 1629 | + | // 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 node | 1664 | 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 pane | 1752 | // 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.container | 2442 | + | 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.parent | 2471 | 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 window | 2476 | // 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 there | 2482 | // 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 paned | 2499 | // 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 = split | 2526 | + | 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 = split | 2529 | + | 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) | ||