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