refactor(workspace): rewrite closePane with simplified architecture and helper functions
- commit
- 05fb5c299a023f900802e50aff38f663aa925633
- author
- Brice · · 2025-09-30 05:15 UTC
- parent
- 91bff85
- signature
- unsigned
1 file · +314 -380
M internal/app/browser/workspace_pane_ops.go +314 -380
| @@ -3,12 +3,48 @@ package browser | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "errors" | |
| 6 | - | "fmt" | |
| 7 | 6 | "log" | |
| 8 | 7 | ||
| 9 | 8 | "github.com/bnema/dumber/pkg/webkit" | |
| 10 | 9 | ) | |
| 11 | 10 | ||
| 11 | + | // closeContext manages the context for a pane close operation | |
| 12 | + | type closeContext struct { | |
| 13 | + | wm *WorkspaceManager | |
| 14 | + | target *paneNode | |
| 15 | + | remaining int | |
| 16 | + | err error | |
| 17 | + | generation uint | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // beginClose initializes a close context with validation | |
| 21 | + | func (wm *WorkspaceManager) beginClose(node *paneNode) closeContext { | |
| 22 | + | ctx := closeContext{wm: wm, target: node, generation: wm.nextCleanupGeneration()} | |
| 23 | + | switch { | |
| 24 | + | case wm == nil: | |
| 25 | + | ctx.err = errors.New("workspace manager nil") | |
| 26 | + | case node == nil || !node.isLeaf: | |
| 27 | + | ctx.err = errors.New("invalid close target") | |
| 28 | + | case node.pane == nil || node.pane.webView == nil: | |
| 29 | + | ctx.err = errors.New("close target missing webview") | |
| 30 | + | default: | |
| 31 | + | ctx.remaining = len(wm.app.panes) | |
| 32 | + | } | |
| 33 | + | return ctx | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // Generation returns the cleanup generation for this context | |
| 37 | + | func (ctx closeContext) Generation() uint { | |
| 38 | + | return ctx.generation | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // finish handles cleanup operations after close completion | |
| 42 | + | func (ctx closeContext) finish() { | |
| 43 | + | if ctx.err == nil { | |
| 44 | + | ctx.wm.updateMainPane() | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 12 | 48 | // collectLeaves returns all leaf nodes in the workspace tree | |
| 13 | 49 | func (wm *WorkspaceManager) collectLeaves() []*paneNode { | |
| 14 | 50 | return wm.collectLeavesFrom(wm.root) | |
| @@ -703,451 +739,349 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 703 | 739 | return newLeaf, nil | |
| 704 | 740 | } | |
| 705 | 741 | ||
| 706 | - | // closePane closes a specific pane and handles tree restructuring | |
| 707 | - | func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) { | |
| 708 | - | if wm == nil || node == nil || !node.isLeaf { | |
| 709 | - | return nil, errors.New("close target must be a leaf pane") | |
| 742 | + | // Helper functions for the simplified closePane implementation | |
| 743 | + | ||
| 744 | + | // getSibling returns the sibling node for a given node | |
| 745 | + | func (wm *WorkspaceManager) getSibling(node *paneNode) *paneNode { | |
| 746 | + | if node.parent == nil { | |
| 747 | + | return nil | |
| 710 | 748 | } | |
| 711 | - | if node.pane == nil || node.pane.webView == nil { | |
| 712 | - | return nil, errors.New("close target missing webview") | |
| 749 | + | if node.parent.left == node { | |
| 750 | + | return node.parent.right | |
| 713 | 751 | } | |
| 752 | + | return node.parent.left | |
| 753 | + | } | |
| 714 | 754 | ||
| 715 | - | var promoted *paneNode | |
| 755 | + | // nextCleanupGeneration returns the next cleanup generation counter | |
| 756 | + | func (wm *WorkspaceManager) nextCleanupGeneration() uint { | |
| 757 | + | wm.cleanupCounter++ | |
| 758 | + | return wm.cleanupCounter | |
| 759 | + | } | |
| 716 | 760 | ||
| 717 | - | // Handle closing panes in a stack | |
| 718 | - | if node.parent != nil && node.parent.isStacked { | |
| 719 | - | return nil, wm.stackedPaneManager.CloseStackedPane(node) | |
| 761 | + | // ensureWidgets validates and recovers SafeWidget instances | |
| 762 | + | func (wm *WorkspaceManager) ensureWidgets(nodes ...*paneNode) { | |
| 763 | + | for _, n := range nodes { | |
| 764 | + | if n == nil || n.container == nil { | |
| 765 | + | continue | |
| 766 | + | } | |
| 767 | + | if n.container.IsValid() { | |
| 768 | + | continue | |
| 769 | + | } | |
| 770 | + | ptr := n.container.Ptr() | |
| 771 | + | typeInfo := n.container.typeInfo | |
| 772 | + | n.container = wm.widgetRegistry.Register(ptr, typeInfo) | |
| 720 | 773 | } | |
| 774 | + | } | |
| 721 | 775 | ||
| 722 | - | if node.parent != nil && node.parent.container != nil { | |
| 723 | - | node.parent.container.Execute(func(panedPtr uintptr) error { | |
| 724 | - | if node.parent.left == node { | |
| 725 | - | webkit.PanedSetStartChild(panedPtr, 0) | |
| 726 | - | // GTK4 automatically unparents when setting child to 0 | |
| 727 | - | } else if node.parent.right == node { | |
| 728 | - | webkit.PanedSetEndChild(panedPtr, 0) | |
| 729 | - | // GTK4 automatically unparents when setting child to 0 | |
| 730 | - | } | |
| 731 | - | return nil | |
| 732 | - | }) | |
| 733 | - | } else if node.container != nil { | |
| 734 | - | // Only manually unparent if widget wasn't auto-unparented above | |
| 735 | - | node.container.Execute(func(containerPtr uintptr) error { | |
| 736 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 737 | - | webkit.WidgetUnparent(containerPtr) | |
| 738 | - | } | |
| 739 | - | return nil | |
| 740 | - | }) | |
| 776 | + | // promoteSibling promotes a sibling node to replace its parent in the tree | |
| 777 | + | func (wm *WorkspaceManager) promoteSibling(grand *paneNode, parent *paneNode, sibling *paneNode) { | |
| 778 | + | if grand == nil { | |
| 779 | + | wm.root = sibling | |
| 780 | + | sibling.parent = nil | |
| 781 | + | return | |
| 741 | 782 | } | |
| 783 | + | sibling.parent = grand | |
| 784 | + | if grand.left == parent { | |
| 785 | + | grand.left = sibling | |
| 786 | + | } else { | |
| 787 | + | grand.right = sibling | |
| 788 | + | } | |
| 789 | + | } | |
| 742 | 790 | ||
| 743 | - | remaining := len(wm.app.panes) | |
| 744 | - | willBeLastPane := remaining <= 1 | |
| 745 | - | paneCleaned := false | |
| 746 | - | ensureCleanup := func() { | |
| 747 | - | if paneCleaned { | |
| 748 | - | return | |
| 791 | + | // swapContainers updates GTK widget hierarchy for promoted siblings | |
| 792 | + | func (wm *WorkspaceManager) swapContainers(grand *paneNode, sibling *paneNode) { | |
| 793 | + | if grand == nil { | |
| 794 | + | // When TreeRebalancer is enabled, skip GTK attachment here as it will be | |
| 795 | + | // handled by the rebalancer's promotion transaction to avoid double-attachment | |
| 796 | + | if wm.treeRebalancer == nil || !wm.treeRebalancer.enabled { | |
| 797 | + | wm.attachRoot(sibling) | |
| 749 | 798 | } | |
| 750 | - | node.pane.Cleanup() | |
| 751 | - | paneCleaned = true | |
| 799 | + | return | |
| 752 | 800 | } | |
| 753 | - | ||
| 754 | - | if remaining == 2 { | |
| 755 | - | var sibling *paneNode | |
| 756 | - | if node.parent != nil { | |
| 757 | - | if node.parent.left == node { | |
| 758 | - | sibling = node.parent.right | |
| 759 | - | } else if node.parent.right == node { | |
| 760 | - | sibling = node.parent.left | |
| 801 | + | grand.container.Execute(func(gPtr uintptr) error { | |
| 802 | + | sibling.container.Execute(func(sPtr uintptr) error { | |
| 803 | + | // GTK4 auto-unparents when setting paned child | |
| 804 | + | // But we should verify the widget is ready for reparenting | |
| 805 | + | if parent := webkit.WidgetGetParent(sPtr); parent != 0 && parent != gPtr { | |
| 806 | + | log.Printf("[workspace] widget %#x has unexpected parent %#x, expected %#x or 0", sPtr, parent, gPtr) | |
| 761 | 807 | } | |
| 762 | - | } | |
| 763 | - | if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil { | |
| 764 | - | if sibling.isPopup && !node.isPopup { | |
| 765 | - | parentNode := node.parent | |
| 766 | - | var parentContainer *SafeWidget | |
| 767 | - | if parentNode != nil { | |
| 768 | - | parentContainer = parentNode.container | |
| 769 | - | } | |
| 770 | - | ||
| 771 | - | // Detach remaining GTK widgets before destroying the WebViews so GTK | |
| 772 | - | // does not attempt to dispose still-parented children. | |
| 773 | - | if parentContainer != nil { | |
| 774 | - | parentContainer.Execute(func(panedPtr uintptr) error { | |
| 775 | - | if parentNode.left == sibling { | |
| 776 | - | webkit.PanedSetStartChild(panedPtr, 0) | |
| 777 | - | } else if parentNode.right == sibling { | |
| 778 | - | webkit.PanedSetEndChild(panedPtr, 0) | |
| 779 | - | } | |
| 780 | - | return nil | |
| 781 | - | }) | |
| 782 | - | } | |
| 783 | - | if sibling.container != nil { | |
| 784 | - | sibling.container.Execute(func(containerPtr uintptr) error { | |
| 785 | - | webkit.WidgetUnparent(containerPtr) | |
| 786 | - | return nil | |
| 787 | - | }) | |
| 788 | - | } | |
| 789 | - | if parentContainer != nil && wm.window != nil { | |
| 790 | - | wm.window.SetChild(0) | |
| 791 | - | } | |
| 792 | - | ||
| 793 | - | log.Printf("[workspace] closing primary pane with related popup present; exiting") | |
| 794 | - | ensureCleanup() | |
| 795 | - | sibling.pane.Cleanup() | |
| 796 | 808 | ||
| 797 | - | wm.detachHover(node) | |
| 798 | - | wm.detachHover(sibling) | |
| 799 | - | ||
| 800 | - | // Clean up workspace tracking for both panes | |
| 801 | - | node.pane.CleanupFromWorkspace(wm) | |
| 802 | - | sibling.pane.CleanupFromWorkspace(wm) | |
| 809 | + | if grand.left == sibling { | |
| 810 | + | webkit.PanedSetStartChild(gPtr, sPtr) | |
| 811 | + | log.Printf("[workspace] swapContainers: set widget %#x as start child of paned %#x", sPtr, gPtr) | |
| 812 | + | } else { | |
| 813 | + | webkit.PanedSetEndChild(gPtr, sPtr) | |
| 814 | + | log.Printf("[workspace] swapContainers: set widget %#x as end child of paned %#x", sPtr, gPtr) | |
| 815 | + | } | |
| 816 | + | webkit.WidgetQueueAllocate(gPtr) | |
| 817 | + | return nil | |
| 818 | + | }) | |
| 819 | + | return nil | |
| 820 | + | }) | |
| 821 | + | } | |
| 803 | 822 | ||
| 804 | - | if node.pane.webView != nil { | |
| 805 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 806 | - | log.Printf("[workspace] failed to destroy primary webview: %v", err) | |
| 807 | - | } | |
| 808 | - | } | |
| 809 | - | if sibling.pane.webView != nil { | |
| 810 | - | if err := sibling.pane.webView.Destroy(); err != nil { | |
| 811 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 812 | - | } | |
| 813 | - | } | |
| 823 | + | // attachRoot attaches a node as the new window root | |
| 824 | + | func (wm *WorkspaceManager) attachRoot(root *paneNode) { | |
| 825 | + | if root == nil || root.container == nil || wm.window == nil { | |
| 826 | + | return | |
| 827 | + | } | |
| 828 | + | root.container.Execute(func(ptr uintptr) error { | |
| 829 | + | // Check if widget has a parent and unparent it first | |
| 830 | + | if parent := webkit.WidgetGetParent(ptr); parent != 0 { | |
| 831 | + | log.Printf("[workspace] unparenting widget %#x from parent %#x before window attach", ptr, parent) | |
| 832 | + | webkit.WidgetUnparent(ptr) | |
| 833 | + | } | |
| 814 | 834 | ||
| 815 | - | wm.root = nil | |
| 816 | - | wm.mainPane = nil | |
| 835 | + | // Now safe to set as window child | |
| 836 | + | wm.window.SetChild(ptr) | |
| 837 | + | webkit.WidgetQueueAllocate(ptr) | |
| 838 | + | webkit.WidgetShow(ptr) | |
| 817 | 839 | ||
| 818 | - | webkit.QuitMainLoop() | |
| 819 | - | return nil, nil | |
| 820 | - | } | |
| 840 | + | // Verify attachment succeeded | |
| 841 | + | if finalParent := webkit.WidgetGetParent(ptr); finalParent == 0 { | |
| 842 | + | log.Printf("[workspace] WARNING: widget %#x has no parent after SetChild", ptr) | |
| 843 | + | } else { | |
| 844 | + | log.Printf("[workspace] attachRoot successful: widget %#x now child of %#x", ptr, finalParent) | |
| 821 | 845 | } | |
| 846 | + | return nil | |
| 847 | + | }) | |
| 848 | + | } | |
| 849 | + | ||
| 850 | + | // cleanupPane safely cleans up a pane node with generation tracking | |
| 851 | + | func (wm *WorkspaceManager) cleanupPane(node *paneNode, generation uint) { | |
| 852 | + | if node == nil { | |
| 853 | + | return | |
| 822 | 854 | } | |
| 823 | - | if willBeLastPane && wm.root == node { | |
| 824 | - | log.Printf("[workspace] closing final pane; exiting browser") | |
| 825 | - | ensureCleanup() | |
| 826 | - | wm.detachHover(node) | |
| 827 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 828 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 829 | - | } | |
| 830 | - | webkit.QuitMainLoop() | |
| 831 | - | return nil, nil | |
| 855 | + | if !node.widgetValid { | |
| 856 | + | return | |
| 832 | 857 | } | |
| 833 | 858 | ||
| 834 | - | parent := node.parent | |
| 835 | - | if parent == nil { | |
| 836 | - | // This is the root pane (no parent in tree structure) | |
| 837 | - | if node != wm.root { | |
| 838 | - | return nil, errors.New("inconsistent state: node has no parent but is not root") | |
| 839 | - | } | |
| 859 | + | node.widgetValid = false | |
| 860 | + | node.cleanupGeneration = generation | |
| 840 | 861 | ||
| 841 | - | // Check if we can find a replacement root | |
| 842 | - | replacement := wm.findReplacementRoot(node) | |
| 843 | - | if replacement == nil { | |
| 844 | - | // No other panes exist, this is the final pane | |
| 845 | - | log.Printf("[workspace] closing final pane; exiting browser") | |
| 846 | - | ensureCleanup() | |
| 847 | - | wm.detachHover(node) | |
| 848 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 849 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 850 | - | } | |
| 851 | - | webkit.QuitMainLoop() | |
| 852 | - | return nil, nil | |
| 862 | + | if node.pane != nil { | |
| 863 | + | node.pane.CleanupFromWorkspace(wm) | |
| 864 | + | if node.pane.webView != nil { | |
| 865 | + | node.pane.webView.Destroy() | |
| 853 | 866 | } | |
| 867 | + | } | |
| 854 | 868 | ||
| 855 | - | // We have a replacement - delegate root status and close this pane | |
| 856 | - | log.Printf("[workspace] delegating root status from node=%#x to replacement=%#x", node.container, replacement.container) | |
| 869 | + | if node.container != nil { | |
| 870 | + | node.container.Invalidate() | |
| 871 | + | node.container = nil | |
| 872 | + | } | |
| 857 | 873 | ||
| 858 | - | // Clean up workspace tracking | |
| 859 | - | node.pane.CleanupFromWorkspace(wm) | |
| 874 | + | node.parent = nil | |
| 875 | + | node.left = nil | |
| 876 | + | node.right = nil | |
| 877 | + | } | |
| 860 | 878 | ||
| 861 | - | if wm.mainPane == node { | |
| 862 | - | wm.mainPane = nil | |
| 863 | - | } | |
| 879 | + | // decommissionParent cleans up a parent node after promotion | |
| 880 | + | func (wm *WorkspaceManager) decommissionParent(parent *paneNode, generation uint) { | |
| 881 | + | if parent == nil { | |
| 882 | + | return | |
| 883 | + | } | |
| 864 | 884 | ||
| 865 | - | // Clear current active if it's the node being closed | |
| 866 | - | if wm.GetActiveNode() == node { | |
| 867 | - | } | |
| 885 | + | // For branch nodes (paneds), ensure the GTK widget is destroyed properly | |
| 886 | + | if !parent.isLeaf && parent.container != nil { | |
| 887 | + | parent.container.Execute(func(ptr uintptr) error { | |
| 888 | + | // Clear any remaining children first to ensure clean destruction | |
| 889 | + | // Note: Children may already be reparented during promotion, so verify parent relationship | |
| 890 | + | if webkit.IsPaned(ptr) { | |
| 891 | + | startChild := webkit.PanedGetStartChild(ptr) | |
| 892 | + | endChild := webkit.PanedGetEndChild(ptr) | |
| 868 | 893 | ||
| 869 | - | // Set replacement as new root | |
| 870 | - | wm.root = replacement | |
| 871 | - | replacement.parent = nil // Critical: clear parent pointer for new root | |
| 872 | - | promoted = replacement | |
| 873 | - | ||
| 874 | - | // Replace window child - must unparent first to satisfy GTK4 assertion | |
| 875 | - | if wm.window != nil && replacement.container != nil { | |
| 876 | - | replacement.container.Execute(func(containerPtr uintptr) error { | |
| 877 | - | // Unparent from current parent before setting as window child | |
| 878 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | |
| 879 | - | webkit.WidgetUnparent(containerPtr) | |
| 894 | + | // Only clear children that are still valid AND still parented to this paned | |
| 895 | + | // If the child was reparented during swapContainers, its parent will be different | |
| 896 | + | if startChild != 0 && webkit.WidgetIsValid(startChild) && webkit.WidgetGetParent(startChild) == ptr { | |
| 897 | + | webkit.PanedSetStartChild(ptr, 0) | |
| 880 | 898 | } | |
| 881 | - | wm.window.SetChild(containerPtr) | |
| 882 | - | webkit.WidgetQueueAllocate(containerPtr) | |
| 883 | - | webkit.WidgetShow(containerPtr) | |
| 884 | - | return nil | |
| 885 | - | }) | |
| 886 | - | } | |
| 887 | - | ||
| 888 | - | // Focus a suitable pane | |
| 889 | - | focusTarget := wm.leftmostLeaf(replacement) | |
| 890 | - | if focusTarget != nil { | |
| 891 | - | wm.SetActivePane(focusTarget, SourceClose) | |
| 892 | - | } | |
| 893 | - | ||
| 894 | - | // Destroy the webview and detach hover AFTER rearranging hierarchy | |
| 895 | - | // Only destroy the webview if this is the final pane, otherwise just clean up | |
| 896 | - | ensureCleanup() | |
| 897 | - | wm.detachHover(node) | |
| 898 | - | switch { | |
| 899 | - | case node.isPopup: | |
| 900 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 901 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 902 | - | } | |
| 903 | - | case willBeLastPane: | |
| 904 | - | // This is the last pane, safe to destroy completely | |
| 905 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 906 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 899 | + | if endChild != 0 && webkit.WidgetIsValid(endChild) && webkit.WidgetGetParent(endChild) == ptr { | |
| 900 | + | webkit.PanedSetEndChild(ptr, 0) | |
| 901 | + | } | |
| 902 | + | log.Printf("[workspace] cleared paned children before destruction: %#x", ptr) | |
| 907 | 903 | } | |
| 908 | - | default: | |
| 909 | - | // Multiple panes remain, don't destroy the window - just clean up the webview | |
| 910 | - | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | |
| 911 | - | // TODO: Add a method to destroy just the webview without the window | |
| 912 | - | } | |
| 913 | - | ||
| 914 | - | wm.updateMainPane() | |
| 915 | - | // Update CSS classes after pane count changes | |
| 916 | - | log.Printf("[workspace] root pane closed and delegated; panes remaining=%d", len(wm.app.panes)) | |
| 917 | - | return promoted, nil | |
| 904 | + | // Hook widget destruction | |
| 905 | + | webkit.WidgetHookDestroy(ptr) | |
| 906 | + | log.Printf("[workspace] destroyed paned widget: %#x", ptr) | |
| 907 | + | return nil | |
| 908 | + | }) | |
| 918 | 909 | } | |
| 919 | 910 | ||
| 920 | - | var sibling *paneNode | |
| 921 | - | if parent.left == node { | |
| 922 | - | sibling = parent.right | |
| 923 | - | } else { | |
| 924 | - | sibling = parent.left | |
| 911 | + | wm.cleanupPane(parent, generation) | |
| 912 | + | } | |
| 913 | + | ||
| 914 | + | // promoteNewRoot handles root replacement when closing the root pane | |
| 915 | + | func (wm *WorkspaceManager) promoteNewRoot(ctx closeContext, oldRoot *paneNode) (*paneNode, error) { | |
| 916 | + | candidate := wm.findReplacementRoot(oldRoot) | |
| 917 | + | if candidate == nil { | |
| 918 | + | return wm.cleanupAndExit(oldRoot) | |
| 925 | 919 | } | |
| 926 | - | if sibling == nil { | |
| 927 | - | return nil, errors.New("pane close failed: missing sibling") | |
| 920 | + | ||
| 921 | + | sibling := wm.getSibling(candidate) | |
| 922 | + | if sibling != nil { | |
| 923 | + | wm.promoteSibling(candidate.parent.parent, candidate.parent, sibling) | |
| 928 | 924 | } | |
| 929 | 925 | ||
| 930 | - | grand := parent.parent | |
| 926 | + | candidate.parent = nil | |
| 927 | + | wm.root = candidate | |
| 931 | 928 | ||
| 932 | - | ensureSafeWidget := func(owner string, widget **SafeWidget) error { | |
| 933 | - | if widget == nil { | |
| 934 | - | return nil | |
| 935 | - | } | |
| 936 | - | current := *widget | |
| 937 | - | if current == nil { | |
| 938 | - | return fmt.Errorf("%s widget missing", owner) | |
| 939 | - | } | |
| 940 | - | if current.IsValid() { | |
| 941 | - | return nil | |
| 942 | - | } | |
| 929 | + | wm.attachRoot(candidate) | |
| 943 | 930 | ||
| 944 | - | ptr := current.Ptr() | |
| 945 | - | typeInfo := current.typeInfo | |
| 946 | - | current.Invalidate() | |
| 947 | - | if wm.widgetRegistry != nil && ptr != 0 { | |
| 948 | - | wm.widgetRegistry.Unregister(ptr) | |
| 949 | - | } | |
| 931 | + | wm.cleanupPane(oldRoot, ctx.Generation()) | |
| 932 | + | return candidate, nil | |
| 933 | + | } | |
| 950 | 934 | ||
| 951 | - | if ptr == 0 || !webkit.WidgetIsValid(ptr) { | |
| 952 | - | *widget = nil | |
| 953 | - | return fmt.Errorf("%s widget %#x invalidated", owner, ptr) | |
| 954 | - | } | |
| 935 | + | // cleanupAndExit handles the final pane cleanup and application exit | |
| 936 | + | func (wm *WorkspaceManager) cleanupAndExit(node *paneNode) (*paneNode, error) { | |
| 937 | + | log.Printf("[workspace] closing final pane; exiting browser") | |
| 938 | + | wm.cleanupPane(node, wm.nextCleanupGeneration()) | |
| 939 | + | wm.detachHover(node) | |
| 940 | + | if err := node.pane.webView.Destroy(); err != nil { | |
| 941 | + | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 942 | + | } | |
| 943 | + | webkit.QuitMainLoop() | |
| 944 | + | return nil, nil | |
| 945 | + | } | |
| 955 | 946 | ||
| 956 | - | if wm.widgetRegistry == nil { | |
| 957 | - | return fmt.Errorf("%s widget registry unavailable", owner) | |
| 958 | - | } | |
| 947 | + | // setFocusToLeaf sets focus to the leftmost leaf of a node | |
| 948 | + | func (wm *WorkspaceManager) setFocusToLeaf(node *paneNode) { | |
| 949 | + | focusTarget := wm.leftmostLeaf(node) | |
| 950 | + | if focusTarget != nil { | |
| 951 | + | wm.SetActivePane(focusTarget, SourceClose) | |
| 952 | + | } | |
| 953 | + | } | |
| 959 | 954 | ||
| 960 | - | reRegistered := wm.widgetRegistry.Register(ptr, typeInfo) | |
| 961 | - | if reRegistered == nil { | |
| 962 | - | return fmt.Errorf("%s widget %#x re-registration failed", owner, ptr) | |
| 963 | - | } | |
| 964 | - | *widget = reRegistered | |
| 965 | - | log.Printf("[workspace] re-registered %s widget: ptr=%#x type=%s", owner, ptr, typeInfo) | |
| 966 | - | return nil | |
| 955 | + | // closePane closes a specific pane and handles tree restructuring (simplified implementation) | |
| 956 | + | func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) { | |
| 957 | + | ctx := wm.beginClose(node) | |
| 958 | + | defer ctx.finish() | |
| 959 | + | ||
| 960 | + | // STEP 1: Basic validation (quick fail) | |
| 961 | + | if ctx.err != nil { | |
| 962 | + | return nil, ctx.err | |
| 967 | 963 | } | |
| 968 | 964 | ||
| 969 | - | if err := ensureSafeWidget("sibling", &sibling.container); err != nil { | |
| 970 | - | log.Printf("[workspace] close aborted: %v", err) | |
| 971 | - | return nil, err | |
| 965 | + | // STEP 2: Handle stacked panes via compatibility shim | |
| 966 | + | if node.parent != nil && node.parent.isStacked { | |
| 967 | + | return wm.closeStackedPaneCompat(node) | |
| 972 | 968 | } | |
| 973 | - | if parent.container != nil { | |
| 974 | - | if err := ensureSafeWidget("parent", &parent.container); err != nil { | |
| 975 | - | log.Printf("[workspace] close aborted: %v", err) | |
| 976 | - | return nil, err | |
| 977 | - | } | |
| 969 | + | ||
| 970 | + | // STEP 3: Handle trivial exit cases | |
| 971 | + | if ctx.remaining == 1 { | |
| 972 | + | return wm.cleanupAndExit(node) | |
| 978 | 973 | } | |
| 979 | - | if grand != nil && grand.container != nil { | |
| 980 | - | if err := ensureSafeWidget("grandparent", &grand.container); err != nil { | |
| 981 | - | log.Printf("[workspace] close aborted: %v", err) | |
| 982 | - | return nil, err | |
| 983 | - | } | |
| 974 | + | if node == wm.root { | |
| 975 | + | return wm.promoteNewRoot(ctx, node) | |
| 984 | 976 | } | |
| 985 | 977 | ||
| 986 | - | if err := wm.validateWidgetsForReparenting(sibling, parent, grand); err != nil { | |
| 987 | - | log.Printf("[workspace] invalid widget state prior to close: %v", err) | |
| 988 | - | return nil, err | |
| 989 | - | } | |
| 978 | + | // STEP 4: Promote sibling in-place | |
| 979 | + | parent := node.parent | |
| 980 | + | sibling := wm.getSibling(node) | |
| 981 | + | grandparent := parent.parent | |
| 982 | + | wm.ensureWidgets(grandparent, parent, sibling) | |
| 990 | 983 | ||
| 991 | - | log.Printf("[workspace] closing pane: target=%#x parent=%#x sibling=%#x remaining=%d", node.container, parent.container, sibling.container, remaining) | |
| 984 | + | wm.promoteSibling(grandparent, parent, sibling) | |
| 992 | 985 | ||
| 993 | - | // Clean up workspace tracking | |
| 994 | - | node.pane.CleanupFromWorkspace(wm) | |
| 986 | + | // STEP 5: GTK updates leverage auto-unparenting | |
| 987 | + | wm.swapContainers(grandparent, sibling) | |
| 995 | 988 | ||
| 996 | - | if wm.mainPane == node { | |
| 997 | - | wm.mainPane = nil | |
| 989 | + | // STEP 6: Cleanup & focus | |
| 990 | + | wm.cleanupPane(node, ctx.Generation()) | |
| 991 | + | wm.decommissionParent(parent, ctx.Generation()) | |
| 992 | + | wm.setFocusToLeaf(sibling) | |
| 993 | + | ||
| 994 | + | return sibling, nil | |
| 995 | + | } | |
| 996 | + | ||
| 997 | + | // closeStackedPaneCompat provides compatibility layer for closing stacked panes | |
| 998 | + | func (wm *WorkspaceManager) closeStackedPaneCompat(node *paneNode) (*paneNode, error) { | |
| 999 | + | stack := node.parent | |
| 1000 | + | ||
| 1001 | + | // Find index | |
| 1002 | + | index := -1 | |
| 1003 | + | for i, pane := range stack.stackedPanes { | |
| 1004 | + | if pane == node { | |
| 1005 | + | index = i | |
| 1006 | + | break | |
| 1007 | + | } | |
| 998 | 1008 | } | |
| 999 | 1009 | ||
| 1000 | - | // Clear current active if it's the node being closed | |
| 1001 | - | if wm.GetActiveNode() == node { | |
| 1010 | + | if index == -1 { | |
| 1011 | + | return nil, errors.New("pane not in stack") | |
| 1002 | 1012 | } | |
| 1003 | 1013 | ||
| 1004 | - | promoted = sibling | |
| 1005 | - | if grand == nil { | |
| 1006 | - | // Parent is the root node. Promote sibling to become the new root. | |
| 1007 | - | log.Printf("[workspace] promoting sibling to root: container=%#x, isLeaf=%v", sibling.container, sibling.isLeaf) | |
| 1014 | + | // Remove from array | |
| 1015 | + | stack.stackedPanes = append( | |
| 1016 | + | stack.stackedPanes[:index], | |
| 1017 | + | stack.stackedPanes[index+1:]..., | |
| 1018 | + | ) | |
| 1008 | 1019 | ||
| 1009 | - | // Update tree structure first | |
| 1010 | - | wm.root = sibling | |
| 1011 | - | sibling.parent = nil // Critical: clear parent pointer for new root | |
| 1012 | - | ||
| 1013 | - | // For root promotion, handle the widget transition carefully | |
| 1014 | - | if wm.window != nil && sibling.container != nil { | |
| 1015 | - | sibling.container.Execute(func(containerPtr uintptr) error { | |
| 1016 | - | parentBefore := webkit.WidgetGetParent(containerPtr) | |
| 1017 | - | log.Printf("[workspace] root promotion: sibling parent before=%#x", parentBefore) | |
| 1018 | - | ||
| 1019 | - | // Only unparent if it has a parent (the paned) | |
| 1020 | - | if parentBefore != 0 { | |
| 1021 | - | // Get info about the paned before we destroy it | |
| 1022 | - | oldParentStart := webkit.PanedGetStartChild(parentBefore) | |
| 1023 | - | oldParentEnd := webkit.PanedGetEndChild(parentBefore) | |
| 1024 | - | log.Printf("[workspace] root promotion: old parent start=%#x end=%#x", oldParentStart, oldParentEnd) | |
| 1025 | - | ||
| 1026 | - | // Unparent the sibling from the paned | |
| 1027 | - | webkit.WidgetUnparent(containerPtr) | |
| 1028 | - | ||
| 1029 | - | // Ensure the widget is properly configured for window child | |
| 1030 | - | webkit.WidgetSetHExpand(containerPtr, true) | |
| 1031 | - | webkit.WidgetSetVExpand(containerPtr, true) | |
| 1032 | - | } | |
| 1020 | + | // If only one pane left, unstack it | |
| 1021 | + | if len(stack.stackedPanes) == 1 { | |
| 1022 | + | remaining := stack.stackedPanes[0] | |
| 1033 | 1023 | ||
| 1034 | - | // Set as window child | |
| 1035 | - | wm.window.SetChild(containerPtr) | |
| 1036 | - | ||
| 1037 | - | // Queue draw and allocation to ensure proper rendering | |
| 1038 | - | webkit.WidgetQueueAllocate(containerPtr) | |
| 1039 | - | webkit.WidgetShow(containerPtr) | |
| 1040 | - | ||
| 1041 | - | // Verify attachment succeeded | |
| 1042 | - | parentAfter := webkit.WidgetGetParent(containerPtr) | |
| 1043 | - | if parentAfter == 0 { | |
| 1044 | - | log.Printf("[workspace] WARNING: root promotion attachment failed, widget has no parent") | |
| 1045 | - | // Force another attempt at setting the child | |
| 1046 | - | wm.window.SetChild(containerPtr) | |
| 1047 | - | webkit.WidgetQueueAllocate(containerPtr) | |
| 1048 | - | parentAfter = webkit.WidgetGetParent(containerPtr) | |
| 1049 | - | } | |
| 1024 | + | // Replace stack with remaining pane in tree | |
| 1025 | + | remaining.parent = stack.parent | |
| 1026 | + | if stack.parent != nil { | |
| 1027 | + | if stack.parent.left == stack { | |
| 1028 | + | stack.parent.left = remaining | |
| 1029 | + | } else { | |
| 1030 | + | stack.parent.right = remaining | |
| 1031 | + | } | |
| 1050 | 1032 | ||
| 1051 | - | log.Printf("[workspace] root promotion: sibling parent after=%#x", parentAfter) | |
| 1052 | - | if parentAfter != 0 { | |
| 1053 | - | log.Printf("[workspace] successfully promoted sibling to root: %#x", containerPtr) | |
| 1054 | - | } else { | |
| 1055 | - | log.Printf("[workspace] ERROR: failed to attach promoted sibling as window child: %#x", containerPtr) | |
| 1056 | - | } | |
| 1033 | + | // Update GTK widgets | |
| 1034 | + | stack.parent.container.Execute(func(pPtr uintptr) error { | |
| 1035 | + | remaining.container.Execute(func(rPtr uintptr) error { | |
| 1036 | + | if stack.parent.left == remaining { | |
| 1037 | + | webkit.PanedSetStartChild(pPtr, rPtr) | |
| 1038 | + | } else { | |
| 1039 | + | webkit.PanedSetEndChild(pPtr, rPtr) | |
| 1040 | + | } | |
| 1041 | + | return nil | |
| 1042 | + | }) | |
| 1057 | 1043 | return nil | |
| 1058 | 1044 | }) | |
| 1059 | - | } | |
| 1060 | - | } else { | |
| 1061 | - | // Parent has a grandparent, so promote sibling to take parent's place | |
| 1062 | - | log.Printf("[workspace] promoting sibling to parent's position: sibling=%#x grand=%#x", sibling.container, grand.container) | |
| 1063 | - | ||
| 1064 | - | // Update tree structure | |
| 1065 | - | sibling.parent = grand | |
| 1066 | - | if grand.left == parent { | |
| 1067 | - | grand.left = sibling | |
| 1068 | 1045 | } else { | |
| 1069 | - | grand.right = sibling | |
| 1070 | - | } | |
| 1071 | - | ||
| 1072 | - | // Move sibling to grandparent's paned widget | |
| 1073 | - | // GTK4 handles unparenting automatically when we set the new paned child | |
| 1074 | - | grand.container.Execute(func(grandPtr uintptr) error { | |
| 1075 | - | sibling.container.Execute(func(siblingPtr uintptr) error { | |
| 1076 | - | parentBefore := webkit.WidgetGetParent(siblingPtr) | |
| 1077 | - | log.Printf("[workspace] promotion: sibling current parent=%#x grand=%#x", parentBefore, grandPtr) | |
| 1078 | - | ||
| 1079 | - | startBefore := webkit.PanedGetStartChild(grandPtr) | |
| 1080 | - | endBefore := webkit.PanedGetEndChild(grandPtr) | |
| 1081 | - | log.Printf("[workspace] promotion: grand start before=%#x end before=%#x", startBefore, endBefore) | |
| 1082 | - | ||
| 1083 | - | if grand.left == sibling { | |
| 1084 | - | webkit.PanedSetStartChild(grandPtr, siblingPtr) | |
| 1085 | - | } else { | |
| 1086 | - | webkit.PanedSetEndChild(grandPtr, siblingPtr) | |
| 1087 | - | } | |
| 1088 | - | ||
| 1089 | - | webkit.WidgetQueueAllocate(grandPtr) | |
| 1090 | - | webkit.WidgetQueueAllocate(siblingPtr) | |
| 1091 | - | webkit.WidgetShow(siblingPtr) | |
| 1092 | - | ||
| 1093 | - | parentAfter := webkit.WidgetGetParent(siblingPtr) | |
| 1094 | - | startAfter := webkit.PanedGetStartChild(grandPtr) | |
| 1095 | - | endAfter := webkit.PanedGetEndChild(grandPtr) | |
| 1096 | - | log.Printf("[workspace] promotion: sibling parent after=%#x", parentAfter) | |
| 1097 | - | log.Printf("[workspace] promotion: grand start after=%#x end after=%#x", startAfter, endAfter) | |
| 1098 | - | if parentAfter == 0 { | |
| 1099 | - | log.Printf("[workspace] WARNING: sibling widget %#x has no parent after promotion", siblingPtr) | |
| 1100 | - | } | |
| 1101 | - | log.Printf("[workspace] successfully promoted sibling to parent position: %#x", siblingPtr) | |
| 1046 | + | // Stack was root | |
| 1047 | + | wm.root = remaining | |
| 1048 | + | remaining.container.Execute(func(ptr uintptr) error { | |
| 1049 | + | wm.window.SetChild(ptr) | |
| 1102 | 1050 | return nil | |
| 1103 | 1051 | }) | |
| 1052 | + | } | |
| 1104 | 1053 | ||
| 1105 | - | // Also queue allocate the grandparent to ensure layout updates | |
| 1106 | - | return nil | |
| 1107 | - | }) | |
| 1108 | - | } | |
| 1054 | + | // Cleanup stack container | |
| 1055 | + | wm.cleanupPane(stack, wm.nextCleanupGeneration()) | |
| 1109 | 1056 | ||
| 1110 | - | // Parent container is now detached; ensure it is cleaned up to avoid lingering invalid widgets | |
| 1111 | - | if parent.container != nil { | |
| 1112 | - | ptr := parent.container.Ptr() | |
| 1113 | - | parent.container.Invalidate() | |
| 1114 | - | if wm.widgetRegistry != nil && ptr != 0 { | |
| 1115 | - | wm.widgetRegistry.Unregister(ptr) | |
| 1116 | - | } | |
| 1117 | - | parent.container = nil | |
| 1057 | + | return remaining, nil | |
| 1118 | 1058 | } | |
| 1119 | - | parent.left = nil | |
| 1120 | - | parent.right = nil | |
| 1121 | - | parent.parent = nil | |
| 1122 | 1059 | ||
| 1123 | - | // Find a suitable focus target | |
| 1124 | - | focusTarget := wm.leftmostLeaf(sibling) | |
| 1125 | - | if focusTarget != nil { | |
| 1126 | - | wm.SetActivePane(focusTarget, SourceClose) | |
| 1060 | + | // Update active index if needed | |
| 1061 | + | if stack.activeStackIndex >= len(stack.stackedPanes) { | |
| 1062 | + | stack.activeStackIndex = len(stack.stackedPanes) - 1 | |
| 1127 | 1063 | } | |
| 1128 | 1064 | ||
| 1129 | - | // Clean up the node being closed | |
| 1130 | - | ensureCleanup() | |
| 1131 | - | wm.detachHover(node) | |
| 1065 | + | // Remove widget from GTK box | |
| 1066 | + | stack.container.Execute(func(boxPtr uintptr) error { | |
| 1067 | + | node.container.Execute(func(nodePtr uintptr) error { | |
| 1068 | + | webkit.BoxRemove(boxPtr, nodePtr) | |
| 1069 | + | return nil | |
| 1070 | + | }) | |
| 1071 | + | return nil | |
| 1072 | + | }) | |
| 1132 | 1073 | ||
| 1133 | - | switch { | |
| 1134 | - | case node.isPopup: | |
| 1135 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 1136 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 1137 | - | } | |
| 1138 | - | case willBeLastPane: | |
| 1139 | - | // This is the last pane, safe to destroy completely | |
| 1140 | - | if err := node.pane.webView.Destroy(); err != nil { | |
| 1141 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 1142 | - | } | |
| 1143 | - | default: | |
| 1144 | - | // Multiple panes remain, don't destroy the window - just clean up the webview | |
| 1145 | - | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | |
| 1146 | - | // TODO: Add a method to destroy just the webview without the window | |
| 1074 | + | // Show new active pane | |
| 1075 | + | if stack.activeStackIndex >= 0 { | |
| 1076 | + | active := stack.stackedPanes[stack.activeStackIndex] | |
| 1077 | + | active.container.Execute(func(ptr uintptr) error { | |
| 1078 | + | webkit.WidgetShow(ptr) | |
| 1079 | + | return nil | |
| 1080 | + | }) | |
| 1147 | 1081 | } | |
| 1148 | 1082 | ||
| 1149 | - | wm.updateMainPane() | |
| 1150 | - | // Update CSS classes after pane count changes | |
| 1151 | - | log.Printf("[workspace] pane closed; panes remaining=%d", len(wm.app.panes)) | |
| 1152 | - | return promoted, nil | |
| 1083 | + | // Cleanup closed pane | |
| 1084 | + | wm.cleanupPane(node, wm.nextCleanupGeneration()) | |
| 1085 | + | ||
| 1086 | + | return stack, nil | |
| 1153 | 1087 | } | |
| @@ -3,12 +3,48 @@ package browser | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | - | "fmt" | |||
| 7 | "log" | 6 | "log" | ||
| 8 | 7 | ||||
| 9 | "github.com/bnema/dumber/pkg/webkit" | 8 | "github.com/bnema/dumber/pkg/webkit" | ||
| 10 | ) | 9 | ) | ||
| 11 | 10 | ||||
| 11 | + | // closeContext manages the context for a pane close operation | |||
| 12 | + | type closeContext struct { | |||
| 13 | + | wm *WorkspaceManager | |||
| 14 | + | target *paneNode | |||
| 15 | + | remaining int | |||
| 16 | + | err error | |||
| 17 | + | generation uint | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // beginClose initializes a close context with validation | |||
| 21 | + | func (wm *WorkspaceManager) beginClose(node *paneNode) closeContext { | |||
| 22 | + | ctx := closeContext{wm: wm, target: node, generation: wm.nextCleanupGeneration()} | |||
| 23 | + | switch { | |||
| 24 | + | case wm == nil: | |||
| 25 | + | ctx.err = errors.New("workspace manager nil") | |||
| 26 | + | case node == nil || !node.isLeaf: | |||
| 27 | + | ctx.err = errors.New("invalid close target") | |||
| 28 | + | case node.pane == nil || node.pane.webView == nil: | |||
| 29 | + | ctx.err = errors.New("close target missing webview") | |||
| 30 | + | default: | |||
| 31 | + | ctx.remaining = len(wm.app.panes) | |||
| 32 | + | } | |||
| 33 | + | return ctx | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // Generation returns the cleanup generation for this context | |||
| 37 | + | func (ctx closeContext) Generation() uint { | |||
| 38 | + | return ctx.generation | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // finish handles cleanup operations after close completion | |||
| 42 | + | func (ctx closeContext) finish() { | |||
| 43 | + | if ctx.err == nil { | |||
| 44 | + | ctx.wm.updateMainPane() | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 12 | // collectLeaves returns all leaf nodes in the workspace tree | 48 | // collectLeaves returns all leaf nodes in the workspace tree | ||
| 13 | func (wm *WorkspaceManager) collectLeaves() []*paneNode { | 49 | func (wm *WorkspaceManager) collectLeaves() []*paneNode { | ||
| 14 | return wm.collectLeavesFrom(wm.root) | 50 | return wm.collectLeavesFrom(wm.root) | ||
| @@ -703,451 +739,349 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||||
| 703 | return newLeaf, nil | 739 | return newLeaf, nil | ||
| 704 | } | 740 | } | ||
| 705 | 741 | ||||
| 706 | - | // closePane closes a specific pane and handles tree restructuring | 742 | + | // Helper functions for the simplified closePane implementation |
| 707 | - | func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) { | 743 | + | |
| 708 | - | if wm == nil || node == nil || !node.isLeaf { | 744 | + | // getSibling returns the sibling node for a given node |
| 709 | - | return nil, errors.New("close target must be a leaf pane") | 745 | + | func (wm *WorkspaceManager) getSibling(node *paneNode) *paneNode { |
| 746 | + | if node.parent == nil { | |||
| 747 | + | return nil | |||
| 710 | } | 748 | } | ||
| 711 | - | if node.pane == nil || node.pane.webView == nil { | 749 | + | if node.parent.left == node { |
| 712 | - | return nil, errors.New("close target missing webview") | 750 | + | return node.parent.right |
| 713 | } | 751 | } | ||
| 752 | + | return node.parent.left | |||
| 753 | + | } | |||
| 714 | 754 | ||||
| 715 | - | var promoted *paneNode | 755 | + | // nextCleanupGeneration returns the next cleanup generation counter |
| 756 | + | func (wm *WorkspaceManager) nextCleanupGeneration() uint { | |||
| 757 | + | wm.cleanupCounter++ | |||
| 758 | + | return wm.cleanupCounter | |||
| 759 | + | } | |||
| 716 | 760 | ||||
| 717 | - | // Handle closing panes in a stack | 761 | + | // ensureWidgets validates and recovers SafeWidget instances |
| 718 | - | if node.parent != nil && node.parent.isStacked { | 762 | + | func (wm *WorkspaceManager) ensureWidgets(nodes ...*paneNode) { |
| 719 | - | return nil, wm.stackedPaneManager.CloseStackedPane(node) | 763 | + | for _, n := range nodes { |
| 764 | + | if n == nil || n.container == nil { | |||
| 765 | + | continue | |||
| 766 | + | } | |||
| 767 | + | if n.container.IsValid() { | |||
| 768 | + | continue | |||
| 769 | + | } | |||
| 770 | + | ptr := n.container.Ptr() | |||
| 771 | + | typeInfo := n.container.typeInfo | |||
| 772 | + | n.container = wm.widgetRegistry.Register(ptr, typeInfo) | |||
| 720 | } | 773 | } | ||
| 774 | + | } | |||
| 721 | 775 | ||||
| 722 | - | if node.parent != nil && node.parent.container != nil { | 776 | + | // promoteSibling promotes a sibling node to replace its parent in the tree |
| 723 | - | node.parent.container.Execute(func(panedPtr uintptr) error { | 777 | + | func (wm *WorkspaceManager) promoteSibling(grand *paneNode, parent *paneNode, sibling *paneNode) { |
| 724 | - | if node.parent.left == node { | 778 | + | if grand == nil { |
| 725 | - | webkit.PanedSetStartChild(panedPtr, 0) | 779 | + | wm.root = sibling |
| 726 | - | // GTK4 automatically unparents when setting child to 0 | 780 | + | sibling.parent = nil |
| 727 | - | } else if node.parent.right == node { | 781 | + | return |
| 728 | - | webkit.PanedSetEndChild(panedPtr, 0) | |||
| 729 | - | // GTK4 automatically unparents when setting child to 0 | |||
| 730 | - | } | |||
| 731 | - | return nil | |||
| 732 | - | }) | |||
| 733 | - | } else if node.container != nil { | |||
| 734 | - | // Only manually unparent if widget wasn't auto-unparented above | |||
| 735 | - | node.container.Execute(func(containerPtr uintptr) error { | |||
| 736 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | |||
| 737 | - | webkit.WidgetUnparent(containerPtr) | |||
| 738 | - | } | |||
| 739 | - | return nil | |||
| 740 | - | }) | |||
| 741 | } | 782 | } | ||
| 783 | + | sibling.parent = grand | |||
| 784 | + | if grand.left == parent { | |||
| 785 | + | grand.left = sibling | |||
| 786 | + | } else { | |||
| 787 | + | grand.right = sibling | |||
| 788 | + | } | |||
| 789 | + | } | |||
| 742 | 790 | ||||
| 743 | - | remaining := len(wm.app.panes) | 791 | + | // swapContainers updates GTK widget hierarchy for promoted siblings |
| 744 | - | willBeLastPane := remaining <= 1 | 792 | + | func (wm *WorkspaceManager) swapContainers(grand *paneNode, sibling *paneNode) { |
| 745 | - | paneCleaned := false | 793 | + | if grand == nil { |
| 746 | - | ensureCleanup := func() { | 794 | + | // When TreeRebalancer is enabled, skip GTK attachment here as it will be |
| 747 | - | if paneCleaned { | 795 | + | // handled by the rebalancer's promotion transaction to avoid double-attachment |
| 748 | - | return | 796 | + | if wm.treeRebalancer == nil || !wm.treeRebalancer.enabled { |
| 797 | + | wm.attachRoot(sibling) | |||
| 749 | } | 798 | } | ||
| 750 | - | node.pane.Cleanup() | 799 | + | return |
| 751 | - | paneCleaned = true | |||
| 752 | } | 800 | } | ||
| 753 | - | 801 | + | grand.container.Execute(func(gPtr uintptr) error { | |
| 754 | - | if remaining == 2 { | 802 | + | sibling.container.Execute(func(sPtr uintptr) error { |
| 755 | - | var sibling *paneNode | 803 | + | // GTK4 auto-unparents when setting paned child |
| 756 | - | if node.parent != nil { | 804 | + | // But we should verify the widget is ready for reparenting |
| 757 | - | if node.parent.left == node { | 805 | + | if parent := webkit.WidgetGetParent(sPtr); parent != 0 && parent != gPtr { |
| 758 | - | sibling = node.parent.right | 806 | + | log.Printf("[workspace] widget %#x has unexpected parent %#x, expected %#x or 0", sPtr, parent, gPtr) |
| 759 | - | } else if node.parent.right == node { | |||
| 760 | - | sibling = node.parent.left | |||
| 761 | } | 807 | } | ||
| 762 | - | } | |||
| 763 | - | if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil { | |||
| 764 | - | if sibling.isPopup && !node.isPopup { | |||
| 765 | - | parentNode := node.parent | |||
| 766 | - | var parentContainer *SafeWidget | |||
| 767 | - | if parentNode != nil { | |||
| 768 | - | parentContainer = parentNode.container | |||
| 769 | - | } | |||
| 770 | - | ||||
| 771 | - | // Detach remaining GTK widgets before destroying the WebViews so GTK | |||
| 772 | - | // does not attempt to dispose still-parented children. | |||
| 773 | - | if parentContainer != nil { | |||
| 774 | - | parentContainer.Execute(func(panedPtr uintptr) error { | |||
| 775 | - | if parentNode.left == sibling { | |||
| 776 | - | webkit.PanedSetStartChild(panedPtr, 0) | |||
| 777 | - | } else if parentNode.right == sibling { | |||
| 778 | - | webkit.PanedSetEndChild(panedPtr, 0) | |||
| 779 | - | } | |||
| 780 | - | return nil | |||
| 781 | - | }) | |||
| 782 | - | } | |||
| 783 | - | if sibling.container != nil { | |||
| 784 | - | sibling.container.Execute(func(containerPtr uintptr) error { | |||
| 785 | - | webkit.WidgetUnparent(containerPtr) | |||
| 786 | - | return nil | |||
| 787 | - | }) | |||
| 788 | - | } | |||
| 789 | - | if parentContainer != nil && wm.window != nil { | |||
| 790 | - | wm.window.SetChild(0) | |||
| 791 | - | } | |||
| 792 | - | ||||
| 793 | - | log.Printf("[workspace] closing primary pane with related popup present; exiting") | |||
| 794 | - | ensureCleanup() | |||
| 795 | - | sibling.pane.Cleanup() | |||
| 796 | 808 | ||||
| 797 | - | wm.detachHover(node) | 809 | + | if grand.left == sibling { |
| 798 | - | wm.detachHover(sibling) | 810 | + | webkit.PanedSetStartChild(gPtr, sPtr) |
| 799 | - | 811 | + | log.Printf("[workspace] swapContainers: set widget %#x as start child of paned %#x", sPtr, gPtr) | |
| 800 | - | // Clean up workspace tracking for both panes | 812 | + | } else { |
| 801 | - | node.pane.CleanupFromWorkspace(wm) | 813 | + | webkit.PanedSetEndChild(gPtr, sPtr) |
| 802 | - | sibling.pane.CleanupFromWorkspace(wm) | 814 | + | log.Printf("[workspace] swapContainers: set widget %#x as end child of paned %#x", sPtr, gPtr) |
| 815 | + | } | |||
| 816 | + | webkit.WidgetQueueAllocate(gPtr) | |||
| 817 | + | return nil | |||
| 818 | + | }) | |||
| 819 | + | return nil | |||
| 820 | + | }) | |||
| 821 | + | } | |||
| 803 | 822 | ||||
| 804 | - | if node.pane.webView != nil { | 823 | + | // attachRoot attaches a node as the new window root |
| 805 | - | if err := node.pane.webView.Destroy(); err != nil { | 824 | + | func (wm *WorkspaceManager) attachRoot(root *paneNode) { |
| 806 | - | log.Printf("[workspace] failed to destroy primary webview: %v", err) | 825 | + | if root == nil || root.container == nil || wm.window == nil { |
| 807 | - | } | 826 | + | return |
| 808 | - | } | 827 | + | } |
| 809 | - | if sibling.pane.webView != nil { | 828 | + | root.container.Execute(func(ptr uintptr) error { |
| 810 | - | if err := sibling.pane.webView.Destroy(); err != nil { | 829 | + | // Check if widget has a parent and unparent it first |
| 811 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | 830 | + | if parent := webkit.WidgetGetParent(ptr); parent != 0 { |
| 812 | - | } | 831 | + | log.Printf("[workspace] unparenting widget %#x from parent %#x before window attach", ptr, parent) |
| 813 | - | } | 832 | + | webkit.WidgetUnparent(ptr) |
| 833 | + | } | |||
| 814 | 834 | ||||
| 815 | - | wm.root = nil | 835 | + | // Now safe to set as window child |
| 816 | - | wm.mainPane = nil | 836 | + | wm.window.SetChild(ptr) |
| 837 | + | webkit.WidgetQueueAllocate(ptr) | |||
| 838 | + | webkit.WidgetShow(ptr) | |||
| 817 | 839 | ||||
| 818 | - | webkit.QuitMainLoop() | 840 | + | // Verify attachment succeeded |
| 819 | - | return nil, nil | 841 | + | if finalParent := webkit.WidgetGetParent(ptr); finalParent == 0 { |
| 820 | - | } | 842 | + | log.Printf("[workspace] WARNING: widget %#x has no parent after SetChild", ptr) |
| 843 | + | } else { | |||
| 844 | + | log.Printf("[workspace] attachRoot successful: widget %#x now child of %#x", ptr, finalParent) | |||
| 821 | } | 845 | } | ||
| 846 | + | return nil | |||
| 847 | + | }) | |||
| 848 | + | } | |||
| 849 | + | ||||
| 850 | + | // cleanupPane safely cleans up a pane node with generation tracking | |||
| 851 | + | func (wm *WorkspaceManager) cleanupPane(node *paneNode, generation uint) { | |||
| 852 | + | if node == nil { | |||
| 853 | + | return | |||
| 822 | } | 854 | } | ||
| 823 | - | if willBeLastPane && wm.root == node { | 855 | + | if !node.widgetValid { |
| 824 | - | log.Printf("[workspace] closing final pane; exiting browser") | 856 | + | return |
| 825 | - | ensureCleanup() | |||
| 826 | - | wm.detachHover(node) | |||
| 827 | - | if err := node.pane.webView.Destroy(); err != nil { | |||
| 828 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |||
| 829 | - | } | |||
| 830 | - | webkit.QuitMainLoop() | |||
| 831 | - | return nil, nil | |||
| 832 | } | 857 | } | ||
| 833 | 858 | ||||
| 834 | - | parent := node.parent | 859 | + | node.widgetValid = false |
| 835 | - | if parent == nil { | 860 | + | node.cleanupGeneration = generation |
| 836 | - | // This is the root pane (no parent in tree structure) | |||
| 837 | - | if node != wm.root { | |||
| 838 | - | return nil, errors.New("inconsistent state: node has no parent but is not root") | |||
| 839 | - | } | |||
| 840 | 861 | ||||
| 841 | - | // Check if we can find a replacement root | 862 | + | if node.pane != nil { |
| 842 | - | replacement := wm.findReplacementRoot(node) | 863 | + | node.pane.CleanupFromWorkspace(wm) |
| 843 | - | if replacement == nil { | 864 | + | if node.pane.webView != nil { |
| 844 | - | // No other panes exist, this is the final pane | 865 | + | node.pane.webView.Destroy() |
| 845 | - | log.Printf("[workspace] closing final pane; exiting browser") | |||
| 846 | - | ensureCleanup() | |||
| 847 | - | wm.detachHover(node) | |||
| 848 | - | if err := node.pane.webView.Destroy(); err != nil { | |||
| 849 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |||
| 850 | - | } | |||
| 851 | - | webkit.QuitMainLoop() | |||
| 852 | - | return nil, nil | |||
| 853 | } | 866 | } | ||
| 867 | + | } | |||
| 854 | 868 | ||||
| 855 | - | // We have a replacement - delegate root status and close this pane | 869 | + | if node.container != nil { |
| 856 | - | log.Printf("[workspace] delegating root status from node=%#x to replacement=%#x", node.container, replacement.container) | 870 | + | node.container.Invalidate() |
| 871 | + | node.container = nil | |||
| 872 | + | } | |||
| 857 | 873 | ||||
| 858 | - | // Clean up workspace tracking | 874 | + | node.parent = nil |
| 859 | - | node.pane.CleanupFromWorkspace(wm) | 875 | + | node.left = nil |
| 876 | + | node.right = nil | |||
| 877 | + | } | |||
| 860 | 878 | ||||
| 861 | - | if wm.mainPane == node { | 879 | + | // decommissionParent cleans up a parent node after promotion |
| 862 | - | wm.mainPane = nil | 880 | + | func (wm *WorkspaceManager) decommissionParent(parent *paneNode, generation uint) { |
| 863 | - | } | 881 | + | if parent == nil { |
| 882 | + | return | |||
| 883 | + | } | |||
| 864 | 884 | ||||
| 865 | - | // Clear current active if it's the node being closed | 885 | + | // For branch nodes (paneds), ensure the GTK widget is destroyed properly |
| 866 | - | if wm.GetActiveNode() == node { | 886 | + | if !parent.isLeaf && parent.container != nil { |
| 867 | - | } | 887 | + | parent.container.Execute(func(ptr uintptr) error { |
| 888 | + | // Clear any remaining children first to ensure clean destruction | |||
| 889 | + | // Note: Children may already be reparented during promotion, so verify parent relationship | |||
| 890 | + | if webkit.IsPaned(ptr) { | |||
| 891 | + | startChild := webkit.PanedGetStartChild(ptr) | |||
| 892 | + | endChild := webkit.PanedGetEndChild(ptr) | |||
| 868 | 893 | ||||
| 869 | - | // Set replacement as new root | 894 | + | // Only clear children that are still valid AND still parented to this paned |
| 870 | - | wm.root = replacement | 895 | + | // If the child was reparented during swapContainers, its parent will be different |
| 871 | - | replacement.parent = nil // Critical: clear parent pointer for new root | 896 | + | if startChild != 0 && webkit.WidgetIsValid(startChild) && webkit.WidgetGetParent(startChild) == ptr { |
| 872 | - | promoted = replacement | 897 | + | webkit.PanedSetStartChild(ptr, 0) |
| 873 | - | ||||
| 874 | - | // Replace window child - must unparent first to satisfy GTK4 assertion | |||
| 875 | - | if wm.window != nil && replacement.container != nil { | |||
| 876 | - | replacement.container.Execute(func(containerPtr uintptr) error { | |||
| 877 | - | // Unparent from current parent before setting as window child | |||
| 878 | - | if webkit.WidgetGetParent(containerPtr) != 0 { | |||
| 879 | - | webkit.WidgetUnparent(containerPtr) | |||
| 880 | } | 898 | } | ||
| 881 | - | wm.window.SetChild(containerPtr) | 899 | + | if endChild != 0 && webkit.WidgetIsValid(endChild) && webkit.WidgetGetParent(endChild) == ptr { |
| 882 | - | webkit.WidgetQueueAllocate(containerPtr) | 900 | + | webkit.PanedSetEndChild(ptr, 0) |
| 883 | - | webkit.WidgetShow(containerPtr) | 901 | + | } |
| 884 | - | return nil | 902 | + | log.Printf("[workspace] cleared paned children before destruction: %#x", ptr) |
| 885 | - | }) | |||
| 886 | - | } | |||
| 887 | - | ||||
| 888 | - | // Focus a suitable pane | |||
| 889 | - | focusTarget := wm.leftmostLeaf(replacement) | |||
| 890 | - | if focusTarget != nil { | |||
| 891 | - | wm.SetActivePane(focusTarget, SourceClose) | |||
| 892 | - | } | |||
| 893 | - | ||||
| 894 | - | // Destroy the webview and detach hover AFTER rearranging hierarchy | |||
| 895 | - | // Only destroy the webview if this is the final pane, otherwise just clean up | |||
| 896 | - | ensureCleanup() | |||
| 897 | - | wm.detachHover(node) | |||
| 898 | - | switch { | |||
| 899 | - | case node.isPopup: | |||
| 900 | - | if err := node.pane.webView.Destroy(); err != nil { | |||
| 901 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |||
| 902 | - | } | |||
| 903 | - | case willBeLastPane: | |||
| 904 | - | // This is the last pane, safe to destroy completely | |||
| 905 | - | if err := node.pane.webView.Destroy(); err != nil { | |||
| 906 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |||
| 907 | } | 903 | } | ||
| 908 | - | default: | 904 | + | // Hook widget destruction |
| 909 | - | // Multiple panes remain, don't destroy the window - just clean up the webview | 905 | + | webkit.WidgetHookDestroy(ptr) |
| 910 | - | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | 906 | + | log.Printf("[workspace] destroyed paned widget: %#x", ptr) |
| 911 | - | // TODO: Add a method to destroy just the webview without the window | 907 | + | return nil |
| 912 | - | } | 908 | + | }) |
| 913 | - | ||||
| 914 | - | wm.updateMainPane() | |||
| 915 | - | // Update CSS classes after pane count changes | |||
| 916 | - | log.Printf("[workspace] root pane closed and delegated; panes remaining=%d", len(wm.app.panes)) | |||
| 917 | - | return promoted, nil | |||
| 918 | } | 909 | } | ||
| 919 | 910 | ||||
| 920 | - | var sibling *paneNode | 911 | + | wm.cleanupPane(parent, generation) |
| 921 | - | if parent.left == node { | 912 | + | } |
| 922 | - | sibling = parent.right | 913 | + | |
| 923 | - | } else { | 914 | + | // promoteNewRoot handles root replacement when closing the root pane |
| 924 | - | sibling = parent.left | 915 | + | func (wm *WorkspaceManager) promoteNewRoot(ctx closeContext, oldRoot *paneNode) (*paneNode, error) { |
| 916 | + | candidate := wm.findReplacementRoot(oldRoot) | |||
| 917 | + | if candidate == nil { | |||
| 918 | + | return wm.cleanupAndExit(oldRoot) | |||
| 925 | } | 919 | } | ||
| 926 | - | if sibling == nil { | 920 | + | |
| 927 | - | return nil, errors.New("pane close failed: missing sibling") | 921 | + | sibling := wm.getSibling(candidate) |
| 922 | + | if sibling != nil { | |||
| 923 | + | wm.promoteSibling(candidate.parent.parent, candidate.parent, sibling) | |||
| 928 | } | 924 | } | ||
| 929 | 925 | ||||
| 930 | - | grand := parent.parent | 926 | + | candidate.parent = nil |
| 927 | + | wm.root = candidate | |||
| 931 | 928 | ||||
| 932 | - | ensureSafeWidget := func(owner string, widget **SafeWidget) error { | 929 | + | wm.attachRoot(candidate) |
| 933 | - | if widget == nil { | |||
| 934 | - | return nil | |||
| 935 | - | } | |||
| 936 | - | current := *widget | |||
| 937 | - | if current == nil { | |||
| 938 | - | return fmt.Errorf("%s widget missing", owner) | |||
| 939 | - | } | |||
| 940 | - | if current.IsValid() { | |||
| 941 | - | return nil | |||
| 942 | - | } | |||
| 943 | 930 | ||||
| 944 | - | ptr := current.Ptr() | 931 | + | wm.cleanupPane(oldRoot, ctx.Generation()) |
| 945 | - | typeInfo := current.typeInfo | 932 | + | return candidate, nil |
| 946 | - | current.Invalidate() | 933 | + | } |
| 947 | - | if wm.widgetRegistry != nil && ptr != 0 { | |||
| 948 | - | wm.widgetRegistry.Unregister(ptr) | |||
| 949 | - | } | |||
| 950 | 934 | ||||
| 951 | - | if ptr == 0 || !webkit.WidgetIsValid(ptr) { | 935 | + | // cleanupAndExit handles the final pane cleanup and application exit |
| 952 | - | *widget = nil | 936 | + | func (wm *WorkspaceManager) cleanupAndExit(node *paneNode) (*paneNode, error) { |
| 953 | - | return fmt.Errorf("%s widget %#x invalidated", owner, ptr) | 937 | + | log.Printf("[workspace] closing final pane; exiting browser") |
| 954 | - | } | 938 | + | wm.cleanupPane(node, wm.nextCleanupGeneration()) |
| 939 | + | wm.detachHover(node) | |||
| 940 | + | if err := node.pane.webView.Destroy(); err != nil { | |||
| 941 | + | log.Printf("[workspace] failed to destroy webview: %v", err) | |||
| 942 | + | } | |||
| 943 | + | webkit.QuitMainLoop() | |||
| 944 | + | return nil, nil | |||
| 945 | + | } | |||
| 955 | 946 | ||||
| 956 | - | if wm.widgetRegistry == nil { | 947 | + | // setFocusToLeaf sets focus to the leftmost leaf of a node |
| 957 | - | return fmt.Errorf("%s widget registry unavailable", owner) | 948 | + | func (wm *WorkspaceManager) setFocusToLeaf(node *paneNode) { |
| 958 | - | } | 949 | + | focusTarget := wm.leftmostLeaf(node) |
| 950 | + | if focusTarget != nil { | |||
| 951 | + | wm.SetActivePane(focusTarget, SourceClose) | |||
| 952 | + | } | |||
| 953 | + | } | |||
| 959 | 954 | ||||
| 960 | - | reRegistered := wm.widgetRegistry.Register(ptr, typeInfo) | 955 | + | // closePane closes a specific pane and handles tree restructuring (simplified implementation) |
| 961 | - | if reRegistered == nil { | 956 | + | func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) { |
| 962 | - | return fmt.Errorf("%s widget %#x re-registration failed", owner, ptr) | 957 | + | ctx := wm.beginClose(node) |
| 963 | - | } | 958 | + | defer ctx.finish() |
| 964 | - | *widget = reRegistered | 959 | + | |
| 965 | - | log.Printf("[workspace] re-registered %s widget: ptr=%#x type=%s", owner, ptr, typeInfo) | 960 | + | // STEP 1: Basic validation (quick fail) |
| 966 | - | return nil | 961 | + | if ctx.err != nil { |
| 962 | + | return nil, ctx.err | |||
| 967 | } | 963 | } | ||
| 968 | 964 | ||||
| 969 | - | if err := ensureSafeWidget("sibling", &sibling.container); err != nil { | 965 | + | // STEP 2: Handle stacked panes via compatibility shim |
| 970 | - | log.Printf("[workspace] close aborted: %v", err) | 966 | + | if node.parent != nil && node.parent.isStacked { |
| 971 | - | return nil, err | 967 | + | return wm.closeStackedPaneCompat(node) |
| 972 | } | 968 | } | ||
| 973 | - | if parent.container != nil { | 969 | + | |
| 974 | - | if err := ensureSafeWidget("parent", &parent.container); err != nil { | 970 | + | // STEP 3: Handle trivial exit cases |
| 975 | - | log.Printf("[workspace] close aborted: %v", err) | 971 | + | if ctx.remaining == 1 { |
| 976 | - | return nil, err | 972 | + | return wm.cleanupAndExit(node) |
| 977 | - | } | |||
| 978 | } | 973 | } | ||
| 979 | - | if grand != nil && grand.container != nil { | 974 | + | if node == wm.root { |
| 980 | - | if err := ensureSafeWidget("grandparent", &grand.container); err != nil { | 975 | + | return wm.promoteNewRoot(ctx, node) |
| 981 | - | log.Printf("[workspace] close aborted: %v", err) | |||
| 982 | - | return nil, err | |||
| 983 | - | } | |||
| 984 | } | 976 | } | ||
| 985 | 977 | ||||
| 986 | - | if err := wm.validateWidgetsForReparenting(sibling, parent, grand); err != nil { | 978 | + | // STEP 4: Promote sibling in-place |
| 987 | - | log.Printf("[workspace] invalid widget state prior to close: %v", err) | 979 | + | parent := node.parent |
| 988 | - | return nil, err | 980 | + | sibling := wm.getSibling(node) |
| 989 | - | } | 981 | + | grandparent := parent.parent |
| 982 | + | wm.ensureWidgets(grandparent, parent, sibling) | |||
| 990 | 983 | ||||
| 991 | - | log.Printf("[workspace] closing pane: target=%#x parent=%#x sibling=%#x remaining=%d", node.container, parent.container, sibling.container, remaining) | 984 | + | wm.promoteSibling(grandparent, parent, sibling) |
| 992 | 985 | ||||
| 993 | - | // Clean up workspace tracking | 986 | + | // STEP 5: GTK updates leverage auto-unparenting |
| 994 | - | node.pane.CleanupFromWorkspace(wm) | 987 | + | wm.swapContainers(grandparent, sibling) |
| 995 | 988 | ||||
| 996 | - | if wm.mainPane == node { | 989 | + | // STEP 6: Cleanup & focus |
| 997 | - | wm.mainPane = nil | 990 | + | wm.cleanupPane(node, ctx.Generation()) |
| 991 | + | wm.decommissionParent(parent, ctx.Generation()) | |||
| 992 | + | wm.setFocusToLeaf(sibling) | |||
| 993 | + | ||||
| 994 | + | return sibling, nil | |||
| 995 | + | } | |||
| 996 | + | ||||
| 997 | + | // closeStackedPaneCompat provides compatibility layer for closing stacked panes | |||
| 998 | + | func (wm *WorkspaceManager) closeStackedPaneCompat(node *paneNode) (*paneNode, error) { | |||
| 999 | + | stack := node.parent | |||
| 1000 | + | ||||
| 1001 | + | // Find index | |||
| 1002 | + | index := -1 | |||
| 1003 | + | for i, pane := range stack.stackedPanes { | |||
| 1004 | + | if pane == node { | |||
| 1005 | + | index = i | |||
| 1006 | + | break | |||
| 1007 | + | } | |||
| 998 | } | 1008 | } | ||
| 999 | 1009 | ||||
| 1000 | - | // Clear current active if it's the node being closed | 1010 | + | if index == -1 { |
| 1001 | - | if wm.GetActiveNode() == node { | 1011 | + | return nil, errors.New("pane not in stack") |
| 1002 | } | 1012 | } | ||
| 1003 | 1013 | ||||
| 1004 | - | promoted = sibling | 1014 | + | // Remove from array |
| 1005 | - | if grand == nil { | 1015 | + | stack.stackedPanes = append( |
| 1006 | - | // Parent is the root node. Promote sibling to become the new root. | 1016 | + | stack.stackedPanes[:index], |
| 1007 | - | log.Printf("[workspace] promoting sibling to root: container=%#x, isLeaf=%v", sibling.container, sibling.isLeaf) | 1017 | + | stack.stackedPanes[index+1:]..., |
| 1018 | + | ) | |||
| 1008 | 1019 | ||||
| 1009 | - | // Update tree structure first | 1020 | + | // If only one pane left, unstack it |
| 1010 | - | wm.root = sibling | 1021 | + | if len(stack.stackedPanes) == 1 { |
| 1011 | - | sibling.parent = nil // Critical: clear parent pointer for new root | 1022 | + | remaining := stack.stackedPanes[0] |
| 1012 | - | ||||
| 1013 | - | // For root promotion, handle the widget transition carefully | |||
| 1014 | - | if wm.window != nil && sibling.container != nil { | |||
| 1015 | - | sibling.container.Execute(func(containerPtr uintptr) error { | |||
| 1016 | - | parentBefore := webkit.WidgetGetParent(containerPtr) | |||
| 1017 | - | log.Printf("[workspace] root promotion: sibling parent before=%#x", parentBefore) | |||
| 1018 | - | ||||
| 1019 | - | // Only unparent if it has a parent (the paned) | |||
| 1020 | - | if parentBefore != 0 { | |||
| 1021 | - | // Get info about the paned before we destroy it | |||
| 1022 | - | oldParentStart := webkit.PanedGetStartChild(parentBefore) | |||
| 1023 | - | oldParentEnd := webkit.PanedGetEndChild(parentBefore) | |||
| 1024 | - | log.Printf("[workspace] root promotion: old parent start=%#x end=%#x", oldParentStart, oldParentEnd) | |||
| 1025 | - | ||||
| 1026 | - | // Unparent the sibling from the paned | |||
| 1027 | - | webkit.WidgetUnparent(containerPtr) | |||
| 1028 | - | ||||
| 1029 | - | // Ensure the widget is properly configured for window child | |||
| 1030 | - | webkit.WidgetSetHExpand(containerPtr, true) | |||
| 1031 | - | webkit.WidgetSetVExpand(containerPtr, true) | |||
| 1032 | - | } | |||
| 1033 | 1023 | ||||
| 1034 | - | // Set as window child | 1024 | + | // Replace stack with remaining pane in tree |
| 1035 | - | wm.window.SetChild(containerPtr) | 1025 | + | remaining.parent = stack.parent |
| 1036 | - | 1026 | + | if stack.parent != nil { | |
| 1037 | - | // Queue draw and allocation to ensure proper rendering | 1027 | + | if stack.parent.left == stack { |
| 1038 | - | webkit.WidgetQueueAllocate(containerPtr) | 1028 | + | stack.parent.left = remaining |
| 1039 | - | webkit.WidgetShow(containerPtr) | 1029 | + | } else { |
| 1040 | - | 1030 | + | stack.parent.right = remaining | |
| 1041 | - | // Verify attachment succeeded | 1031 | + | } |
| 1042 | - | parentAfter := webkit.WidgetGetParent(containerPtr) | |||
| 1043 | - | if parentAfter == 0 { | |||
| 1044 | - | log.Printf("[workspace] WARNING: root promotion attachment failed, widget has no parent") | |||
| 1045 | - | // Force another attempt at setting the child | |||
| 1046 | - | wm.window.SetChild(containerPtr) | |||
| 1047 | - | webkit.WidgetQueueAllocate(containerPtr) | |||
| 1048 | - | parentAfter = webkit.WidgetGetParent(containerPtr) | |||
| 1049 | - | } | |||
| 1050 | 1032 | ||||
| 1051 | - | log.Printf("[workspace] root promotion: sibling parent after=%#x", parentAfter) | 1033 | + | // Update GTK widgets |
| 1052 | - | if parentAfter != 0 { | 1034 | + | stack.parent.container.Execute(func(pPtr uintptr) error { |
| 1053 | - | log.Printf("[workspace] successfully promoted sibling to root: %#x", containerPtr) | 1035 | + | remaining.container.Execute(func(rPtr uintptr) error { |
| 1054 | - | } else { | 1036 | + | if stack.parent.left == remaining { |
| 1055 | - | log.Printf("[workspace] ERROR: failed to attach promoted sibling as window child: %#x", containerPtr) | 1037 | + | webkit.PanedSetStartChild(pPtr, rPtr) |
| 1056 | - | } | 1038 | + | } else { |
| 1039 | + | webkit.PanedSetEndChild(pPtr, rPtr) | |||
| 1040 | + | } | |||
| 1041 | + | return nil | |||
| 1042 | + | }) | |||
| 1057 | return nil | 1043 | return nil | ||
| 1058 | }) | 1044 | }) | ||
| 1059 | - | } | |||
| 1060 | - | } else { | |||
| 1061 | - | // Parent has a grandparent, so promote sibling to take parent's place | |||
| 1062 | - | log.Printf("[workspace] promoting sibling to parent's position: sibling=%#x grand=%#x", sibling.container, grand.container) | |||
| 1063 | - | ||||
| 1064 | - | // Update tree structure | |||
| 1065 | - | sibling.parent = grand | |||
| 1066 | - | if grand.left == parent { | |||
| 1067 | - | grand.left = sibling | |||
| 1068 | } else { | 1045 | } else { | ||
| 1069 | - | grand.right = sibling | 1046 | + | // Stack was root |
| 1070 | - | } | 1047 | + | wm.root = remaining |
| 1071 | - | 1048 | + | remaining.container.Execute(func(ptr uintptr) error { | |
| 1072 | - | // Move sibling to grandparent's paned widget | 1049 | + | wm.window.SetChild(ptr) |
| 1073 | - | // GTK4 handles unparenting automatically when we set the new paned child | |||
| 1074 | - | grand.container.Execute(func(grandPtr uintptr) error { | |||
| 1075 | - | sibling.container.Execute(func(siblingPtr uintptr) error { | |||
| 1076 | - | parentBefore := webkit.WidgetGetParent(siblingPtr) | |||
| 1077 | - | log.Printf("[workspace] promotion: sibling current parent=%#x grand=%#x", parentBefore, grandPtr) | |||
| 1078 | - | ||||
| 1079 | - | startBefore := webkit.PanedGetStartChild(grandPtr) | |||
| 1080 | - | endBefore := webkit.PanedGetEndChild(grandPtr) | |||
| 1081 | - | log.Printf("[workspace] promotion: grand start before=%#x end before=%#x", startBefore, endBefore) | |||
| 1082 | - | ||||
| 1083 | - | if grand.left == sibling { | |||
| 1084 | - | webkit.PanedSetStartChild(grandPtr, siblingPtr) | |||
| 1085 | - | } else { | |||
| 1086 | - | webkit.PanedSetEndChild(grandPtr, siblingPtr) | |||
| 1087 | - | } | |||
| 1088 | - | ||||
| 1089 | - | webkit.WidgetQueueAllocate(grandPtr) | |||
| 1090 | - | webkit.WidgetQueueAllocate(siblingPtr) | |||
| 1091 | - | webkit.WidgetShow(siblingPtr) | |||
| 1092 | - | ||||
| 1093 | - | parentAfter := webkit.WidgetGetParent(siblingPtr) | |||
| 1094 | - | startAfter := webkit.PanedGetStartChild(grandPtr) | |||
| 1095 | - | endAfter := webkit.PanedGetEndChild(grandPtr) | |||
| 1096 | - | log.Printf("[workspace] promotion: sibling parent after=%#x", parentAfter) | |||
| 1097 | - | log.Printf("[workspace] promotion: grand start after=%#x end after=%#x", startAfter, endAfter) | |||
| 1098 | - | if parentAfter == 0 { | |||
| 1099 | - | log.Printf("[workspace] WARNING: sibling widget %#x has no parent after promotion", siblingPtr) | |||
| 1100 | - | } | |||
| 1101 | - | log.Printf("[workspace] successfully promoted sibling to parent position: %#x", siblingPtr) | |||
| 1102 | return nil | 1050 | return nil | ||
| 1103 | }) | 1051 | }) | ||
| 1052 | + | } | |||
| 1104 | 1053 | ||||
| 1105 | - | // Also queue allocate the grandparent to ensure layout updates | 1054 | + | // Cleanup stack container |
| 1106 | - | return nil | 1055 | + | wm.cleanupPane(stack, wm.nextCleanupGeneration()) |
| 1107 | - | }) | |||
| 1108 | - | } | |||
| 1109 | 1056 | ||||
| 1110 | - | // Parent container is now detached; ensure it is cleaned up to avoid lingering invalid widgets | 1057 | + | return remaining, nil |
| 1111 | - | if parent.container != nil { | |||
| 1112 | - | ptr := parent.container.Ptr() | |||
| 1113 | - | parent.container.Invalidate() | |||
| 1114 | - | if wm.widgetRegistry != nil && ptr != 0 { | |||
| 1115 | - | wm.widgetRegistry.Unregister(ptr) | |||
| 1116 | - | } | |||
| 1117 | - | parent.container = nil | |||
| 1118 | } | 1058 | } | ||
| 1119 | - | parent.left = nil | |||
| 1120 | - | parent.right = nil | |||
| 1121 | - | parent.parent = nil | |||
| 1122 | 1059 | ||||
| 1123 | - | // Find a suitable focus target | 1060 | + | // Update active index if needed |
| 1124 | - | focusTarget := wm.leftmostLeaf(sibling) | 1061 | + | if stack.activeStackIndex >= len(stack.stackedPanes) { |
| 1125 | - | if focusTarget != nil { | 1062 | + | stack.activeStackIndex = len(stack.stackedPanes) - 1 |
| 1126 | - | wm.SetActivePane(focusTarget, SourceClose) | |||
| 1127 | } | 1063 | } | ||
| 1128 | 1064 | ||||
| 1129 | - | // Clean up the node being closed | 1065 | + | // Remove widget from GTK box |
| 1130 | - | ensureCleanup() | 1066 | + | stack.container.Execute(func(boxPtr uintptr) error { |
| 1131 | - | wm.detachHover(node) | 1067 | + | node.container.Execute(func(nodePtr uintptr) error { |
| 1068 | + | webkit.BoxRemove(boxPtr, nodePtr) | |||
| 1069 | + | return nil | |||
| 1070 | + | }) | |||
| 1071 | + | return nil | |||
| 1072 | + | }) | |||
| 1132 | 1073 | ||||
| 1133 | - | switch { | 1074 | + | // Show new active pane |
| 1134 | - | case node.isPopup: | 1075 | + | if stack.activeStackIndex >= 0 { |
| 1135 | - | if err := node.pane.webView.Destroy(); err != nil { | 1076 | + | active := stack.stackedPanes[stack.activeStackIndex] |
| 1136 | - | log.Printf("[workspace] failed to destroy popup webview: %v", err) | 1077 | + | active.container.Execute(func(ptr uintptr) error { |
| 1137 | - | } | 1078 | + | webkit.WidgetShow(ptr) |
| 1138 | - | case willBeLastPane: | 1079 | + | return nil |
| 1139 | - | // This is the last pane, safe to destroy completely | 1080 | + | }) |
| 1140 | - | if err := node.pane.webView.Destroy(); err != nil { | |||
| 1141 | - | log.Printf("[workspace] failed to destroy webview: %v", err) | |||
| 1142 | - | } | |||
| 1143 | - | default: | |||
| 1144 | - | // Multiple panes remain, don't destroy the window - just clean up the webview | |||
| 1145 | - | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | |||
| 1146 | - | // TODO: Add a method to destroy just the webview without the window | |||
| 1147 | } | 1081 | } | ||
| 1148 | 1082 | ||||
| 1149 | - | wm.updateMainPane() | 1083 | + | // Cleanup closed pane |
| 1150 | - | // Update CSS classes after pane count changes | 1084 | + | wm.cleanupPane(node, wm.nextCleanupGeneration()) |
| 1151 | - | log.Printf("[workspace] pane closed; panes remaining=%d", len(wm.app.panes)) | 1085 | + | |
| 1152 | - | return promoted, nil | 1086 | + | return stack, nil |
| 1153 | } | 1087 | } | ||