diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 15a846f..55290c7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -46,6 +46,11 @@ dragged.** - The **Application** and **About** blocks are about 2 px tighter: every stacked row group in the app now shares one spacing derived from the theme rather than three separately tuned numbers. +- The **Theme** dropdown is no longer flush against the **Notifications** + checkbox. That shared row spacing pulls rows together by one text inset, which + the rows above have to give but a dropdown — which paints its box out to the + row's edge — does not, so the gap collapsed to about a pixel. The Theme row + now keeps the same gap the checkbox rows have. ## 0.15.0 - 2026-07-26 diff --git a/src/ui/layout.go b/src/ui/layout.go index 3c1f821..3dad52f 100644 --- a/src/ui/layout.go +++ b/src/ui/layout.go @@ -2,6 +2,8 @@ package ui import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -59,6 +61,17 @@ func (l minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { // than hard-coded so it follows a theme that changes SizeNameInnerPadding. func rowOverlap() float32 { return -theme.InnerPadding() } +// cancelRowOverlap exempts one row from the rowOverlap() spacing of the section +// it sits in, by padding its top edge with exactly what rowOverlap takes away. +// The overlap assumes both neighbours are text rows: each insets its text, so +// one padding's worth is duplicated and can go. A row whose value paints its own +// box to the row's edge — a Select, an Entry, a Button — has no such inset, so +// the overlap eats the visible gap instead and the box ends up flush against the +// row above it. +func cancelRowOverlap(row fyne.CanvasObject) fyne.CanvasObject { + return container.New(layout.NewCustomPaddedLayout(-rowOverlap(), 0, 0, 0), row) +} + // initialSplitOffset returns the container.Split offset that opens a horizontal // split with its leading pane at the given natural width. SetOffset takes a // ratio, but a pane's natural width is absolute, so the ratio is derived from diff --git a/src/ui/layout_test.go b/src/ui/layout_test.go index 32f21c5..b86c158 100644 --- a/src/ui/layout_test.go +++ b/src/ui/layout_test.go @@ -5,6 +5,7 @@ import ( "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" ) // TestRowOverlapMatchesInnerPadding pins rowOverlap to theme.InnerPadding, the @@ -27,6 +28,37 @@ func TestRowOverlapMatchesInnerPadding(t *testing.T) { } } +// TestCancelRowOverlapAddsBackOneInnerPadding is the regression guard for the +// Settings tab's Theme row sitting flush against the Notifications checkbox: +// the wrapper must add exactly the padding rowOverlap removes, on the top edge +// only, so the row below is unaffected and the width does not change. +func TestCancelRowOverlapAddsBackOneInnerPadding(t *testing.T) { + testApp := test.NewApp() + defer testApp.Quit() + + child := widget.NewSelect([]string{"System"}, nil) + wrapped := cancelRowOverlap(child) + + childMin, wrappedMin := child.MinSize(), wrapped.MinSize() + if got, want := wrappedMin.Height, childMin.Height-rowOverlap(); got != want { + t.Errorf("wrapped height = %v, want %v (child %v plus one inner padding)", got, want, childMin.Height) + } + if got, want := wrappedMin.Width, childMin.Width; got != want { + t.Errorf("wrapped width = %v, want the child's %v", got, want) + } + + wrapped.Resize(wrappedMin) + if got, want := child.Position().Y, -rowOverlap(); got != want { + t.Errorf("child Y = %v, want %v", got, want) + } + if got := child.Position().X; got != 0 { + t.Errorf("child X = %v, want 0", got) + } + if got, want := child.Size().Height, childMin.Height; got != want { + t.Errorf("child height = %v, want %v: the padding must not be taken out of the row", got, want) + } +} + // TestCaptionColumnWidth covers the shapes F10's shared helper has to handle: // no captions, one, and several of varying length at two text sizes. func TestCaptionColumnWidth(t *testing.T) { diff --git a/src/ui/settings_view_layout.go b/src/ui/settings_view_layout.go index a4928bf..6afd217 100644 --- a/src/ui/settings_view_layout.go +++ b/src/ui/settings_view_layout.go @@ -60,7 +60,11 @@ func newSettingsLayout(f settingsFormFields) fyne.CanvasObject { settingsRow(capW, "", f.autostartStatus), settingsRow(capW, "Tray", f.minimizeToTray), settingsRow(capW, "Notifications", f.notifications), - settingsRow(capW, "Theme", f.themeSelect), + // Theme is the one row here whose value is not text: the Select paints + // a box out to the row's edge, so the section's overlap would leave it + // flush against the Notifications checkbox. Cancelling the overlap for + // this row alone restores the gap the checkbox rows have. + cancelRowOverlap(settingsRow(capW, "Theme", f.themeSelect)), ), widget.NewSeparator(), // Queue used to inline its own container.NewVBox at the theme's default