1// workspace_pane_mode.go - Backend-driven pane mode implementation
2package browser
3
4import (
5 "log"
6
7 "github.com/bnema/dumber/pkg/webkit"
8)
9
10// EnterPaneMode activates pane mode on the currently focused pane
11// This is called directly from the keyboard bridge, not from JavaScript
12func (wm *WorkspaceManager) EnterPaneMode() {
13 wm.paneMutex.Lock()
14 defer wm.paneMutex.Unlock()
15
16 // Get the currently focused pane
17 activeNode := wm.GetActiveNode()
18 if activeNode == nil {
19 log.Printf("[pane-mode] Cannot enter pane mode: no active pane")
20 return
21 }
22
23 // Already in pane mode on this pane?
24 if wm.paneModeActive && wm.paneModeActivePane == activeNode {
25 log.Printf("[pane-mode] Pane mode already active on this pane")
26 return
27 }
28
29 log.Printf("[pane-mode] Entering pane mode on pane %p", activeNode)
30
31 wm.paneModeActive = true
32 wm.paneModeActivePane = activeNode
33
34 if activeNode.pane != nil && activeNode.pane.webView != nil {
35 wm.paneModeSource = activeNode.pane.webView
36 }
37
38 // Apply visual border to workspace root
39 wm.applyPaneModeBorder()
40
41 // Notify JavaScript for UI feedback (visual indicators)
42 wm.dispatchPaneModeEvent("entered", "")
43}
44
45// ExitPaneMode deactivates pane mode
46func (wm *WorkspaceManager) ExitPaneMode(reason string) {
47 wm.paneMutex.Lock()
48 defer wm.paneMutex.Unlock()
49
50 if !wm.paneModeActive {
51 return
52 }
53
54 log.Printf("[pane-mode] Exiting pane mode: %s", reason)
55
56 wm.paneModeActive = false
57 wm.paneModeActivePane = nil
58 wm.paneModeSource = nil
59
60 // Remove visual border from workspace root
61 wm.removePaneModeBorder()
62
63 // Notify JavaScript
64 wm.dispatchPaneModeEvent("exited", reason)
65}
66
67// HandlePaneAction processes pane mode actions (close, split, etc.)
68// Called from keyboard bridge when user presses action keys in pane mode
69func (wm *WorkspaceManager) HandlePaneAction(action string) {
70 wm.paneMutex.Lock()
71
72 if !wm.paneModeActive {
73 wm.paneMutex.Unlock()
74 log.Printf("[pane-mode] Ignoring action '%s': pane mode not active", action)
75 return
76 }
77
78 activePane := wm.paneModeActivePane
79 wm.paneMutex.Unlock()
80
81 if activePane == nil {
82 log.Printf("[pane-mode] Ignoring action '%s': no active pane", action)
83 wm.ExitPaneMode("no-active-pane")
84 return
85 }
86
87 log.Printf("[pane-mode] Handling action '%s' on pane %p", action, activePane)
88
89 switch action {
90 case "close":
91 wm.handlePaneClose(activePane)
92 case "split-right", "split-left", "split-up", "split-down":
93 wm.handlePaneSplit(activePane, action)
94 case "stack":
95 wm.handlePaneStack(activePane)
96 default:
97 log.Printf("[pane-mode] Unknown action: %s", action)
98 }
99
100 // Exit pane mode after action
101 wm.ExitPaneMode(action)
102}
103
104// handlePaneClose closes the pane that has pane mode active
105func (wm *WorkspaceManager) handlePaneClose(node *paneNode) {
106 if node == nil {
107 return
108 }
109
110 log.Printf("[pane-mode] Closing pane %p", node)
111
112 // Notify JavaScript first so it can show toast
113 wm.dispatchPaneModeEvent("action", "close")
114
115 // Close the pane
116 if err := wm.ClosePane(node); err != nil {
117 log.Printf("[pane-mode] Failed to close pane: %v", err)
118 }
119}
120
121// handlePaneSplit splits the pane in the specified direction
122func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
123 if node == nil {
124 return
125 }
126
127 // Extract direction from action (e.g., "split-right" -> "right")
128 var direction string
129 switch action {
130 case "split-right":
131 direction = "right"
132 case "split-left":
133 direction = "left"
134 case "split-up":
135 direction = "up"
136 case "split-down":
137 direction = "down"
138 default:
139 log.Printf("[pane-mode] Invalid split action: %s", action)
140 return
141 }
142
143 log.Printf("[pane-mode] Splitting pane %p in direction %s", node, direction)
144
145 // Notify JavaScript first so it can show toast
146 wm.dispatchPaneModeEvent("action", action)
147
148 // Perform the split (pass nil so it creates a fresh pane, not popup mode)
149 newNode, err := wm.splitNode(node, direction, nil)
150 if err != nil {
151 log.Printf("[pane-mode] Failed to split pane: %v", err)
152 return
153 }
154
155 // Load initial URL in the new pane so scripts execute
156 wm.clonePaneState(node, newNode)
157
158 log.Printf("[pane-mode] Split successful: direction=%s", direction)
159}
160
161// handlePaneStack creates a stacked pane
162func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
163 if node == nil {
164 return
165 }
166
167 log.Printf("[pane-mode] Stacking pane %p", node)
168
169 // Notify JavaScript first so it can show toast
170 wm.dispatchPaneModeEvent("action", "stack")
171
172 // Perform the stack operation
173 newNode, err := wm.stackedPaneManager.StackPane(node)
174 if err != nil {
175 log.Printf("[pane-mode] Failed to stack pane: %v", err)
176 return
177 }
178
179 // Load initial URL in the new pane so scripts execute
180 wm.clonePaneState(node, newNode)
181
182 log.Printf("[pane-mode] Stack successful")
183}
184
185// dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
186func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
187 if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
188 return
189 }
190
191 view := wm.paneModeActivePane.pane.webView
192 data := map[string]interface{}{
193 "event": event,
194 "detail": detail,
195 }
196
197 if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil {
198 log.Printf("[pane-mode] Failed to dispatch event: %v", err)
199 }
200}
201
202// applyPaneModeBorder applies pane mode visual indicator using GTK margins
203func (wm *WorkspaceManager) applyPaneModeBorder() {
204 if wm.root == nil || wm.root.container == nil || wm.window == nil {
205 return
206 }
207
208 // Save the container reference so we can remove margins from it later
209 // (even if wm.root.container changes due to splits)
210 wm.paneModeContainer = wm.root.container
211
212 // Apply 4px margins to root container to create space for the border
213 webkit.WidgetSetMargin(wm.root.container, 4)
214
215 // Add CSS class to window for background color (the "border" color shows in the margin space)
216 webkit.WidgetAddCSSClass(wm.window.AsWindow(), "pane-mode-active")
217
218 // Force resize/allocation to apply margin changes immediately
219 webkit.WidgetQueueResize(wm.root.container)
220 webkit.WidgetQueueAllocate(wm.root.container)
221
222 // Queue redraw to show changes
223 webkit.WidgetQueueDraw(wm.window.AsWindow())
224 webkit.WidgetQueueDraw(wm.root.container)
225
226 log.Printf("[pane-mode] Applied border to workspace root using margins (container=%p)", wm.root.container)
227}
228
229// removePaneModeBorder removes the pane mode visual indicator
230func (wm *WorkspaceManager) removePaneModeBorder() {
231 if wm.window == nil {
232 return
233 }
234
235 // Use the saved container reference (the one that had margins applied)
236 // This is important because wm.root.container may have changed due to splits
237 container := wm.paneModeContainer
238 if container == nil {
239 log.Printf("[pane-mode] Warning: no saved container, trying current root")
240 if wm.root == nil || wm.root.container == nil {
241 return
242 }
243 container = wm.root.container
244 }
245
246 // Remove margins from the container that had them applied
247 webkit.WidgetSetMargin(container, 0)
248
249 // Remove CSS class from window
250 webkit.WidgetRemoveCSSClass(wm.window.AsWindow(), "pane-mode-active")
251
252 // Force resize/allocation to apply margin changes immediately
253 webkit.WidgetQueueResize(container)
254 webkit.WidgetQueueAllocate(container)
255
256 // Queue redraw to show changes
257 webkit.WidgetQueueDraw(wm.window.AsWindow())
258 webkit.WidgetQueueDraw(container)
259
260 // Clear the saved container reference
261 wm.paneModeContainer = nil
262
263 log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container)
264}