fix(browser): eliminate viewport flicker and GTK CSS errors in active pane borders
- Replace border-based approach with outline for zero layout impact - Fix CSS class management to prevent GTK counting bloom filter errors - Use outline with -2px offset for clean visual focus indication - Add safety checks for CSS class existence before removal - Eliminate viewport width flickering when switching between pane types
- commit
- 7c100fb7dc2ae5780a3fc0e07bc96b6d21290110
- author
- Brice · · 2025-09-27 21:33 UTC
- parent
- 07cfeef
- signature
- unsigned
2 files · +196 -8
M internal/app/browser/workspace_css.go +46 -8
| @@ -3,7 +3,6 @@ package browser | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "fmt" | |
| 6 | - | "log" | |
| 7 | 6 | ||
| 8 | 7 | "github.com/bnema/dumber/internal/config" | |
| 9 | 8 | "github.com/bnema/dumber/pkg/webkit" | |
| @@ -35,12 +34,20 @@ func getStackTitleTextColor(isDark bool) string { | |||
| 35 | 34 | return "#333333" | |
| 36 | 35 | } | |
| 37 | 36 | ||
| 38 | - | // generateStackedPaneCSS generates the CSS for stacked pane title bars | |
| 39 | - | func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |
| 37 | + | // getActivePaneBorderColor returns the border color for active panes based on theme | |
| 38 | + | func getActivePaneBorderColor(isDark bool) string { | |
| 39 | + | if isDark { | |
| 40 | + | return "#4A90E2" // Deeper blue pastel for dark theme | |
| 41 | + | } | |
| 42 | + | return "#87CEEB" // Sky blue pastel for light theme | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes | |
| 46 | + | func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |
| 40 | 47 | cfg := config.Get() | |
| 41 | 48 | styling := cfg.Workspace.Styling | |
| 42 | 49 | ||
| 43 | - | // Get appropriate border colors based on GTK theme preference | |
| 50 | + | // Get appropriate colors based on GTK theme preference | |
| 44 | 51 | var inactiveBorderColor, windowBackgroundColor string | |
| 45 | 52 | isDark := webkit.PrefersDarkTheme() | |
| 46 | 53 | if isDark { | |
| @@ -51,16 +58,32 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||
| 51 | 58 | windowBackgroundColor = "#ffffff" // Light window background | |
| 52 | 59 | } | |
| 53 | 60 | ||
| 61 | + | activeBorderColor := getActivePaneBorderColor(isDark) | |
| 62 | + | ||
| 54 | 63 | css := fmt.Sprintf(`window { | |
| 55 | 64 | background-color: %s; | |
| 56 | 65 | } | |
| 57 | 66 | ||
| 67 | + | /* Active pane border styling using outline (no layout impact) */ | |
| 68 | + | .workspace-pane-active { | |
| 69 | + | outline: 2px solid %s; | |
| 70 | + | outline-offset: -2px; | |
| 71 | + | transition: outline-color %dms ease-in-out; | |
| 72 | + | } | |
| 73 | + | ||
| 58 | 74 | /* Stacked panes styling */ | |
| 59 | 75 | .stacked-pane-container { | |
| 60 | 76 | background-color: %s; | |
| 61 | 77 | border-radius: %dpx; | |
| 62 | 78 | } | |
| 63 | 79 | ||
| 80 | + | /* Active stacked pane container gets the outline */ | |
| 81 | + | .stacked-pane-container.workspace-pane-active { | |
| 82 | + | outline: 2px solid %s; | |
| 83 | + | outline-offset: -2px; | |
| 84 | + | transition: outline-color %dms ease-in-out; | |
| 85 | + | } | |
| 86 | + | ||
| 64 | 87 | .stacked-pane-title { | |
| 65 | 88 | background-color: %s; | |
| 66 | 89 | border-bottom: 1px solid %s; | |
| @@ -87,8 +110,12 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||
| 87 | 110 | /* Collapsed panes are hidden - handled in code via widget visibility */ | |
| 88 | 111 | }`, | |
| 89 | 112 | windowBackgroundColor, // window background | |
| 113 | + | activeBorderColor, // workspace-pane-active border | |
| 114 | + | styling.TransitionDuration, // workspace-pane-active transition | |
| 90 | 115 | windowBackgroundColor, // stacked-pane-container background | |
| 91 | 116 | styling.BorderRadius, // stacked-pane-container border-radius | |
| 117 | + | activeBorderColor, // stacked-pane-container.workspace-pane-active border | |
| 118 | + | styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition | |
| 92 | 119 | getStackTitleBg(isDark), // stacked-pane-title background | |
| 93 | 120 | inactiveBorderColor, // stacked-pane-title border-bottom | |
| 94 | 121 | styling.TransitionDuration, // stacked-pane-title transition | |
| @@ -99,12 +126,23 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||
| 99 | 126 | return css | |
| 100 | 127 | } | |
| 101 | 128 | ||
| 102 | - | // ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes | |
| 103 | - | func (wm *WorkspaceManager) ensureStackedPaneStyles() { | |
| 129 | + | // generateStackedPaneCSS generates the CSS for stacked pane title bars (deprecated - use generateWorkspaceCSS) | |
| 130 | + | func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |
| 131 | + | // Delegate to the new comprehensive CSS generator | |
| 132 | + | return wm.generateWorkspaceCSS() | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // ensureWorkspaceStyles ensures that CSS styles are applied for workspace panes | |
| 136 | + | func (wm *WorkspaceManager) ensureWorkspaceStyles() { | |
| 104 | 137 | if wm == nil || wm.cssInitialized { | |
| 105 | 138 | return | |
| 106 | 139 | } | |
| 107 | - | stackedPaneCSS := wm.generateStackedPaneCSS() | |
| 108 | - | webkit.AddCSSProvider(stackedPaneCSS) | |
| 140 | + | workspaceCSS := wm.generateWorkspaceCSS() | |
| 141 | + | webkit.AddCSSProvider(workspaceCSS) | |
| 109 | 142 | wm.cssInitialized = true | |
| 110 | 143 | } | |
| 144 | + | ||
| 145 | + | // ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes (deprecated - use ensureWorkspaceStyles) | |
| 146 | + | func (wm *WorkspaceManager) ensureStackedPaneStyles() { | |
| 147 | + | wm.ensureWorkspaceStyles() | |
| 148 | + | } | |
| @@ -3,7 +3,6 @@ package browser | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | - | "log" | |||
| 7 | 6 | ||||
| 8 | "github.com/bnema/dumber/internal/config" | 7 | "github.com/bnema/dumber/internal/config" | ||
| 9 | "github.com/bnema/dumber/pkg/webkit" | 8 | "github.com/bnema/dumber/pkg/webkit" | ||
| @@ -35,12 +34,20 @@ func getStackTitleTextColor(isDark bool) string { | |||||
| 35 | return "#333333" | 34 | return "#333333" | ||
| 36 | } | 35 | } | ||
| 37 | 36 | ||||
| 38 | - | // generateStackedPaneCSS generates the CSS for stacked pane title bars | 37 | + | // getActivePaneBorderColor returns the border color for active panes based on theme |
| 39 | - | func (wm *WorkspaceManager) generateStackedPaneCSS() string { | 38 | + | func getActivePaneBorderColor(isDark bool) string { |
| 39 | + | if isDark { | |||
| 40 | + | return "#4A90E2" // Deeper blue pastel for dark theme | |||
| 41 | + | } | |||
| 42 | + | return "#87CEEB" // Sky blue pastel for light theme | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes | |||
| 46 | + | func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 40 | cfg := config.Get() | 47 | cfg := config.Get() | ||
| 41 | styling := cfg.Workspace.Styling | 48 | styling := cfg.Workspace.Styling | ||
| 42 | 49 | ||||
| 43 | - | // Get appropriate border colors based on GTK theme preference | 50 | + | // Get appropriate colors based on GTK theme preference |
| 44 | var inactiveBorderColor, windowBackgroundColor string | 51 | var inactiveBorderColor, windowBackgroundColor string | ||
| 45 | isDark := webkit.PrefersDarkTheme() | 52 | isDark := webkit.PrefersDarkTheme() | ||
| 46 | if isDark { | 53 | if isDark { | ||
| @@ -51,16 +58,32 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||||
| 51 | windowBackgroundColor = "#ffffff" // Light window background | 58 | windowBackgroundColor = "#ffffff" // Light window background | ||
| 52 | } | 59 | } | ||
| 53 | 60 | ||||
| 61 | + | activeBorderColor := getActivePaneBorderColor(isDark) | |||
| 62 | + | ||||
| 54 | css := fmt.Sprintf(`window { | 63 | css := fmt.Sprintf(`window { | ||
| 55 | background-color: %s; | 64 | background-color: %s; | ||
| 56 | } | 65 | } | ||
| 57 | 66 | ||||
| 67 | + | /* Active pane border styling using outline (no layout impact) */ | |||
| 68 | + | .workspace-pane-active { | |||
| 69 | + | outline: 2px solid %s; | |||
| 70 | + | outline-offset: -2px; | |||
| 71 | + | transition: outline-color %dms ease-in-out; | |||
| 72 | + | } | |||
| 73 | + | ||||
| 58 | /* Stacked panes styling */ | 74 | /* Stacked panes styling */ | ||
| 59 | .stacked-pane-container { | 75 | .stacked-pane-container { | ||
| 60 | background-color: %s; | 76 | background-color: %s; | ||
| 61 | border-radius: %dpx; | 77 | border-radius: %dpx; | ||
| 62 | } | 78 | } | ||
| 63 | 79 | ||||
| 80 | + | /* Active stacked pane container gets the outline */ | |||
| 81 | + | .stacked-pane-container.workspace-pane-active { | |||
| 82 | + | outline: 2px solid %s; | |||
| 83 | + | outline-offset: -2px; | |||
| 84 | + | transition: outline-color %dms ease-in-out; | |||
| 85 | + | } | |||
| 86 | + | ||||
| 64 | .stacked-pane-title { | 87 | .stacked-pane-title { | ||
| 65 | background-color: %s; | 88 | background-color: %s; | ||
| 66 | border-bottom: 1px solid %s; | 89 | border-bottom: 1px solid %s; | ||
| @@ -87,8 +110,12 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||||
| 87 | /* Collapsed panes are hidden - handled in code via widget visibility */ | 110 | /* Collapsed panes are hidden - handled in code via widget visibility */ | ||
| 88 | }`, | 111 | }`, | ||
| 89 | windowBackgroundColor, // window background | 112 | windowBackgroundColor, // window background | ||
| 113 | + | activeBorderColor, // workspace-pane-active border | |||
| 114 | + | styling.TransitionDuration, // workspace-pane-active transition | |||
| 90 | windowBackgroundColor, // stacked-pane-container background | 115 | windowBackgroundColor, // stacked-pane-container background | ||
| 91 | styling.BorderRadius, // stacked-pane-container border-radius | 116 | styling.BorderRadius, // stacked-pane-container border-radius | ||
| 117 | + | activeBorderColor, // stacked-pane-container.workspace-pane-active border | |||
| 118 | + | styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition | |||
| 92 | getStackTitleBg(isDark), // stacked-pane-title background | 119 | getStackTitleBg(isDark), // stacked-pane-title background | ||
| 93 | inactiveBorderColor, // stacked-pane-title border-bottom | 120 | inactiveBorderColor, // stacked-pane-title border-bottom | ||
| 94 | styling.TransitionDuration, // stacked-pane-title transition | 121 | styling.TransitionDuration, // stacked-pane-title transition | ||
| @@ -99,12 +126,23 @@ func (wm *WorkspaceManager) generateStackedPaneCSS() string { | |||||
| 99 | return css | 126 | return css | ||
| 100 | } | 127 | } | ||
| 101 | 128 | ||||
| 102 | - | // ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes | 129 | + | // generateStackedPaneCSS generates the CSS for stacked pane title bars (deprecated - use generateWorkspaceCSS) |
| 103 | - | func (wm *WorkspaceManager) ensureStackedPaneStyles() { | 130 | + | func (wm *WorkspaceManager) generateStackedPaneCSS() string { |
| 131 | + | // Delegate to the new comprehensive CSS generator | |||
| 132 | + | return wm.generateWorkspaceCSS() | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // ensureWorkspaceStyles ensures that CSS styles are applied for workspace panes | |||
| 136 | + | func (wm *WorkspaceManager) ensureWorkspaceStyles() { | |||
| 104 | if wm == nil || wm.cssInitialized { | 137 | if wm == nil || wm.cssInitialized { | ||
| 105 | return | 138 | return | ||
| 106 | } | 139 | } | ||
| 107 | - | stackedPaneCSS := wm.generateStackedPaneCSS() | 140 | + | workspaceCSS := wm.generateWorkspaceCSS() |
| 108 | - | webkit.AddCSSProvider(stackedPaneCSS) | 141 | + | webkit.AddCSSProvider(workspaceCSS) |
| 109 | wm.cssInitialized = true | 142 | wm.cssInitialized = true | ||
| 110 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | // ensureStackedPaneStyles ensures that CSS styles are applied for stacked panes (deprecated - use ensureWorkspaceStyles) | |||
| 146 | + | func (wm *WorkspaceManager) ensureStackedPaneStyles() { | |||
| 147 | + | wm.ensureWorkspaceStyles() | |||
| 148 | + | } | |||
M internal/app/browser/workspace_utils.go +150 -0
| @@ -4,6 +4,7 @@ package browser | |||
| 4 | 4 | import ( | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | + | "log" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/dumber/pkg/webkit" | |
| 9 | 10 | ) | |
| @@ -97,6 +98,14 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||
| 97 | 98 | // Register the main container | |
| 98 | 99 | node.container = wm.registerWidget(containerPtr, "pane-container") | |
| 99 | 100 | ||
| 101 | + | // Add base CSS class to the container | |
| 102 | + | if node.container != nil { | |
| 103 | + | node.container.Execute(func(ptr uintptr) error { | |
| 104 | + | webkit.WidgetAddCSSClass(ptr, basePaneClass) | |
| 105 | + | return nil | |
| 106 | + | }) | |
| 107 | + | } | |
| 108 | + | ||
| 100 | 109 | // Initialize other widget fields as nil (will be set when needed) | |
| 101 | 110 | node.titleBar = nil | |
| 102 | 111 | node.stackWrapper = nil | |
| @@ -142,3 +151,144 @@ func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) { | |||
| 142 | 151 | func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) { | |
| 143 | 152 | node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper") | |
| 144 | 153 | } | |
| 154 | + | ||
| 155 | + | // Centralized Active Pane Border Management System | |
| 156 | + | // Handles all pane types: regular, popup, stacked, split panes | |
| 157 | + | ||
| 158 | + | // PaneBorderContext holds the context for applying borders to any pane type | |
| 159 | + | type PaneBorderContext struct { | |
| 160 | + | webViewWidget uintptr // The WebView's native widget (for margin) | |
| 161 | + | borderContainer *SafeWidget // The container that gets background color | |
| 162 | + | cssClasses []string // Additional CSS classes to apply | |
| 163 | + | paneType string // Description for debugging | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // determineBorderContext analyzes a pane node and determines the correct border context | |
| 167 | + | func (wm *WorkspaceManager) determineBorderContext(node *paneNode) *PaneBorderContext { | |
| 168 | + | if node == nil { | |
| 169 | + | return nil | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | ctx := &PaneBorderContext{} | |
| 173 | + | ||
| 174 | + | // Step 1: Get WebView widget for margin (same for all pane types) | |
| 175 | + | if node.pane != nil && node.pane.webView != nil { | |
| 176 | + | ctx.webViewWidget = node.pane.webView.RootWidget() | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // Step 2: Determine border container and CSS classes based on pane hierarchy and type | |
| 180 | + | switch { | |
| 181 | + | case node.parent != nil && node.parent.isStacked: | |
| 182 | + | // STACKED PANE: Border goes on the stack container | |
| 183 | + | ctx.borderContainer = node.parent.container | |
| 184 | + | ctx.cssClasses = []string{stackContainerClass, activePaneClass} | |
| 185 | + | ctx.paneType = "stacked" | |
| 186 | + | ||
| 187 | + | case node.windowType == webkit.WindowTypePopup: | |
| 188 | + | // POPUP PANE: Border goes on the popup's own container | |
| 189 | + | ctx.borderContainer = node.container | |
| 190 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |
| 191 | + | ctx.paneType = "popup" | |
| 192 | + | ||
| 193 | + | case node.isLeaf && !node.isStacked: | |
| 194 | + | // REGULAR LEAF PANE: Border goes on the pane's container | |
| 195 | + | ctx.borderContainer = node.container | |
| 196 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |
| 197 | + | ctx.paneType = "regular-leaf" | |
| 198 | + | ||
| 199 | + | case !node.isLeaf: | |
| 200 | + | // SPLIT PANE (branch node): Border goes on the split container | |
| 201 | + | ctx.borderContainer = node.container | |
| 202 | + | ctx.cssClasses = []string{basePaneClass, multiPaneClass, activePaneClass} | |
| 203 | + | ctx.paneType = "split-branch" | |
| 204 | + | ||
| 205 | + | default: | |
| 206 | + | // FALLBACK: Use the node's own container | |
| 207 | + | ctx.borderContainer = node.container | |
| 208 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |
| 209 | + | ctx.paneType = "fallback" | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | return ctx | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // applyActivePaneBorder adds the active pane visual border using the centralized system | |
| 216 | + | func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |
| 217 | + | ctx := wm.determineBorderContext(node) | |
| 218 | + | if ctx == nil { | |
| 219 | + | return | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // Apply CSS classes to border container (provides border styling) | |
| 223 | + | if ctx.borderContainer != nil { | |
| 224 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |
| 225 | + | // Only add the active class - base classes should already be set | |
| 226 | + | webkit.WidgetAddCSSClass(containerPtr, activePaneClass) | |
| 227 | + | return nil | |
| 228 | + | }) | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | log.Printf("[workspace] Applied border to %s pane: %p", ctx.paneType, node) | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // removeActivePaneBorder removes the active pane visual border using the centralized system | |
| 235 | + | func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | |
| 236 | + | ctx := wm.determineBorderContext(node) | |
| 237 | + | if ctx == nil { | |
| 238 | + | return | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | // Remove CSS classes from border container | |
| 242 | + | if ctx.borderContainer != nil { | |
| 243 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |
| 244 | + | // Only remove the active class if it exists | |
| 245 | + | if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | |
| 246 | + | webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass) | |
| 247 | + | } | |
| 248 | + | return nil | |
| 249 | + | }) | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | log.Printf("[workspace] Removed border from %s pane: %p", ctx.paneType, node) | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // removeActivePaneBorderFromAll removes active pane borders from all containers using centralized system | |
| 256 | + | func (wm *WorkspaceManager) removeActivePaneBorderFromAll() { | |
| 257 | + | if wm == nil { | |
| 258 | + | return | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | log.Printf("[workspace] Removing active borders from all panes") | |
| 262 | + | ||
| 263 | + | // Use the centralized system for each pane | |
| 264 | + | for _, node := range wm.viewToNode { | |
| 265 | + | if node != nil { | |
| 266 | + | wm.removeActivePaneBorder(node) | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // isActivePaneBorderApplied checks if the active pane border is currently applied to a node using centralized system | |
| 272 | + | func (wm *WorkspaceManager) isActivePaneBorderApplied(node *paneNode) bool { | |
| 273 | + | ctx := wm.determineBorderContext(node) | |
| 274 | + | if ctx == nil || ctx.borderContainer == nil { | |
| 275 | + | return false | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | var hasClass bool | |
| 279 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |
| 280 | + | hasClass = webkit.WidgetHasCSSClass(containerPtr, activePaneClass) | |
| 281 | + | return nil | |
| 282 | + | }) | |
| 283 | + | ||
| 284 | + | return hasClass | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // getBorderContextInfo returns debugging information about a pane's border context | |
| 288 | + | func (wm *WorkspaceManager) getBorderContextInfo(node *paneNode) string { | |
| 289 | + | ctx := wm.determineBorderContext(node) | |
| 290 | + | if ctx == nil { | |
| 291 | + | return "no-context" | |
| 292 | + | } | |
| 293 | + | return ctx.paneType | |
| 294 | + | } | |
| @@ -4,6 +4,7 @@ package browser | |||||
| 4 | import ( | 4 | import ( | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "log" | |||
| 7 | 8 | ||||
| 8 | "github.com/bnema/dumber/pkg/webkit" | 9 | "github.com/bnema/dumber/pkg/webkit" | ||
| 9 | ) | 10 | ) | ||
| @@ -97,6 +98,14 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||||
| 97 | // Register the main container | 98 | // Register the main container | ||
| 98 | node.container = wm.registerWidget(containerPtr, "pane-container") | 99 | node.container = wm.registerWidget(containerPtr, "pane-container") | ||
| 99 | 100 | ||||
| 101 | + | // Add base CSS class to the container | |||
| 102 | + | if node.container != nil { | |||
| 103 | + | node.container.Execute(func(ptr uintptr) error { | |||
| 104 | + | webkit.WidgetAddCSSClass(ptr, basePaneClass) | |||
| 105 | + | return nil | |||
| 106 | + | }) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 100 | // Initialize other widget fields as nil (will be set when needed) | 109 | // Initialize other widget fields as nil (will be set when needed) | ||
| 101 | node.titleBar = nil | 110 | node.titleBar = nil | ||
| 102 | node.stackWrapper = nil | 111 | node.stackWrapper = nil | ||
| @@ -142,3 +151,144 @@ func (wm *WorkspaceManager) setTitleBar(node *paneNode, ptr uintptr) { | |||||
| 142 | func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) { | 151 | func (wm *WorkspaceManager) setStackWrapper(node *paneNode, ptr uintptr) { | ||
| 143 | node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper") | 152 | node.stackWrapper = wm.registerWidget(ptr, "stack-wrapper") | ||
| 144 | } | 153 | } | ||
| 154 | + | ||||
| 155 | + | // Centralized Active Pane Border Management System | |||
| 156 | + | // Handles all pane types: regular, popup, stacked, split panes | |||
| 157 | + | ||||
| 158 | + | // PaneBorderContext holds the context for applying borders to any pane type | |||
| 159 | + | type PaneBorderContext struct { | |||
| 160 | + | webViewWidget uintptr // The WebView's native widget (for margin) | |||
| 161 | + | borderContainer *SafeWidget // The container that gets background color | |||
| 162 | + | cssClasses []string // Additional CSS classes to apply | |||
| 163 | + | paneType string // Description for debugging | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // determineBorderContext analyzes a pane node and determines the correct border context | |||
| 167 | + | func (wm *WorkspaceManager) determineBorderContext(node *paneNode) *PaneBorderContext { | |||
| 168 | + | if node == nil { | |||
| 169 | + | return nil | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | ctx := &PaneBorderContext{} | |||
| 173 | + | ||||
| 174 | + | // Step 1: Get WebView widget for margin (same for all pane types) | |||
| 175 | + | if node.pane != nil && node.pane.webView != nil { | |||
| 176 | + | ctx.webViewWidget = node.pane.webView.RootWidget() | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // Step 2: Determine border container and CSS classes based on pane hierarchy and type | |||
| 180 | + | switch { | |||
| 181 | + | case node.parent != nil && node.parent.isStacked: | |||
| 182 | + | // STACKED PANE: Border goes on the stack container | |||
| 183 | + | ctx.borderContainer = node.parent.container | |||
| 184 | + | ctx.cssClasses = []string{stackContainerClass, activePaneClass} | |||
| 185 | + | ctx.paneType = "stacked" | |||
| 186 | + | ||||
| 187 | + | case node.windowType == webkit.WindowTypePopup: | |||
| 188 | + | // POPUP PANE: Border goes on the popup's own container | |||
| 189 | + | ctx.borderContainer = node.container | |||
| 190 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |||
| 191 | + | ctx.paneType = "popup" | |||
| 192 | + | ||||
| 193 | + | case node.isLeaf && !node.isStacked: | |||
| 194 | + | // REGULAR LEAF PANE: Border goes on the pane's container | |||
| 195 | + | ctx.borderContainer = node.container | |||
| 196 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |||
| 197 | + | ctx.paneType = "regular-leaf" | |||
| 198 | + | ||||
| 199 | + | case !node.isLeaf: | |||
| 200 | + | // SPLIT PANE (branch node): Border goes on the split container | |||
| 201 | + | ctx.borderContainer = node.container | |||
| 202 | + | ctx.cssClasses = []string{basePaneClass, multiPaneClass, activePaneClass} | |||
| 203 | + | ctx.paneType = "split-branch" | |||
| 204 | + | ||||
| 205 | + | default: | |||
| 206 | + | // FALLBACK: Use the node's own container | |||
| 207 | + | ctx.borderContainer = node.container | |||
| 208 | + | ctx.cssClasses = []string{basePaneClass, activePaneClass} | |||
| 209 | + | ctx.paneType = "fallback" | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | return ctx | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // applyActivePaneBorder adds the active pane visual border using the centralized system | |||
| 216 | + | func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |||
| 217 | + | ctx := wm.determineBorderContext(node) | |||
| 218 | + | if ctx == nil { | |||
| 219 | + | return | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // Apply CSS classes to border container (provides border styling) | |||
| 223 | + | if ctx.borderContainer != nil { | |||
| 224 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |||
| 225 | + | // Only add the active class - base classes should already be set | |||
| 226 | + | webkit.WidgetAddCSSClass(containerPtr, activePaneClass) | |||
| 227 | + | return nil | |||
| 228 | + | }) | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | log.Printf("[workspace] Applied border to %s pane: %p", ctx.paneType, node) | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // removeActivePaneBorder removes the active pane visual border using the centralized system | |||
| 235 | + | func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | |||
| 236 | + | ctx := wm.determineBorderContext(node) | |||
| 237 | + | if ctx == nil { | |||
| 238 | + | return | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | // Remove CSS classes from border container | |||
| 242 | + | if ctx.borderContainer != nil { | |||
| 243 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |||
| 244 | + | // Only remove the active class if it exists | |||
| 245 | + | if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) { | |||
| 246 | + | webkit.WidgetRemoveCSSClass(containerPtr, activePaneClass) | |||
| 247 | + | } | |||
| 248 | + | return nil | |||
| 249 | + | }) | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | log.Printf("[workspace] Removed border from %s pane: %p", ctx.paneType, node) | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // removeActivePaneBorderFromAll removes active pane borders from all containers using centralized system | |||
| 256 | + | func (wm *WorkspaceManager) removeActivePaneBorderFromAll() { | |||
| 257 | + | if wm == nil { | |||
| 258 | + | return | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | log.Printf("[workspace] Removing active borders from all panes") | |||
| 262 | + | ||||
| 263 | + | // Use the centralized system for each pane | |||
| 264 | + | for _, node := range wm.viewToNode { | |||
| 265 | + | if node != nil { | |||
| 266 | + | wm.removeActivePaneBorder(node) | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // isActivePaneBorderApplied checks if the active pane border is currently applied to a node using centralized system | |||
| 272 | + | func (wm *WorkspaceManager) isActivePaneBorderApplied(node *paneNode) bool { | |||
| 273 | + | ctx := wm.determineBorderContext(node) | |||
| 274 | + | if ctx == nil || ctx.borderContainer == nil { | |||
| 275 | + | return false | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | var hasClass bool | |||
| 279 | + | ctx.borderContainer.Execute(func(containerPtr uintptr) error { | |||
| 280 | + | hasClass = webkit.WidgetHasCSSClass(containerPtr, activePaneClass) | |||
| 281 | + | return nil | |||
| 282 | + | }) | |||
| 283 | + | ||||
| 284 | + | return hasClass | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | // getBorderContextInfo returns debugging information about a pane's border context | |||
| 288 | + | func (wm *WorkspaceManager) getBorderContextInfo(node *paneNode) string { | |||
| 289 | + | ctx := wm.determineBorderContext(node) | |||
| 290 | + | if ctx == nil { | |||
| 291 | + | return "no-context" | |||
| 292 | + | } | |||
| 293 | + | return ctx.paneType | |||
| 294 | + | } | |||