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