detached 0a17c0a

fix: resolve stacked pane interactivity after close operations

When closing a pane in a 2-pane stack via Ctrl+W or Ctrl+P+X, the remaining pane
became non-interactable with a gray background after automatic conversion back to
regular pane mode.

Root cause: The complex widget reparenting during stack-to-regular conversion
wasn't properly restoring widget visibility and focus state.

Changes:
- Add WebView.SetActive() calls in focus manager for proper active state sync
- Add comprehensive focus detection logic for stack-to-regular conversion
- Add explicit widget visibility restoration after reparenting
- Add diagnostic logging for widget states during focus operations
- Add proper focus management mirroring the working stack creation logic

Both Ctrl+W (native) and Ctrl+P+X (JS workspace) shortcuts now properly maintain
pane interactivity after closing stacked panes.

Fixes focus management issues in stacked_panes.go:CloseStackedPane method.
commit
0a17c0a05354891f2368c8de7505fa6e9c871756
author
bamen · · 2025-09-25 17:54 UTC
committer
bamen · · 2025-09-25 20:26 UTC
parent
d148d83
signature
unsigned
5 files · +99 -357
M CHANGELOG.md +2 -1
@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
66
77 ### Added
88 - **Pin/favorite sites**: Pin websites from history or quick access to prioritize them in "Jump back in" section. Uses localStorage persistence with star icons for pin/unpin actions
9-- **Zellij-style stacked panes**: Added Ctrl+P → 's' to stack panes instead of splitting, with Alt+Up/Down navigation between stacked panes. Features collapsed title bars showing "Page Title - domain" for inactive panes and full WebView interaction for the active pane. Includes GTK4 CSS styling for proper visual feedback
9+- **Zellij-style stacked panes**: Added Ctrl+P → 's' to stack panes instead of splitting, with Alt+Up/Down navigation between stacked panes. Features collapsed page title bars showing for inactive panes and full WebView interaction for the active pane. Includes GTK4 CSS styling for proper visual feedback
1010
1111 ### Changed
1212 - **History item layout**: Removed "• domain" from titles, now highlight domain within the full URL using same color as title text
@@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
2020 - **Stacked pane focus conflicts**: Fixed focus stealing issues in stacked panes by implementing focus-aware navigation that preserves active pane when spawning siblings
2121 - **WebView title updates**: Enhanced webview title change handling to properly update stacked pane title bars in addition to database storage
2222 - **GTK4 widget lifecycle**: Improved widget parenting/unparenting operations with proper GTK4 validation and automatic cleanup to prevent critical warnings
23+- **Stacked pane close behavior**: Fixed remaining pane becoming non-interactable (gray background) when closing stacked panes via Ctrl+W or Ctrl+P+X. Added proper widget visibility restoration and focus management during stack-to-regular conversion
2324
2425 ## [0.10.0] - 2025-09-24
2526
@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
6 6
7 ### Added7 ### Added
8 - **Pin/favorite sites**: Pin websites from history or quick access to prioritize them in "Jump back in" section. Uses localStorage persistence with star icons for pin/unpin actions8 - **Pin/favorite sites**: Pin websites from history or quick access to prioritize them in "Jump back in" section. Uses localStorage persistence with star icons for pin/unpin actions
9-- **Zellij-style stacked panes**: Added Ctrl+P → 's' to stack panes instead of splitting, with Alt+Up/Down navigation between stacked panes. Features collapsed title bars showing "Page Title - domain" for inactive panes and full WebView interaction for the active pane. Includes GTK4 CSS styling for proper visual feedback9+- **Zellij-style stacked panes**: Added Ctrl+P → 's' to stack panes instead of splitting, with Alt+Up/Down navigation between stacked panes. Features collapsed page title bars showing for inactive panes and full WebView interaction for the active pane. Includes GTK4 CSS styling for proper visual feedback
10 10
11 ### Changed11 ### Changed
12 - **History item layout**: Removed "• domain" from titles, now highlight domain within the full URL using same color as title text12 - **History item layout**: Removed "• domain" from titles, now highlight domain within the full URL using same color as title text
@@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
20 - **Stacked pane focus conflicts**: Fixed focus stealing issues in stacked panes by implementing focus-aware navigation that preserves active pane when spawning siblings20 - **Stacked pane focus conflicts**: Fixed focus stealing issues in stacked panes by implementing focus-aware navigation that preserves active pane when spawning siblings
21 - **WebView title updates**: Enhanced webview title change handling to properly update stacked pane title bars in addition to database storage21 - **WebView title updates**: Enhanced webview title change handling to properly update stacked pane title bars in addition to database storage
22 - **GTK4 widget lifecycle**: Improved widget parenting/unparenting operations with proper GTK4 validation and automatic cleanup to prevent critical warnings22 - **GTK4 widget lifecycle**: Improved widget parenting/unparenting operations with proper GTK4 validation and automatic cleanup to prevent critical warnings
23+- **Stacked pane close behavior**: Fixed remaining pane becoming non-interactable (gray background) when closing stacked panes via Ctrl+W or Ctrl+P+X. Added proper widget visibility restoration and focus management during stack-to-regular conversion
23 24
24 ## [0.10.0] - 2025-09-2425 ## [0.10.0] - 2025-09-24
25 26
M internal/app/browser/focus_manager.go +6 -0
@@ -180,6 +180,9 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
180180 func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
181181 // Notify old pane it lost focus
182182 if oldPane != nil && oldPane.pane != nil && oldPane.pane.webView != nil {
183+ // Update WebView internal active state
184+ oldPane.pane.webView.SetActive(false)
185+
183186 oldDetail := map[string]any{
184187 "active": false,
185188 "paneId": fm.getPaneID(oldPane),
@@ -191,6 +194,9 @@ func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
191194
192195 // Notify new pane it gained focus
193196 if newPane != nil && newPane.pane != nil && newPane.pane.webView != nil {
197+ // Update WebView internal active state
198+ newPane.pane.webView.SetActive(true)
199+
194200 newDetail := map[string]any{
195201 "active": true,
196202 "paneId": fm.getPaneID(newPane),
@@ -180,6 +180,9 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
180 func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {180 func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
181 // Notify old pane it lost focus181 // Notify old pane it lost focus
182 if oldPane != nil && oldPane.pane != nil && oldPane.pane.webView != nil {182 if oldPane != nil && oldPane.pane != nil && oldPane.pane.webView != nil {
183+ // Update WebView internal active state
184+ oldPane.pane.webView.SetActive(false)
185+
183 oldDetail := map[string]any{186 oldDetail := map[string]any{
184 "active": false,187 "active": false,
185 "paneId": fm.getPaneID(oldPane),188 "paneId": fm.getPaneID(oldPane),
@@ -191,6 +194,9 @@ func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
191 194
192 // Notify new pane it gained focus195 // Notify new pane it gained focus
193 if newPane != nil && newPane.pane != nil && newPane.pane.webView != nil {196 if newPane != nil && newPane.pane != nil && newPane.pane.webView != nil {
197+ // Update WebView internal active state
198+ newPane.pane.webView.SetActive(true)
199+
194 newDetail := map[string]any{200 newDetail := map[string]any{
195 "active": true,201 "active": true,
196 "paneId": fm.getPaneID(newPane),202 "paneId": fm.getPaneID(newPane),
M internal/app/browser/stacked_panes.go +78 -6
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "log"
77 "time"
8+ "unsafe"
89
910 "github.com/bnema/dumber/pkg/webkit"
1011 )
@@ -468,9 +469,28 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
468469
469470 // Check if this pane is in a stack and has a title bar
470471 if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {
471- // Update the title bar label with the new title
472- spm.updateTitleBarLabel(node, title)
473- log.Printf("[workspace] updated title bar for WebView %s: %s", webView.ID(), title)
472+ // CRITICAL: Only update title bar for INACTIVE panes
473+ // Active panes should never show their title bar
474+ stackNode := node.parent
475+ activeIndex := stackNode.activeStackIndex
476+
477+ // Find this pane's index in the stack
478+ paneIndex := -1
479+ for i, pane := range stackNode.stackedPanes {
480+ if pane == node {
481+ paneIndex = i
482+ break
483+ }
484+ }
485+
486+ if paneIndex != -1 && paneIndex != activeIndex {
487+ // This is an INACTIVE pane - safe to update title bar
488+ spm.updateTitleBarLabel(node, title)
489+ log.Printf("[workspace] updated title bar for INACTIVE WebView %s: %s", webView.ID(), title)
490+ } else {
491+ // This is the ACTIVE pane - title bar should remain hidden
492+ log.Printf("[workspace] skipped title bar update for ACTIVE WebView %s: %s", webView.ID(), title)
493+ }
474494 }
475495 }
476496
@@ -632,13 +652,65 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
632652 lastPane.stackedPanes = nil
633653 lastPane.titleBar = 0
634654
655+ // CRITICAL: Ensure the container is visible after reparenting
656+ // The container may have been hidden during stack operations
657+ webkit.WidgetSetVisible(lastPaneContainer, true)
658+
635659 // Update the viewToNode mapping to point to the lastPane, not stackNode
636660 spm.wm.viewToNode[lastPane.pane.webView] = lastPane
637661
638- // Focus the remaining pane if it was the active one being closed
639- if spm.wm.currentlyFocused == node {
640- spm.wm.currentlyFocused = lastPane
662+ // FIXED: Focus the remaining pane if any pane from this stack was currently focused
663+ // Check if currentlyFocused is part of this stack (including the pane being closed)
664+ shouldFocus := false
665+ if spm.wm.currentlyFocused != nil {
666+ log.Printf("[workspace] DEBUG: currentlyFocused=%p, node being closed=%p, lastPane=%p",
667+ spm.wm.currentlyFocused, node, lastPane)
668+ // Check if currentlyFocused is the pane being closed
669+ if spm.wm.currentlyFocused == node {
670+ shouldFocus = true
671+ log.Printf("[workspace] DEBUG: shouldFocus=true (closing focused pane)")
672+ } else if spm.wm.currentlyFocused.parent == stackNode {
673+ // Check if currentlyFocused is another pane in this stack
674+ shouldFocus = true
675+ log.Printf("[workspace] DEBUG: shouldFocus=true (focused pane in same stack)")
676+ }
677+ }
678+
679+ if shouldFocus {
680+ log.Printf("[workspace] focusing remaining pane after stack conversion")
681+
682+ // Log widget state before focus operations
683+ if lastPaneWidget := lastPane.pane.webView.Widget(); lastPaneWidget != 0 {
684+ visible := webkit.WidgetGetVisible(lastPaneWidget)
685+ log.Printf("[workspace] Widget state before focus: widget=%p visible=%v",
686+ unsafe.Pointer(lastPaneWidget), visible)
687+ }
688+
689+ // Update CSS classes (mirror finalizeStackCreation)
690+ spm.wm.ensurePaneBaseClasses()
691+
692+ // Mirror the exact logic from finalizeStackCreation that works perfectly:
693+ // 1. First call SetActivePane (this does all the focus work)
694+ // 2. Then set currentlyFocused
641695 spm.wm.focusManager.SetActivePane(lastPane)
696+ spm.wm.currentlyFocused = lastPane
697+
698+ // CRITICAL: Ensure widget visibility and focus after reparenting
699+ // The widget may need explicit show/focus calls after being reparented
700+ if lastPaneWidget := lastPane.pane.webView.Widget(); lastPaneWidget != 0 {
701+ log.Printf("[workspace] Ensuring widget visibility after reparenting")
702+ webkit.WidgetShow(lastPaneWidget)
703+ webkit.WidgetGrabFocus(lastPaneWidget)
704+
705+ // Log widget state after focus operations
706+ visible := webkit.WidgetGetVisible(lastPaneWidget)
707+ log.Printf("[workspace] Widget state after focus: widget=%p visible=%v",
708+ unsafe.Pointer(lastPaneWidget), visible)
709+ }
710+
711+ log.Printf("[workspace] DEBUG: applied finalizeStackCreation focus logic in reverse")
712+ } else {
713+ log.Printf("[workspace] DEBUG: not focusing remaining pane (shouldFocus=false)")
642714 }
643715
644716 log.Printf("[workspace] converted single-pane stack back to regular pane")
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "log"6 "log"
7 "time"7 "time"
8+ "unsafe"
8 9
9 "github.com/bnema/dumber/pkg/webkit"10 "github.com/bnema/dumber/pkg/webkit"
10 )11 )
@@ -468,9 +469,28 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
468 469
469 // Check if this pane is in a stack and has a title bar470 // Check if this pane is in a stack and has a title bar
470 if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {471 if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {
471- // Update the title bar label with the new title472+ // CRITICAL: Only update title bar for INACTIVE panes
472- spm.updateTitleBarLabel(node, title)473+ // Active panes should never show their title bar
473- log.Printf("[workspace] updated title bar for WebView %s: %s", webView.ID(), title)474+ stackNode := node.parent
475+ activeIndex := stackNode.activeStackIndex
476+
477+ // Find this pane's index in the stack
478+ paneIndex := -1
479+ for i, pane := range stackNode.stackedPanes {
480+ if pane == node {
481+ paneIndex = i
482+ break
483+ }
484+ }
485+
486+ if paneIndex != -1 && paneIndex != activeIndex {
487+ // This is an INACTIVE pane - safe to update title bar
488+ spm.updateTitleBarLabel(node, title)
489+ log.Printf("[workspace] updated title bar for INACTIVE WebView %s: %s", webView.ID(), title)
490+ } else {
491+ // This is the ACTIVE pane - title bar should remain hidden
492+ log.Printf("[workspace] skipped title bar update for ACTIVE WebView %s: %s", webView.ID(), title)
493+ }
474 }494 }
475 }495 }
476 496
@@ -632,13 +652,65 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
632 lastPane.stackedPanes = nil652 lastPane.stackedPanes = nil
633 lastPane.titleBar = 0653 lastPane.titleBar = 0
634 654
655+ // CRITICAL: Ensure the container is visible after reparenting
656+ // The container may have been hidden during stack operations
657+ webkit.WidgetSetVisible(lastPaneContainer, true)
658+
635 // Update the viewToNode mapping to point to the lastPane, not stackNode659 // Update the viewToNode mapping to point to the lastPane, not stackNode
636 spm.wm.viewToNode[lastPane.pane.webView] = lastPane660 spm.wm.viewToNode[lastPane.pane.webView] = lastPane
637 661
638- // Focus the remaining pane if it was the active one being closed662+ // FIXED: Focus the remaining pane if any pane from this stack was currently focused
639- if spm.wm.currentlyFocused == node {663+ // Check if currentlyFocused is part of this stack (including the pane being closed)
640- spm.wm.currentlyFocused = lastPane664+ shouldFocus := false
665+ if spm.wm.currentlyFocused != nil {
666+ log.Printf("[workspace] DEBUG: currentlyFocused=%p, node being closed=%p, lastPane=%p",
667+ spm.wm.currentlyFocused, node, lastPane)
668+ // Check if currentlyFocused is the pane being closed
669+ if spm.wm.currentlyFocused == node {
670+ shouldFocus = true
671+ log.Printf("[workspace] DEBUG: shouldFocus=true (closing focused pane)")
672+ } else if spm.wm.currentlyFocused.parent == stackNode {
673+ // Check if currentlyFocused is another pane in this stack
674+ shouldFocus = true
675+ log.Printf("[workspace] DEBUG: shouldFocus=true (focused pane in same stack)")
676+ }
677+ }
678+
679+ if shouldFocus {
680+ log.Printf("[workspace] focusing remaining pane after stack conversion")
681+
682+ // Log widget state before focus operations
683+ if lastPaneWidget := lastPane.pane.webView.Widget(); lastPaneWidget != 0 {
684+ visible := webkit.WidgetGetVisible(lastPaneWidget)
685+ log.Printf("[workspace] Widget state before focus: widget=%p visible=%v",
686+ unsafe.Pointer(lastPaneWidget), visible)
687+ }
688+
689+ // Update CSS classes (mirror finalizeStackCreation)
690+ spm.wm.ensurePaneBaseClasses()
691+
692+ // Mirror the exact logic from finalizeStackCreation that works perfectly:
693+ // 1. First call SetActivePane (this does all the focus work)
694+ // 2. Then set currentlyFocused
641 spm.wm.focusManager.SetActivePane(lastPane)695 spm.wm.focusManager.SetActivePane(lastPane)
696+ spm.wm.currentlyFocused = lastPane
697+
698+ // CRITICAL: Ensure widget visibility and focus after reparenting
699+ // The widget may need explicit show/focus calls after being reparented
700+ if lastPaneWidget := lastPane.pane.webView.Widget(); lastPaneWidget != 0 {
701+ log.Printf("[workspace] Ensuring widget visibility after reparenting")
702+ webkit.WidgetShow(lastPaneWidget)
703+ webkit.WidgetGrabFocus(lastPaneWidget)
704+
705+ // Log widget state after focus operations
706+ visible := webkit.WidgetGetVisible(lastPaneWidget)
707+ log.Printf("[workspace] Widget state after focus: widget=%p visible=%v",
708+ unsafe.Pointer(lastPaneWidget), visible)
709+ }
710+
711+ log.Printf("[workspace] DEBUG: applied finalizeStackCreation focus logic in reverse")
712+ } else {
713+ log.Printf("[workspace] DEBUG: not focusing remaining pane (shouldFocus=false)")
642 }714 }
643 715
644 log.Printf("[workspace] converted single-pane stack back to regular pane")716 log.Printf("[workspace] converted single-pane stack back to regular pane")
M internal/app/browser/workspace_manager.go +3 -340
@@ -1211,349 +1211,12 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
12111211 return newLeaf, nil
12121212 }
12131213
1214-// stackPane creates a new pane stacked on top of the target pane.
1215-// Similar to splitNode but creates a stack container instead of a paned widget.
1216-func (wm *WorkspaceManager) stackPane(target *paneNode) (*paneNode, error) {
1217- if target == nil || !target.isLeaf || target.pane == nil {
1218- return nil, errors.New("stack target must be a leaf pane")
1219- }
1220-
1221- newView, err := wm.createWebView()
1222- if err != nil {
1223- return nil, err
1224- }
1225-
1226- newPane, err := wm.createPane(newView)
1227- if err != nil {
1228- return nil, err
1229- }
1230-
1231- if handler := newPane.MessageHandler(); handler != nil {
1232- handler.SetWorkspaceObserver(wm)
1233- }
1234-
1235- newContainer := newPane.webView.RootWidget()
1236- if newContainer == 0 {
1237- return nil, errors.New("new pane missing container")
1238- }
1239- webkit.WidgetSetHExpand(newContainer, true)
1240- webkit.WidgetSetVExpand(newContainer, true)
1241- webkit.WidgetRealizeInContainer(newContainer)
1242-
1243- var stackNode *paneNode
1244- var insertIndex int
1245-
1246- // Check if target is already in a stack
1247- if target.parent != nil && target.parent.isStacked {
1248- // Target is already in a stack - find the stack container and insertion point
1249- stackNode = target.parent
1250-
1251- // Find the current position of the target pane in the stack
1252- currentIndex := -1
1253- for i, pane := range stackNode.stackedPanes {
1254- if pane == target {
1255- currentIndex = i
1256- break
1257- }
1258- }
1259-
1260- if currentIndex == -1 {
1261- return nil, errors.New("target pane not found in stack")
1262- }
1263-
1264- // Insert the new pane right after the current pane
1265- insertIndex = currentIndex + 1
1266- log.Printf("[workspace] adding to existing stack: currentIndex=%d insertIndex=%d stackSize=%d",
1267- currentIndex, insertIndex, len(stackNode.stackedPanes))
1268- } else if target.isStacked {
1269- // This shouldn't happen with current structure, but handle it
1270- return nil, errors.New("target pane has inconsistent stacking state")
1271- } else {
1272- // Target is not stacked - create initial stack
1273- log.Printf("[workspace] converting pane to stacked: %p", target)
1274-
1275- // Create the wrapper container - this is what will be used by splitNode
1276- stackWrapperContainer := webkit.NewBox(webkit.OrientationVertical, 0)
1277- if stackWrapperContainer == 0 {
1278- return nil, errors.New("failed to create stack wrapper container")
1279- }
1280- webkit.WidgetSetHExpand(stackWrapperContainer, true)
1281- webkit.WidgetSetVExpand(stackWrapperContainer, true)
1282-
1283- // Create the internal box for the actual stacked widgets (titles + webviews)
1284- stackInternalBox := webkit.NewBox(webkit.OrientationVertical, 0)
1285- if stackInternalBox == 0 {
1286- return nil, errors.New("failed to create stack internal box")
1287- }
1288- webkit.WidgetSetHExpand(stackInternalBox, true)
1289- webkit.WidgetSetVExpand(stackInternalBox, true)
1290-
1291- // The internal box goes inside the wrapper
1292- webkit.BoxAppend(stackWrapperContainer, stackInternalBox)
1293-
1294- // Get the existing container and parent info
1295- existingContainer := target.container
1296- parent := target.parent
1297-
1298- // Create title bar for the existing pane
1299- titleBar := wm.createTitleBar(target)
1300-
1301- // Keep existing container visible during transition to prevent rendering glitch
1302- // webkit.WidgetSetVisible(existingContainer, false) // REMOVED - causes rendering block
1303- webkit.WidgetSetVisible(titleBar, false)
1304-
1305- // Detach existing container from its current parent first
1306- // This is necessary before we can add it to the stack
1307- if parent == nil {
1308- // Target is the root - remove it from window
1309- if wm.window != nil {
1310- wm.window.SetChild(0)
1311- }
1312- // Unparent if it has a GTK parent
1313- if webkit.WidgetGetParent(existingContainer) != 0 {
1314- webkit.WidgetUnparent(existingContainer)
1315- }
1316- } else if parent.container != 0 {
1317- // Target has a parent paned - remove it (automatically unparents in GTK4)
1318- if parent.left == target {
1319- webkit.PanedSetStartChild(parent.container, 0)
1320- } else if parent.right == target {
1321- webkit.PanedSetEndChild(parent.container, 0)
1322- }
1323- }
1324-
1325- // Build the complete stack structure with hidden widgets
1326- webkit.BoxAppend(stackInternalBox, titleBar)
1327- webkit.BoxAppend(stackInternalBox, existingContainer)
1328-
1329- // Immediately reattach the stack wrapper to minimize visibility gap
1330- if parent == nil {
1331- if wm.window != nil {
1332- wm.window.SetChild(stackWrapperContainer)
1333- }
1334- } else if parent.container != 0 {
1335- if parent.left == target {
1336- webkit.PanedSetStartChild(parent.container, stackWrapperContainer)
1337- } else if parent.right == target {
1338- webkit.PanedSetEndChild(parent.container, stackWrapperContainer)
1339- }
1340- }
1341-
1342- // Convert target to a stacked leaf node
1343- target.isStacked = true
1344- target.isLeaf = true
1345- target.container = existingContainer
1346- target.titleBar = titleBar
1347-
1348- // Create the stack container node - container points to wrapper, stackWrapper points to internal box
1349- stackNode = &paneNode{
1350- isStacked: true,
1351- isLeaf: false,
1352- container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.)
1353- stackWrapper: stackInternalBox, // Internal box for stack operations
1354- stackedPanes: []*paneNode{target},
1355- activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0)
1356- parent: parent,
1357- }
1358-
1359- // Update target's parent to be the stack node
1360- target.parent = stackNode
1361-
1362- // Update parent references (GTK operations already done above)
1363- if parent == nil {
1364- wm.root = stackNode
1365- } else {
1366- if parent.left == target {
1367- parent.left = stackNode
1368- } else if parent.right == target {
1369- parent.right = stackNode
1370- }
1371- }
1372-
1373- // New pane will be inserted at index 1 (after the original pane)
1374- insertIndex = 1
1375- }
1376-
1377- // Create new leaf node for the new pane
1378- newLeaf := &paneNode{
1379- pane: newPane,
1380- parent: stackNode,
1381- container: newContainer,
1382- isLeaf: true,
1383- }
1384-
1385- // Create title bar for the new pane
1386- newTitleBar := wm.createTitleBar(newLeaf)
1387- newLeaf.titleBar = newTitleBar
1388-
1389- // Keep new container hidden initially - will be shown after transition
1390- webkit.WidgetSetVisible(newContainer, false)
1391- webkit.WidgetSetVisible(newTitleBar, false)
1392-
1393- // Insert the new pane at the correct position in the slice
1394- stackNode.stackedPanes = append(stackNode.stackedPanes, nil) // Expand slice
1395- copy(stackNode.stackedPanes[insertIndex+1:], stackNode.stackedPanes[insertIndex:]) // Shift elements
1396- stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane
1397-
1398- // Unparent the new container before adding it to the stack (only if it has a parent)
1399- if webkit.WidgetGetParent(newContainer) != 0 {
1400- webkit.WidgetUnparent(newContainer)
1401- }
1402-
1403- // Add the new widgets to the internal stack box (not the wrapper)
1404- // GTK will handle the visual order correctly as long as we manage visibility properly
1405- webkit.BoxAppend(stackNode.stackWrapper, newTitleBar)
1406- webkit.BoxAppend(stackNode.stackWrapper, newContainer)
1407-
1408- // Update workspace state first
1409- wm.viewToNode[newPane.webView] = newLeaf
1410- wm.ensureHover(newLeaf)
1411- wm.app.panes = append(wm.app.panes, newPane)
1412- if newPane.zoomController != nil {
1413- newPane.zoomController.ApplyInitialZoom()
1414- }
1415-
1416- // Update CSS classes
1417- wm.ensurePaneBaseClasses()
1418-
1419- // Mark stack operation timestamp to prevent focus conflicts
1420- wm.lastStackOperation = time.Now()
1421-
1422- // Update stack visibility to show current state (existing pane still active)
1423- wm.stackedPaneManager.UpdateStackVisibility(stackNode)
1424-
1425- // Transition to the new pane after a brief delay to avoid rendering conflicts
1426- webkit.IdleAdd(func() bool {
1427- // Now switch to the new pane
1428- stackNode.activeStackIndex = insertIndex
1429- wm.stackedPaneManager.UpdateStackVisibility(stackNode)
1430- wm.currentlyFocused = newLeaf
1431- wm.focusManager.SetActivePane(newLeaf)
1432- return false // Remove idle callback
1433- })
1434-
1435- log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
1436- stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)
1437- return newLeaf, nil
1438-}
1439-
14401214 // UpdateTitleBar updates the title bar label for a WebView in stacked panes
14411215 func (wm *WorkspaceManager) UpdateTitleBar(webView *webkit.WebView, title string) {
1442- if wm == nil || webView == nil || title == "" {
1443- return
1444- }
1445-
1446- // Find the pane node for this WebView
1447- node, exists := wm.viewToNode[webView]
1448- if !exists || node == nil || !node.isLeaf {
1449- return
1216+ // Delegate to StackedPaneManager which has the correct logic
1217+ if wm.stackedPaneManager != nil {
1218+ wm.stackedPaneManager.UpdateTitleBar(webView, title)
14501219 }
1451-
1452- // Check if this pane is in a stack and has a title bar
1453- if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {
1454- // Update the title bar label with the new title
1455- wm.updateTitleBarLabel(node, title)
1456- log.Printf("[workspace] updated title bar for WebView %s: %s", webView.ID(), title)
1457- }
1458-}
1459-
1460-// updateTitleBarLabel updates the label widget within a title bar by recreating it
1461-func (wm *WorkspaceManager) updateTitleBarLabel(node *paneNode, title string) {
1462- if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked {
1463- return
1464- }
1465-
1466- // Format the title for display
1467- displayTitle := title
1468- if len(displayTitle) > 50 {
1469- displayTitle = displayTitle[:47] + "..."
1470- }
1471-
1472- // Recreate the title bar with the new title
1473- stackNode := node.parent
1474- oldTitleBar := node.titleBar
1475-
1476- // Create new title bar with updated title
1477- newTitleBar := wm.createTitleBarWithTitle(node, displayTitle)
1478- if newTitleBar == 0 {
1479- log.Printf("[workspace] failed to create new title bar")
1480- return
1481- }
1482-
1483- // Replace the old title bar in the internal stack box (not the wrapper)
1484- // Insert the new one after the old one, then remove the old one
1485- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newTitleBar, oldTitleBar)
1486- webkit.BoxRemove(stackNode.stackWrapper, oldTitleBar)
1487-
1488- // Update the node reference
1489- node.titleBar = newTitleBar
1490-
1491- log.Printf("[workspace] updated title bar label: %s", displayTitle)
1492-}
1493-
1494-// createTitleBarWithTitle creates a title bar with a specific title
1495-func (wm *WorkspaceManager) createTitleBarWithTitle(pane *paneNode, title string) uintptr {
1496- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
1497- if titleBox == 0 {
1498- log.Printf("[workspace] failed to create title bar box")
1499- return 0
1500- }
1501- webkit.WidgetSetHExpand(titleBox, true)
1502- webkit.WidgetSetVExpand(titleBox, false)
1503-
1504- // Use the provided title
1505- displayTitle := title
1506- if displayTitle == "" {
1507- displayTitle = "New Tab"
1508- }
1509-
1510- label := webkit.NewLabel(displayTitle)
1511- if label != 0 {
1512- webkit.LabelSetEllipsize(label, webkit.EllipsizeEnd)
1513- webkit.WidgetSetHExpand(label, true)
1514- webkit.BoxAppend(titleBox, label)
1515- }
1516-
1517- // Add CSS classes
1518- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
1519- webkit.WidgetAddCSSClass(label, "stacked-pane-title-text")
1520-
1521- log.Printf("[workspace] created title bar with custom title: box=%#x label=%#x title=%s", titleBox, label, displayTitle)
1522- return titleBox
1523-}
1524-
1525-// createTitleBar creates a title bar widget for a pane in a stack.
1526-func (wm *WorkspaceManager) createTitleBar(pane *paneNode) uintptr {
1527- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
1528- if titleBox == 0 {
1529- log.Printf("[workspace] failed to create title bar box")
1530- return 0
1531- }
1532-
1533- webkit.WidgetSetHExpand(titleBox, true)
1534- webkit.WidgetSetVExpand(titleBox, false)
1535-
1536- // Create label for page title and domain
1537- title := "New Tab"
1538- if pane.pane != nil && pane.pane.webView != nil {
1539- if pageTitle := pane.pane.webView.GetTitle(); pageTitle != "" {
1540- title = pageTitle
1541- }
1542- }
1543-
1544- label := webkit.NewLabel(title)
1545- if label != 0 {
1546- webkit.LabelSetEllipsize(label, webkit.EllipsizeEnd)
1547- webkit.WidgetSetHExpand(label, true)
1548- webkit.BoxAppend(titleBox, label)
1549- }
1550-
1551- // Add CSS classes
1552- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
1553- webkit.WidgetAddCSSClass(label, "stacked-pane-title-text")
1554-
1555- log.Printf("[workspace] created title bar: box=%#x label=%#x title=%s", titleBox, label, title)
1556- return titleBox
15571220 }
15581221
15591222 // updateStackVisibility updates the visibility of panes in a stack.
@@ -1211,349 +1211,12 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
1211 return newLeaf, nil1211 return newLeaf, nil
1212 }1212 }
1213 1213
1214-// stackPane creates a new pane stacked on top of the target pane.
1215-// Similar to splitNode but creates a stack container instead of a paned widget.
1216-func (wm *WorkspaceManager) stackPane(target *paneNode) (*paneNode, error) {
1217- if target == nil || !target.isLeaf || target.pane == nil {
1218- return nil, errors.New("stack target must be a leaf pane")
1219- }
1220-
1221- newView, err := wm.createWebView()
1222- if err != nil {
1223- return nil, err
1224- }
1225-
1226- newPane, err := wm.createPane(newView)
1227- if err != nil {
1228- return nil, err
1229- }
1230-
1231- if handler := newPane.MessageHandler(); handler != nil {
1232- handler.SetWorkspaceObserver(wm)
1233- }
1234-
1235- newContainer := newPane.webView.RootWidget()
1236- if newContainer == 0 {
1237- return nil, errors.New("new pane missing container")
1238- }
1239- webkit.WidgetSetHExpand(newContainer, true)
1240- webkit.WidgetSetVExpand(newContainer, true)
1241- webkit.WidgetRealizeInContainer(newContainer)
1242-
1243- var stackNode *paneNode
1244- var insertIndex int
1245-
1246- // Check if target is already in a stack
1247- if target.parent != nil && target.parent.isStacked {
1248- // Target is already in a stack - find the stack container and insertion point
1249- stackNode = target.parent
1250-
1251- // Find the current position of the target pane in the stack
1252- currentIndex := -1
1253- for i, pane := range stackNode.stackedPanes {
1254- if pane == target {
1255- currentIndex = i
1256- break
1257- }
1258- }
1259-
1260- if currentIndex == -1 {
1261- return nil, errors.New("target pane not found in stack")
1262- }
1263-
1264- // Insert the new pane right after the current pane
1265- insertIndex = currentIndex + 1
1266- log.Printf("[workspace] adding to existing stack: currentIndex=%d insertIndex=%d stackSize=%d",
1267- currentIndex, insertIndex, len(stackNode.stackedPanes))
1268- } else if target.isStacked {
1269- // This shouldn't happen with current structure, but handle it
1270- return nil, errors.New("target pane has inconsistent stacking state")
1271- } else {
1272- // Target is not stacked - create initial stack
1273- log.Printf("[workspace] converting pane to stacked: %p", target)
1274-
1275- // Create the wrapper container - this is what will be used by splitNode
1276- stackWrapperContainer := webkit.NewBox(webkit.OrientationVertical, 0)
1277- if stackWrapperContainer == 0 {
1278- return nil, errors.New("failed to create stack wrapper container")
1279- }
1280- webkit.WidgetSetHExpand(stackWrapperContainer, true)
1281- webkit.WidgetSetVExpand(stackWrapperContainer, true)
1282-
1283- // Create the internal box for the actual stacked widgets (titles + webviews)
1284- stackInternalBox := webkit.NewBox(webkit.OrientationVertical, 0)
1285- if stackInternalBox == 0 {
1286- return nil, errors.New("failed to create stack internal box")
1287- }
1288- webkit.WidgetSetHExpand(stackInternalBox, true)
1289- webkit.WidgetSetVExpand(stackInternalBox, true)
1290-
1291- // The internal box goes inside the wrapper
1292- webkit.BoxAppend(stackWrapperContainer, stackInternalBox)
1293-
1294- // Get the existing container and parent info
1295- existingContainer := target.container
1296- parent := target.parent
1297-
1298- // Create title bar for the existing pane
1299- titleBar := wm.createTitleBar(target)
1300-
1301- // Keep existing container visible during transition to prevent rendering glitch
1302- // webkit.WidgetSetVisible(existingContainer, false) // REMOVED - causes rendering block
1303- webkit.WidgetSetVisible(titleBar, false)
1304-
1305- // Detach existing container from its current parent first
1306- // This is necessary before we can add it to the stack
1307- if parent == nil {
1308- // Target is the root - remove it from window
1309- if wm.window != nil {
1310- wm.window.SetChild(0)
1311- }
1312- // Unparent if it has a GTK parent
1313- if webkit.WidgetGetParent(existingContainer) != 0 {
1314- webkit.WidgetUnparent(existingContainer)
1315- }
1316- } else if parent.container != 0 {
1317- // Target has a parent paned - remove it (automatically unparents in GTK4)
1318- if parent.left == target {
1319- webkit.PanedSetStartChild(parent.container, 0)
1320- } else if parent.right == target {
1321- webkit.PanedSetEndChild(parent.container, 0)
1322- }
1323- }
1324-
1325- // Build the complete stack structure with hidden widgets
1326- webkit.BoxAppend(stackInternalBox, titleBar)
1327- webkit.BoxAppend(stackInternalBox, existingContainer)
1328-
1329- // Immediately reattach the stack wrapper to minimize visibility gap
1330- if parent == nil {
1331- if wm.window != nil {
1332- wm.window.SetChild(stackWrapperContainer)
1333- }
1334- } else if parent.container != 0 {
1335- if parent.left == target {
1336- webkit.PanedSetStartChild(parent.container, stackWrapperContainer)
1337- } else if parent.right == target {
1338- webkit.PanedSetEndChild(parent.container, stackWrapperContainer)
1339- }
1340- }
1341-
1342- // Convert target to a stacked leaf node
1343- target.isStacked = true
1344- target.isLeaf = true
1345- target.container = existingContainer
1346- target.titleBar = titleBar
1347-
1348- // Create the stack container node - container points to wrapper, stackWrapper points to internal box
1349- stackNode = &paneNode{
1350- isStacked: true,
1351- isLeaf: false,
1352- container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.)
1353- stackWrapper: stackInternalBox, // Internal box for stack operations
1354- stackedPanes: []*paneNode{target},
1355- activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0)
1356- parent: parent,
1357- }
1358-
1359- // Update target's parent to be the stack node
1360- target.parent = stackNode
1361-
1362- // Update parent references (GTK operations already done above)
1363- if parent == nil {
1364- wm.root = stackNode
1365- } else {
1366- if parent.left == target {
1367- parent.left = stackNode
1368- } else if parent.right == target {
1369- parent.right = stackNode
1370- }
1371- }
1372-
1373- // New pane will be inserted at index 1 (after the original pane)
1374- insertIndex = 1
1375- }
1376-
1377- // Create new leaf node for the new pane
1378- newLeaf := &paneNode{
1379- pane: newPane,
1380- parent: stackNode,
1381- container: newContainer,
1382- isLeaf: true,
1383- }
1384-
1385- // Create title bar for the new pane
1386- newTitleBar := wm.createTitleBar(newLeaf)
1387- newLeaf.titleBar = newTitleBar
1388-
1389- // Keep new container hidden initially - will be shown after transition
1390- webkit.WidgetSetVisible(newContainer, false)
1391- webkit.WidgetSetVisible(newTitleBar, false)
1392-
1393- // Insert the new pane at the correct position in the slice
1394- stackNode.stackedPanes = append(stackNode.stackedPanes, nil) // Expand slice
1395- copy(stackNode.stackedPanes[insertIndex+1:], stackNode.stackedPanes[insertIndex:]) // Shift elements
1396- stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane
1397-
1398- // Unparent the new container before adding it to the stack (only if it has a parent)
1399- if webkit.WidgetGetParent(newContainer) != 0 {
1400- webkit.WidgetUnparent(newContainer)
1401- }
1402-
1403- // Add the new widgets to the internal stack box (not the wrapper)
1404- // GTK will handle the visual order correctly as long as we manage visibility properly
1405- webkit.BoxAppend(stackNode.stackWrapper, newTitleBar)
1406- webkit.BoxAppend(stackNode.stackWrapper, newContainer)
1407-
1408- // Update workspace state first
1409- wm.viewToNode[newPane.webView] = newLeaf
1410- wm.ensureHover(newLeaf)
1411- wm.app.panes = append(wm.app.panes, newPane)
1412- if newPane.zoomController != nil {
1413- newPane.zoomController.ApplyInitialZoom()
1414- }
1415-
1416- // Update CSS classes
1417- wm.ensurePaneBaseClasses()
1418-
1419- // Mark stack operation timestamp to prevent focus conflicts
1420- wm.lastStackOperation = time.Now()
1421-
1422- // Update stack visibility to show current state (existing pane still active)
1423- wm.stackedPaneManager.UpdateStackVisibility(stackNode)
1424-
1425- // Transition to the new pane after a brief delay to avoid rendering conflicts
1426- webkit.IdleAdd(func() bool {
1427- // Now switch to the new pane
1428- stackNode.activeStackIndex = insertIndex
1429- wm.stackedPaneManager.UpdateStackVisibility(stackNode)
1430- wm.currentlyFocused = newLeaf
1431- wm.focusManager.SetActivePane(newLeaf)
1432- return false // Remove idle callback
1433- })
1434-
1435- log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
1436- stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)
1437- return newLeaf, nil
1438-}
1439-
1440 // UpdateTitleBar updates the title bar label for a WebView in stacked panes1214 // UpdateTitleBar updates the title bar label for a WebView in stacked panes
1441 func (wm *WorkspaceManager) UpdateTitleBar(webView *webkit.WebView, title string) {1215 func (wm *WorkspaceManager) UpdateTitleBar(webView *webkit.WebView, title string) {
1442- if wm == nil || webView == nil || title == "" {1216+ // Delegate to StackedPaneManager which has the correct logic
1443- return1217+ if wm.stackedPaneManager != nil {
1444- }1218+ wm.stackedPaneManager.UpdateTitleBar(webView, title)
1445-
1446- // Find the pane node for this WebView
1447- node, exists := wm.viewToNode[webView]
1448- if !exists || node == nil || !node.isLeaf {
1449- return
1450 }1219 }
1451-
1452- // Check if this pane is in a stack and has a title bar
1453- if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {
1454- // Update the title bar label with the new title
1455- wm.updateTitleBarLabel(node, title)
1456- log.Printf("[workspace] updated title bar for WebView %s: %s", webView.ID(), title)
1457- }
1458-}
1459-
1460-// updateTitleBarLabel updates the label widget within a title bar by recreating it
1461-func (wm *WorkspaceManager) updateTitleBarLabel(node *paneNode, title string) {
1462- if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked {
1463- return
1464- }
1465-
1466- // Format the title for display
1467- displayTitle := title
1468- if len(displayTitle) > 50 {
1469- displayTitle = displayTitle[:47] + "..."
1470- }
1471-
1472- // Recreate the title bar with the new title
1473- stackNode := node.parent
1474- oldTitleBar := node.titleBar
1475-
1476- // Create new title bar with updated title
1477- newTitleBar := wm.createTitleBarWithTitle(node, displayTitle)
1478- if newTitleBar == 0 {
1479- log.Printf("[workspace] failed to create new title bar")
1480- return
1481- }
1482-
1483- // Replace the old title bar in the internal stack box (not the wrapper)
1484- // Insert the new one after the old one, then remove the old one
1485- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newTitleBar, oldTitleBar)
1486- webkit.BoxRemove(stackNode.stackWrapper, oldTitleBar)
1487-
1488- // Update the node reference
1489- node.titleBar = newTitleBar
1490-
1491- log.Printf("[workspace] updated title bar label: %s", displayTitle)
1492-}
1493-
1494-// createTitleBarWithTitle creates a title bar with a specific title
1495-func (wm *WorkspaceManager) createTitleBarWithTitle(pane *paneNode, title string) uintptr {
1496- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
1497- if titleBox == 0 {
1498- log.Printf("[workspace] failed to create title bar box")
1499- return 0
1500- }
1501- webkit.WidgetSetHExpand(titleBox, true)
1502- webkit.WidgetSetVExpand(titleBox, false)
1503-
1504- // Use the provided title
1505- displayTitle := title
1506- if displayTitle == "" {
1507- displayTitle = "New Tab"
1508- }
1509-
1510- label := webkit.NewLabel(displayTitle)
1511- if label != 0 {
1512- webkit.LabelSetEllipsize(label, webkit.EllipsizeEnd)
1513- webkit.WidgetSetHExpand(label, true)
1514- webkit.BoxAppend(titleBox, label)
1515- }
1516-
1517- // Add CSS classes
1518- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
1519- webkit.WidgetAddCSSClass(label, "stacked-pane-title-text")
1520-
1521- log.Printf("[workspace] created title bar with custom title: box=%#x label=%#x title=%s", titleBox, label, displayTitle)
1522- return titleBox
1523-}
1524-
1525-// createTitleBar creates a title bar widget for a pane in a stack.
1526-func (wm *WorkspaceManager) createTitleBar(pane *paneNode) uintptr {
1527- titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8)
1528- if titleBox == 0 {
1529- log.Printf("[workspace] failed to create title bar box")
1530- return 0
1531- }
1532-
1533- webkit.WidgetSetHExpand(titleBox, true)
1534- webkit.WidgetSetVExpand(titleBox, false)
1535-
1536- // Create label for page title and domain
1537- title := "New Tab"
1538- if pane.pane != nil && pane.pane.webView != nil {
1539- if pageTitle := pane.pane.webView.GetTitle(); pageTitle != "" {
1540- title = pageTitle
1541- }
1542- }
1543-
1544- label := webkit.NewLabel(title)
1545- if label != 0 {
1546- webkit.LabelSetEllipsize(label, webkit.EllipsizeEnd)
1547- webkit.WidgetSetHExpand(label, true)
1548- webkit.BoxAppend(titleBox, label)
1549- }
1550-
1551- // Add CSS classes
1552- webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title")
1553- webkit.WidgetAddCSSClass(label, "stacked-pane-title-text")
1554-
1555- log.Printf("[workspace] created title bar: box=%#x label=%#x title=%s", titleBox, label, title)
1556- return titleBox
1557 }1220 }
1558 1221
1559 // updateStackVisibility updates the visibility of panes in a stack.1222 // updateStackVisibility updates the visibility of panes in a stack.
M internal/app/browser/workspace_test.go +10 -10
@@ -92,7 +92,7 @@ func (twm *TestWorkspaceManager) createTestStack(t *testing.T, numPanes int) *pa
9292 target := twm.root
9393
9494 // Stack the root pane to create the first stack with 2 panes
95- newLeaf, err := twm.stackPane(target)
95+ newLeaf, err := twm.stackedPaneManager.StackPane(target)
9696 if err != nil {
9797 t.Fatalf("Failed to create initial stack: %v", err)
9898 }
@@ -107,7 +107,7 @@ func (twm *TestWorkspaceManager) createTestStack(t *testing.T, numPanes int) *pa
107107 // Add additional panes to the existing stack
108108 for i := 2; i < numPanes; i++ {
109109 // Create a new pane by stacking one of the existing panes in the stack
110- additionalPane, err := twm.stackPane(stackContainer.stackedPanes[0])
110+ additionalPane, err := twm.stackedPaneManager.StackPane(stackContainer.stackedPanes[0])
111111 if err != nil {
112112 t.Fatalf("Failed to stack pane %d: %v", i, err)
113113 }
@@ -324,7 +324,7 @@ func TestStackCreation(t *testing.T) {
324324 }
325325
326326 // Create first stack
327- newPane, err := twm.stackPane(twm.root)
327+ newPane, err := twm.stackedPaneManager.StackPane(twm.root)
328328 if err != nil {
329329 t.Fatalf("Failed to create stack: %v", err)
330330 }
@@ -779,7 +779,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
779779 t.Fatalf("Failed to create split for second stack: %v", err)
780780 }
781781
782- stackB, err := twm.stackPane(newPane)
782+ stackB, err := twm.stackedPaneManager.StackPane(newPane)
783783 if err != nil {
784784 t.Fatalf("Failed to create second stack: %v", err)
785785 }
@@ -830,7 +830,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
830830 }
831831
832832 // Create second stack
833- stackB, err := twm.stackPane(rightPane)
833+ stackB, err := twm.stackedPaneManager.StackPane(rightPane)
834834 if err != nil {
835835 t.Fatalf("Failed to create stack B: %v", err)
836836 }
@@ -875,7 +875,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
875875 }
876876
877877 // Create stackB on right
878- stackB, err := twm.stackPane(rightPane)
878+ stackB, err := twm.stackedPaneManager.StackPane(rightPane)
879879 if err != nil {
880880 t.Fatalf("Failed to create stackB: %v", err)
881881 }
@@ -887,7 +887,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
887887 t.Fatalf("Failed to split stackB: %v", err)
888888 }
889889
890- stackC, err := twm.stackPane(bottomPane)
890+ stackC, err := twm.stackedPaneManager.StackPane(bottomPane)
891891 if err != nil {
892892 t.Fatalf("Failed to create stackC: %v", err)
893893 }
@@ -1031,7 +1031,7 @@ func TestUserScenarioCtrlPSCtrlPR(t *testing.T) {
10311031
10321032 // Step 2: Ctrl+P S - Stack the root pane (simulates user's Ctrl+P S)
10331033 rootPaneContainer := twm.root.container
1034- _, err := twm.stackPane(twm.root)
1034+ _, err := twm.stackedPaneManager.StackPane(twm.root)
10351035 if err != nil {
10361036 t.Fatalf("Stack pane failed: %v", err)
10371037 }
@@ -1114,7 +1114,7 @@ func TestUserScenarioCtrlPSCtrlPR(t *testing.T) {
11141114 }
11151115
11161116 // Step 2: Ctrl+P S - Stack the root pane
1117- _, err := twm.stackPane(twm.root)
1117+ _, err := twm.stackedPaneManager.StackPane(twm.root)
11181118 if err != nil {
11191119 t.Fatalf("Stack pane failed: %v", err)
11201120 }
@@ -1272,7 +1272,7 @@ func TestSiblingPromotionFix(t *testing.T) {
12721272 // When Target is closed, Stack gets promoted to replace Split
12731273
12741274 // Step 1: Create a stack
1275- _, err := twm.stackPane(twm.root)
1275+ _, err := twm.stackedPaneManager.StackPane(twm.root)
12761276 if err != nil {
12771277 t.Fatalf("Stack creation failed: %v", err)
12781278 }
@@ -92,7 +92,7 @@ func (twm *TestWorkspaceManager) createTestStack(t *testing.T, numPanes int) *pa
92 target := twm.root92 target := twm.root
93 93
94 // Stack the root pane to create the first stack with 2 panes94 // Stack the root pane to create the first stack with 2 panes
95- newLeaf, err := twm.stackPane(target)95+ newLeaf, err := twm.stackedPaneManager.StackPane(target)
96 if err != nil {96 if err != nil {
97 t.Fatalf("Failed to create initial stack: %v", err)97 t.Fatalf("Failed to create initial stack: %v", err)
98 }98 }
@@ -107,7 +107,7 @@ func (twm *TestWorkspaceManager) createTestStack(t *testing.T, numPanes int) *pa
107 // Add additional panes to the existing stack107 // Add additional panes to the existing stack
108 for i := 2; i < numPanes; i++ {108 for i := 2; i < numPanes; i++ {
109 // Create a new pane by stacking one of the existing panes in the stack109 // Create a new pane by stacking one of the existing panes in the stack
110- additionalPane, err := twm.stackPane(stackContainer.stackedPanes[0])110+ additionalPane, err := twm.stackedPaneManager.StackPane(stackContainer.stackedPanes[0])
111 if err != nil {111 if err != nil {
112 t.Fatalf("Failed to stack pane %d: %v", i, err)112 t.Fatalf("Failed to stack pane %d: %v", i, err)
113 }113 }
@@ -324,7 +324,7 @@ func TestStackCreation(t *testing.T) {
324 }324 }
325 325
326 // Create first stack326 // Create first stack
327- newPane, err := twm.stackPane(twm.root)327+ newPane, err := twm.stackedPaneManager.StackPane(twm.root)
328 if err != nil {328 if err != nil {
329 t.Fatalf("Failed to create stack: %v", err)329 t.Fatalf("Failed to create stack: %v", err)
330 }330 }
@@ -779,7 +779,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
779 t.Fatalf("Failed to create split for second stack: %v", err)779 t.Fatalf("Failed to create split for second stack: %v", err)
780 }780 }
781 781
782- stackB, err := twm.stackPane(newPane)782+ stackB, err := twm.stackedPaneManager.StackPane(newPane)
783 if err != nil {783 if err != nil {
784 t.Fatalf("Failed to create second stack: %v", err)784 t.Fatalf("Failed to create second stack: %v", err)
785 }785 }
@@ -830,7 +830,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
830 }830 }
831 831
832 // Create second stack832 // Create second stack
833- stackB, err := twm.stackPane(rightPane)833+ stackB, err := twm.stackedPaneManager.StackPane(rightPane)
834 if err != nil {834 if err != nil {
835 t.Fatalf("Failed to create stack B: %v", err)835 t.Fatalf("Failed to create stack B: %v", err)
836 }836 }
@@ -875,7 +875,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
875 }875 }
876 876
877 // Create stackB on right877 // Create stackB on right
878- stackB, err := twm.stackPane(rightPane)878+ stackB, err := twm.stackedPaneManager.StackPane(rightPane)
879 if err != nil {879 if err != nil {
880 t.Fatalf("Failed to create stackB: %v", err)880 t.Fatalf("Failed to create stackB: %v", err)
881 }881 }
@@ -887,7 +887,7 @@ func TestMultipleStacksNavigation(t *testing.T) {
887 t.Fatalf("Failed to split stackB: %v", err)887 t.Fatalf("Failed to split stackB: %v", err)
888 }888 }
889 889
890- stackC, err := twm.stackPane(bottomPane)890+ stackC, err := twm.stackedPaneManager.StackPane(bottomPane)
891 if err != nil {891 if err != nil {
892 t.Fatalf("Failed to create stackC: %v", err)892 t.Fatalf("Failed to create stackC: %v", err)
893 }893 }
@@ -1031,7 +1031,7 @@ func TestUserScenarioCtrlPSCtrlPR(t *testing.T) {
1031 1031
1032 // Step 2: Ctrl+P S - Stack the root pane (simulates user's Ctrl+P S)1032 // Step 2: Ctrl+P S - Stack the root pane (simulates user's Ctrl+P S)
1033 rootPaneContainer := twm.root.container1033 rootPaneContainer := twm.root.container
1034- _, err := twm.stackPane(twm.root)1034+ _, err := twm.stackedPaneManager.StackPane(twm.root)
1035 if err != nil {1035 if err != nil {
1036 t.Fatalf("Stack pane failed: %v", err)1036 t.Fatalf("Stack pane failed: %v", err)
1037 }1037 }
@@ -1114,7 +1114,7 @@ func TestUserScenarioCtrlPSCtrlPR(t *testing.T) {
1114 }1114 }
1115 1115
1116 // Step 2: Ctrl+P S - Stack the root pane1116 // Step 2: Ctrl+P S - Stack the root pane
1117- _, err := twm.stackPane(twm.root)1117+ _, err := twm.stackedPaneManager.StackPane(twm.root)
1118 if err != nil {1118 if err != nil {
1119 t.Fatalf("Stack pane failed: %v", err)1119 t.Fatalf("Stack pane failed: %v", err)
1120 }1120 }
@@ -1272,7 +1272,7 @@ func TestSiblingPromotionFix(t *testing.T) {
1272 // When Target is closed, Stack gets promoted to replace Split1272 // When Target is closed, Stack gets promoted to replace Split
1273 1273
1274 // Step 1: Create a stack1274 // Step 1: Create a stack
1275- _, err := twm.stackPane(twm.root)1275+ _, err := twm.stackedPaneManager.StackPane(twm.root)
1276 if err != nil {1276 if err != nil {
1277 t.Fatalf("Stack creation failed: %v", err)1277 t.Fatalf("Stack creation failed: %v", err)
1278 }1278 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close