detached f101a25

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.
88 - **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.
99 - **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.
1010 - **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.
1112
1213 ### Fixed
1314 - **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.
1415
1516 ### Changed
1617 - **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.
1720
1821 ## [0.14.1] - 2025-11-18
1922
@@ -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 ### Fixed13 ### 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 ### Changed16 ### 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-1821 ## [0.14.1] - 2025-11-18
19 22
M docs/CONFIG.md +4 -5
@@ -273,15 +273,14 @@ cancel = ["escape"]
273273
274274 | Key | Type | Default | Description |
275275 |-----|------|---------|-------------|
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 |
277277 | `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 |
281279 | `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 |
282281 | `workspace.styling.tab_mode_border_color` | string | `"#FFA500"` | Tab mode border color (orange) |
283282 | `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%) |
285284
286285 ## Content Filtering
287286
@@ -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 Filtering285 ## 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) {
9999 // Initialize widgets properly using workspace manager helper
100100 spm.wm.initializePaneWidgets(newLeaf, newContainer)
101101
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+
102110 // Create title bar for the new pane
103111 newTitleBar := spm.createTitleBar(newLeaf)
104112 spm.wm.setTitleBar(newLeaf, newTitleBar)
@@ -99,6 +99,14 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) {
99 // Initialize widgets properly using workspace manager helper99 // 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 pane110 // 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 {
2323 rootContainer gtk.Widgetter // Main vertical box containing tab bar + content
2424 tabBar gtk.Widgetter // Horizontal tab bar container
2525 ContentArea gtk.Widgetter // Container for active workspace (exported for border styling)
26- tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins
2726 progressBar gtk.Widgetter // Thin load progress indicator
2827
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+
2932 // Modal state
3033 tabModeActive bool
3134 tabModeTimer *time.Timer
@@ -159,11 +162,27 @@ func (tm *TabManager) createRootContainer() error {
159162 rootOverlay.AddOverlay(progressBar)
160163 }
161164
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+
162179 // Store references
163180 tm.rootContainer = rootOverlay
164181 tm.tabBar = tabBar
165182 tm.ContentArea = contentArea
166183 tm.progressBar = progressBar
184+ tm.paneModeOverlay = paneModeOverlay
185+ tm.tabModeOverlay = tabModeOverlay
167186
168187 logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position))
169188 return nil
@@ -23,9 +23,12 @@ type TabManager struct {
23 rootContainer gtk.Widgetter // Main vertical box containing tab bar + content23 rootContainer gtk.Widgetter // Main vertical box containing tab bar + content
24 tabBar gtk.Widgetter // Horizontal tab bar container24 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 indicator26 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 state32 // Modal state
30 tabModeActive bool33 tabModeActive bool
31 tabModeTimer *time.Timer34 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 references179 // Store references
163 tm.rootContainer = rootOverlay180 tm.rootContainer = rootOverlay
164 tm.tabBar = tabBar181 tm.tabBar = tabBar
165 tm.ContentArea = contentArea182 tm.ContentArea = contentArea
166 tm.progressBar = progressBar183 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 nil188 return nil
M internal/app/browser/tab_mode.go +4 -79
@@ -5,8 +5,6 @@ import (
55 "time"
66
77 "github.com/bnema/dumber/internal/logging"
8- "github.com/bnema/dumber/pkg/webkit"
9- "github.com/diamondburned/gotk4/pkg/gtk/v4"
108 )
119
1210 // EnterTabMode activates the modal tab management mode.
@@ -202,88 +200,15 @@ func (tm *TabManager) handleRenameTab() {
202200 }
203201
204202 // 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.
206204 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()
248206 }
249207
250208 // removeTabModeBorder removes the visual indicator when exiting tab mode.
209+// Uses an overlay widget to hide the border without affecting layout.
251210 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()
287212 }
288213
289214 // 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
9595 return "#87CEEB" // Sky blue pastel for light theme
9696 }
9797
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-
11798 // generateWorkspaceCSS generates the complete CSS for workspace panes and stacked panes
11899 func (wm *WorkspaceManager) generateWorkspaceCSS() string {
119100 cfg := config.Get()
@@ -129,7 +110,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
129110 }
130111
131112 activeBorderColor := getActivePaneBorderColor(styling, isDark)
132- inactiveBorderColor := getInactivePaneBorderColor(styling, isDark)
133113
134114 // Pane mode border color - use configured value or fallback to blue
135115 paneModeColor := styling.PaneModeBorderColor
@@ -163,33 +143,42 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
163143 margin: 0;
164144 }
165145
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;
169171 }
170172
171173 /* Workspace root container */
172174 paned, box {
173175 background-color: %s;
174- transition: margin 150ms ease-in-out;
175176 }
176177
177- /* Base pane styling - configurable inactive borders */
178+ /* Base pane styling - no borders (using overlays instead) */
178179 .workspace-pane, .stacked-pane-container {
179- border-width: %dpx;
180- border-style: solid;
181- border-color: %s;
182- border-radius: %dpx;
180+ border: none;
183181 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;
193182 }
194183
195184 /* Stacked panes styling */
@@ -197,12 +186,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
197186 background-color: %s;
198187 }
199188
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-
206189 .stacked-pane-title {
207190 background-color: %s;
208191 border-bottom: %s;
@@ -267,11 +250,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
267250 animation: tab-progress-stripe 1.3s linear infinite;
268251 }
269252
270- /* Tab mode active - window background changes to border color (like pane mode) */
271- window.tab-mode-active {
272- background-color: %s;
273- }
274-
275253 /* Tab button styling - matches stacked pane titles */
276254 button.tab-button {
277255 background-color: %s;
@@ -345,17 +323,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
345323 100%% { background-position: -100%% 0%%; }
346324 }`,
347325 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
349333 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
356334 windowBackgroundColor, // stacked-pane-container background
357- styling.InactiveBorderWidth, // stacked-pane-container.active border-width
358- inactiveBorderColor, // stacked-pane-container.active border-color
359335 getStackTitleBg(isDark), // stacked-pane-title background
360336 stackedTitleBorder, // stacked-pane-title border-bottom
361337 titlePaddingVertical, // stacked-pane-title padding vertical
@@ -373,7 +349,6 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
373349 "#606060", // tab progress gradient start (neutral gray)
374350 "#747474", // tab progress gradient middle (neutral gray)
375351 "#8a8a8a", // tab progress gradient end (neutral gray)
376- tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue)
377352 getStackTitleBg(isDark), // tab-button background (same as stacked title)
378353 getStackTitleBorderColor(isDark), // tab-button border-right
379354 titlePaddingVertical, // tab-button padding vertical
@@ -95,25 +95,6 @@ func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool
95 return "#87CEEB" // Sky blue pastel for light theme95 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 panes98 // 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 blue114 // Pane mode border color - use configured value or fallback to blue
135 paneModeColor := styling.PaneModeBorderColor115 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 background325 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 background333 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 background334 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 background335 getStackTitleBg(isDark), // stacked-pane-title background
360 stackedTitleBorder, // stacked-pane-title border-bottom336 stackedTitleBorder, // stacked-pane-title border-bottom
361 titlePaddingVertical, // stacked-pane-title padding vertical337 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-right353 getStackTitleBorderColor(isDark), // tab-button border-right
379 titlePaddingVertical, // tab-button padding vertical354 titlePaddingVertical, // tab-button padding vertical
M internal/app/browser/workspace_pane_mode.go +4 -82
@@ -3,9 +3,6 @@ package browser
33
44 import (
55 "log"
6-
7- "github.com/bnema/dumber/pkg/webkit"
8- "github.com/diamondburned/gotk4/pkg/gtk/v4"
96 )
107
118 // EnterPaneMode activates pane mode on the currently focused pane
@@ -205,91 +202,16 @@ func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
205202 }
206203 }
207204
208-// applyPaneModeBorder applies pane mode visual indicator using GTK margins
205+// applyPaneModeBorder applies pane mode visual indicator using overlay
209206 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
221207 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()
237209 }
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)
257210 }
258211
259212 // removePaneModeBorder removes the pane mode visual indicator
260213 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()
275216 }
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)
295217 }
@@ -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 pane8 // 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 margins205+// 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 indicator212 // 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
476476 }
477477 wm.initializePaneWidgets(newLeaf, newContainer)
478478
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+
479487 split := &paneNode{
480488 parent: splitTarget.parent,
481489 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 {
5151 right *paneNode
5252
5353 // 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
5859
5960 // Window type tracking
6061 windowType webkit.WindowType // Tab or Popup
@@ -51,10 +51,11 @@ type paneNode struct {
51 right *paneNode51 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.Orientation55+ borderOverlay gtk.Widgetter // Border overlay widget (for active pane visual indicator)
56- isLeaf bool56+ orientation gtk.Orientation
57- isPopup bool // Deprecated: use windowType instead57+ isLeaf bool
58+ isPopup bool // Deprecated: use windowType instead
58 59
59 // Window type tracking60 // Window type tracking
60 windowType webkit.WindowType // Tab or Popup61 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) {
5959
6060 // initializePaneWidgets sets up widgets for a paneNode
6161 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+ }
6375
6476 // Add base CSS class to the container
6577 if node.container != nil {
@@ -78,7 +90,20 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Wid
7890 // setContainer sets the container widget
7991 func (wm *WorkspaceManager) setContainer(node *paneNode, widget gtk.Widgetter, typeInfo string) {
8092 _ = 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+ }
82107
83108 // Mark widget validity based on container presence so idle guards run for stacks too
84109 if widget != nil {
@@ -144,9 +169,9 @@ func (wm *WorkspaceManager) borderTargets(node *paneNode) []gtk.Widgetter {
144169 return targets
145170 }
146171
147-// applyActivePaneBorder marks the target widgets as active for CSS styling.
172+// applyActivePaneBorder shows the border overlay for the active pane.
148173 func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
149- if wm == nil || node == nil {
174+ if wm == nil || node == nil || node.borderOverlay == nil {
150175 return
151176 }
152177
@@ -156,36 +181,18 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
156181 return
157182 }
158183
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)
170186 }
171187
172-// removeActivePaneBorder clears the active styling from a pane.
188+// removeActivePaneBorder hides the border overlay for a pane.
173189 func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
174- if wm == nil || node == nil {
190+ if wm == nil || node == nil || node.borderOverlay == nil {
175191 return
176192 }
177193
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)
189196 }
190197
191198 // Widget cleanup utilities
@@ -59,7 +59,19 @@ func mapDirection(direction string) (gtk.Orientation, bool) {
59 59
60 // initializePaneWidgets sets up widgets for a paneNode60 // 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 = widget62+ // 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 container76 // 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 widget90 // 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 used92 _ = typeInfo // Type info kept for API compatibility but not used
81- node.container = widget93+
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 too108 // 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 targets169 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 return175 return
151 }176 }
152 177
@@ -156,36 +181,18 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
156 return181 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 return191 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 utilities198 // Widget cleanup utilities
M internal/config/config.go +11 -13
@@ -336,26 +336,25 @@ type PopupBehaviorConfig struct {
336336
337337 // WorkspaceStylingConfig defines visual styling for workspace panes.
338338 type WorkspaceStylingConfig struct {
339- // BorderWidth in pixels for active pane borders
339+ // BorderWidth in pixels for active pane borders (overlay)
340340 BorderWidth int `mapstructure:"border_width" yaml:"border_width" json:"border_width"`
341341 // BorderColor for focused panes (CSS color value or theme variable)
342342 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"`
349346 // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
350347 // Defaults to "#4A90E2" (blue) if not set
351348 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"`
352352 // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable)
353353 // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor
354354 TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"`
355+
355356 // TransitionDuration in milliseconds for border animations
356357 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"`
359358 // UIScale is a multiplier for UI elements like title bars (1.0 = 100%, 1.2 = 120%)
360359 UIScale float64 `mapstructure:"ui_scale" yaml:"ui_scale" json:"ui_scale"`
361360 }
@@ -754,12 +753,11 @@ func (m *Manager) setDefaults() {
754753 m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
755754 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
756755 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)
760757 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)
761760 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
762- m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius)
763761 m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale)
764762
765763 // 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 borders339+ // 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 set347 // 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 PaneModeBorderColor353 // 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 animations356 // 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 filtering763 // Content filtering
M internal/config/defaults.go +23 -20
@@ -30,16 +30,21 @@ const (
3030 defaultPopupPlacement = "right"
3131
3232 // 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%)
4348 )
4449
4550 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -197,16 +202,14 @@ func DefaultConfig() *Config {
197202 OAuthAutoClose: true, // Auto-close OAuth popups on success
198203 },
199204 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,
210213 },
211214 },
212215 ContentFiltering: ContentFilteringConfig{
@@ -30,16 +30,21 @@ const (
30 defaultPopupPlacement = "right"30 defaultPopupPlacement = "right"
31 31
32 // Workspace styling defaults32 // Workspace styling defaults
33- defaultBorderWidth = 133+ // Active pane border (overlay)
34- defaultBorderColor = "@theme_selected_bg_color"34+ defaultBorderWidth = 1
35- defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift35+ defaultBorderColor = "@theme_selected_bg_color"
36- defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable36+
37- defaultShowStackedTitleBorder = false // Hidden by default37+ // Pane mode border (Ctrl+P N - overlay)
38- defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator38+ defaultPaneModeBorderWidth = 4
39- defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator (distinct from pane mode)39+ defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator
40- defaultTransitionDuration = 12040+
41- defaultBorderRadius = 041+ // 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 error50 // 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 success202 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 {
7070 if config.Workspace.Styling.BorderWidth < 0 {
7171 validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative")
7272 }
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")
7578 }
7679 if config.Workspace.Styling.TransitionDuration < 0 {
7780 validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative")
7881 }
79- if config.Workspace.Styling.BorderRadius < 0 {
80- validationErrors = append(validationErrors, "workspace.styling.border_radius must be non-negative")
81- }
8282 if config.Workspace.Styling.UIScale < 0.5 || config.Workspace.Styling.UIScale > 3.0 {
8383 validationErrors = append(validationErrors, "workspace.styling.ui_scale must be between 0.5 and 3.0")
8484 }
@@ -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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close