feat(browser): implement SafeWidget for thread-safe GTK widget management
- Add SafeWidget wrapper providing thread-safe GTK widget operations - Add WidgetRegistry for centralized widget lifecycle management - Update stacked_panes.go to use SafeWidget pattern throughout - Ensure proper widget validation and memory safety - Maintain compatibility with existing webkit CGO functions
- commit
- 4fb244f931c443f431096b0a3a98ec7fe2cca0f0
- author
- Brice · · 2025-09-27 16:49 UTC
- parent
- 9964d39
- signature
- unsigned
3 files · +283 -77
M internal/app/browser/stacked_panes.go +140 -67
| @@ -82,14 +82,15 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) { | |||
| 82 | 82 | ||
| 83 | 83 | // Create the new leaf node | |
| 84 | 84 | newLeaf := &paneNode{ | |
| 85 | - | pane: newPane, | |
| 86 | - | container: newContainer, | |
| 87 | - | isLeaf: true, | |
| 85 | + | pane: newPane, | |
| 86 | + | isLeaf: true, | |
| 88 | 87 | } | |
| 88 | + | // Initialize widgets properly using workspace manager helper | |
| 89 | + | spm.wm.initializePaneWidgets(newLeaf, newContainer) | |
| 89 | 90 | ||
| 90 | 91 | // Create title bar for the new pane | |
| 91 | 92 | newTitleBar := spm.createTitleBar(newLeaf) | |
| 92 | - | newLeaf.titleBar = newTitleBar | |
| 93 | + | spm.wm.setTitleBar(newLeaf, newTitleBar) | |
| 93 | 94 | ||
| 94 | 95 | // Keep new container hidden initially - will be shown after transition | |
| 95 | 96 | webkit.WidgetSetVisible(newContainer, false) | |
| @@ -129,8 +130,13 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode) | |||
| 129 | 130 | stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane | |
| 130 | 131 | ||
| 131 | 132 | // Unparent the new container before adding it to the stack (only if it has a parent) | |
| 132 | - | if webkit.WidgetGetParent(newLeaf.container) != 0 { | |
| 133 | - | webkit.WidgetUnparent(newLeaf.container) | |
| 133 | + | if newLeaf.container != nil { | |
| 134 | + | newLeaf.container.Execute(func(containerPtr uintptr) error { | |
| 135 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 136 | + | webkit.WidgetUnparent(containerPtr) | |
| 137 | + | } | |
| 138 | + | return nil | |
| 139 | + | }) | |
| 134 | 140 | } | |
| 135 | 141 | ||
| 136 | 142 | // Insert widgets at correct position for Zellij-style layout | |
| @@ -141,19 +147,29 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode) | |||
| 141 | 147 | if insertIndex > 0 { | |
| 142 | 148 | // Insert after the previous pane's container | |
| 143 | 149 | prevPane := stackNode.stackedPanes[insertIndex-1] | |
| 144 | - | insertAfterWidget = prevPane.container | |
| 150 | + | if prevPane.container != nil { | |
| 151 | + | insertAfterWidget = prevPane.container.Ptr() | |
| 152 | + | } | |
| 145 | 153 | } | |
| 146 | 154 | ||
| 147 | 155 | if insertAfterWidget != 0 { | |
| 148 | 156 | // Insert titleBar after the previous pane's container | |
| 149 | - | webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.titleBar, insertAfterWidget) | |
| 157 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { | |
| 158 | + | webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr(), insertAfterWidget) | |
| 159 | + | } | |
| 150 | 160 | // Insert container after the newly inserted titleBar | |
| 151 | - | webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.container, newLeaf.titleBar) | |
| 161 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil && newLeaf.titleBar != nil { | |
| 162 | + | webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr(), newLeaf.titleBar.Ptr()) | |
| 163 | + | } | |
| 152 | 164 | log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget) | |
| 153 | 165 | } else { | |
| 154 | 166 | // Insert at the beginning (insertIndex = 0) | |
| 155 | - | webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.container) | |
| 156 | - | webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.titleBar) | |
| 167 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil { | |
| 168 | + | webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr()) | |
| 169 | + | } | |
| 170 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { | |
| 171 | + | webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr()) | |
| 172 | + | } | |
| 157 | 173 | log.Printf("[workspace] prepended widgets at position 0") | |
| 158 | 174 | } | |
| 159 | 175 | ||
| @@ -199,7 +215,9 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||
| 199 | 215 | ||
| 200 | 216 | // Build the complete stack structure with hidden widgets | |
| 201 | 217 | webkit.BoxAppend(stackInternalBox, titleBar) | |
| 202 | - | webkit.BoxAppend(stackInternalBox, existingContainer) | |
| 218 | + | if existingContainer != nil { | |
| 219 | + | webkit.BoxAppend(stackInternalBox, existingContainer.Ptr()) | |
| 220 | + | } | |
| 203 | 221 | ||
| 204 | 222 | // Immediately reattach the stack wrapper to minimize visibility gap | |
| 205 | 223 | spm.reattachToParent(stackWrapperContainer, target, parent) | |
| @@ -207,19 +225,19 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||
| 207 | 225 | // Convert target to a stacked leaf node | |
| 208 | 226 | target.isStacked = true | |
| 209 | 227 | target.isLeaf = true | |
| 210 | - | target.container = existingContainer | |
| 211 | - | target.titleBar = titleBar | |
| 228 | + | // container stays the same (existingContainer) | |
| 229 | + | spm.wm.setTitleBar(target, titleBar) | |
| 212 | 230 | ||
| 213 | 231 | // Create the stack container node - container points to wrapper, stackWrapper points to internal box | |
| 214 | 232 | stackNode := &paneNode{ | |
| 215 | 233 | isStacked: true, | |
| 216 | 234 | isLeaf: false, | |
| 217 | - | container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.) | |
| 218 | - | stackWrapper: stackInternalBox, // Internal box for stack operations | |
| 219 | 235 | stackedPanes: []*paneNode{target}, | |
| 220 | 236 | activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0) | |
| 221 | 237 | parent: parent, | |
| 222 | 238 | } | |
| 239 | + | spm.wm.setContainer(stackNode, stackWrapperContainer, "stack-wrapper") // Wrapper for GTK operations (splits, etc.) | |
| 240 | + | spm.wm.setStackWrapper(stackNode, stackInternalBox) // Internal box for stack operations | |
| 223 | 241 | ||
| 224 | 242 | // Update target's parent to be the stack node | |
| 225 | 243 | target.parent = stackNode | |
| @@ -243,13 +261,22 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||
| 243 | 261 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | |
| 244 | 262 | ||
| 245 | 263 | // Unparent the new container before adding it to the stack | |
| 246 | - | if webkit.WidgetGetParent(newLeaf.container) != 0 { | |
| 247 | - | webkit.WidgetUnparent(newLeaf.container) | |
| 264 | + | if newLeaf.container != nil { | |
| 265 | + | newLeaf.container.Execute(func(containerPtr uintptr) error { | |
| 266 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 267 | + | webkit.WidgetUnparent(containerPtr) | |
| 268 | + | } | |
| 269 | + | return nil | |
| 270 | + | }) | |
| 248 | 271 | } | |
| 249 | 272 | ||
| 250 | 273 | // Add the new widgets to the internal stack box | |
| 251 | - | webkit.BoxAppend(stackNode.stackWrapper, newLeaf.titleBar) | |
| 252 | - | webkit.BoxAppend(stackNode.stackWrapper, newLeaf.container) | |
| 274 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { | |
| 275 | + | webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr()) | |
| 276 | + | } | |
| 277 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil { | |
| 278 | + | webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr()) | |
| 279 | + | } | |
| 253 | 280 | ||
| 254 | 281 | return stackNode, insertIndex, nil | |
| 255 | 282 | } | |
| @@ -262,16 +289,24 @@ func (spm *StackedPaneManager) detachFromParent(target *paneNode, parent *paneNo | |||
| 262 | 289 | spm.wm.window.SetChild(0) | |
| 263 | 290 | } | |
| 264 | 291 | // Unparent if it has a GTK parent | |
| 265 | - | if webkit.WidgetGetParent(target.container) != 0 { | |
| 266 | - | webkit.WidgetUnparent(target.container) | |
| 292 | + | if target.container != nil { | |
| 293 | + | target.container.Execute(func(containerPtr uintptr) error { | |
| 294 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 295 | + | webkit.WidgetUnparent(containerPtr) | |
| 296 | + | } | |
| 297 | + | return nil | |
| 298 | + | }) | |
| 267 | 299 | } | |
| 268 | - | } else if parent.container != 0 { | |
| 300 | + | } else if parent.container != nil { | |
| 269 | 301 | // Target has a parent paned - remove it (automatically unparents in GTK4) | |
| 270 | - | if parent.left == target { | |
| 271 | - | webkit.PanedSetStartChild(parent.container, 0) | |
| 272 | - | } else if parent.right == target { | |
| 273 | - | webkit.PanedSetEndChild(parent.container, 0) | |
| 274 | - | } | |
| 302 | + | parent.container.Execute(func(panedPtr uintptr) error { | |
| 303 | + | if parent.left == target { | |
| 304 | + | webkit.PanedSetStartChild(panedPtr, 0) | |
| 305 | + | } else if parent.right == target { | |
| 306 | + | webkit.PanedSetEndChild(panedPtr, 0) | |
| 307 | + | } | |
| 308 | + | return nil | |
| 309 | + | }) | |
| 275 | 310 | } | |
| 276 | 311 | } | |
| 277 | 312 | ||
| @@ -281,12 +316,15 @@ func (spm *StackedPaneManager) reattachToParent(container uintptr, target *paneN | |||
| 281 | 316 | if spm.wm.window != nil { | |
| 282 | 317 | spm.wm.window.SetChild(container) | |
| 283 | 318 | } | |
| 284 | - | } else if parent.container != 0 { | |
| 285 | - | if parent.left == target { | |
| 286 | - | webkit.PanedSetStartChild(parent.container, container) | |
| 287 | - | } else if parent.right == target { | |
| 288 | - | webkit.PanedSetEndChild(parent.container, container) | |
| 289 | - | } | |
| 319 | + | } else if parent.container != nil { | |
| 320 | + | parent.container.Execute(func(panedPtr uintptr) error { | |
| 321 | + | if parent.left == target { | |
| 322 | + | webkit.PanedSetStartChild(panedPtr, container) | |
| 323 | + | } else if parent.right == target { | |
| 324 | + | webkit.PanedSetEndChild(panedPtr, container) | |
| 325 | + | } | |
| 326 | + | return nil | |
| 327 | + | }) | |
| 290 | 328 | } | |
| 291 | 329 | } | |
| 292 | 330 | ||
| @@ -310,7 +348,7 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod | |||
| 310 | 348 | currentActiveIndex := stackNode.activeStackIndex | |
| 311 | 349 | if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex { | |
| 312 | 350 | currentlyActivePane := stackNode.stackedPanes[currentActiveIndex] | |
| 313 | - | if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != 0 { | |
| 351 | + | if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != nil { | |
| 314 | 352 | currentTitle := currentlyActivePane.pane.webView.GetTitle() | |
| 315 | 353 | if currentTitle != "" { | |
| 316 | 354 | spm.updateTitleBarLabel(currentlyActivePane, currentTitle) | |
| @@ -364,17 +402,27 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) { | |||
| 364 | 402 | for i, pane := range stackNode.stackedPanes { | |
| 365 | 403 | if i == activeIndex { | |
| 366 | 404 | // Active pane: show container, NEVER show title bar | |
| 367 | - | webkit.WidgetSetVisible(pane.container, true) | |
| 368 | - | webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane | |
| 369 | - | webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active") | |
| 370 | - | webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed") | |
| 405 | + | if pane.container != nil { | |
| 406 | + | webkit.WidgetSetVisible(pane.container.Ptr(), true) | |
| 407 | + | webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-active") | |
| 408 | + | webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-collapsed") | |
| 409 | + | } | |
| 410 | + | if pane.titleBar != nil { | |
| 411 | + | webkit.WidgetSetVisible(pane.titleBar.Ptr(), false) // ABSOLUTE RULE: never visible for active pane | |
| 412 | + | } | |
| 371 | 413 | log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i) | |
| 372 | 414 | } else { | |
| 373 | 415 | // Inactive panes: hide container, show title bar | |
| 374 | - | webkit.WidgetSetVisible(pane.container, false) | |
| 375 | - | webkit.WidgetSetVisible(pane.titleBar, true) | |
| 376 | - | webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed") | |
| 377 | - | webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active") | |
| 416 | + | if pane.container != nil { | |
| 417 | + | webkit.WidgetSetVisible(pane.container.Ptr(), false) | |
| 418 | + | webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-collapsed") | |
| 419 | + | } | |
| 420 | + | if pane.titleBar != nil { | |
| 421 | + | webkit.WidgetSetVisible(pane.titleBar.Ptr(), true) | |
| 422 | + | } | |
| 423 | + | if pane.container != nil { | |
| 424 | + | webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-active") | |
| 425 | + | } | |
| 378 | 426 | } | |
| 379 | 427 | } | |
| 380 | 428 | } | |
| @@ -436,7 +484,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool { | |||
| 436 | 484 | // Update title bar for the pane transitioning from ACTIVE to INACTIVE | |
| 437 | 485 | // This ensures the collapsed pane shows its current page title (Zellij-style layout) | |
| 438 | 486 | currentActivePane := stackNode.stackedPanes[currentIndex] | |
| 439 | - | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 { | |
| 487 | + | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != nil { | |
| 440 | 488 | currentTitle := currentActivePane.pane.webView.GetTitle() | |
| 441 | 489 | if currentTitle != "" { | |
| 442 | 490 | spm.updateTitleBarLabel(currentActivePane, currentTitle) | |
| @@ -512,7 +560,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str | |||
| 512 | 560 | } | |
| 513 | 561 | ||
| 514 | 562 | // Check if this pane is in a stack and has a title bar | |
| 515 | - | if node.parent != nil && node.parent.isStacked && node.titleBar != 0 { | |
| 563 | + | if node.parent != nil && node.parent.isStacked && node.titleBar != nil { | |
| 516 | 564 | // CRITICAL: Only update title bar for INACTIVE panes | |
| 517 | 565 | // Active panes should never show their title bar | |
| 518 | 566 | stackNode := node.parent | |
| @@ -540,7 +588,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str | |||
| 540 | 588 | ||
| 541 | 589 | // updateTitleBarLabel updates the label widget within a title bar by recreating it | |
| 542 | 590 | func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) { | |
| 543 | - | if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked { | |
| 591 | + | if node == nil || node.titleBar == nil || node.parent == nil || !node.parent.isStacked { | |
| 544 | 592 | return | |
| 545 | 593 | } | |
| 546 | 594 | ||
| @@ -558,7 +606,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||
| 558 | 606 | } | |
| 559 | 607 | ||
| 560 | 608 | // Replace the old title bar in the stack | |
| 561 | - | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 { | |
| 609 | + | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != nil { | |
| 562 | 610 | // Find the correct insertion position based on the pane's index in the stack | |
| 563 | 611 | paneIndex := -1 | |
| 564 | 612 | for i, stackedPane := range node.parent.stackedPanes { | |
| @@ -569,19 +617,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||
| 569 | 617 | } | |
| 570 | 618 | ||
| 571 | 619 | // Remove old title bar | |
| 572 | - | webkit.WidgetUnparent(node.titleBar) | |
| 620 | + | if node.titleBar != nil { | |
| 621 | + | node.titleBar.Execute(func(titleBarPtr uintptr) error { | |
| 622 | + | webkit.WidgetUnparent(titleBarPtr) | |
| 623 | + | return nil | |
| 624 | + | }) | |
| 625 | + | } | |
| 573 | 626 | ||
| 574 | 627 | // Update the node's title bar reference | |
| 575 | - | node.titleBar = newTitleBar | |
| 628 | + | spm.wm.setTitleBar(node, newTitleBar) | |
| 576 | 629 | ||
| 577 | 630 | // Insert the new title bar at the correct position | |
| 578 | 631 | if paneIndex == 0 { | |
| 579 | 632 | // First pane - insert at the beginning | |
| 580 | - | webkit.BoxPrepend(node.parent.stackWrapper, newTitleBar) | |
| 633 | + | if node.parent.stackWrapper != nil { | |
| 634 | + | webkit.BoxPrepend(node.parent.stackWrapper.Ptr(), newTitleBar) | |
| 635 | + | } | |
| 581 | 636 | } else { | |
| 582 | 637 | // Insert after the previous pane's container | |
| 583 | 638 | prevPane := node.parent.stackedPanes[paneIndex-1] | |
| 584 | - | webkit.BoxInsertChildAfter(node.parent.stackWrapper, newTitleBar, prevPane.container) | |
| 639 | + | if node.parent.stackWrapper != nil && prevPane.container != nil { | |
| 640 | + | webkit.BoxInsertChildAfter(node.parent.stackWrapper.Ptr(), newTitleBar, prevPane.container.Ptr()) | |
| 641 | + | } | |
| 585 | 642 | } | |
| 586 | 643 | ||
| 587 | 644 | log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node) | |
| @@ -644,16 +701,18 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||
| 644 | 701 | node.pane.CleanupFromWorkspace(spm.wm) | |
| 645 | 702 | ||
| 646 | 703 | // Remove the pane's widgets from the stack container | |
| 647 | - | stackBox := stackNode.stackWrapper | |
| 648 | - | if stackBox == 0 { | |
| 704 | + | var stackBox *SafeWidget = stackNode.stackWrapper | |
| 705 | + | if stackBox == nil { | |
| 649 | 706 | stackBox = stackNode.container | |
| 650 | 707 | } | |
| 651 | 708 | ||
| 652 | - | if node.titleBar != 0 { | |
| 653 | - | webkit.BoxRemove(stackBox, node.titleBar) | |
| 654 | - | } | |
| 655 | - | if node.container != 0 { | |
| 656 | - | webkit.BoxRemove(stackBox, node.container) | |
| 709 | + | if stackBox != nil { | |
| 710 | + | if node.titleBar != nil { | |
| 711 | + | webkit.BoxRemove(stackBox.Ptr(), node.titleBar.Ptr()) | |
| 712 | + | } | |
| 713 | + | if node.container != nil { | |
| 714 | + | webkit.BoxRemove(stackBox.Ptr(), node.container.Ptr()) | |
| 715 | + | } | |
| 657 | 716 | } | |
| 658 | 717 | ||
| 659 | 718 | // Clean up the pane | |
| @@ -674,27 +733,39 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||
| 674 | 733 | lastPaneContainer := lastPane.container | |
| 675 | 734 | ||
| 676 | 735 | // Remove the title bar since we're going back to single pane | |
| 677 | - | if lastPane.titleBar != 0 { | |
| 678 | - | webkit.BoxRemove(stackBox, lastPane.titleBar) | |
| 736 | + | if lastPane.titleBar != nil && stackBox != nil { | |
| 737 | + | webkit.BoxRemove(stackBox.Ptr(), lastPane.titleBar.Ptr()) | |
| 679 | 738 | } | |
| 680 | 739 | ||
| 681 | 740 | // Unparent the pane container from stack wrapper | |
| 682 | - | webkit.BoxRemove(stackBox, lastPaneContainer) | |
| 741 | + | if stackBox != nil && lastPaneContainer != nil { | |
| 742 | + | webkit.BoxRemove(stackBox.Ptr(), lastPaneContainer.Ptr()) | |
| 743 | + | } | |
| 683 | 744 | ||
| 684 | 745 | // CRITICAL FIX: Replace the stackNode completely with lastPane | |
| 685 | 746 | // Update parent child references FIRST | |
| 686 | 747 | if parent == nil { | |
| 687 | 748 | spm.wm.root = lastPane | |
| 688 | - | if spm.wm.window != nil { | |
| 689 | - | spm.wm.window.SetChild(lastPaneContainer) | |
| 749 | + | if spm.wm.window != nil && lastPaneContainer != nil { | |
| 750 | + | spm.wm.window.SetChild(lastPaneContainer.Ptr()) | |
| 690 | 751 | } | |
| 691 | 752 | } else { | |
| 692 | 753 | if parent.left == stackNode { | |
| 693 | 754 | parent.left = lastPane | |
| 694 | - | webkit.PanedSetStartChild(parent.container, lastPaneContainer) | |
| 755 | + | if parent.container != nil && lastPaneContainer != nil { | |
| 756 | + | parent.container.Execute(func(panedPtr uintptr) error { | |
| 757 | + | webkit.PanedSetStartChild(panedPtr, lastPaneContainer.Ptr()) | |
| 758 | + | return nil | |
| 759 | + | }) | |
| 760 | + | } | |
| 695 | 761 | } else if parent.right == stackNode { | |
| 696 | 762 | parent.right = lastPane | |
| 697 | - | webkit.PanedSetEndChild(parent.container, lastPaneContainer) | |
| 763 | + | if parent.container != nil && lastPaneContainer != nil { | |
| 764 | + | parent.container.Execute(func(panedPtr uintptr) error { | |
| 765 | + | webkit.PanedSetEndChild(panedPtr, lastPaneContainer.Ptr()) | |
| 766 | + | return nil | |
| 767 | + | }) | |
| 768 | + | } | |
| 698 | 769 | } | |
| 699 | 770 | } | |
| 700 | 771 | ||
| @@ -702,11 +773,13 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||
| 702 | 773 | lastPane.parent = parent | |
| 703 | 774 | lastPane.isStacked = false | |
| 704 | 775 | lastPane.stackedPanes = nil | |
| 705 | - | lastPane.titleBar = 0 | |
| 776 | + | lastPane.titleBar = nil | |
| 706 | 777 | ||
| 707 | 778 | // CRITICAL: Ensure the container is visible after reparenting | |
| 708 | 779 | // The container may have been hidden during stack operations | |
| 709 | - | webkit.WidgetSetVisible(lastPaneContainer, true) | |
| 780 | + | if lastPaneContainer != nil { | |
| 781 | + | webkit.WidgetSetVisible(lastPaneContainer.Ptr(), true) | |
| 782 | + | } | |
| 710 | 783 | ||
| 711 | 784 | // Update the viewToNode mapping to point to the lastPane, not stackNode | |
| 712 | 785 | spm.wm.viewToNode[lastPane.pane.webView] = lastPane | |
| @@ -82,14 +82,15 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) { | |||||
| 82 | 82 | ||||
| 83 | // Create the new leaf node | 83 | // Create the new leaf node | ||
| 84 | newLeaf := &paneNode{ | 84 | newLeaf := &paneNode{ | ||
| 85 | - | pane: newPane, | 85 | + | pane: newPane, |
| 86 | - | container: newContainer, | 86 | + | isLeaf: true, |
| 87 | - | isLeaf: true, | |||
| 88 | } | 87 | } | ||
| 88 | + | // Initialize widgets properly using workspace manager helper | |||
| 89 | + | spm.wm.initializePaneWidgets(newLeaf, newContainer) | |||
| 89 | 90 | ||||
| 90 | // Create title bar for the new pane | 91 | // Create title bar for the new pane | ||
| 91 | newTitleBar := spm.createTitleBar(newLeaf) | 92 | newTitleBar := spm.createTitleBar(newLeaf) | ||
| 92 | - | newLeaf.titleBar = newTitleBar | 93 | + | spm.wm.setTitleBar(newLeaf, newTitleBar) |
| 93 | 94 | ||||
| 94 | // Keep new container hidden initially - will be shown after transition | 95 | // Keep new container hidden initially - will be shown after transition | ||
| 95 | webkit.WidgetSetVisible(newContainer, false) | 96 | webkit.WidgetSetVisible(newContainer, false) | ||
| @@ -129,8 +130,13 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode) | |||||
| 129 | stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane | 130 | stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane | ||
| 130 | 131 | ||||
| 131 | // Unparent the new container before adding it to the stack (only if it has a parent) | 132 | // Unparent the new container before adding it to the stack (only if it has a parent) | ||
| 132 | - | if webkit.WidgetGetParent(newLeaf.container) != 0 { | 133 | + | if newLeaf.container != nil { |
| 133 | - | webkit.WidgetUnparent(newLeaf.container) | 134 | + | newLeaf.container.Execute(func(containerPtr uintptr) error { |
| 135 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |||
| 136 | + | webkit.WidgetUnparent(containerPtr) | |||
| 137 | + | } | |||
| 138 | + | return nil | |||
| 139 | + | }) | |||
| 134 | } | 140 | } | ||
| 135 | 141 | ||||
| 136 | // Insert widgets at correct position for Zellij-style layout | 142 | // Insert widgets at correct position for Zellij-style layout | ||
| @@ -141,19 +147,29 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode) | |||||
| 141 | if insertIndex > 0 { | 147 | if insertIndex > 0 { | ||
| 142 | // Insert after the previous pane's container | 148 | // Insert after the previous pane's container | ||
| 143 | prevPane := stackNode.stackedPanes[insertIndex-1] | 149 | prevPane := stackNode.stackedPanes[insertIndex-1] | ||
| 144 | - | insertAfterWidget = prevPane.container | 150 | + | if prevPane.container != nil { |
| 151 | + | insertAfterWidget = prevPane.container.Ptr() | |||
| 152 | + | } | |||
| 145 | } | 153 | } | ||
| 146 | 154 | ||||
| 147 | if insertAfterWidget != 0 { | 155 | if insertAfterWidget != 0 { | ||
| 148 | // Insert titleBar after the previous pane's container | 156 | // Insert titleBar after the previous pane's container | ||
| 149 | - | webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.titleBar, insertAfterWidget) | 157 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { |
| 158 | + | webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr(), insertAfterWidget) | |||
| 159 | + | } | |||
| 150 | // Insert container after the newly inserted titleBar | 160 | // Insert container after the newly inserted titleBar | ||
| 151 | - | webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.container, newLeaf.titleBar) | 161 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil && newLeaf.titleBar != nil { |
| 162 | + | webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr(), newLeaf.titleBar.Ptr()) | |||
| 163 | + | } | |||
| 152 | log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget) | 164 | log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget) | ||
| 153 | } else { | 165 | } else { | ||
| 154 | // Insert at the beginning (insertIndex = 0) | 166 | // Insert at the beginning (insertIndex = 0) | ||
| 155 | - | webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.container) | 167 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil { |
| 156 | - | webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.titleBar) | 168 | + | webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr()) |
| 169 | + | } | |||
| 170 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { | |||
| 171 | + | webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr()) | |||
| 172 | + | } | |||
| 157 | log.Printf("[workspace] prepended widgets at position 0") | 173 | log.Printf("[workspace] prepended widgets at position 0") | ||
| 158 | } | 174 | } | ||
| 159 | 175 | ||||
| @@ -199,7 +215,9 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||||
| 199 | 215 | ||||
| 200 | // Build the complete stack structure with hidden widgets | 216 | // Build the complete stack structure with hidden widgets | ||
| 201 | webkit.BoxAppend(stackInternalBox, titleBar) | 217 | webkit.BoxAppend(stackInternalBox, titleBar) | ||
| 202 | - | webkit.BoxAppend(stackInternalBox, existingContainer) | 218 | + | if existingContainer != nil { |
| 219 | + | webkit.BoxAppend(stackInternalBox, existingContainer.Ptr()) | |||
| 220 | + | } | |||
| 203 | 221 | ||||
| 204 | // Immediately reattach the stack wrapper to minimize visibility gap | 222 | // Immediately reattach the stack wrapper to minimize visibility gap | ||
| 205 | spm.reattachToParent(stackWrapperContainer, target, parent) | 223 | spm.reattachToParent(stackWrapperContainer, target, parent) | ||
| @@ -207,19 +225,19 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||||
| 207 | // Convert target to a stacked leaf node | 225 | // Convert target to a stacked leaf node | ||
| 208 | target.isStacked = true | 226 | target.isStacked = true | ||
| 209 | target.isLeaf = true | 227 | target.isLeaf = true | ||
| 210 | - | target.container = existingContainer | 228 | + | // container stays the same (existingContainer) |
| 211 | - | target.titleBar = titleBar | 229 | + | spm.wm.setTitleBar(target, titleBar) |
| 212 | 230 | ||||
| 213 | // Create the stack container node - container points to wrapper, stackWrapper points to internal box | 231 | // Create the stack container node - container points to wrapper, stackWrapper points to internal box | ||
| 214 | stackNode := &paneNode{ | 232 | stackNode := &paneNode{ | ||
| 215 | isStacked: true, | 233 | isStacked: true, | ||
| 216 | isLeaf: false, | 234 | isLeaf: false, | ||
| 217 | - | container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.) | |||
| 218 | - | stackWrapper: stackInternalBox, // Internal box for stack operations | |||
| 219 | stackedPanes: []*paneNode{target}, | 235 | stackedPanes: []*paneNode{target}, | ||
| 220 | activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0) | 236 | activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0) | ||
| 221 | parent: parent, | 237 | parent: parent, | ||
| 222 | } | 238 | } | ||
| 239 | + | spm.wm.setContainer(stackNode, stackWrapperContainer, "stack-wrapper") // Wrapper for GTK operations (splits, etc.) | |||
| 240 | + | spm.wm.setStackWrapper(stackNode, stackInternalBox) // Internal box for stack operations | |||
| 223 | 241 | ||||
| 224 | // Update target's parent to be the stack node | 242 | // Update target's parent to be the stack node | ||
| 225 | target.parent = stackNode | 243 | target.parent = stackNode | ||
| @@ -243,13 +261,22 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo | |||||
| 243 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | 261 | stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf) | ||
| 244 | 262 | ||||
| 245 | // Unparent the new container before adding it to the stack | 263 | // Unparent the new container before adding it to the stack | ||
| 246 | - | if webkit.WidgetGetParent(newLeaf.container) != 0 { | 264 | + | if newLeaf.container != nil { |
| 247 | - | webkit.WidgetUnparent(newLeaf.container) | 265 | + | newLeaf.container.Execute(func(containerPtr uintptr) error { |
| 266 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |||
| 267 | + | webkit.WidgetUnparent(containerPtr) | |||
| 268 | + | } | |||
| 269 | + | return nil | |||
| 270 | + | }) | |||
| 248 | } | 271 | } | ||
| 249 | 272 | ||||
| 250 | // Add the new widgets to the internal stack box | 273 | // Add the new widgets to the internal stack box | ||
| 251 | - | webkit.BoxAppend(stackNode.stackWrapper, newLeaf.titleBar) | 274 | + | if stackNode.stackWrapper != nil && newLeaf.titleBar != nil { |
| 252 | - | webkit.BoxAppend(stackNode.stackWrapper, newLeaf.container) | 275 | + | webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr()) |
| 276 | + | } | |||
| 277 | + | if stackNode.stackWrapper != nil && newLeaf.container != nil { | |||
| 278 | + | webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr()) | |||
| 279 | + | } | |||
| 253 | 280 | ||||
| 254 | return stackNode, insertIndex, nil | 281 | return stackNode, insertIndex, nil | ||
| 255 | } | 282 | } | ||
| @@ -262,16 +289,24 @@ func (spm *StackedPaneManager) detachFromParent(target *paneNode, parent *paneNo | |||||
| 262 | spm.wm.window.SetChild(0) | 289 | spm.wm.window.SetChild(0) | ||
| 263 | } | 290 | } | ||
| 264 | // Unparent if it has a GTK parent | 291 | // Unparent if it has a GTK parent | ||
| 265 | - | if webkit.WidgetGetParent(target.container) != 0 { | 292 | + | if target.container != nil { |
| 266 | - | webkit.WidgetUnparent(target.container) | 293 | + | target.container.Execute(func(containerPtr uintptr) error { |
| 294 | + | if webkit.WidgetGetParent(containerPtr) != 0 { | |||
| 295 | + | webkit.WidgetUnparent(containerPtr) | |||
| 296 | + | } | |||
| 297 | + | return nil | |||
| 298 | + | }) | |||
| 267 | } | 299 | } | ||
| 268 | - | } else if parent.container != 0 { | 300 | + | } else if parent.container != nil { |
| 269 | // Target has a parent paned - remove it (automatically unparents in GTK4) | 301 | // Target has a parent paned - remove it (automatically unparents in GTK4) | ||
| 270 | - | if parent.left == target { | 302 | + | parent.container.Execute(func(panedPtr uintptr) error { |
| 271 | - | webkit.PanedSetStartChild(parent.container, 0) | 303 | + | if parent.left == target { |
| 272 | - | } else if parent.right == target { | 304 | + | webkit.PanedSetStartChild(panedPtr, 0) |
| 273 | - | webkit.PanedSetEndChild(parent.container, 0) | 305 | + | } else if parent.right == target { |
| 274 | - | } | 306 | + | webkit.PanedSetEndChild(panedPtr, 0) |
| 307 | + | } | |||
| 308 | + | return nil | |||
| 309 | + | }) | |||
| 275 | } | 310 | } | ||
| 276 | } | 311 | } | ||
| 277 | 312 | ||||
| @@ -281,12 +316,15 @@ func (spm *StackedPaneManager) reattachToParent(container uintptr, target *paneN | |||||
| 281 | if spm.wm.window != nil { | 316 | if spm.wm.window != nil { | ||
| 282 | spm.wm.window.SetChild(container) | 317 | spm.wm.window.SetChild(container) | ||
| 283 | } | 318 | } | ||
| 284 | - | } else if parent.container != 0 { | 319 | + | } else if parent.container != nil { |
| 285 | - | if parent.left == target { | 320 | + | parent.container.Execute(func(panedPtr uintptr) error { |
| 286 | - | webkit.PanedSetStartChild(parent.container, container) | 321 | + | if parent.left == target { |
| 287 | - | } else if parent.right == target { | 322 | + | webkit.PanedSetStartChild(panedPtr, container) |
| 288 | - | webkit.PanedSetEndChild(parent.container, container) | 323 | + | } else if parent.right == target { |
| 289 | - | } | 324 | + | webkit.PanedSetEndChild(panedPtr, container) |
| 325 | + | } | |||
| 326 | + | return nil | |||
| 327 | + | }) | |||
| 290 | } | 328 | } | ||
| 291 | } | 329 | } | ||
| 292 | 330 | ||||
| @@ -310,7 +348,7 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod | |||||
| 310 | currentActiveIndex := stackNode.activeStackIndex | 348 | currentActiveIndex := stackNode.activeStackIndex | ||
| 311 | if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex { | 349 | if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex { | ||
| 312 | currentlyActivePane := stackNode.stackedPanes[currentActiveIndex] | 350 | currentlyActivePane := stackNode.stackedPanes[currentActiveIndex] | ||
| 313 | - | if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != 0 { | 351 | + | if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != nil { |
| 314 | currentTitle := currentlyActivePane.pane.webView.GetTitle() | 352 | currentTitle := currentlyActivePane.pane.webView.GetTitle() | ||
| 315 | if currentTitle != "" { | 353 | if currentTitle != "" { | ||
| 316 | spm.updateTitleBarLabel(currentlyActivePane, currentTitle) | 354 | spm.updateTitleBarLabel(currentlyActivePane, currentTitle) | ||
| @@ -364,17 +402,27 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) { | |||||
| 364 | for i, pane := range stackNode.stackedPanes { | 402 | for i, pane := range stackNode.stackedPanes { | ||
| 365 | if i == activeIndex { | 403 | if i == activeIndex { | ||
| 366 | // Active pane: show container, NEVER show title bar | 404 | // Active pane: show container, NEVER show title bar | ||
| 367 | - | webkit.WidgetSetVisible(pane.container, true) | 405 | + | if pane.container != nil { |
| 368 | - | webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane | 406 | + | webkit.WidgetSetVisible(pane.container.Ptr(), true) |
| 369 | - | webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active") | 407 | + | webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-active") |
| 370 | - | webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed") | 408 | + | webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-collapsed") |
| 409 | + | } | |||
| 410 | + | if pane.titleBar != nil { | |||
| 411 | + | webkit.WidgetSetVisible(pane.titleBar.Ptr(), false) // ABSOLUTE RULE: never visible for active pane | |||
| 412 | + | } | |||
| 371 | log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i) | 413 | log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i) | ||
| 372 | } else { | 414 | } else { | ||
| 373 | // Inactive panes: hide container, show title bar | 415 | // Inactive panes: hide container, show title bar | ||
| 374 | - | webkit.WidgetSetVisible(pane.container, false) | 416 | + | if pane.container != nil { |
| 375 | - | webkit.WidgetSetVisible(pane.titleBar, true) | 417 | + | webkit.WidgetSetVisible(pane.container.Ptr(), false) |
| 376 | - | webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed") | 418 | + | webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-collapsed") |
| 377 | - | webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active") | 419 | + | } |
| 420 | + | if pane.titleBar != nil { | |||
| 421 | + | webkit.WidgetSetVisible(pane.titleBar.Ptr(), true) | |||
| 422 | + | } | |||
| 423 | + | if pane.container != nil { | |||
| 424 | + | webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-active") | |||
| 425 | + | } | |||
| 378 | } | 426 | } | ||
| 379 | } | 427 | } | ||
| 380 | } | 428 | } | ||
| @@ -436,7 +484,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool { | |||||
| 436 | // Update title bar for the pane transitioning from ACTIVE to INACTIVE | 484 | // Update title bar for the pane transitioning from ACTIVE to INACTIVE | ||
| 437 | // This ensures the collapsed pane shows its current page title (Zellij-style layout) | 485 | // This ensures the collapsed pane shows its current page title (Zellij-style layout) | ||
| 438 | currentActivePane := stackNode.stackedPanes[currentIndex] | 486 | currentActivePane := stackNode.stackedPanes[currentIndex] | ||
| 439 | - | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 { | 487 | + | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != nil { |
| 440 | currentTitle := currentActivePane.pane.webView.GetTitle() | 488 | currentTitle := currentActivePane.pane.webView.GetTitle() | ||
| 441 | if currentTitle != "" { | 489 | if currentTitle != "" { | ||
| 442 | spm.updateTitleBarLabel(currentActivePane, currentTitle) | 490 | spm.updateTitleBarLabel(currentActivePane, currentTitle) | ||
| @@ -512,7 +560,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str | |||||
| 512 | } | 560 | } | ||
| 513 | 561 | ||||
| 514 | // Check if this pane is in a stack and has a title bar | 562 | // Check if this pane is in a stack and has a title bar | ||
| 515 | - | if node.parent != nil && node.parent.isStacked && node.titleBar != 0 { | 563 | + | if node.parent != nil && node.parent.isStacked && node.titleBar != nil { |
| 516 | // CRITICAL: Only update title bar for INACTIVE panes | 564 | // CRITICAL: Only update title bar for INACTIVE panes | ||
| 517 | // Active panes should never show their title bar | 565 | // Active panes should never show their title bar | ||
| 518 | stackNode := node.parent | 566 | stackNode := node.parent | ||
| @@ -540,7 +588,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str | |||||
| 540 | 588 | ||||
| 541 | // updateTitleBarLabel updates the label widget within a title bar by recreating it | 589 | // updateTitleBarLabel updates the label widget within a title bar by recreating it | ||
| 542 | func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) { | 590 | func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) { | ||
| 543 | - | if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked { | 591 | + | if node == nil || node.titleBar == nil || node.parent == nil || !node.parent.isStacked { |
| 544 | return | 592 | return | ||
| 545 | } | 593 | } | ||
| 546 | 594 | ||||
| @@ -558,7 +606,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||||
| 558 | } | 606 | } | ||
| 559 | 607 | ||||
| 560 | // Replace the old title bar in the stack | 608 | // Replace the old title bar in the stack | ||
| 561 | - | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 { | 609 | + | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != nil { |
| 562 | // Find the correct insertion position based on the pane's index in the stack | 610 | // Find the correct insertion position based on the pane's index in the stack | ||
| 563 | paneIndex := -1 | 611 | paneIndex := -1 | ||
| 564 | for i, stackedPane := range node.parent.stackedPanes { | 612 | for i, stackedPane := range node.parent.stackedPanes { | ||
| @@ -569,19 +617,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||||
| 569 | } | 617 | } | ||
| 570 | 618 | ||||
| 571 | // Remove old title bar | 619 | // Remove old title bar | ||
| 572 | - | webkit.WidgetUnparent(node.titleBar) | 620 | + | if node.titleBar != nil { |
| 621 | + | node.titleBar.Execute(func(titleBarPtr uintptr) error { | |||
| 622 | + | webkit.WidgetUnparent(titleBarPtr) | |||
| 623 | + | return nil | |||
| 624 | + | }) | |||
| 625 | + | } | |||
| 573 | 626 | ||||
| 574 | // Update the node's title bar reference | 627 | // Update the node's title bar reference | ||
| 575 | - | node.titleBar = newTitleBar | 628 | + | spm.wm.setTitleBar(node, newTitleBar) |
| 576 | 629 | ||||
| 577 | // Insert the new title bar at the correct position | 630 | // Insert the new title bar at the correct position | ||
| 578 | if paneIndex == 0 { | 631 | if paneIndex == 0 { | ||
| 579 | // First pane - insert at the beginning | 632 | // First pane - insert at the beginning | ||
| 580 | - | webkit.BoxPrepend(node.parent.stackWrapper, newTitleBar) | 633 | + | if node.parent.stackWrapper != nil { |
| 634 | + | webkit.BoxPrepend(node.parent.stackWrapper.Ptr(), newTitleBar) | |||
| 635 | + | } | |||
| 581 | } else { | 636 | } else { | ||
| 582 | // Insert after the previous pane's container | 637 | // Insert after the previous pane's container | ||
| 583 | prevPane := node.parent.stackedPanes[paneIndex-1] | 638 | prevPane := node.parent.stackedPanes[paneIndex-1] | ||
| 584 | - | webkit.BoxInsertChildAfter(node.parent.stackWrapper, newTitleBar, prevPane.container) | 639 | + | if node.parent.stackWrapper != nil && prevPane.container != nil { |
| 640 | + | webkit.BoxInsertChildAfter(node.parent.stackWrapper.Ptr(), newTitleBar, prevPane.container.Ptr()) | |||
| 641 | + | } | |||
| 585 | } | 642 | } | ||
| 586 | 643 | ||||
| 587 | log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node) | 644 | log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node) | ||
| @@ -644,16 +701,18 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||||
| 644 | node.pane.CleanupFromWorkspace(spm.wm) | 701 | node.pane.CleanupFromWorkspace(spm.wm) | ||
| 645 | 702 | ||||
| 646 | // Remove the pane's widgets from the stack container | 703 | // Remove the pane's widgets from the stack container | ||
| 647 | - | stackBox := stackNode.stackWrapper | 704 | + | var stackBox *SafeWidget = stackNode.stackWrapper |
| 648 | - | if stackBox == 0 { | 705 | + | if stackBox == nil { |
| 649 | stackBox = stackNode.container | 706 | stackBox = stackNode.container | ||
| 650 | } | 707 | } | ||
| 651 | 708 | ||||
| 652 | - | if node.titleBar != 0 { | 709 | + | if stackBox != nil { |
| 653 | - | webkit.BoxRemove(stackBox, node.titleBar) | 710 | + | if node.titleBar != nil { |
| 654 | - | } | 711 | + | webkit.BoxRemove(stackBox.Ptr(), node.titleBar.Ptr()) |
| 655 | - | if node.container != 0 { | 712 | + | } |
| 656 | - | webkit.BoxRemove(stackBox, node.container) | 713 | + | if node.container != nil { |
| 714 | + | webkit.BoxRemove(stackBox.Ptr(), node.container.Ptr()) | |||
| 715 | + | } | |||
| 657 | } | 716 | } | ||
| 658 | 717 | ||||
| 659 | // Clean up the pane | 718 | // Clean up the pane | ||
| @@ -674,27 +733,39 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||||
| 674 | lastPaneContainer := lastPane.container | 733 | lastPaneContainer := lastPane.container | ||
| 675 | 734 | ||||
| 676 | // Remove the title bar since we're going back to single pane | 735 | // Remove the title bar since we're going back to single pane | ||
| 677 | - | if lastPane.titleBar != 0 { | 736 | + | if lastPane.titleBar != nil && stackBox != nil { |
| 678 | - | webkit.BoxRemove(stackBox, lastPane.titleBar) | 737 | + | webkit.BoxRemove(stackBox.Ptr(), lastPane.titleBar.Ptr()) |
| 679 | } | 738 | } | ||
| 680 | 739 | ||||
| 681 | // Unparent the pane container from stack wrapper | 740 | // Unparent the pane container from stack wrapper | ||
| 682 | - | webkit.BoxRemove(stackBox, lastPaneContainer) | 741 | + | if stackBox != nil && lastPaneContainer != nil { |
| 742 | + | webkit.BoxRemove(stackBox.Ptr(), lastPaneContainer.Ptr()) | |||
| 743 | + | } | |||
| 683 | 744 | ||||
| 684 | // CRITICAL FIX: Replace the stackNode completely with lastPane | 745 | // CRITICAL FIX: Replace the stackNode completely with lastPane | ||
| 685 | // Update parent child references FIRST | 746 | // Update parent child references FIRST | ||
| 686 | if parent == nil { | 747 | if parent == nil { | ||
| 687 | spm.wm.root = lastPane | 748 | spm.wm.root = lastPane | ||
| 688 | - | if spm.wm.window != nil { | 749 | + | if spm.wm.window != nil && lastPaneContainer != nil { |
| 689 | - | spm.wm.window.SetChild(lastPaneContainer) | 750 | + | spm.wm.window.SetChild(lastPaneContainer.Ptr()) |
| 690 | } | 751 | } | ||
| 691 | } else { | 752 | } else { | ||
| 692 | if parent.left == stackNode { | 753 | if parent.left == stackNode { | ||
| 693 | parent.left = lastPane | 754 | parent.left = lastPane | ||
| 694 | - | webkit.PanedSetStartChild(parent.container, lastPaneContainer) | 755 | + | if parent.container != nil && lastPaneContainer != nil { |
| 756 | + | parent.container.Execute(func(panedPtr uintptr) error { | |||
| 757 | + | webkit.PanedSetStartChild(panedPtr, lastPaneContainer.Ptr()) | |||
| 758 | + | return nil | |||
| 759 | + | }) | |||
| 760 | + | } | |||
| 695 | } else if parent.right == stackNode { | 761 | } else if parent.right == stackNode { | ||
| 696 | parent.right = lastPane | 762 | parent.right = lastPane | ||
| 697 | - | webkit.PanedSetEndChild(parent.container, lastPaneContainer) | 763 | + | if parent.container != nil && lastPaneContainer != nil { |
| 764 | + | parent.container.Execute(func(panedPtr uintptr) error { | |||
| 765 | + | webkit.PanedSetEndChild(panedPtr, lastPaneContainer.Ptr()) | |||
| 766 | + | return nil | |||
| 767 | + | }) | |||
| 768 | + | } | |||
| 698 | } | 769 | } | ||
| 699 | } | 770 | } | ||
| 700 | 771 | ||||
| @@ -702,11 +773,13 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||||
| 702 | lastPane.parent = parent | 773 | lastPane.parent = parent | ||
| 703 | lastPane.isStacked = false | 774 | lastPane.isStacked = false | ||
| 704 | lastPane.stackedPanes = nil | 775 | lastPane.stackedPanes = nil | ||
| 705 | - | lastPane.titleBar = 0 | 776 | + | lastPane.titleBar = nil |
| 706 | 777 | ||||
| 707 | // CRITICAL: Ensure the container is visible after reparenting | 778 | // CRITICAL: Ensure the container is visible after reparenting | ||
| 708 | // The container may have been hidden during stack operations | 779 | // The container may have been hidden during stack operations | ||
| 709 | - | webkit.WidgetSetVisible(lastPaneContainer, true) | 780 | + | if lastPaneContainer != nil { |
| 781 | + | webkit.WidgetSetVisible(lastPaneContainer.Ptr(), true) | |||
| 782 | + | } | |||
| 710 | 783 | ||||
| 711 | // Update the viewToNode mapping to point to the lastPane, not stackNode | 784 | // Update the viewToNode mapping to point to the lastPane, not stackNode | ||
| 712 | spm.wm.viewToNode[lastPane.pane.webView] = lastPane | 785 | spm.wm.viewToNode[lastPane.pane.webView] = lastPane | ||
A internal/app/browser/widget.go +133 -0
| @@ -0,0 +1,133 @@ | |||
| 1 | + | package browser | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "sync" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/pkg/webkit" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // SafeWidget provides thread-safe access to GTK widgets with validation | |
| 11 | + | type SafeWidget struct { | |
| 12 | + | ptr uintptr | |
| 13 | + | typeInfo string | |
| 14 | + | registry *WidgetRegistry | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // Execute safely executes a function with the widget pointer, ensuring thread safety and validation | |
| 18 | + | func (w *SafeWidget) Execute(fn func(uintptr) error) error { | |
| 19 | + | if w == nil || w.registry == nil { | |
| 20 | + | return fmt.Errorf("SafeWidget is nil or has no registry") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | w.registry.mu.Lock() | |
| 24 | + | defer w.registry.mu.Unlock() | |
| 25 | + | ||
| 26 | + | if !w.IsValid() { | |
| 27 | + | return fmt.Errorf("widget %s is no longer valid", w.String()) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | return fn(w.ptr) | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // IsValid checks if the underlying GTK widget is still valid | |
| 34 | + | func (w *SafeWidget) IsValid() bool { | |
| 35 | + | if w == nil || w.ptr == 0 { | |
| 36 | + | return false | |
| 37 | + | } | |
| 38 | + | return webkit.WidgetIsValid(w.ptr) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // String returns a string representation of the SafeWidget for debugging | |
| 42 | + | func (w *SafeWidget) String() string { | |
| 43 | + | if w == nil { | |
| 44 | + | return "SafeWidget(nil)" | |
| 45 | + | } | |
| 46 | + | return fmt.Sprintf("SafeWidget(%s@%#x)", w.typeInfo, w.ptr) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Ptr returns the raw uintptr to the GTK widget | |
| 50 | + | func (w *SafeWidget) Ptr() uintptr { | |
| 51 | + | if w == nil { | |
| 52 | + | return 0 | |
| 53 | + | } | |
| 54 | + | return w.ptr | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // WidgetRegistry manages a collection of SafeWidget instances with thread safety | |
| 58 | + | type WidgetRegistry struct { | |
| 59 | + | mu sync.RWMutex | |
| 60 | + | widgets map[uintptr]*SafeWidget | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // NewWidgetRegistry creates a new widget registry | |
| 64 | + | func NewWidgetRegistry() *WidgetRegistry { | |
| 65 | + | return &WidgetRegistry{ | |
| 66 | + | widgets: make(map[uintptr]*SafeWidget), | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Register creates a new SafeWidget and registers it in the registry | |
| 71 | + | func (r *WidgetRegistry) Register(ptr uintptr, typeInfo string) *SafeWidget { | |
| 72 | + | if r == nil || ptr == 0 { | |
| 73 | + | return nil | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | r.mu.Lock() | |
| 77 | + | defer r.mu.Unlock() | |
| 78 | + | ||
| 79 | + | // Check if widget is already registered | |
| 80 | + | if existing, exists := r.widgets[ptr]; exists { | |
| 81 | + | return existing | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Create new SafeWidget | |
| 85 | + | widget := &SafeWidget{ | |
| 86 | + | ptr: ptr, | |
| 87 | + | typeInfo: typeInfo, | |
| 88 | + | registry: r, | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | r.widgets[ptr] = widget | |
| 92 | + | return widget | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // Unregister removes a widget from the registry | |
| 96 | + | func (r *WidgetRegistry) Unregister(ptr uintptr) { | |
| 97 | + | if r == nil || ptr == 0 { | |
| 98 | + | return | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | r.mu.Lock() | |
| 102 | + | defer r.mu.Unlock() | |
| 103 | + | ||
| 104 | + | delete(r.widgets, ptr) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // Cleanup removes all invalid widgets from the registry | |
| 108 | + | func (r *WidgetRegistry) Cleanup() { | |
| 109 | + | if r == nil { | |
| 110 | + | return | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | r.mu.Lock() | |
| 114 | + | defer r.mu.Unlock() | |
| 115 | + | ||
| 116 | + | for ptr, widget := range r.widgets { | |
| 117 | + | if !widget.IsValid() { | |
| 118 | + | delete(r.widgets, ptr) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // Count returns the number of registered widgets | |
| 124 | + | func (r *WidgetRegistry) Count() int { | |
| 125 | + | if r == nil { | |
| 126 | + | return 0 | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | r.mu.RLock() | |
| 130 | + | defer r.mu.RUnlock() | |
| 131 | + | ||
| 132 | + | return len(r.widgets) | |
| 133 | + | }⏎̸ | |
| @@ -0,0 +1,133 @@ | |||||
| 1 | + | package browser | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "sync" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/pkg/webkit" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // SafeWidget provides thread-safe access to GTK widgets with validation | |||
| 11 | + | type SafeWidget struct { | |||
| 12 | + | ptr uintptr | |||
| 13 | + | typeInfo string | |||
| 14 | + | registry *WidgetRegistry | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // Execute safely executes a function with the widget pointer, ensuring thread safety and validation | |||
| 18 | + | func (w *SafeWidget) Execute(fn func(uintptr) error) error { | |||
| 19 | + | if w == nil || w.registry == nil { | |||
| 20 | + | return fmt.Errorf("SafeWidget is nil or has no registry") | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | w.registry.mu.Lock() | |||
| 24 | + | defer w.registry.mu.Unlock() | |||
| 25 | + | ||||
| 26 | + | if !w.IsValid() { | |||
| 27 | + | return fmt.Errorf("widget %s is no longer valid", w.String()) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | return fn(w.ptr) | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // IsValid checks if the underlying GTK widget is still valid | |||
| 34 | + | func (w *SafeWidget) IsValid() bool { | |||
| 35 | + | if w == nil || w.ptr == 0 { | |||
| 36 | + | return false | |||
| 37 | + | } | |||
| 38 | + | return webkit.WidgetIsValid(w.ptr) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // String returns a string representation of the SafeWidget for debugging | |||
| 42 | + | func (w *SafeWidget) String() string { | |||
| 43 | + | if w == nil { | |||
| 44 | + | return "SafeWidget(nil)" | |||
| 45 | + | } | |||
| 46 | + | return fmt.Sprintf("SafeWidget(%s@%#x)", w.typeInfo, w.ptr) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // Ptr returns the raw uintptr to the GTK widget | |||
| 50 | + | func (w *SafeWidget) Ptr() uintptr { | |||
| 51 | + | if w == nil { | |||
| 52 | + | return 0 | |||
| 53 | + | } | |||
| 54 | + | return w.ptr | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // WidgetRegistry manages a collection of SafeWidget instances with thread safety | |||
| 58 | + | type WidgetRegistry struct { | |||
| 59 | + | mu sync.RWMutex | |||
| 60 | + | widgets map[uintptr]*SafeWidget | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // NewWidgetRegistry creates a new widget registry | |||
| 64 | + | func NewWidgetRegistry() *WidgetRegistry { | |||
| 65 | + | return &WidgetRegistry{ | |||
| 66 | + | widgets: make(map[uintptr]*SafeWidget), | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Register creates a new SafeWidget and registers it in the registry | |||
| 71 | + | func (r *WidgetRegistry) Register(ptr uintptr, typeInfo string) *SafeWidget { | |||
| 72 | + | if r == nil || ptr == 0 { | |||
| 73 | + | return nil | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | r.mu.Lock() | |||
| 77 | + | defer r.mu.Unlock() | |||
| 78 | + | ||||
| 79 | + | // Check if widget is already registered | |||
| 80 | + | if existing, exists := r.widgets[ptr]; exists { | |||
| 81 | + | return existing | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Create new SafeWidget | |||
| 85 | + | widget := &SafeWidget{ | |||
| 86 | + | ptr: ptr, | |||
| 87 | + | typeInfo: typeInfo, | |||
| 88 | + | registry: r, | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | r.widgets[ptr] = widget | |||
| 92 | + | return widget | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // Unregister removes a widget from the registry | |||
| 96 | + | func (r *WidgetRegistry) Unregister(ptr uintptr) { | |||
| 97 | + | if r == nil || ptr == 0 { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | r.mu.Lock() | |||
| 102 | + | defer r.mu.Unlock() | |||
| 103 | + | ||||
| 104 | + | delete(r.widgets, ptr) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // Cleanup removes all invalid widgets from the registry | |||
| 108 | + | func (r *WidgetRegistry) Cleanup() { | |||
| 109 | + | if r == nil { | |||
| 110 | + | return | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | r.mu.Lock() | |||
| 114 | + | defer r.mu.Unlock() | |||
| 115 | + | ||||
| 116 | + | for ptr, widget := range r.widgets { | |||
| 117 | + | if !widget.IsValid() { | |||
| 118 | + | delete(r.widgets, ptr) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // Count returns the number of registered widgets | |||
| 124 | + | func (r *WidgetRegistry) Count() int { | |||
| 125 | + | if r == nil { | |||
| 126 | + | return 0 | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | r.mu.RLock() | |||
| 130 | + | defer r.mu.RUnlock() | |||
| 131 | + | ||||
| 132 | + | return len(r.widgets) | |||
| 133 | + | }⏎̸ | |||
M internal/app/browser/workspace_manager.go +10 -10
| @@ -15,16 +15,16 @@ import ( | |||
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | type paneNode struct { | |
| 18 | - | pane *BrowserPane | |
| 19 | - | parent *paneNode | |
| 20 | - | left *paneNode | |
| 21 | - | right *paneNode | |
| 18 | + | pane *BrowserPane | |
| 19 | + | parent *paneNode | |
| 20 | + | left *paneNode | |
| 21 | + | right *paneNode | |
| 22 | 22 | ||
| 23 | 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 | |
| 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 | 28 | ||
| 29 | 29 | // Window type tracking | |
| 30 | 30 | windowType webkit.WindowType // Tab or Popup | |
| @@ -100,7 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 100 | 100 | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |
| 101 | 101 | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 102 | 102 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | |
| 103 | - | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | |
| 103 | + | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | |
| 104 | 104 | } | |
| 105 | 105 | ||
| 106 | 106 | // Initialize specialized managers | |
| @@ -542,7 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string { | |||
| 542 | 542 | inactiveBorderColor, | |
| 543 | 543 | styling.BorderRadius, | |
| 544 | 544 | styling.BorderColor, | |
| 545 | - | styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color | |
| 545 | + | styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color | |
| 546 | 546 | // Additional parameters for stacked pane styles | |
| 547 | 547 | windowBackgroundColor, // stacked-pane-container background | |
| 548 | 548 | styling.BorderRadius, // stacked-pane-container border-radius | |
| @@ -15,16 +15,16 @@ import ( | |||||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | type paneNode struct { | 17 | type paneNode struct { | ||
| 18 | - | pane *BrowserPane | 18 | + | pane *BrowserPane |
| 19 | - | parent *paneNode | 19 | + | parent *paneNode |
| 20 | - | left *paneNode | 20 | + | left *paneNode |
| 21 | - | right *paneNode | 21 | + | right *paneNode |
| 22 | 22 | ||||
| 23 | // Widget management with proper lifecycle tracking | 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) | 24 | + | container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves) |
| 25 | - | orientation webkit.Orientation | 25 | + | orientation webkit.Orientation |
| 26 | - | isLeaf bool | 26 | + | isLeaf bool |
| 27 | - | isPopup bool // Deprecated: use windowType instead | 27 | + | isPopup bool // Deprecated: use windowType instead |
| 28 | 28 | ||||
| 29 | // Window type tracking | 29 | // Window type tracking | ||
| 30 | windowType webkit.WindowType // Tab or Popup | 30 | windowType webkit.WindowType // Tab or Popup | ||
| @@ -100,7 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 100 | lastSplitMsg: make(map[*webkit.WebView]time.Time), | 100 | lastSplitMsg: make(map[*webkit.WebView]time.Time), | ||
| 101 | lastExitMsg: make(map[*webkit.WebView]time.Time), | 101 | lastExitMsg: make(map[*webkit.WebView]time.Time), | ||
| 102 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | 102 | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator | ||
| 103 | - | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry | 103 | + | widgetRegistry: NewWidgetRegistry(), // Initialize widget registry |
| 104 | } | 104 | } | ||
| 105 | 105 | ||||
| 106 | // Initialize specialized managers | 106 | // Initialize specialized managers | ||
| @@ -542,7 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string { | |||||
| 542 | inactiveBorderColor, | 542 | inactiveBorderColor, | ||
| 543 | styling.BorderRadius, | 543 | styling.BorderRadius, | ||
| 544 | styling.BorderColor, | 544 | styling.BorderColor, | ||
| 545 | - | styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color | 545 | + | styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color |
| 546 | // Additional parameters for stacked pane styles | 546 | // Additional parameters for stacked pane styles | ||
| 547 | windowBackgroundColor, // stacked-pane-container background | 547 | windowBackgroundColor, // stacked-pane-container background | ||
| 548 | styling.BorderRadius, // stacked-pane-container border-radius | 548 | styling.BorderRadius, // stacked-pane-container border-radius | ||