1// workspace_css.go - CSS generation and styling for workspace panes
2package browser
3
4import (
5 "fmt"
6 "strings"
7
8 "github.com/bnema/dumber/internal/config"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/dumber/pkg/webkit"
11)
12
13// Helper functions for stacked pane CSS colors
14
15// getStackTitleBg returns the background color for stack titles based on theme
16func getStackTitleBg(isDark bool) string {
17 if isDark {
18 return "#404040"
19 }
20 return "#f0f0f0"
21}
22
23// getStackTitleHoverBg returns the hover background color for stack titles based on theme
24func getStackTitleHoverBg(isDark bool) string {
25 if isDark {
26 return "#505050"
27 }
28 return "#e0e0e0"
29}
30
31// getStackTitleTextColor returns the text color for stack titles based on theme
32func getStackTitleTextColor(isDark bool) string {
33 if isDark {
34 return "#ffffff"
35 }
36 return "#333333"
37}
38
39// getStackTitleBorderColor returns a subtle border color slightly darker than the title background
40func getStackTitleBorderColor(isDark bool) string {
41 if isDark {
42 return "#353535" // Slightly darker than #404040
43 }
44 return "#e5e5e5" // Slightly darker than #f0f0f0
45}
46
47// getActiveTabBg returns a distinct background color for the active tab
48func getActiveTabBg(isDark bool) string {
49 if isDark {
50 return "#707070" // Significantly lighter gray than inactive (#404040) - similar to stacked panes
51 }
52 return "#c0c0c0" // Significantly darker gray than inactive (#f0f0f0) - similar to stacked panes
53}
54
55// getEntryBg returns the background color for text entry widgets
56func getEntryBg(isDark bool) string {
57 if isDark {
58 return "#2a2a2a" // Darker background for dark theme
59 }
60 return "#e0e0e0" // Darker gray for light theme
61}
62
63// getEntryFocusBg returns the background color for focused entry widgets
64func getEntryFocusBg(isDark bool) string {
65 if isDark {
66 return "#323232" // Slightly lighter when focused
67 }
68 return "#f5f5f5" // Very light gray when focused
69}
70
71// getEntryBorderColor returns the border color for entry widgets
72func getEntryBorderColor(isDark bool) string {
73 if isDark {
74 return "#555555" // Subtle border for dark theme
75 }
76 return "#cccccc" // Subtle border for light theme
77}
78
79// getActivePaneBorderColor returns the border color for active panes based on config and theme
80func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string {
81 // Use configured border color if set
82 if styling.BorderColor != "" {
83 // Check if it's a GTK theme variable (starts with @)
84 if strings.HasPrefix(styling.BorderColor, "@") {
85 return styling.BorderColor
86 }
87 // Return the configured color as-is (could be hex, rgb, etc.)
88 return styling.BorderColor
89 }
90
91 // Fallback to hardcoded colors based on theme
92 if isDark {
93 return "#4A90E2" // Deeper blue pastel for dark theme
94 }
95 return "#87CEEB" // Sky blue pastel for light theme
96}
97
98// generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes
99func (wm *WorkspaceManager) generateWorkspaceCSS() string {
100 cfg := config.Get()
101 styling := cfg.Workspace.Styling
102
103 // Get appropriate colors based on GTK theme preference
104 var windowBackgroundColor string
105 isDark := webkit.PrefersDarkTheme()
106 if isDark {
107 windowBackgroundColor = "#2b2b2b" // Dark window background
108 } else {
109 windowBackgroundColor = "#ffffff" // Light window background
110 }
111
112 activeBorderColor := getActivePaneBorderColor(styling, isDark)
113
114 // Pane mode border color - use configured value or fallback to blue
115 paneModeColor := styling.PaneModeBorderColor
116 if paneModeColor == "" {
117 paneModeColor = "#4A90E2" // Fallback to blue if not configured
118 }
119
120 // Tab mode border color - use configured value or fallback to orange
121 tabModeColor := styling.TabModeBorderColor
122 if tabModeColor == "" {
123 tabModeColor = "#FFA500" // Fallback to orange if not configured
124 }
125
126 // Stacked title border - subtle separator between titles
127 stackedTitleBorder := fmt.Sprintf("1px solid %s", getStackTitleBorderColor(isDark))
128
129 // UI Scale calculations for title bar elements (base values at 1.0 scale)
130 uiScale := styling.UIScale
131 if uiScale <= 0 {
132 uiScale = 1.0 // Fallback to default if invalid
133 }
134 titleFontSize := int(12 * uiScale) // Base: 12px
135 titlePaddingVertical := int(4 * uiScale) // Base: 4px
136 titlePaddingHorizontal := int(8 * uiScale) // Base: 8px
137 titleMinHeight := int(24 * uiScale) // Base: 24px
138 faviconMargin := int(4 * uiScale) // Base: 4px
139
140 css := fmt.Sprintf(`window {
141 background-color: %s;
142 padding: 0;
143 margin: 0;
144 }
145
146 /* Pane mode border overlay - floats over content without layout shift */
147 .pane-mode-border {
148 border: %dpx solid %s;
149 border-radius: 0;
150 background-color: transparent;
151 pointer-events: none;
152 }
153
154 /* Tab mode border overlay - floats over content without layout shift */
155 .tab-mode-border {
156 border: %dpx solid %s;
157 border-radius: 0;
158 background-color: transparent;
159 pointer-events: none;
160 }
161
162 /* Pane border overlay - active pane indicator without layout shift */
163 .pane-border-overlay {
164 border: %dpx solid %s;
165 border-radius: 0;
166 background-color: transparent;
167 pointer-events: none;
168 transition-property: opacity;
169 transition-duration: %dms;
170 transition-timing-function: ease-in-out;
171 }
172
173 /* Workspace root container */
174 paned, box {
175 background-color: %s;
176 }
177
178 /* Base pane styling - no borders (using overlays instead) */
179 .workspace-pane, .stacked-pane-container {
180 border: none;
181 margin: 0;
182 }
183
184 /* Stacked panes styling */
185 .stacked-pane-container {
186 background-color: %s;
187 }
188
189 .stacked-pane-title {
190 background-color: %s;
191 border-bottom: %s;
192 padding: %dpx %dpx;
193 min-height: %dpx;
194 transition: background-color %dms ease-in-out;
195 }
196
197 .stacked-pane-title:hover {
198 background-color: %s;
199 }
200
201 .stacked-pane-title-text {
202 font-size: %dpx;
203 color: %s;
204 font-weight: 500;
205 }
206
207 .stacked-pane-favicon {
208 margin-right: %dpx;
209 }
210
211 .stacked-pane-active {
212 /* Active pane is fully visible */
213 }
214
215 .stacked-pane-collapsed {
216 /* Collapsed panes are hidden - handled in code via widget visibility */
217 }
218
219 /* Tab bar styling */
220 .tab-bar {
221 background-color: %s;
222 border-top: 2px solid %s;
223 padding: 0;
224 min-height: %dpx;
225 }
226
227 /* Tab progress bar */
228 progressbar.tab-progress-bar {
229 min-height: 4px;
230 padding: 0;
231 margin: 0;
232 border: none;
233 transition: opacity 180ms ease-in-out;
234 }
235
236 progressbar.tab-progress-bar trough {
237 min-height: 4px;
238 background: transparent;
239 border: none;
240 padding: 0;
241 }
242
243 progressbar.tab-progress-bar progress {
244 min-height: 4px;
245 border-radius: 0;
246 background-image: linear-gradient(90deg, %s 0%%, %s 50%%, %s 100%%);
247 background-size: 240%% 100%%;
248 background-position: 0%% 0%%;
249 transition: width 180ms ease-in-out, background-position 420ms linear;
250 animation: tab-progress-stripe 1.3s linear infinite;
251 }
252
253 /* Tab button styling - matches stacked pane titles */
254 button.tab-button {
255 background-color: %s;
256 background-image: none;
257 border: none;
258 border-right: 1px solid %s;
259 border-radius: 0;
260 padding: %dpx %dpx;
261 transition: background-color %dms ease-in-out;
262 }
263
264 button.tab-button:hover {
265 background-color: %s;
266 background-image: none;
267 }
268
269 button.tab-button.tab-button-active {
270 background-color: %s;
271 background-image: none;
272 border: none;
273 border-right: 1px solid %s;
274 border-radius: 0;
275 font-weight: 600;
276 }
277
278 /* Tab title text */
279 .tab-title {
280 font-size: %dpx;
281 color: %s;
282 font-weight: 500;
283 }
284
285 /* Tab rename entry */
286 entry.tab-rename-entry {
287 background-color: %s;
288 color: %s;
289 font-size: %dpx;
290 padding: 2px %dpx;
291 border: 1px solid %s;
292 border-radius: 3px;
293 box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.4);
294 min-width: 80px;
295 min-height: 0;
296 }
297
298 entry.tab-rename-entry:focus {
299 background-color: %s;
300 border-color: %s;
301 box-shadow: inset 1px 1px 4px rgba(0, 0, 0, 0.5);
302 outline: none;
303 }
304
305 /* Tab content area */
306 .tab-content-area {
307 background-color: %s;
308 }
309
310 /* Tab workspace containers */
311 .tab-workspace-container {
312 background-color: %s;
313 }
314
315 /* Pulse animation for tab mode */
316 @keyframes pulse-border {
317 0%%, 100%% { opacity: 1.0; }
318 50%% { opacity: 0.6; }
319 }
320
321 @keyframes tab-progress-stripe {
322 0%% { background-position: 0%% 0%%; }
323 100%% { background-position: -100%% 0%%; }
324 }`,
325 windowBackgroundColor, // window background
326 styling.PaneModeBorderWidth, // .pane-mode-border border-width
327 paneModeColor, // .pane-mode-border border-color
328 styling.TabModeBorderWidth, // .tab-mode-border border-width
329 tabModeColor, // .tab-mode-border border-color
330 styling.BorderWidth, // .pane-border-overlay border-width
331 activeBorderColor, // .pane-border-overlay border-color
332 styling.TransitionDuration, // .pane-border-overlay transition-duration
333 windowBackgroundColor, // paned, box background
334 windowBackgroundColor, // stacked-pane-container background
335 getStackTitleBg(isDark), // stacked-pane-title background
336 stackedTitleBorder, // stacked-pane-title border-bottom
337 titlePaddingVertical, // stacked-pane-title padding vertical
338 titlePaddingHorizontal, // stacked-pane-title padding horizontal
339 titleMinHeight, // stacked-pane-title min-height
340 styling.TransitionDuration, // stacked-pane-title transition
341 getStackTitleHoverBg(isDark), // stacked-pane-title:hover background
342 titleFontSize, // stacked-pane-title-text font-size
343 getStackTitleTextColor(isDark), // stacked-pane-title-text color
344 faviconMargin, // stacked-pane-favicon margin-right
345 // Tab bar styles
346 windowBackgroundColor, // tab-bar background
347 getStackTitleBorderColor(isDark), // tab-bar border-top
348 int(32*uiScale), // tab-bar min-height
349 "#606060", // tab progress gradient start (neutral gray)
350 "#747474", // tab progress gradient middle (neutral gray)
351 "#8a8a8a", // tab progress gradient end (neutral gray)
352 getStackTitleBg(isDark), // tab-button background (same as stacked title)
353 getStackTitleBorderColor(isDark), // tab-button border-right
354 titlePaddingVertical, // tab-button padding vertical
355 titlePaddingHorizontal, // tab-button padding horizontal
356 styling.TransitionDuration, // tab-button transition
357 getStackTitleHoverBg(isDark), // tab-button:hover background
358 getActiveTabBg(isDark), // tab-button-active background (much more visible)
359 getStackTitleBorderColor(isDark), // tab-button-active border-right
360 titleFontSize, // tab-title font-size
361 getStackTitleTextColor(isDark), // tab-title color
362 // Tab rename entry styles
363 getEntryBg(isDark), // entry.tab-rename-entry background
364 getStackTitleTextColor(isDark), // entry.tab-rename-entry color (text)
365 titleFontSize, // entry.tab-rename-entry font-size
366 titlePaddingHorizontal, // entry.tab-rename-entry padding horizontal (vertical is hardcoded 2px)
367 getEntryBorderColor(isDark), // entry.tab-rename-entry border-color
368 getEntryFocusBg(isDark), // entry.tab-rename-entry:focus background
369 activeBorderColor, // entry.tab-rename-entry:focus border-color (use active pane color)
370 windowBackgroundColor, // tab-content-area background
371 windowBackgroundColor, // tab-workspace-container background
372 )
373
374 return css
375}
376
377// Global CSS provider tracking to prevent duplication
378var (
379 globalCSSInitialized bool
380 globalCSSContent string
381)
382
383// ensureWorkspaceStyles ensures that CSS styles are applied for workspace panes
384// Improved to prevent CSS provider duplication by using global CSS tracking
385func (wm *WorkspaceManager) ensureWorkspaceStyles() {
386 if wm == nil {
387 return
388 }
389
390 // Generate CSS content
391 workspaceCSS := wm.generateWorkspaceCSS()
392
393 // Only apply CSS if it hasn't been applied globally or if content changed
394 if !globalCSSInitialized || globalCSSContent != workspaceCSS {
395 if err := webkit.AddCSSProvider(workspaceCSS); err != nil {
396 logging.Error(fmt.Sprintf("[workspace] WARNING: Failed to add CSS provider: %v", err))
397 }
398 globalCSSInitialized = true
399 globalCSSContent = workspaceCSS
400 logging.Info(fmt.Sprintf("[workspace] Applied workspace CSS styles (%d bytes)", len(workspaceCSS)))
401
402 previewLen := 200
403 if len(workspaceCSS) < previewLen {
404 previewLen = len(workspaceCSS)
405 }
406 logging.Info(fmt.Sprintf("[workspace] CSS preview: %s...", workspaceCSS[:previewLen]))
407 }
408
409 wm.cssInitialized = true
410}