From 9a45a7be6fe15f903b1a9d28c86873fb31c2f608 Mon Sep 17 00:00:00 2001 From: mixeme Date: Mon, 27 Jul 2026 22:04:18 +0300 Subject: [PATCH] fix(ui): give the Settings Theme row back its top gap The Application section stacks its rows with rowOverlap(), a negative spacing that trades away one label's duplicated text inset. The Theme row's value is a Select, which paints its box out to the row's edge and has no inset to give, so the overlap closed the gap instead: 0.46 px between the Notifications checkbox and the dropdown, against ~8 px between the checkbox rows. cancelRowOverlap adds that one padding back on the Theme row's top edge only, restoring the gap to 7.5 px without touching the other rows or the column width. Co-Authored-By: Claude Opus 5 --- docs/CHANGELOG.md | 5 +++++ src/ui/layout.go | 13 +++++++++++++ src/ui/layout_test.go | 32 ++++++++++++++++++++++++++++++++ src/ui/settings_view_layout.go | 6 +++++- 4 files changed, 55 insertions(+), 1 deletion(-) 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