feat(ui): content-measured History columns, shared caption widths, settings_view split

Stage 5: generalize logColumnWidth into textColumnWidth so History's
Trigger/Job/State/Detail/Log columns size from measured text instead of
pixel constants that clipped at larger text sizes (F6, F14).

Stage 6: captionColumnWidth replaces detailCaptionWidth and
settingsLabelWidth with one theme-derived helper; jobs_view_details.go
now builds its metadata rows and their width from a single
metadataRows() list instead of two hand-kept ones (F10); the Settings
button row drops its transparent-rectangle spacers for a
CustomPaddedLayout (F8); the remaining eight fyne.TextTruncate call
sites move to the non-deprecated Truncation field (N1).

Stage 7: settings_view.go split into settings_view.go (field
construction/save/load/validate), settings_view_layout.go (the
two-column layout and settingsSection/settingsRow), and
settings_view_helpers.go (fyneVersion, dialogs, path helpers),
mirroring the jobs_view.go split. Along the way, Queue/Storage's inline
VBox and Application/About's settingsSection collapse into one
settingsSection(title, spacing, rows...) constructor, and
chooseFile/chooseJSONFile merge into one function with a filter
argument.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-07-27 16:26:13 +03:00
parent 57e6fe410e
commit cebd41a5ac
12 changed files with 621 additions and 269 deletions
+74
View File
@@ -1,6 +1,7 @@
package ui
import (
"strings"
"testing"
"time"
@@ -218,6 +219,79 @@ func TestHistoryCellTemplateIsPlainText(t *testing.T) {
}
}
// TestTextColumnWidthClamps covers the three shapes textColumnWidth has to
// handle: a sample narrower than min, one that lands between the bounds, and
// one wide enough to hit the max cap.
func TestTextColumnWidthClamps(t *testing.T) {
testApp := test.NewApp()
defer testApp.Quit()
min, max := float32(50), float32(120)
if got := textColumnWidth([]string{"x"}, min, max); got != min {
t.Errorf("below-min sample: got %v, want the floor %v", got, min)
}
inRange := textWidth("mid-sized value") + cellPadding()
if inRange <= min || inRange >= max {
t.Skip("fixture sample no longer lands strictly between the bounds under this theme")
}
if got := textColumnWidth([]string{"mid-sized value"}, min, max); got != inRange {
t.Errorf("in-range sample: got %v, want %v", got, inRange)
}
if got := textColumnWidth([]string{strings.Repeat("0", 200)}, min, max); got != max {
t.Errorf("above-max sample: got %v, want the cap %v", got, max)
}
if got := textColumnWidth(nil, min, max); got != min {
t.Errorf("no samples: got %v, want the floor %v", got, min)
}
}
// TestHistoryColumnsFitTheirContent guards F6/F14: every column must be at
// least as wide as its widest known or actually-present value, at the default
// theme and at a scaled one, so nothing that used to be a pixel constant
// clips again.
func TestHistoryColumnsFitTheirContent(t *testing.T) {
testApp := test.NewApp()
defer testApp.Quit()
rows := []event{
{
Time: "2026-06-01 12:00:00",
Trigger: "Schedule",
JobName: "A moderately long job name for width testing",
State: "Jobs loaded",
Detail: "A somewhat longer detail message describing what happened",
LogFile: `/logs/20260601-120000_SomeJobName.log`,
},
}
check := func(when string) {
t.Helper()
widths := historyColumnWidths(rows)
samples := [][]string{
{historyTimeSample},
historyTriggerSamples,
{rows[0].JobName},
historyStateSamples,
{rows[0].Detail},
{logFileName(rows[0].LogFile)},
}
min, max := textColumnMinWidth(), textColumnMaxWidth()
for col, colSamples := range samples {
want := textColumnWidth(colSamples, min, max)
if col == 0 {
want = textWidth(historyTimeSample) + cellPadding()
}
if widths[col] < want {
t.Errorf("%s: column %d width = %v, want at least %v", when, col, widths[col], want)
}
}
}
check("default theme")
testApp.Settings().SetTheme(test.NewTheme())
check("scaled theme")
}
func TestNewEventUsesConsistentTimestampShape(t *testing.T) {
ev := newEvent(1, "Job", "OK", "detail")
if _, err := time.Parse("2006-01-02 15:04:05", ev.Time); err != nil {