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