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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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( | |||
| 128 | 128 | mockTitleBar := mocks.NewMockBoxWidget(t) | |
| 129 | 129 | mockFavicon := mocks.NewMockImageWidget(t) | |
| 130 | 130 | mockLabel := mocks.NewMockLabelWidget(t) | |
| 131 | - | mockButton := mocks.NewMockButtonWidget(t) | |
| 132 | 131 | mockCloseButton := mocks.NewMockButtonWidget(t) | |
| 133 | 132 | ||
| 134 | 133 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once() | |
| 135 | 134 | mockStackBox.EXPECT().SetHexpand(true).Once() | |
| 136 | 135 | mockStackBox.EXPECT().SetVexpand(true).Once() | |
| 137 | 136 | ||
| 137 | + | // Title bar creation - now directly used without button wrapper | |
| 138 | 138 | mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() | |
| 139 | 139 | mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() | |
| 140 | + | mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once() | |
| 140 | 141 | mockTitleBar.EXPECT().SetVexpand(false).Once() | |
| 142 | + | mockTitleBar.EXPECT().SetHexpand(true).Once() | |
| 141 | 143 | ||
| 142 | 144 | mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() | |
| 143 | 145 | mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once() | |
| @@ -160,22 +162,21 @@ func setupStackedLeafMocks( | |||
| 160 | 162 | mockCloseButton.EXPECT().SetHexpand(false).Once() | |
| 161 | 163 | mockTitleBar.EXPECT().Append(mockCloseButton).Once() | |
| 162 | 164 | ||
| 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() | |
| 170 | 167 | ||
| 171 | - | // Click handlers | |
| 172 | - | mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() | |
| 168 | + | // Close button click handler | |
| 173 | 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 | 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 | 180 | container.EXPECT().SetVisible(true).Once() | |
| 180 | 181 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 181 | 182 | ||
| @@ -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 button | 165 | + | // 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 handlers | 168 | + | // 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 ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/logging" | |
| 9 | 9 | "github.com/jwijenbergh/puregotk/v4/gobject" | |
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | // ErrStackEmpty is returned when operating on an empty stack. | |
| @@ -34,10 +35,11 @@ type stackedPane struct { | |||
| 34 | 35 | isActive bool | |
| 35 | 36 | ||
| 36 | 37 | // Signal handler IDs for cleanup on removal | |
| 37 | - | titleClickSignalID uint32 | |
| 38 | 38 | closeClickSignalID uint32 | |
| 39 | - | titleButton ButtonWidget // stored for signal disconnection | |
| 40 | 39 | closeButton ButtonWidget // stored for signal disconnection | |
| 40 | + | ||
| 41 | + | // Retained callback for GestureClick to prevent GC | |
| 42 | + | titleClickCallback interface{} | |
| 41 | 43 | } | |
| 42 | 44 | ||
| 43 | 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 | 73 | // titleBarComponents holds the widgets created for a pane's title bar. | |
| 72 | 74 | 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 | |
| 78 | 79 | } | |
| 79 | 80 | ||
| 80 | 81 | // createTitleBar creates the title bar widgets for a pane. | |
| 81 | 82 | func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarComponents { | |
| 82 | 83 | // Create title bar container - must not expand vertically | |
| 84 | + | // This is now the clickable container directly (using GestureClick), not wrapped in a button | |
| 83 | 85 | titleBar := sv.factory.NewBox(OrientationHorizontal, 4) | |
| 84 | 86 | titleBar.AddCssClass("stacked-pane-titlebar") | |
| 87 | + | titleBar.AddCssClass("stacked-pane-title-clickable") // For hover styling | |
| 85 | 88 | titleBar.SetVexpand(false) | |
| 89 | + | titleBar.SetHexpand(true) // Fill horizontal space | |
| 86 | 90 | ||
| 87 | 91 | // Create favicon image | |
| 88 | 92 | favicon := sv.factory.NewImage() | |
| @@ -111,20 +115,11 @@ func (sv *StackedView) createTitleBar(title, faviconIconName string) titleBarCom | |||
| 111 | 115 | closeBtn.SetHexpand(false) | |
| 112 | 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 | 118 | 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, | |
| 128 | 123 | } | |
| 129 | 124 | } | |
| 130 | 125 | ||
| @@ -145,7 +140,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa | |||
| 145 | 140 | tb := sv.createTitleBar(title, faviconIconName) | |
| 146 | 141 | ||
| 147 | 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 | 145 | pane := &stackedPane{ | |
| 151 | 146 | paneID: paneID, | |
| @@ -155,20 +150,19 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa | |||
| 155 | 150 | favicon: tb.favicon, | |
| 156 | 151 | label: tb.label, | |
| 157 | 152 | isActive: false, | |
| 158 | - | titleClickSignalID: titleSignalID, | |
| 159 | 153 | closeClickSignalID: closeSignalID, | |
| 160 | - | titleButton: tb.titleButton, | |
| 161 | 154 | closeButton: tb.closeBtn, | |
| 155 | + | titleClickCallback: titleClickCb, | |
| 162 | 156 | } | |
| 163 | 157 | ||
| 164 | 158 | index := len(sv.panes) | |
| 165 | 159 | sv.panes = append(sv.panes, pane) | |
| 166 | 160 | ||
| 167 | - | // Add to container | |
| 161 | + | // Add to container - now we add the titleBar directly (not wrapped in a button) | |
| 168 | 162 | log.Debug(). | |
| 169 | 163 | 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) | |
| 172 | 166 | if container != nil { | |
| 173 | 167 | sv.box.Append(container) | |
| 174 | 168 | } | |
| @@ -186,10 +180,40 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa | |||
| 186 | 180 | ||
| 187 | 181 | // connectTitleBarHandlers connects the click handlers for a title bar. | |
| 188 | 182 | // 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 | + | ||
| 193 | 217 | sv.mu.RLock() | |
| 194 | 218 | callback := sv.onActivate | |
| 195 | 219 | currentIndex := sv.findPaneIndexInternal(paneID) | |
| @@ -198,7 +222,9 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str | |||
| 198 | 222 | if callback != nil && currentIndex >= 0 { | |
| 199 | 223 | callback(currentIndex) | |
| 200 | 224 | } | |
| 201 | - | }) | |
| 225 | + | } | |
| 226 | + | clickCtrl.ConnectPressed(&clickCb) | |
| 227 | + | tb.titleBar.AddController(&clickCtrl.EventController) | |
| 202 | 228 | ||
| 203 | 229 | // Connect close button click handler | |
| 204 | 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, closeSignalID | |
| 240 | + | return clickCb, closeSignalID | |
| 215 | 241 | } | |
| 216 | 242 | ||
| 217 | 243 | // disconnectPaneSignals disconnects signal handlers from a pane's buttons. | |
| 218 | 244 | // This prevents memory leaks when panes are removed from the stack. | |
| 219 | 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 | 247 | func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) { | |
| 221 | 248 | if pane == nil { | |
| 222 | 249 | return | |
| 223 | 250 | } | |
| 224 | 251 | ||
| 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 | |
| 227 | 255 | ||
| 228 | 256 | // Disconnect close button click signal | |
| 229 | 257 | disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID) | |
| @@ -281,7 +309,7 @@ func (sv *StackedView) InsertPaneAfter( | |||
| 281 | 309 | tb := sv.createTitleBar(title, faviconIconName) | |
| 282 | 310 | ||
| 283 | 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 | 314 | pane := &stackedPane{ | |
| 287 | 315 | paneID: paneID, | |
| @@ -291,10 +319,9 @@ func (sv *StackedView) InsertPaneAfter( | |||
| 291 | 319 | favicon: tb.favicon, | |
| 292 | 320 | label: tb.label, | |
| 293 | 321 | isActive: false, | |
| 294 | - | titleClickSignalID: titleSignalID, | |
| 295 | 322 | closeClickSignalID: closeSignalID, | |
| 296 | - | titleButton: tb.titleButton, | |
| 297 | 323 | closeButton: tb.closeBtn, | |
| 324 | + | titleClickCallback: titleClickCb, | |
| 298 | 325 | } | |
| 299 | 326 | ||
| 300 | 327 | // Insert into slice at correct position | |
| @@ -303,7 +330,7 @@ func (sv *StackedView) InsertPaneAfter( | |||
| 303 | 330 | sv.panes[insertIndex] = pane | |
| 304 | 331 | ||
| 305 | 332 | // 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 | 335 | // Set this pane as active | |
| 309 | 336 | sv.setActiveInternal(ctx, insertIndex) | |
| @@ -316,27 +343,26 @@ func (sv *StackedView) InsertPaneAfter( | |||
| 316 | 343 | return insertIndex | |
| 317 | 344 | } | |
| 318 | 345 | ||
| 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) { | |
| 321 | 348 | if insertIndex > 0 && sv.panes[insertIndex-1] != nil { | |
| 322 | 349 | // Insert after the previous pane's container | |
| 323 | 350 | prevPane := sv.panes[insertIndex-1] | |
| 324 | 351 | if prevPane.container != nil { | |
| 325 | - | sv.box.InsertChildAfter(titleButton, prevPane.container) | |
| 352 | + | sv.box.InsertChildAfter(titleBar, prevPane.container) | |
| 326 | 353 | if container != nil { | |
| 327 | - | sv.box.InsertChildAfter(container, titleButton) | |
| 354 | + | sv.box.InsertChildAfter(container, titleBar) | |
| 328 | 355 | } | |
| 329 | 356 | } 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) | |
| 334 | 360 | if container != nil { | |
| 335 | - | sv.box.InsertChildAfter(container, titleButton) | |
| 361 | + | sv.box.InsertChildAfter(container, titleBar) | |
| 336 | 362 | } | |
| 337 | 363 | } else { | |
| 338 | 364 | // Fallback to append | |
| 339 | - | sv.box.Append(titleButton) | |
| 365 | + | sv.box.Append(titleBar) | |
| 340 | 366 | if container != nil { | |
| 341 | 367 | sv.box.Append(container) | |
| 342 | 368 | } | |
| @@ -347,7 +373,7 @@ func (sv *StackedView) insertTitleBarWidgets(titleButton ButtonWidget, container | |||
| 347 | 373 | if container != nil { | |
| 348 | 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 | 407 | // Remove from container | |
| 382 | 408 | 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) | |
| 388 | 411 | } | |
| 389 | 412 | if pane.container != nil { | |
| 390 | 413 | sv.box.Remove(pane.container) | |
| @@ -451,17 +474,15 @@ func (sv *StackedView) updateVisibilityInternal(ctx context.Context) { | |||
| 451 | 474 | pane.isActive = isActive | |
| 452 | 475 | ||
| 453 | 476 | // 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 | 478 | 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") | |
| 465 | 486 | } | |
| 466 | 487 | ||
| 467 | 488 | // 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 bool | 35 | isActive bool | ||
| 35 | 36 | ||||
| 36 | // Signal handler IDs for cleanup on removal | 37 | // Signal handler IDs for cleanup on removal | ||
| 37 | - | titleClickSignalID uint32 | |||
| 38 | closeClickSignalID uint32 | 38 | closeClickSignalID uint32 | ||
| 39 | - | titleButton ButtonWidget // stored for signal disconnection | |||
| 40 | closeButton ButtonWidget // stored for signal disconnection | 39 | 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 BoxWidget | 75 | + | titleBar BoxWidget |
| 74 | - | titleButton ButtonWidget | 76 | + | closeBtn ButtonWidget |
| 75 | - | closeBtn ButtonWidget | 77 | + | favicon ImageWidget |
| 76 | - | favicon ImageWidget | 78 | + | 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 vertically | 83 | // 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 image | 91 | // 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 container | 161 | + | // 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 activation | 185 | + | 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.onActivate | 218 | 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 handler | 229 | // 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, closeSignalID | 240 | + | 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 | return | 249 | return | ||
| 223 | } | 250 | } | ||
| 224 | 251 | ||||
| 225 | - | // Disconnect title button click signal | 252 | + | // 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 signal | 256 | // 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 position | 327 | // Insert into slice at correct position | ||
| @@ -303,7 +330,7 @@ func (sv *StackedView) InsertPaneAfter( | |||||
| 303 | sv.panes[insertIndex] = pane | 330 | sv.panes[insertIndex] = pane | ||
| 304 | 331 | ||||
| 305 | // Insert widgets at correct position in GTK box | 332 | // 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 active | 335 | // 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 insertIndex | 343 | 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 container | 349 | // 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 append | 364 | // 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 container | 407 | // 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 parent | 409 | + | // 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 = isActive | 474 | pane.isActive = isActive | ||
| 452 | 475 | ||||
| 453 | // Title bar is always visible for inactive panes, hidden for active | 476 | // 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 pane | 488 | // 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 | |||
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | 27 | // 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 | 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 | 33 | mockTitleBar := mocks.NewMockBoxWidget(t) | |
| 32 | 34 | mockFavicon := mocks.NewMockImageWidget(t) | |
| 33 | 35 | mockLabel := mocks.NewMockLabelWidget(t) | |
| 34 | - | mockButton := mocks.NewMockButtonWidget(t) | |
| 35 | 36 | mockCloseButton := mocks.NewMockButtonWidget(t) | |
| 36 | 37 | mockContainer := mocks.NewMockWidget(t) | |
| 37 | 38 | ||
| 38 | - | // Title bar creation | |
| 39 | + | // Title bar creation - now directly used without button wrapper | |
| 39 | 40 | mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() | |
| 40 | 41 | mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() | |
| 42 | + | mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once() | |
| 41 | 43 | mockTitleBar.EXPECT().SetVexpand(false).Once() | |
| 44 | + | mockTitleBar.EXPECT().SetHexpand(true).Once() | |
| 42 | 45 | ||
| 43 | 46 | // Favicon | |
| 44 | 47 | mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() | |
| @@ -63,27 +66,20 @@ func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox | |||
| 63 | 66 | mockCloseButton.EXPECT().SetHexpand(false).Once() | |
| 64 | 67 | mockTitleBar.EXPECT().Append(mockCloseButton).Once() | |
| 65 | 68 | ||
| 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() | |
| 73 | 71 | ||
| 74 | - | // Click handlers (title bar activation and close button) | |
| 75 | - | mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() | |
| 72 | + | // Close button click handler | |
| 76 | 73 | mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once() | |
| 77 | 74 | ||
| 78 | 75 | // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests | |
| 79 | - | mockButton.EXPECT().GtkWidget().Return(nil).Maybe() | |
| 80 | 76 | mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe() | |
| 81 | 77 | ||
| 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() | |
| 84 | 80 | mockBox.EXPECT().Append(mockContainer).Once() | |
| 85 | 81 | ||
| 86 | - | return mockTitleBar, mockFavicon, mockLabel, mockButton, mockContainer | |
| 82 | + | return mockTitleBar, mockFavicon, mockLabel, mockContainer | |
| 87 | 83 | } | |
| 88 | 84 | ||
| 89 | 85 | func TestNewStackedView_EmptyStack(t *testing.T) { | |
| @@ -103,13 +99,12 @@ func TestAddPane_SinglePane_BecomesActive(t *testing.T) { | |||
| 103 | 99 | // Arrange | |
| 104 | 100 | ctx := context.Background() | |
| 105 | 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 | 103 | _ = mockFavicon | |
| 108 | 104 | _ = mockLabel | |
| 109 | - | _ = mockButton | |
| 110 | 105 | ||
| 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 | |
| 113 | 108 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 114 | 109 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 115 | 110 | ||
| @@ -130,26 +125,24 @@ func TestAddPane_MultiplePanes_LastBecomesActive(t *testing.T) { | |||
| 130 | 125 | mockFactory, mockBox := setupMockFactory(t) | |
| 131 | 126 | ||
| 132 | 127 | // First pane | |
| 133 | - | mockTitleBar1, mockFavicon1, mockLabel1, mockButton1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 128 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 134 | 129 | _ = mockFavicon1 | |
| 135 | 130 | _ = mockLabel1 | |
| 136 | - | _ = mockButton1 | |
| 137 | - | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 131 | + | mockTitleBar1.EXPECT().SetVisible(false).Once() // Active pane hides its title bar | |
| 138 | 132 | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 139 | 133 | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 140 | 134 | ||
| 141 | 135 | // 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 | 137 | _ = mockFavicon2 | |
| 144 | 138 | _ = mockLabel2 | |
| 145 | - | _ = mockButton2 | |
| 146 | 139 | ||
| 147 | 140 | // 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 | 142 | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 150 | 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 | 146 | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 154 | 147 | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 155 | 148 | ||
| @@ -173,22 +166,17 @@ func TestRemovePane_MiddlePane(t *testing.T) { | |||
| 173 | 166 | // Setup 3 panes (simplified - just track the key behaviors) | |
| 174 | 167 | containers := make([]*mocks.MockWidget, 3) | |
| 175 | 168 | titleBars := make([]*mocks.MockBoxWidget, 3) | |
| 176 | - | buttons := make([]*mocks.MockButtonWidget, 3) | |
| 177 | 169 | ||
| 178 | 170 | 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) | |
| 182 | 172 | } | |
| 183 | 173 | ||
| 184 | 174 | // Allow any visibility calls during setup and removal | |
| 185 | 175 | for i := 0; i < 3; i++ { | |
| 186 | - | titleBars[i].EXPECT().GetParent().Return(buttons[i]).Maybe() | |
| 187 | 176 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 177 | + | titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 188 | 178 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 189 | 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 | 182 | sv := layout.NewStackedView(mockFactory) | |
| @@ -197,8 +185,8 @@ func TestRemovePane_MiddlePane(t *testing.T) { | |||
| 197 | 185 | sv.AddPane(ctx, "pane-3", "Page 3", "", containers[2]) | |
| 198 | 186 | ||
| 199 | 187 | // 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() | |
| 202 | 190 | mockBox.EXPECT().Remove(containers[1]).Once() | |
| 203 | 191 | ||
| 204 | 192 | // Act | |
| @@ -213,12 +201,11 @@ func TestRemovePane_LastPane_ReturnsError(t *testing.T) { | |||
| 213 | 201 | // Arrange | |
| 214 | 202 | ctx := context.Background() | |
| 215 | 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 | 205 | _ = mockFavicon | |
| 218 | 206 | _ = 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 | 209 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 223 | 210 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 224 | 211 | ||
| @@ -250,12 +237,11 @@ func TestRemovePane_IndexOutOfBounds(t *testing.T) { | |||
| 250 | 237 | // Arrange | |
| 251 | 238 | ctx := context.Background() | |
| 252 | 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 | 241 | _ = mockFavicon | |
| 255 | 242 | _ = 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 | 245 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 260 | 246 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 261 | 247 | ||
| @@ -278,15 +264,14 @@ func TestSetActive_ValidIndex(t *testing.T) { | |||
| 278 | 264 | titleBars := make([]*mocks.MockBoxWidget, 2) | |
| 279 | 265 | ||
| 280 | 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 | 268 | _ = mockFavicon | |
| 283 | - | _ = mockOverlay | |
| 284 | - | _ = mockSpinner | |
| 269 | + | _ = mockLabel | |
| 285 | 270 | ||
| 286 | 271 | titleBars[i] = titleBar | |
| 287 | 272 | containers[i] = container | |
| 288 | 273 | ||
| 289 | - | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 274 | + | titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 290 | 275 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 291 | 276 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 292 | 277 | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -308,12 +293,11 @@ func TestSetActive_OutOfBounds(t *testing.T) { | |||
| 308 | 293 | // Arrange | |
| 309 | 294 | ctx := context.Background() | |
| 310 | 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 | 297 | _ = mockFavicon | |
| 313 | 298 | _ = 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 | 301 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 318 | 302 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 319 | 303 | ||
| @@ -344,9 +328,9 @@ func TestUpdateTitle(t *testing.T) { | |||
| 344 | 328 | // Arrange | |
| 345 | 329 | ctx := context.Background() | |
| 346 | 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 | 334 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 351 | 335 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 352 | 336 | ||
| @@ -378,9 +362,9 @@ func TestUpdateFavicon(t *testing.T) { | |||
| 378 | 362 | // Arrange | |
| 379 | 363 | ctx := context.Background() | |
| 380 | 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 | 368 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 385 | 369 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 386 | 370 | ||
| @@ -400,12 +384,11 @@ func TestGetContainer(t *testing.T) { | |||
| 400 | 384 | // Arrange | |
| 401 | 385 | ctx := context.Background() | |
| 402 | 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 | 388 | _ = mockFavicon | |
| 405 | - | _ = mockOverlay | |
| 406 | - | _ = mockSpinner | |
| 389 | + | _ = mockLabel | |
| 407 | 390 | ||
| 408 | - | mockTitleBar.EXPECT().GetParent().Return(nil).Maybe() | |
| 391 | + | mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar | |
| 409 | 392 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 410 | 393 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 411 | 394 | ||
| @@ -454,15 +437,14 @@ func TestNavigateNext_WrapsAround(t *testing.T) { | |||
| 454 | 437 | titleBars := make([]*mocks.MockBoxWidget, 2) | |
| 455 | 438 | ||
| 456 | 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 | 441 | _ = mockFavicon | |
| 459 | - | _ = mockOverlay | |
| 460 | - | _ = mockSpinner | |
| 442 | + | _ = mockLabel | |
| 461 | 443 | ||
| 462 | 444 | titleBars[i] = titleBar | |
| 463 | 445 | containers[i] = container | |
| 464 | 446 | ||
| 465 | - | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 447 | + | titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 466 | 448 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 467 | 449 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 468 | 450 | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -492,15 +474,14 @@ func TestNavigatePrevious_WrapsAround(t *testing.T) { | |||
| 492 | 474 | titleBars := make([]*mocks.MockBoxWidget, 2) | |
| 493 | 475 | ||
| 494 | 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 | 478 | _ = mockFavicon | |
| 497 | - | _ = mockOverlay | |
| 498 | - | _ = mockSpinner | |
| 479 | + | _ = mockLabel | |
| 499 | 480 | ||
| 500 | 481 | titleBars[i] = titleBar | |
| 501 | 482 | containers[i] = container | |
| 502 | 483 | ||
| 503 | - | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 484 | + | titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 504 | 485 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 505 | 486 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 506 | 487 | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -575,14 +556,15 @@ func setupInsertPaneMocks( | |||
| 575 | 556 | mockTitleBar := mocks.NewMockBoxWidget(t) | |
| 576 | 557 | mockFavicon := mocks.NewMockImageWidget(t) | |
| 577 | 558 | mockLabel := mocks.NewMockLabelWidget(t) | |
| 578 | - | mockButton := mocks.NewMockButtonWidget(t) | |
| 579 | 559 | mockCloseButton := mocks.NewMockButtonWidget(t) | |
| 580 | 560 | mockContainer := mocks.NewMockWidget(t) | |
| 581 | 561 | ||
| 582 | - | // Title bar creation | |
| 562 | + | // Title bar creation - now directly used without button wrapper | |
| 583 | 563 | mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once() | |
| 584 | 564 | mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once() | |
| 565 | + | mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once() | |
| 585 | 566 | mockTitleBar.EXPECT().SetVexpand(false).Once() | |
| 567 | + | mockTitleBar.EXPECT().SetHexpand(true).Once() | |
| 586 | 568 | ||
| 587 | 569 | // Favicon | |
| 588 | 570 | mockFactory.EXPECT().NewImage().Return(mockFavicon).Once() | |
| @@ -607,26 +589,23 @@ func setupInsertPaneMocks( | |||
| 607 | 589 | mockCloseButton.EXPECT().SetHexpand(false).Once() | |
| 608 | 590 | mockTitleBar.EXPECT().Append(mockCloseButton).Once() | |
| 609 | 591 | ||
| 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() | |
| 617 | 594 | ||
| 618 | - | // Click handlers (title bar activation and close button) | |
| 619 | - | mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once() | |
| 595 | + | // Close button click handler | |
| 620 | 596 | mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once() | |
| 621 | 597 | ||
| 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 | |
| 623 | 602 | 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() | |
| 626 | 605 | } else { | |
| 627 | 606 | // Insert at beginning using Prepend | |
| 628 | 607 | mockBox.EXPECT().Prepend(mockContainer).Once() | |
| 629 | - | mockBox.EXPECT().Prepend(mockButton).Once() | |
| 608 | + | mockBox.EXPECT().Prepend(mockTitleBar).Once() | |
| 630 | 609 | } | |
| 631 | 610 | ||
| 632 | 611 | return mockTitleBar, mockContainer | |
| @@ -638,11 +617,10 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) { | |||
| 638 | 617 | mockFactory, mockBox := setupMockFactory(t) | |
| 639 | 618 | ||
| 640 | 619 | // First pane - appended normally | |
| 641 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 620 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 642 | 621 | _ = 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 | |
| 646 | 624 | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 647 | 625 | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 648 | 626 | ||
| @@ -653,11 +631,11 @@ func TestInsertPaneAfter_AtBeginning(t *testing.T) { | |||
| 653 | 631 | mockTitleBar2, mockContainer2 := setupInsertPaneMocks(t, mockFactory, mockBox, nil) | |
| 654 | 632 | ||
| 655 | 633 | // Visibility updates | |
| 656 | - | mockTitleBar1.EXPECT().GetParent().Return(nil).Maybe() | |
| 634 | + | mockTitleBar1.EXPECT().SetVisible(true).Once() // First pane becomes inactive, show its title bar | |
| 657 | 635 | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 658 | 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 | 639 | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 662 | 640 | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 663 | 641 | ||
| @@ -680,8 +658,8 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) { | |||
| 680 | 658 | titleBars := make([]*mocks.MockBoxWidget, 2) | |
| 681 | 659 | ||
| 682 | 660 | 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() | |
| 685 | 663 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 686 | 664 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 687 | 665 | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -696,7 +674,7 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) { | |||
| 696 | 674 | ||
| 697 | 675 | // Insert new pane after index 0 (should become index 1) | |
| 698 | 676 | mockTitleBar3, mockContainer3 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[0]) | |
| 699 | - | mockTitleBar3.EXPECT().GetParent().Return(nil).Maybe() | |
| 677 | + | mockTitleBar3.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 700 | 678 | mockContainer3.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 701 | 679 | mockTitleBar3.EXPECT().AddCssClass("active").Maybe() | |
| 702 | 680 | mockTitleBar3.EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -716,11 +694,10 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) { | |||
| 716 | 694 | mockFactory, mockBox := setupMockFactory(t) | |
| 717 | 695 | ||
| 718 | 696 | // First pane | |
| 719 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 697 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 720 | 698 | _ = 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 | |
| 724 | 701 | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 725 | 702 | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 726 | 703 | ||
| @@ -730,11 +707,11 @@ func TestInsertPaneAfter_AtEnd(t *testing.T) { | |||
| 730 | 707 | // Insert after last pane (afterIndex=0, becomes index 1) | |
| 731 | 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 | 711 | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 735 | 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 | 715 | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 739 | 716 | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 740 | 717 | ||
| @@ -757,15 +734,14 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) { | |||
| 757 | 734 | titleBars := make([]*mocks.MockBoxWidget, 3) | |
| 758 | 735 | ||
| 759 | 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 | 738 | _ = mockFavicon | |
| 762 | - | _ = mockOverlay | |
| 763 | - | _ = mockSpinner | |
| 739 | + | _ = mockLabel | |
| 764 | 740 | ||
| 765 | 741 | titleBars[i] = titleBar | |
| 766 | 742 | containers[i] = container | |
| 767 | 743 | ||
| 768 | - | titleBars[i].EXPECT().GetParent().Return(nil).Maybe() | |
| 744 | + | titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 769 | 745 | containers[i].EXPECT().SetVisible(mock.Anything).Maybe() | |
| 770 | 746 | titleBars[i].EXPECT().AddCssClass("active").Maybe() | |
| 771 | 747 | titleBars[i].EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -781,7 +757,7 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) { | |||
| 781 | 757 | ||
| 782 | 758 | // Insert D after B (should be at index 2, C moves to index 3) | |
| 783 | 759 | mockTitleBar4, mockContainer4 := setupInsertPaneMocks(t, mockFactory, mockBox, containers[1]) | |
| 784 | - | mockTitleBar4.EXPECT().GetParent().Return(nil).Maybe() | |
| 760 | + | mockTitleBar4.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 785 | 761 | mockContainer4.EXPECT().SetVisible(mock.Anything).Maybe() | |
| 786 | 762 | mockTitleBar4.EXPECT().AddCssClass("active").Maybe() | |
| 787 | 763 | mockTitleBar4.EXPECT().RemoveCssClass("active").Maybe() | |
| @@ -812,11 +788,10 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) { | |||
| 812 | 788 | mockFactory, mockBox := setupMockFactory(t) | |
| 813 | 789 | ||
| 814 | 790 | // First pane | |
| 815 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 791 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | |
| 816 | 792 | _ = 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 | |
| 820 | 795 | mockContainer1.EXPECT().SetVisible(true).Once() | |
| 821 | 796 | mockTitleBar1.EXPECT().AddCssClass("active").Once() | |
| 822 | 797 | ||
| @@ -826,11 +801,11 @@ func TestInsertPaneAfter_InvalidIndexClamped(t *testing.T) { | |||
| 826 | 801 | // Try to insert at invalid index (100) - should clamp to end | |
| 827 | 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 | 805 | mockContainer1.EXPECT().SetVisible(false).Once() | |
| 831 | 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 | 809 | mockContainer2.EXPECT().SetVisible(true).Once() | |
| 835 | 810 | mockTitleBar2.EXPECT().AddCssClass("active").Once() | |
| 836 | 811 | ||
| @@ -25,20 +25,23 @@ func setupMockFactory(t *testing.T) (*mocks.MockWidgetFactory, *mocks.MockBoxWid | |||||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | // setupPaneMocks creates mocks needed for AddPane | 27 | // 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 creation | 39 | + | // 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 | // Favicon | 46 | // 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 bar | 69 | + | // 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 tests | 75 | // 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 box | 78 | + | // 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, mockContainer | 82 | + | 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 | // Arrange | 99 | // 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 | _ = mockFavicon | 103 | _ = mockFavicon | ||
| 108 | _ = mockLabel | 104 | _ = mockLabel | ||
| 109 | - | _ = mockButton | |||
| 110 | 105 | ||||
| 111 | - | // Visibility updates for active pane | 106 | + | // 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 pane | 127 | // First pane | ||
| 133 | - | mockTitleBar1, mockFavicon1, mockLabel1, mockButton1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | 128 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) |
| 134 | _ = mockFavicon1 | 129 | _ = mockFavicon1 | ||
| 135 | _ = mockLabel1 | 130 | _ = mockLabel1 | ||
| 136 | - | _ = mockButton1 | 131 | + | 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 inactive | 135 | // 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 | _ = mockFavicon2 | 137 | _ = mockFavicon2 | ||
| 144 | _ = mockLabel2 | 138 | _ = mockLabel2 | ||
| 145 | - | _ = mockButton2 | |||
| 146 | 139 | ||||
| 147 | // When second pane is added, update visibility for both | 140 | // 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.MockButtonWidget | 171 | + | 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 removal | 174 | // 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 widget | 188 | + | // 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 | // Act | 192 | // Act | ||
| @@ -213,12 +201,11 @@ func TestRemovePane_LastPane_ReturnsError(t *testing.T) { | |||||
| 213 | // Arrange | 201 | // 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 | _ = mockFavicon | 205 | _ = mockFavicon | ||
| 218 | _ = mockLabel | 206 | _ = 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 | // Arrange | 237 | // 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 | _ = mockFavicon | 241 | _ = mockFavicon | ||
| 255 | _ = mockLabel | 242 | _ = 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 | _ = mockFavicon | 268 | _ = mockFavicon | ||
| 283 | - | _ = mockOverlay | 269 | + | _ = mockLabel |
| 284 | - | _ = mockSpinner | |||
| 285 | 270 | ||||
| 286 | titleBars[i] = titleBar | 271 | titleBars[i] = titleBar | ||
| 287 | containers[i] = container | 272 | 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 | // Arrange | 293 | // 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 | _ = mockFavicon | 297 | _ = mockFavicon | ||
| 313 | _ = mockLabel | 298 | _ = 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 | // Arrange | 328 | // 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 | // Arrange | 362 | // 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 | // Arrange | 384 | // 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 | _ = mockFavicon | 388 | _ = mockFavicon | ||
| 405 | - | _ = mockOverlay | 389 | + | _ = 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 | _ = mockFavicon | 441 | _ = mockFavicon | ||
| 459 | - | _ = mockOverlay | 442 | + | _ = mockLabel |
| 460 | - | _ = mockSpinner | |||
| 461 | 443 | ||||
| 462 | titleBars[i] = titleBar | 444 | titleBars[i] = titleBar | ||
| 463 | containers[i] = container | 445 | 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 | _ = mockFavicon | 478 | _ = mockFavicon | ||
| 497 | - | _ = mockOverlay | 479 | + | _ = mockLabel |
| 498 | - | _ = mockSpinner | |||
| 499 | 480 | ||||
| 500 | titleBars[i] = titleBar | 481 | titleBars[i] = titleBar | ||
| 501 | containers[i] = container | 482 | 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 creation | 562 | + | // 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 | // Favicon | 569 | // 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 bar | 592 | + | // 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 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 | |||
| 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 Prepend | 606 | // 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, mockContainer | 611 | 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 normally | 619 | // First pane - appended normally | ||
| 641 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | 620 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) |
| 642 | _ = mockFavicon1 | 621 | _ = mockFavicon1 | ||
| 643 | - | _ = mockOverlay1 | 622 | + | _ = mockLabel1 |
| 644 | - | _ = mockSpinner1 | 623 | + | 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 updates | 633 | // 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 pane | 696 | // First pane | ||
| 719 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | 697 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) |
| 720 | _ = mockFavicon1 | 698 | _ = mockFavicon1 | ||
| 721 | - | _ = mockOverlay1 | 699 | + | _ = mockLabel1 |
| 722 | - | _ = mockSpinner1 | 700 | + | 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 | _ = mockFavicon | 738 | _ = mockFavicon | ||
| 762 | - | _ = mockOverlay | 739 | + | _ = mockLabel |
| 763 | - | _ = mockSpinner | |||
| 764 | 740 | ||||
| 765 | titleBars[i] = titleBar | 741 | titleBars[i] = titleBar | ||
| 766 | containers[i] = container | 742 | 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 pane | 790 | // First pane | ||
| 815 | - | mockTitleBar1, mockFavicon1, mockOverlay1, mockSpinner1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) | 791 | + | mockTitleBar1, mockFavicon1, mockLabel1, mockContainer1 := setupPaneMocks(t, mockFactory, mockBox) |
| 816 | _ = mockFavicon1 | 792 | _ = mockFavicon1 | ||
| 817 | - | _ = mockOverlay1 | 793 | + | _ = mockLabel1 |
| 818 | - | _ = mockSpinner1 | 794 | + | 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 end | 801 | // 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) ( | |||
| 19 | 19 | mockStackBox.EXPECT().SetHexpand(true).Once() | |
| 20 | 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 | 23 | _ = 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 | |
| 27 | 26 | mockContainer.EXPECT().SetVisible(true).Once() | |
| 28 | 27 | mockTitleBar.EXPECT().AddCssClass("active").Once() | |
| 29 | 28 | ||
| @@ -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 | _ = mockFavicon | 23 | _ = mockFavicon | ||
| 24 | - | _ = mockOverlay | 24 | + | _ = mockLabel |
| 25 | - | _ = mockSpinner | 25 | + | 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 { | |||
| 572 | 572 | return `/* ===== Stacked Pane Styling ===== */ | |
| 573 | 573 | ||
| 574 | 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 | 576 | .stacked-pane-titlebar { | |
| 576 | 577 | background-color: var(--surface-variant); | |
| 577 | 578 | border-bottom: 0.0625em solid var(--border); | |
| @@ -579,27 +580,15 @@ func generateStackedPaneCSS(p Palette) string { | |||
| 579 | 580 | min-height: 1.5em; | |
| 580 | 581 | } | |
| 581 | 582 | ||
| 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 { | |
| 591 | 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 | 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 | 592 | /* Title bar content box */ | |
| 604 | 593 | .stacked-pane-titlebar.active { | |
| 605 | 594 | background-color: shade(var(--surface-variant), 1.2); | |
| @@ -620,7 +609,8 @@ button.stacked-pane-title-button:focus { | |||
| 620 | 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 | 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 | 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 | 646 | opacity: 0.7; | |
| 657 | 647 | } | |
| 658 | 648 | ||
| @@ -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 | ||||