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