refactor(workspace): migrate all borders to overlay architecture
- Implement GTK overlay pattern for all three border types (active pane, pane mode, tab mode) - Eliminate layout shifts when borders appear/disappear - Add configurable border widths: pane_mode_border_width and tab_mode_border_width (default: 4px) - Remove inactive border styling - only active panes show borders now - Remove unnecessary config options: inactive_border_width, inactive_border_color, show_stacked_title_border, border_radius - Create borders_overlay.go with unified overlay management - Simplify border application logic using visibility toggles instead of CSS class manipulation
- commit
- f101a25aec5f78558b927d01a98fbb425d6d8292
- author
- bnema · · 2025-11-18 18:11 UTC
- committer
- bnema · · 2025-11-18 22:24 UTC
- parent
- a703a07
- signature
- unsigned
14 files · +259 -296
M CHANGELOG.md +3 -0
| @@ -8,12 +8,15 @@ All notable changes to this project will be documented in this file. | |||
| 8 | 8 | - **Page load progress bar**: Thin animated indicator at window bottom showing web page loading progress. Tracks WebView load events with gradient animation, auto-hides when complete. | |
| 9 | 9 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | |
| 10 | 10 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | |
| 11 | + | - **Configurable border widths**: Added `pane_mode_border_width` and `tab_mode_border_width` configuration options (default: 4px) for full border customization. | |
| 11 | 12 | ||
| 12 | 13 | ### Fixed | |
| 13 | 14 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | |
| 14 | 15 | ||
| 15 | 16 | ### Changed | |
| 16 | 17 | - **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity. | |
| 18 | + | - **Border overlay architecture**: All borders (active pane, pane mode, tab mode) now use GTK overlay pattern preventing layout shifts when borders appear/disappear. Removed inactive border styling - only active panes show borders. | |
| 19 | + | - **Simplified border configuration**: Removed `inactive_border_width`, `inactive_border_color`, `show_stacked_title_border`, and `border_radius` config options. All borders are now overlay-based with no layout impact. | |
| 17 | 20 | ||
| 18 | 21 | ## [0.14.1] - 2025-11-18 | |
| 19 | 22 | ||
| @@ -8,12 +8,15 @@ All notable changes to this project will be documented in this file. | |||||
| 8 | - **Page load progress bar**: Thin animated indicator at window bottom showing web page loading progress. Tracks WebView load events with gradient animation, auto-hides when complete. | 8 | - **Page load progress bar**: Thin animated indicator at window bottom showing web page loading progress. Tracks WebView load events with gradient animation, auto-hides when complete. | ||
| 9 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | 9 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | ||
| 10 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | 10 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | ||
| 11 | + | - **Configurable border widths**: Added `pane_mode_border_width` and `tab_mode_border_width` configuration options (default: 4px) for full border customization. | |||
| 11 | 12 | ||||
| 12 | ### Fixed | 13 | ### Fixed | ||
| 13 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | 14 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | ||
| 14 | 15 | ||||
| 15 | ### Changed | 16 | ### Changed | ||
| 16 | - **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity. | 17 | - **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity. | ||
| 18 | + | - **Border overlay architecture**: All borders (active pane, pane mode, tab mode) now use GTK overlay pattern preventing layout shifts when borders appear/disappear. Removed inactive border styling - only active panes show borders. | |||
| 19 | + | - **Simplified border configuration**: Removed `inactive_border_width`, `inactive_border_color`, `show_stacked_title_border`, and `border_radius` config options. All borders are now overlay-based with no layout impact. | |||
| 17 | 20 | ||||
| 18 | ## [0.14.1] - 2025-11-18 | 21 | ## [0.14.1] - 2025-11-18 | ||
| 19 | 22 | ||||
M docs/CONFIG.md +4 -5
| @@ -273,15 +273,14 @@ cancel = ["escape"] | |||
| 273 | 273 | ||
| 274 | 274 | | Key | Type | Default | Description | | |
| 275 | 275 | |-----|------|---------|-------------| | |
| 276 | - | | `workspace.styling.border_width` | int | `1` | Active pane border width (px) | | |
| 276 | + | | `workspace.styling.border_width` | int | `1` | Active pane border width (px) - overlay | | |
| 277 | 277 | | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color | | |
| 278 | - | | `workspace.styling.inactive_border_width` | int | `1` | Inactive pane border width (px) | | |
| 279 | - | | `workspace.styling.inactive_border_color` | string | `"@theme_unfocused_bg_color"` | Inactive pane border color | | |
| 280 | - | | `workspace.styling.show_stacked_title_border` | bool | `false` | Show separator on stacked pane titles | | |
| 278 | + | | `workspace.styling.pane_mode_border_width` | int | `4` | Pane mode border width (px) - Ctrl+P N overlay | | |
| 281 | 279 | | `workspace.styling.pane_mode_border_color` | string | `"#4A90E2"` | Pane mode border color (blue) | | |
| 280 | + | | `workspace.styling.tab_mode_border_width` | int | `4` | Tab mode border width (px) - Ctrl+P T overlay | | |
| 282 | 281 | | `workspace.styling.tab_mode_border_color` | string | `"#FFA500"` | Tab mode border color (orange) | | |
| 283 | 282 | | `workspace.styling.transition_duration` | int | `120` | Border transition duration (ms) | | |
| 284 | - | | `workspace.styling.border_radius` | int | `0` | Border radius (px) | | |
| 283 | + | | `workspace.styling.ui_scale` | float | `1.0` | UI scale multiplier (1.0 = 100%, 1.2 = 120%) | | |
| 285 | 284 | ||
| 286 | 285 | ## Content Filtering | |
| 287 | 286 | ||
| @@ -273,15 +273,14 @@ cancel = ["escape"] | |||||
| 273 | 273 | ||||
| 274 | | Key | Type | Default | Description | | 274 | | Key | Type | Default | Description | | ||
| 275 | |-----|------|---------|-------------| | 275 | |-----|------|---------|-------------| | ||
| 276 | - | | `workspace.styling.border_width` | int | `1` | Active pane border width (px) | | 276 | + | | `workspace.styling.border_width` | int | `1` | Active pane border width (px) - overlay | |
| 277 | | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color | | 277 | | `workspace.styling.border_color` | string | `"@theme_selected_bg_color"` | Active pane border color | | ||
| 278 | - | | `workspace.styling.inactive_border_width` | int | `1` | Inactive pane border width (px) | | 278 | + | | `workspace.styling.pane_mode_border_width` | int | `4` | Pane mode border width (px) - Ctrl+P N overlay | |
| 279 | - | | `workspace.styling.inactive_border_color` | string | `"@theme_unfocused_bg_color"` | Inactive pane border color | | |||
| 280 | - | | `workspace.styling.show_stacked_title_border` | bool | `false` | Show separator on stacked pane titles | | |||
| 281 | | `workspace.styling.pane_mode_border_color` | string | `"#4A90E2"` | Pane mode border color (blue) | | 279 | | `workspace.styling.pane_mode_border_color` | string | `"#4A90E2"` | Pane mode border color (blue) | | ||
| 280 | + | | `workspace.styling.tab_mode_border_width` | int | `4` | Tab mode border width (px) - Ctrl+P T overlay | | |||
| 282 | | `workspace.styling.tab_mode_border_color` | string | `"#FFA500"` | Tab mode border color (orange) | | 281 | | `workspace.styling.tab_mode_border_color` | string | `"#FFA500"` | Tab mode border color (orange) | | ||
| 283 | | `workspace.styling.transition_duration` | int | `120` | Border transition duration (ms) | | 282 | | `workspace.styling.transition_duration` | int | `120` | Border transition duration (ms) | | ||
| 284 | - | | `workspace.styling.border_radius` | int | `0` | Border radius (px) | | 283 | + | | `workspace.styling.ui_scale` | float | `1.0` | UI scale multiplier (1.0 = 100%, 1.2 = 120%) | |
| 285 | 284 | ||||
| 286 | ## Content Filtering | 285 | ## Content Filtering | ||
| 287 | 286 | ||||
A internal/app/browser/borders_overlay.go +95 -0
| @@ -0,0 +1,95 @@ | |||
| 1 | + | package browser | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/dumber/internal/logging" | |
| 5 | + | "github.com/bnema/dumber/pkg/webkit" | |
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // createBorderOverlay builds a transparent overlay widget that displays as a border when visible. | |
| 10 | + | // This allows showing visual borders without affecting layout or causing content shifts. | |
| 11 | + | func (tm *TabManager) createBorderOverlay(cssClass string) gtk.Widgetter { | |
| 12 | + | box := gtk.NewBox(gtk.OrientationVertical, 0) | |
| 13 | + | if box == nil { | |
| 14 | + | logging.Error("[tabs] Failed to create border overlay for " + cssClass) | |
| 15 | + | return nil | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | box.AddCSSClass(cssClass) | |
| 19 | + | box.SetHExpand(true) | |
| 20 | + | box.SetVExpand(true) | |
| 21 | + | box.SetCanTarget(false) // Don't intercept mouse events - overlay is visual only | |
| 22 | + | ||
| 23 | + | return box | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // wrapPaneInOverlay wraps a pane container widget in a gtk.Overlay with a border overlay. | |
| 27 | + | // This allows showing active pane borders without affecting layout or causing content shifts. | |
| 28 | + | // Returns the overlay (new container) and the border widget (to store in paneNode.borderOverlay). | |
| 29 | + | func wrapPaneInOverlay(paneContainer gtk.Widgetter) (*gtk.Overlay, gtk.Widgetter) { | |
| 30 | + | // Create the overlay container | |
| 31 | + | overlay := gtk.NewOverlay() | |
| 32 | + | if overlay == nil { | |
| 33 | + | logging.Error("[workspace] Failed to create overlay for pane border") | |
| 34 | + | return nil, nil | |
| 35 | + | } | |
| 36 | + | overlay.SetHExpand(true) | |
| 37 | + | overlay.SetVExpand(true) | |
| 38 | + | ||
| 39 | + | // Set the pane container as the main child | |
| 40 | + | overlay.SetChild(paneContainer) | |
| 41 | + | ||
| 42 | + | // Create the border overlay widget (transparent box with border CSS) | |
| 43 | + | borderBox := gtk.NewBox(gtk.OrientationVertical, 0) | |
| 44 | + | if borderBox == nil { | |
| 45 | + | logging.Error("[workspace] Failed to create border overlay box") | |
| 46 | + | return overlay, nil | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | borderBox.AddCSSClass("pane-border-overlay") | |
| 50 | + | borderBox.SetHExpand(true) | |
| 51 | + | borderBox.SetVExpand(true) | |
| 52 | + | borderBox.SetCanTarget(false) // Don't intercept mouse events - overlay is visual only | |
| 53 | + | ||
| 54 | + | // Add the border overlay to the overlay (hidden by default) | |
| 55 | + | overlay.AddOverlay(borderBox) | |
| 56 | + | webkit.WidgetSetVisible(borderBox, false) | |
| 57 | + | ||
| 58 | + | return overlay, borderBox | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // showPaneModeBorder displays the pane mode border overlay. | |
| 62 | + | func (tm *TabManager) showPaneModeBorder() { | |
| 63 | + | if tm.paneModeOverlay == nil { | |
| 64 | + | return | |
| 65 | + | } | |
| 66 | + | webkit.WidgetSetVisible(tm.paneModeOverlay, true) | |
| 67 | + | logging.Debug("[pane-mode] Showing pane mode border overlay") | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // hidePaneModeBorder hides the pane mode border overlay. | |
| 71 | + | func (tm *TabManager) hidePaneModeBorder() { | |
| 72 | + | if tm.paneModeOverlay == nil { | |
| 73 | + | return | |
| 74 | + | } | |
| 75 | + | webkit.WidgetSetVisible(tm.paneModeOverlay, false) | |
| 76 | + | logging.Debug("[pane-mode] Hiding pane mode border overlay") | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // showTabModeBorder displays the tab mode border overlay. | |
| 80 | + | func (tm *TabManager) showTabModeBorder() { | |
| 81 | + | if tm.tabModeOverlay == nil { | |
| 82 | + | return | |
| 83 | + | } | |
| 84 | + | webkit.WidgetSetVisible(tm.tabModeOverlay, true) | |
| 85 | + | logging.Debug("[tab-mode] Showing tab mode border overlay") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // hideTabModeBorder hides the tab mode border overlay. | |
| 89 | + | func (tm *TabManager) hideTabModeBorder() { | |
| 90 | + | if tm.tabModeOverlay == nil { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | webkit.WidgetSetVisible(tm.tabModeOverlay, false) | |
| 94 | + | logging.Debug("[tab-mode] Hiding tab mode border overlay") | |
| 95 | + | } | |
| @@ -0,0 +1,95 @@ | |||||
| 1 | + | package browser | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/dumber/internal/logging" | |||
| 5 | + | "github.com/bnema/dumber/pkg/webkit" | |||
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // createBorderOverlay builds a transparent overlay widget that displays as a border when visible. | |||
| 10 | + | // This allows showing visual borders without affecting layout or causing content shifts. | |||
| 11 | + | func (tm *TabManager) createBorderOverlay(cssClass string) gtk.Widgetter { | |||
| 12 | + | box := gtk.NewBox(gtk.OrientationVertical, 0) | |||
| 13 | + | if box == nil { | |||
| 14 | + | logging.Error("[tabs] Failed to create border overlay for " + cssClass) | |||
| 15 | + | return nil | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | box.AddCSSClass(cssClass) | |||
| 19 | + | box.SetHExpand(true) | |||
| 20 | + | box.SetVExpand(true) | |||
| 21 | + | box.SetCanTarget(false) // Don't intercept mouse events - overlay is visual only | |||
| 22 | + | ||||
| 23 | + | return box | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // wrapPaneInOverlay wraps a pane container widget in a gtk.Overlay with a border overlay. | |||
| 27 | + | // This allows showing active pane borders without affecting layout or causing content shifts. | |||
| 28 | + | // Returns the overlay (new container) and the border widget (to store in paneNode.borderOverlay). | |||
| 29 | + | func wrapPaneInOverlay(paneContainer gtk.Widgetter) (*gtk.Overlay, gtk.Widgetter) { | |||
| 30 | + | // Create the overlay container | |||
| 31 | + | overlay := gtk.NewOverlay() | |||
| 32 | + | if overlay == nil { | |||
| 33 | + | logging.Error("[workspace] Failed to create overlay for pane border") | |||
| 34 | + | return nil, nil | |||
| 35 | + | } | |||
| 36 | + | overlay.SetHExpand(true) | |||
| 37 | + | overlay.SetVExpand(true) | |||
| 38 | + | ||||
| 39 | + | // Set the pane container as the main child | |||
| 40 | + | overlay.SetChild(paneContainer) | |||
| 41 | + | ||||
| 42 | + | // Create the border overlay widget (transparent box with border CSS) | |||
| 43 | + | borderBox := gtk.NewBox(gtk.OrientationVertical, 0) | |||
| 44 | + | if borderBox == nil { | |||
| 45 | + | logging.Error("[workspace] Failed to create border overlay box") | |||
| 46 | + | return overlay, nil | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | borderBox.AddCSSClass("pane-border-overlay") | |||
| 50 | + | borderBox.SetHExpand(true) | |||
| 51 | + | borderBox.SetVExpand(true) | |||
| 52 | + | borderBox.SetCanTarget(false) // Don't intercept mouse events - overlay is visual only | |||
| 53 | + | ||||
| 54 | + | // Add the border overlay to the overlay (hidden by default) | |||
| 55 | + | overlay.AddOverlay(borderBox) | |||
| 56 | + | webkit.WidgetSetVisible(borderBox, false) | |||
| 57 | + | ||||
| 58 | + | return overlay, borderBox | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // showPaneModeBorder displays the pane mode border overlay. | |||
| 62 | + | func (tm *TabManager) showPaneModeBorder() { | |||
| 63 | + | if tm.paneModeOverlay == nil { | |||
| 64 | + | return | |||
| 65 | + | } | |||
| 66 | + | webkit.WidgetSetVisible(tm.paneModeOverlay, true) | |||
| 67 | + | logging.Debug("[pane-mode] Showing pane mode border overlay") | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // hidePaneModeBorder hides the pane mode border overlay. | |||
| 71 | + | func (tm *TabManager) hidePaneModeBorder() { | |||
| 72 | + | if tm.paneModeOverlay == nil { | |||
| 73 | + | return | |||
| 74 | + | } | |||
| 75 | + | webkit.WidgetSetVisible(tm.paneModeOverlay, false) | |||
| 76 | + | logging.Debug("[pane-mode] Hiding pane mode border overlay") | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // showTabModeBorder displays the tab mode border overlay. | |||
| 80 | + | func (tm *TabManager) showTabModeBorder() { | |||
| 81 | + | if tm.tabModeOverlay == nil { | |||
| 82 | + | return | |||
| 83 | + | } | |||
| 84 | + | webkit.WidgetSetVisible(tm.tabModeOverlay, true) | |||
| 85 | + | logging.Debug("[tab-mode] Showing tab mode border overlay") | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // hideTabModeBorder hides the tab mode border overlay. | |||
| 89 | + | func (tm *TabManager) hideTabModeBorder() { | |||
| 90 | + | if tm.tabModeOverlay == nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | webkit.WidgetSetVisible(tm.tabModeOverlay, false) | |||
| 94 | + | logging.Debug("[tab-mode] Hiding tab mode border overlay") | |||
| 95 | + | } | |||
M internal/app/browser/stacked_panes.go +8 -0
| @@ -99,6 +99,14 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) { | |||
| 99 | 99 | // Initialize widgets properly using workspace manager helper | |
| 100 | 100 | spm.wm.initializePaneWidgets(newLeaf, newContainer) | |
| 101 | 101 | ||
| 102 | + | // After wrapping the WebView in an overlay, operate on that container. | |
| 103 | + | newContainer = newLeaf.container | |
| 104 | + | if newContainer == nil { | |
| 105 | + | return nil, errors.New("new stacked pane missing container overlay") | |
| 106 | + | } | |
| 107 | + | webkit.WidgetSetHExpand(newContainer, true) | |
| 108 | + | webkit.WidgetSetVExpand(newContainer, true) | |
| 109 | + | ||
| 102 | 110 | // Create title bar for the new pane | |
| 103 | 111 | newTitleBar := spm.createTitleBar(newLeaf) | |
| 104 | 112 | spm.wm.setTitleBar(newLeaf, newTitleBar) | |
| @@ -99,6 +99,14 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) { | |||||
| 99 | // Initialize widgets properly using workspace manager helper | 99 | // Initialize widgets properly using workspace manager helper | ||
| 100 | spm.wm.initializePaneWidgets(newLeaf, newContainer) | 100 | spm.wm.initializePaneWidgets(newLeaf, newContainer) | ||
| 101 | 101 | ||||
| 102 | + | // After wrapping the WebView in an overlay, operate on that container. | |||
| 103 | + | newContainer = newLeaf.container | |||
| 104 | + | if newContainer == nil { | |||
| 105 | + | return nil, errors.New("new stacked pane missing container overlay") | |||
| 106 | + | } | |||
| 107 | + | webkit.WidgetSetHExpand(newContainer, true) | |||
| 108 | + | webkit.WidgetSetVExpand(newContainer, true) | |||
| 109 | + | ||||
| 102 | // Create title bar for the new pane | 110 | // Create title bar for the new pane | ||
| 103 | newTitleBar := spm.createTitleBar(newLeaf) | 111 | newTitleBar := spm.createTitleBar(newLeaf) | ||
| 104 | spm.wm.setTitleBar(newLeaf, newTitleBar) | 112 | spm.wm.setTitleBar(newLeaf, newTitleBar) | ||
M internal/app/browser/tab_manager.go +20 -1
| @@ -23,9 +23,12 @@ type TabManager struct { | |||
| 23 | 23 | rootContainer gtk.Widgetter // Main vertical box containing tab bar + content | |
| 24 | 24 | tabBar gtk.Widgetter // Horizontal tab bar container | |
| 25 | 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | |
| 26 | - | tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins | |
| 27 | 26 | progressBar gtk.Widgetter // Thin load progress indicator | |
| 28 | 27 | ||
| 28 | + | // Border overlays (float over content without layout shift) | |
| 29 | + | paneModeOverlay gtk.Widgetter // Border overlay for pane mode | |
| 30 | + | tabModeOverlay gtk.Widgetter // Border overlay for tab mode | |
| 31 | + | ||
| 29 | 32 | // Modal state | |
| 30 | 33 | tabModeActive bool | |
| 31 | 34 | tabModeTimer *time.Timer | |
| @@ -159,11 +162,27 @@ func (tm *TabManager) createRootContainer() error { | |||
| 159 | 162 | rootOverlay.AddOverlay(progressBar) | |
| 160 | 163 | } | |
| 161 | 164 | ||
| 165 | + | // Create border overlays (hidden by default, shown when pane/tab mode is active) | |
| 166 | + | paneModeOverlay := tm.createBorderOverlay("pane-mode-border") | |
| 167 | + | tabModeOverlay := tm.createBorderOverlay("tab-mode-border") | |
| 168 | + | ||
| 169 | + | if paneModeOverlay != nil { | |
| 170 | + | rootOverlay.AddOverlay(paneModeOverlay) | |
| 171 | + | webkit.WidgetSetVisible(paneModeOverlay, false) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | if tabModeOverlay != nil { | |
| 175 | + | rootOverlay.AddOverlay(tabModeOverlay) | |
| 176 | + | webkit.WidgetSetVisible(tabModeOverlay, false) | |
| 177 | + | } | |
| 178 | + | ||
| 162 | 179 | // Store references | |
| 163 | 180 | tm.rootContainer = rootOverlay | |
| 164 | 181 | tm.tabBar = tabBar | |
| 165 | 182 | tm.ContentArea = contentArea | |
| 166 | 183 | tm.progressBar = progressBar | |
| 184 | + | tm.paneModeOverlay = paneModeOverlay | |
| 185 | + | tm.tabModeOverlay = tabModeOverlay | |
| 167 | 186 | ||
| 168 | 187 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | |
| 169 | 188 | return nil | |
| @@ -23,9 +23,12 @@ type TabManager struct { | |||||
| 23 | rootContainer gtk.Widgetter // Main vertical box containing tab bar + content | 23 | rootContainer gtk.Widgetter // Main vertical box containing tab bar + content | ||
| 24 | tabBar gtk.Widgetter // Horizontal tab bar container | 24 | tabBar gtk.Widgetter // Horizontal tab bar container | ||
| 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | ||
| 26 | - | tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins | |||
| 27 | progressBar gtk.Widgetter // Thin load progress indicator | 26 | progressBar gtk.Widgetter // Thin load progress indicator | ||
| 28 | 27 | ||||
| 28 | + | // Border overlays (float over content without layout shift) | |||
| 29 | + | paneModeOverlay gtk.Widgetter // Border overlay for pane mode | |||
| 30 | + | tabModeOverlay gtk.Widgetter // Border overlay for tab mode | |||
| 31 | + | ||||
| 29 | // Modal state | 32 | // Modal state | ||
| 30 | tabModeActive bool | 33 | tabModeActive bool | ||
| 31 | tabModeTimer *time.Timer | 34 | tabModeTimer *time.Timer | ||
| @@ -159,11 +162,27 @@ func (tm *TabManager) createRootContainer() error { | |||||
| 159 | rootOverlay.AddOverlay(progressBar) | 162 | rootOverlay.AddOverlay(progressBar) | ||
| 160 | } | 163 | } | ||
| 161 | 164 | ||||
| 165 | + | // Create border overlays (hidden by default, shown when pane/tab mode is active) | |||
| 166 | + | paneModeOverlay := tm.createBorderOverlay("pane-mode-border") | |||
| 167 | + | tabModeOverlay := tm.createBorderOverlay("tab-mode-border") | |||
| 168 | + | ||||
| 169 | + | if paneModeOverlay != nil { | |||
| 170 | + | rootOverlay.AddOverlay(paneModeOverlay) | |||
| 171 | + | webkit.WidgetSetVisible(paneModeOverlay, false) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | if tabModeOverlay != nil { | |||
| 175 | + | rootOverlay.AddOverlay(tabModeOverlay) | |||
| 176 | + | webkit.WidgetSetVisible(tabModeOverlay, false) | |||
| 177 | + | } | |||
| 178 | + | ||||
| 162 | // Store references | 179 | // Store references | ||
| 163 | tm.rootContainer = rootOverlay | 180 | tm.rootContainer = rootOverlay | ||
| 164 | tm.tabBar = tabBar | 181 | tm.tabBar = tabBar | ||
| 165 | tm.ContentArea = contentArea | 182 | tm.ContentArea = contentArea | ||
| 166 | tm.progressBar = progressBar | 183 | tm.progressBar = progressBar | ||
| 184 | + | tm.paneModeOverlay = paneModeOverlay | |||
| 185 | + | tm.tabModeOverlay = tabModeOverlay | |||
| 167 | 186 | ||||
| 168 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | 187 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | ||
| 169 | return nil | 188 | return nil | ||
M internal/app/browser/tab_mode.go +4 -79
| @@ -5,8 +5,6 @@ import ( | |||
| 5 | 5 | "time" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/internal/logging" | |
| 8 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 9 | - | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 10 | 8 | ) | |
| 11 | 9 | ||
| 12 | 10 | // EnterTabMode activates the modal tab management mode. | |
| @@ -202,88 +200,15 @@ func (tm *TabManager) handleRenameTab() { | |||
| 202 | 200 | } | |
| 203 | 201 | ||
| 204 | 202 | // applyTabModeBorder applies visual indicator when tab mode is active. | |
| 205 | - | // This adds an orange border around the entire workspace area. | |
| 203 | + | // Uses an overlay widget to show the border without affecting layout. | |
| 206 | 204 | func (tm *TabManager) applyTabModeBorder() { | |
| 207 | - | logging.Debug("[tab-mode] applyTabModeBorder: entry") | |
| 208 | - | ||
| 209 | - | if tm.window == nil { | |
| 210 | - | logging.Debug("[tab-mode] applyTabModeBorder: window is nil, returning") | |
| 211 | - | return | |
| 212 | - | } | |
| 213 | - | ||
| 214 | - | // Determine which container should receive the margins: | |
| 215 | - | // - Prefer the root container (direct child of the window) so the window background shows | |
| 216 | - | // - Fallback to ContentArea for older builds | |
| 217 | - | var target gtk.Widgetter | |
| 218 | - | if tm.rootContainer != nil { | |
| 219 | - | target = tm.rootContainer | |
| 220 | - | logging.Debug("[tab-mode] applyTabModeBorder: using root container for border") | |
| 221 | - | } else if tm.ContentArea != nil { | |
| 222 | - | target = tm.ContentArea | |
| 223 | - | logging.Debug("[tab-mode] applyTabModeBorder: using content area fallback for border") | |
| 224 | - | } else { | |
| 225 | - | logging.Debug("[tab-mode] applyTabModeBorder: no container available, returning") | |
| 226 | - | return | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | // Remember the container for removal | |
| 230 | - | tm.tabModeTarget = target | |
| 231 | - | ||
| 232 | - | // Apply 4px margins to create space for the border | |
| 233 | - | webkit.WidgetSetMargin(target, 4) | |
| 234 | - | ||
| 235 | - | // Add CSS class to window for background color (the "border" color shows in the margin space) | |
| 236 | - | gtkWindow := tm.window.AsWindow() | |
| 237 | - | webkit.WidgetAddCSSClass(gtkWindow, "tab-mode-active") | |
| 238 | - | ||
| 239 | - | // Force resize/allocation to apply margin changes immediately | |
| 240 | - | webkit.WidgetQueueResize(target) | |
| 241 | - | webkit.WidgetQueueAllocate(target) | |
| 242 | - | ||
| 243 | - | // Queue redraw to show changes | |
| 244 | - | webkit.WidgetQueueDraw(gtkWindow) | |
| 245 | - | webkit.WidgetQueueDraw(target) | |
| 246 | - | ||
| 247 | - | logging.Debug("[tab-mode] applyTabModeBorder: border applied successfully") | |
| 205 | + | tm.showTabModeBorder() | |
| 248 | 206 | } | |
| 249 | 207 | ||
| 250 | 208 | // removeTabModeBorder removes the visual indicator when exiting tab mode. | |
| 209 | + | // Uses an overlay widget to hide the border without affecting layout. | |
| 251 | 210 | func (tm *TabManager) removeTabModeBorder() { | |
| 252 | - | if tm.window == nil { | |
| 253 | - | return | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | // Use whichever container received margins | |
| 257 | - | target := tm.tabModeTarget | |
| 258 | - | if target == nil { | |
| 259 | - | if tm.rootContainer != nil { | |
| 260 | - | target = tm.rootContainer | |
| 261 | - | } else { | |
| 262 | - | target = tm.ContentArea | |
| 263 | - | } | |
| 264 | - | } | |
| 265 | - | if target == nil { | |
| 266 | - | return | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | // Remove margins | |
| 270 | - | webkit.WidgetSetMargin(target, 0) | |
| 271 | - | ||
| 272 | - | // Remove CSS class from window | |
| 273 | - | gtkWindow := tm.window.AsWindow() | |
| 274 | - | webkit.WidgetRemoveCSSClass(gtkWindow, "tab-mode-active") | |
| 275 | - | ||
| 276 | - | // Force resize/allocation to apply margin changes immediately | |
| 277 | - | webkit.WidgetQueueResize(target) | |
| 278 | - | webkit.WidgetQueueAllocate(target) | |
| 279 | - | ||
| 280 | - | // Queue redraw to show changes | |
| 281 | - | webkit.WidgetQueueDraw(gtkWindow) | |
| 282 | - | webkit.WidgetQueueDraw(target) | |
| 283 | - | ||
| 284 | - | tm.tabModeTarget = nil | |
| 285 | - | ||
| 286 | - | logging.Debug("[tab-mode] Removed tab mode visual indicator") | |
| 211 | + | tm.hideTabModeBorder() | |
| 287 | 212 | } | |
| 288 | 213 | ||
| 289 | 214 | // dispatchTabModeEventInternal sends an event without acquiring locks. | |
| @@ -5,8 +5,6 @@ import ( | |||||
| 5 | "time" | 5 | "time" | ||
| 6 | 6 | ||||
| 7 | "github.com/bnema/dumber/internal/logging" | 7 | "github.com/bnema/dumber/internal/logging" | ||
| 8 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 9 | - | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 10 | ) | 8 | ) | ||
| 11 | 9 | ||||
| 12 | // EnterTabMode activates the modal tab management mode. | 10 | // EnterTabMode activates the modal tab management mode. | ||
| @@ -202,88 +200,15 @@ func (tm *TabManager) handleRenameTab() { | |||||
| 202 | } | 200 | } | ||
| 203 | 201 | ||||
| 204 | // applyTabModeBorder applies visual indicator when tab mode is active. | 202 | // applyTabModeBorder applies visual indicator when tab mode is active. | ||
| 205 | - | // This adds an orange border around the entire workspace area. | 203 | + | // Uses an overlay widget to show the border without affecting layout. |
| 206 | func (tm *TabManager) applyTabModeBorder() { | 204 | func (tm *TabManager) applyTabModeBorder() { | ||
| 207 | - | logging.Debug("[tab-mode] applyTabModeBorder: entry") | 205 | + | tm.showTabModeBorder() |
| 208 | - | ||||
| 209 | - | if tm.window == nil { | |||
| 210 | - | logging.Debug("[tab-mode] applyTabModeBorder: window is nil, returning") | |||
| 211 | - | return | |||
| 212 | - | } | |||
| 213 | - | ||||
| 214 | - | // Determine which container should receive the margins: | |||
| 215 | - | // - Prefer the root container (direct child of the window) so the window background shows | |||
| 216 | - | // - Fallback to ContentArea for older builds | |||
| 217 | - | var target gtk.Widgetter | |||
| 218 | - | if tm.rootContainer != nil { | |||
| 219 | - | target = tm.rootContainer | |||
| 220 | - | logging.Debug("[tab-mode] applyTabModeBorder: using root container for border") | |||
| 221 | - | } else if tm.ContentArea != nil { | |||
| 222 | - | target = tm.ContentArea | |||
| 223 | - | logging.Debug("[tab-mode] applyTabModeBorder: using content area fallback for border") | |||
| 224 | - | } else { | |||
| 225 | - | logging.Debug("[tab-mode] applyTabModeBorder: no container available, returning") | |||
| 226 | - | return | |||
| 227 | - | } | |||
| 228 | - | ||||
| 229 | - | // Remember the container for removal | |||
| 230 | - | tm.tabModeTarget = target | |||
| 231 | - | ||||
| 232 | - | // Apply 4px margins to create space for the border | |||
| 233 | - | webkit.WidgetSetMargin(target, 4) | |||
| 234 | - | ||||
| 235 | - | // Add CSS class to window for background color (the "border" color shows in the margin space) | |||
| 236 | - | gtkWindow := tm.window.AsWindow() | |||
| 237 | - | webkit.WidgetAddCSSClass(gtkWindow, "tab-mode-active") | |||
| 238 | - | ||||
| 239 | - | // Force resize/allocation to apply margin changes immediately | |||
| 240 | - | webkit.WidgetQueueResize(target) | |||
| 241 | - | webkit.WidgetQueueAllocate(target) | |||
| 242 | - | ||||
| 243 | - | // Queue redraw to show changes | |||
| 244 | - | webkit.WidgetQueueDraw(gtkWindow) | |||
| 245 | - | webkit.WidgetQueueDraw(target) | |||
| 246 | - | ||||
| 247 | - | logging.Debug("[tab-mode] applyTabModeBorder: border applied successfully") | |||
| 248 | } | 206 | } | ||
| 249 | 207 | ||||
| 250 | // removeTabModeBorder removes the visual indicator when exiting tab mode. | 208 | // removeTabModeBorder removes the visual indicator when exiting tab mode. | ||
| 209 | + | // Uses an overlay widget to hide the border without affecting layout. | |||
| 251 | func (tm *TabManager) removeTabModeBorder() { | 210 | func (tm *TabManager) removeTabModeBorder() { | ||
| 252 | - | if tm.window == nil { | 211 | + | tm.hideTabModeBorder() |
| 253 | - | return | |||
| 254 | - | } | |||
| 255 | - | ||||
| 256 | - | // Use whichever container received margins | |||
| 257 | - | target := tm.tabModeTarget | |||
| 258 | - | if target == nil { | |||
| 259 | - | if tm.rootContainer != nil { | |||
| 260 | - | target = tm.rootContainer | |||
| 261 | - | } else { | |||
| 262 | - | target = tm.ContentArea | |||
| 263 | - | } | |||
| 264 | - | } | |||
| 265 | - | if target == nil { | |||
| 266 | - | return | |||
| 267 | - | } | |||
| 268 | - | ||||
| 269 | - | // Remove margins | |||
| 270 | - | webkit.WidgetSetMargin(target, 0) | |||
| 271 | - | ||||
| 272 | - | // Remove CSS class from window | |||
| 273 | - | gtkWindow := tm.window.AsWindow() | |||
| 274 | - | webkit.WidgetRemoveCSSClass(gtkWindow, "tab-mode-active") | |||
| 275 | - | ||||
| 276 | - | // Force resize/allocation to apply margin changes immediately | |||
| 277 | - | webkit.WidgetQueueResize(target) | |||
| 278 | - | webkit.WidgetQueueAllocate(target) | |||
| 279 | - | ||||
| 280 | - | // Queue redraw to show changes | |||
| 281 | - | webkit.WidgetQueueDraw(gtkWindow) | |||
| 282 | - | webkit.WidgetQueueDraw(target) | |||
| 283 | - | ||||
| 284 | - | tm.tabModeTarget = nil | |||
| 285 | - | ||||
| 286 | - | logging.Debug("[tab-mode] Removed tab mode visual indicator") | |||
| 287 | } | 212 | } | ||
| 288 | 213 | ||||
| 289 | // dispatchTabModeEventInternal sends an event without acquiring locks. | 214 | // dispatchTabModeEventInternal sends an event without acquiring locks. | ||
M internal/app/browser/workspace_css.go +34 -59
| @@ -95,25 +95,6 @@ func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool | |||
| 95 | 95 | return "#87CEEB" // Sky blue pastel for light theme | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | - | // getInactivePaneBorderColor returns the border color for inactive panes based on config and theme | |
| 99 | - | func getInactivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | |
| 100 | - | // Use configured border color if set | |
| 101 | - | if styling.InactiveBorderColor != "" { | |
| 102 | - | // Check if it's a GTK theme variable (starts with @) | |
| 103 | - | if strings.HasPrefix(styling.InactiveBorderColor, "@") { | |
| 104 | - | return styling.InactiveBorderColor | |
| 105 | - | } | |
| 106 | - | // Return the configured color as-is (could be hex, rgb, etc.) | |
| 107 | - | return styling.InactiveBorderColor | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // Fallback to hardcoded colors based on theme | |
| 111 | - | if isDark { | |
| 112 | - | return "#333333" // Dark border for dark theme | |
| 113 | - | } | |
| 114 | - | return "#dddddd" // Light border for light theme | |
| 115 | - | } | |
| 116 | - | ||
| 117 | 98 | // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes | |
| 118 | 99 | func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |
| 119 | 100 | cfg := config.Get() | |
| @@ -129,7 +110,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 129 | 110 | } | |
| 130 | 111 | ||
| 131 | 112 | activeBorderColor := getActivePaneBorderColor(styling, isDark) | |
| 132 | - | inactiveBorderColor := getInactivePaneBorderColor(styling, isDark) | |
| 133 | 113 | ||
| 134 | 114 | // Pane mode border color - use configured value or fallback to blue | |
| 135 | 115 | paneModeColor := styling.PaneModeBorderColor | |
| @@ -163,33 +143,42 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 163 | 143 | margin: 0; | |
| 164 | 144 | } | |
| 165 | 145 | ||
| 166 | - | /* Pane mode active - change window background to border color */ | |
| 167 | - | window.pane-mode-active { | |
| 168 | - | background-color: %s; | |
| 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; | |
| 169 | 171 | } | |
| 170 | 172 | ||
| 171 | 173 | /* Workspace root container */ | |
| 172 | 174 | paned, box { | |
| 173 | 175 | background-color: %s; | |
| 174 | - | transition: margin 150ms ease-in-out; | |
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | - | /* Base pane styling - configurable inactive borders */ | |
| 178 | + | /* Base pane styling - no borders (using overlays instead) */ | |
| 178 | 179 | .workspace-pane, .stacked-pane-container { | |
| 179 | - | border-width: %dpx; | |
| 180 | - | border-style: solid; | |
| 181 | - | border-color: %s; | |
| 182 | - | border-radius: %dpx; | |
| 180 | + | border: none; | |
| 183 | 181 | margin: 0; | |
| 184 | - | transition-property: border-color; | |
| 185 | - | transition-duration: %dms; | |
| 186 | - | transition-timing-function: ease-in-out; | |
| 187 | - | } | |
| 188 | - | ||
| 189 | - | /* Active pane border styling */ | |
| 190 | - | .workspace-pane-active { | |
| 191 | - | border-width: %dpx; | |
| 192 | - | border-color: %s; | |
| 193 | 182 | } | |
| 194 | 183 | ||
| 195 | 184 | /* Stacked panes styling */ | |
| @@ -197,12 +186,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 197 | 186 | background-color: %s; | |
| 198 | 187 | } | |
| 199 | 188 | ||
| 200 | - | /* Stacked pane containers keep inactive border when active */ | |
| 201 | - | .stacked-pane-container.workspace-pane-active { | |
| 202 | - | border-width: %dpx; | |
| 203 | - | border-color: %s; | |
| 204 | - | } | |
| 205 | - | ||
| 206 | 189 | .stacked-pane-title { | |
| 207 | 190 | background-color: %s; | |
| 208 | 191 | border-bottom: %s; | |
| @@ -267,11 +250,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 267 | 250 | animation: tab-progress-stripe 1.3s linear infinite; | |
| 268 | 251 | } | |
| 269 | 252 | ||
| 270 | - | /* Tab mode active - window background changes to border color (like pane mode) */ | |
| 271 | - | window.tab-mode-active { | |
| 272 | - | background-color: %s; | |
| 273 | - | } | |
| 274 | - | ||
| 275 | 253 | /* Tab button styling - matches stacked pane titles */ | |
| 276 | 254 | button.tab-button { | |
| 277 | 255 | background-color: %s; | |
| @@ -345,17 +323,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 345 | 323 | 100%% { background-position: -100%% 0%%; } | |
| 346 | 324 | }`, | |
| 347 | 325 | windowBackgroundColor, // window background | |
| 348 | - | paneModeColor, // window.pane-mode-active background (border color) | |
| 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 | |
| 349 | 333 | windowBackgroundColor, // paned, box background | |
| 350 | - | styling.InactiveBorderWidth, // base pane border-width (inactive) | |
| 351 | - | inactiveBorderColor, // base pane border-color (inactive) | |
| 352 | - | styling.BorderRadius, // base pane border radius | |
| 353 | - | styling.TransitionDuration, // transition-duration | |
| 354 | - | styling.BorderWidth, // workspace-pane-active border-width | |
| 355 | - | activeBorderColor, // workspace-pane-active border color | |
| 356 | 334 | windowBackgroundColor, // stacked-pane-container background | |
| 357 | - | styling.InactiveBorderWidth, // stacked-pane-container.active border-width | |
| 358 | - | inactiveBorderColor, // stacked-pane-container.active border-color | |
| 359 | 335 | getStackTitleBg(isDark), // stacked-pane-title background | |
| 360 | 336 | stackedTitleBorder, // stacked-pane-title border-bottom | |
| 361 | 337 | titlePaddingVertical, // stacked-pane-title padding vertical | |
| @@ -373,7 +349,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 373 | 349 | "#606060", // tab progress gradient start (neutral gray) | |
| 374 | 350 | "#747474", // tab progress gradient middle (neutral gray) | |
| 375 | 351 | "#8a8a8a", // tab progress gradient end (neutral gray) | |
| 376 | - | tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue) | |
| 377 | 352 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | |
| 378 | 353 | getStackTitleBorderColor(isDark), // tab-button border-right | |
| 379 | 354 | titlePaddingVertical, // tab-button padding vertical | |
| @@ -95,25 +95,6 @@ func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool | |||||
| 95 | return "#87CEEB" // Sky blue pastel for light theme | 95 | return "#87CEEB" // Sky blue pastel for light theme | ||
| 96 | } | 96 | } | ||
| 97 | 97 | ||||
| 98 | - | // getInactivePaneBorderColor returns the border color for inactive panes based on config and theme | |||
| 99 | - | func getInactivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | |||
| 100 | - | // Use configured border color if set | |||
| 101 | - | if styling.InactiveBorderColor != "" { | |||
| 102 | - | // Check if it's a GTK theme variable (starts with @) | |||
| 103 | - | if strings.HasPrefix(styling.InactiveBorderColor, "@") { | |||
| 104 | - | return styling.InactiveBorderColor | |||
| 105 | - | } | |||
| 106 | - | // Return the configured color as-is (could be hex, rgb, etc.) | |||
| 107 | - | return styling.InactiveBorderColor | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | // Fallback to hardcoded colors based on theme | |||
| 111 | - | if isDark { | |||
| 112 | - | return "#333333" // Dark border for dark theme | |||
| 113 | - | } | |||
| 114 | - | return "#dddddd" // Light border for light theme | |||
| 115 | - | } | |||
| 116 | - | ||||
| 117 | // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes | 98 | // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes | ||
| 118 | func (wm *WorkspaceManager) generateWorkspaceCSS() string { | 99 | func (wm *WorkspaceManager) generateWorkspaceCSS() string { | ||
| 119 | cfg := config.Get() | 100 | cfg := config.Get() | ||
| @@ -129,7 +110,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 129 | } | 110 | } | ||
| 130 | 111 | ||||
| 131 | activeBorderColor := getActivePaneBorderColor(styling, isDark) | 112 | activeBorderColor := getActivePaneBorderColor(styling, isDark) | ||
| 132 | - | inactiveBorderColor := getInactivePaneBorderColor(styling, isDark) | |||
| 133 | 113 | ||||
| 134 | // Pane mode border color - use configured value or fallback to blue | 114 | // Pane mode border color - use configured value or fallback to blue | ||
| 135 | paneModeColor := styling.PaneModeBorderColor | 115 | paneModeColor := styling.PaneModeBorderColor | ||
| @@ -163,33 +143,42 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 163 | margin: 0; | 143 | margin: 0; | ||
| 164 | } | 144 | } | ||
| 165 | 145 | ||||
| 166 | - | /* Pane mode active - change window background to border color */ | 146 | + | /* Pane mode border overlay - floats over content without layout shift */ |
| 167 | - | window.pane-mode-active { | 147 | + | .pane-mode-border { |
| 168 | - | background-color: %s; | 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; | |||
| 169 | } | 171 | } | ||
| 170 | 172 | ||||
| 171 | /* Workspace root container */ | 173 | /* Workspace root container */ | ||
| 172 | paned, box { | 174 | paned, box { | ||
| 173 | background-color: %s; | 175 | background-color: %s; | ||
| 174 | - | transition: margin 150ms ease-in-out; | |||
| 175 | } | 176 | } | ||
| 176 | 177 | ||||
| 177 | - | /* Base pane styling - configurable inactive borders */ | 178 | + | /* Base pane styling - no borders (using overlays instead) */ |
| 178 | .workspace-pane, .stacked-pane-container { | 179 | .workspace-pane, .stacked-pane-container { | ||
| 179 | - | border-width: %dpx; | 180 | + | border: none; |
| 180 | - | border-style: solid; | |||
| 181 | - | border-color: %s; | |||
| 182 | - | border-radius: %dpx; | |||
| 183 | margin: 0; | 181 | margin: 0; | ||
| 184 | - | transition-property: border-color; | |||
| 185 | - | transition-duration: %dms; | |||
| 186 | - | transition-timing-function: ease-in-out; | |||
| 187 | - | } | |||
| 188 | - | ||||
| 189 | - | /* Active pane border styling */ | |||
| 190 | - | .workspace-pane-active { | |||
| 191 | - | border-width: %dpx; | |||
| 192 | - | border-color: %s; | |||
| 193 | } | 182 | } | ||
| 194 | 183 | ||||
| 195 | /* Stacked panes styling */ | 184 | /* Stacked panes styling */ | ||
| @@ -197,12 +186,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 197 | background-color: %s; | 186 | background-color: %s; | ||
| 198 | } | 187 | } | ||
| 199 | 188 | ||||
| 200 | - | /* Stacked pane containers keep inactive border when active */ | |||
| 201 | - | .stacked-pane-container.workspace-pane-active { | |||
| 202 | - | border-width: %dpx; | |||
| 203 | - | border-color: %s; | |||
| 204 | - | } | |||
| 205 | - | ||||
| 206 | .stacked-pane-title { | 189 | .stacked-pane-title { | ||
| 207 | background-color: %s; | 190 | background-color: %s; | ||
| 208 | border-bottom: %s; | 191 | border-bottom: %s; | ||
| @@ -267,11 +250,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 267 | animation: tab-progress-stripe 1.3s linear infinite; | 250 | animation: tab-progress-stripe 1.3s linear infinite; | ||
| 268 | } | 251 | } | ||
| 269 | 252 | ||||
| 270 | - | /* Tab mode active - window background changes to border color (like pane mode) */ | |||
| 271 | - | window.tab-mode-active { | |||
| 272 | - | background-color: %s; | |||
| 273 | - | } | |||
| 274 | - | ||||
| 275 | /* Tab button styling - matches stacked pane titles */ | 253 | /* Tab button styling - matches stacked pane titles */ | ||
| 276 | button.tab-button { | 254 | button.tab-button { | ||
| 277 | background-color: %s; | 255 | background-color: %s; | ||
| @@ -345,17 +323,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 345 | 100%% { background-position: -100%% 0%%; } | 323 | 100%% { background-position: -100%% 0%%; } | ||
| 346 | }`, | 324 | }`, | ||
| 347 | windowBackgroundColor, // window background | 325 | windowBackgroundColor, // window background | ||
| 348 | - | paneModeColor, // window.pane-mode-active background (border color) | 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 | |||
| 349 | windowBackgroundColor, // paned, box background | 333 | windowBackgroundColor, // paned, box background | ||
| 350 | - | styling.InactiveBorderWidth, // base pane border-width (inactive) | |||
| 351 | - | inactiveBorderColor, // base pane border-color (inactive) | |||
| 352 | - | styling.BorderRadius, // base pane border radius | |||
| 353 | - | styling.TransitionDuration, // transition-duration | |||
| 354 | - | styling.BorderWidth, // workspace-pane-active border-width | |||
| 355 | - | activeBorderColor, // workspace-pane-active border color | |||
| 356 | windowBackgroundColor, // stacked-pane-container background | 334 | windowBackgroundColor, // stacked-pane-container background | ||
| 357 | - | styling.InactiveBorderWidth, // stacked-pane-container.active border-width | |||
| 358 | - | inactiveBorderColor, // stacked-pane-container.active border-color | |||
| 359 | getStackTitleBg(isDark), // stacked-pane-title background | 335 | getStackTitleBg(isDark), // stacked-pane-title background | ||
| 360 | stackedTitleBorder, // stacked-pane-title border-bottom | 336 | stackedTitleBorder, // stacked-pane-title border-bottom | ||
| 361 | titlePaddingVertical, // stacked-pane-title padding vertical | 337 | titlePaddingVertical, // stacked-pane-title padding vertical | ||
| @@ -373,7 +349,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 373 | "#606060", // tab progress gradient start (neutral gray) | 349 | "#606060", // tab progress gradient start (neutral gray) | ||
| 374 | "#747474", // tab progress gradient middle (neutral gray) | 350 | "#747474", // tab progress gradient middle (neutral gray) | ||
| 375 | "#8a8a8a", // tab progress gradient end (neutral gray) | 351 | "#8a8a8a", // tab progress gradient end (neutral gray) | ||
| 376 | - | tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue) | |||
| 377 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | 352 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | ||
| 378 | getStackTitleBorderColor(isDark), // tab-button border-right | 353 | getStackTitleBorderColor(isDark), // tab-button border-right | ||
| 379 | titlePaddingVertical, // tab-button padding vertical | 354 | titlePaddingVertical, // tab-button padding vertical | ||
M internal/app/browser/workspace_pane_mode.go +4 -82
| @@ -3,9 +3,6 @@ package browser | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "log" | |
| 6 | - | ||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 8 | - | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 9 | 6 | ) | |
| 10 | 7 | ||
| 11 | 8 | // EnterPaneMode activates pane mode on the currently focused pane | |
| @@ -205,91 +202,16 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) { | |||
| 205 | 202 | } | |
| 206 | 203 | } | |
| 207 | 204 | ||
| 208 | - | // applyPaneModeBorder applies pane mode visual indicator using GTK margins | |
| 205 | + | // applyPaneModeBorder applies pane mode visual indicator using overlay | |
| 209 | 206 | func (wm *WorkspaceManager) applyPaneModeBorder() { | |
| 210 | - | window := wm.resolveWindow() | |
| 211 | - | if window == nil { | |
| 212 | - | log.Printf("[pane-mode] Cannot apply border: missing window reference") | |
| 213 | - | return | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | // Determine which container to apply margins to: | |
| 217 | - | // - If tab manager exists, apply to its root container (direct child of window) | |
| 218 | - | // so margins reveal the window background color | |
| 219 | - | // - Otherwise, apply to workspace root (direct child of window) | |
| 220 | - | var targetContainer gtk.Widgetter | |
| 221 | 207 | if wm.app != nil && wm.app.tabManager != nil { | |
| 222 | - | if rootContainer := wm.app.tabManager.GetRootContainer(); rootContainer != nil { | |
| 223 | - | // Tab environment: apply margins to the root container that's attached to the window | |
| 224 | - | targetContainer = rootContainer | |
| 225 | - | log.Printf("[pane-mode] Using tab manager root container for border") | |
| 226 | - | } else if wm.app.tabManager.ContentArea != nil { | |
| 227 | - | // Fallback: older builds where root container isn't available yet | |
| 228 | - | targetContainer = wm.app.tabManager.ContentArea | |
| 229 | - | log.Printf("[pane-mode] Using tab manager content area fallback for border") | |
| 230 | - | } | |
| 231 | - | } else if wm.root != nil && wm.root.container != nil { | |
| 232 | - | // Non-tab environment: apply margins to workspace root | |
| 233 | - | targetContainer = wm.root.container | |
| 234 | - | log.Printf("[pane-mode] Using workspace root for border") | |
| 235 | - | } else { | |
| 236 | - | return | |
| 208 | + | wm.app.tabManager.showPaneModeBorder() | |
| 237 | 209 | } | |
| 238 | - | ||
| 239 | - | // Save the container reference so we can remove margins from it later | |
| 240 | - | wm.paneModeContainer = targetContainer | |
| 241 | - | ||
| 242 | - | // Apply 4px margins to create space for the border | |
| 243 | - | webkit.WidgetSetMargin(targetContainer, 4) | |
| 244 | - | ||
| 245 | - | // Add CSS class to window for background color (the "border" color shows in the margin space) | |
| 246 | - | webkit.WidgetAddCSSClass(window.AsWindow(), "pane-mode-active") | |
| 247 | - | ||
| 248 | - | // Force resize/allocation to apply margin changes immediately | |
| 249 | - | webkit.WidgetQueueResize(targetContainer) | |
| 250 | - | webkit.WidgetQueueAllocate(targetContainer) | |
| 251 | - | ||
| 252 | - | // Queue redraw to show changes | |
| 253 | - | webkit.WidgetQueueDraw(window.AsWindow()) | |
| 254 | - | webkit.WidgetQueueDraw(targetContainer) | |
| 255 | - | ||
| 256 | - | log.Printf("[pane-mode] Applied border using margins (container=%p)", targetContainer) | |
| 257 | 210 | } | |
| 258 | 211 | ||
| 259 | 212 | // removePaneModeBorder removes the pane mode visual indicator | |
| 260 | 213 | func (wm *WorkspaceManager) removePaneModeBorder() { | |
| 261 | - | window := wm.resolveWindow() | |
| 262 | - | if window == nil { | |
| 263 | - | return | |
| 264 | - | } | |
| 265 | - | ||
| 266 | - | // Use the saved container reference (the one that had margins applied) | |
| 267 | - | // This is important because wm.root.container may have changed due to splits | |
| 268 | - | container := wm.paneModeContainer | |
| 269 | - | if container == nil { | |
| 270 | - | log.Printf("[pane-mode] Warning: no saved container, trying current root") | |
| 271 | - | if wm.root == nil || wm.root.container == nil { | |
| 272 | - | return | |
| 273 | - | } | |
| 274 | - | container = wm.root.container | |
| 214 | + | if wm.app != nil && wm.app.tabManager != nil { | |
| 215 | + | wm.app.tabManager.hidePaneModeBorder() | |
| 275 | 216 | } | |
| 276 | - | ||
| 277 | - | // Remove margins from the container that had them applied | |
| 278 | - | webkit.WidgetSetMargin(container, 0) | |
| 279 | - | ||
| 280 | - | // Remove CSS class from window | |
| 281 | - | webkit.WidgetRemoveCSSClass(window.AsWindow(), "pane-mode-active") | |
| 282 | - | ||
| 283 | - | // Force resize/allocation to apply margin changes immediately | |
| 284 | - | webkit.WidgetQueueResize(container) | |
| 285 | - | webkit.WidgetQueueAllocate(container) | |
| 286 | - | ||
| 287 | - | // Queue redraw to show changes | |
| 288 | - | webkit.WidgetQueueDraw(window.AsWindow()) | |
| 289 | - | webkit.WidgetQueueDraw(container) | |
| 290 | - | ||
| 291 | - | // Clear the saved container reference | |
| 292 | - | wm.paneModeContainer = nil | |
| 293 | - | ||
| 294 | - | log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container) | |
| 295 | 217 | } | |
| @@ -3,9 +3,6 @@ package browser | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "log" | 5 | "log" | ||
| 6 | - | ||||
| 7 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 8 | - | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 9 | ) | 6 | ) | ||
| 10 | 7 | ||||
| 11 | // EnterPaneMode activates pane mode on the currently focused pane | 8 | // EnterPaneMode activates pane mode on the currently focused pane | ||
| @@ -205,91 +202,16 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) { | |||||
| 205 | } | 202 | } | ||
| 206 | } | 203 | } | ||
| 207 | 204 | ||||
| 208 | - | // applyPaneModeBorder applies pane mode visual indicator using GTK margins | 205 | + | // applyPaneModeBorder applies pane mode visual indicator using overlay |
| 209 | func (wm *WorkspaceManager) applyPaneModeBorder() { | 206 | func (wm *WorkspaceManager) applyPaneModeBorder() { | ||
| 210 | - | window := wm.resolveWindow() | |||
| 211 | - | if window == nil { | |||
| 212 | - | log.Printf("[pane-mode] Cannot apply border: missing window reference") | |||
| 213 | - | return | |||
| 214 | - | } | |||
| 215 | - | ||||
| 216 | - | // Determine which container to apply margins to: | |||
| 217 | - | // - If tab manager exists, apply to its root container (direct child of window) | |||
| 218 | - | // so margins reveal the window background color | |||
| 219 | - | // - Otherwise, apply to workspace root (direct child of window) | |||
| 220 | - | var targetContainer gtk.Widgetter | |||
| 221 | if wm.app != nil && wm.app.tabManager != nil { | 207 | if wm.app != nil && wm.app.tabManager != nil { | ||
| 222 | - | if rootContainer := wm.app.tabManager.GetRootContainer(); rootContainer != nil { | 208 | + | wm.app.tabManager.showPaneModeBorder() |
| 223 | - | // Tab environment: apply margins to the root container that's attached to the window | |||
| 224 | - | targetContainer = rootContainer | |||
| 225 | - | log.Printf("[pane-mode] Using tab manager root container for border") | |||
| 226 | - | } else if wm.app.tabManager.ContentArea != nil { | |||
| 227 | - | // Fallback: older builds where root container isn't available yet | |||
| 228 | - | targetContainer = wm.app.tabManager.ContentArea | |||
| 229 | - | log.Printf("[pane-mode] Using tab manager content area fallback for border") | |||
| 230 | - | } | |||
| 231 | - | } else if wm.root != nil && wm.root.container != nil { | |||
| 232 | - | // Non-tab environment: apply margins to workspace root | |||
| 233 | - | targetContainer = wm.root.container | |||
| 234 | - | log.Printf("[pane-mode] Using workspace root for border") | |||
| 235 | - | } else { | |||
| 236 | - | return | |||
| 237 | } | 209 | } | ||
| 238 | - | ||||
| 239 | - | // Save the container reference so we can remove margins from it later | |||
| 240 | - | wm.paneModeContainer = targetContainer | |||
| 241 | - | ||||
| 242 | - | // Apply 4px margins to create space for the border | |||
| 243 | - | webkit.WidgetSetMargin(targetContainer, 4) | |||
| 244 | - | ||||
| 245 | - | // Add CSS class to window for background color (the "border" color shows in the margin space) | |||
| 246 | - | webkit.WidgetAddCSSClass(window.AsWindow(), "pane-mode-active") | |||
| 247 | - | ||||
| 248 | - | // Force resize/allocation to apply margin changes immediately | |||
| 249 | - | webkit.WidgetQueueResize(targetContainer) | |||
| 250 | - | webkit.WidgetQueueAllocate(targetContainer) | |||
| 251 | - | ||||
| 252 | - | // Queue redraw to show changes | |||
| 253 | - | webkit.WidgetQueueDraw(window.AsWindow()) | |||
| 254 | - | webkit.WidgetQueueDraw(targetContainer) | |||
| 255 | - | ||||
| 256 | - | log.Printf("[pane-mode] Applied border using margins (container=%p)", targetContainer) | |||
| 257 | } | 210 | } | ||
| 258 | 211 | ||||
| 259 | // removePaneModeBorder removes the pane mode visual indicator | 212 | // removePaneModeBorder removes the pane mode visual indicator | ||
| 260 | func (wm *WorkspaceManager) removePaneModeBorder() { | 213 | func (wm *WorkspaceManager) removePaneModeBorder() { | ||
| 261 | - | window := wm.resolveWindow() | 214 | + | if wm.app != nil && wm.app.tabManager != nil { |
| 262 | - | if window == nil { | 215 | + | wm.app.tabManager.hidePaneModeBorder() |
| 263 | - | return | |||
| 264 | - | } | |||
| 265 | - | ||||
| 266 | - | // Use the saved container reference (the one that had margins applied) | |||
| 267 | - | // This is important because wm.root.container may have changed due to splits | |||
| 268 | - | container := wm.paneModeContainer | |||
| 269 | - | if container == nil { | |||
| 270 | - | log.Printf("[pane-mode] Warning: no saved container, trying current root") | |||
| 271 | - | if wm.root == nil || wm.root.container == nil { | |||
| 272 | - | return | |||
| 273 | - | } | |||
| 274 | - | container = wm.root.container | |||
| 275 | } | 216 | } | ||
| 276 | - | ||||
| 277 | - | // Remove margins from the container that had them applied | |||
| 278 | - | webkit.WidgetSetMargin(container, 0) | |||
| 279 | - | ||||
| 280 | - | // Remove CSS class from window | |||
| 281 | - | webkit.WidgetRemoveCSSClass(window.AsWindow(), "pane-mode-active") | |||
| 282 | - | ||||
| 283 | - | // Force resize/allocation to apply margin changes immediately | |||
| 284 | - | webkit.WidgetQueueResize(container) | |||
| 285 | - | webkit.WidgetQueueAllocate(container) | |||
| 286 | - | ||||
| 287 | - | // Queue redraw to show changes | |||
| 288 | - | webkit.WidgetQueueDraw(window.AsWindow()) | |||
| 289 | - | webkit.WidgetQueueDraw(container) | |||
| 290 | - | ||||
| 291 | - | // Clear the saved container reference | |||
| 292 | - | wm.paneModeContainer = nil | |||
| 293 | - | ||||
| 294 | - | log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container) | |||
| 295 | } | 217 | } | ||
M internal/app/browser/workspace_pane_ops.go +8 -0
| @@ -476,6 +476,14 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||
| 476 | 476 | } | |
| 477 | 477 | wm.initializePaneWidgets(newLeaf, newContainer) | |
| 478 | 478 | ||
| 479 | + | // After wrapping the WebView in an overlay, use the overlay container for layout operations. | |
| 480 | + | newContainer = newLeaf.container | |
| 481 | + | if newContainer == nil { | |
| 482 | + | return nil, errors.New("new pane missing container overlay") | |
| 483 | + | } | |
| 484 | + | webkit.WidgetSetHExpand(newContainer, true) | |
| 485 | + | webkit.WidgetSetVExpand(newContainer, true) | |
| 486 | + | ||
| 479 | 487 | split := &paneNode{ | |
| 480 | 488 | parent: splitTarget.parent, | |
| 481 | 489 | orientation: orientation, | |
| @@ -476,6 +476,14 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi | |||||
| 476 | } | 476 | } | ||
| 477 | wm.initializePaneWidgets(newLeaf, newContainer) | 477 | wm.initializePaneWidgets(newLeaf, newContainer) | ||
| 478 | 478 | ||||
| 479 | + | // After wrapping the WebView in an overlay, use the overlay container for layout operations. | |||
| 480 | + | newContainer = newLeaf.container | |||
| 481 | + | if newContainer == nil { | |||
| 482 | + | return nil, errors.New("new pane missing container overlay") | |||
| 483 | + | } | |||
| 484 | + | webkit.WidgetSetHExpand(newContainer, true) | |||
| 485 | + | webkit.WidgetSetVExpand(newContainer, true) | |||
| 486 | + | ||||
| 479 | split := &paneNode{ | 487 | split := &paneNode{ | ||
| 480 | parent: splitTarget.parent, | 488 | parent: splitTarget.parent, | ||
| 481 | orientation: orientation, | 489 | orientation: orientation, | ||
M internal/app/browser/workspace_types.go +5 -4
| @@ -51,10 +51,11 @@ type paneNode struct { | |||
| 51 | 51 | right *paneNode | |
| 52 | 52 | ||
| 53 | 53 | // Widget management - gotk4 widgets (all ops on main thread) | |
| 54 | - | container gtk.Widgetter // Main container: *gtk.Paned (branch), *gtk.Box (stack), or WebView widget (leaf) | |
| 55 | - | orientation gtk.Orientation | |
| 56 | - | isLeaf bool | |
| 57 | - | isPopup bool // Deprecated: use windowType instead | |
| 54 | + | container gtk.Widgetter // Main container: *gtk.Paned (branch), *gtk.Box (stack), or WebView widget (leaf) | |
| 55 | + | borderOverlay gtk.Widgetter // Border overlay widget (for active pane visual indicator) | |
| 56 | + | orientation gtk.Orientation | |
| 57 | + | isLeaf bool | |
| 58 | + | isPopup bool // Deprecated: use windowType instead | |
| 58 | 59 | ||
| 59 | 60 | // Window type tracking | |
| 60 | 61 | windowType webkit.WindowType // Tab or Popup | |
| @@ -51,10 +51,11 @@ type paneNode struct { | |||||
| 51 | right *paneNode | 51 | right *paneNode | ||
| 52 | 52 | ||||
| 53 | // Widget management - gotk4 widgets (all ops on main thread) | 53 | // Widget management - gotk4 widgets (all ops on main thread) | ||
| 54 | - | container gtk.Widgetter // Main container: *gtk.Paned (branch), *gtk.Box (stack), or WebView widget (leaf) | 54 | + | container gtk.Widgetter // Main container: *gtk.Paned (branch), *gtk.Box (stack), or WebView widget (leaf) |
| 55 | - | orientation gtk.Orientation | 55 | + | borderOverlay gtk.Widgetter // Border overlay widget (for active pane visual indicator) |
| 56 | - | isLeaf bool | 56 | + | orientation gtk.Orientation |
| 57 | - | isPopup bool // Deprecated: use windowType instead | 57 | + | isLeaf bool |
| 58 | + | isPopup bool // Deprecated: use windowType instead | |||
| 58 | 59 | ||||
| 59 | // Window type tracking | 60 | // Window type tracking | ||
| 60 | windowType webkit.WindowType // Tab or Popup | 61 | windowType webkit.WindowType // Tab or Popup | ||
M internal/app/browser/workspace_utils.go +35 -28
| @@ -59,7 +59,19 @@ func mapDirection(direction string) (gtk.Orientation, bool) { | |||
| 59 | 59 | ||
| 60 | 60 | // initializePaneWidgets sets up widgets for a paneNode | |
| 61 | 61 | func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Widgetter) { | |
| 62 | - | node.container = widget | |
| 62 | + | // Only wrap leaf nodes in overlays (not branch nodes like GtkPaned) | |
| 63 | + | if node.isLeaf { | |
| 64 | + | // Wrap the widget in an overlay with a border (for active pane indication) | |
| 65 | + | overlay, borderWidget := wrapPaneInOverlay(widget) | |
| 66 | + | ||
| 67 | + | // Store the overlay as the container and the border widget | |
| 68 | + | node.container = overlay | |
| 69 | + | node.borderOverlay = borderWidget | |
| 70 | + | } else { | |
| 71 | + | // Branch nodes (splits, stacks) don't need border overlays | |
| 72 | + | node.container = widget | |
| 73 | + | node.borderOverlay = nil | |
| 74 | + | } | |
| 63 | 75 | ||
| 64 | 76 | // Add base CSS class to the container | |
| 65 | 77 | if node.container != nil { | |
| @@ -78,7 +90,20 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Wid | |||
| 78 | 90 | // setContainer sets the container widget | |
| 79 | 91 | func (wm *WorkspaceManager) setContainer(node *paneNode, widget gtk.Widgetter, typeInfo string) { | |
| 80 | 92 | _ = typeInfo // Type info kept for API compatibility but not used | |
| 81 | - | node.container = widget | |
| 93 | + | ||
| 94 | + | // Only wrap leaf nodes in overlays (not branch nodes like stacked containers) | |
| 95 | + | if node.isLeaf { | |
| 96 | + | // Wrap the widget in an overlay with a border (for active pane indication) | |
| 97 | + | overlay, borderWidget := wrapPaneInOverlay(widget) | |
| 98 | + | ||
| 99 | + | // Store the overlay as the container and the border widget | |
| 100 | + | node.container = overlay | |
| 101 | + | node.borderOverlay = borderWidget | |
| 102 | + | } else { | |
| 103 | + | // Branch nodes (splits, stacks) don't need border overlays | |
| 104 | + | node.container = widget | |
| 105 | + | node.borderOverlay = nil | |
| 106 | + | } | |
| 82 | 107 | ||
| 83 | 108 | // Mark widget validity based on container presence so idle guards run for stacks too | |
| 84 | 109 | if widget != nil { | |
| @@ -144,9 +169,9 @@ func (wm *WorkspaceManager) borderTargets(node *paneNode) []gtk.Widgetter { | |||
| 144 | 169 | return targets | |
| 145 | 170 | } | |
| 146 | 171 | ||
| 147 | - | // applyActivePaneBorder marks the target widgets as active for CSS styling. | |
| 172 | + | // applyActivePaneBorder shows the border overlay for the active pane. | |
| 148 | 173 | func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |
| 149 | - | if wm == nil || node == nil { | |
| 174 | + | if wm == nil || node == nil || node.borderOverlay == nil { | |
| 150 | 175 | return | |
| 151 | 176 | } | |
| 152 | 177 | ||
| @@ -156,36 +181,18 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |||
| 156 | 181 | return | |
| 157 | 182 | } | |
| 158 | 183 | ||
| 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 | - | } | |
| 184 | + | webkit.WidgetSetVisible(node.borderOverlay, true) | |
| 185 | + | log.Printf("[workspace] Showing border overlay for pane %p", node) | |
| 170 | 186 | } | |
| 171 | 187 | ||
| 172 | - | // removeActivePaneBorder clears the active styling from a pane. | |
| 188 | + | // removeActivePaneBorder hides the border overlay for a pane. | |
| 173 | 189 | func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | |
| 174 | - | if wm == nil || node == nil { | |
| 190 | + | if wm == nil || node == nil || node.borderOverlay == nil { | |
| 175 | 191 | return | |
| 176 | 192 | } | |
| 177 | 193 | ||
| 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 | - | } | |
| 194 | + | webkit.WidgetSetVisible(node.borderOverlay, false) | |
| 195 | + | log.Printf("[workspace] Hiding border overlay for pane %p", node) | |
| 189 | 196 | } | |
| 190 | 197 | ||
| 191 | 198 | // Widget cleanup utilities | |
| @@ -59,7 +59,19 @@ func mapDirection(direction string) (gtk.Orientation, bool) { | |||||
| 59 | 59 | ||||
| 60 | // initializePaneWidgets sets up widgets for a paneNode | 60 | // initializePaneWidgets sets up widgets for a paneNode | ||
| 61 | func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Widgetter) { | 61 | func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Widgetter) { | ||
| 62 | - | node.container = widget | 62 | + | // Only wrap leaf nodes in overlays (not branch nodes like GtkPaned) |
| 63 | + | if node.isLeaf { | |||
| 64 | + | // Wrap the widget in an overlay with a border (for active pane indication) | |||
| 65 | + | overlay, borderWidget := wrapPaneInOverlay(widget) | |||
| 66 | + | ||||
| 67 | + | // Store the overlay as the container and the border widget | |||
| 68 | + | node.container = overlay | |||
| 69 | + | node.borderOverlay = borderWidget | |||
| 70 | + | } else { | |||
| 71 | + | // Branch nodes (splits, stacks) don't need border overlays | |||
| 72 | + | node.container = widget | |||
| 73 | + | node.borderOverlay = nil | |||
| 74 | + | } | |||
| 63 | 75 | ||||
| 64 | // Add base CSS class to the container | 76 | // Add base CSS class to the container | ||
| 65 | if node.container != nil { | 77 | if node.container != nil { | ||
| @@ -78,7 +90,20 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Wid | |||||
| 78 | // setContainer sets the container widget | 90 | // setContainer sets the container widget | ||
| 79 | func (wm *WorkspaceManager) setContainer(node *paneNode, widget gtk.Widgetter, typeInfo string) { | 91 | func (wm *WorkspaceManager) setContainer(node *paneNode, widget gtk.Widgetter, typeInfo string) { | ||
| 80 | _ = typeInfo // Type info kept for API compatibility but not used | 92 | _ = typeInfo // Type info kept for API compatibility but not used | ||
| 81 | - | node.container = widget | 93 | + | |
| 94 | + | // Only wrap leaf nodes in overlays (not branch nodes like stacked containers) | |||
| 95 | + | if node.isLeaf { | |||
| 96 | + | // Wrap the widget in an overlay with a border (for active pane indication) | |||
| 97 | + | overlay, borderWidget := wrapPaneInOverlay(widget) | |||
| 98 | + | ||||
| 99 | + | // Store the overlay as the container and the border widget | |||
| 100 | + | node.container = overlay | |||
| 101 | + | node.borderOverlay = borderWidget | |||
| 102 | + | } else { | |||
| 103 | + | // Branch nodes (splits, stacks) don't need border overlays | |||
| 104 | + | node.container = widget | |||
| 105 | + | node.borderOverlay = nil | |||
| 106 | + | } | |||
| 82 | 107 | ||||
| 83 | // Mark widget validity based on container presence so idle guards run for stacks too | 108 | // Mark widget validity based on container presence so idle guards run for stacks too | ||
| 84 | if widget != nil { | 109 | if widget != nil { | ||
| @@ -144,9 +169,9 @@ func (wm *WorkspaceManager) borderTargets(node *paneNode) []gtk.Widgetter { | |||||
| 144 | return targets | 169 | return targets | ||
| 145 | } | 170 | } | ||
| 146 | 171 | ||||
| 147 | - | // applyActivePaneBorder marks the target widgets as active for CSS styling. | 172 | + | // applyActivePaneBorder shows the border overlay for the active pane. |
| 148 | func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | 173 | func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | ||
| 149 | - | if wm == nil || node == nil { | 174 | + | if wm == nil || node == nil || node.borderOverlay == nil { |
| 150 | return | 175 | return | ||
| 151 | } | 176 | } | ||
| 152 | 177 | ||||
| @@ -156,36 +181,18 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) { | |||||
| 156 | return | 181 | return | ||
| 157 | } | 182 | } | ||
| 158 | 183 | ||||
| 159 | - | targets := wm.borderTargets(node) | 184 | + | webkit.WidgetSetVisible(node.borderOverlay, true) |
| 160 | - | for _, widget := range targets { | 185 | + | log.Printf("[workspace] Showing border overlay for pane %p", node) |
| 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 | } | 186 | } | ||
| 171 | 187 | ||||
| 172 | - | // removeActivePaneBorder clears the active styling from a pane. | 188 | + | // removeActivePaneBorder hides the border overlay for a pane. |
| 173 | func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | 189 | func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) { | ||
| 174 | - | if wm == nil || node == nil { | 190 | + | if wm == nil || node == nil || node.borderOverlay == nil { |
| 175 | return | 191 | return | ||
| 176 | } | 192 | } | ||
| 177 | 193 | ||||
| 178 | - | targets := wm.borderTargets(node) | 194 | + | webkit.WidgetSetVisible(node.borderOverlay, false) |
| 179 | - | for _, widget := range targets { | 195 | + | log.Printf("[workspace] Hiding border overlay for pane %p", node) |
| 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 | } | 196 | } | ||
| 190 | 197 | ||||
| 191 | // Widget cleanup utilities | 198 | // Widget cleanup utilities | ||
M internal/config/config.go +11 -13
| @@ -336,26 +336,25 @@ type PopupBehaviorConfig struct { | |||
| 336 | 336 | ||
| 337 | 337 | // WorkspaceStylingConfig defines visual styling for workspace panes. | |
| 338 | 338 | type WorkspaceStylingConfig struct { | |
| 339 | - | // BorderWidth in pixels for active pane borders | |
| 339 | + | // BorderWidth in pixels for active pane borders (overlay) | |
| 340 | 340 | BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"` | |
| 341 | 341 | // BorderColor for focused panes (CSS color value or theme variable) | |
| 342 | 342 | BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"` | |
| 343 | - | // InactiveBorderWidth in pixels for inactive pane borders (0 = hidden) | |
| 344 | - | InactiveBorderWidth int `mapstructure:"inactive_border_width" yaml:"inactive_border_width" json:"inactive_border_width"` | |
| 345 | - | // InactiveBorderColor for unfocused panes (CSS color value or theme variable) | |
| 346 | - | InactiveBorderColor string `mapstructure:"inactive_border_color" yaml:"inactive_border_color" json:"inactive_border_color"` | |
| 347 | - | // ShowStackedTitleBorder enables the separator line below stacked pane titles | |
| 348 | - | ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"` | |
| 343 | + | ||
| 344 | + | // PaneModeBorderWidth in pixels for pane mode indicator border (Ctrl+P N overlay) | |
| 345 | + | PaneModeBorderWidth int `mapstructure:"pane_mode_border_width" yaml:"pane_mode_border_width" json:"pane_mode_border_width"` | |
| 349 | 346 | // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable) | |
| 350 | 347 | // Defaults to "#4A90E2" (blue) if not set | |
| 351 | 348 | PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"` | |
| 349 | + | ||
| 350 | + | // TabModeBorderWidth in pixels for tab mode indicator border (Ctrl+P T overlay) | |
| 351 | + | TabModeBorderWidth int `mapstructure:"tab_mode_border_width" yaml:"tab_mode_border_width" json:"tab_mode_border_width"` | |
| 352 | 352 | // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable) | |
| 353 | 353 | // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor | |
| 354 | 354 | TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"` | |
| 355 | + | ||
| 355 | 356 | // TransitionDuration in milliseconds for border animations | |
| 356 | 357 | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | |
| 357 | - | // BorderRadius in pixels for pane border corners | |
| 358 | - | BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"` | |
| 359 | 358 | // UIScale is a multiplier for UI elements like title bars (1.0 = 100%, 1.2 = 120%) | |
| 360 | 359 | UIScale float64 `mapstructure:"ui_scale" yaml:"ui_scale" json:"ui_scale"` | |
| 361 | 360 | } | |
| @@ -754,12 +753,11 @@ func (m *Manager) setDefaults() { | |||
| 754 | 753 | m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose) | |
| 755 | 754 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | |
| 756 | 755 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | |
| 757 | - | m.viper.SetDefault("workspace.styling.inactive_border_width", defaults.Workspace.Styling.InactiveBorderWidth) | |
| 758 | - | m.viper.SetDefault("workspace.styling.inactive_border_color", defaults.Workspace.Styling.InactiveBorderColor) | |
| 759 | - | m.viper.SetDefault("workspace.styling.show_stacked_title_border", defaults.Workspace.Styling.ShowStackedTitleBorder) | |
| 756 | + | m.viper.SetDefault("workspace.styling.pane_mode_border_width", defaults.Workspace.Styling.PaneModeBorderWidth) | |
| 760 | 757 | m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor) | |
| 758 | + | m.viper.SetDefault("workspace.styling.tab_mode_border_width", defaults.Workspace.Styling.TabModeBorderWidth) | |
| 759 | + | m.viper.SetDefault("workspace.styling.tab_mode_border_color", defaults.Workspace.Styling.TabModeBorderColor) | |
| 761 | 760 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | |
| 762 | - | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | |
| 763 | 761 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | |
| 764 | 762 | ||
| 765 | 763 | // Content filtering | |
| @@ -336,26 +336,25 @@ type PopupBehaviorConfig struct { | |||||
| 336 | 336 | ||||
| 337 | // WorkspaceStylingConfig defines visual styling for workspace panes. | 337 | // WorkspaceStylingConfig defines visual styling for workspace panes. | ||
| 338 | type WorkspaceStylingConfig struct { | 338 | type WorkspaceStylingConfig struct { | ||
| 339 | - | // BorderWidth in pixels for active pane borders | 339 | + | // BorderWidth in pixels for active pane borders (overlay) |
| 340 | BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"` | 340 | BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"` | ||
| 341 | // BorderColor for focused panes (CSS color value or theme variable) | 341 | // BorderColor for focused panes (CSS color value or theme variable) | ||
| 342 | BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"` | 342 | BorderColor string `mapstructure:"border_color" yaml:"border_color" json:"border_color"` | ||
| 343 | - | // InactiveBorderWidth in pixels for inactive pane borders (0 = hidden) | 343 | + | |
| 344 | - | InactiveBorderWidth int `mapstructure:"inactive_border_width" yaml:"inactive_border_width" json:"inactive_border_width"` | 344 | + | // PaneModeBorderWidth in pixels for pane mode indicator border (Ctrl+P N overlay) |
| 345 | - | // InactiveBorderColor for unfocused panes (CSS color value or theme variable) | 345 | + | PaneModeBorderWidth int `mapstructure:"pane_mode_border_width" yaml:"pane_mode_border_width" json:"pane_mode_border_width"` |
| 346 | - | InactiveBorderColor string `mapstructure:"inactive_border_color" yaml:"inactive_border_color" json:"inactive_border_color"` | |||
| 347 | - | // ShowStackedTitleBorder enables the separator line below stacked pane titles | |||
| 348 | - | ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"` | |||
| 349 | // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable) | 346 | // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable) | ||
| 350 | // Defaults to "#4A90E2" (blue) if not set | 347 | // Defaults to "#4A90E2" (blue) if not set | ||
| 351 | PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"` | 348 | PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"` | ||
| 349 | + | ||||
| 350 | + | // TabModeBorderWidth in pixels for tab mode indicator border (Ctrl+P T overlay) | |||
| 351 | + | TabModeBorderWidth int `mapstructure:"tab_mode_border_width" yaml:"tab_mode_border_width" json:"tab_mode_border_width"` | |||
| 352 | // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable) | 352 | // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable) | ||
| 353 | // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor | 353 | // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor | ||
| 354 | TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"` | 354 | TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"` | ||
| 355 | + | ||||
| 355 | // TransitionDuration in milliseconds for border animations | 356 | // TransitionDuration in milliseconds for border animations | ||
| 356 | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | 357 | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | ||
| 357 | - | // BorderRadius in pixels for pane border corners | |||
| 358 | - | BorderRadius int `mapstructure:"border_radius" yaml:"border_radius" json:"border_radius"` | |||
| 359 | // UIScale is a multiplier for UI elements like title bars (1.0 = 100%, 1.2 = 120%) | 358 | // UIScale is a multiplier for UI elements like title bars (1.0 = 100%, 1.2 = 120%) | ||
| 360 | UIScale float64 `mapstructure:"ui_scale" yaml:"ui_scale" json:"ui_scale"` | 359 | UIScale float64 `mapstructure:"ui_scale" yaml:"ui_scale" json:"ui_scale"` | ||
| 361 | } | 360 | } | ||
| @@ -754,12 +753,11 @@ func (m *Manager) setDefaults() { | |||||
| 754 | m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose) | 753 | m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose) | ||
| 755 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | 754 | m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth) | ||
| 756 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | 755 | m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor) | ||
| 757 | - | m.viper.SetDefault("workspace.styling.inactive_border_width", defaults.Workspace.Styling.InactiveBorderWidth) | 756 | + | m.viper.SetDefault("workspace.styling.pane_mode_border_width", defaults.Workspace.Styling.PaneModeBorderWidth) |
| 758 | - | m.viper.SetDefault("workspace.styling.inactive_border_color", defaults.Workspace.Styling.InactiveBorderColor) | |||
| 759 | - | m.viper.SetDefault("workspace.styling.show_stacked_title_border", defaults.Workspace.Styling.ShowStackedTitleBorder) | |||
| 760 | m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor) | 757 | m.viper.SetDefault("workspace.styling.pane_mode_border_color", defaults.Workspace.Styling.PaneModeBorderColor) | ||
| 758 | + | m.viper.SetDefault("workspace.styling.tab_mode_border_width", defaults.Workspace.Styling.TabModeBorderWidth) | |||
| 759 | + | m.viper.SetDefault("workspace.styling.tab_mode_border_color", defaults.Workspace.Styling.TabModeBorderColor) | |||
| 761 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | 760 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | ||
| 762 | - | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | |||
| 763 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | 761 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | ||
| 764 | 762 | ||||
| 765 | // Content filtering | 763 | // Content filtering | ||
M internal/config/defaults.go +23 -20
| @@ -30,16 +30,21 @@ const ( | |||
| 30 | 30 | defaultPopupPlacement = "right" | |
| 31 | 31 | ||
| 32 | 32 | // Workspace styling defaults | |
| 33 | - | defaultBorderWidth = 1 | |
| 34 | - | defaultBorderColor = "@theme_selected_bg_color" | |
| 35 | - | defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift | |
| 36 | - | defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable | |
| 37 | - | defaultShowStackedTitleBorder = false // Hidden by default | |
| 38 | - | defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator | |
| 39 | - | defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator (distinct from pane mode) | |
| 40 | - | defaultTransitionDuration = 120 | |
| 41 | - | defaultBorderRadius = 0 | |
| 42 | - | defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%) | |
| 33 | + | // Active pane border (overlay) | |
| 34 | + | defaultBorderWidth = 1 | |
| 35 | + | defaultBorderColor = "@theme_selected_bg_color" | |
| 36 | + | ||
| 37 | + | // Pane mode border (Ctrl+P N - overlay) | |
| 38 | + | defaultPaneModeBorderWidth = 4 | |
| 39 | + | defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator | |
| 40 | + | ||
| 41 | + | // Tab mode border (Ctrl+P T - overlay) | |
| 42 | + | defaultTabModeBorderWidth = 4 | |
| 43 | + | defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator | |
| 44 | + | ||
| 45 | + | // Other styling | |
| 46 | + | defaultTransitionDuration = 120 | |
| 47 | + | defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%) | |
| 43 | 48 | ) | |
| 44 | 49 | ||
| 45 | 50 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | |
| @@ -197,16 +202,14 @@ func DefaultConfig() *Config { | |||
| 197 | 202 | OAuthAutoClose: true, // Auto-close OAuth popups on success | |
| 198 | 203 | }, | |
| 199 | 204 | Styling: WorkspaceStylingConfig{ | |
| 200 | - | BorderWidth: defaultBorderWidth, | |
| 201 | - | BorderColor: defaultBorderColor, | |
| 202 | - | InactiveBorderWidth: defaultInactiveBorderWidth, | |
| 203 | - | InactiveBorderColor: defaultInactiveBorderColor, | |
| 204 | - | ShowStackedTitleBorder: defaultShowStackedTitleBorder, | |
| 205 | - | PaneModeBorderColor: defaultPaneModeBorderColor, | |
| 206 | - | TabModeBorderColor: defaultTabModeBorderColor, | |
| 207 | - | TransitionDuration: defaultTransitionDuration, | |
| 208 | - | BorderRadius: defaultBorderRadius, | |
| 209 | - | UIScale: defaultUIScale, | |
| 205 | + | BorderWidth: defaultBorderWidth, | |
| 206 | + | BorderColor: defaultBorderColor, | |
| 207 | + | PaneModeBorderWidth: defaultPaneModeBorderWidth, | |
| 208 | + | PaneModeBorderColor: defaultPaneModeBorderColor, | |
| 209 | + | TabModeBorderWidth: defaultTabModeBorderWidth, | |
| 210 | + | TabModeBorderColor: defaultTabModeBorderColor, | |
| 211 | + | TransitionDuration: defaultTransitionDuration, | |
| 212 | + | UIScale: defaultUIScale, | |
| 210 | 213 | }, | |
| 211 | 214 | }, | |
| 212 | 215 | ContentFiltering: ContentFilteringConfig{ | |
| @@ -30,16 +30,21 @@ const ( | |||||
| 30 | defaultPopupPlacement = "right" | 30 | defaultPopupPlacement = "right" | ||
| 31 | 31 | ||||
| 32 | // Workspace styling defaults | 32 | // Workspace styling defaults | ||
| 33 | - | defaultBorderWidth = 1 | 33 | + | // Active pane border (overlay) |
| 34 | - | defaultBorderColor = "@theme_selected_bg_color" | 34 | + | defaultBorderWidth = 1 |
| 35 | - | defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift | 35 | + | defaultBorderColor = "@theme_selected_bg_color" |
| 36 | - | defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable | 36 | + | |
| 37 | - | defaultShowStackedTitleBorder = false // Hidden by default | 37 | + | // Pane mode border (Ctrl+P N - overlay) |
| 38 | - | defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator | 38 | + | defaultPaneModeBorderWidth = 4 |
| 39 | - | defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator (distinct from pane mode) | 39 | + | defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator |
| 40 | - | defaultTransitionDuration = 120 | 40 | + | |
| 41 | - | defaultBorderRadius = 0 | 41 | + | // Tab mode border (Ctrl+P T - overlay) |
| 42 | - | defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%) | 42 | + | defaultTabModeBorderWidth = 4 |
| 43 | + | defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator | |||
| 44 | + | ||||
| 45 | + | // Other styling | |||
| 46 | + | defaultTransitionDuration = 120 | |||
| 47 | + | defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%) | |||
| 43 | ) | 48 | ) | ||
| 44 | 49 | ||||
| 45 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | 50 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | ||
| @@ -197,16 +202,14 @@ func DefaultConfig() *Config { | |||||
| 197 | OAuthAutoClose: true, // Auto-close OAuth popups on success | 202 | OAuthAutoClose: true, // Auto-close OAuth popups on success | ||
| 198 | }, | 203 | }, | ||
| 199 | Styling: WorkspaceStylingConfig{ | 204 | Styling: WorkspaceStylingConfig{ | ||
| 200 | - | BorderWidth: defaultBorderWidth, | 205 | + | BorderWidth: defaultBorderWidth, |
| 201 | - | BorderColor: defaultBorderColor, | 206 | + | BorderColor: defaultBorderColor, |
| 202 | - | InactiveBorderWidth: defaultInactiveBorderWidth, | 207 | + | PaneModeBorderWidth: defaultPaneModeBorderWidth, |
| 203 | - | InactiveBorderColor: defaultInactiveBorderColor, | 208 | + | PaneModeBorderColor: defaultPaneModeBorderColor, |
| 204 | - | ShowStackedTitleBorder: defaultShowStackedTitleBorder, | 209 | + | TabModeBorderWidth: defaultTabModeBorderWidth, |
| 205 | - | PaneModeBorderColor: defaultPaneModeBorderColor, | 210 | + | TabModeBorderColor: defaultTabModeBorderColor, |
| 206 | - | TabModeBorderColor: defaultTabModeBorderColor, | 211 | + | TransitionDuration: defaultTransitionDuration, |
| 207 | - | TransitionDuration: defaultTransitionDuration, | 212 | + | UIScale: defaultUIScale, |
| 208 | - | BorderRadius: defaultBorderRadius, | |||
| 209 | - | UIScale: defaultUIScale, | |||
| 210 | }, | 213 | }, | ||
| 211 | }, | 214 | }, | ||
| 212 | ContentFiltering: ContentFilteringConfig{ | 215 | ContentFiltering: ContentFilteringConfig{ | ||
M internal/config/validation.go +5 -5
| @@ -70,15 +70,15 @@ func validateConfig(config *Config) error { | |||
| 70 | 70 | if config.Workspace.Styling.BorderWidth < 0 { | |
| 71 | 71 | validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative") | |
| 72 | 72 | } | |
| 73 | - | if config.Workspace.Styling.InactiveBorderWidth < 0 { | |
| 74 | - | validationErrors = append(validationErrors, "workspace.styling.inactive_border_width must be non-negative") | |
| 73 | + | if config.Workspace.Styling.PaneModeBorderWidth < 0 { | |
| 74 | + | validationErrors = append(validationErrors, "workspace.styling.pane_mode_border_width must be non-negative") | |
| 75 | + | } | |
| 76 | + | if config.Workspace.Styling.TabModeBorderWidth < 0 { | |
| 77 | + | validationErrors = append(validationErrors, "workspace.styling.tab_mode_border_width must be non-negative") | |
| 75 | 78 | } | |
| 76 | 79 | if config.Workspace.Styling.TransitionDuration < 0 { | |
| 77 | 80 | validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative") | |
| 78 | 81 | } | |
| 79 | - | if config.Workspace.Styling.BorderRadius < 0 { | |
| 80 | - | validationErrors = append(validationErrors, "workspace.styling.border_radius must be non-negative") | |
| 81 | - | } | |
| 82 | 82 | if config.Workspace.Styling.UIScale < 0.5 || config.Workspace.Styling.UIScale > 3.0 { | |
| 83 | 83 | validationErrors = append(validationErrors, "workspace.styling.ui_scale must be between 0.5 and 3.0") | |
| 84 | 84 | } | |
| @@ -70,15 +70,15 @@ func validateConfig(config *Config) error { | |||||
| 70 | if config.Workspace.Styling.BorderWidth < 0 { | 70 | if config.Workspace.Styling.BorderWidth < 0 { | ||
| 71 | validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative") | 71 | validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative") | ||
| 72 | } | 72 | } | ||
| 73 | - | if config.Workspace.Styling.InactiveBorderWidth < 0 { | 73 | + | if config.Workspace.Styling.PaneModeBorderWidth < 0 { |
| 74 | - | validationErrors = append(validationErrors, "workspace.styling.inactive_border_width must be non-negative") | 74 | + | validationErrors = append(validationErrors, "workspace.styling.pane_mode_border_width must be non-negative") |
| 75 | + | } | |||
| 76 | + | if config.Workspace.Styling.TabModeBorderWidth < 0 { | |||
| 77 | + | validationErrors = append(validationErrors, "workspace.styling.tab_mode_border_width must be non-negative") | |||
| 75 | } | 78 | } | ||
| 76 | if config.Workspace.Styling.TransitionDuration < 0 { | 79 | if config.Workspace.Styling.TransitionDuration < 0 { | ||
| 77 | validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative") | 80 | validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative") | ||
| 78 | } | 81 | } | ||
| 79 | - | if config.Workspace.Styling.BorderRadius < 0 { | |||
| 80 | - | validationErrors = append(validationErrors, "workspace.styling.border_radius must be non-negative") | |||
| 81 | - | } | |||
| 82 | if config.Workspace.Styling.UIScale < 0.5 || config.Workspace.Styling.UIScale > 3.0 { | 82 | if config.Workspace.Styling.UIScale < 0.5 || config.Workspace.Styling.UIScale > 3.0 { | ||
| 83 | validationErrors = append(validationErrors, "workspace.styling.ui_scale must be between 0.5 and 3.0") | 83 | validationErrors = append(validationErrors, "workspace.styling.ui_scale must be between 0.5 and 3.0") | ||
| 84 | } | 84 | } | ||