1// workspace_utils.go - Utility functions and helpers for workspace management
2package browser
3
4import (
5 "fmt"
6 "log"
7
8 "github.com/bnema/dumber/pkg/webkit"
9)
10
11// hasMultiplePanes returns true if there are multiple panes in the workspace
12func (wm *WorkspaceManager) hasMultiplePanes() bool {
13 return wm != nil && wm.app != nil && len(wm.app.panes) > 1
14}
15
16// UpdateTitleBar updates the title bar label for a WebView in stacked panes
17func (wm *WorkspaceManager) UpdateTitleBar(webView *webkit.WebView, title string) {
18 // Delegate to StackedPaneManager which has the correct logic
19 if wm.stackedPaneManager != nil {
20 wm.stackedPaneManager.UpdateTitleBar(webView, title)
21 }
22}
23
24// ensureGUIInPane lazily loads GUI components into a pane when it gains focus
25func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
26 if pane == nil {
27 return
28 }
29
30 // GUI is already injected globally via WebKit's enableUserContentManager
31 if !pane.HasGUI() {
32 pane.SetHasGUI(true)
33 pane.SetGUIComponent("manager", true)
34 pane.SetGUIComponent("omnibox", true)
35 }
36}
37
38// mapDirection maps a direction string to GTK orientation and positioning
39func mapDirection(direction string) (webkit.Orientation, bool) {
40 switch direction {
41 case "left":
42 return webkit.OrientationHorizontal, false
43 case "up":
44 return webkit.OrientationVertical, false
45 case "down":
46 return webkit.OrientationVertical, true
47 default:
48 return webkit.OrientationHorizontal, true
49 }
50}
51
52// Widget management utilities
53
54// initializePaneWidgets sets up widgets for a paneNode
55func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr uintptr) {
56 node.container = containerPtr
57
58 // Add base CSS class to the container
59 if node.container != 0 && webkit.WidgetIsValid(node.container) {
60 webkit.WidgetAddCSSClass(node.container, basePaneClass)
61 }
62
63 // Initialize other widget fields as zero (will be set when needed)
64 node.titleBar = 0
65 node.stackWrapper = 0
66
67 // Initialize cleanup tracking fields
68 node.widgetValid = true
69 node.cleanupGeneration = 0
70}
71
72// setContainer sets the container widget pointer
73func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {
74 _ = typeInfo // Type info kept for API compatibility but not used
75 node.container = ptr
76
77 // Mark widget validity based on container presence so idle guards run for stacks too
78 if ptr != 0 {
79 node.widgetValid = true
80 } else {
81 node.widgetValid = false
82 }
83}
84
85// setTitleBar sets the titleBar widget pointer
86func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) {
87 node.titleBar = ptr
88}
89
90// setStackWrapper sets the stackWrapper widget pointer
91func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) {
92 node.stackWrapper = ptr
93}
94
95// Centralized Active Pane Border Management System
96// Handles all pane types: regular, popup, stacked, split panes
97
98// PaneBorderContext holds the context for applying borders to any pane type
99type PaneBorderContext struct {
100 webViewWidget uintptr // The WebView's native widget (for margin)
101 borderContainer uintptr // The container that gets background color
102 cssClasses []string // Additional CSS classes to apply
103 paneType string // Description for debugging
104}
105
106// determineBorderContext analyzes a pane node and determines the correct border context
107func (wm *WorkspaceManager) determineBorderContext(node *paneNode) *PaneBorderContext {
108 if node == nil {
109 return nil
110 }
111
112 ctx := &PaneBorderContext{}
113
114 // Step 1: Get WebView widget for margin (same for all pane types)
115 if node.pane != nil && node.pane.webView != nil {
116 ctx.webViewWidget = node.pane.webView.RootWidget()
117 }
118
119 // Step 2: Determine the border container based on pane type
120 switch {
121 case node.windowType == webkit.WindowTypePopup:
122 // Popup panes: Border on popup container
123 ctx.borderContainer = node.container
124 ctx.cssClasses = []string{activePaneClass}
125 ctx.paneType = "popup"
126
127 case node.parent != nil && node.parent.isStacked:
128 // Pane in stack: Border on stack wrapper container
129 ctx.borderContainer = node.parent.stackWrapper
130 ctx.cssClasses = []string{activePaneClass}
131 ctx.paneType = "stacked"
132
133 default:
134 // Regular pane: Border on pane container
135 ctx.borderContainer = node.container
136 ctx.cssClasses = []string{activePaneClass}
137 ctx.paneType = "regular"
138 }
139
140 return ctx
141}
142
143// applyActivePaneBorder applies the active pane border using the border context
144func (wm *WorkspaceManager) applyActivePaneBorder(ctx *PaneBorderContext) {
145 if ctx == nil {
146 log.Printf("[workspace] applyActivePaneBorder: nil context")
147 return
148 }
149
150 // Skip applying border if there's only one pane
151 if !wm.hasMultiplePanes() {
152 log.Printf("[workspace] Skipping active border: only one pane exists")
153 return
154 }
155
156 // Apply margin to WebView widget (creates space for border)
157 if ctx.webViewWidget != 0 && webkit.WidgetIsValid(ctx.webViewWidget) {
158 webkit.WidgetSetMargin(ctx.webViewWidget, 2)
159 }
160
161 // Apply CSS classes to border container
162 if ctx.borderContainer != 0 && webkit.WidgetIsValid(ctx.borderContainer) {
163 for _, class := range ctx.cssClasses {
164 webkit.WidgetAddCSSClass(ctx.borderContainer, class)
165 }
166 }
167
168 log.Printf("[workspace] Applied active border: type=%s webView=%#x container=%#x",
169 ctx.paneType, ctx.webViewWidget, ctx.borderContainer)
170}
171
172// removeActivePaneBorder removes the active pane border from a node
173func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
174 if node == nil {
175 return
176 }
177
178 ctx := wm.determineBorderContext(node)
179 if ctx == nil {
180 return
181 }
182
183 // Remove margin from WebView widget
184 if ctx.webViewWidget != 0 && webkit.WidgetIsValid(ctx.webViewWidget) {
185 webkit.WidgetSetMargin(ctx.webViewWidget, 0)
186 }
187
188 // Remove CSS classes from border container
189 if ctx.borderContainer != 0 && webkit.WidgetIsValid(ctx.borderContainer) {
190 for _, class := range ctx.cssClasses {
191 webkit.WidgetRemoveCSSClass(ctx.borderContainer, class)
192 }
193 }
194
195 log.Printf("[workspace] Removed active border: type=%s webView=%#x container=%#x",
196 ctx.paneType, ctx.webViewWidget, ctx.borderContainer)
197}
198
199// Widget cleanup utilities
200
201// cleanupWidget safely invalidates and cleans up a widget
202func (wm *WorkspaceManager) cleanupWidget(ptr uintptr) {
203 if ptr == 0 {
204 return
205 }
206
207 // Widget will be destroyed by GTK when unparented/removed
208 // No manual cleanup needed - GTK4 handles reference counting
209}
210
211// scheduleIdleGuarded queues a callback to run on the GTK main loop and tracks it against
212// the provided pane nodes so it can be cancelled safely if the nodes are destroyed.
213func (wm *WorkspaceManager) scheduleIdleGuarded(fn func() bool, nodes ...*paneNode) uintptr {
214 if wm == nil || fn == nil {
215 return 0
216 }
217 guards := uniquePaneNodes(nodes...)
218 var handle uintptr
219 handle = webkit.IdleAdd(func() bool {
220 defer wm.releaseIdleHandle(handle)
221 for _, node := range guards {
222 if node == nil {
223 continue
224 }
225 if !node.widgetValid {
226 return false
227 }
228 }
229 return fn()
230 })
231 if handle != 0 {
232 wm.registerIdleHandle(handle, guards...)
233 }
234 return handle
235}
236
237func uniquePaneNodes(nodes ...*paneNode) []*paneNode {
238 if len(nodes) == 0 {
239 return nil
240 }
241 seen := make(map[*paneNode]struct{}, len(nodes))
242 unique := make([]*paneNode, 0, len(nodes))
243 for _, node := range nodes {
244 if node == nil {
245 continue
246 }
247 if _, ok := seen[node]; ok {
248 continue
249 }
250 seen[node] = struct{}{}
251 unique = append(unique, node)
252 }
253 return unique
254}
255
256func (wm *WorkspaceManager) registerIdleHandle(handle uintptr, nodes ...*paneNode) {
257 if wm == nil || handle == 0 {
258 return
259 }
260 unique := uniquePaneNodes(nodes...)
261 if len(unique) == 0 {
262 return
263 }
264 if wm.pendingIdle == nil {
265 wm.pendingIdle = make(map[uintptr][]*paneNode)
266 }
267 wm.pendingIdle[handle] = unique
268 for _, node := range unique {
269 if node.pendingIdleHandles == nil {
270 node.pendingIdleHandles = make(map[uintptr]struct{})
271 }
272 node.pendingIdleHandles[handle] = struct{}{}
273 }
274}
275
276func (wm *WorkspaceManager) releaseIdleHandle(handle uintptr) {
277 if wm == nil || handle == 0 {
278 return
279 }
280 if wm.pendingIdle == nil {
281 return
282 }
283 nodes, ok := wm.pendingIdle[handle]
284 if !ok {
285 return
286 }
287 delete(wm.pendingIdle, handle)
288 for _, node := range nodes {
289 if node == nil || node.pendingIdleHandles == nil {
290 continue
291 }
292 delete(node.pendingIdleHandles, handle)
293 if len(node.pendingIdleHandles) == 0 {
294 node.pendingIdleHandles = nil
295 }
296 }
297}
298
299func (wm *WorkspaceManager) cancelIdleHandle(handle uintptr) {
300 if handle == 0 {
301 return
302 }
303 wm.releaseIdleHandle(handle)
304 webkit.IdleRemove(handle)
305}
306
307func (wm *WorkspaceManager) cancelIdleHandles(node *paneNode) {
308 if wm == nil || node == nil || len(node.pendingIdleHandles) == 0 {
309 return
310 }
311 handles := make([]uintptr, 0, len(node.pendingIdleHandles))
312 for handle := range node.pendingIdleHandles {
313 handles = append(handles, handle)
314 }
315 for _, handle := range handles {
316 wm.cancelIdleHandle(handle)
317 }
318}
319
320// Note: nextCleanupGeneration, updateMainPane, dumpTreeState, paneCloseLogf
321// are defined in workspace_pane_ops.go and workspace_debug.go
322
323// formatPaneInfo returns a debug string for a pane node
324func formatPaneInfo(node *paneNode) string {
325 if node == nil {
326 return "nil"
327 }
328
329 var info string
330 if node.isLeaf {
331 info = fmt.Sprintf("leaf(container=%#x", node.container)
332 if node.pane != nil && node.pane.webView != nil {
333 info += fmt.Sprintf(" webview=%s", node.pane.webView.ID())
334 }
335 if node.windowType == webkit.WindowTypePopup {
336 info += " popup"
337 }
338 info += ")"
339 } else if node.isStacked {
340 info = fmt.Sprintf("stack(panes=%d active=%d container=%#x)",
341 len(node.stackedPanes), node.activeStackIndex, node.container)
342 } else {
343 info = fmt.Sprintf("branch(container=%#x orientation=%v)",
344 node.container, node.orientation)
345 }
346
347 return info
348}