Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,52 @@ because it turns other people's test suites red.

### Changed

- **The window lays its forms out in columns.** A field now takes as many
columns of the form as its value needs and no more, so `Format`, `Size`,
`How many files` and `Damage` stand in one row instead of one under another,
and a box for a number is no longer as wide as a box for a path. The window
opens at the height of the form it shows, and an open list is as wide as
the menu it drops from.

- **Every section of a form folds away, and the settings of a format and of a
damage stand in groups of their own.** Each group is framed, with a line
down its left edge in the colour of what it is about - blue for a format's
settings, amber for a damage's, grey for notes to the manifest - so opening
both no longer runs them into the fields above them and into each other. A
refusal about a field inside a folded section opens that section.

- **A refusal about a field is written under its row, across the form.** It
used to wrap inside the width of the field, three lines deep for one
sentence.

- **A size below what a format can make comes with a button that uses the
smallest size that works.** The refusal names that size to the byte, and
the button puts it in the box. Nothing is filled in by itself.

- **When a run will not write because a file, a manifest or another run is
already in the output directory, the refusal stands under `Output directory`
with a button that opens it.** It used to be one line at the foot of the
window, scrolled, with no way to the directory but a file manager.

- **A menu shows its arrow in the accent colour**, so it can be told from a
box to type in and from a button, which it matched to the pixel.

- **A box to tick carries its name beside it**, and pressing the name ticks the
box. The sentence about building on a preset says "with the box ticked"
instead of "with the switch on".

- **A grey value in an empty box says it is a default**, as in `default: 60`,
because a grey `60` read as a value somebody had typed.

- **Smaller things:** `Preview` and `Generate` stand apart, and so do
`Duplicate` and `Remove`. `Donate` is quieter, and on `About` it stands in
the Support card instead of a bar of its own. The line at the foot counts
formats past three instead of naming all twenty-six. What a preset typically
finds and how to use the program are written in ordinary text instead of
the smallest grey. The licence on `About` wraps to the window, and the code
carried in the program is listed as a table. A menu holding the keyboard is
marked with a ring instead of being filled blue.

- **`Generate` means the same thing on every screen.** The `Several batches`
screen used to open with `Batch name` and `Size` empty under a red star
each, so the button that works straight away on `Single batch` turned you
Expand Down Expand Up @@ -416,6 +462,9 @@ because it turns other people's test suites red.

### Fixed

- **A preview or a run refused while it was being planned no longer leaves
"Working out what this would cost..." standing over the refusal.**

- **A refusal about two settings that bound each other now says how far over
you are.** Asking for a picture of 20000 by 2001 pixels was turned down with
"together they come to 40 megapixels and the limit is 40" - the same number
Expand Down
51 changes: 51 additions & 0 deletions internal/guard/actionbarheight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"

"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
Expand Down Expand Up @@ -381,3 +382,53 @@ func progressUnder(o fyne.CanvasObject) *parts.Progress {
})
return found
}

// TestTheBarsButtonsClearItsRailInTheNarrowestWindow narrows the window as far
// as it goes and asks whether the centred buttons still stand clear of the
// rail at the left of the bar.
//
// Measured before it existed, with guirender on the batch screen: "Add a
// batch" covered Preview from about 495 px on main and from about 588 px on
// #126, whose buttons grew wider gaps - and Preview was gone altogether at
// 495, while the window let itself be made that narrow. The bar laid its rail
// over the column in a stack, and a stack's minimum is the larger of its two
// children rather than what they need side by side. Found while checking an
// outside review of #126 (docs/REVIEW-126-2026-09-23.md).
//
// The narrowest window is asked of the window rather than typed here: it is
// the minimum of everything the window holds plus the padding the window
// draws round it, which is what the system will not let a person go below.
// The padding is added by hand because the test window does not hold its
// minimum - asked for 1 px it lays the screen out in 1 px - and the first
// run of this guard went red on a window 8 px narrower than any real one.
// Positions are read off the laid out screen, because the promise is about
// what is drawn (GUI rule 10).
func TestTheBarsButtonsClearItsRailInTheNarrowestWindow(t *testing.T) {
content, w := screenInAWindow(t, text.TabRecipe())
width := w.Content().MinSize().Width
if w.Padded() {
width += 2 * theme.Padding()
}
narrowest := fyne.NewSize(width, window.LargestOpening.Height)
w.Resize(narrowest)
content.Refresh()
w.Resize(narrowest)

rail := buttonNamed(content, text.ButtonAddBatch())
first := buttonNamed(content, text.ButtonPreview())
if rail == nil || first == nil {
t.Fatalf("the batch screen has no %q or no %q button, so there is no rail and no row to hold apart",
text.ButtonAddBatch(), text.ButtonPreview())
}
drv := fyne.CurrentApp().Driver()
railEnds := drv.AbsolutePositionForObject(rail).X + rail.Size().Width
rowStarts := drv.AbsolutePositionForObject(first).X
if gap := rowStarts - railEnds; gap < parts.GapColumns-0.5 {
t.Errorf("in a window %.0f px wide, the narrowest it allows, %q ends at x=%.1f and %q starts at x=%.1f - "+
"a gap of %.1f px where the bar keeps %d.\n"+
"Reason: the buttons are centred and the rail stands over the same row, so a bar allowed to be\n"+
"narrower than the two side by side draws one button over the other.",
narrowest.Width, text.ButtonAddBatch(), railEnds, text.ButtonPreview(), rowStarts, gap, parts.GapColumns)
}
t.Logf("narrowest window %.0f px: the rail ends at %.1f, the row starts at %.1f", narrowest.Width, railEnds, rowStarts)
}
9 changes: 6 additions & 3 deletions internal/guard/actionrail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ import (
// held in a centred column it slides right by half of what the window gained.
// At 1600 px the old layout put it 300 px further right.
//
// Every screen, because the bar is on all four and Donate is the one control
// that is on all four - see TestTheDonateButtonIsOnEveryScreen.
// The three work screens. About had the bar too until 2026-09-23, holding
// Donate and nothing else, and it was taken off that screen on the owner's
// word from the running window: its Donate stands in the Support card now,
// inside the page, where standing with the form's column is what it should do.
// TestTheDonateButtonIsOnEveryScreen still asks that it is there.
func TestWhatIsNotAboutTheRunStandsAtTheEdgeOfTheBar(t *testing.T) {
for _, tab := range []string{
text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabAbout(),
text.TabOneTarget(), text.TabPresets(), text.TabRecipe(),
} {
t.Run(tab, func(t *testing.T) {
content, w := screenInAWindow(t, tab)
Expand Down
17 changes: 13 additions & 4 deletions internal/guard/boxwidth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,13 @@ func TestADeclaredSettingStandsOnTheSameEdgeAsTheFieldsAboveIt(t *testing.T) {
if !ok {
t.Fatal("the format list is not laid out")
}
for _, name := range []string{"width", "height"} {
// Since 2026-09-23 the settings a format declares stand in a group
// framed inside the section, laid in the same grid of columns the
// section uses - so the first of them begins on the group's inner
// edge, the format list's edge moved in by the frame's room, and
// none of them begins before it.
edge := format.X + parts.GroupInset
for i, name := range []string{"width", "height"} {
control := controlUnder(screen, text.SettingLabel(name))
if control == nil {
t.Fatalf("bmp declares %s and no field on this screen holds it", name)
Expand All @@ -321,9 +327,12 @@ func TestADeclaredSettingStandsOnTheSameEdgeAsTheFieldsAboveIt(t *testing.T) {
if !ok {
t.Fatalf("the box for %s is not laid out", name)
}
if off := box.X - format.X; off > 1 || off < -1 {
t.Errorf("the box for %s begins at x=%.0f and the format list at x=%.0f, so the settings a "+
"format declares do not stand in the column of controls", name, box.X, format.X)
if off := box.X - edge; i == 0 && (off > 1 || off < -1) {
t.Errorf("the box for %s begins at x=%.0f and the group's inner edge is at x=%.0f, so the "+
"first setting a format declares does not stand where its group begins", name, box.X, edge)
}
if box.X < edge-1 {
t.Errorf("the box for %s begins at x=%.0f, before the group's inner edge at x=%.0f", name, box.X, edge)
}
}
})
Expand Down
14 changes: 14 additions & 0 deletions internal/guard/controlnames_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ func TestEveryControlOnTheFormStandsUnderAName(t *testing.T) {
}
walk(field.Objects[1], func(inner fyne.CanvasObject) { named[inner] = head })
})
// A box to tick carries its name BESIDE the square since 2026-09-23 - the
// line is the square, then the name (parts.ToggleSaying) - so for a switch
// the name is asked of what stands after it rather than over it.
walk(batches, func(obj fyne.CanvasObject) {
line, ok := obj.(*fyne.Container)
if !ok || len(line.Objects) < 2 {
return
}
if check, isToggle := line.Objects[0].(*parts.Toggle); isToggle {
if head, is := headingOf(line.Objects[1]); is && head != "" {
named[check] = head
}
}
})

if got, is := named[switchOnIt]; !is {
t.Errorf("the switch that chooses between %q, %q and %q stands on the form with no name over it, "+
Expand Down
13 changes: 12 additions & 1 deletion internal/guard/detailpopup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func detailButtonBeside(o fyne.CanvasObject, label string) *parts.DetailButton {
var found *parts.DetailButton
walk(o, func(obj fyne.CanvasObject) {
row, ok := obj.(*fyne.Container)
if !ok || len(row.Objects) < 2 || namedOnScreen(row.Objects[0]) != label {
if !ok || len(row.Objects) < 2 || nameOfRow(row) != label {
return
}
// Searched rather than taken from position one. A heading row grew a
Expand All @@ -213,6 +213,17 @@ func detailButtonBeside(o fyne.CanvasObject, label string) *parts.DetailButton {
return found
}

// nameOfRow is the name a heading row carries: its first thing, or - on the
// line of a box to tick, which is the square and then its name since
// 2026-09-23 (parts.ToggleSaying) - its second.
func nameOfRow(row *fyne.Container) string {
if _, isToggle := row.Objects[0].(*parts.Toggle); isToggle {
head, _ := headingOf(row.Objects[1])
return head
}
return namedOnScreen(row.Objects[0])
}

// namedOnScreen is the words a heading shows. A switch's name is a heading in
// the column like every other field's since 2026-09-15, so there is no special
// case for it here any more - it carries no words of its own.
Expand Down
21 changes: 17 additions & 4 deletions internal/guard/foldhead_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,28 @@ func TestTheHeadRowOverhangsTheColumnAndTheTitleDoesNot(t *testing.T) {
if !ok {
t.Fatalf("the recipe screen has no field named %q", text.FieldFormat())
}
if off := title.X - field.X; off > 1 || off < -1 {
t.Errorf("the title of batch 1 starts at %.1f px and the name of its first field at %.1f px - one edge for everything a person reads", title.X, field.X)
// The arrow in FRONT of the title since 2026-09-23, on the owner's word
// from the running window, so it is the arrow that stands on the edge the
// fields stand on and the title follows it.
arrow := arrowIn(t, screen, head)
arrowAt, ok := absoluteOf(screen, arrow)
if !ok {
t.Fatal("the arrow is not on the screen it was found in")
}
if off := arrowAt.X - field.X; off > 1 || off < -1 {
t.Errorf("the arrow of batch 1 starts at %.1f px and the name of its first field at %.1f px - "+
"the arrow stands on the edge everything else starts on", arrowAt.X, field.X)
}
if title.X < arrowAt.X+arrow.Size().Width {
t.Errorf("the title of batch 1 starts at %.1f px, over its arrow, which ends at %.1f px",
title.X, arrowAt.X+arrow.Size().Width)
}
headAt, ok := absoluteOf(screen, head)
if !ok {
t.Fatal("the head row is not on the screen it was found in")
}
if got := title.X - headAt.X; got < parts.TabInset-1 || got > parts.TabInset+1 {
t.Errorf("the row starts %.1f px left of its title, and it has to start TabInset (%v) left of it - the room the fill and the ring draw in", got, parts.TabInset)
if got := arrowAt.X - headAt.X; got < parts.TabInset-1 || got > parts.TabInset+1 {
t.Errorf("the row starts %.1f px left of its arrow, and it has to start TabInset (%v) left of it - the room the fill and the ring draw in", got, parts.TabInset)
}
}

Expand Down
Loading
Loading