detached 9c5173a

fix: stacked pane close button now properly closes instead of switching pane

commit
9c5173afe89ea1e788907c941d254f7b7f253bae
author
brice · · 2026-01-01 18:49 UTC
committer
brice · · 2026-01-01 18:56 UTC
parent
2b2832e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +189 -203
M internal/ui/component/workspace_view_test.go +13 -12
@@ -128,16 +128,18 @@ func setupStackedLeafMocks(
128128 mockTitleBar := mocks.NewMockBoxWidget(t)
129129 mockFavicon := mocks.NewMockImageWidget(t)
130130 mockLabel := mocks.NewMockLabelWidget(t)
131- mockButton := mocks.NewMockButtonWidget(t)
132131 mockCloseButton := mocks.NewMockButtonWidget(t)
133132
134133 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
135134 mockStackBox.EXPECT().SetHexpand(true).Once()
136135 mockStackBox.EXPECT().SetVexpand(true).Once()
137136
137+ // Title bar creation - now directly used without button wrapper
138138 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
139139 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
140+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
140141 mockTitleBar.EXPECT().SetVexpand(false).Once()
142+ mockTitleBar.EXPECT().SetHexpand(true).Once()
141143
142144 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
143145 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
@@ -160,22 +162,21 @@ func setupStackedLeafMocks(
160162 mockCloseButton.EXPECT().SetHexpand(false).Once()
161163 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
162164
163- // Title bar button
164- mockFactory.EXPECT().NewButton().Return(mockButton).Once()
165- mockButton.EXPECT().SetChild(mockTitleBar).Once()
166- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
167- mockButton.EXPECT().SetFocusOnClick(false).Once()
168- mockButton.EXPECT().SetVexpand(false).Once()
169- mockButton.EXPECT().SetHexpand(true).Once()
165+ // GestureClick is added to titleBar via AddController
166+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
170167
171- // Click handlers
172- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
168+ // Close button click handler
173169 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
174170
175- mockStackBox.EXPECT().Append(mockButton).Once()
171+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
172+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
173+
174+ // Adding to main box - now titleBar is added directly (no button wrapper)
175+ mockStackBox.EXPECT().Append(mockTitleBar).Once()
176176 mockStackBox.EXPECT().Append(container).Once()
177177
178- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
178+ // Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
179+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
179180 container.EXPECT().SetVisible(true).Once()
180181 mockTitleBar.EXPECT().AddCssClass("active").Once()
181182
@@ -128,16 +128,18 @@ func setupStackedLeafMocks(
128 mockTitleBar := mocks.NewMockBoxWidget(t)128 mockTitleBar := mocks.NewMockBoxWidget(t)
129 mockFavicon := mocks.NewMockImageWidget(t)129 mockFavicon := mocks.NewMockImageWidget(t)
130 mockLabel := mocks.NewMockLabelWidget(t)130 mockLabel := mocks.NewMockLabelWidget(t)
131- mockButton := mocks.NewMockButtonWidget(t)
132 mockCloseButton := mocks.NewMockButtonWidget(t)131 mockCloseButton := mocks.NewMockButtonWidget(t)
133 132
134 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()133 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
135 mockStackBox.EXPECT().SetHexpand(true).Once()134 mockStackBox.EXPECT().SetHexpand(true).Once()
136 mockStackBox.EXPECT().SetVexpand(true).Once()135 mockStackBox.EXPECT().SetVexpand(true).Once()
137 136
137+ // Title bar creation - now directly used without button wrapper
138 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()138 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
139 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()139 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
140+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
140 mockTitleBar.EXPECT().SetVexpand(false).Once()141 mockTitleBar.EXPECT().SetVexpand(false).Once()
142+ mockTitleBar.EXPECT().SetHexpand(true).Once()
141 143
142 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()144 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
143 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()145 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
@@ -160,22 +162,21 @@ func setupStackedLeafMocks(
160 mockCloseButton.EXPECT().SetHexpand(false).Once()162 mockCloseButton.EXPECT().SetHexpand(false).Once()
161 mockTitleBar.EXPECT().Append(mockCloseButton).Once()163 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
162 164
163- // Title bar button165+ // GestureClick is added to titleBar via AddController
164- mockFactory.EXPECT().NewButton().Return(mockButton).Once()166+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
165- mockButton.EXPECT().SetChild(mockTitleBar).Once()
166- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
167- mockButton.EXPECT().SetFocusOnClick(false).Once()
168- mockButton.EXPECT().SetVexpand(false).Once()
169- mockButton.EXPECT().SetHexpand(true).Once()
170 167
171- // Click handlers168+ // Close button click handler
172- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
173 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()169 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
174 170
175- mockStackBox.EXPECT().Append(mockButton).Once()171+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
172+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
173+
174+ // Adding to main box - now titleBar is added directly (no button wrapper)
175+ mockStackBox.EXPECT().Append(mockTitleBar).Once()
176 mockStackBox.EXPECT().Append(container).Once()176 mockStackBox.EXPECT().Append(container).Once()
177 177
178- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()178+ // Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
179+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
179 container.EXPECT().SetVisible(true).Once()180 container.EXPECT().SetVisible(true).Once()
180 mockTitleBar.EXPECT().AddCssClass("active").Once()181 mockTitleBar.EXPECT().AddCssClass("active").Once()
181 182
M internal/ui/layout/stacked.go +85 -64
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/dumber/internal/logging"
99 "github.com/jwijenbergh/puregotk/v4/gobject"
10+ "github.com/jwijenbergh/puregotk/v4/gtk"
1011 )
1112
1213 // ErrStackEmpty is returned when operating on an empty stack.
@@ -34,10 +35,11 @@ type stackedPane struct {
3435 isActive bool
3536
3637 // Signal handler IDs for cleanup on removal
37- titleClickSignalID uint32
3838 closeClickSignalID uint32
39- titleButton ButtonWidget // stored for signal disconnection
4039 closeButton ButtonWidget // stored for signal disconnection
40+
41+ // Retained callback for GestureClick to prevent GC
42+ titleClickCallback interface{}
4143 }
4244
4345 // StackedView manages a stack of panes where only one is visible at a time.
@@ -70,19 +72,21 @@ func NewStackedView(factory WidgetFactory) *StackedView {
7072
7173 // titleBarComponents holds the widgets created for a pane's title bar.
7274 type titleBarComponents struct {
73- titleBar BoxWidget
74- titleButton ButtonWidget
75- closeBtn ButtonWidget
76- favicon ImageWidget
77- label LabelWidget
75+ titleBar BoxWidget
76+ closeBtn ButtonWidget
77+ favicon ImageWidget
78+ label LabelWidget
7879 }
7980
8081 // createTitleBar creates the title bar widgets for a pane.
8182 func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarComponents {
8283 // Create title bar container - must not expand vertically
84+ // This is now the clickable container directly (using GestureClick), not wrapped in a button
8385 titleBar := sv.factory.NewBox(OrientationHorizontal, 4)
8486 titleBar.AddCssClass("stacked-pane-titlebar")
87+ titleBar.AddCssClass("stacked-pane-title-clickable") // For hover styling
8588 titleBar.SetVexpand(false)
89+ titleBar.SetHexpand(true) // Fill horizontal space
8690
8791 // Create favicon image
8892 favicon := sv.factory.NewImage()
@@ -111,20 +115,11 @@ func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarCom
111115 closeBtn.SetHexpand(false)
112116 titleBar.Append(closeBtn)
113117
114- // Make title bar clickable - ensure it doesn't expand vertically
115- titleButton := sv.factory.NewButton()
116- titleButton.SetChild(titleBar)
117- titleButton.AddCssClass("stacked-pane-title-button")
118- titleButton.SetFocusOnClick(false)
119- titleButton.SetVexpand(false) // Critical: don't let title bar expand vertically
120- titleButton.SetHexpand(true) // But fill horizontal space
121-
122118 return titleBarComponents{
123- titleBar: titleBar,
124- titleButton: titleButton,
125- closeBtn: closeBtn,
126- favicon: favicon,
127- label: label,
119+ titleBar: titleBar,
120+ closeBtn: closeBtn,
121+ favicon: favicon,
122+ label: label,
128123 }
129124 }
130125
@@ -145,7 +140,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
145140 tb := sv.createTitleBar(title, faviconIconName)
146141
147142 // Connect click handlers using paneID (not index, to handle removals)
148- titleSignalID, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
143+ titleClickCb, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
149144
150145 pane := &stackedPane{
151146 paneID: paneID,
@@ -155,20 +150,19 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
155150 favicon: tb.favicon,
156151 label: tb.label,
157152 isActive: false,
158- titleClickSignalID: titleSignalID,
159153 closeClickSignalID: closeSignalID,
160- titleButton: tb.titleButton,
161154 closeButton: tb.closeBtn,
155+ titleClickCallback: titleClickCb,
162156 }
163157
164158 index := len(sv.panes)
165159 sv.panes = append(sv.panes, pane)
166160
167- // Add to container
161+ // Add to container - now we add the titleBar directly (not wrapped in a button)
168162 log.Debug().
169163 Int("index", index).
170- Msg("StackedView: appending titleButton and container to box")
171- sv.box.Append(tb.titleButton)
164+ Msg("StackedView: appending titleBar and container to box")
165+ sv.box.Append(tb.titleBar)
172166 if container != nil {
173167 sv.box.Append(container)
174168 }
@@ -186,10 +180,40 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
186180
187181 // connectTitleBarHandlers connects the click handlers for a title bar.
188182 // Uses paneID lookup instead of captured index to handle pane removals correctly.
189-// Returns the signal IDs for later disconnection.
190-func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID string) (titleSignalID, closeSignalID uint32) {
191- // Connect title bar click handler for activation
192- titleSignalID = tb.titleButton.ConnectClicked(func() {
183+// Returns the retained callback (to prevent GC) and close signal ID for disconnection.
184+func (sv *StackedView) connectTitleBarHandlers(
185+ tb titleBarComponents, paneID string,
186+) (titleClickCallback interface{}, closeSignalID uint32) {
187+ // Connect title bar click handler using GestureClick
188+ // This prevents event propagation issues with nested buttons
189+ clickCtrl := gtk.NewGestureClick()
190+
191+ // Store reference to close button for hit testing
192+ closeBtn := tb.closeBtn
193+
194+ clickCb := func(_ gtk.GestureClick, _ int, clickX float64, clickY float64) {
195+ // Check if click is on the close button - if so, don't activate
196+ // The close button handles its own click event
197+ if closeBtn != nil {
198+ closeBtnWidget := closeBtn.GtkWidget()
199+ if closeBtnWidget != nil {
200+ // Get close button's allocated position and size
201+ btnWidth := closeBtn.GetAllocatedWidth()
202+ btnHeight := closeBtn.GetAllocatedHeight()
203+
204+ // Get close button's position relative to the title bar
205+ btnX, btnY, ok := closeBtn.ComputePoint(tb.titleBar)
206+ if ok && btnWidth > 0 && btnHeight > 0 {
207+ // Check if click is within close button bounds
208+ if clickX >= btnX && clickX <= btnX+float64(btnWidth) &&
209+ clickY >= btnY && clickY <= btnY+float64(btnHeight) {
210+ // Click is on close button, don't activate
211+ return
212+ }
213+ }
214+ }
215+ }
216+
193217 sv.mu.RLock()
194218 callback := sv.onActivate
195219 currentIndex := sv.findPaneIndexInternal(paneID)
@@ -198,7 +222,9 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
198222 if callback != nil && currentIndex >= 0 {
199223 callback(currentIndex)
200224 }
201- })
225+ }
226+ clickCtrl.ConnectPressed(&clickCb)
227+ tb.titleBar.AddController(&clickCtrl.EventController)
202228
203229 // Connect close button click handler
204230 closeSignalID = tb.closeBtn.ConnectClicked(func() {
@@ -211,19 +237,21 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
211237 }
212238 })
213239
214- return titleSignalID, closeSignalID
240+ return clickCb, closeSignalID
215241 }
216242
217243 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.
218244 // This prevents memory leaks when panes are removed from the stack.
219245 // Note: This is a no-op when using mock widgets in tests (GtkWidget returns nil).
246+// Note: GestureClick callbacks are cleaned up automatically when the widget is destroyed.
220247 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {
221248 if pane == nil {
222249 return
223250 }
224251
225- // Disconnect title button click signal
226- disconnectButtonSignal(pane.titleButton, pane.titleClickSignalID)
252+ // Clear retained callback reference to allow GC
253+ // The GestureClick controller is owned by the widget and will be cleaned up when the widget is destroyed
254+ pane.titleClickCallback = nil
227255
228256 // Disconnect close button click signal
229257 disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID)
@@ -281,7 +309,7 @@ func (sv *StackedView) InsertPaneAfter(
281309 tb := sv.createTitleBar(title, faviconIconName)
282310
283311 // Connect click handlers using paneID (not index, to handle removals)
284- titleSignalID, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
312+ titleClickCb, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
285313
286314 pane := &stackedPane{
287315 paneID: paneID,
@@ -291,10 +319,9 @@ func (sv *StackedView) InsertPaneAfter(
291319 favicon: tb.favicon,
292320 label: tb.label,
293321 isActive: false,
294- titleClickSignalID: titleSignalID,
295322 closeClickSignalID: closeSignalID,
296- titleButton: tb.titleButton,
297323 closeButton: tb.closeBtn,
324+ titleClickCallback: titleClickCb,
298325 }
299326
300327 // Insert into slice at correct position
@@ -303,7 +330,7 @@ func (sv *StackedView) InsertPaneAfter(
303330 sv.panes[insertIndex] = pane
304331
305332 // Insert widgets at correct position in GTK box
306- sv.insertTitleBarWidgets(tb.titleButton, container, insertIndex)
333+ sv.insertTitleBarWidgets(tb.titleBar, container, insertIndex)
307334
308335 // Set this pane as active
309336 sv.setActiveInternal(ctx, insertIndex)
@@ -316,27 +343,26 @@ func (sv *StackedView) InsertPaneAfter(
316343 return insertIndex
317344 }
318345
319-// insertTitleBarWidgets inserts the title bar button and container at the correct position.
320-func (sv *StackedView) insertTitleBarWidgets(titleButton ButtonWidget, container Widget, insertIndex int) {
346+// insertTitleBarWidgets inserts the title bar and container at the correct position.
347+func (sv *StackedView) insertTitleBarWidgets(titleBar BoxWidget, container Widget, insertIndex int) {
321348 if insertIndex > 0 && sv.panes[insertIndex-1] != nil {
322349 // Insert after the previous pane's container
323350 prevPane := sv.panes[insertIndex-1]
324351 if prevPane.container != nil {
325- sv.box.InsertChildAfter(titleButton, prevPane.container)
352+ sv.box.InsertChildAfter(titleBar, prevPane.container)
326353 if container != nil {
327- sv.box.InsertChildAfter(container, titleButton)
354+ sv.box.InsertChildAfter(container, titleBar)
328355 }
329356 } else {
330- // No container, insert after title bar's parent (the button)
331- prevTitleParent := prevPane.titleBar.GetParent()
332- if prevTitleParent != nil {
333- sv.box.InsertChildAfter(titleButton, prevTitleParent)
357+ // No container, insert after the previous pane's title bar directly
358+ if prevPane.titleBar != nil {
359+ sv.box.InsertChildAfter(titleBar, prevPane.titleBar)
334360 if container != nil {
335- sv.box.InsertChildAfter(container, titleButton)
361+ sv.box.InsertChildAfter(container, titleBar)
336362 }
337363 } else {
338364 // Fallback to append
339- sv.box.Append(titleButton)
365+ sv.box.Append(titleBar)
340366 if container != nil {
341367 sv.box.Append(container)
342368 }
@@ -347,7 +373,7 @@ func (sv *StackedView) insertTitleBarWidgets(titleButton ButtonWidget, container
347373 if container != nil {
348374 sv.box.Prepend(container)
349375 }
350- sv.box.Prepend(titleButton)
376+ sv.box.Prepend(titleBar)
351377 }
352378 }
353379
@@ -380,11 +406,8 @@ func (sv *StackedView) RemovePane(ctx context.Context, index int) error {
380406
381407 // Remove from container
382408 if pane.titleBar != nil {
383- // The title bar is wrapped in a button, need to get parent
384- parent := pane.titleBar.GetParent()
385- if parent != nil {
386- sv.box.Remove(parent)
387- }
409+ // The title bar is now added directly to the box (no button wrapper)
410+ sv.box.Remove(pane.titleBar)
388411 }
389412 if pane.container != nil {
390413 sv.box.Remove(pane.container)
@@ -451,17 +474,15 @@ func (sv *StackedView) updateVisibilityInternal(ctx context.Context) {
451474 pane.isActive = isActive
452475
453476 // Title bar is always visible for inactive panes, hidden for active
477+ // The title bar is now added directly to the box (no button wrapper)
454478 if pane.titleBar != nil {
455- parent := pane.titleBar.GetParent()
456- if parent != nil {
457- parent.SetVisible(!isActive)
458- log.Debug().
459- Int("pane_index", i).
460- Str("title", pane.title).
461- Bool("is_active", isActive).
462- Bool("titlebar_visible", !isActive).
463- Msg("StackedView: set titlebar visibility")
464- }
479+ pane.titleBar.SetVisible(!isActive)
480+ log.Debug().
481+ Int("pane_index", i).
482+ Str("title", pane.title).
483+ Bool("is_active", isActive).
484+ Bool("titlebar_visible", !isActive).
485+ Msg("StackedView: set titlebar visibility")
465486 }
466487
467488 // Container is visible only for active pane
@@ -7,6 +7,7 @@ import (
7 7
8 "github.com/bnema/dumber/internal/logging"8 "github.com/bnema/dumber/internal/logging"
9 "github.com/jwijenbergh/puregotk/v4/gobject"9 "github.com/jwijenbergh/puregotk/v4/gobject"
10+ "github.com/jwijenbergh/puregotk/v4/gtk"
10 )11 )
11 12
12 // ErrStackEmpty is returned when operating on an empty stack.13 // ErrStackEmpty is returned when operating on an empty stack.
@@ -34,10 +35,11 @@ type stackedPane struct {
34 isActive bool35 isActive bool
35 36
36 // Signal handler IDs for cleanup on removal37 // Signal handler IDs for cleanup on removal
37- titleClickSignalID uint32
38 closeClickSignalID uint3238 closeClickSignalID uint32
39- titleButton ButtonWidget // stored for signal disconnection
40 closeButton ButtonWidget // stored for signal disconnection39 closeButton ButtonWidget // stored for signal disconnection
40+
41+ // Retained callback for GestureClick to prevent GC
42+ titleClickCallback interface{}
41 }43 }
42 44
43 // StackedView manages a stack of panes where only one is visible at a time.45 // StackedView manages a stack of panes where only one is visible at a time.
@@ -70,19 +72,21 @@ func NewStackedView(factory WidgetFactory) *StackedView {
70 72
71 // titleBarComponents holds the widgets created for a pane's title bar.73 // titleBarComponents holds the widgets created for a pane's title bar.
72 type titleBarComponents struct {74 type titleBarComponents struct {
73- titleBar BoxWidget75+ titleBar BoxWidget
74- titleButton ButtonWidget76+ closeBtn ButtonWidget
75- closeBtn ButtonWidget77+ favicon ImageWidget
76- favicon ImageWidget78+ label LabelWidget
77- label LabelWidget
78 }79 }
79 80
80 // createTitleBar creates the title bar widgets for a pane.81 // createTitleBar creates the title bar widgets for a pane.
81 func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarComponents {82 func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarComponents {
82 // Create title bar container - must not expand vertically83 // Create title bar container - must not expand vertically
84+ // This is now the clickable container directly (using GestureClick), not wrapped in a button
83 titleBar := sv.factory.NewBox(OrientationHorizontal, 4)85 titleBar := sv.factory.NewBox(OrientationHorizontal, 4)
84 titleBar.AddCssClass("stacked-pane-titlebar")86 titleBar.AddCssClass("stacked-pane-titlebar")
87+ titleBar.AddCssClass("stacked-pane-title-clickable") // For hover styling
85 titleBar.SetVexpand(false)88 titleBar.SetVexpand(false)
89+ titleBar.SetHexpand(true) // Fill horizontal space
86 90
87 // Create favicon image91 // Create favicon image
88 favicon := sv.factory.NewImage()92 favicon := sv.factory.NewImage()
@@ -111,20 +115,11 @@ func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarCom
111 closeBtn.SetHexpand(false)115 closeBtn.SetHexpand(false)
112 titleBar.Append(closeBtn)116 titleBar.Append(closeBtn)
113 117
114- // Make title bar clickable - ensure it doesn't expand vertically
115- titleButton := sv.factory.NewButton()
116- titleButton.SetChild(titleBar)
117- titleButton.AddCssClass("stacked-pane-title-button")
118- titleButton.SetFocusOnClick(false)
119- titleButton.SetVexpand(false) // Critical: don't let title bar expand vertically
120- titleButton.SetHexpand(true) // But fill horizontal space
121-
122 return titleBarComponents{118 return titleBarComponents{
123- titleBar: titleBar,119+ titleBar: titleBar,
124- titleButton: titleButton,120+ closeBtn: closeBtn,
125- closeBtn: closeBtn,121+ favicon: favicon,
126- favicon: favicon,122+ label: label,
127- label: label,
128 }123 }
129 }124 }
130 125
@@ -145,7 +140,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
145 tb := sv.createTitleBar(title, faviconIconName)140 tb := sv.createTitleBar(title, faviconIconName)
146 141
147 // Connect click handlers using paneID (not index, to handle removals)142 // Connect click handlers using paneID (not index, to handle removals)
148- titleSignalID, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)143+ titleClickCb, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
149 144
150 pane := &stackedPane{145 pane := &stackedPane{
151 paneID: paneID,146 paneID: paneID,
@@ -155,20 +150,19 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
155 favicon: tb.favicon,150 favicon: tb.favicon,
156 label: tb.label,151 label: tb.label,
157 isActive: false,152 isActive: false,
158- titleClickSignalID: titleSignalID,
159 closeClickSignalID: closeSignalID,153 closeClickSignalID: closeSignalID,
160- titleButton: tb.titleButton,
161 closeButton: tb.closeBtn,154 closeButton: tb.closeBtn,
155+ titleClickCallback: titleClickCb,
162 }156 }
163 157
164 index := len(sv.panes)158 index := len(sv.panes)
165 sv.panes = append(sv.panes, pane)159 sv.panes = append(sv.panes, pane)
166 160
167- // Add to container161+ // Add to container - now we add the titleBar directly (not wrapped in a button)
168 log.Debug().162 log.Debug().
169 Int("index", index).163 Int("index", index).
170- Msg("StackedView: appending titleButton and container to box")164+ Msg("StackedView: appending titleBar and container to box")
171- sv.box.Append(tb.titleButton)165+ sv.box.Append(tb.titleBar)
172 if container != nil {166 if container != nil {
173 sv.box.Append(container)167 sv.box.Append(container)
174 }168 }
@@ -186,10 +180,40 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
186 180
187 // connectTitleBarHandlers connects the click handlers for a title bar.181 // connectTitleBarHandlers connects the click handlers for a title bar.
188 // Uses paneID lookup instead of captured index to handle pane removals correctly.182 // Uses paneID lookup instead of captured index to handle pane removals correctly.
189-// Returns the signal IDs for later disconnection.183+// Returns the retained callback (to prevent GC) and close signal ID for disconnection.
190-func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID string) (titleSignalID, closeSignalID uint32) {184+func (sv *StackedView) connectTitleBarHandlers(
191- // Connect title bar click handler for activation185+ tb titleBarComponents, paneID string,
192- titleSignalID = tb.titleButton.ConnectClicked(func() {186+) (titleClickCallback interface{}, closeSignalID uint32) {
187+ // Connect title bar click handler using GestureClick
188+ // This prevents event propagation issues with nested buttons
189+ clickCtrl := gtk.NewGestureClick()
190+
191+ // Store reference to close button for hit testing
192+ closeBtn := tb.closeBtn
193+
194+ clickCb := func(_ gtk.GestureClick, _ int, clickX float64, clickY float64) {
195+ // Check if click is on the close button - if so, don't activate
196+ // The close button handles its own click event
197+ if closeBtn != nil {
198+ closeBtnWidget := closeBtn.GtkWidget()
199+ if closeBtnWidget != nil {
200+ // Get close button's allocated position and size
201+ btnWidth := closeBtn.GetAllocatedWidth()
202+ btnHeight := closeBtn.GetAllocatedHeight()
203+
204+ // Get close button's position relative to the title bar
205+ btnX, btnY, ok := closeBtn.ComputePoint(tb.titleBar)
206+ if ok && btnWidth > 0 && btnHeight > 0 {
207+ // Check if click is within close button bounds
208+ if clickX >= btnX && clickX <= btnX+float64(btnWidth) &&
209+ clickY >= btnY && clickY <= btnY+float64(btnHeight) {
210+ // Click is on close button, don't activate
211+ return
212+ }
213+ }
214+ }
215+ }
216+
193 sv.mu.RLock()217 sv.mu.RLock()
194 callback := sv.onActivate218 callback := sv.onActivate
195 currentIndex := sv.findPaneIndexInternal(paneID)219 currentIndex := sv.findPaneIndexInternal(paneID)
@@ -198,7 +222,9 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
198 if callback != nil && currentIndex >= 0 {222 if callback != nil && currentIndex >= 0 {
199 callback(currentIndex)223 callback(currentIndex)
200 }224 }
201- })225+ }
226+ clickCtrl.ConnectPressed(&clickCb)
227+ tb.titleBar.AddController(&clickCtrl.EventController)
202 228
203 // Connect close button click handler229 // Connect close button click handler
204 closeSignalID = tb.closeBtn.ConnectClicked(func() {230 closeSignalID = tb.closeBtn.ConnectClicked(func() {
@@ -211,19 +237,21 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
211 }237 }
212 })238 })
213 239
214- return titleSignalID, closeSignalID240+ return clickCb, closeSignalID
215 }241 }
216 242
217 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.243 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.
218 // This prevents memory leaks when panes are removed from the stack.244 // This prevents memory leaks when panes are removed from the stack.
219 // Note: This is a no-op when using mock widgets in tests (GtkWidget returns nil).245 // Note: This is a no-op when using mock widgets in tests (GtkWidget returns nil).
246+// Note: GestureClick callbacks are cleaned up automatically when the widget is destroyed.
220 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {247 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {
221 if pane == nil {248 if pane == nil {
222 return249 return
223 }250 }
224 251
225- // Disconnect title button click signal252+ // Clear retained callback reference to allow GC
226- disconnectButtonSignal(pane.titleButton, pane.titleClickSignalID)253+ // The GestureClick controller is owned by the widget and will be cleaned up when the widget is destroyed
254+ pane.titleClickCallback = nil
227 255
228 // Disconnect close button click signal256 // Disconnect close button click signal
229 disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID)257 disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID)
@@ -281,7 +309,7 @@ func (sv *StackedView) InsertPaneAfter(
281 tb := sv.createTitleBar(title, faviconIconName)309 tb := sv.createTitleBar(title, faviconIconName)
282 310
283 // Connect click handlers using paneID (not index, to handle removals)311 // Connect click handlers using paneID (not index, to handle removals)
284- titleSignalID, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)312+ titleClickCb, closeSignalID := sv.connectTitleBarHandlers(tb, paneID)
285 313
286 pane := &stackedPane{314 pane := &stackedPane{
287 paneID: paneID,315 paneID: paneID,
@@ -291,10 +319,9 @@ func (sv *StackedView) InsertPaneAfter(
291 favicon: tb.favicon,319 favicon: tb.favicon,
292 label: tb.label,320 label: tb.label,
293 isActive: false,321 isActive: false,
294- titleClickSignalID: titleSignalID,
295 closeClickSignalID: closeSignalID,322 closeClickSignalID: closeSignalID,
296- titleButton: tb.titleButton,
297 closeButton: tb.closeBtn,323 closeButton: tb.closeBtn,
324+ titleClickCallback: titleClickCb,
298 }325 }
299 326
300 // Insert into slice at correct position327 // Insert into slice at correct position
@@ -303,7 +330,7 @@ func (sv *StackedView) InsertPaneAfter(
303 sv.panes[insertIndex] = pane330 sv.panes[insertIndex] = pane
304 331
305 // Insert widgets at correct position in GTK box332 // Insert widgets at correct position in GTK box
306- sv.insertTitleBarWidgets(tb.titleButton, container, insertIndex)333+ sv.insertTitleBarWidgets(tb.titleBar, container, insertIndex)
307 334
308 // Set this pane as active335 // Set this pane as active
309 sv.setActiveInternal(ctx, insertIndex)336 sv.setActiveInternal(ctx, insertIndex)
@@ -316,27 +343,26 @@ func (sv *StackedView) InsertPaneAfter(
316 return insertIndex343 return insertIndex
317 }344 }
318 345
319-// insertTitleBarWidgets inserts the title bar button and container at the correct position.346+// insertTitleBarWidgets inserts the title bar and container at the correct position.
320-func (sv *StackedView) insertTitleBarWidgets(titleButton ButtonWidget, container Widget, insertIndex int) {347+func (sv *StackedView) insertTitleBarWidgets(titleBar BoxWidget, container Widget, insertIndex int) {
321 if insertIndex > 0 && sv.panes[insertIndex-1] != nil {348 if insertIndex > 0 && sv.panes[insertIndex-1] != nil {
322 // Insert after the previous pane's container349 // Insert after the previous pane's container
323 prevPane := sv.panes[insertIndex-1]350 prevPane := sv.panes[insertIndex-1]
324 if prevPane.container != nil {351 if prevPane.container != nil {
325- sv.box.InsertChildAfter(titleButton, prevPane.container)352+ sv.box.InsertChildAfter(titleBar, prevPane.container)
326 if container != nil {353 if container != nil {
327- sv.box.InsertChildAfter(container, titleButton)354+ sv.box.InsertChildAfter(container, titleBar)
328 }355 }
329 } else {356 } else {
330- // No container, insert after title bar's parent (the button)357+ // No container, insert after the previous pane's title bar directly
331- prevTitleParent := prevPane.titleBar.GetParent()358+ if prevPane.titleBar != nil {
332- if prevTitleParent != nil {359+ sv.box.InsertChildAfter(titleBar, prevPane.titleBar)
333- sv.box.InsertChildAfter(titleButton, prevTitleParent)
334 if container != nil {360 if container != nil {
335- sv.box.InsertChildAfter(container, titleButton)361+ sv.box.InsertChildAfter(container, titleBar)
336 }362 }
337 } else {363 } else {
338 // Fallback to append364 // Fallback to append
339- sv.box.Append(titleButton)365+ sv.box.Append(titleBar)
340 if container != nil {366 if container != nil {
341 sv.box.Append(container)367 sv.box.Append(container)
342 }368 }
@@ -347,7 +373,7 @@ func (sv *StackedView) insertTitleBarWidgets(titleButton ButtonWidget, container
347 if container != nil {373 if container != nil {
348 sv.box.Prepend(container)374 sv.box.Prepend(container)
349 }375 }
350- sv.box.Prepend(titleButton)376+ sv.box.Prepend(titleBar)
351 }377 }
352 }378 }
353 379
@@ -380,11 +406,8 @@ func (sv *StackedView) RemovePane(ctx context.Context, index int) error {
380 406
381 // Remove from container407 // Remove from container
382 if pane.titleBar != nil {408 if pane.titleBar != nil {
383- // The title bar is wrapped in a button, need to get parent409+ // The title bar is now added directly to the box (no button wrapper)
384- parent := pane.titleBar.GetParent()410+ sv.box.Remove(pane.titleBar)
385- if parent != nil {
386- sv.box.Remove(parent)
387- }
388 }411 }
389 if pane.container != nil {412 if pane.container != nil {
390 sv.box.Remove(pane.container)413 sv.box.Remove(pane.container)
@@ -451,17 +474,15 @@ func (sv *StackedView) updateVisibilityInternal(ctx context.Context) {
451 pane.isActive = isActive474 pane.isActive = isActive
452 475
453 // Title bar is always visible for inactive panes, hidden for active476 // Title bar is always visible for inactive panes, hidden for active
477+ // The title bar is now added directly to the box (no button wrapper)
454 if pane.titleBar != nil {478 if pane.titleBar != nil {
455- parent := pane.titleBar.GetParent()479+ pane.titleBar.SetVisible(!isActive)
456- if parent != nil {480+ log.Debug().
457- parent.SetVisible(!isActive)481+ Int("pane_index", i).
458- log.Debug().482+ Str("title", pane.title).
459- Int("pane_index", i).483+ Bool("is_active", isActive).
460- Str("title", pane.title).484+ Bool("titlebar_visible", !isActive).
461- Bool("is_active", isActive).485+ Msg("StackedView: set titlebar visibility")
462- Bool("titlebar_visible", !isActive).
463- Msg("StackedView: set titlebar visibility")
464- }
465 }486 }
466 487
467 // Container is visible only for active pane488 // Container is visible only for active pane
M internal/ui/layout/stacked_test.go +81 -106
@@ -25,20 +25,23 @@ func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWid
2525 }
2626
2727 // setupPaneMocks creates mocks needed for AddPane
28+// Note: The stacked view now uses GestureClick on the titleBar directly instead of wrapping it in a button.
29+// The GestureClick is added via AddController which we mock to accept any EventController.
2830 func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) (
29- *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockButtonWidget, *mocks.MockWidget,
31+ *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockWidget,
3032 ) {
3133 mockTitleBar := mocks.NewMockBoxWidget(t)
3234 mockFavicon := mocks.NewMockImageWidget(t)
3335 mockLabel := mocks.NewMockLabelWidget(t)
34- mockButton := mocks.NewMockButtonWidget(t)
3536 mockCloseButton := mocks.NewMockButtonWidget(t)
3637 mockContainer := mocks.NewMockWidget(t)
3738
38- // Title bar creation
39+ // Title bar creation - now directly used without button wrapper
3940 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
4041 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
42+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
4143 mockTitleBar.EXPECT().SetVexpand(false).Once()
44+ mockTitleBar.EXPECT().SetHexpand(true).Once()
4245
4346 // Favicon
4447 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
@@ -63,27 +66,20 @@ func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox
6366 mockCloseButton.EXPECT().SetHexpand(false).Once()
6467 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
6568
66- // Button wrapping title bar
67- mockFactory.EXPECT().NewButton().Return(mockButton).Once()
68- mockButton.EXPECT().SetChild(mockTitleBar).Once()
69- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
70- mockButton.EXPECT().SetFocusOnClick(false).Once()
71- mockButton.EXPECT().SetVexpand(false).Once()
72- mockButton.EXPECT().SetHexpand(true).Once()
69+ // GestureClick is added to titleBar via AddController
70+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
7371
74- // Click handlers (title bar activation and close button)
75- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
72+ // Close button click handler
7673 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
7774
7875 // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
79- mockButton.EXPECT().GtkWidget().Return(nil).Maybe()
8076 mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
8177
82- // Adding to main box
83- mockBox.EXPECT().Append(mockButton).Once()
78+ // Adding to main box - now titleBar is added directly (no button wrapper)
79+ mockBox.EXPECT().Append(mockTitleBar).Once()
8480 mockBox.EXPECT().Append(mockContainer).Once()
8581
86- return mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer
82+ return mockTitleBar, mockFavicon, mockLabel, mockContainer
8783 }
8884
8985 func TestNewStackedView_EmptyStack(t *testing.T) {
@@ -103,13 +99,12 @@ func TestAddPane_SinglePane_BecomesActive(t *testing.T) {
10399 // Arrange
104100 ctx := context.Background()
105101 mockFactory, mockBox := setupMockFactory(t)
106- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
102+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
107103 _ = mockFavicon
108104 _ = mockLabel
109- _ = mockButton
110105
111- // Visibility updates for active pane
112- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
106+ // Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
107+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
113108 mockContainer.EXPECT().SetVisible(true).Once()
114109 mockTitleBar.EXPECT().AddCssClass("active").Once()
115110
@@ -130,26 +125,24 @@ func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) {
130125 mockFactory, mockBox := setupMockFactory(t)
131126
132127 // First pane
133- mockTitleBar1, mockFavicon1, mockLabel1, mockButton1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
128+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
134129 _ = mockFavicon1
135130 _ = mockLabel1
136- _ = mockButton1
137- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
131+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
138132 mockContainer1.EXPECT().SetVisible(true).Once()
139133 mockTitleBar1.EXPECT().AddCssClass("active").Once()
140134
141135 // Second pane - first pane becomes inactive
142- mockTitleBar2, mockFavicon2, mockLabel2, mockButton2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)
136+ mockTitleBar2, mockFavicon2, mockLabel2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)
143137 _ = mockFavicon2
144138 _ = mockLabel2
145- _ = mockButton2
146139
147140 // When second pane is added, update visibility for both
148- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
141+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
149142 mockContainer1.EXPECT().SetVisible(false).Once()
150143 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
151144
152- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()
145+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
153146 mockContainer2.EXPECT().SetVisible(true).Once()
154147 mockTitleBar2.EXPECT().AddCssClass("active").Once()
155148
@@ -173,22 +166,17 @@ func TestRemovePane_MiddlePane(t *testing.T) {
173166 // Setup 3 panes (simplified - just track the key behaviors)
174167 containers := make([]*mocks.MockWidget, 3)
175168 titleBars := make([]*mocks.MockBoxWidget, 3)
176- buttons := make([]*mocks.MockButtonWidget, 3)
177169
178170 for i := 0; i < 3; i++ {
179- var btn *mocks.MockButtonWidget
180- titleBars[i], _, _, btn, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
181- buttons[i] = btn
171+ titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
182172 }
183173
184174 // Allow any visibility calls during setup and removal
185175 for i := 0; i < 3; i++ {
186- titleBars[i].EXPECT().GetParent().Return(buttons[i]).Maybe()
187176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
188178 titleBars[i].EXPECT().AddCssClass("active").Maybe()
189179 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
190- // Button visibility changes when panes are activated/deactivated
191- buttons[i].EXPECT().SetVisible(mock.Anything).Maybe()
192180 }
193181
194182 sv := layout.NewStackedView(mockFactory)
@@ -197,8 +185,8 @@ func TestRemovePane_MiddlePane(t *testing.T) {
197185 sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2])
198186
199187 // Remove middle pane (index 1)
200- // The parent of titleBar is the button widget
201- mockBox.EXPECT().Remove(buttons[1]).Once()
188+ // The titleBar is now directly in the box (no button wrapper)
189+ mockBox.EXPECT().Remove(titleBars[1]).Once()
202190 mockBox.EXPECT().Remove(containers[1]).Once()
203191
204192 // Act
@@ -213,12 +201,11 @@ func TestRemovePane_LastPane_ReturnsError(t *testing.T) {
213201 // Arrange
214202 ctx := context.Background()
215203 mockFactory, mockBox := setupMockFactory(t)
216- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
204+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
217205 _ = mockFavicon
218206 _ = mockLabel
219- _ = mockButton
220207
221- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
208+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
222209 mockContainer.EXPECT().SetVisible(true).Once()
223210 mockTitleBar.EXPECT().AddCssClass("active").Once()
224211
@@ -250,12 +237,11 @@ func TestRemovePane_IndexOutOfBounds(t *testing.T) {
250237 // Arrange
251238 ctx := context.Background()
252239 mockFactory, mockBox := setupMockFactory(t)
253- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
240+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
254241 _ = mockFavicon
255242 _ = mockLabel
256- _ = mockButton
257243
258- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
244+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
259245 mockContainer.EXPECT().SetVisible(true).Once()
260246 mockTitleBar.EXPECT().AddCssClass("active").Once()
261247
@@ -278,15 +264,14 @@ func TestSetActive_ValidIndex(t *testing.T) {
278264 titleBars := make([]*mocks.MockBoxWidget, 2)
279265
280266 for i := 0; i < 2; i++ {
281- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)
267+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
282268 _ = mockFavicon
283- _ = mockOverlay
284- _ = mockSpinner
269+ _ = mockLabel
285270
286271 titleBars[i] = titleBar
287272 containers[i] = container
288273
289- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()
274+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
290275 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
291276 titleBars[i].EXPECT().AddCssClass("active").Maybe()
292277 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -308,12 +293,11 @@ func TestSetActive_OutOfBounds(t *testing.T) {
308293 // Arrange
309294 ctx := context.Background()
310295 mockFactory, mockBox := setupMockFactory(t)
311- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
296+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
312297 _ = mockFavicon
313298 _ = mockLabel
314- _ = mockButton
315299
316- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
300+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
317301 mockContainer.EXPECT().SetVisible(true).Once()
318302 mockTitleBar.EXPECT().AddCssClass("active").Once()
319303
@@ -344,9 +328,9 @@ func TestUpdateTitle(t *testing.T) {
344328 // Arrange
345329 ctx := context.Background()
346330 mockFactory, mockBox := setupMockFactory(t)
347- mockTitleBar, _, mockLabel, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
331+ mockTitleBar, _, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
348332
349- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
333+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
350334 mockContainer.EXPECT().SetVisible(true).Once()
351335 mockTitleBar.EXPECT().AddCssClass("active").Once()
352336
@@ -378,9 +362,9 @@ func TestUpdateFavicon(t *testing.T) {
378362 // Arrange
379363 ctx := context.Background()
380364 mockFactory, mockBox := setupMockFactory(t)
381- mockTitleBar, mockFavicon, _, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
365+ mockTitleBar, mockFavicon, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
382366
383- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
367+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
384368 mockContainer.EXPECT().SetVisible(true).Once()
385369 mockTitleBar.EXPECT().AddCssClass("active").Once()
386370
@@ -400,12 +384,11 @@ func TestGetContainer(t *testing.T) {
400384 // Arrange
401385 ctx := context.Background()
402386 mockFactory, mockBox := setupMockFactory(t)
403- mockTitleBar, mockFavicon, mockOverlay, mockSpinner, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
387+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
404388 _ = mockFavicon
405- _ = mockOverlay
406- _ = mockSpinner
389+ _ = mockLabel
407390
408- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
391+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
409392 mockContainer.EXPECT().SetVisible(true).Once()
410393 mockTitleBar.EXPECT().AddCssClass("active").Once()
411394
@@ -454,15 +437,14 @@ func TestNavigateNext_WrapsAround(t *testing.T) {
454437 titleBars := make([]*mocks.MockBoxWidget, 2)
455438
456439 for i := 0; i < 2; i++ {
457- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)
440+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
458441 _ = mockFavicon
459- _ = mockOverlay
460- _ = mockSpinner
442+ _ = mockLabel
461443
462444 titleBars[i] = titleBar
463445 containers[i] = container
464446
465- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()
447+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
466448 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
467449 titleBars[i].EXPECT().AddCssClass("active").Maybe()
468450 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -492,15 +474,14 @@ func TestNavigatePrevious_WrapsAround(t *testing.T) {
492474 titleBars := make([]*mocks.MockBoxWidget, 2)
493475
494476 for i := 0; i < 2; i++ {
495- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)
477+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
496478 _ = mockFavicon
497- _ = mockOverlay
498- _ = mockSpinner
479+ _ = mockLabel
499480
500481 titleBars[i] = titleBar
501482 containers[i] = container
502483
503- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()
484+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
504485 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
505486 titleBars[i].EXPECT().AddCssClass("active").Maybe()
506487 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -575,14 +556,15 @@ func setupInsertPaneMocks(
575556 mockTitleBar := mocks.NewMockBoxWidget(t)
576557 mockFavicon := mocks.NewMockImageWidget(t)
577558 mockLabel := mocks.NewMockLabelWidget(t)
578- mockButton := mocks.NewMockButtonWidget(t)
579559 mockCloseButton := mocks.NewMockButtonWidget(t)
580560 mockContainer := mocks.NewMockWidget(t)
581561
582- // Title bar creation
562+ // Title bar creation - now directly used without button wrapper
583563 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
584564 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
565+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
585566 mockTitleBar.EXPECT().SetVexpand(false).Once()
567+ mockTitleBar.EXPECT().SetHexpand(true).Once()
586568
587569 // Favicon
588570 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
@@ -607,26 +589,23 @@ func setupInsertPaneMocks(
607589 mockCloseButton.EXPECT().SetHexpand(false).Once()
608590 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
609591
610- // Button wrapping title bar
611- mockFactory.EXPECT().NewButton().Return(mockButton).Once()
612- mockButton.EXPECT().SetChild(mockTitleBar).Once()
613- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
614- mockButton.EXPECT().SetFocusOnClick(false).Once()
615- mockButton.EXPECT().SetVexpand(false).Once()
616- mockButton.EXPECT().SetHexpand(true).Once()
592+ // GestureClick is added to titleBar via AddController
593+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
617594
618- // Click handlers (title bar activation and close button)
619- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
595+ // Close button click handler
620596 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
621597
622- // Position-aware insertion using InsertChildAfter
598+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
599+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
600+
601+ // Position-aware insertion using InsertChildAfter - now using titleBar directly
623602 if siblingContainer != nil {
624- mockBox.EXPECT().InsertChildAfter(mockButton, siblingContainer).Once()
625- mockBox.EXPECT().InsertChildAfter(mockContainer, mockButton).Once()
603+ mockBox.EXPECT().InsertChildAfter(mockTitleBar, siblingContainer).Once()
604+ mockBox.EXPECT().InsertChildAfter(mockContainer, mockTitleBar).Once()
626605 } else {
627606 // Insert at beginning using Prepend
628607 mockBox.EXPECT().Prepend(mockContainer).Once()
629- mockBox.EXPECT().Prepend(mockButton).Once()
608+ mockBox.EXPECT().Prepend(mockTitleBar).Once()
630609 }
631610
632611 return mockTitleBar, mockContainer
@@ -638,11 +617,10 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) {
638617 mockFactory, mockBox := setupMockFactory(t)
639618
640619 // First pane - appended normally
641- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
620+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
642621 _ = mockFavicon1
643- _ = mockOverlay1
644- _ = mockSpinner1
645- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
622+ _ = mockLabel1
623+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
646624 mockContainer1.EXPECT().SetVisible(true).Once()
647625 mockTitleBar1.EXPECT().AddCssClass("active").Once()
648626
@@ -653,11 +631,11 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) {
653631 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil)
654632
655633 // Visibility updates
656- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
634+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
657635 mockContainer1.EXPECT().SetVisible(false).Once()
658636 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
659637
660- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()
638+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
661639 mockContainer2.EXPECT().SetVisible(true).Once()
662640 mockTitleBar2.EXPECT().AddCssClass("active").Once()
663641
@@ -680,8 +658,8 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
680658 titleBars := make([]*mocks.MockBoxWidget, 2)
681659
682660 for i := 0; i < 2; i++ {
683- titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
684- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()
661+ titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
662+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
685663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
686664 titleBars[i].EXPECT().AddCssClass("active").Maybe()
687665 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -696,7 +674,7 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
696674
697675 // Insert new pane after index 0 (should become index 1)
698676 mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0])
699- mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe()
677+ mockTitleBar3.EXPECT().SetVisible(mock.Anything).Maybe()
700678 mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe()
701679 mockTitleBar3.EXPECT().AddCssClass("active").Maybe()
702680 mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe()
@@ -716,11 +694,10 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) {
716694 mockFactory, mockBox := setupMockFactory(t)
717695
718696 // First pane
719- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
697+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
720698 _ = mockFavicon1
721- _ = mockOverlay1
722- _ = mockSpinner1
723- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
699+ _ = mockLabel1
700+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
724701 mockContainer1.EXPECT().SetVisible(true).Once()
725702 mockTitleBar1.EXPECT().AddCssClass("active").Once()
726703
@@ -730,11 +707,11 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) {
730707 // Insert after last pane (afterIndex=0, becomes index 1)
731708 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
732709
733- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
710+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
734711 mockContainer1.EXPECT().SetVisible(false).Once()
735712 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
736713
737- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()
714+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
738715 mockContainer2.EXPECT().SetVisible(true).Once()
739716 mockTitleBar2.EXPECT().AddCssClass("active").Once()
740717
@@ -757,15 +734,14 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
757734 titleBars := make([]*mocks.MockBoxWidget, 3)
758735
759736 for i := 0; i < 3; i++ {
760- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)
737+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
761738 _ = mockFavicon
762- _ = mockOverlay
763- _ = mockSpinner
739+ _ = mockLabel
764740
765741 titleBars[i] = titleBar
766742 containers[i] = container
767743
768- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()
744+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
769745 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
770746 titleBars[i].EXPECT().AddCssClass("active").Maybe()
771747 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -781,7 +757,7 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
781757
782758 // Insert D after B (should be at index 2, C moves to index 3)
783759 mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1])
784- mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe()
760+ mockTitleBar4.EXPECT().SetVisible(mock.Anything).Maybe()
785761 mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe()
786762 mockTitleBar4.EXPECT().AddCssClass("active").Maybe()
787763 mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe()
@@ -812,11 +788,10 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
812788 mockFactory, mockBox := setupMockFactory(t)
813789
814790 // First pane
815- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
791+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
816792 _ = mockFavicon1
817- _ = mockOverlay1
818- _ = mockSpinner1
819- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
793+ _ = mockLabel1
794+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
820795 mockContainer1.EXPECT().SetVisible(true).Once()
821796 mockTitleBar1.EXPECT().AddCssClass("active").Once()
822797
@@ -826,11 +801,11 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
826801 // Try to insert at invalid index (100) - should clamp to end
827802 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
828803
829- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
804+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
830805 mockContainer1.EXPECT().SetVisible(false).Once()
831806 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
832807
833- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()
808+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
834809 mockContainer2.EXPECT().SetVisible(true).Once()
835810 mockTitleBar2.EXPECT().AddCssClass("active").Once()
836811
@@ -25,20 +25,23 @@ func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWid
25 }25 }
26 26
27 // setupPaneMocks creates mocks needed for AddPane27 // setupPaneMocks creates mocks needed for AddPane
28+// Note: The stacked view now uses GestureClick on the titleBar directly instead of wrapping it in a button.
29+// The GestureClick is added via AddController which we mock to accept any EventController.
28 func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) (30 func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox *mocks.MockBoxWidget) (
29- *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockButtonWidget, *mocks.MockWidget,31+ *mocks.MockBoxWidget, *mocks.MockImageWidget, *mocks.MockLabelWidget, *mocks.MockWidget,
30 ) {32 ) {
31 mockTitleBar := mocks.NewMockBoxWidget(t)33 mockTitleBar := mocks.NewMockBoxWidget(t)
32 mockFavicon := mocks.NewMockImageWidget(t)34 mockFavicon := mocks.NewMockImageWidget(t)
33 mockLabel := mocks.NewMockLabelWidget(t)35 mockLabel := mocks.NewMockLabelWidget(t)
34- mockButton := mocks.NewMockButtonWidget(t)
35 mockCloseButton := mocks.NewMockButtonWidget(t)36 mockCloseButton := mocks.NewMockButtonWidget(t)
36 mockContainer := mocks.NewMockWidget(t)37 mockContainer := mocks.NewMockWidget(t)
37 38
38- // Title bar creation39+ // Title bar creation - now directly used without button wrapper
39 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()40 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
40 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()41 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
42+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
41 mockTitleBar.EXPECT().SetVexpand(false).Once()43 mockTitleBar.EXPECT().SetVexpand(false).Once()
44+ mockTitleBar.EXPECT().SetHexpand(true).Once()
42 45
43 // Favicon46 // Favicon
44 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()47 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
@@ -63,27 +66,20 @@ func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox
63 mockCloseButton.EXPECT().SetHexpand(false).Once()66 mockCloseButton.EXPECT().SetHexpand(false).Once()
64 mockTitleBar.EXPECT().Append(mockCloseButton).Once()67 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
65 68
66- // Button wrapping title bar69+ // GestureClick is added to titleBar via AddController
67- mockFactory.EXPECT().NewButton().Return(mockButton).Once()70+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
68- mockButton.EXPECT().SetChild(mockTitleBar).Once()
69- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
70- mockButton.EXPECT().SetFocusOnClick(false).Once()
71- mockButton.EXPECT().SetVexpand(false).Once()
72- mockButton.EXPECT().SetHexpand(true).Once()
73 71
74- // Click handlers (title bar activation and close button)72+ // Close button click handler
75- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
76 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()73 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
77 74
78 // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests75 // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
79- mockButton.EXPECT().GtkWidget().Return(nil).Maybe()
80 mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()76 mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
81 77
82- // Adding to main box78+ // Adding to main box - now titleBar is added directly (no button wrapper)
83- mockBox.EXPECT().Append(mockButton).Once()79+ mockBox.EXPECT().Append(mockTitleBar).Once()
84 mockBox.EXPECT().Append(mockContainer).Once()80 mockBox.EXPECT().Append(mockContainer).Once()
85 81
86- return mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer82+ return mockTitleBar, mockFavicon, mockLabel, mockContainer
87 }83 }
88 84
89 func TestNewStackedView_EmptyStack(t *testing.T) {85 func TestNewStackedView_EmptyStack(t *testing.T) {
@@ -103,13 +99,12 @@ func TestAddPane_SinglePane_BecomesActive(t *testing.T) {
103 // Arrange99 // Arrange
104 ctx := context.Background()100 ctx := context.Background()
105 mockFactory, mockBox := setupMockFactory(t)101 mockFactory, mockBox := setupMockFactory(t)
106- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)102+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
107 _ = mockFavicon103 _ = mockFavicon
108 _ = mockLabel104 _ = mockLabel
109- _ = mockButton
110 105
111- // Visibility updates for active pane106+ // Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
112- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()107+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
113 mockContainer.EXPECT().SetVisible(true).Once()108 mockContainer.EXPECT().SetVisible(true).Once()
114 mockTitleBar.EXPECT().AddCssClass("active").Once()109 mockTitleBar.EXPECT().AddCssClass("active").Once()
115 110
@@ -130,26 +125,24 @@ func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) {
130 mockFactory, mockBox := setupMockFactory(t)125 mockFactory, mockBox := setupMockFactory(t)
131 126
132 // First pane127 // First pane
133- mockTitleBar1, mockFavicon1, mockLabel1, mockButton1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)128+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
134 _ = mockFavicon1129 _ = mockFavicon1
135 _ = mockLabel1130 _ = mockLabel1
136- _ = mockButton1131+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
137- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
138 mockContainer1.EXPECT().SetVisible(true).Once()132 mockContainer1.EXPECT().SetVisible(true).Once()
139 mockTitleBar1.EXPECT().AddCssClass("active").Once()133 mockTitleBar1.EXPECT().AddCssClass("active").Once()
140 134
141 // Second pane - first pane becomes inactive135 // Second pane - first pane becomes inactive
142- mockTitleBar2, mockFavicon2, mockLabel2, mockButton2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)136+ mockTitleBar2, mockFavicon2, mockLabel2, mockContainer2 := setupPaneMocks(t, mockFactory, mockBox)
143 _ = mockFavicon2137 _ = mockFavicon2
144 _ = mockLabel2138 _ = mockLabel2
145- _ = mockButton2
146 139
147 // When second pane is added, update visibility for both140 // When second pane is added, update visibility for both
148- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()141+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
149 mockContainer1.EXPECT().SetVisible(false).Once()142 mockContainer1.EXPECT().SetVisible(false).Once()
150 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()143 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
151 144
152- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()145+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
153 mockContainer2.EXPECT().SetVisible(true).Once()146 mockContainer2.EXPECT().SetVisible(true).Once()
154 mockTitleBar2.EXPECT().AddCssClass("active").Once()147 mockTitleBar2.EXPECT().AddCssClass("active").Once()
155 148
@@ -173,22 +166,17 @@ func TestRemovePane_MiddlePane(t *testing.T) {
173 // Setup 3 panes (simplified - just track the key behaviors)166 // Setup 3 panes (simplified - just track the key behaviors)
174 containers := make([]*mocks.MockWidget, 3)167 containers := make([]*mocks.MockWidget, 3)
175 titleBars := make([]*mocks.MockBoxWidget, 3)168 titleBars := make([]*mocks.MockBoxWidget, 3)
176- buttons := make([]*mocks.MockButtonWidget, 3)
177 169
178 for i := 0; i < 3; i++ {170 for i := 0; i < 3; i++ {
179- var btn *mocks.MockButtonWidget171+ titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
180- titleBars[i], _, _, btn, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
181- buttons[i] = btn
182 }172 }
183 173
184 // Allow any visibility calls during setup and removal174 // Allow any visibility calls during setup and removal
185 for i := 0; i < 3; i++ {175 for i := 0; i < 3; i++ {
186- titleBars[i].EXPECT().GetParent().Return(buttons[i]).Maybe()
187 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
188 titleBars[i].EXPECT().AddCssClass("active").Maybe()178 titleBars[i].EXPECT().AddCssClass("active").Maybe()
189 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()179 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
190- // Button visibility changes when panes are activated/deactivated
191- buttons[i].EXPECT().SetVisible(mock.Anything).Maybe()
192 }180 }
193 181
194 sv := layout.NewStackedView(mockFactory)182 sv := layout.NewStackedView(mockFactory)
@@ -197,8 +185,8 @@ func TestRemovePane_MiddlePane(t *testing.T) {
197 sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2])185 sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2])
198 186
199 // Remove middle pane (index 1)187 // Remove middle pane (index 1)
200- // The parent of titleBar is the button widget188+ // The titleBar is now directly in the box (no button wrapper)
201- mockBox.EXPECT().Remove(buttons[1]).Once()189+ mockBox.EXPECT().Remove(titleBars[1]).Once()
202 mockBox.EXPECT().Remove(containers[1]).Once()190 mockBox.EXPECT().Remove(containers[1]).Once()
203 191
204 // Act192 // Act
@@ -213,12 +201,11 @@ func TestRemovePane_LastPane_ReturnsError(t *testing.T) {
213 // Arrange201 // Arrange
214 ctx := context.Background()202 ctx := context.Background()
215 mockFactory, mockBox := setupMockFactory(t)203 mockFactory, mockBox := setupMockFactory(t)
216- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)204+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
217 _ = mockFavicon205 _ = mockFavicon
218 _ = mockLabel206 _ = mockLabel
219- _ = mockButton
220 207
221- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()208+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
222 mockContainer.EXPECT().SetVisible(true).Once()209 mockContainer.EXPECT().SetVisible(true).Once()
223 mockTitleBar.EXPECT().AddCssClass("active").Once()210 mockTitleBar.EXPECT().AddCssClass("active").Once()
224 211
@@ -250,12 +237,11 @@ func TestRemovePane_IndexOutOfBounds(t *testing.T) {
250 // Arrange237 // Arrange
251 ctx := context.Background()238 ctx := context.Background()
252 mockFactory, mockBox := setupMockFactory(t)239 mockFactory, mockBox := setupMockFactory(t)
253- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)240+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
254 _ = mockFavicon241 _ = mockFavicon
255 _ = mockLabel242 _ = mockLabel
256- _ = mockButton
257 243
258- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()244+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
259 mockContainer.EXPECT().SetVisible(true).Once()245 mockContainer.EXPECT().SetVisible(true).Once()
260 mockTitleBar.EXPECT().AddCssClass("active").Once()246 mockTitleBar.EXPECT().AddCssClass("active").Once()
261 247
@@ -278,15 +264,14 @@ func TestSetActive_ValidIndex(t *testing.T) {
278 titleBars := make([]*mocks.MockBoxWidget, 2)264 titleBars := make([]*mocks.MockBoxWidget, 2)
279 265
280 for i := 0; i < 2; i++ {266 for i := 0; i < 2; i++ {
281- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)267+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
282 _ = mockFavicon268 _ = mockFavicon
283- _ = mockOverlay269+ _ = mockLabel
284- _ = mockSpinner
285 270
286 titleBars[i] = titleBar271 titleBars[i] = titleBar
287 containers[i] = container272 containers[i] = container
288 273
289- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()274+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
290 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()275 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
291 titleBars[i].EXPECT().AddCssClass("active").Maybe()276 titleBars[i].EXPECT().AddCssClass("active").Maybe()
292 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()277 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -308,12 +293,11 @@ func TestSetActive_OutOfBounds(t *testing.T) {
308 // Arrange293 // Arrange
309 ctx := context.Background()294 ctx := context.Background()
310 mockFactory, mockBox := setupMockFactory(t)295 mockFactory, mockBox := setupMockFactory(t)
311- mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer := setupPaneMocks(t, mockFactory, mockBox)296+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
312 _ = mockFavicon297 _ = mockFavicon
313 _ = mockLabel298 _ = mockLabel
314- _ = mockButton
315 299
316- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()300+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
317 mockContainer.EXPECT().SetVisible(true).Once()301 mockContainer.EXPECT().SetVisible(true).Once()
318 mockTitleBar.EXPECT().AddCssClass("active").Once()302 mockTitleBar.EXPECT().AddCssClass("active").Once()
319 303
@@ -344,9 +328,9 @@ func TestUpdateTitle(t *testing.T) {
344 // Arrange328 // Arrange
345 ctx := context.Background()329 ctx := context.Background()
346 mockFactory, mockBox := setupMockFactory(t)330 mockFactory, mockBox := setupMockFactory(t)
347- mockTitleBar, _, mockLabel, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)331+ mockTitleBar, _, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
348 332
349- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()333+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
350 mockContainer.EXPECT().SetVisible(true).Once()334 mockContainer.EXPECT().SetVisible(true).Once()
351 mockTitleBar.EXPECT().AddCssClass("active").Once()335 mockTitleBar.EXPECT().AddCssClass("active").Once()
352 336
@@ -378,9 +362,9 @@ func TestUpdateFavicon(t *testing.T) {
378 // Arrange362 // Arrange
379 ctx := context.Background()363 ctx := context.Background()
380 mockFactory, mockBox := setupMockFactory(t)364 mockFactory, mockBox := setupMockFactory(t)
381- mockTitleBar, mockFavicon, _, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)365+ mockTitleBar, mockFavicon, _, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
382 366
383- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()367+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
384 mockContainer.EXPECT().SetVisible(true).Once()368 mockContainer.EXPECT().SetVisible(true).Once()
385 mockTitleBar.EXPECT().AddCssClass("active").Once()369 mockTitleBar.EXPECT().AddCssClass("active").Once()
386 370
@@ -400,12 +384,11 @@ func TestGetContainer(t *testing.T) {
400 // Arrange384 // Arrange
401 ctx := context.Background()385 ctx := context.Background()
402 mockFactory, mockBox := setupMockFactory(t)386 mockFactory, mockBox := setupMockFactory(t)
403- mockTitleBar, mockFavicon, mockOverlay, mockSpinner, mockContainer := setupPaneMocks(t, mockFactory, mockBox)387+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockBox)
404 _ = mockFavicon388 _ = mockFavicon
405- _ = mockOverlay389+ _ = mockLabel
406- _ = mockSpinner
407 390
408- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()391+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
409 mockContainer.EXPECT().SetVisible(true).Once()392 mockContainer.EXPECT().SetVisible(true).Once()
410 mockTitleBar.EXPECT().AddCssClass("active").Once()393 mockTitleBar.EXPECT().AddCssClass("active").Once()
411 394
@@ -454,15 +437,14 @@ func TestNavigateNext_WrapsAround(t *testing.T) {
454 titleBars := make([]*mocks.MockBoxWidget, 2)437 titleBars := make([]*mocks.MockBoxWidget, 2)
455 438
456 for i := 0; i < 2; i++ {439 for i := 0; i < 2; i++ {
457- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)440+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
458 _ = mockFavicon441 _ = mockFavicon
459- _ = mockOverlay442+ _ = mockLabel
460- _ = mockSpinner
461 443
462 titleBars[i] = titleBar444 titleBars[i] = titleBar
463 containers[i] = container445 containers[i] = container
464 446
465- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()447+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
466 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()448 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
467 titleBars[i].EXPECT().AddCssClass("active").Maybe()449 titleBars[i].EXPECT().AddCssClass("active").Maybe()
468 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()450 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -492,15 +474,14 @@ func TestNavigatePrevious_WrapsAround(t *testing.T) {
492 titleBars := make([]*mocks.MockBoxWidget, 2)474 titleBars := make([]*mocks.MockBoxWidget, 2)
493 475
494 for i := 0; i < 2; i++ {476 for i := 0; i < 2; i++ {
495- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)477+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
496 _ = mockFavicon478 _ = mockFavicon
497- _ = mockOverlay479+ _ = mockLabel
498- _ = mockSpinner
499 480
500 titleBars[i] = titleBar481 titleBars[i] = titleBar
501 containers[i] = container482 containers[i] = container
502 483
503- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()484+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
504 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()485 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
505 titleBars[i].EXPECT().AddCssClass("active").Maybe()486 titleBars[i].EXPECT().AddCssClass("active").Maybe()
506 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()487 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -575,14 +556,15 @@ func setupInsertPaneMocks(
575 mockTitleBar := mocks.NewMockBoxWidget(t)556 mockTitleBar := mocks.NewMockBoxWidget(t)
576 mockFavicon := mocks.NewMockImageWidget(t)557 mockFavicon := mocks.NewMockImageWidget(t)
577 mockLabel := mocks.NewMockLabelWidget(t)558 mockLabel := mocks.NewMockLabelWidget(t)
578- mockButton := mocks.NewMockButtonWidget(t)
579 mockCloseButton := mocks.NewMockButtonWidget(t)559 mockCloseButton := mocks.NewMockButtonWidget(t)
580 mockContainer := mocks.NewMockWidget(t)560 mockContainer := mocks.NewMockWidget(t)
581 561
582- // Title bar creation562+ // Title bar creation - now directly used without button wrapper
583 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()563 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
584 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()564 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
565+ mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
585 mockTitleBar.EXPECT().SetVexpand(false).Once()566 mockTitleBar.EXPECT().SetVexpand(false).Once()
567+ mockTitleBar.EXPECT().SetHexpand(true).Once()
586 568
587 // Favicon569 // Favicon
588 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()570 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
@@ -607,26 +589,23 @@ func setupInsertPaneMocks(
607 mockCloseButton.EXPECT().SetHexpand(false).Once()589 mockCloseButton.EXPECT().SetHexpand(false).Once()
608 mockTitleBar.EXPECT().Append(mockCloseButton).Once()590 mockTitleBar.EXPECT().Append(mockCloseButton).Once()
609 591
610- // Button wrapping title bar592+ // GestureClick is added to titleBar via AddController
611- mockFactory.EXPECT().NewButton().Return(mockButton).Once()593+ mockTitleBar.EXPECT().AddController(mock.Anything).Once()
612- mockButton.EXPECT().SetChild(mockTitleBar).Once()
613- mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
614- mockButton.EXPECT().SetFocusOnClick(false).Once()
615- mockButton.EXPECT().SetVexpand(false).Once()
616- mockButton.EXPECT().SetHexpand(true).Once()
617 594
618- // Click handlers (title bar activation and close button)595+ // Close button click handler
619- mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
620 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()596 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
621 597
622- // Position-aware insertion using InsertChildAfter598+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
599+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
600+
601+ // Position-aware insertion using InsertChildAfter - now using titleBar directly
623 if siblingContainer != nil {602 if siblingContainer != nil {
624- mockBox.EXPECT().InsertChildAfter(mockButton, siblingContainer).Once()603+ mockBox.EXPECT().InsertChildAfter(mockTitleBar, siblingContainer).Once()
625- mockBox.EXPECT().InsertChildAfter(mockContainer, mockButton).Once()604+ mockBox.EXPECT().InsertChildAfter(mockContainer, mockTitleBar).Once()
626 } else {605 } else {
627 // Insert at beginning using Prepend606 // Insert at beginning using Prepend
628 mockBox.EXPECT().Prepend(mockContainer).Once()607 mockBox.EXPECT().Prepend(mockContainer).Once()
629- mockBox.EXPECT().Prepend(mockButton).Once()608+ mockBox.EXPECT().Prepend(mockTitleBar).Once()
630 }609 }
631 610
632 return mockTitleBar, mockContainer611 return mockTitleBar, mockContainer
@@ -638,11 +617,10 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) {
638 mockFactory, mockBox := setupMockFactory(t)617 mockFactory, mockBox := setupMockFactory(t)
639 618
640 // First pane - appended normally619 // First pane - appended normally
641- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)620+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
642 _ = mockFavicon1621 _ = mockFavicon1
643- _ = mockOverlay1622+ _ = mockLabel1
644- _ = mockSpinner1623+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
645- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
646 mockContainer1.EXPECT().SetVisible(true).Once()624 mockContainer1.EXPECT().SetVisible(true).Once()
647 mockTitleBar1.EXPECT().AddCssClass("active").Once()625 mockTitleBar1.EXPECT().AddCssClass("active").Once()
648 626
@@ -653,11 +631,11 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) {
653 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil)631 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil)
654 632
655 // Visibility updates633 // Visibility updates
656- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()634+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
657 mockContainer1.EXPECT().SetVisible(false).Once()635 mockContainer1.EXPECT().SetVisible(false).Once()
658 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()636 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
659 637
660- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()638+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
661 mockContainer2.EXPECT().SetVisible(true).Once()639 mockContainer2.EXPECT().SetVisible(true).Once()
662 mockTitleBar2.EXPECT().AddCssClass("active").Once()640 mockTitleBar2.EXPECT().AddCssClass("active").Once()
663 641
@@ -680,8 +658,8 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
680 titleBars := make([]*mocks.MockBoxWidget, 2)658 titleBars := make([]*mocks.MockBoxWidget, 2)
681 659
682 for i := 0; i < 2; i++ {660 for i := 0; i < 2; i++ {
683- titleBars[i], _, _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)661+ titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
684- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()662+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
685 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
686 titleBars[i].EXPECT().AddCssClass("active").Maybe()664 titleBars[i].EXPECT().AddCssClass("active").Maybe()
687 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()665 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -696,7 +674,7 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
696 674
697 // Insert new pane after index 0 (should become index 1)675 // Insert new pane after index 0 (should become index 1)
698 mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0])676 mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0])
699- mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe()677+ mockTitleBar3.EXPECT().SetVisible(mock.Anything).Maybe()
700 mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe()678 mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe()
701 mockTitleBar3.EXPECT().AddCssClass("active").Maybe()679 mockTitleBar3.EXPECT().AddCssClass("active").Maybe()
702 mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe()680 mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe()
@@ -716,11 +694,10 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) {
716 mockFactory, mockBox := setupMockFactory(t)694 mockFactory, mockBox := setupMockFactory(t)
717 695
718 // First pane696 // First pane
719- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)697+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
720 _ = mockFavicon1698 _ = mockFavicon1
721- _ = mockOverlay1699+ _ = mockLabel1
722- _ = mockSpinner1700+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
723- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
724 mockContainer1.EXPECT().SetVisible(true).Once()701 mockContainer1.EXPECT().SetVisible(true).Once()
725 mockTitleBar1.EXPECT().AddCssClass("active").Once()702 mockTitleBar1.EXPECT().AddCssClass("active").Once()
726 703
@@ -730,11 +707,11 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) {
730 // Insert after last pane (afterIndex=0, becomes index 1)707 // Insert after last pane (afterIndex=0, becomes index 1)
731 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)708 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
732 709
733- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()710+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
734 mockContainer1.EXPECT().SetVisible(false).Once()711 mockContainer1.EXPECT().SetVisible(false).Once()
735 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()712 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
736 713
737- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()714+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
738 mockContainer2.EXPECT().SetVisible(true).Once()715 mockContainer2.EXPECT().SetVisible(true).Once()
739 mockTitleBar2.EXPECT().AddCssClass("active").Once()716 mockTitleBar2.EXPECT().AddCssClass("active").Once()
740 717
@@ -757,15 +734,14 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
757 titleBars := make([]*mocks.MockBoxWidget, 3)734 titleBars := make([]*mocks.MockBoxWidget, 3)
758 735
759 for i := 0; i < 3; i++ {736 for i := 0; i < 3; i++ {
760- titleBar, mockFavicon, mockOverlay, mockSpinner, container := setupPaneMocks(t, mockFactory, mockBox)737+ titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
761 _ = mockFavicon738 _ = mockFavicon
762- _ = mockOverlay739+ _ = mockLabel
763- _ = mockSpinner
764 740
765 titleBars[i] = titleBar741 titleBars[i] = titleBar
766 containers[i] = container742 containers[i] = container
767 743
768- titleBars[i].EXPECT().GetParent().Return(nil).Maybe()744+ titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
769 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()745 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
770 titleBars[i].EXPECT().AddCssClass("active").Maybe()746 titleBars[i].EXPECT().AddCssClass("active").Maybe()
771 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()747 titleBars[i].EXPECT().RemoveCssClass("active").Maybe()
@@ -781,7 +757,7 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
781 757
782 // Insert D after B (should be at index 2, C moves to index 3)758 // Insert D after B (should be at index 2, C moves to index 3)
783 mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1])759 mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1])
784- mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe()760+ mockTitleBar4.EXPECT().SetVisible(mock.Anything).Maybe()
785 mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe()761 mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe()
786 mockTitleBar4.EXPECT().AddCssClass("active").Maybe()762 mockTitleBar4.EXPECT().AddCssClass("active").Maybe()
787 mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe()763 mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe()
@@ -812,11 +788,10 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
812 mockFactory, mockBox := setupMockFactory(t)788 mockFactory, mockBox := setupMockFactory(t)
813 789
814 // First pane790 // First pane
815- mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)791+ mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox)
816 _ = mockFavicon1792 _ = mockFavicon1
817- _ = mockOverlay1793+ _ = mockLabel1
818- _ = mockSpinner1794+ mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
819- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()
820 mockContainer1.EXPECT().SetVisible(true).Once()795 mockContainer1.EXPECT().SetVisible(true).Once()
821 mockTitleBar1.EXPECT().AddCssClass("active").Once()796 mockTitleBar1.EXPECT().AddCssClass("active").Once()
822 797
@@ -826,11 +801,11 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) {
826 // Try to insert at invalid index (100) - should clamp to end801 // Try to insert at invalid index (100) - should clamp to end
827 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)802 mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, mockContainer1)
828 803
829- mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe()804+ mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar
830 mockContainer1.EXPECT().SetVisible(false).Once()805 mockContainer1.EXPECT().SetVisible(false).Once()
831 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()806 mockTitleBar1.EXPECT().RemoveCssClass("active").Once()
832 807
833- mockTitleBar2.EXPECT().GetParent().Return(nil).Maybe()808+ mockTitleBar2.EXPECT().SetVisible(false).Once() // Second pane is active, hide its title bar
834 mockContainer2.EXPECT().SetVisible(true).Once()809 mockContainer2.EXPECT().SetVisible(true).Once()
835 mockTitleBar2.EXPECT().AddCssClass("active").Once()810 mockTitleBar2.EXPECT().AddCssClass("active").Once()
836 811
M internal/ui/layout/tree_test.go +3 -4
@@ -19,11 +19,10 @@ func setupStackedLeafMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (
1919 mockStackBox.EXPECT().SetHexpand(true).Once()
2020 mockStackBox.EXPECT().SetVexpand(true).Once()
2121
22- mockTitleBar, mockFavicon, mockOverlay, mockSpinner, mockContainer := setupPaneMocks(t, mockFactory, mockStackBox)
22+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockStackBox)
2323 _ = mockFavicon
24- _ = mockOverlay
25- _ = mockSpinner
26- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
24+ _ = mockLabel
25+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
2726 mockContainer.EXPECT().SetVisible(true).Once()
2827 mockTitleBar.EXPECT().AddCssClass("active").Once()
2928
@@ -19,11 +19,10 @@ func setupStackedLeafMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (
19 mockStackBox.EXPECT().SetHexpand(true).Once()19 mockStackBox.EXPECT().SetHexpand(true).Once()
20 mockStackBox.EXPECT().SetVexpand(true).Once()20 mockStackBox.EXPECT().SetVexpand(true).Once()
21 21
22- mockTitleBar, mockFavicon, mockOverlay, mockSpinner, mockContainer := setupPaneMocks(t, mockFactory, mockStackBox)22+ mockTitleBar, mockFavicon, mockLabel, mockContainer := setupPaneMocks(t, mockFactory, mockStackBox)
23 _ = mockFavicon23 _ = mockFavicon
24- _ = mockOverlay24+ _ = mockLabel
25- _ = mockSpinner25+ mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
26- mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
27 mockContainer.EXPECT().SetVisible(true).Once()26 mockContainer.EXPECT().SetVisible(true).Once()
28 mockTitleBar.EXPECT().AddCssClass("active").Once()27 mockTitleBar.EXPECT().AddCssClass("active").Once()
29 28
M internal/ui/theme/css.go +7 -17
@@ -572,6 +572,7 @@ func generateStackedPaneCSS(p Palette) string {
572572 return `/* ===== Stacked Pane Styling ===== */
573573
574574 /* Title bar for inactive panes in a stack */
575+/* The title bar is now a Box with GestureClick, not wrapped in a button */
575576 .stacked-pane-titlebar {
576577 background-color: var(--surface-variant);
577578 border-bottom: 0.0625em solid var(--border);
@@ -579,27 +580,15 @@ func generateStackedPaneCSS(p Palette) string {
579580 min-height: 1.5em;
580581 }
581582
582-/* Title bar button wrapper - clickable area */
583-button.stacked-pane-title-button {
584- background-color: var(--surface-variant);
585- background-image: none;
586- border: none;
587- border-bottom: 0.0625em solid var(--border);
588- border-radius: 0;
589- padding: 0;
590- margin: 0;
583+/* Clickable title bar - hover styling */
584+.stacked-pane-titlebar.stacked-pane-title-clickable {
591585 transition: background-color 150ms ease-in-out;
592586 }
593587
594-button.stacked-pane-title-button:hover {
588+.stacked-pane-titlebar.stacked-pane-title-clickable:hover {
595589 background-color: shade(var(--surface-variant), 1.15);
596590 }
597591
598-button.stacked-pane-title-button:focus {
599- outline: none;
600- box-shadow: none;
601-}
602-
603592 /* Title bar content box */
604593 .stacked-pane-titlebar.active {
605594 background-color: shade(var(--surface-variant), 1.2);
@@ -620,7 +609,8 @@ button.stacked-pane-title-button:focus {
620609 font-weight: 400;
621610 }
622611
623-button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
612+/* Hover effect on title bar label */
613+.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
624614 color: var(--accent);
625615 }
626616
@@ -652,7 +642,7 @@ button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
652642 }
653643
654644 /* Show close button more prominently when hovering title bar */
655-button.stacked-pane-title-button:hover .stacked-pane-close-button {
645+.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
656646 opacity: 0.7;
657647 }
658648
@@ -572,6 +572,7 @@ func generateStackedPaneCSS(p Palette) string {
572 return `/* ===== Stacked Pane Styling ===== */572 return `/* ===== Stacked Pane Styling ===== */
573 573
574 /* Title bar for inactive panes in a stack */574 /* Title bar for inactive panes in a stack */
575+/* The title bar is now a Box with GestureClick, not wrapped in a button */
575 .stacked-pane-titlebar {576 .stacked-pane-titlebar {
576 background-color: var(--surface-variant);577 background-color: var(--surface-variant);
577 border-bottom: 0.0625em solid var(--border);578 border-bottom: 0.0625em solid var(--border);
@@ -579,27 +580,15 @@ func generateStackedPaneCSS(p Palette) string {
579 min-height: 1.5em;580 min-height: 1.5em;
580 }581 }
581 582
582-/* Title bar button wrapper - clickable area */583+/* Clickable title bar - hover styling */
583-button.stacked-pane-title-button {584+.stacked-pane-titlebar.stacked-pane-title-clickable {
584- background-color: var(--surface-variant);
585- background-image: none;
586- border: none;
587- border-bottom: 0.0625em solid var(--border);
588- border-radius: 0;
589- padding: 0;
590- margin: 0;
591 transition: background-color 150ms ease-in-out;585 transition: background-color 150ms ease-in-out;
592 }586 }
593 587
594-button.stacked-pane-title-button:hover {588+.stacked-pane-titlebar.stacked-pane-title-clickable:hover {
595 background-color: shade(var(--surface-variant), 1.15);589 background-color: shade(var(--surface-variant), 1.15);
596 }590 }
597 591
598-button.stacked-pane-title-button:focus {
599- outline: none;
600- box-shadow: none;
601-}
602-
603 /* Title bar content box */592 /* Title bar content box */
604 .stacked-pane-titlebar.active {593 .stacked-pane-titlebar.active {
605 background-color: shade(var(--surface-variant), 1.2);594 background-color: shade(var(--surface-variant), 1.2);
@@ -620,7 +609,8 @@ button.stacked-pane-title-button:focus {
620 font-weight: 400;609 font-weight: 400;
621 }610 }
622 611
623-button.stacked-pane-title-button:hover .stacked-pane-titlebar label {612+/* Hover effect on title bar label */
613+.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
624 color: var(--accent);614 color: var(--accent);
625 }615 }
626 616
@@ -652,7 +642,7 @@ button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
652 }642 }
653 643
654 /* Show close button more prominently when hovering title bar */644 /* Show close button more prominently when hovering title bar */
655-button.stacked-pane-title-button:hover .stacked-pane-close-button {645+.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
656 opacity: 0.7;646 opacity: 0.7;
657 }647 }
658 648

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