fix(workspace): enhance widget safety and fix isStacked semantics in supporting components
- commit
- 505bddb0b2d239dbb341e97add9569db1dcba252
- author
- Brice · · 2025-09-30 05:15 UTC
- parent
- 05fb5c2
- signature
- unsigned
4 files · +62 -58
M internal/app/browser/stacked_panes.go +12 -3
| @@ -225,8 +225,10 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||
| 225 | 225 | // Immediately reattach the stack wrapper to minimize visibility gap | |
| 226 | 226 | spm.reattachToParent(stackWrapperContainer, target, parent) | |
| 227 | 227 | ||
| 228 | - | // Convert target to a stacked leaf node | |
| 229 | - | target.isStacked = true | |
| 228 | + | // Keep target as a regular leaf node (it's just inside a stack now) | |
| 229 | + | // Individual panes within a stack should NOT be marked as isStacked | |
| 230 | + | // Only the stack container itself has isStacked=true | |
| 231 | + | target.isStacked = false | |
| 230 | 232 | target.isLeaf = true | |
| 231 | 233 | // container stays the same (existingContainer) | |
| 232 | 234 | spm.wm.setTitleBar(target, titleBar) | |
| @@ -264,10 +266,17 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||
| 264 | 266 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | |
| 265 | 267 | ||
| 266 | 268 | // Unparent the new container before adding it to the stack | |
| 269 | + | // This is critical: even freshly created WebViews might have internal parent refs | |
| 267 | 270 | if newLeaf.container != nil { | |
| 268 | 271 | newLeaf.container.Execute(func(containerPtr uintptr) error { | |
| 269 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 272 | + | parent := webkit.WidgetGetParent(containerPtr) | |
| 273 | + | if parent != 0 { | |
| 274 | + | log.Printf("[workspace] unparenting new pane container %#x from parent %#x before stack append", containerPtr, parent) | |
| 270 | 275 | webkit.WidgetUnparent(containerPtr) | |
| 276 | + | // Verify unparent succeeded | |
| 277 | + | if finalParent := webkit.WidgetGetParent(containerPtr); finalParent != 0 { | |
| 278 | + | log.Printf("[workspace] WARNING: container %#x still has parent %#x after unparent", containerPtr, finalParent) | |
| 279 | + | } | |
| 271 | 280 | } | |
| 272 | 281 | return nil | |
| 273 | 282 | }) | |
| @@ -225,8 +225,10 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||||
| 225 | // Immediately reattach the stack wrapper to minimize visibility gap | 225 | // Immediately reattach the stack wrapper to minimize visibility gap | ||
| 226 | spm.reattachToParent(stackWrapperContainer, target, parent) | 226 | spm.reattachToParent(stackWrapperContainer, target, parent) | ||
| 227 | 227 | ||||
| 228 | - | // Convert target to a stacked leaf node | 228 | + | // Keep target as a regular leaf node (it's just inside a stack now) |
| 229 | - | target.isStacked = true | 229 | + | // Individual panes within a stack should NOT be marked as isStacked |
| 230 | + | // Only the stack container itself has isStacked=true | |||
| 231 | + | target.isStacked = false | |||
| 230 | target.isLeaf = true | 232 | target.isLeaf = true | ||
| 231 | // container stays the same (existingContainer) | 233 | // container stays the same (existingContainer) | ||
| 232 | spm.wm.setTitleBar(target, titleBar) | 234 | spm.wm.setTitleBar(target, titleBar) | ||
| @@ -264,10 +266,17 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||||
| 264 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | 266 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | ||
| 265 | 267 | ||||
| 266 | // Unparent the new container before adding it to the stack | 268 | // Unparent the new container before adding it to the stack | ||
| 269 | + | // This is critical: even freshly created WebViews might have internal parent refs | |||
| 267 | if newLeaf.container != nil { | 270 | if newLeaf.container != nil { | ||
| 268 | newLeaf.container.Execute(func(containerPtr uintptr) error { | 271 | newLeaf.container.Execute(func(containerPtr uintptr) error { | ||
| 269 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | 272 | + | parent := webkit.WidgetGetParent(containerPtr) |
| 273 | + | if parent != 0 { | |||
| 274 | + | log.Printf("[workspace] unparenting new pane container %#x from parent %#x before stack append", containerPtr, parent) | |||
| 270 | webkit.WidgetUnparent(containerPtr) | 275 | webkit.WidgetUnparent(containerPtr) | ||
| 276 | + | // Verify unparent succeeded | |||
| 277 | + | if finalParent := webkit.WidgetGetParent(containerPtr); finalParent != 0 { | |||
| 278 | + | log.Printf("[workspace] WARNING: container %#x still has parent %#x after unparent", containerPtr, finalParent) | |||
| 279 | + | } | |||
| 271 | } | 280 | } | ||
| 272 | return nil | 281 | return nil | ||
| 273 | }) | 282 | }) | ||
M internal/app/browser/workspace_state_tombstone.go +7 -7
| @@ -18,7 +18,7 @@ type StateTombstone struct { | |||
| 18 | 18 | ID string | |
| 19 | 19 | Timestamp time.Time | |
| 20 | 20 | Operation string | |
| 21 | - | TreeState *TreeSnapshot | |
| 21 | + | TreeState *TombstoneTreeSnapshot | |
| 22 | 22 | WidgetState *WidgetSnapshot | |
| 23 | 23 | FocusState *FocusSnapshot | |
| 24 | 24 | StackState *StackSnapshot | |
| @@ -26,8 +26,8 @@ type StateTombstone struct { | |||
| 26 | 26 | Size int | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | - | // TreeSnapshot captures the tree structure | |
| 30 | - | type TreeSnapshot struct { | |
| 29 | + | // TombstoneTreeSnapshot captures the tree structure for state tombstones | |
| 30 | + | type TombstoneTreeSnapshot struct { | |
| 31 | 31 | RootID string | |
| 32 | 32 | MainPaneID string | |
| 33 | 33 | Nodes map[string]*NodeSnapshot | |
| @@ -106,7 +106,7 @@ var gobRegisterOnce sync.Once | |||
| 106 | 106 | ||
| 107 | 107 | func registerTombstoneGobTypes() { | |
| 108 | 108 | gobRegisterOnce.Do(func() { | |
| 109 | - | gob.Register(&TreeSnapshot{}) | |
| 109 | + | gob.Register(&TombstoneTreeSnapshot{}) | |
| 110 | 110 | gob.Register(&NodeSnapshot{}) | |
| 111 | 111 | gob.Register(&WidgetSnapshot{}) | |
| 112 | 112 | gob.Register(&FocusSnapshot{}) | |
| @@ -207,8 +207,8 @@ func (stm *StateTombstoneManager) CaptureState(operation string) (*StateTombston | |||
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | 209 | // captureTreeState captures the current tree structure | |
| 210 | - | func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) { | |
| 211 | - | snapshot := &TreeSnapshot{ | |
| 210 | + | func (stm *StateTombstoneManager) captureTreeState() (*TombstoneTreeSnapshot, error) { | |
| 211 | + | snapshot := &TombstoneTreeSnapshot{ | |
| 212 | 212 | Nodes: make(map[string]*NodeSnapshot), | |
| 213 | 213 | ParentLinks: make(map[string]string), | |
| 214 | 214 | ChildLinks: make(map[string][2]string), | |
| @@ -232,7 +232,7 @@ func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) { | |||
| 232 | 232 | } | |
| 233 | 233 | ||
| 234 | 234 | // captureNodeRecursive recursively captures node information | |
| 235 | - | func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TreeSnapshot) { | |
| 235 | + | func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TombstoneTreeSnapshot) { | |
| 236 | 236 | if node == nil { | |
| 237 | 237 | return | |
| 238 | 238 | } | |
| @@ -18,7 +18,7 @@ type StateTombstone struct { | |||||
| 18 | ID string | 18 | ID string | ||
| 19 | Timestamp time.Time | 19 | Timestamp time.Time | ||
| 20 | Operation string | 20 | Operation string | ||
| 21 | - | TreeState *TreeSnapshot | 21 | + | TreeState *TombstoneTreeSnapshot |
| 22 | WidgetState *WidgetSnapshot | 22 | WidgetState *WidgetSnapshot | ||
| 23 | FocusState *FocusSnapshot | 23 | FocusState *FocusSnapshot | ||
| 24 | StackState *StackSnapshot | 24 | StackState *StackSnapshot | ||
| @@ -26,8 +26,8 @@ type StateTombstone struct { | |||||
| 26 | Size int | 26 | Size int | ||
| 27 | } | 27 | } | ||
| 28 | 28 | ||||
| 29 | - | // TreeSnapshot captures the tree structure | 29 | + | // TombstoneTreeSnapshot captures the tree structure for state tombstones |
| 30 | - | type TreeSnapshot struct { | 30 | + | type TombstoneTreeSnapshot struct { |
| 31 | RootID string | 31 | RootID string | ||
| 32 | MainPaneID string | 32 | MainPaneID string | ||
| 33 | Nodes map[string]*NodeSnapshot | 33 | Nodes map[string]*NodeSnapshot | ||
| @@ -106,7 +106,7 @@ var gobRegisterOnce sync.Once | |||||
| 106 | 106 | ||||
| 107 | func registerTombstoneGobTypes() { | 107 | func registerTombstoneGobTypes() { | ||
| 108 | gobRegisterOnce.Do(func() { | 108 | gobRegisterOnce.Do(func() { | ||
| 109 | - | gob.Register(&TreeSnapshot{}) | 109 | + | gob.Register(&TombstoneTreeSnapshot{}) |
| 110 | gob.Register(&NodeSnapshot{}) | 110 | gob.Register(&NodeSnapshot{}) | ||
| 111 | gob.Register(&WidgetSnapshot{}) | 111 | gob.Register(&WidgetSnapshot{}) | ||
| 112 | gob.Register(&FocusSnapshot{}) | 112 | gob.Register(&FocusSnapshot{}) | ||
| @@ -207,8 +207,8 @@ func (stm *StateTombstoneManager) CaptureState(operation string) (*StateTombston | |||||
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
| 209 | // captureTreeState captures the current tree structure | 209 | // captureTreeState captures the current tree structure | ||
| 210 | - | func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) { | 210 | + | func (stm *StateTombstoneManager) captureTreeState() (*TombstoneTreeSnapshot, error) { |
| 211 | - | snapshot := &TreeSnapshot{ | 211 | + | snapshot := &TombstoneTreeSnapshot{ |
| 212 | Nodes: make(map[string]*NodeSnapshot), | 212 | Nodes: make(map[string]*NodeSnapshot), | ||
| 213 | ParentLinks: make(map[string]string), | 213 | ParentLinks: make(map[string]string), | ||
| 214 | ChildLinks: make(map[string][2]string), | 214 | ChildLinks: make(map[string][2]string), | ||
| @@ -232,7 +232,7 @@ func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) { | |||||
| 232 | } | 232 | } | ||
| 233 | 233 | ||||
| 234 | // captureNodeRecursive recursively captures node information | 234 | // captureNodeRecursive recursively captures node information | ||
| 235 | - | func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TreeSnapshot) { | 235 | + | func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TombstoneTreeSnapshot) { |
| 236 | if node == nil { | 236 | if node == nil { | ||
| 237 | return | 237 | return | ||
| 238 | } | 238 | } | ||
M internal/app/browser/workspace_tree_rebalancer.go +33 -11
| @@ -622,15 +622,14 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction | |||
| 622 | 622 | log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent) | |
| 623 | 623 | webkit.WidgetUnparent(ptr) | |
| 624 | 624 | } else { | |
| 625 | - | // Parent is not a container - likely already attached to window | |
| 626 | - | log.Printf("[tree-rebalancer] widget already attached to non-container parent %#x, skipping unparent", currentParent) | |
| 627 | - | // Just ensure proper expansion and visibility | |
| 628 | - | webkit.WidgetSetHExpand(ptr, true) | |
| 629 | - | webkit.WidgetSetVExpand(ptr, true) | |
| 630 | - | webkit.WidgetQueueAllocate(ptr) | |
| 631 | - | webkit.WidgetShow(ptr) | |
| 632 | - | log.Printf("[tree-rebalancer] promotion window attach successful: widget %#x already has parent %#x", ptr, currentParent) | |
| 633 | - | return nil | |
| 625 | + | // Parent is likely a window or other non-container widget | |
| 626 | + | // For safety, check if it's a toplevel window by attempting unparent | |
| 627 | + | log.Printf("[tree-rebalancer] widget %#x has non-container parent %#x, checking if unparent needed", ptr, currentParent) | |
| 628 | + | if webkit.WidgetIsValid(currentParent) { | |
| 629 | + | // If parent is valid but not a container, try unparent for safety | |
| 630 | + | webkit.WidgetUnparent(ptr) | |
| 631 | + | log.Printf("[tree-rebalancer] unparented widget %#x from non-container parent %#x", ptr, currentParent) | |
| 632 | + | } | |
| 634 | 633 | } | |
| 635 | 634 | } | |
| 636 | 635 | ||
| @@ -687,16 +686,39 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction | |||
| 687 | 686 | if childPtr == 0 || !webkit.WidgetIsValid(childPtr) { | |
| 688 | 687 | return fmt.Errorf("promotion reparent: invalid child widget") | |
| 689 | 688 | } | |
| 690 | - | if webkit.WidgetGetParent(childPtr) != 0 { | |
| 689 | + | ||
| 690 | + | // Check if widget is already correctly parented (closePane.swapContainers already did the work) | |
| 691 | + | currentParent := webkit.WidgetGetParent(childPtr) | |
| 692 | + | if currentParent == parentPtr { | |
| 693 | + | log.Printf("[tree-rebalancer] widget %#x already correctly parented to %#x, skipping reparent", childPtr, parentPtr) | |
| 694 | + | // Just ensure properties are set correctly | |
| 695 | + | webkit.WidgetResetSizeRequest(childPtr) | |
| 696 | + | webkit.WidgetSetHExpand(childPtr, true) | |
| 697 | + | webkit.WidgetSetVExpand(childPtr, true) | |
| 698 | + | webkit.WidgetQueueAllocate(childPtr) | |
| 699 | + | webkit.WidgetQueueAllocate(parentPtr) | |
| 700 | + | return nil | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | // Widget needs reparenting - unparent from wrong parent if needed | |
| 704 | + | if currentParent != 0 { | |
| 705 | + | log.Printf("[tree-rebalancer] unparenting widget %#x from incorrect parent %#x", childPtr, currentParent) | |
| 691 | 706 | webkit.WidgetUnparent(childPtr) | |
| 692 | 707 | } | |
| 693 | 708 | ||
| 694 | - | // Reattach promoted widget to the GtkPaned and request full allocation | |
| 709 | + | // Reattach promoted widget to the GtkPaned | |
| 695 | 710 | if parent.left == node { | |
| 696 | 711 | webkit.PanedSetStartChild(parentPtr, childPtr) | |
| 697 | 712 | } else { | |
| 698 | 713 | webkit.PanedSetEndChild(parentPtr, childPtr) | |
| 699 | 714 | } | |
| 715 | + | ||
| 716 | + | // Verify attachment succeeded before setting properties | |
| 717 | + | finalParent := webkit.WidgetGetParent(childPtr) | |
| 718 | + | if finalParent != parentPtr { | |
| 719 | + | return fmt.Errorf("failed to attach widget %#x to parent %#x (current parent: %#x)", childPtr, parentPtr, finalParent) | |
| 720 | + | } | |
| 721 | + | ||
| 700 | 722 | webkit.WidgetResetSizeRequest(childPtr) | |
| 701 | 723 | webkit.WidgetSetHExpand(childPtr, true) | |
| 702 | 724 | webkit.WidgetSetVExpand(childPtr, true) | |
| @@ -622,15 +622,14 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction | |||||
| 622 | log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent) | 622 | log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent) | ||
| 623 | webkit.WidgetUnparent(ptr) | 623 | webkit.WidgetUnparent(ptr) | ||
| 624 | } else { | 624 | } else { | ||
| 625 | - | // Parent is not a container - likely already attached to window | 625 | + | // Parent is likely a window or other non-container widget |
| 626 | - | log.Printf("[tree-rebalancer] widget already attached to non-container parent %#x, skipping unparent", currentParent) | 626 | + | // For safety, check if it's a toplevel window by attempting unparent |
| 627 | - | // Just ensure proper expansion and visibility | 627 | + | log.Printf("[tree-rebalancer] widget %#x has non-container parent %#x, checking if unparent needed", ptr, currentParent) |
| 628 | - | webkit.WidgetSetHExpand(ptr, true) | 628 | + | if webkit.WidgetIsValid(currentParent) { |
| 629 | - | webkit.WidgetSetVExpand(ptr, true) | 629 | + | // If parent is valid but not a container, try unparent for safety |
| 630 | - | webkit.WidgetQueueAllocate(ptr) | 630 | + | webkit.WidgetUnparent(ptr) |
| 631 | - | webkit.WidgetShow(ptr) | 631 | + | log.Printf("[tree-rebalancer] unparented widget %#x from non-container parent %#x", ptr, currentParent) |
| 632 | - | log.Printf("[tree-rebalancer] promotion window attach successful: widget %#x already has parent %#x", ptr, currentParent) | 632 | + | } |
| 633 | - | return nil | |||
| 634 | } | 633 | } | ||
| 635 | } | 634 | } | ||
| 636 | 635 | ||||
| @@ -687,16 +686,39 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction | |||||
| 687 | if childPtr == 0 || !webkit.WidgetIsValid(childPtr) { | 686 | if childPtr == 0 || !webkit.WidgetIsValid(childPtr) { | ||
| 688 | return fmt.Errorf("promotion reparent: invalid child widget") | 687 | return fmt.Errorf("promotion reparent: invalid child widget") | ||
| 689 | } | 688 | } | ||
| 690 | - | if webkit.WidgetGetParent(childPtr) != 0 { | 689 | + | |
| 690 | + | // Check if widget is already correctly parented (closePane.swapContainers already did the work) | |||
| 691 | + | currentParent := webkit.WidgetGetParent(childPtr) | |||
| 692 | + | if currentParent == parentPtr { | |||
| 693 | + | log.Printf("[tree-rebalancer] widget %#x already correctly parented to %#x, skipping reparent", childPtr, parentPtr) | |||
| 694 | + | // Just ensure properties are set correctly | |||
| 695 | + | webkit.WidgetResetSizeRequest(childPtr) | |||
| 696 | + | webkit.WidgetSetHExpand(childPtr, true) | |||
| 697 | + | webkit.WidgetSetVExpand(childPtr, true) | |||
| 698 | + | webkit.WidgetQueueAllocate(childPtr) | |||
| 699 | + | webkit.WidgetQueueAllocate(parentPtr) | |||
| 700 | + | return nil | |||
| 701 | + | } | |||
| 702 | + | ||||
| 703 | + | // Widget needs reparenting - unparent from wrong parent if needed | |||
| 704 | + | if currentParent != 0 { | |||
| 705 | + | log.Printf("[tree-rebalancer] unparenting widget %#x from incorrect parent %#x", childPtr, currentParent) | |||
| 691 | webkit.WidgetUnparent(childPtr) | 706 | webkit.WidgetUnparent(childPtr) | ||
| 692 | } | 707 | } | ||
| 693 | 708 | ||||
| 694 | - | // Reattach promoted widget to the GtkPaned and request full allocation | 709 | + | // Reattach promoted widget to the GtkPaned |
| 695 | if parent.left == node { | 710 | if parent.left == node { | ||
| 696 | webkit.PanedSetStartChild(parentPtr, childPtr) | 711 | webkit.PanedSetStartChild(parentPtr, childPtr) | ||
| 697 | } else { | 712 | } else { | ||
| 698 | webkit.PanedSetEndChild(parentPtr, childPtr) | 713 | webkit.PanedSetEndChild(parentPtr, childPtr) | ||
| 699 | } | 714 | } | ||
| 715 | + | ||||
| 716 | + | // Verify attachment succeeded before setting properties | |||
| 717 | + | finalParent := webkit.WidgetGetParent(childPtr) | |||
| 718 | + | if finalParent != parentPtr { | |||
| 719 | + | return fmt.Errorf("failed to attach widget %#x to parent %#x (current parent: %#x)", childPtr, parentPtr, finalParent) | |||
| 720 | + | } | |||
| 721 | + | ||||
| 700 | webkit.WidgetResetSizeRequest(childPtr) | 722 | webkit.WidgetResetSizeRequest(childPtr) | ||
| 701 | webkit.WidgetSetHExpand(childPtr, true) | 723 | webkit.WidgetSetHExpand(childPtr, true) | ||
| 702 | webkit.WidgetSetVExpand(childPtr, true) | 724 | webkit.WidgetSetVExpand(childPtr, true) | ||
M internal/app/browser/workspace_utils.go +10 -37
| @@ -2,7 +2,6 @@ | |||
| 2 | 2 | package browser | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "errors" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "log" | |
| 8 | 7 | ||
| @@ -55,36 +54,6 @@ func mapDirection(direction string) (webkit.Orientation, bool) { | |||
| 55 | 54 | ||
| 56 | 55 | // Widget management utilities | |
| 57 | 56 | ||
| 58 | - | // validateWidgetsForReparenting validates that all widgets needed for reparenting are valid | |
| 59 | - | func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error { | |
| 60 | - | // Validate sibling container (this is the most critical one) | |
| 61 | - | if sibling.container == nil { | |
| 62 | - | return errors.New("sibling container is nil") | |
| 63 | - | } | |
| 64 | - | if !sibling.container.IsValid() { | |
| 65 | - | return fmt.Errorf("sibling container %s is not valid", sibling.container.String()) | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | // Validate parent container if present | |
| 69 | - | if parent != nil && parent.container != nil && !parent.container.IsValid() { | |
| 70 | - | return fmt.Errorf("parent container %s is not valid", parent.container.String()) | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | // Validate grandparent container if present | |
| 74 | - | if grand != nil && grand.container != nil && !grand.container.IsValid() { | |
| 75 | - | return fmt.Errorf("grandparent container %s is not valid", grand.container.String()) | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | return nil | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | // safeWidgetOperation performs a widget operation with proper locking and validation | |
| 82 | - | func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error { | |
| 83 | - | wm.widgetMutex.Lock() | |
| 84 | - | defer wm.widgetMutex.Unlock() | |
| 85 | - | return operation() | |
| 86 | - | } | |
| 87 | - | ||
| 88 | 57 | // registerWidget registers a widget with the registry and returns a SafeWidget | |
| 89 | 58 | func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget { | |
| 90 | 59 | if ptr == 0 { | |
| @@ -109,6 +78,10 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||
| 109 | 78 | // Initialize other widget fields as nil (will be set when needed) | |
| 110 | 79 | node.titleBar = nil | |
| 111 | 80 | node.stackWrapper = nil | |
| 81 | + | ||
| 82 | + | // Initialize enhanced close refactoring fields | |
| 83 | + | node.widgetValid = true | |
| 84 | + | node.cleanupGeneration = 0 | |
| 112 | 85 | } | |
| 113 | 86 | ||
| 114 | 87 | // Helper functions for safe widget operations | |
| @@ -223,9 +196,9 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |||
| 223 | 196 | if ctx.borderContainer != nil { | |
| 224 | 197 | var applied bool | |
| 225 | 198 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |
| 226 | - | // Safety check: verify container pointer is valid | |
| 227 | - | if containerPtr == 0 { | |
| 228 | - | return fmt.Errorf("invalid container pointer") | |
| 199 | + | // Safety check: verify container pointer is valid and widget still exists | |
| 200 | + | if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) { | |
| 201 | + | return fmt.Errorf("invalid container pointer or destroyed widget") | |
| 229 | 202 | } | |
| 230 | 203 | if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | |
| 231 | 204 | return nil | |
| @@ -252,9 +225,9 @@ func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | |||
| 252 | 225 | if ctx.borderContainer != nil { | |
| 253 | 226 | var removed bool | |
| 254 | 227 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |
| 255 | - | // Safety check: verify container pointer is valid | |
| 256 | - | if containerPtr == 0 { | |
| 257 | - | return fmt.Errorf("invalid container pointer") | |
| 228 | + | // Safety check: verify container pointer is valid and widget still exists | |
| 229 | + | if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) { | |
| 230 | + | return fmt.Errorf("invalid container pointer or destroyed widget") | |
| 258 | 231 | } | |
| 259 | 232 | if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | |
| 260 | 233 | return nil | |
| @@ -2,7 +2,6 @@ | |||||
| 2 | package browser | 2 | package browser | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "errors" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "log" | 6 | "log" | ||
| 8 | 7 | ||||
| @@ -55,36 +54,6 @@ func mapDirection(direction string) (webkit.Orientation, bool) { | |||||
| 55 | 54 | ||||
| 56 | // Widget management utilities | 55 | // Widget management utilities | ||
| 57 | 56 | ||||
| 58 | - | // validateWidgetsForReparenting validates that all widgets needed for reparenting are valid | |||
| 59 | - | func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error { | |||
| 60 | - | // Validate sibling container (this is the most critical one) | |||
| 61 | - | if sibling.container == nil { | |||
| 62 | - | return errors.New("sibling container is nil") | |||
| 63 | - | } | |||
| 64 | - | if !sibling.container.IsValid() { | |||
| 65 | - | return fmt.Errorf("sibling container %s is not valid", sibling.container.String()) | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | // Validate parent container if present | |||
| 69 | - | if parent != nil && parent.container != nil && !parent.container.IsValid() { | |||
| 70 | - | return fmt.Errorf("parent container %s is not valid", parent.container.String()) | |||
| 71 | - | } | |||
| 72 | - | ||||
| 73 | - | // Validate grandparent container if present | |||
| 74 | - | if grand != nil && grand.container != nil && !grand.container.IsValid() { | |||
| 75 | - | return fmt.Errorf("grandparent container %s is not valid", grand.container.String()) | |||
| 76 | - | } | |||
| 77 | - | ||||
| 78 | - | return nil | |||
| 79 | - | } | |||
| 80 | - | ||||
| 81 | - | // safeWidgetOperation performs a widget operation with proper locking and validation | |||
| 82 | - | func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error { | |||
| 83 | - | wm.widgetMutex.Lock() | |||
| 84 | - | defer wm.widgetMutex.Unlock() | |||
| 85 | - | return operation() | |||
| 86 | - | } | |||
| 87 | - | ||||
| 88 | // registerWidget registers a widget with the registry and returns a SafeWidget | 57 | // registerWidget registers a widget with the registry and returns a SafeWidget | ||
| 89 | func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget { | 58 | func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget { | ||
| 90 | if ptr == 0 { | 59 | if ptr == 0 { | ||
| @@ -109,6 +78,10 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||||
| 109 | // Initialize other widget fields as nil (will be set when needed) | 78 | // Initialize other widget fields as nil (will be set when needed) | ||
| 110 | node.titleBar = nil | 79 | node.titleBar = nil | ||
| 111 | node.stackWrapper = nil | 80 | node.stackWrapper = nil | ||
| 81 | + | ||||
| 82 | + | // Initialize enhanced close refactoring fields | |||
| 83 | + | node.widgetValid = true | |||
| 84 | + | node.cleanupGeneration = 0 | |||
| 112 | } | 85 | } | ||
| 113 | 86 | ||||
| 114 | // Helper functions for safe widget operations | 87 | // Helper functions for safe widget operations | ||
| @@ -223,9 +196,9 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |||||
| 223 | if ctx.borderContainer != nil { | 196 | if ctx.borderContainer != nil { | ||
| 224 | var applied bool | 197 | var applied bool | ||
| 225 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | 198 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | ||
| 226 | - | // Safety check: verify container pointer is valid | 199 | + | // Safety check: verify container pointer is valid and widget still exists |
| 227 | - | if containerPtr == 0 { | 200 | + | if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) { |
| 228 | - | return fmt.Errorf("invalid container pointer") | 201 | + | return fmt.Errorf("invalid container pointer or destroyed widget") |
| 229 | } | 202 | } | ||
| 230 | if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | 203 | if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | ||
| 231 | return nil | 204 | return nil | ||
| @@ -252,9 +225,9 @@ func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | |||||
| 252 | if ctx.borderContainer != nil { | 225 | if ctx.borderContainer != nil { | ||
| 253 | var removed bool | 226 | var removed bool | ||
| 254 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | 227 | if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error { | ||
| 255 | - | // Safety check: verify container pointer is valid | 228 | + | // Safety check: verify container pointer is valid and widget still exists |
| 256 | - | if containerPtr == 0 { | 229 | + | if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) { |
| 257 | - | return fmt.Errorf("invalid container pointer") | 230 | + | return fmt.Errorf("invalid container pointer or destroyed widget") |
| 258 | } | 231 | } | ||
| 259 | if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | 232 | if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | ||
| 260 | return nil | 233 | return nil | ||