feat(workspace): add configurable styling for pane borders
- commit
- 96a4258455890ee0bd5af97554d98f3d4fc418cd
- author
- bamen · · 2025-09-19 05:13 UTC
- parent
- cb15db6
- signature
- unsigned
3 files · +64 -15
M internal/app/browser/workspace_manager.go +34 -15
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/dumber/internal/app/messaging" | |
| 13 | + | "github.com/bnema/dumber/internal/config" | |
| 13 | 14 | "github.com/bnema/dumber/pkg/webkit" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| @@ -47,15 +48,6 @@ type WorkspaceManager struct { | |||
| 47 | 48 | } | |
| 48 | 49 | ||
| 49 | 50 | const ( | |
| 50 | - | // TODO : should be defined via Config + Defaults | |
| 51 | - | activePaneCSS = `.workspace-pane { | |
| 52 | - | box-shadow: inset 0 0 0 0px transparent; | |
| 53 | - | transition: box-shadow 150ms ease-in-out; | |
| 54 | - | } | |
| 55 | - | ||
| 56 | - | .workspace-pane-active { | |
| 57 | - | box-shadow: inset 0 0 0 2px @theme_selected_bg_color; | |
| 58 | - | }` | |
| 59 | 51 | activePaneClass = "workspace-pane-active" | |
| 60 | 52 | basePaneClass = "workspace-pane" | |
| 61 | 53 | multiPaneClass = "workspace-multi-pane" | |
| @@ -281,8 +273,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||
| 281 | 273 | } | |
| 282 | 274 | } | |
| 283 | 275 | ||
| 284 | - | // Remove active class from previous pane only if we have multiple panes | |
| 285 | - | if wm.active != nil && wm.active.container != 0 && wm.hasMultiplePanes() { | |
| 276 | + | // Remove active class from previous pane (border stays, just color changes) | |
| 277 | + | if wm.active != nil && wm.active.container != 0 { | |
| 286 | 278 | webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass) | |
| 287 | 279 | } | |
| 288 | 280 | ||
| @@ -312,10 +304,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||
| 312 | 304 | container := node.container | |
| 313 | 305 | viewWidget := node.pane.webView.Widget() | |
| 314 | 306 | if container != 0 && container != previousContainer { | |
| 315 | - | // Only add active class if we have multiple panes (no border for single pane) | |
| 316 | - | if wm.hasMultiplePanes() { | |
| 317 | - | webkit.WidgetAddCSSClass(container, activePaneClass) | |
| 318 | - | } | |
| 307 | + | // Always add active class (border visibility controlled by CSS multi-pane rules) | |
| 308 | + | webkit.WidgetAddCSSClass(container, activePaneClass) | |
| 319 | 309 | if !webkit.WidgetIsValid(container) { | |
| 320 | 310 | log.Printf("[workspace] focus aborted: container invalid widget=%#x", container) | |
| 321 | 311 | return | |
| @@ -369,10 +359,39 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||
| 369 | 359 | } | |
| 370 | 360 | } | |
| 371 | 361 | ||
| 362 | + | // generateActivePaneCSS generates the CSS for workspace panes based on config | |
| 363 | + | func (wm *WorkspaceManager) generateActivePaneCSS() string { | |
| 364 | + | cfg := config.Get() | |
| 365 | + | styling := cfg.Workspace.Styling | |
| 366 | + | ||
| 367 | + | return fmt.Sprintf(`.workspace-pane { | |
| 368 | + | border: %dpx solid transparent; | |
| 369 | + | transition: border-color %dms ease-in-out; | |
| 370 | + | border-radius: %dpx; | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | .workspace-pane.workspace-multi-pane { | |
| 374 | + | border: %dpx solid transparent; | |
| 375 | + | border-radius: %dpx; | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | .workspace-pane.workspace-multi-pane.workspace-pane-active { | |
| 379 | + | border-color: %s; | |
| 380 | + | }`, | |
| 381 | + | styling.BorderWidth, | |
| 382 | + | styling.TransitionDuration, | |
| 383 | + | styling.BorderRadius, | |
| 384 | + | styling.BorderWidth, | |
| 385 | + | styling.BorderRadius, | |
| 386 | + | styling.BorderColor, | |
| 387 | + | ) | |
| 388 | + | } | |
| 389 | + | ||
| 372 | 390 | func (wm *WorkspaceManager) ensureStyles() { | |
| 373 | 391 | if wm == nil || wm.cssInitialized { | |
| 374 | 392 | return | |
| 375 | 393 | } | |
| 394 | + | activePaneCSS := wm.generateActivePaneCSS() | |
| 376 | 395 | webkit.AddCSSProvider(activePaneCSS) | |
| 377 | 396 | wm.cssInitialized = true | |
| 378 | 397 | } | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "time" | 10 | "time" | ||
| 11 | 11 | ||||
| 12 | "github.com/bnema/dumber/internal/app/messaging" | 12 | "github.com/bnema/dumber/internal/app/messaging" | ||
| 13 | + | "github.com/bnema/dumber/internal/config" | |||
| 13 | "github.com/bnema/dumber/pkg/webkit" | 14 | "github.com/bnema/dumber/pkg/webkit" | ||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| @@ -47,15 +48,6 @@ type WorkspaceManager struct { | |||||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| 49 | const ( | 50 | const ( | ||
| 50 | - | // TODO : should be defined via Config + Defaults | |||
| 51 | - | activePaneCSS = `.workspace-pane { | |||
| 52 | - | box-shadow: inset 0 0 0 0px transparent; | |||
| 53 | - | transition: box-shadow 150ms ease-in-out; | |||
| 54 | - | } | |||
| 55 | - | ||||
| 56 | - | .workspace-pane-active { | |||
| 57 | - | box-shadow: inset 0 0 0 2px @theme_selected_bg_color; | |||
| 58 | - | }` | |||
| 59 | activePaneClass = "workspace-pane-active" | 51 | activePaneClass = "workspace-pane-active" | ||
| 60 | basePaneClass = "workspace-pane" | 52 | basePaneClass = "workspace-pane" | ||
| 61 | multiPaneClass = "workspace-multi-pane" | 53 | multiPaneClass = "workspace-multi-pane" | ||
| @@ -281,8 +273,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||||
| 281 | } | 273 | } | ||
| 282 | } | 274 | } | ||
| 283 | 275 | ||||
| 284 | - | // Remove active class from previous pane only if we have multiple panes | 276 | + | // Remove active class from previous pane (border stays, just color changes) |
| 285 | - | if wm.active != nil && wm.active.container != 0 && wm.hasMultiplePanes() { | 277 | + | if wm.active != nil && wm.active.container != 0 { |
| 286 | webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass) | 278 | webkit.WidgetRemoveCSSClass(wm.active.container, activePaneClass) | ||
| 287 | } | 279 | } | ||
| 288 | 280 | ||||
| @@ -312,10 +304,8 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||||
| 312 | container := node.container | 304 | container := node.container | ||
| 313 | viewWidget := node.pane.webView.Widget() | 305 | viewWidget := node.pane.webView.Widget() | ||
| 314 | if container != 0 && container != previousContainer { | 306 | if container != 0 && container != previousContainer { | ||
| 315 | - | // Only add active class if we have multiple panes (no border for single pane) | 307 | + | // Always add active class (border visibility controlled by CSS multi-pane rules) |
| 316 | - | if wm.hasMultiplePanes() { | 308 | + | webkit.WidgetAddCSSClass(container, activePaneClass) |
| 317 | - | webkit.WidgetAddCSSClass(container, activePaneClass) | |||
| 318 | - | } | |||
| 319 | if !webkit.WidgetIsValid(container) { | 309 | if !webkit.WidgetIsValid(container) { | ||
| 320 | log.Printf("[workspace] focus aborted: container invalid widget=%#x", container) | 310 | log.Printf("[workspace] focus aborted: container invalid widget=%#x", container) | ||
| 321 | return | 311 | return | ||
| @@ -369,10 +359,39 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||||
| 369 | } | 359 | } | ||
| 370 | } | 360 | } | ||
| 371 | 361 | ||||
| 362 | + | // generateActivePaneCSS generates the CSS for workspace panes based on config | |||
| 363 | + | func (wm *WorkspaceManager) generateActivePaneCSS() string { | |||
| 364 | + | cfg := config.Get() | |||
| 365 | + | styling := cfg.Workspace.Styling | |||
| 366 | + | ||||
| 367 | + | return fmt.Sprintf(`.workspace-pane { | |||
| 368 | + | border: %dpx solid transparent; | |||
| 369 | + | transition: border-color %dms ease-in-out; | |||
| 370 | + | border-radius: %dpx; | |||
| 371 | + | } | |||
| 372 | + | ||||
| 373 | + | .workspace-pane.workspace-multi-pane { | |||
| 374 | + | border: %dpx solid transparent; | |||
| 375 | + | border-radius: %dpx; | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | .workspace-pane.workspace-multi-pane.workspace-pane-active { | |||
| 379 | + | border-color: %s; | |||
| 380 | + | }`, | |||
| 381 | + | styling.BorderWidth, | |||
| 382 | + | styling.TransitionDuration, | |||
| 383 | + | styling.BorderRadius, | |||
| 384 | + | styling.BorderWidth, | |||
| 385 | + | styling.BorderRadius, | |||
| 386 | + | styling.BorderColor, | |||
| 387 | + | ) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 372 | func (wm *WorkspaceManager) ensureStyles() { | 390 | func (wm *WorkspaceManager) ensureStyles() { | ||
| 373 | if wm == nil || wm.cssInitialized { | 391 | if wm == nil || wm.cssInitialized { | ||
| 374 | return | 392 | return | ||
| 375 | } | 393 | } | ||
| 394 | + | activePaneCSS := wm.generateActivePaneCSS() | |||
| 376 | webkit.AddCSSProvider(activePaneCSS) | 395 | webkit.AddCSSProvider(activePaneCSS) | ||
| 377 | wm.cssInitialized = true | 396 | wm.cssInitialized = true | ||
| 378 | } | 397 | } | ||
M internal/config/config.go +18 -0
| @@ -234,6 +234,8 @@ type WorkspaceConfig struct { | |||
| 234 | 234 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"` | |
| 235 | 235 | // Popups configures default popup placement rules. | |
| 236 | 236 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"` | |
| 237 | + | // Styling configures workspace visual appearance. | |
| 238 | + | Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" json:"styling"` | |
| 237 | 239 | } | |
| 238 | 240 | ||
| 239 | 241 | // PaneModeConfig defines modal behaviour for pane management. | |
| @@ -258,6 +260,18 @@ type PopupBehaviorConfig struct { | |||
| 258 | 260 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | |
| 259 | 261 | } | |
| 260 | 262 | ||
| 263 | + | // WorkspaceStylingConfig defines visual styling for workspace panes. | |
| 264 | + | type WorkspaceStylingConfig struct { | |
| 265 | + | // BorderWidth in pixels for pane borders | |
| 266 | + | BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"` | |
| 267 | + | // BorderColor for focused panes (CSS color value or theme variable) | |
| 268 | + | BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"` | |
| 269 | + | // TransitionDuration in milliseconds for border animations | |
| 270 | + | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | |
| 271 | + | // BorderRadius in pixels for pane border corners | |
| 272 | + | BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"` | |
| 273 | + | } | |
| 274 | + | ||
| 261 | 275 | // Manager handles configuration loading, watching, and reloading. | |
| 262 | 276 | type Manager struct { | |
| 263 | 277 | config *Config | |
| @@ -664,6 +678,10 @@ func (m *Manager) setDefaults() { | |||
| 664 | 678 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | |
| 665 | 679 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | |
| 666 | 680 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | |
| 681 | + | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | |
| 682 | + | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | |
| 683 | + | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | |
| 684 | + | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | |
| 667 | 685 | } | |
| 668 | 686 | ||
| 669 | 687 | func (m *Manager) ensurePersistedDefaults(cfg *Config) { | |
| @@ -234,6 +234,8 @@ type WorkspaceConfig struct { | |||||
| 234 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"` | 234 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"` | ||
| 235 | // Popups configures default popup placement rules. | 235 | // Popups configures default popup placement rules. | ||
| 236 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"` | 236 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"` | ||
| 237 | + | // Styling configures workspace visual appearance. | |||
| 238 | + | Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" json:"styling"` | |||
| 237 | } | 239 | } | ||
| 238 | 240 | ||||
| 239 | // PaneModeConfig defines modal behaviour for pane management. | 241 | // PaneModeConfig defines modal behaviour for pane management. | ||
| @@ -258,6 +260,18 @@ type PopupBehaviorConfig struct { | |||||
| 258 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | 260 | FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"` | ||
| 259 | } | 261 | } | ||
| 260 | 262 | ||||
| 263 | + | // WorkspaceStylingConfig defines visual styling for workspace panes. | |||
| 264 | + | type WorkspaceStylingConfig struct { | |||
| 265 | + | // BorderWidth in pixels for pane borders | |||
| 266 | + | BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"` | |||
| 267 | + | // BorderColor for focused panes (CSS color value or theme variable) | |||
| 268 | + | BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"` | |||
| 269 | + | // TransitionDuration in milliseconds for border animations | |||
| 270 | + | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | |||
| 271 | + | // BorderRadius in pixels for pane border corners | |||
| 272 | + | BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"` | |||
| 273 | + | } | |||
| 274 | + | ||||
| 261 | // Manager handles configuration loading, watching, and reloading. | 275 | // Manager handles configuration loading, watching, and reloading. | ||
| 262 | type Manager struct { | 276 | type Manager struct { | ||
| 263 | config *Config | 277 | config *Config | ||
| @@ -664,6 +678,10 @@ func (m *Manager) setDefaults() { | |||||
| 664 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | 678 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | ||
| 665 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | 679 | m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane) | ||
| 666 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | 680 | m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext) | ||
| 681 | + | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | |||
| 682 | + | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | |||
| 683 | + | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | |||
| 684 | + | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | |||
| 667 | } | 685 | } | ||
| 668 | 686 | ||||
| 669 | func (m *Manager) ensurePersistedDefaults(cfg *Config) { | 687 | func (m *Manager) ensurePersistedDefaults(cfg *Config) { | ||
M internal/config/defaults.go +12 -0
| @@ -30,6 +30,12 @@ const ( | |||
| 30 | 30 | defaultPaneActivationShortcut = "cmdorctrl+p" | |
| 31 | 31 | defaultPaneTimeoutMilliseconds = 3000 | |
| 32 | 32 | defaultPopupPlacement = "right" | |
| 33 | + | ||
| 34 | + | // Workspace styling defaults | |
| 35 | + | defaultBorderWidth = 2 | |
| 36 | + | defaultBorderColor = "@theme_selected_bg_color" | |
| 37 | + | defaultTransitionDuration = 120 | |
| 38 | + | defaultBorderRadius = 0 | |
| 33 | 39 | ) | |
| 34 | 40 | ||
| 35 | 41 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | |
| @@ -181,6 +187,12 @@ func DefaultConfig() *Config { | |||
| 181 | 187 | OpenInNewPane: true, | |
| 182 | 188 | FollowPaneContext: true, | |
| 183 | 189 | }, | |
| 190 | + | Styling: WorkspaceStylingConfig{ | |
| 191 | + | BorderWidth: defaultBorderWidth, | |
| 192 | + | BorderColor: defaultBorderColor, | |
| 193 | + | TransitionDuration: defaultTransitionDuration, | |
| 194 | + | BorderRadius: defaultBorderRadius, | |
| 195 | + | }, | |
| 184 | 196 | }, | |
| 185 | 197 | } | |
| 186 | 198 | } | |
| @@ -30,6 +30,12 @@ const ( | |||||
| 30 | defaultPaneActivationShortcut = "cmdorctrl+p" | 30 | defaultPaneActivationShortcut = "cmdorctrl+p" | ||
| 31 | defaultPaneTimeoutMilliseconds = 3000 | 31 | defaultPaneTimeoutMilliseconds = 3000 | ||
| 32 | defaultPopupPlacement = "right" | 32 | defaultPopupPlacement = "right" | ||
| 33 | + | ||||
| 34 | + | // Workspace styling defaults | |||
| 35 | + | defaultBorderWidth = 2 | |||
| 36 | + | defaultBorderColor = "@theme_selected_bg_color" | |||
| 37 | + | defaultTransitionDuration = 120 | |||
| 38 | + | defaultBorderRadius = 0 | |||
| 33 | ) | 39 | ) | ||
| 34 | 40 | ||||
| 35 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | 41 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | ||
| @@ -181,6 +187,12 @@ func DefaultConfig() *Config { | |||||
| 181 | OpenInNewPane: true, | 187 | OpenInNewPane: true, | ||
| 182 | FollowPaneContext: true, | 188 | FollowPaneContext: true, | ||
| 183 | }, | 189 | }, | ||
| 190 | + | Styling: WorkspaceStylingConfig{ | |||
| 191 | + | BorderWidth: defaultBorderWidth, | |||
| 192 | + | BorderColor: defaultBorderColor, | |||
| 193 | + | TransitionDuration: defaultTransitionDuration, | |||
| 194 | + | BorderRadius: defaultBorderRadius, | |||
| 195 | + | }, | |||
| 184 | }, | 196 | }, | ||
| 185 | } | 197 | } | ||
| 186 | } | 198 | } | ||