Fix popup root split resizing and focus handling
- commit
- 4ca17b2843ef2338ea03e9633845220bf01cfa9b
- author
- bamen · · 2025-10-12 21:09 UTC
- committer
- bamen · · 2025-10-12 18:16 UTC
- parent
- d244331
- signature
- unsigned
3 files · +80 -61
M internal/app/browser/focus_state_machine.go +11 -2
| @@ -709,8 +709,17 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) { | |||
| 709 | 709 | log.Printf("[FSM] Processing focus request %s: %s -> %p", | |
| 710 | 710 | request.ID, request.Source, request.TargetNode) | |
| 711 | 711 | ||
| 712 | - | // Execute focus change | |
| 713 | - | if err := fsm.executeFocusChange(request); err != nil { | |
| 712 | + | // GTK operations must run on main thread - always use IdleAdd for simplicity | |
| 713 | + | var err error | |
| 714 | + | done := make(chan struct{}) | |
| 715 | + | _ = webkit.IdleAdd(func() bool { | |
| 716 | + | err = fsm.executeFocusChange(request) | |
| 717 | + | close(done) | |
| 718 | + | return false | |
| 719 | + | }) | |
| 720 | + | <-done | |
| 721 | + | ||
| 722 | + | if err != nil { | |
| 714 | 723 | fsm.mu.Lock() | |
| 715 | 724 | fsm.metrics.FailedRequests++ | |
| 716 | 725 | fsm.mu.Unlock() | |
| @@ -709,8 +709,17 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) { | |||||
| 709 | log.Printf("[FSM] Processing focus request %s: %s -> %p", | 709 | log.Printf("[FSM] Processing focus request %s: %s -> %p", | ||
| 710 | request.ID, request.Source, request.TargetNode) | 710 | request.ID, request.Source, request.TargetNode) | ||
| 711 | 711 | ||||
| 712 | - | // Execute focus change | 712 | + | // GTK operations must run on main thread - always use IdleAdd for simplicity |
| 713 | - | if err := fsm.executeFocusChange(request); err != nil { | 713 | + | var err error |
| 714 | + | done := make(chan struct{}) | |||
| 715 | + | _ = webkit.IdleAdd(func() bool { | |||
| 716 | + | err = fsm.executeFocusChange(request) | |||
| 717 | + | close(done) | |||
| 718 | + | return false | |||
| 719 | + | }) | |||
| 720 | + | <-done | |||
| 721 | + | ||||
| 722 | + | if err != nil { | |||
| 714 | fsm.mu.Lock() | 723 | fsm.mu.Lock() | ||
| 715 | fsm.metrics.FailedRequests++ | 724 | fsm.metrics.FailedRequests++ | ||
| 716 | fsm.mu.Unlock() | 725 | fsm.mu.Unlock() | ||
M internal/app/browser/workspace_pane_ops.go +67 -55
| @@ -393,18 +393,10 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 393 | 393 | } | |
| 394 | 394 | paned.SetHExpand(true) | |
| 395 | 395 | paned.SetVExpand(true) | |
| 396 | - | ||
| 397 | - | // Configure resize behavior based on whether this is a popup | |
| 398 | - | if existingPane != nil { | |
| 399 | - | // For popup panes: preserve existing pane size, let popup take remaining space | |
| 400 | - | paned.SetResizeStartChild(false) // existing pane keeps its size | |
| 401 | - | paned.SetResizeEndChild(true) // popup can resize | |
| 402 | - | log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible") | |
| 403 | - | } else { | |
| 404 | - | // For normal splits: both panes can resize | |
| 405 | - | paned.SetResizeStartChild(true) | |
| 406 | - | paned.SetResizeEndChild(true) | |
| 407 | - | } | |
| 396 | + | ||
| 397 | + | // Allow both children to resize; avoids shrinking existing pane to zero during root splits | |
| 398 | + | paned.SetResizeStartChild(true) | |
| 399 | + | paned.SetResizeEndChild(true) | |
| 408 | 400 | ||
| 409 | 401 | newLeaf := &paneNode{ | |
| 410 | 402 | pane: newPane, | |
| @@ -422,25 +414,21 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 422 | 414 | parent := split.parent | |
| 423 | 415 | ||
| 424 | 416 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | |
| 417 | + | // NOTE: The active class is correctly removed here - focus will transfer to the NEW pane | |
| 418 | + | // The focus state machine will apply the active class to the new pane via SetActivePane() | |
| 425 | 419 | if splitTargetContainer != nil { | |
| 426 | 420 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | |
| 427 | 421 | if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) { | |
| 428 | 422 | webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass) | |
| 423 | + | log.Printf("[workspace] removed active class from split target (will transfer to new pane)") | |
| 429 | 424 | } | |
| 430 | 425 | } | |
| 431 | 426 | ||
| 432 | 427 | // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented. | |
| 433 | 428 | wm.safelyDetachControllersBeforeReparent(splitTarget) | |
| 434 | 429 | ||
| 435 | - | // Detach split target container from its current GTK parent before inserting into new paned. | |
| 436 | - | if parent == nil { | |
| 437 | - | // Split target is the root - remove it from the window | |
| 438 | - | log.Printf("[workspace] removing split target container=%p from window", splitTargetContainer) | |
| 439 | - | if wm.window != nil { | |
| 440 | - | wm.window.SetChild(nil) | |
| 441 | - | } | |
| 442 | - | // GTK4 will auto-unparent when we add to new paned - no manual unparent needed | |
| 443 | - | } else if parent.container != nil { | |
| 430 | + | // For non-root splits, unparent from parent paned | |
| 431 | + | if parent != nil && parent.container != nil { | |
| 444 | 432 | // Split target has a parent paned - remove it (automatically unparents in GTK4) | |
| 445 | 433 | log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container) | |
| 446 | 434 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | |
| @@ -453,6 +441,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 453 | 441 | webkit.WidgetQueueAllocate(parent.container) | |
| 454 | 442 | } | |
| 455 | 443 | } | |
| 444 | + | // NOTE: For root splits (parent == nil), we DON'T unparent here. | |
| 445 | + | // We'll unparent immediately before adding to paned below to minimize orphan time. | |
| 456 | 446 | ||
| 457 | 447 | // Set up the tree structure first | |
| 458 | 448 | if existingFirst { | |
| @@ -479,29 +469,48 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 479 | 469 | ||
| 480 | 470 | // GTK4 handles widget operations automatically - no need to force redraw | |
| 481 | 471 | ||
| 482 | - | // Add both containers to the new paned | |
| 483 | - | if existingFirst { | |
| 484 | - | if splitTargetContainer != nil { | |
| 472 | + | // Handle root split: attach paned to window FIRST, then let GTK handle reparenting | |
| 473 | + | if parent == nil { | |
| 474 | + | // ROOT SPLIT: Attach empty paned to window, then add children | |
| 475 | + | // GTK will automatically reparent the existing widget when we call SetStartChild/SetEndChild | |
| 476 | + | if wm.window != nil { | |
| 477 | + | // Clear any existing window child (the splitTargetContainer) | |
| 478 | + | wm.window.SetChild(nil) | |
| 479 | + | ||
| 480 | + | // Attach the new paned to window FIRST | |
| 481 | + | wm.window.SetChild(paned) | |
| 482 | + | log.Printf("[workspace] ROOT SPLIT: attached paned %p to window", paned) | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // Now add children to the paned - GTK will handle reparenting automatically | |
| 486 | + | if existingFirst { | |
| 485 | 487 | paned.SetStartChild(splitTargetContainer) | |
| 486 | 488 | paned.SetEndChild(newContainer) | |
| 487 | - | log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer) | |
| 488 | - | } | |
| 489 | - | } else { | |
| 490 | - | if splitTargetContainer != nil { | |
| 489 | + | log.Printf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer) | |
| 490 | + | } else { | |
| 491 | 491 | paned.SetStartChild(newContainer) | |
| 492 | 492 | paned.SetEndChild(splitTargetContainer) | |
| 493 | - | log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer) | |
| 494 | - | } | |
| 495 | - | } | |
| 496 | - | ||
| 497 | - | // Attach the new paned to its parent | |
| 498 | - | if parent == nil { | |
| 499 | - | if wm.window != nil { | |
| 500 | - | wm.window.SetChild(paned) | |
| 501 | - | paned.QueueAllocate() | |
| 502 | - | log.Printf("[workspace] paned set as window child: paned=%p", paned) | |
| 493 | + | log.Printf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer) | |
| 503 | 494 | } | |
| 504 | 495 | } else { | |
| 496 | + | // NON-ROOT SPLIT: Standard approach | |
| 497 | + | ||
| 498 | + | // Add both containers to the new paned | |
| 499 | + | if existingFirst { | |
| 500 | + | if splitTargetContainer != nil { | |
| 501 | + | paned.SetStartChild(splitTargetContainer) | |
| 502 | + | paned.SetEndChild(newContainer) | |
| 503 | + | log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer) | |
| 504 | + | } | |
| 505 | + | } else { | |
| 506 | + | if splitTargetContainer != nil { | |
| 507 | + | paned.SetStartChild(newContainer) | |
| 508 | + | paned.SetEndChild(splitTargetContainer) | |
| 509 | + | log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer) | |
| 510 | + | } | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | // Attach the new paned to its parent | |
| 505 | 514 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | |
| 506 | 515 | if parent.left == split { | |
| 507 | 516 | parentPaned.SetStartChild(paned) | |
| @@ -519,22 +528,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 519 | 528 | // Synchronize the divider once the paned has a meaningful allocation. | |
| 520 | 529 | wm.syncPanedDivider(paned, orientation, split) | |
| 521 | 530 | ||
| 522 | - | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |
| 523 | - | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| 524 | - | if target != nil && target.container != nil { | |
| 525 | - | wm.scheduleIdleGuarded(func() bool { | |
| 526 | - | if target == nil || !target.widgetValid || target.container == nil { | |
| 527 | - | return false | |
| 528 | - | } | |
| 529 | - | // Show widget to reconnect WebKit rendering pipeline | |
| 530 | - | webkit.WidgetShow(target.container) | |
| 531 | - | // Force GTK to recalculate size and recreate rendering surface | |
| 532 | - | webkit.WidgetQueueResize(target.container) | |
| 533 | - | webkit.WidgetQueueDraw(target.container) | |
| 534 | - | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |
| 535 | - | return false | |
| 536 | - | }, target) | |
| 537 | - | } | |
| 531 | + | // GTK4 automatically handles widget rendering after reparenting - no manual updates needed | |
| 538 | 532 | ||
| 539 | 533 | wm.viewToNode[newPane.webView] = newLeaf | |
| 540 | 534 | wm.ensureHover(newLeaf) | |
| @@ -555,7 +549,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 555 | 549 | // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow | |
| 556 | 550 | log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)") | |
| 557 | 551 | } | |
| 558 | - | ||
| 552 | + | ||
| 559 | 553 | if newPane.zoomController != nil { | |
| 560 | 554 | newPane.zoomController.ApplyInitialZoom() | |
| 561 | 555 | } | |
| @@ -574,6 +568,24 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 574 | 568 | if newContainer != nil { | |
| 575 | 569 | webkit.WidgetShow(newContainer) | |
| 576 | 570 | } | |
| 571 | + | ||
| 572 | + | // CRITICAL: For root splits, also show the split target container after reparenting | |
| 573 | + | // When the split target is reparented from window → paned, GTK doesn't preserve visibility | |
| 574 | + | if parent == nil && splitTargetContainer != nil { | |
| 575 | + | webkit.WidgetShow(splitTargetContainer) | |
| 576 | + | webkit.WidgetQueueAllocate(splitTargetContainer) | |
| 577 | + | webkit.WidgetQueueDraw(splitTargetContainer) | |
| 578 | + | } | |
| 579 | + | ||
| 580 | + | // Ensure the underlying WebView widget is visible after reparenting | |
| 581 | + | if parent == nil && splitTarget != nil && splitTarget.pane != nil && splitTarget.pane.webView != nil { | |
| 582 | + | if webViewWidget := splitTarget.pane.webView.AsWidget(); webViewWidget != nil { | |
| 583 | + | webkit.WidgetShow(webViewWidget) | |
| 584 | + | webkit.WidgetQueueAllocate(webViewWidget) | |
| 585 | + | webkit.WidgetQueueDraw(webViewWidget) | |
| 586 | + | } | |
| 587 | + | } | |
| 588 | + | ||
| 577 | 589 | // Attach GTK focus controller to new pane | |
| 578 | 590 | if wm.focusStateMachine != nil { | |
| 579 | 591 | wm.focusStateMachine.attachGTKController(newLeaf) | |
| @@ -393,18 +393,10 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 393 | } | 393 | } | ||
| 394 | paned.SetHExpand(true) | 394 | paned.SetHExpand(true) | ||
| 395 | paned.SetVExpand(true) | 395 | paned.SetVExpand(true) | ||
| 396 | - | 396 | + | ||
| 397 | - | // Configure resize behavior based on whether this is a popup | 397 | + | // Allow both children to resize; avoids shrinking existing pane to zero during root splits |
| 398 | - | if existingPane != nil { | 398 | + | paned.SetResizeStartChild(true) |
| 399 | - | // For popup panes: preserve existing pane size, let popup take remaining space | 399 | + | paned.SetResizeEndChild(true) |
| 400 | - | paned.SetResizeStartChild(false) // existing pane keeps its size | |||
| 401 | - | paned.SetResizeEndChild(true) // popup can resize | |||
| 402 | - | log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible") | |||
| 403 | - | } else { | |||
| 404 | - | // For normal splits: both panes can resize | |||
| 405 | - | paned.SetResizeStartChild(true) | |||
| 406 | - | paned.SetResizeEndChild(true) | |||
| 407 | - | } | |||
| 408 | 400 | ||||
| 409 | newLeaf := &paneNode{ | 401 | newLeaf := &paneNode{ | ||
| 410 | pane: newPane, | 402 | pane: newPane, | ||
| @@ -422,25 +414,21 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 422 | parent := split.parent | 414 | parent := split.parent | ||
| 423 | 415 | ||||
| 424 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | 416 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | ||
| 417 | + | // NOTE: The active class is correctly removed here - focus will transfer to the NEW pane | |||
| 418 | + | // The focus state machine will apply the active class to the new pane via SetActivePane() | |||
| 425 | if splitTargetContainer != nil { | 419 | if splitTargetContainer != nil { | ||
| 426 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | 420 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | ||
| 427 | if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) { | 421 | if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) { | ||
| 428 | webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass) | 422 | webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass) | ||
| 423 | + | log.Printf("[workspace] removed active class from split target (will transfer to new pane)") | |||
| 429 | } | 424 | } | ||
| 430 | } | 425 | } | ||
| 431 | 426 | ||||
| 432 | // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented. | 427 | // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented. | ||
| 433 | wm.safelyDetachControllersBeforeReparent(splitTarget) | 428 | wm.safelyDetachControllersBeforeReparent(splitTarget) | ||
| 434 | 429 | ||||
| 435 | - | // Detach split target container from its current GTK parent before inserting into new paned. | 430 | + | // For non-root splits, unparent from parent paned |
| 436 | - | if parent == nil { | 431 | + | if parent != nil && parent.container != nil { |
| 437 | - | // Split target is the root - remove it from the window | |||
| 438 | - | log.Printf("[workspace] removing split target container=%p from window", splitTargetContainer) | |||
| 439 | - | if wm.window != nil { | |||
| 440 | - | wm.window.SetChild(nil) | |||
| 441 | - | } | |||
| 442 | - | // GTK4 will auto-unparent when we add to new paned - no manual unparent needed | |||
| 443 | - | } else if parent.container != nil { | |||
| 444 | // Split target has a parent paned - remove it (automatically unparents in GTK4) | 432 | // Split target has a parent paned - remove it (automatically unparents in GTK4) | ||
| 445 | log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container) | 433 | log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container) | ||
| 446 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | 434 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | ||
| @@ -453,6 +441,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 453 | webkit.WidgetQueueAllocate(parent.container) | 441 | webkit.WidgetQueueAllocate(parent.container) | ||
| 454 | } | 442 | } | ||
| 455 | } | 443 | } | ||
| 444 | + | // NOTE: For root splits (parent == nil), we DON'T unparent here. | |||
| 445 | + | // We'll unparent immediately before adding to paned below to minimize orphan time. | |||
| 456 | 446 | ||||
| 457 | // Set up the tree structure first | 447 | // Set up the tree structure first | ||
| 458 | if existingFirst { | 448 | if existingFirst { | ||
| @@ -479,29 +469,48 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 479 | 469 | ||||
| 480 | // GTK4 handles widget operations automatically - no need to force redraw | 470 | // GTK4 handles widget operations automatically - no need to force redraw | ||
| 481 | 471 | ||||
| 482 | - | // Add both containers to the new paned | 472 | + | // Handle root split: attach paned to window FIRST, then let GTK handle reparenting |
| 483 | - | if existingFirst { | 473 | + | if parent == nil { |
| 484 | - | if splitTargetContainer != nil { | 474 | + | // ROOT SPLIT: Attach empty paned to window, then add children |
| 475 | + | // GTK will automatically reparent the existing widget when we call SetStartChild/SetEndChild | |||
| 476 | + | if wm.window != nil { | |||
| 477 | + | // Clear any existing window child (the splitTargetContainer) | |||
| 478 | + | wm.window.SetChild(nil) | |||
| 479 | + | ||||
| 480 | + | // Attach the new paned to window FIRST | |||
| 481 | + | wm.window.SetChild(paned) | |||
| 482 | + | log.Printf("[workspace] ROOT SPLIT: attached paned %p to window", paned) | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // Now add children to the paned - GTK will handle reparenting automatically | |||
| 486 | + | if existingFirst { | |||
| 485 | paned.SetStartChild(splitTargetContainer) | 487 | paned.SetStartChild(splitTargetContainer) | ||
| 486 | paned.SetEndChild(newContainer) | 488 | paned.SetEndChild(newContainer) | ||
| 487 | - | log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer) | 489 | + | log.Printf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer) |
| 488 | - | } | 490 | + | } else { |
| 489 | - | } else { | |||
| 490 | - | if splitTargetContainer != nil { | |||
| 491 | paned.SetStartChild(newContainer) | 491 | paned.SetStartChild(newContainer) | ||
| 492 | paned.SetEndChild(splitTargetContainer) | 492 | paned.SetEndChild(splitTargetContainer) | ||
| 493 | - | log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer) | 493 | + | log.Printf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer) |
| 494 | - | } | |||
| 495 | - | } | |||
| 496 | - | ||||
| 497 | - | // Attach the new paned to its parent | |||
| 498 | - | if parent == nil { | |||
| 499 | - | if wm.window != nil { | |||
| 500 | - | wm.window.SetChild(paned) | |||
| 501 | - | paned.QueueAllocate() | |||
| 502 | - | log.Printf("[workspace] paned set as window child: paned=%p", paned) | |||
| 503 | } | 494 | } | ||
| 504 | } else { | 495 | } else { | ||
| 496 | + | // NON-ROOT SPLIT: Standard approach | |||
| 497 | + | ||||
| 498 | + | // Add both containers to the new paned | |||
| 499 | + | if existingFirst { | |||
| 500 | + | if splitTargetContainer != nil { | |||
| 501 | + | paned.SetStartChild(splitTargetContainer) | |||
| 502 | + | paned.SetEndChild(newContainer) | |||
| 503 | + | log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer) | |||
| 504 | + | } | |||
| 505 | + | } else { | |||
| 506 | + | if splitTargetContainer != nil { | |||
| 507 | + | paned.SetStartChild(newContainer) | |||
| 508 | + | paned.SetEndChild(splitTargetContainer) | |||
| 509 | + | log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer) | |||
| 510 | + | } | |||
| 511 | + | } | |||
| 512 | + | ||||
| 513 | + | // Attach the new paned to its parent | |||
| 505 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | 514 | if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil { | ||
| 506 | if parent.left == split { | 515 | if parent.left == split { | ||
| 507 | parentPaned.SetStartChild(paned) | 516 | parentPaned.SetStartChild(paned) | ||
| @@ -519,22 +528,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 519 | // Synchronize the divider once the paned has a meaningful allocation. | 528 | // Synchronize the divider once the paned has a meaningful allocation. | ||
| 520 | wm.syncPanedDivider(paned, orientation, split) | 529 | wm.syncPanedDivider(paned, orientation, split) | ||
| 521 | 530 | ||||
| 522 | - | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | 531 | + | // GTK4 automatically handles widget rendering after reparenting - no manual updates needed |
| 523 | - | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |||
| 524 | - | if target != nil && target.container != nil { | |||
| 525 | - | wm.scheduleIdleGuarded(func() bool { | |||
| 526 | - | if target == nil || !target.widgetValid || target.container == nil { | |||
| 527 | - | return false | |||
| 528 | - | } | |||
| 529 | - | // Show widget to reconnect WebKit rendering pipeline | |||
| 530 | - | webkit.WidgetShow(target.container) | |||
| 531 | - | // Force GTK to recalculate size and recreate rendering surface | |||
| 532 | - | webkit.WidgetQueueResize(target.container) | |||
| 533 | - | webkit.WidgetQueueDraw(target.container) | |||
| 534 | - | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |||
| 535 | - | return false | |||
| 536 | - | }, target) | |||
| 537 | - | } | |||
| 538 | 532 | ||||
| 539 | wm.viewToNode[newPane.webView] = newLeaf | 533 | wm.viewToNode[newPane.webView] = newLeaf | ||
| 540 | wm.ensureHover(newLeaf) | 534 | wm.ensureHover(newLeaf) | ||
| @@ -555,7 +549,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 555 | // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow | 549 | // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow | ||
| 556 | log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)") | 550 | log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)") | ||
| 557 | } | 551 | } | ||
| 558 | - | 552 | + | ||
| 559 | if newPane.zoomController != nil { | 553 | if newPane.zoomController != nil { | ||
| 560 | newPane.zoomController.ApplyInitialZoom() | 554 | newPane.zoomController.ApplyInitialZoom() | ||
| 561 | } | 555 | } | ||
| @@ -574,6 +568,24 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 574 | if newContainer != nil { | 568 | if newContainer != nil { | ||
| 575 | webkit.WidgetShow(newContainer) | 569 | webkit.WidgetShow(newContainer) | ||
| 576 | } | 570 | } | ||
| 571 | + | ||||
| 572 | + | // CRITICAL: For root splits, also show the split target container after reparenting | |||
| 573 | + | // When the split target is reparented from window → paned, GTK doesn't preserve visibility | |||
| 574 | + | if parent == nil && splitTargetContainer != nil { | |||
| 575 | + | webkit.WidgetShow(splitTargetContainer) | |||
| 576 | + | webkit.WidgetQueueAllocate(splitTargetContainer) | |||
| 577 | + | webkit.WidgetQueueDraw(splitTargetContainer) | |||
| 578 | + | } | |||
| 579 | + | ||||
| 580 | + | // Ensure the underlying WebView widget is visible after reparenting | |||
| 581 | + | if parent == nil && splitTarget != nil && splitTarget.pane != nil && splitTarget.pane.webView != nil { | |||
| 582 | + | if webViewWidget := splitTarget.pane.webView.AsWidget(); webViewWidget != nil { | |||
| 583 | + | webkit.WidgetShow(webViewWidget) | |||
| 584 | + | webkit.WidgetQueueAllocate(webViewWidget) | |||
| 585 | + | webkit.WidgetQueueDraw(webViewWidget) | |||
| 586 | + | } | |||
| 587 | + | } | |||
| 588 | + | ||||
| 577 | // Attach GTK focus controller to new pane | 589 | // Attach GTK focus controller to new pane | ||
| 578 | if wm.focusStateMachine != nil { | 590 | if wm.focusStateMachine != nil { | ||
| 579 | wm.focusStateMachine.attachGTKController(newLeaf) | 591 | wm.focusStateMachine.attachGTKController(newLeaf) | ||
M internal/app/browser/workspace_popup.go +2 -4
| @@ -533,10 +533,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||
| 533 | 533 | ||
| 534 | 534 | wm.ensureGUIInPane(popupPane) | |
| 535 | 535 | ||
| 536 | - | // WebKit handles loading the URL automatically, just ensure visibility | |
| 537 | - | if err := wrappedView.Show(); err != nil { | |
| 538 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |
| 539 | - | } | |
| 536 | + | // Widgets are already shown by splitNode - no need to show again | |
| 537 | + | // GTK4 handles visibility automatically during reparenting | |
| 540 | 538 | ||
| 541 | 539 | log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url) | |
| 542 | 540 | } | |
| @@ -533,10 +533,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||||
| 533 | 533 | ||||
| 534 | wm.ensureGUIInPane(popupPane) | 534 | wm.ensureGUIInPane(popupPane) | ||
| 535 | 535 | ||||
| 536 | - | // WebKit handles loading the URL automatically, just ensure visibility | 536 | + | // Widgets are already shown by splitNode - no need to show again |
| 537 | - | if err := wrappedView.Show(); err != nil { | 537 | + | // GTK4 handles visibility automatically during reparenting |
| 538 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |||
| 539 | - | } | |||
| 540 | 538 | ||||
| 541 | log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url) | 539 | log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url) | ||
| 542 | } | 540 | } | ||