1// workspace_pane_ops.go - Pane creation, splitting, closing and tree operations
2package browser
3
4import (
5 "errors"
6 "log"
7
8 "github.com/bnema/dumber/pkg/webkit"
9)
10
11// closeContext manages the context for a pane close operation
12type 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
21func (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
37func (ctx closeContext) Generation() uint {
38 return ctx.generation
39}
40
41// finish handles cleanup operations after close completion
42func (ctx closeContext) finish() {
43 if ctx.err == nil {
44 ctx.wm.updateMainPane()
45 }
46}
47
48// collectLeaves returns all leaf nodes in the workspace tree
49func (wm *WorkspaceManager) collectLeaves() []*paneNode {
50 return wm.collectLeavesFrom(wm.root)
51}
52
53// collectLeavesFrom collects all leaf nodes from a given subtree
54func (wm *WorkspaceManager) collectLeavesFrom(node *paneNode) []*paneNode {
55 var leaves []*paneNode
56 visited := make(map[*paneNode]bool)
57
58 var walk func(*paneNode, int)
59 walk = func(n *paneNode, depth int) {
60 // Prevent infinite recursion and cycles
61 const maxDepth = 50
62 if n == nil || depth > maxDepth {
63 return
64 }
65 if visited[n] {
66 log.Printf("[workspace] collectLeavesFrom: cycle detected in tree")
67 return
68 }
69 visited[n] = true
70
71 if n.isLeaf {
72 leaves = append(leaves, n)
73 return
74 }
75
76 // Handle stacked panes - only include the currently ACTIVE pane as a leaf
77 // This prevents multiple CSS classes and ensures correct focus management
78 if n.isStacked && len(n.stackedPanes) > 0 {
79 activeIndex := n.activeStackIndex
80 if activeIndex >= 0 && activeIndex < len(n.stackedPanes) {
81 // Only the active pane in the stack counts as a leaf for focus/navigation
82 activePaneInStack := n.stackedPanes[activeIndex]
83 walk(activePaneInStack, depth+1)
84 }
85 return
86 }
87
88 // Handle regular split nodes
89 walk(n.left, depth+1)
90 walk(n.right, depth+1)
91 }
92 walk(node, 0)
93 return leaves
94}
95
96// createWebView creates a new WebView with the workspace's configuration
97func (wm *WorkspaceManager) createWebView() (*webkit.WebView, error) {
98 if wm == nil || wm.createWebViewFn == nil {
99 return nil, errors.New("workspace manager missing webview factory")
100 }
101 return wm.createWebViewFn()
102}
103
104// createPane creates a new BrowserPane for the given WebView
105func (wm *WorkspaceManager) createPane(view *webkit.WebView) (*BrowserPane, error) {
106 if wm == nil || wm.createPaneFn == nil {
107 return nil, errors.New("workspace manager missing pane factory")
108 }
109 return wm.createPaneFn(view)
110}
111
112// clonePaneState sets up a new pane with cloned state from another pane
113func (wm *WorkspaceManager) clonePaneState(_ *paneNode, target *paneNode) {
114 if wm == nil || target == nil {
115 return
116 }
117 if target.pane == nil || target.pane.webView == nil {
118 return
119 }
120
121 const blankURL = "about:blank"
122
123 if err := target.pane.webView.LoadURL(blankURL); err != nil {
124 log.Printf("[workspace] failed to prepare blank pane: %v", err)
125 }
126
127 // Omnibox will auto-open on about:blank via client-side detection
128}
129
130// safelyDetachControllersBeforeReparent removes GTK controllers that GTK will auto-clean up during reparenting.
131// It marks nodes for reattachment once the widget hierarchy settles after the split operation.
132func (wm *WorkspaceManager) safelyDetachControllersBeforeReparent(node *paneNode) {
133 if wm == nil || node == nil {
134 return
135 }
136
137 markForDetachment := func(target *paneNode) {
138 if target == nil {
139 return
140 }
141
142 if target.hoverToken != 0 {
143 target.pendingHoverReattach = true
144 wm.detachHover(target)
145 }
146
147 if wm.focusStateMachine != nil && target.focusControllerToken != 0 {
148 token := target.focusControllerToken
149 target.pendingFocusReattach = true
150 wm.focusStateMachine.detachGTKController(target, token)
151 }
152 }
153
154 if node.isStacked {
155 for _, child := range node.stackedPanes {
156 markForDetachment(child)
157 }
158 return
159 }
160
161 if node.isLeaf {
162 markForDetachment(node)
163 }
164}
165
166// closeCurrentPane closes the currently focused pane
167func (wm *WorkspaceManager) closeCurrentPane() {
168 if wm == nil || wm.GetActiveNode() == nil {
169 return
170 }
171 if err := wm.ClosePane(wm.GetActiveNode()); err != nil {
172 log.Printf("[workspace] close current pane failed: %v", err)
173 }
174}
175
176// leftmostLeaf finds the leftmost leaf node in a subtree
177func (wm *WorkspaceManager) leftmostLeaf(node *paneNode) *paneNode {
178 for node != nil && !node.isLeaf {
179 if node.isStacked {
180 if node.activeStackIndex >= 0 && node.activeStackIndex < len(node.stackedPanes) {
181 candidate := node.stackedPanes[node.activeStackIndex]
182 if candidate != nil {
183 return candidate
184 }
185 }
186 if len(node.stackedPanes) > 0 {
187 return node.stackedPanes[0]
188 }
189 return nil
190 }
191 if node.left != nil {
192 node = node.left
193 continue
194 }
195 node = node.right
196 }
197 return node
198}
199
200// findReplacementRoot finds a suitable replacement when closing the current root pane
201func (wm *WorkspaceManager) findReplacementRoot(excludeNode *paneNode) *paneNode {
202 if wm == nil || wm.root == nil {
203 return nil
204 }
205
206 // If root is being closed and there are other panes, find a replacement
207 leaves := wm.collectLeaves()
208 for _, leaf := range leaves {
209 if leaf != excludeNode && leaf != nil && leaf.isLeaf {
210 // Find the topmost ancestor that's not the current root
211 current := leaf
212 for current.parent != nil && current.parent != wm.root {
213 current = current.parent
214 }
215
216 // If this leaf is a direct child of root, or root only has one subtree,
217 // we can promote this subtree to be the new root
218 if current.parent == wm.root {
219 // If the sibling is being excluded, promote this subtree
220 var sibling *paneNode
221 if wm.root.left == current {
222 sibling = wm.root.right
223 } else {
224 sibling = wm.root.left
225 }
226
227 if sibling == excludeNode {
228 // The sibling is being closed, so promote this subtree
229 return current
230 }
231 }
232
233 // Otherwise, return the first suitable leaf
234 return leaf
235 }
236 }
237
238 return nil
239}
240
241// updateMainPane updates the main pane reference based on current state
242func (wm *WorkspaceManager) updateMainPane() {
243 if len(wm.app.panes) == 1 {
244 if leaf := wm.viewToNode[wm.app.panes[0].webView]; leaf != nil {
245 wm.mainPane = leaf
246 }
247 return
248 }
249
250 if wm.mainPane == nil || !wm.mainPane.isLeaf {
251 if wm.GetActiveNode() != nil && wm.GetActiveNode().isLeaf {
252 wm.mainPane = wm.GetActiveNode()
253 }
254 }
255}
256
257// Helper methods to support clean pane removal from workspace tracking
258// These methods implement the interface expected by BrowserPane.CleanupFromWorkspace()
259
260// removeFromMaps removes a WebView from all workspace tracking maps
261func (wm *WorkspaceManager) removeFromMaps(webView *webkit.WebView) {
262 if wm == nil || webView == nil {
263 return
264 }
265
266 delete(wm.viewToNode, webView)
267 delete(wm.lastSplitMsg, webView)
268 delete(wm.lastExitMsg, webView)
269 log.Printf("[workspace] removed webview %p from tracking maps", webView)
270}
271
272// removeFromAppPanes removes a BrowserPane from the app.panes slice
273func (wm *WorkspaceManager) removeFromAppPanes(pane *BrowserPane) {
274 if wm == nil || wm.app == nil || pane == nil {
275 return
276 }
277
278 for i, p := range wm.app.panes {
279 if p == pane {
280 wm.app.panes = append(wm.app.panes[:i], wm.app.panes[i+1:]...)
281 log.Printf("[workspace] removed pane from app.panes slice, remaining: %d", len(wm.app.panes))
282 return
283 }
284 }
285}
286
287// insertPopupPane inserts a pre-created popup pane into the workspace
288func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {
289 if target == nil {
290 return errors.New("insert target cannot be nil")
291 }
292
293 // Accept both leaf panes and stacked panes as valid targets
294 if !target.isLeaf && !target.isStacked {
295 return errors.New("insert target must be a leaf pane or stacked pane")
296 }
297
298 // For leaf panes, require a pane
299 if target.isLeaf && target.pane == nil {
300 return errors.New("leaf target missing pane")
301 }
302
303 // For stacked panes, we trust the caller - production stacks have varied structures
304 // Do not validate internal stack structure here
305
306 if newPane == nil || newPane.webView == nil {
307 return errors.New("new pane missing webview")
308 }
309
310 if handler := newPane.MessageHandler(); handler != nil {
311 handler.SetWorkspaceObserver(wm)
312 }
313
314 newContainer := newPane.webView.RootWidget()
315 if newContainer == 0 {
316 return errors.New("new pane missing container")
317 }
318
319 webkit.WidgetSetHExpand(newContainer, true)
320 webkit.WidgetSetVExpand(newContainer, true)
321 webkit.WidgetRealizeInContainer(newContainer)
322
323 // Also realize the WebView widget itself for proper popup rendering
324 webViewWidget := newPane.webView.Widget()
325 if webViewWidget != 0 {
326 webkit.WidgetRealizeInContainer(webViewWidget)
327 }
328
329 // CRITICAL: Handle stacked panes correctly - same logic as splitNode
330 // When the target is inside a stacked pane, we need to split around the entire stack,
331 // not the individual pane within the stack
332 originalTarget := target
333 var stackContainer *paneNode
334 if target.parent != nil && target.parent.isStacked {
335 // Target is in a stack - we'll create the split around the entire stack
336 stackContainer = target.parent
337 log.Printf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer)
338 }
339
340 // Determine what we're actually splitting
341 var splitTarget *paneNode
342 var splitTargetContainer uintptr
343
344 if stackContainer != nil {
345 // Case: splitting from inside a stack - we split around the entire stack
346 splitTarget = stackContainer
347 splitTargetContainer = stackContainer.container
348 log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget)
349 } else {
350 // Case: normal split from a simple pane or direct stack target
351 splitTarget = target
352 splitTargetContainer = target.container
353 log.Printf("[workspace] normal split from pane: target=%p", splitTarget)
354 }
355
356 if splitTargetContainer == 0 {
357 return errors.New("split target missing container")
358 }
359
360 orientation, existingFirst := mapDirection(direction)
361 log.Printf("[workspace] inserting popup direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p", direction, orientation, existingFirst, splitTarget.parent)
362
363 paned := webkit.NewPaned(orientation)
364 if paned == 0 {
365 return errors.New("failed to create GtkPaned")
366 }
367 webkit.WidgetSetHExpand(paned, true)
368 webkit.WidgetSetVExpand(paned, true)
369 // For popup panes: preserve existing pane size, let popup take remaining space
370 webkit.PanedSetResizeStart(paned, false) // existing pane keeps its size
371 webkit.PanedSetResizeEnd(paned, true) // popup can resize
372 log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible")
373
374 newLeaf := &paneNode{
375 pane: newPane,
376 isLeaf: true,
377 }
378 wm.initializePaneWidgets(newLeaf, newContainer)
379
380 split := &paneNode{
381 parent: splitTarget.parent,
382 orientation: orientation,
383 isLeaf: false,
384 }
385 wm.initializePaneWidgets(split, paned)
386
387 parent := split.parent
388
389 // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout
390 // Only attempt this if the parent container is actually a paned widget
391 var parentDividerPos int
392 if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) && webkit.IsPaned(parent.container) {
393 parentDividerPos = webkit.PanedGetPosition(parent.container)
394 log.Printf("[workspace] captured parent divider position: %d", parentDividerPos)
395 } else if parent != nil && parent.container != 0 {
396 log.Printf("[workspace] parent container is not a paned widget (likely a stacked container)")
397 }
398
399 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption
400 if splitTargetContainer != 0 && webkit.WidgetIsValid(splitTargetContainer) {
401 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)
402 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {
403 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)
404 }
405
406 // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline
407 // This forces WebKit to detach its compositor, preventing rendering corruption
408 webkit.WidgetHide(splitTargetContainer)
409 log.Printf("[workspace] Hidden split target container before reparenting: %#x", splitTargetContainer)
410 }
411
412 // Prevent GTK from auto-removing controllers while the widget is temporarily unparented.
413 wm.safelyDetachControllersBeforeReparent(splitTarget)
414
415 // Detach existing container from its current GTK parent before inserting into new paned.
416 if parent == nil {
417 // Split target is the root - remove it from the window
418 log.Printf("[workspace] removing split target container=%#x from window", splitTargetContainer)
419 if wm.window != nil {
420 wm.window.SetChild(0)
421 }
422 // GTK4 will auto-unparent when we add to new paned - no manual unparent needed
423 } else if parent.container != 0 {
424 // Split target has a parent paned - unparent it from there
425 log.Printf("[workspace] unparenting split target container from parent paned")
426 if webkit.WidgetIsValid(parent.container) {
427 log.Printf("[workspace] executing unparent: parentPtr=%#x", parent.container)
428 if parent.left == splitTarget {
429 webkit.PanedSetStartChild(parent.container, 0)
430 } else if parent.right == splitTarget {
431 webkit.PanedSetEndChild(parent.container, 0)
432 }
433 webkit.WidgetQueueAllocate(parent.container)
434 }
435 }
436
437 // Add both containers to the new paned
438 if existingFirst {
439 split.left = splitTarget
440 split.right = newLeaf
441 webkit.PanedSetStartChild(paned, splitTargetContainer)
442 webkit.PanedSetEndChild(paned, newContainer)
443 log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer)
444 } else {
445 split.left = newLeaf
446 split.right = splitTarget
447 webkit.PanedSetStartChild(paned, newContainer)
448 webkit.PanedSetEndChild(paned, splitTargetContainer)
449 log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer)
450 }
451
452 splitTarget.parent = split
453 newLeaf.parent = split
454
455 if parent == nil {
456 wm.root = split
457 if wm.window != nil {
458 wm.window.SetChild(paned)
459 webkit.WidgetQueueAllocate(paned)
460 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
461 }
462 } else {
463 if webkit.WidgetIsValid(parent.container) {
464 log.Printf("[workspace] executing parent reparent: parentPtr=%#x, paned=%#x", parent.container, paned)
465 if parent.left == splitTarget {
466 parent.left = split
467 webkit.PanedSetStartChild(parent.container, paned)
468 } else if parent.right == splitTarget {
469 parent.right = split
470 webkit.PanedSetEndChild(parent.container, paned)
471 }
472 webkit.WidgetQueueAllocate(parent.container)
473 webkit.WidgetQueueAllocate(paned)
474 log.Printf("[workspace] paned inserted into parent successfully")
475
476 // CRITICAL: Restore parent paned's divider position to preserve existing layout
477 // Only attempt this if we captured a position and the parent is actually a paned widget
478 if parentDividerPos > 0 && webkit.IsPaned(parent.container) {
479 webkit.PanedSetPosition(parent.container, parentDividerPos)
480 log.Printf("[workspace] restored parent divider position: %d", parentDividerPos)
481 }
482 }
483 }
484
485 webkit.WidgetShow(paned)
486
487 // Force GTK to re-layout the new GtkPaned container once both children are attached.
488 wm.scheduleIdleGuarded(func() bool {
489 if split == nil || !split.widgetValid {
490 return false
491 }
492 if split.container != 0 && webkit.WidgetIsValid(split.container) {
493 webkit.WidgetQueueResize(split.container)
494 webkit.WidgetQueueDraw(split.container)
495 }
496 return false
497 }, split)
498
499 // CRITICAL: Set initial 50/50 split position after showing paned
500 // GTK needs the widget to be realized before we can set position based on allocation
501 // Schedule this to run after GTK has allocated space to the paned
502 wm.scheduleIdleGuarded(func() bool {
503 if !webkit.WidgetIsValid(paned) {
504 return false
505 }
506 // Get paned allocation to calculate 50% position
507 alloc := webkit.WidgetGetAllocation(paned)
508 var splitPos int
509 if orientation == webkit.OrientationHorizontal {
510 splitPos = alloc.Width / 2
511 } else {
512 splitPos = alloc.Height / 2
513 }
514 if splitPos > 0 {
515 webkit.PanedSetPosition(paned, splitPos)
516 log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height)
517 }
518 return false
519 }, split)
520
521 // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting
522 // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues
523 if splitTarget != nil && splitTarget.container != 0 {
524 wm.scheduleIdleGuarded(func() bool {
525 if splitTarget == nil || !splitTarget.widgetValid || splitTarget.container == 0 {
526 return false
527 }
528 // Show widget to reconnect WebKit rendering pipeline
529 webkit.WidgetShow(splitTarget.container)
530 // Force GTK to recalculate size and recreate rendering surface
531 webkit.WidgetQueueResize(splitTarget.container)
532 webkit.WidgetQueueDraw(splitTarget.container)
533 log.Printf("[workspace] Shown and queued resize+draw for split target container after reparenting")
534 return false
535 }, splitTarget)
536 }
537
538 wm.viewToNode[newPane.webView] = newLeaf
539 wm.ensureHover(newLeaf)
540 wm.ensureHover(splitTarget)
541 wm.app.panes = append(wm.app.panes, newPane)
542 if newPane.zoomController != nil {
543 newPane.zoomController.ApplyInitialZoom()
544 }
545
546 // Update CSS classes for all panes now that we have multiple panes
547
548 reattachTargets := []*paneNode{splitTarget}
549 if splitTarget != nil && splitTarget.isStacked {
550 reattachTargets = append(reattachTargets, splitTarget.stackedPanes...)
551 }
552 guardNodes := append([]*paneNode{newLeaf}, reattachTargets...)
553 wm.scheduleIdleGuarded(func() bool {
554 if newLeaf == nil || !newLeaf.widgetValid {
555 return false
556 }
557 if newContainer != 0 && webkit.WidgetIsValid(newContainer) {
558 webkit.WidgetShow(newContainer)
559 }
560 // Attach GTK focus controller to new pane
561 if wm.focusStateMachine != nil {
562 wm.focusStateMachine.attachGTKController(newLeaf)
563 }
564
565 for _, candidate := range reattachTargets {
566 if candidate == nil || !candidate.widgetValid {
567 continue
568 }
569 if candidate.pendingHoverReattach {
570 wm.ensureHover(candidate)
571 if candidate.hoverToken != 0 {
572 candidate.pendingHoverReattach = false
573 }
574 }
575 if candidate.pendingFocusReattach && wm.focusStateMachine != nil {
576 wm.focusStateMachine.attachGTKController(candidate)
577 if candidate.focusControllerToken != 0 {
578 candidate.pendingFocusReattach = false
579 }
580 }
581 }
582
583 wm.SetActivePane(newLeaf, SourceSplit)
584 return false
585 }, guardNodes...)
586
587 return nil
588}
589
590// splitNode splits a target pane into two panes in the specified direction
591func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*paneNode, error) {
592 if target == nil || !target.isLeaf || target.pane == nil {
593 return nil, errors.New("split target must be a leaf pane")
594 }
595
596 // IMPORTANT: If the target is inside a stacked pane, we need to split AROUND the stack,
597 // not the individual pane or the stack itself. We create a new split at the stack's parent level.
598 originalTarget := target
599 var stackContainer *paneNode
600 if target.parent != nil && target.parent.isStacked {
601 // Target is in a stack - we'll create the split around the entire stack
602 stackContainer = target.parent
603 log.Printf("[workspace] target is in stack, will split around the stack: originalTarget=%p stackContainer=%p", originalTarget, stackContainer)
604 }
605
606 // Determine what we're actually splitting
607 var splitTarget *paneNode
608 var splitTargetContainer uintptr
609
610 if stackContainer != nil {
611 // Case: splitting from inside a stack - we split around the entire stack
612 splitTarget = stackContainer
613 splitTargetContainer = stackContainer.container
614 log.Printf("[workspace] splitting around stack: stackContainer=%p", splitTarget)
615 } else {
616 // Case: normal split from a simple pane
617 splitTarget = target
618 splitTargetContainer = target.container
619 log.Printf("[workspace] normal split from pane: target=%p", splitTarget)
620 }
621
622 newView, err := wm.createWebView()
623 if err != nil {
624 return nil, err
625 }
626
627 newPane, err := wm.createPane(newView)
628 if err != nil {
629 return nil, err
630 }
631
632 if handler := newPane.MessageHandler(); handler != nil {
633 handler.SetWorkspaceObserver(wm)
634 }
635
636 newContainer := newPane.webView.RootWidget()
637 if newContainer == 0 {
638 return nil, errors.New("new pane missing container")
639 }
640 webkit.WidgetSetHExpand(newContainer, true)
641 webkit.WidgetSetVExpand(newContainer, true)
642 webkit.WidgetRealizeInContainer(newContainer)
643
644 // Use the determined split target container
645 if splitTargetContainer == 0 {
646 return nil, errors.New("split target missing container")
647 }
648
649 // Only apply GTK operations if this is a simple widget (leaf pane)
650 // For stacked containers, we need to be more careful
651 if splitTarget.isLeaf {
652 if webkit.WidgetIsValid(splitTargetContainer) {
653 webkit.WidgetSetHExpand(splitTargetContainer, true)
654 webkit.WidgetSetVExpand(splitTargetContainer, true)
655 webkit.WidgetRealizeInContainer(splitTargetContainer)
656 }
657 } else if splitTarget.isStacked {
658 // Stack containers need the same setup as regular panes for proper splitting
659 if webkit.WidgetIsValid(splitTargetContainer) {
660 webkit.WidgetSetHExpand(splitTargetContainer, true)
661 webkit.WidgetSetVExpand(splitTargetContainer, true)
662 webkit.WidgetRealizeInContainer(splitTargetContainer)
663 log.Printf("[workspace] configured stack wrapper for split: %#x", splitTargetContainer)
664 }
665 }
666
667 orientation, existingFirst := mapDirection(direction)
668 log.Printf("[workspace] splitting direction=%s orientation=%v existingFirst=%v splitTarget.parent=%p splitTarget.isStacked=%v",
669 direction, orientation, existingFirst, splitTarget.parent, splitTarget.isStacked)
670
671 paned := webkit.NewPaned(orientation)
672 if paned == 0 {
673 return nil, errors.New("failed to create GtkPaned")
674 }
675 webkit.WidgetSetHExpand(paned, true)
676 webkit.WidgetSetVExpand(paned, true)
677 webkit.PanedSetResizeStart(paned, true)
678 webkit.PanedSetResizeEnd(paned, true)
679
680 newLeaf := &paneNode{
681 pane: newPane,
682 isLeaf: true,
683 }
684 wm.initializePaneWidgets(newLeaf, newContainer)
685
686 split := &paneNode{
687 parent: splitTarget.parent,
688 orientation: orientation,
689 isLeaf: false,
690 }
691 wm.initializePaneWidgets(split, paned)
692
693 parent := split.parent
694
695 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption
696 if splitTargetContainer != 0 && webkit.WidgetIsValid(splitTargetContainer) {
697 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)
698 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {
699 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)
700 }
701 }
702
703 // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented.
704 wm.safelyDetachControllersBeforeReparent(splitTarget)
705
706 // Detach split target container from its current GTK parent before inserting into new paned.
707 if parent == nil {
708 // Split target is the root - remove it from the window
709 log.Printf("[workspace] removing split target container=%#x from window", splitTargetContainer)
710 if wm.window != nil {
711 wm.window.SetChild(0)
712 }
713 // GTK4 will auto-unparent when we add to new paned - no manual unparent needed
714 } else if parent.container != 0 {
715 // Split target has a parent paned - remove it (automatically unparents in GTK4)
716 log.Printf("[workspace] unparenting split target container=%#x from parent paned=%#x", splitTargetContainer, parent.container)
717 if webkit.WidgetIsValid(parent.container) {
718 if parent.left == splitTarget {
719 webkit.PanedSetStartChild(parent.container, 0)
720 } else if parent.right == splitTarget {
721 webkit.PanedSetEndChild(parent.container, 0)
722 }
723 // GTK4 automatically unparents when we set paned child to 0 - no manual unparent needed
724 webkit.WidgetQueueAllocate(parent.container)
725 }
726 }
727
728 // Set up the tree structure first
729 if existingFirst {
730 split.left = splitTarget
731 split.right = newLeaf
732 } else {
733 split.left = newLeaf
734 split.right = splitTarget
735 }
736
737 splitTarget.parent = split
738 newLeaf.parent = split
739
740 // Update tree root/parent references
741 if parent == nil {
742 wm.root = split
743 } else {
744 if parent.left == splitTarget {
745 parent.left = split
746 } else if parent.right == splitTarget {
747 parent.right = split
748 }
749 }
750
751 // GTK4 handles widget operations automatically - no need to force redraw
752
753 // Add both containers to the new paned
754 if existingFirst {
755 if webkit.WidgetIsValid(splitTargetContainer) {
756 webkit.PanedSetStartChild(paned, splitTargetContainer)
757 webkit.PanedSetEndChild(paned, newContainer)
758 log.Printf("[workspace] added splitTarget=%#x as start child, new=%#x as end child", splitTargetContainer, newContainer)
759 }
760 } else {
761 if webkit.WidgetIsValid(splitTargetContainer) {
762 webkit.PanedSetStartChild(paned, newContainer)
763 webkit.PanedSetEndChild(paned, splitTargetContainer)
764 log.Printf("[workspace] added new=%#x as start child, splitTarget=%#x as end child", newContainer, splitTargetContainer)
765 }
766 }
767
768 // Attach the new paned to its parent
769 if parent == nil {
770 if wm.window != nil {
771 wm.window.SetChild(paned)
772 webkit.WidgetQueueAllocate(paned)
773 log.Printf("[workspace] paned set as window child: paned=%#x", paned)
774 }
775 } else {
776 if webkit.WidgetIsValid(parent.container) {
777 if parent.left == split {
778 webkit.PanedSetStartChild(parent.container, paned)
779 } else if parent.right == split {
780 webkit.PanedSetEndChild(parent.container, paned)
781 }
782 webkit.WidgetQueueAllocate(parent.container)
783 webkit.WidgetQueueAllocate(paned)
784 log.Printf("[workspace] paned inserted into parent=%#x", parent.container)
785 }
786 }
787
788 webkit.WidgetShow(paned)
789
790 // CRITICAL: Set initial 50/50 split position after showing paned
791 // GTK needs the widget to be realized before we can set position based on allocation
792 // Schedule this to run after GTK has allocated space to the paned
793 wm.scheduleIdleGuarded(func() bool {
794 if !webkit.WidgetIsValid(paned) {
795 return false
796 }
797 // Get paned allocation to calculate 50% position
798 alloc := webkit.WidgetGetAllocation(paned)
799 var splitPos int
800 if orientation == webkit.OrientationHorizontal {
801 splitPos = alloc.Width / 2
802 } else {
803 splitPos = alloc.Height / 2
804 }
805 if splitPos > 0 {
806 webkit.PanedSetPosition(paned, splitPos)
807 log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height)
808 }
809 return false
810 }, split)
811
812 // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting
813 // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues
814 if target != nil && target.container != 0 {
815 wm.scheduleIdleGuarded(func() bool {
816 if target == nil || !target.widgetValid || target.container == 0 {
817 return false
818 }
819 // Show widget to reconnect WebKit rendering pipeline
820 webkit.WidgetShow(target.container)
821 // Force GTK to recalculate size and recreate rendering surface
822 webkit.WidgetQueueResize(target.container)
823 webkit.WidgetQueueDraw(target.container)
824 log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting")
825 return false
826 }, target)
827 }
828
829 wm.viewToNode[newPane.webView] = newLeaf
830 wm.ensureHover(newLeaf)
831
832 // Don't ensure hover on originalTarget when splitting from stack - it competes with new pane focus
833 // The originalTarget is still in the stack and should not interfere with the new split pane
834 if originalTarget == target {
835 // Only ensure hover on target if it's a regular (non-stack) split
836 if !target.pendingHoverReattach {
837 wm.ensureHover(target)
838 }
839 }
840
841 wm.app.panes = append(wm.app.panes, newPane)
842 if newPane.zoomController != nil {
843 newPane.zoomController.ApplyInitialZoom()
844 }
845
846 // Update CSS classes for all panes now that we have multiple panes
847
848 reattachTargets := []*paneNode{splitTarget}
849 if splitTarget != nil && splitTarget.isStacked {
850 reattachTargets = append(reattachTargets, splitTarget.stackedPanes...)
851 }
852 guardNodes := append([]*paneNode{newLeaf}, reattachTargets...)
853 wm.scheduleIdleGuarded(func() bool {
854 if newLeaf == nil || !newLeaf.widgetValid {
855 return false
856 }
857 if newContainer != 0 && webkit.WidgetIsValid(newContainer) {
858 webkit.WidgetShow(newContainer)
859 }
860 // Attach GTK focus controller to new pane
861 if wm.focusStateMachine != nil {
862 wm.focusStateMachine.attachGTKController(newLeaf)
863 }
864
865 for _, candidate := range reattachTargets {
866 if candidate == nil || !candidate.widgetValid {
867 continue
868 }
869 if candidate.pendingHoverReattach {
870 wm.ensureHover(candidate)
871 if candidate.hoverToken != 0 {
872 candidate.pendingHoverReattach = false
873 }
874 }
875 if candidate.pendingFocusReattach && wm.focusStateMachine != nil {
876 wm.focusStateMachine.attachGTKController(candidate)
877 if candidate.focusControllerToken != 0 {
878 candidate.pendingFocusReattach = false
879 }
880 }
881 }
882 wm.SetActivePane(newLeaf, SourceSplit)
883 return false
884 }, guardNodes...)
885
886 return newLeaf, nil
887}
888
889// Helper functions for the simplified closePane implementation
890
891// getSibling returns the sibling node for a given node
892func (wm *WorkspaceManager) getSibling(node *paneNode) *paneNode {
893 if node.parent == nil {
894 return nil
895 }
896 if node.parent.left == node {
897 return node.parent.right
898 }
899 return node.parent.left
900}
901
902// nextCleanupGeneration returns the next cleanup generation counter
903func (wm *WorkspaceManager) nextCleanupGeneration() uint {
904 wm.cleanupCounter++
905 return wm.cleanupCounter
906}
907
908// promoteSibling promotes a sibling node to replace its parent in the tree
909func (wm *WorkspaceManager) promoteSibling(grand *paneNode, parent *paneNode, sibling *paneNode) {
910 if grand == nil {
911 wm.root = sibling
912 sibling.parent = nil
913 return
914 }
915 sibling.parent = grand
916 if grand.left == parent {
917 grand.left = sibling
918 } else {
919 grand.right = sibling
920 }
921}
922
923// swapContainers updates GTK widget hierarchy for promoted siblings
924func (wm *WorkspaceManager) swapContainers(grand *paneNode, sibling *paneNode) {
925 if grand == nil {
926 // When TreeRebalancer is enabled, skip GTK attachment here as it will be
927 // handled by the rebalancer's promotion transaction to avoid double-attachment
928 if wm.treeRebalancer == nil || !wm.treeRebalancer.enabled {
929 wm.attachRoot(sibling)
930 }
931 return
932 }
933 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption
934 if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {
935 if webkit.WidgetHasCSSClass(sibling.container, activePaneClass) {
936 webkit.WidgetRemoveCSSClass(sibling.container, activePaneClass)
937 }
938 }
939
940 if webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {
941 // GTK4 PanedSetStartChild/EndChild auto-unparent from current parent
942 // Focus was already cleared before unparenting closed pane
943 if parent := webkit.WidgetGetParent(sibling.container); parent != 0 && parent != grand.container {
944 log.Printf("[workspace] widget %#x reparenting from %#x to %#x (GTK4 will auto-unparent)", sibling.container, parent, grand.container)
945 }
946
947 if grand.left == sibling {
948 webkit.PanedSetStartChild(grand.container, sibling.container)
949 log.Printf("[workspace] swapContainers: set widget %#x as start child of paned %#x", sibling.container, grand.container)
950 } else {
951 webkit.PanedSetEndChild(grand.container, sibling.container)
952 log.Printf("[workspace] swapContainers: set widget %#x as end child of paned %#x", sibling.container, grand.container)
953 }
954 webkit.WidgetQueueAllocate(grand.container)
955 }
956}
957
958// cascadePromotion handles the case where a paned node has only one child
959// This happens when closing a pane leaves the grandparent with only one child
960func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
961 if singleChildPaned == nil {
962 return
963 }
964
965 // Find the single child
966 var onlyChild *paneNode
967 if singleChildPaned.left != nil && singleChildPaned.right == nil {
968 onlyChild = singleChildPaned.left
969 } else if singleChildPaned.right != nil && singleChildPaned.left == nil {
970 onlyChild = singleChildPaned.right
971 } else {
972 // Either no children or two children - nothing to cascade
973 return
974 }
975
976 log.Printf("[workspace] Cascading promotion: replacing paned %p with its only child %p", singleChildPaned, onlyChild)
977
978 greatGrandparent := singleChildPaned.parent
979
980 // Update tree structure
981 onlyChild.parent = greatGrandparent
982 if greatGrandparent == nil {
983 // Single child becomes new root
984 wm.root = onlyChild
985 log.Printf("[workspace] Only child %p promoted to root", onlyChild)
986
987 // Attach to window
988 if onlyChild.container != 0 && webkit.WidgetIsValid(onlyChild.container) {
989 wm.attachRoot(onlyChild)
990 }
991
992 // Cleanup the now-orphaned paned
993 wm.decommissionParent(singleChildPaned, wm.cleanupCounter)
994 } else {
995 // Replace paned in great-grandparent
996 if greatGrandparent.left == singleChildPaned {
997 greatGrandparent.left = onlyChild
998 } else {
999 greatGrandparent.right = onlyChild
1000 }
1001
1002 log.Printf("[workspace] Only child %p attached to great-grandparent %p", onlyChild, greatGrandparent)
1003
1004 // Reparent widget in GTK
1005 if onlyChild.container != 0 && webkit.WidgetIsValid(onlyChild.container) && greatGrandparent.container != 0 && webkit.WidgetIsValid(greatGrandparent.container) {
1006 // GTK4 PanedSet*Child automatically handles unparenting
1007 if greatGrandparent.left == onlyChild {
1008 webkit.PanedSetStartChild(greatGrandparent.container, onlyChild.container)
1009 } else {
1010 webkit.PanedSetEndChild(greatGrandparent.container, onlyChild.container)
1011 }
1012
1013 webkit.WidgetSetHExpand(onlyChild.container, true)
1014 webkit.WidgetSetVExpand(onlyChild.container, true)
1015 webkit.WidgetQueueAllocate(onlyChild.container)
1016 webkit.WidgetQueueAllocate(greatGrandparent.container)
1017 wm.scheduleIdleGuarded(func() bool {
1018 if onlyChild == nil || !onlyChild.widgetValid {
1019 return false
1020 }
1021 if onlyChild.container != 0 && webkit.WidgetIsValid(onlyChild.container) {
1022 webkit.WidgetShow(onlyChild.container)
1023 webkit.WidgetQueueResize(onlyChild.container)
1024 webkit.WidgetQueueDraw(onlyChild.container)
1025 }
1026 return false
1027 }, onlyChild)
1028 }
1029
1030 // Cleanup the now-orphaned paned
1031 wm.decommissionParent(singleChildPaned, wm.cleanupCounter)
1032
1033 // Check if great-grandparent now also has only one child (recursive case)
1034 if greatGrandparent != nil && ((greatGrandparent.left == nil) != (greatGrandparent.right == nil)) {
1035 log.Printf("[workspace] Great-grandparent %p also has only one child, continuing cascade", greatGrandparent)
1036 wm.cascadePromotion(greatGrandparent)
1037 }
1038 }
1039}
1040
1041// attachRoot attaches a node as the new window root
1042func (wm *WorkspaceManager) attachRoot(root *paneNode) {
1043 if root == nil || root.container == 0 || wm.window == nil {
1044 return
1045 }
1046 if !webkit.WidgetIsValid(root.container) {
1047 return
1048 }
1049
1050 // Hold a transient reference so GTK does not destroy the widget while detached
1051 releaseRef := webkit.WidgetRef(root.container)
1052 if !releaseRef {
1053 log.Printf("[workspace] WARNING: failed to acquire temporary ref for widget %#x; aborting attachRoot", root.container)
1054 return
1055 }
1056 defer func() {
1057 if webkit.WidgetIsValid(root.container) {
1058 webkit.WidgetUnref(root.container)
1059 }
1060 }()
1061
1062 // Detach from previous container (paned, stack, etc.) before replacing the window child.
1063 if parent := webkit.WidgetGetParent(root.container); parent != 0 {
1064 log.Printf("[workspace] unparenting widget %#x from parent %#x before window attach", root.container, parent)
1065 webkit.WidgetUnparent(root.container)
1066 if !webkit.WidgetIsValid(root.container) {
1067 log.Printf("[workspace] WARNING: widget %#x became invalid during unparent; aborting attachRoot", root.container)
1068 return
1069 }
1070 }
1071
1072 // Clear any existing window child to avoid GTK warnings when swapping roots.
1073 wm.window.SetChild(0)
1074
1075 if webkit.WidgetIsValid(root.container) {
1076 webkit.WidgetSetHExpand(root.container, true)
1077 webkit.WidgetSetVExpand(root.container, true)
1078 }
1079
1080 if !webkit.WidgetIsValid(root.container) {
1081 log.Printf("[workspace] WARNING: widget %#x invalid prior to window attach", root.container)
1082 return
1083 }
1084
1085 wm.window.SetChild(root.container)
1086 if webkit.WidgetIsValid(root.container) {
1087 webkit.WidgetQueueAllocate(root.container)
1088 webkit.WidgetShow(root.container)
1089 webkit.WidgetQueueResize(root.container)
1090 webkit.WidgetQueueDraw(root.container)
1091 }
1092
1093 wm.scheduleIdleGuarded(func() bool {
1094 if root == nil || !root.widgetValid {
1095 return false
1096 }
1097 if root.container != 0 && webkit.WidgetIsValid(root.container) {
1098 webkit.WidgetQueueResize(root.container)
1099 webkit.WidgetQueueDraw(root.container)
1100 }
1101 return false
1102 }, root)
1103
1104 // Verify attachment succeeded
1105 if finalParent := webkit.WidgetGetParent(root.container); finalParent == 0 {
1106 log.Printf("[workspace] WARNING: widget %#x has no parent after SetChild", root.container)
1107 } else {
1108 log.Printf("[workspace] attachRoot successful: widget %#x now child of %#x", root.container, finalParent)
1109 }
1110}
1111
1112// cleanupPane safely cleans up a pane node with generation tracking
1113func (wm *WorkspaceManager) cleanupPane(node *paneNode, generation uint) {
1114 if node == nil {
1115 return
1116 }
1117 if !node.widgetValid {
1118 return
1119 }
1120
1121 if wm.focusStateMachine != nil {
1122 wm.focusStateMachine.InvalidateActivePane(node)
1123 }
1124
1125 wm.cancelIdleHandles(node)
1126
1127 node.widgetValid = false
1128 node.cleanupGeneration = generation
1129
1130 if node.pane != nil {
1131 pane := node.pane
1132 pane.Cleanup()
1133 pane.CleanupFromWorkspace(wm)
1134 if pane.webView != nil {
1135 log.Printf("[workspace] releasing WebView reference: %p", pane.webView)
1136 pane.webView = nil
1137 }
1138 node.pane = nil
1139 }
1140
1141 if node.container != 0 {
1142 // Container widget will be destroyed by GTK when unparented
1143 node.container = 0
1144 }
1145
1146 node.pendingIdleHandles = nil
1147 node.parent = nil
1148 node.left = nil
1149 node.right = nil
1150}
1151
1152// decommissionParent cleans up a parent node after promotion
1153func (wm *WorkspaceManager) decommissionParent(parent *paneNode, generation uint) {
1154 if parent == nil {
1155 return
1156 }
1157
1158 // For branch nodes (paneds), destroy the widget
1159 // Children have already been unparented during promotion/close, no need to clear them
1160 if !parent.isLeaf && parent.container != 0 && webkit.WidgetIsValid(parent.container) {
1161 webkit.WidgetHookDestroy(parent.container)
1162 log.Printf("[workspace] destroyed paned widget: %#x", parent.container)
1163 }
1164
1165 wm.cleanupPane(parent, generation)
1166}
1167
1168// promoteNewRoot handles root replacement when closing the root pane
1169func (wm *WorkspaceManager) promoteNewRoot(ctx closeContext, oldRoot *paneNode) (*paneNode, error) {
1170 candidate := wm.findReplacementRoot(oldRoot)
1171 if candidate == nil {
1172 return wm.cleanupAndExit(oldRoot)
1173 }
1174
1175 sibling := wm.getSibling(candidate)
1176 if sibling != nil {
1177 wm.promoteSibling(candidate.parent.parent, candidate.parent, sibling)
1178 }
1179
1180 candidate.parent = nil
1181 wm.root = candidate
1182
1183 wm.attachRoot(candidate)
1184
1185 wm.cleanupPane(oldRoot, ctx.Generation())
1186 return candidate, nil
1187}
1188
1189// cleanupAndExit handles the final pane cleanup and application exit
1190func (wm *WorkspaceManager) cleanupAndExit(node *paneNode) (*paneNode, error) {
1191 log.Printf("[workspace] closing final pane; exiting browser")
1192 wm.cleanupPane(node, wm.nextCleanupGeneration())
1193 wm.detachHover(node)
1194 webkit.QuitMainLoop()
1195 return nil, nil
1196}
1197
1198// setFocusToLeaf sets focus to the leftmost leaf of a node
1199func (wm *WorkspaceManager) setFocusToLeaf(node *paneNode) {
1200 focusTarget := wm.leftmostLeaf(node)
1201 if focusTarget != nil {
1202 wm.SetActivePane(focusTarget, SourceClose)
1203 }
1204}
1205
1206// closePane closes a specific pane and handles tree restructuring (simplified implementation)
1207func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
1208 ctx := wm.beginClose(node)
1209 defer ctx.finish()
1210
1211 // STEP 1: Basic validation (quick fail)
1212 if ctx.err != nil {
1213 return nil, ctx.err
1214 }
1215
1216 // STEP 2: Handle stacked panes via compatibility shim
1217 if node.parent != nil && node.parent.isStacked {
1218 return wm.closeStackedPaneCompat(node)
1219 }
1220
1221 // Ensure we detach hover/focus controllers before the widget hierarchy changes.
1222 wm.detachHover(node)
1223 wm.detachFocus(node)
1224
1225 // STEP 3: Handle trivial exit cases
1226 if ctx.remaining == 1 {
1227 return wm.cleanupAndExit(node)
1228 }
1229 if node == wm.root {
1230 return wm.promoteNewRoot(ctx, node)
1231 }
1232
1233 // STEP 4: Unparent closed pane FIRST to avoid leaving paned with single child
1234 parent := node.parent
1235 sibling := wm.getSibling(node)
1236 grandparent := parent.parent
1237
1238 // Clear grandparent's focus child BEFORE unparenting closed pane
1239 // GTK propagates focus events upward, so we must clear grandparent focus first
1240 if grandparent != nil && grandparent.container != 0 && webkit.WidgetIsValid(grandparent.container) {
1241 webkit.WidgetSetFocusChild(grandparent.container, 0)
1242 }
1243
1244 // Unparent the closed pane's container from the paned BEFORE promoting sibling
1245 if node.container != 0 && webkit.WidgetIsValid(node.container) {
1246 if containerParent := webkit.WidgetGetParent(node.container); containerParent == parent.container {
1247 if parent.left == node {
1248 webkit.PanedSetStartChild(parent.container, 0)
1249 } else if parent.right == node {
1250 webkit.PanedSetEndChild(parent.container, 0)
1251 }
1252 log.Printf("[workspace] unparented closed pane container: %#x", node.container)
1253 }
1254 }
1255
1256 // STEP 5: Promote sibling in-place (tree structure)
1257 wm.promoteSibling(grandparent, parent, sibling)
1258
1259 // STEP 6: GTK handles all reparenting automatically
1260 // PanedSetStartChild/EndChild and window.SetChild auto-unparent widgets
1261 wm.swapContainers(grandparent, sibling)
1262
1263 // Reconnect the promoted sibling's rendering pipeline after GTK reparenting.
1264 if sibling != nil && sibling.container != 0 {
1265 wm.scheduleIdleGuarded(func() bool {
1266 if sibling == nil || !sibling.widgetValid {
1267 return false
1268 }
1269 if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {
1270 webkit.WidgetShow(sibling.container)
1271 webkit.WidgetQueueResize(sibling.container)
1272 webkit.WidgetQueueDraw(sibling.container)
1273 }
1274 return false
1275 }, sibling)
1276 }
1277
1278 // STEP 7: Check if grandparent now has only one child (cascade promotion needed)
1279 if grandparent != nil && ((grandparent.left == nil) != (grandparent.right == nil)) {
1280 // Grandparent has exactly one child - it should be promoted too
1281 log.Printf("[workspace] Grandparent %p now has only one child, cascading promotion", grandparent)
1282 wm.cascadePromotion(grandparent)
1283 }
1284
1285 // STEP 8: Cleanup & focus
1286 wm.cleanupPane(node, ctx.Generation())
1287 wm.decommissionParent(parent, ctx.Generation())
1288 if sibling != nil {
1289 wm.scheduleIdleGuarded(func() bool {
1290 if sibling == nil || !sibling.widgetValid {
1291 return false
1292 }
1293 wm.setFocusToLeaf(sibling)
1294 return false
1295 }, sibling)
1296 }
1297
1298 return sibling, nil
1299}
1300
1301// closeStackedPaneCompat provides compatibility layer for closing stacked panes
1302func (wm *WorkspaceManager) closeStackedPaneCompat(node *paneNode) (*paneNode, error) {
1303 stack := node.parent
1304
1305 // Find index
1306 index := -1
1307 for i, pane := range stack.stackedPanes {
1308 if pane == node {
1309 index = i
1310 break
1311 }
1312 }
1313
1314 if index == -1 {
1315 return nil, errors.New("pane not in stack")
1316 }
1317
1318 // Remove from array
1319 stack.stackedPanes = append(
1320 stack.stackedPanes[:index],
1321 stack.stackedPanes[index+1:]...,
1322 )
1323
1324 // If only one pane left, unstack it
1325 if len(stack.stackedPanes) == 1 {
1326 remaining := stack.stackedPanes[0]
1327
1328 // Replace stack with remaining pane in tree
1329 remaining.parent = stack.parent
1330 if stack.parent != nil {
1331 if stack.parent.left == stack {
1332 stack.parent.left = remaining
1333 } else {
1334 stack.parent.right = remaining
1335 }
1336
1337 // Update GTK widgets
1338 if webkit.WidgetIsValid(stack.parent.container) && webkit.WidgetIsValid(remaining.container) {
1339 if stack.parent.left == remaining {
1340 webkit.PanedSetStartChild(stack.parent.container, remaining.container)
1341 } else {
1342 webkit.PanedSetEndChild(stack.parent.container, remaining.container)
1343 }
1344 }
1345 } else {
1346 // Stack was root
1347 wm.root = remaining
1348 if webkit.WidgetIsValid(remaining.container) {
1349 wm.window.SetChild(remaining.container)
1350 }
1351 }
1352
1353 // Cleanup stack container
1354 wm.cleanupPane(stack, wm.nextCleanupGeneration())
1355
1356 return remaining, nil
1357 }
1358
1359 // Update active index if needed
1360 if stack.activeStackIndex >= len(stack.stackedPanes) {
1361 stack.activeStackIndex = len(stack.stackedPanes) - 1
1362 }
1363
1364 // Remove widget from GTK box
1365 if webkit.WidgetIsValid(stack.container) && webkit.WidgetIsValid(node.container) {
1366 webkit.BoxRemove(stack.container, node.container)
1367 }
1368
1369 // Show new active pane
1370 if stack.activeStackIndex >= 0 {
1371 active := stack.stackedPanes[stack.activeStackIndex]
1372 if webkit.WidgetIsValid(active.container) {
1373 webkit.WidgetShow(active.container)
1374 }
1375 }
1376
1377 // Cleanup closed pane
1378 wm.cleanupPane(node, wm.nextCleanupGeneration())
1379
1380 return stack, nil
1381}