fix(workspace): handle stacked panes correctly in popup insertion
- Add stacked pane detection logic to insertPopupPane - Split around entire stack container instead of individual panes - Fix widget reparenting to prevent WebView corruption - Add webkit.IsPaned() checks to avoid GTK assertion failures
- commit
- 81824f0e896f778fef853c0231550a0dba300cef
- author
- bamen · · 2025-10-05 17:15 UTC
- committer
- bamen · · 2025-10-05 21:09 UTC
- parent
- 00b5779
- signature
- unsigned
5 files · +113 -48
M gui/package-lock.json +15 -0
| @@ -4011,6 +4011,21 @@ | |||
| 4011 | 4011 | } | |
| 4012 | 4012 | } | |
| 4013 | 4013 | }, | |
| 4014 | + | "node_modules/svelte-check/node_modules/picomatch": { | |
| 4015 | + | "version": "4.0.3", | |
| 4016 | + | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", | |
| 4017 | + | "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", | |
| 4018 | + | "dev": true, | |
| 4019 | + | "license": "MIT", | |
| 4020 | + | "optional": true, | |
| 4021 | + | "peer": true, | |
| 4022 | + | "engines": { | |
| 4023 | + | "node": ">=12" | |
| 4024 | + | }, | |
| 4025 | + | "funding": { | |
| 4026 | + | "url": "https://github.com/sponsors/jonschlinkert" | |
| 4027 | + | } | |
| 4028 | + | }, | |
| 4014 | 4029 | "node_modules/tailwindcss": { | |
| 4015 | 4030 | "version": "4.1.13", | |
| 4016 | 4031 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", | |
| @@ -4011,6 +4011,21 @@ | |||||
| 4011 | } | 4011 | } | ||
| 4012 | } | 4012 | } | ||
| 4013 | }, | 4013 | }, | ||
| 4014 | + | "node_modules/svelte-check/node_modules/picomatch": { | |||
| 4015 | + | "version": "4.0.3", | |||
| 4016 | + | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", | |||
| 4017 | + | "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", | |||
| 4018 | + | "dev": true, | |||
| 4019 | + | "license": "MIT", | |||
| 4020 | + | "optional": true, | |||
| 4021 | + | "peer": true, | |||
| 4022 | + | "engines": { | |||
| 4023 | + | "node": ">=12" | |||
| 4024 | + | }, | |||
| 4025 | + | "funding": { | |||
| 4026 | + | "url": "https://github.com/sponsors/jonschlinkert" | |||
| 4027 | + | } | |||
| 4028 | + | }, | |||
| 4014 | "node_modules/tailwindcss": { | 4029 | "node_modules/tailwindcss": { | ||
| 4015 | "version": "4.1.13", | 4030 | "version": "4.1.13", | ||
| 4016 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", | 4031 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", | ||
M internal/app/browser/workspace_pane_ops.go +70 -40
| @@ -340,12 +340,39 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 340 | 340 | webkit.WidgetRealizeInContainer(webViewWidget) | |
| 341 | 341 | } | |
| 342 | 342 | ||
| 343 | - | if target.container == 0 { | |
| 344 | - | return errors.New("existing pane missing container") | |
| 343 | + | // CRITICAL: Handle stacked panes correctly - same logic as splitNode | |
| 344 | + | // When the target is inside a stacked pane, we need to split around the entire stack, | |
| 345 | + | // not the individual pane within the stack | |
| 346 | + | originalTarget := target | |
| 347 | + | var stackContainer *paneNode | |
| 348 | + | if target.parent != nil && target.parent.isStacked { | |
| 349 | + | // Target is in a stack - we'll create the split around the entire stack | |
| 350 | + | stackContainer = target.parent | |
| 351 | + | log.Printf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer) | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | // Determine what we're actually splitting | |
| 355 | + | var splitTarget *paneNode | |
| 356 | + | var splitTargetContainer uintptr | |
| 357 | + | ||
| 358 | + | if stackContainer != nil { | |
| 359 | + | // Case: splitting from inside a stack - we split around the entire stack | |
| 360 | + | splitTarget = stackContainer | |
| 361 | + | splitTargetContainer = stackContainer.container | |
| 362 | + | log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget) | |
| 363 | + | } else { | |
| 364 | + | // Case: normal split from a simple pane or direct stack target | |
| 365 | + | splitTarget = target | |
| 366 | + | splitTargetContainer = target.container | |
| 367 | + | log.Printf("[workspace] normal split from pane: target=%p", splitTarget) | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | if splitTargetContainer == 0 { | |
| 371 | + | return errors.New("split target missing container") | |
| 345 | 372 | } | |
| 346 | 373 | ||
| 347 | 374 | orientation, existingFirst := mapDirection(direction) | |
| 348 | - | log.Printf("[workspace] inserting popup direction=%s orientation=%v existingFirst=%v target.parent=%p", direction, orientation, existingFirst, target.parent) | |
| 375 | + | log.Printf("[workspace] inserting popup direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p", direction, orientation, existingFirst, splitTarget.parent) | |
| 349 | 376 | ||
| 350 | 377 | paned := webkit.NewPaned(orientation) | |
| 351 | 378 | if paned == 0 { | |
| @@ -365,7 +392,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 365 | 392 | wm.initializePaneWidgets(newLeaf, newContainer) | |
| 366 | 393 | ||
| 367 | 394 | split := &paneNode{ | |
| 368 | - | parent: target.parent, | |
| 395 | + | parent: splitTarget.parent, | |
| 369 | 396 | orientation: orientation, | |
| 370 | 397 | isLeaf: false, | |
| 371 | 398 | } | |
| @@ -374,45 +401,47 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 374 | 401 | parent := split.parent | |
| 375 | 402 | ||
| 376 | 403 | // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout | |
| 404 | + | // Only attempt this if the parent container is actually a paned widget | |
| 377 | 405 | var parentDividerPos int | |
| 378 | - | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) { | |
| 406 | + | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) && webkit.IsPaned(parent.container) { | |
| 379 | 407 | parentDividerPos = webkit.PanedGetPosition(parent.container) | |
| 380 | 408 | log.Printf("[workspace] captured parent divider position: %d", parentDividerPos) | |
| 409 | + | } else if parent != nil && parent.container != 0 { | |
| 410 | + | log.Printf("[workspace] parent container is not a paned widget (likely a stacked container)") | |
| 381 | 411 | } | |
| 382 | 412 | ||
| 383 | 413 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | |
| 384 | - | existingContainer := target.container | |
| 385 | - | if existingContainer != 0 && webkit.WidgetIsValid(existingContainer) { | |
| 414 | + | if splitTargetContainer != 0 && webkit.WidgetIsValid(splitTargetContainer) { | |
| 386 | 415 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | |
| 387 | - | if webkit.WidgetHasCSSClass(existingContainer, activePaneClass) { | |
| 388 | - | webkit.WidgetRemoveCSSClass(existingContainer, activePaneClass) | |
| 416 | + | if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) { | |
| 417 | + | webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass) | |
| 389 | 418 | } | |
| 390 | 419 | ||
| 391 | 420 | // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline | |
| 392 | 421 | // This forces WebKit to detach its compositor, preventing rendering corruption | |
| 393 | - | webkit.WidgetHide(existingContainer) | |
| 394 | - | log.Printf("[workspace] Hidden existing container before reparenting: %#x", existingContainer) | |
| 422 | + | webkit.WidgetHide(splitTargetContainer) | |
| 423 | + | log.Printf("[workspace] Hidden split target container before reparenting: %#x", splitTargetContainer) | |
| 395 | 424 | } | |
| 396 | 425 | ||
| 397 | 426 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | |
| 398 | - | wm.safelyDetachControllersBeforeReparent(target) | |
| 427 | + | wm.safelyDetachControllersBeforeReparent(splitTarget) | |
| 399 | 428 | ||
| 400 | 429 | // Detach existing container from its current GTK parent before inserting into new paned. | |
| 401 | 430 | if parent == nil { | |
| 402 | - | // Target is the root - remove it from the window | |
| 403 | - | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | |
| 431 | + | // Split target is the root - remove it from the window | |
| 432 | + | log.Printf("[workspace] removing split target container=%#x from window", splitTargetContainer) | |
| 404 | 433 | if wm.window != nil { | |
| 405 | 434 | wm.window.SetChild(0) | |
| 406 | 435 | } | |
| 407 | 436 | // GTK4 will auto-unparent when we add to new paned - no manual unparent needed | |
| 408 | 437 | } else if parent.container != 0 { | |
| 409 | - | // Target has a parent paned - unparent it from there | |
| 410 | - | log.Printf("[workspace] unparenting existing container from parent paned") | |
| 438 | + | // Split target has a parent paned - unparent it from there | |
| 439 | + | log.Printf("[workspace] unparenting split target container from parent paned") | |
| 411 | 440 | if webkit.WidgetIsValid(parent.container) { | |
| 412 | 441 | log.Printf("[workspace] executing unparent: parentPtr=%#x", parent.container) | |
| 413 | - | if parent.left == target { | |
| 442 | + | if parent.left == splitTarget { | |
| 414 | 443 | webkit.PanedSetStartChild(parent.container, 0) | |
| 415 | - | } else if parent.right == target { | |
| 444 | + | } else if parent.right == splitTarget { | |
| 416 | 445 | webkit.PanedSetEndChild(parent.container, 0) | |
| 417 | 446 | } | |
| 418 | 447 | webkit.WidgetQueueAllocate(parent.container) | |
| @@ -421,20 +450,20 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 421 | 450 | ||
| 422 | 451 | // Add both containers to the new paned | |
| 423 | 452 | if existingFirst { | |
| 424 | - | split.left = target | |
| 453 | + | split.left = splitTarget | |
| 425 | 454 | split.right = newLeaf | |
| 426 | - | webkit.PanedSetStartChild(paned, existingContainer) | |
| 455 | + | webkit.PanedSetStartChild(paned, splitTargetContainer) | |
| 427 | 456 | webkit.PanedSetEndChild(paned, newContainer) | |
| 428 | - | log.Printf("[workspace] added existing=%#x as start child, new=%#x as end child", existingContainer, newContainer) | |
| 457 | + | log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer) | |
| 429 | 458 | } else { | |
| 430 | 459 | split.left = newLeaf | |
| 431 | - | split.right = target | |
| 460 | + | split.right = splitTarget | |
| 432 | 461 | webkit.PanedSetStartChild(paned, newContainer) | |
| 433 | - | webkit.PanedSetEndChild(paned, existingContainer) | |
| 434 | - | log.Printf("[workspace] added new=%#x as start child, existing=%#x as end child", newContainer, existingContainer) | |
| 462 | + | webkit.PanedSetEndChild(paned, splitTargetContainer) | |
| 463 | + | log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer) | |
| 435 | 464 | } | |
| 436 | 465 | ||
| 437 | - | target.parent = split | |
| 466 | + | splitTarget.parent = split | |
| 438 | 467 | newLeaf.parent = split | |
| 439 | 468 | ||
| 440 | 469 | if parent == nil { | |
| @@ -447,10 +476,10 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 447 | 476 | } else { | |
| 448 | 477 | if webkit.WidgetIsValid(parent.container) { | |
| 449 | 478 | log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parent.container, paned) | |
| 450 | - | if parent.left == target { | |
| 479 | + | if parent.left == splitTarget { | |
| 451 | 480 | parent.left = split | |
| 452 | 481 | webkit.PanedSetStartChild(parent.container, paned) | |
| 453 | - | } else if parent.right == target { | |
| 482 | + | } else if parent.right == splitTarget { | |
| 454 | 483 | parent.right = split | |
| 455 | 484 | webkit.PanedSetEndChild(parent.container, paned) | |
| 456 | 485 | } | |
| @@ -459,7 +488,8 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 459 | 488 | log.Printf("[workspace] paned inserted into parent successfully") | |
| 460 | 489 | ||
| 461 | 490 | // CRITICAL: Restore parent paned's divider position to preserve existing layout | |
| 462 | - | if parentDividerPos > 0 { | |
| 491 | + | // Only attempt this if we captured a position and the parent is actually a paned widget | |
| 492 | + | if parentDividerPos > 0 && webkit.IsPaned(parent.container) { | |
| 463 | 493 | webkit.PanedSetPosition(parent.container, parentDividerPos) | |
| 464 | 494 | log.Printf("[workspace] restored parent divider position: %d", parentDividerPos) | |
| 465 | 495 | } | |
| @@ -502,26 +532,26 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 502 | 532 | return false | |
| 503 | 533 | }, split) | |
| 504 | 534 | ||
| 505 | - | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |
| 535 | + | // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting | |
| 506 | 536 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| 507 | - | if target != nil && target.container != 0 { | |
| 537 | + | if splitTarget != nil && splitTarget.container != 0 { | |
| 508 | 538 | wm.scheduleIdleGuarded(func() bool { | |
| 509 | - | if target == nil || !target.widgetValid || target.container == 0 { | |
| 539 | + | if splitTarget == nil || !splitTarget.widgetValid || splitTarget.container == 0 { | |
| 510 | 540 | return false | |
| 511 | 541 | } | |
| 512 | 542 | // Show widget to reconnect WebKit rendering pipeline | |
| 513 | - | webkit.WidgetShow(target.container) | |
| 543 | + | webkit.WidgetShow(splitTarget.container) | |
| 514 | 544 | // Force GTK to recalculate size and recreate rendering surface | |
| 515 | - | webkit.WidgetQueueResize(target.container) | |
| 516 | - | webkit.WidgetQueueDraw(target.container) | |
| 517 | - | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |
| 545 | + | webkit.WidgetQueueResize(splitTarget.container) | |
| 546 | + | webkit.WidgetQueueDraw(splitTarget.container) | |
| 547 | + | log.Printf("[workspace] Shown and queued resize+draw for split target container after reparenting") | |
| 518 | 548 | return false | |
| 519 | - | }, target) | |
| 549 | + | }, splitTarget) | |
| 520 | 550 | } | |
| 521 | 551 | ||
| 522 | 552 | wm.viewToNode[newPane.webView] = newLeaf | |
| 523 | 553 | wm.ensureHover(newLeaf) | |
| 524 | - | wm.ensureHover(target) | |
| 554 | + | wm.ensureHover(splitTarget) | |
| 525 | 555 | wm.app.panes = append(wm.app.panes, newPane) | |
| 526 | 556 | if newPane.zoomController != nil { | |
| 527 | 557 | newPane.zoomController.ApplyInitialZoom() | |
| @@ -529,9 +559,9 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 529 | 559 | ||
| 530 | 560 | // Update CSS classes for all panes now that we have multiple panes | |
| 531 | 561 | ||
| 532 | - | reattachTargets := []*paneNode{target} | |
| 533 | - | if target != nil && target.isStacked { | |
| 534 | - | reattachTargets = append(reattachTargets, target.stackedPanes...) | |
| 562 | + | reattachTargets := []*paneNode{splitTarget} | |
| 563 | + | if splitTarget != nil && splitTarget.isStacked { | |
| 564 | + | reattachTargets = append(reattachTargets, splitTarget.stackedPanes...) | |
| 535 | 565 | } | |
| 536 | 566 | guardNodes := append([]*paneNode{newLeaf}, reattachTargets...) | |
| 537 | 567 | wm.scheduleIdleGuarded(func() bool { | |
| @@ -340,12 +340,39 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 340 | webkit.WidgetRealizeInContainer(webViewWidget) | 340 | webkit.WidgetRealizeInContainer(webViewWidget) | ||
| 341 | } | 341 | } | ||
| 342 | 342 | ||||
| 343 | - | if target.container == 0 { | 343 | + | // CRITICAL: Handle stacked panes correctly - same logic as splitNode |
| 344 | - | return errors.New("existing pane missing container") | 344 | + | // When the target is inside a stacked pane, we need to split around the entire stack, |
| 345 | + | // not the individual pane within the stack | |||
| 346 | + | originalTarget := target | |||
| 347 | + | var stackContainer *paneNode | |||
| 348 | + | if target.parent != nil && target.parent.isStacked { | |||
| 349 | + | // Target is in a stack - we'll create the split around the entire stack | |||
| 350 | + | stackContainer = target.parent | |||
| 351 | + | log.Printf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer) | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | // Determine what we're actually splitting | |||
| 355 | + | var splitTarget *paneNode | |||
| 356 | + | var splitTargetContainer uintptr | |||
| 357 | + | ||||
| 358 | + | if stackContainer != nil { | |||
| 359 | + | // Case: splitting from inside a stack - we split around the entire stack | |||
| 360 | + | splitTarget = stackContainer | |||
| 361 | + | splitTargetContainer = stackContainer.container | |||
| 362 | + | log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget) | |||
| 363 | + | } else { | |||
| 364 | + | // Case: normal split from a simple pane or direct stack target | |||
| 365 | + | splitTarget = target | |||
| 366 | + | splitTargetContainer = target.container | |||
| 367 | + | log.Printf("[workspace] normal split from pane: target=%p", splitTarget) | |||
| 368 | + | } | |||
| 369 | + | ||||
| 370 | + | if splitTargetContainer == 0 { | |||
| 371 | + | return errors.New("split target missing container") | |||
| 345 | } | 372 | } | ||
| 346 | 373 | ||||
| 347 | orientation, existingFirst := mapDirection(direction) | 374 | orientation, existingFirst := mapDirection(direction) | ||
| 348 | - | log.Printf("[workspace] inserting popup direction=%s orientation=%v existingFirst=%v target.parent=%p", direction, orientation, existingFirst, target.parent) | 375 | + | log.Printf("[workspace] inserting popup direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p", direction, orientation, existingFirst, splitTarget.parent) |
| 349 | 376 | ||||
| 350 | paned := webkit.NewPaned(orientation) | 377 | paned := webkit.NewPaned(orientation) | ||
| 351 | if paned == 0 { | 378 | if paned == 0 { | ||
| @@ -365,7 +392,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 365 | wm.initializePaneWidgets(newLeaf, newContainer) | 392 | wm.initializePaneWidgets(newLeaf, newContainer) | ||
| 366 | 393 | ||||
| 367 | split := &paneNode{ | 394 | split := &paneNode{ | ||
| 368 | - | parent: target.parent, | 395 | + | parent: splitTarget.parent, |
| 369 | orientation: orientation, | 396 | orientation: orientation, | ||
| 370 | isLeaf: false, | 397 | isLeaf: false, | ||
| 371 | } | 398 | } | ||
| @@ -374,45 +401,47 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 374 | parent := split.parent | 401 | parent := split.parent | ||
| 375 | 402 | ||||
| 376 | // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout | 403 | // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout | ||
| 404 | + | // Only attempt this if the parent container is actually a paned widget | |||
| 377 | var parentDividerPos int | 405 | var parentDividerPos int | ||
| 378 | - | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) { | 406 | + | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) && webkit.IsPaned(parent.container) { |
| 379 | parentDividerPos = webkit.PanedGetPosition(parent.container) | 407 | parentDividerPos = webkit.PanedGetPosition(parent.container) | ||
| 380 | log.Printf("[workspace] captured parent divider position: %d", parentDividerPos) | 408 | log.Printf("[workspace] captured parent divider position: %d", parentDividerPos) | ||
| 409 | + | } else if parent != nil && parent.container != 0 { | |||
| 410 | + | log.Printf("[workspace] parent container is not a paned widget (likely a stacked container)") | |||
| 381 | } | 411 | } | ||
| 382 | 412 | ||||
| 383 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | 413 | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | ||
| 384 | - | existingContainer := target.container | 414 | + | if splitTargetContainer != 0 && webkit.WidgetIsValid(splitTargetContainer) { |
| 385 | - | if existingContainer != 0 && webkit.WidgetIsValid(existingContainer) { | |||
| 386 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | 415 | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | ||
| 387 | - | if webkit.WidgetHasCSSClass(existingContainer, activePaneClass) { | 416 | + | if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) { |
| 388 | - | webkit.WidgetRemoveCSSClass(existingContainer, activePaneClass) | 417 | + | webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass) |
| 389 | } | 418 | } | ||
| 390 | 419 | ||||
| 391 | // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline | 420 | // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline | ||
| 392 | // This forces WebKit to detach its compositor, preventing rendering corruption | 421 | // This forces WebKit to detach its compositor, preventing rendering corruption | ||
| 393 | - | webkit.WidgetHide(existingContainer) | 422 | + | webkit.WidgetHide(splitTargetContainer) |
| 394 | - | log.Printf("[workspace] Hidden existing container before reparenting: %#x", existingContainer) | 423 | + | log.Printf("[workspace] Hidden split target container before reparenting: %#x", splitTargetContainer) |
| 395 | } | 424 | } | ||
| 396 | 425 | ||||
| 397 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | 426 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | ||
| 398 | - | wm.safelyDetachControllersBeforeReparent(target) | 427 | + | wm.safelyDetachControllersBeforeReparent(splitTarget) |
| 399 | 428 | ||||
| 400 | // Detach existing container from its current GTK parent before inserting into new paned. | 429 | // Detach existing container from its current GTK parent before inserting into new paned. | ||
| 401 | if parent == nil { | 430 | if parent == nil { | ||
| 402 | - | // Target is the root - remove it from the window | 431 | + | // Split target is the root - remove it from the window |
| 403 | - | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | 432 | + | log.Printf("[workspace] removing split target container=%#x from window", splitTargetContainer) |
| 404 | if wm.window != nil { | 433 | if wm.window != nil { | ||
| 405 | wm.window.SetChild(0) | 434 | wm.window.SetChild(0) | ||
| 406 | } | 435 | } | ||
| 407 | // GTK4 will auto-unparent when we add to new paned - no manual unparent needed | 436 | // GTK4 will auto-unparent when we add to new paned - no manual unparent needed | ||
| 408 | } else if parent.container != 0 { | 437 | } else if parent.container != 0 { | ||
| 409 | - | // Target has a parent paned - unparent it from there | 438 | + | // Split target has a parent paned - unparent it from there |
| 410 | - | log.Printf("[workspace] unparenting existing container from parent paned") | 439 | + | log.Printf("[workspace] unparenting split target container from parent paned") |
| 411 | if webkit.WidgetIsValid(parent.container) { | 440 | if webkit.WidgetIsValid(parent.container) { | ||
| 412 | log.Printf("[workspace] executing unparent: parentPtr=%#x", parent.container) | 441 | log.Printf("[workspace] executing unparent: parentPtr=%#x", parent.container) | ||
| 413 | - | if parent.left == target { | 442 | + | if parent.left == splitTarget { |
| 414 | webkit.PanedSetStartChild(parent.container, 0) | 443 | webkit.PanedSetStartChild(parent.container, 0) | ||
| 415 | - | } else if parent.right == target { | 444 | + | } else if parent.right == splitTarget { |
| 416 | webkit.PanedSetEndChild(parent.container, 0) | 445 | webkit.PanedSetEndChild(parent.container, 0) | ||
| 417 | } | 446 | } | ||
| 418 | webkit.WidgetQueueAllocate(parent.container) | 447 | webkit.WidgetQueueAllocate(parent.container) | ||
| @@ -421,20 +450,20 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 421 | 450 | ||||
| 422 | // Add both containers to the new paned | 451 | // Add both containers to the new paned | ||
| 423 | if existingFirst { | 452 | if existingFirst { | ||
| 424 | - | split.left = target | 453 | + | split.left = splitTarget |
| 425 | split.right = newLeaf | 454 | split.right = newLeaf | ||
| 426 | - | webkit.PanedSetStartChild(paned, existingContainer) | 455 | + | webkit.PanedSetStartChild(paned, splitTargetContainer) |
| 427 | webkit.PanedSetEndChild(paned, newContainer) | 456 | webkit.PanedSetEndChild(paned, newContainer) | ||
| 428 | - | log.Printf("[workspace] added existing=%#x as start child, new=%#x as end child", existingContainer, newContainer) | 457 | + | log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer) |
| 429 | } else { | 458 | } else { | ||
| 430 | split.left = newLeaf | 459 | split.left = newLeaf | ||
| 431 | - | split.right = target | 460 | + | split.right = splitTarget |
| 432 | webkit.PanedSetStartChild(paned, newContainer) | 461 | webkit.PanedSetStartChild(paned, newContainer) | ||
| 433 | - | webkit.PanedSetEndChild(paned, existingContainer) | 462 | + | webkit.PanedSetEndChild(paned, splitTargetContainer) |
| 434 | - | log.Printf("[workspace] added new=%#x as start child, existing=%#x as end child", newContainer, existingContainer) | 463 | + | log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer) |
| 435 | } | 464 | } | ||
| 436 | 465 | ||||
| 437 | - | target.parent = split | 466 | + | splitTarget.parent = split |
| 438 | newLeaf.parent = split | 467 | newLeaf.parent = split | ||
| 439 | 468 | ||||
| 440 | if parent == nil { | 469 | if parent == nil { | ||
| @@ -447,10 +476,10 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 447 | } else { | 476 | } else { | ||
| 448 | if webkit.WidgetIsValid(parent.container) { | 477 | if webkit.WidgetIsValid(parent.container) { | ||
| 449 | log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parent.container, paned) | 478 | log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parent.container, paned) | ||
| 450 | - | if parent.left == target { | 479 | + | if parent.left == splitTarget { |
| 451 | parent.left = split | 480 | parent.left = split | ||
| 452 | webkit.PanedSetStartChild(parent.container, paned) | 481 | webkit.PanedSetStartChild(parent.container, paned) | ||
| 453 | - | } else if parent.right == target { | 482 | + | } else if parent.right == splitTarget { |
| 454 | parent.right = split | 483 | parent.right = split | ||
| 455 | webkit.PanedSetEndChild(parent.container, paned) | 484 | webkit.PanedSetEndChild(parent.container, paned) | ||
| 456 | } | 485 | } | ||
| @@ -459,7 +488,8 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 459 | log.Printf("[workspace] paned inserted into parent successfully") | 488 | log.Printf("[workspace] paned inserted into parent successfully") | ||
| 460 | 489 | ||||
| 461 | // CRITICAL: Restore parent paned's divider position to preserve existing layout | 490 | // CRITICAL: Restore parent paned's divider position to preserve existing layout | ||
| 462 | - | if parentDividerPos > 0 { | 491 | + | // Only attempt this if we captured a position and the parent is actually a paned widget |
| 492 | + | if parentDividerPos > 0 && webkit.IsPaned(parent.container) { | |||
| 463 | webkit.PanedSetPosition(parent.container, parentDividerPos) | 493 | webkit.PanedSetPosition(parent.container, parentDividerPos) | ||
| 464 | log.Printf("[workspace] restored parent divider position: %d", parentDividerPos) | 494 | log.Printf("[workspace] restored parent divider position: %d", parentDividerPos) | ||
| 465 | } | 495 | } | ||
| @@ -502,26 +532,26 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 502 | return false | 532 | return false | ||
| 503 | }, split) | 533 | }, split) | ||
| 504 | 534 | ||||
| 505 | - | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | 535 | + | // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting |
| 506 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | 536 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | ||
| 507 | - | if target != nil && target.container != 0 { | 537 | + | if splitTarget != nil && splitTarget.container != 0 { |
| 508 | wm.scheduleIdleGuarded(func() bool { | 538 | wm.scheduleIdleGuarded(func() bool { | ||
| 509 | - | if target == nil || !target.widgetValid || target.container == 0 { | 539 | + | if splitTarget == nil || !splitTarget.widgetValid || splitTarget.container == 0 { |
| 510 | return false | 540 | return false | ||
| 511 | } | 541 | } | ||
| 512 | // Show widget to reconnect WebKit rendering pipeline | 542 | // Show widget to reconnect WebKit rendering pipeline | ||
| 513 | - | webkit.WidgetShow(target.container) | 543 | + | webkit.WidgetShow(splitTarget.container) |
| 514 | // Force GTK to recalculate size and recreate rendering surface | 544 | // Force GTK to recalculate size and recreate rendering surface | ||
| 515 | - | webkit.WidgetQueueResize(target.container) | 545 | + | webkit.WidgetQueueResize(splitTarget.container) |
| 516 | - | webkit.WidgetQueueDraw(target.container) | 546 | + | webkit.WidgetQueueDraw(splitTarget.container) |
| 517 | - | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | 547 | + | log.Printf("[workspace] Shown and queued resize+draw for split target container after reparenting") |
| 518 | return false | 548 | return false | ||
| 519 | - | }, target) | 549 | + | }, splitTarget) |
| 520 | } | 550 | } | ||
| 521 | 551 | ||||
| 522 | wm.viewToNode[newPane.webView] = newLeaf | 552 | wm.viewToNode[newPane.webView] = newLeaf | ||
| 523 | wm.ensureHover(newLeaf) | 553 | wm.ensureHover(newLeaf) | ||
| 524 | - | wm.ensureHover(target) | 554 | + | wm.ensureHover(splitTarget) |
| 525 | wm.app.panes = append(wm.app.panes, newPane) | 555 | wm.app.panes = append(wm.app.panes, newPane) | ||
| 526 | if newPane.zoomController != nil { | 556 | if newPane.zoomController != nil { | ||
| 527 | newPane.zoomController.ApplyInitialZoom() | 557 | newPane.zoomController.ApplyInitialZoom() | ||
| @@ -529,9 +559,9 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 529 | 559 | ||||
| 530 | // Update CSS classes for all panes now that we have multiple panes | 560 | // Update CSS classes for all panes now that we have multiple panes | ||
| 531 | 561 | ||||
| 532 | - | reattachTargets := []*paneNode{target} | 562 | + | reattachTargets := []*paneNode{splitTarget} |
| 533 | - | if target != nil && target.isStacked { | 563 | + | if splitTarget != nil && splitTarget.isStacked { |
| 534 | - | reattachTargets = append(reattachTargets, target.stackedPanes...) | 564 | + | reattachTargets = append(reattachTargets, splitTarget.stackedPanes...) |
| 535 | } | 565 | } | ||
| 536 | guardNodes := append([]*paneNode{newLeaf}, reattachTargets...) | 566 | guardNodes := append([]*paneNode{newLeaf}, reattachTargets...) | ||
| 537 | wm.scheduleIdleGuarded(func() bool { | 567 | wm.scheduleIdleGuarded(func() bool { | ||
M pkg/webkit/keyboard_cgo.go +1 -1
| @@ -514,7 +514,7 @@ func goOnFaviconURIChanged(id C.ulong, pageURI *C.char, faviconURI *C.char) { | |||
| 514 | 514 | pageURLStr := C.GoString(pageURI) | |
| 515 | 515 | faviconURLStr := C.GoString(faviconURI) | |
| 516 | 516 | ||
| 517 | - | log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - page: %s, favicon: %s", uid, pageURLStr, faviconURLStr) | |
| 517 | + | log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - favicon: %s", uid, faviconURLStr) | |
| 518 | 518 | ||
| 519 | 519 | regMu.RLock() | |
| 520 | 520 | vw := viewByID[uid] | |
| @@ -514,7 +514,7 @@ func goOnFaviconURIChanged(id C.ulong, pageURI *C.char, faviconURI *C.char) { | |||||
| 514 | pageURLStr := C.GoString(pageURI) | 514 | pageURLStr := C.GoString(pageURI) | ||
| 515 | faviconURLStr := C.GoString(faviconURI) | 515 | faviconURLStr := C.GoString(faviconURI) | ||
| 516 | 516 | ||||
| 517 | - | log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - page: %s, favicon: %s", uid, pageURLStr, faviconURLStr) | 517 | + | log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - favicon: %s", uid, faviconURLStr) |
| 518 | 518 | ||||
| 519 | regMu.RLock() | 519 | regMu.RLock() | ||
| 520 | vw := viewByID[uid] | 520 | vw := viewByID[uid] | ||
M pkg/webkit/webview_callbacks.h +26 -4
| @@ -63,18 +63,40 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_ | |||
| 63 | 63 | } | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | + | static inline char* mask_uri_for_log(const char* uri) { | |
| 67 | + | if (!uri) return g_strdup("NULL"); | |
| 68 | + | const char* p = strstr(uri, "://"); | |
| 69 | + | const char* start = p ? p + 3 : uri; | |
| 70 | + | const char* slash = strchr(start, '/'); | |
| 71 | + | size_t host_len = slash ? (size_t)(slash - start) : strlen(start); | |
| 72 | + | const size_t max_len = 64; | |
| 73 | + | if (host_len <= max_len) { | |
| 74 | + | return g_strndup(start, host_len); | |
| 75 | + | } else { | |
| 76 | + | char* prefix = g_strndup(start, max_len); | |
| 77 | + | char* res = g_strdup_printf("%s...", prefix); | |
| 78 | + | g_free(prefix); | |
| 79 | + | return res; | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 66 | 83 | static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) { | |
| 67 | 84 | (void)database; | |
| 68 | - | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |
| 69 | - | page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data); | |
| 70 | 85 | ||
| 71 | 86 | if (!favicon_uri || !page_uri) { | |
| 72 | 87 | printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data); | |
| 73 | 88 | return; | |
| 74 | 89 | } | |
| 75 | 90 | ||
| 76 | - | // Pass the page URI and favicon URI to Go | |
| 77 | - | // WebKit manages the actual favicon data - we just need to store the URI mapping | |
| 91 | + | // Log only a masked/shortened form of the URIs (no full URIs) | |
| 92 | + | char* masked_page = mask_uri_for_log(page_uri); | |
| 93 | + | char* masked_favicon = mask_uri_for_log(favicon_uri); | |
| 94 | + | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |
| 95 | + | masked_page, masked_favicon, (unsigned long)user_data); | |
| 96 | + | g_free(masked_page); | |
| 97 | + | g_free(masked_favicon); | |
| 98 | + | ||
| 99 | + | // Pass the full page URI and favicon URI to Go | |
| 78 | 100 | goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri); | |
| 79 | 101 | } | |
| 80 | 102 | ||
| @@ -63,18 +63,40 @@ static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_ | |||||
| 63 | } | 63 | } | ||
| 64 | } | 64 | } | ||
| 65 | 65 | ||||
| 66 | + | static inline char* mask_uri_for_log(const char* uri) { | |||
| 67 | + | if (!uri) return g_strdup("NULL"); | |||
| 68 | + | const char* p = strstr(uri, "://"); | |||
| 69 | + | const char* start = p ? p + 3 : uri; | |||
| 70 | + | const char* slash = strchr(start, '/'); | |||
| 71 | + | size_t host_len = slash ? (size_t)(slash - start) : strlen(start); | |||
| 72 | + | const size_t max_len = 64; | |||
| 73 | + | if (host_len <= max_len) { | |||
| 74 | + | return g_strndup(start, host_len); | |||
| 75 | + | } else { | |||
| 76 | + | char* prefix = g_strndup(start, max_len); | |||
| 77 | + | char* res = g_strdup_printf("%s...", prefix); | |||
| 78 | + | g_free(prefix); | |||
| 79 | + | return res; | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | ||||
| 66 | static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) { | 83 | static inline void on_favicon_changed(WebKitFaviconDatabase* database, gchar* page_uri, gchar* favicon_uri, gpointer user_data) { | ||
| 67 | (void)database; | 84 | (void)database; | ||
| 68 | - | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |||
| 69 | - | page_uri ? page_uri : "NULL", favicon_uri ? favicon_uri : "NULL", (unsigned long)user_data); | |||
| 70 | 85 | ||||
| 71 | if (!favicon_uri || !page_uri) { | 86 | if (!favicon_uri || !page_uri) { | ||
| 72 | printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data); | 87 | printf("[favicon-c] Invalid favicon or page URI for WebView ID %lu\n", (unsigned long)user_data); | ||
| 73 | return; | 88 | return; | ||
| 74 | } | 89 | } | ||
| 75 | 90 | ||||
| 76 | - | // Pass the page URI and favicon URI to Go | 91 | + | // Log only a masked/shortened form of the URIs (no full URIs) |
| 77 | - | // WebKit manages the actual favicon data - we just need to store the URI mapping | 92 | + | char* masked_page = mask_uri_for_log(page_uri); |
| 93 | + | char* masked_favicon = mask_uri_for_log(favicon_uri); | |||
| 94 | + | printf("[favicon-c] Favicon changed - page: %s, favicon: %s, WebView ID: %lu\n", | |||
| 95 | + | masked_page, masked_favicon, (unsigned long)user_data); | |||
| 96 | + | g_free(masked_page); | |||
| 97 | + | g_free(masked_favicon); | |||
| 98 | + | ||||
| 99 | + | // Pass the full page URI and favicon URI to Go | |||
| 78 | goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri); | 100 | goOnFaviconURIChanged((unsigned long)user_data, page_uri, favicon_uri); | ||
| 79 | } | 101 | } | ||
| 80 | 102 | ||||
M pkg/webkit/webview_cgo.go +1 -3
| @@ -1020,7 +1020,7 @@ type WebView struct { | |||
| 1020 | 1020 | windowTypeCallback func(WindowType, *WindowFeatures) | |
| 1021 | 1021 | parentWebView *WebView | |
| 1022 | 1022 | isRelated bool | |
| 1023 | - | isActive bool // Track if this WebView is currently active/focused | |
| 1023 | + | isActive bool // Track if this WebView is currently active/focused | |
| 1024 | 1024 | navigationPolicyHandler func(url string, isUserGesture bool) bool // Handler to decide if navigation should be allowed | |
| 1025 | 1025 | } | |
| 1026 | 1026 | ||
| @@ -1229,7 +1229,6 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1229 | 1229 | C.enable_favicon_database(networkSession) | |
| 1230 | 1230 | ||
| 1231 | 1231 | // Notify favicon changes to Go for caching | |
| 1232 | - | log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id) | |
| 1233 | 1232 | C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id)) | |
| 1234 | 1233 | // Apply hardware acceleration and related settings based on cfg.Rendering | |
| 1235 | 1234 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| @@ -1910,7 +1909,6 @@ func (w *WebView) dispatchFaviconChanged(data []byte) { | |||
| 1910 | 1909 | } | |
| 1911 | 1910 | ||
| 1912 | 1911 | func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) { | |
| 1913 | - | log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI) | |
| 1914 | 1912 | if w != nil && w.faviconURIHandler != nil { | |
| 1915 | 1913 | w.faviconURIHandler(pageURI, faviconURI) | |
| 1916 | 1914 | } | |
| @@ -1020,7 +1020,7 @@ type WebView struct { | |||||
| 1020 | windowTypeCallback func(WindowType, *WindowFeatures) | 1020 | windowTypeCallback func(WindowType, *WindowFeatures) | ||
| 1021 | parentWebView *WebView | 1021 | parentWebView *WebView | ||
| 1022 | isRelated bool | 1022 | isRelated bool | ||
| 1023 | - | isActive bool // Track if this WebView is currently active/focused | 1023 | + | isActive bool // Track if this WebView is currently active/focused |
| 1024 | navigationPolicyHandler func(url string, isUserGesture bool) bool // Handler to decide if navigation should be allowed | 1024 | navigationPolicyHandler func(url string, isUserGesture bool) bool // Handler to decide if navigation should be allowed | ||
| 1025 | } | 1025 | } | ||
| 1026 | 1026 | ||||
| @@ -1229,7 +1229,6 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1229 | C.enable_favicon_database(networkSession) | 1229 | C.enable_favicon_database(networkSession) | ||
| 1230 | 1230 | ||||
| 1231 | // Notify favicon changes to Go for caching | 1231 | // Notify favicon changes to Go for caching | ||
| 1232 | - | log.Printf("[favicon] Connecting favicon signal for WebView ID %d", v.id) | |||
| 1233 | C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id)) | 1232 | C.connect_favicon_changed_with_id(viewWidget, C.ulong(v.id)) | ||
| 1234 | // Apply hardware acceleration and related settings based on cfg.Rendering | 1233 | // Apply hardware acceleration and related settings based on cfg.Rendering | ||
| 1235 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | 1234 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | ||
| @@ -1910,7 +1909,6 @@ func (w *WebView) dispatchFaviconChanged(data []byte) { | |||||
| 1910 | } | 1909 | } | ||
| 1911 | 1910 | ||||
| 1912 | func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) { | 1911 | func (w *WebView) dispatchFaviconURIChanged(pageURI, faviconURI string) { | ||
| 1913 | - | log.Printf("[favicon] Page %s has favicon at %s", pageURI, faviconURI) | |||
| 1914 | if w != nil && w.faviconURIHandler != nil { | 1912 | if w != nil && w.faviconURIHandler != nil { | ||
| 1915 | w.faviconURIHandler(pageURI, faviconURI) | 1913 | w.faviconURIHandler(pageURI, faviconURI) | ||
| 1916 | } | 1914 | } | ||