Skip to content

Commit d6befbb

Browse files
donislawdevclaude
andauthored
gui: the form in columns, groups that say what they are, refusals that offer their fix (#126)
* gui: fields in four columns, groups that say what they are (prototype) A prototype of a new layout, built round by round from the running window. Guards, stored screens and the catalogue are not updated yet. - parts.Grid lays a section's fields in four columns, each field taking the fewest columns that hold it and filling them. A refusal is drawn under its row, across it, instead of inside a 185 px column. - A group of settings that folds away is framed, with a rail in the colour of what it is about: a format's settings, a damage's, notes. - Every section of a work screen folds; a refusal opens the one it is about, and the state survives the batch screen rebuilding. - A box to tick carries its name beside it, wherever it comes from. - Lists are ordinary text with the marker on the first line (O235). - Buttons in a row stand GapButtons apart; Donate is quiet words, and on About it moved into the Support card. - A menu marks the keyboard with a ring only, not a fill. - The run line counts formats past three; a placeholder that is a default says so; the About screen wraps the licence and tabulates carried code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gui: a refusal offers its own fix, and says where the directory is (prototype) - A size below what a format can make carries its minimum as a number, so the refusal under the size box offers a button that puts that exact size in the box. Nothing is filled in by itself. - A refusal about what is already in the output directory - a file or a manifest the run will not write over, or another run writing there - is placed under the output directory box instead of a one line strip at the foot, with a button that opens the directory. Placed by the window: the engine's address for it is unchanged, because the command line reads that address into its machine readable reports. - A refusal takes down "Working out what this would cost...", which a preview or a Generate refused while planning left standing over it. - A field asks the grid for its width without its refusal, so a long sentence or the button under it no longer widens its column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gui: the menu arrow in the accent; guards, catalogue and stored screens for the new layout - A menu draws its arrow in the primary colour. A menu and a box to type in were the same to the pixel; of three ways shown side by side in the running window the owner chose this one. - Seven guards for what the layout promises: short fields share a row a column each, a size refusal offers the smallest size and pressing it puts that size in the box, a refusal about the directory stands under it with the way to it, a refusal stands under its row across it, a menu's arrow is in the accent, a folded section opens for a refusal inside it, and a group of settings wears the colour of its kind. - Guards that described the old layout say what the new one promises: an arrow on the edge the fields start on, a number one column wide, boxes on the grid's columns, a box to tick named beside it, the licence word for word rather than line for line, a default worded as one. - The catalogue draws the grid, a refusal under a row, a fix offered, a box to tick level with a field, the kinds of group and the table of three columns. The stored screens are written again. - Placing a refusal moved out of refuse into a function, and three functions of the prototype were flattened, so the depth ceilings hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gui: every kind of group is named in its switch, and the keys of a section are not words Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * guard: the pictures named after a refusal show one again, and a nil field is reported rather than dereferenced The batch screen opens with its first batch filled in since #125, so the two scenes named recipe-refused pressed Preview on a recipe with nothing wrong in it and stored a successful preview under that name. The scenes now empty the first batch, and the runner refuses to store or compare a picture named after a refusal whose tree carries no refusal. Four comments still describing fields two to a row now describe the grid. Found by CodeRabbit on #126. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gui: no smallest-size button under a limit, where it led back to the same refusal Under "Around a limit" the set is built one byte below the limit, so a limit of 74 for a PNG refuses the file of 73, and the button put 74 back in the box: the same refusal again. The button now stands only under a box that holds the refused size itself. Raising the limit by the shortfall was tried and walked through PNG's gap between 74 and 86 B one press at a time. Found by CodeRabbit on #126. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gui: the window cannot be narrowed until Add a batch covers Preview The bar laid its rail over the centred buttons in a stack, and a stack's minimum is the larger of its two children rather than what they need side by side. From about 588 px wide on this branch (495 px on main) Add a batch covered Preview, and at 495 Preview was gone. The bar's minimum is now the buttons plus twice the rail and a gap, so the window stops there. Found while checking the review of #126. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * guard: the refusal checks of the stored pictures in a file of their own screenpixels_test.go passed the shape gate's ceiling once the scenes of a refusal learned to empty the batch and to prove they refuse. The two helpers that ask a scene's state rather than its pixels move out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * guard: a guard of its own asks that every picture named after a refusal shows one The stored pictures ask it before they compare or write, but they render every screen, and the mutation runner stops them at its memory ceiling before they finish - so the check could not be proven there. Six scenes fit under the ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 114b20d commit d6befbb

106 files changed

Lines changed: 18635 additions & 12880 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,52 @@ because it turns other people's test suites red.
1616

1717
### Changed
1818

19+
- **The window lays its forms out in columns.** A field now takes as many
20+
columns of the form as its value needs and no more, so `Format`, `Size`,
21+
`How many files` and `Damage` stand in one row instead of one under another,
22+
and a box for a number is no longer as wide as a box for a path. The window
23+
opens at the height of the form it shows, and an open list is as wide as
24+
the menu it drops from.
25+
26+
- **Every section of a form folds away, and the settings of a format and of a
27+
damage stand in groups of their own.** Each group is framed, with a line
28+
down its left edge in the colour of what it is about - blue for a format's
29+
settings, amber for a damage's, grey for notes to the manifest - so opening
30+
both no longer runs them into the fields above them and into each other. A
31+
refusal about a field inside a folded section opens that section.
32+
33+
- **A refusal about a field is written under its row, across the form.** It
34+
used to wrap inside the width of the field, three lines deep for one
35+
sentence.
36+
37+
- **A size below what a format can make comes with a button that uses the
38+
smallest size that works.** The refusal names that size to the byte, and
39+
the button puts it in the box. Nothing is filled in by itself.
40+
41+
- **When a run will not write because a file, a manifest or another run is
42+
already in the output directory, the refusal stands under `Output directory`
43+
with a button that opens it.** It used to be one line at the foot of the
44+
window, scrolled, with no way to the directory but a file manager.
45+
46+
- **A menu shows its arrow in the accent colour**, so it can be told from a
47+
box to type in and from a button, which it matched to the pixel.
48+
49+
- **A box to tick carries its name beside it**, and pressing the name ticks the
50+
box. The sentence about building on a preset says "with the box ticked"
51+
instead of "with the switch on".
52+
53+
- **A grey value in an empty box says it is a default**, as in `default: 60`,
54+
because a grey `60` read as a value somebody had typed.
55+
56+
- **Smaller things:** `Preview` and `Generate` stand apart, and so do
57+
`Duplicate` and `Remove`. `Donate` is quieter, and on `About` it stands in
58+
the Support card instead of a bar of its own. The line at the foot counts
59+
formats past three instead of naming all twenty-six. What a preset typically
60+
finds and how to use the program are written in ordinary text instead of
61+
the smallest grey. The licence on `About` wraps to the window, and the code
62+
carried in the program is listed as a table. A menu holding the keyboard is
63+
marked with a ring instead of being filled blue.
64+
1965
- **`Generate` means the same thing on every screen.** The `Several batches`
2066
screen used to open with `Batch name` and `Size` empty under a red star
2167
each, so the button that works straight away on `Single batch` turned you
@@ -416,6 +462,9 @@ because it turns other people's test suites red.
416462

417463
### Fixed
418464

465+
- **A preview or a run refused while it was being planned no longer leaves
466+
"Working out what this would cost..." standing over the refusal.**
467+
419468
- **A refusal about two settings that bound each other now says how far over
420469
you are.** Asking for a picture of 20000 by 2001 pixels was turned down with
421470
"together they come to 40 megapixels and the limit is 40" - the same number

‎internal/guard/actionbarheight_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fyne.io/fyne/v2"
88
"fyne.io/fyne/v2/container"
99
"fyne.io/fyne/v2/test"
10+
"fyne.io/fyne/v2/theme"
1011
"fyne.io/fyne/v2/widget"
1112

1213
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
@@ -381,3 +382,53 @@ func progressUnder(o fyne.CanvasObject) *parts.Progress {
381382
})
382383
return found
383384
}
385+
386+
// TestTheBarsButtonsClearItsRailInTheNarrowestWindow narrows the window as far
387+
// as it goes and asks whether the centred buttons still stand clear of the
388+
// rail at the left of the bar.
389+
//
390+
// Measured before it existed, with guirender on the batch screen: "Add a
391+
// batch" covered Preview from about 495 px on main and from about 588 px on
392+
// #126, whose buttons grew wider gaps - and Preview was gone altogether at
393+
// 495, while the window let itself be made that narrow. The bar laid its rail
394+
// over the column in a stack, and a stack's minimum is the larger of its two
395+
// children rather than what they need side by side. Found while checking an
396+
// outside review of #126 (docs/REVIEW-126-2026-09-23.md).
397+
//
398+
// The narrowest window is asked of the window rather than typed here: it is
399+
// the minimum of everything the window holds plus the padding the window
400+
// draws round it, which is what the system will not let a person go below.
401+
// The padding is added by hand because the test window does not hold its
402+
// minimum - asked for 1 px it lays the screen out in 1 px - and the first
403+
// run of this guard went red on a window 8 px narrower than any real one.
404+
// Positions are read off the laid out screen, because the promise is about
405+
// what is drawn (GUI rule 10).
406+
func TestTheBarsButtonsClearItsRailInTheNarrowestWindow(t *testing.T) {
407+
content, w := screenInAWindow(t, text.TabRecipe())
408+
width := w.Content().MinSize().Width
409+
if w.Padded() {
410+
width += 2 * theme.Padding()
411+
}
412+
narrowest := fyne.NewSize(width, window.LargestOpening.Height)
413+
w.Resize(narrowest)
414+
content.Refresh()
415+
w.Resize(narrowest)
416+
417+
rail := buttonNamed(content, text.ButtonAddBatch())
418+
first := buttonNamed(content, text.ButtonPreview())
419+
if rail == nil || first == nil {
420+
t.Fatalf("the batch screen has no %q or no %q button, so there is no rail and no row to hold apart",
421+
text.ButtonAddBatch(), text.ButtonPreview())
422+
}
423+
drv := fyne.CurrentApp().Driver()
424+
railEnds := drv.AbsolutePositionForObject(rail).X + rail.Size().Width
425+
rowStarts := drv.AbsolutePositionForObject(first).X
426+
if gap := rowStarts - railEnds; gap < parts.GapColumns-0.5 {
427+
t.Errorf("in a window %.0f px wide, the narrowest it allows, %q ends at x=%.1f and %q starts at x=%.1f - "+
428+
"a gap of %.1f px where the bar keeps %d.\n"+
429+
"Reason: the buttons are centred and the rail stands over the same row, so a bar allowed to be\n"+
430+
"narrower than the two side by side draws one button over the other.",
431+
narrowest.Width, text.ButtonAddBatch(), railEnds, text.ButtonPreview(), rowStarts, gap, parts.GapColumns)
432+
}
433+
t.Logf("narrowest window %.0f px: the rail ends at %.1f, the row starts at %.1f", narrowest.Width, railEnds, rowStarts)
434+
}

‎internal/guard/actionrail_test.go‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ import (
2727
// held in a centred column it slides right by half of what the window gained.
2828
// At 1600 px the old layout put it 300 px further right.
2929
//
30-
// Every screen, because the bar is on all four and Donate is the one control
31-
// that is on all four - see TestTheDonateButtonIsOnEveryScreen.
30+
// The three work screens. About had the bar too until 2026-09-23, holding
31+
// Donate and nothing else, and it was taken off that screen on the owner's
32+
// word from the running window: its Donate stands in the Support card now,
33+
// inside the page, where standing with the form's column is what it should do.
34+
// TestTheDonateButtonIsOnEveryScreen still asks that it is there.
3235
func TestWhatIsNotAboutTheRunStandsAtTheEdgeOfTheBar(t *testing.T) {
3336
for _, tab := range []string{
34-
text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabAbout(),
37+
text.TabOneTarget(), text.TabPresets(), text.TabRecipe(),
3538
} {
3639
t.Run(tab, func(t *testing.T) {
3740
content, w := screenInAWindow(t, tab)

‎internal/guard/boxwidth_test.go‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,13 @@ func TestADeclaredSettingStandsOnTheSameEdgeAsTheFieldsAboveIt(t *testing.T) {
312312
if !ok {
313313
t.Fatal("the format list is not laid out")
314314
}
315-
for _, name := range []string{"width", "height"} {
315+
// Since 2026-09-23 the settings a format declares stand in a group
316+
// framed inside the section, laid in the same grid of columns the
317+
// section uses - so the first of them begins on the group's inner
318+
// edge, the format list's edge moved in by the frame's room, and
319+
// none of them begins before it.
320+
edge := format.X + parts.GroupInset
321+
for i, name := range []string{"width", "height"} {
316322
control := controlUnder(screen, text.SettingLabel(name))
317323
if control == nil {
318324
t.Fatalf("bmp declares %s and no field on this screen holds it", name)
@@ -321,9 +327,12 @@ func TestADeclaredSettingStandsOnTheSameEdgeAsTheFieldsAboveIt(t *testing.T) {
321327
if !ok {
322328
t.Fatalf("the box for %s is not laid out", name)
323329
}
324-
if off := box.X - format.X; off > 1 || off < -1 {
325-
t.Errorf("the box for %s begins at x=%.0f and the format list at x=%.0f, so the settings a "+
326-
"format declares do not stand in the column of controls", name, box.X, format.X)
330+
if off := box.X - edge; i == 0 && (off > 1 || off < -1) {
331+
t.Errorf("the box for %s begins at x=%.0f and the group's inner edge is at x=%.0f, so the "+
332+
"first setting a format declares does not stand where its group begins", name, box.X, edge)
333+
}
334+
if box.X < edge-1 {
335+
t.Errorf("the box for %s begins at x=%.0f, before the group's inner edge at x=%.0f", name, box.X, edge)
327336
}
328337
}
329338
})

‎internal/guard/controlnames_test.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ func TestEveryControlOnTheFormStandsUnderAName(t *testing.T) {
4848
}
4949
walk(field.Objects[1], func(inner fyne.CanvasObject) { named[inner] = head })
5050
})
51+
// A box to tick carries its name BESIDE the square since 2026-09-23 - the
52+
// line is the square, then the name (parts.ToggleSaying) - so for a switch
53+
// the name is asked of what stands after it rather than over it.
54+
walk(batches, func(obj fyne.CanvasObject) {
55+
line, ok := obj.(*fyne.Container)
56+
if !ok || len(line.Objects) < 2 {
57+
return
58+
}
59+
if check, isToggle := line.Objects[0].(*parts.Toggle); isToggle {
60+
if head, is := headingOf(line.Objects[1]); is && head != "" {
61+
named[check] = head
62+
}
63+
}
64+
})
5165

5266
if got, is := named[switchOnIt]; !is {
5367
t.Errorf("the switch that chooses between %q, %q and %q stands on the form with no name over it, "+

‎internal/guard/detailpopup_test.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func detailButtonBeside(o fyne.CanvasObject, label string) *parts.DetailButton {
197197
var found *parts.DetailButton
198198
walk(o, func(obj fyne.CanvasObject) {
199199
row, ok := obj.(*fyne.Container)
200-
if !ok || len(row.Objects) < 2 || namedOnScreen(row.Objects[0]) != label {
200+
if !ok || len(row.Objects) < 2 || nameOfRow(row) != label {
201201
return
202202
}
203203
// Searched rather than taken from position one. A heading row grew a
@@ -213,6 +213,17 @@ func detailButtonBeside(o fyne.CanvasObject, label string) *parts.DetailButton {
213213
return found
214214
}
215215

216+
// nameOfRow is the name a heading row carries: its first thing, or - on the
217+
// line of a box to tick, which is the square and then its name since
218+
// 2026-09-23 (parts.ToggleSaying) - its second.
219+
func nameOfRow(row *fyne.Container) string {
220+
if _, isToggle := row.Objects[0].(*parts.Toggle); isToggle {
221+
head, _ := headingOf(row.Objects[1])
222+
return head
223+
}
224+
return namedOnScreen(row.Objects[0])
225+
}
226+
216227
// namedOnScreen is the words a heading shows. A switch's name is a heading in
217228
// the column like every other field's since 2026-09-15, so there is no special
218229
// case for it here any more - it carries no words of its own.

‎internal/guard/foldhead_test.go‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,15 +265,28 @@ func TestTheHeadRowOverhangsTheColumnAndTheTitleDoesNot(t *testing.T) {
265265
if !ok {
266266
t.Fatalf("the recipe screen has no field named %q", text.FieldFormat())
267267
}
268-
if off := title.X - field.X; off > 1 || off < -1 {
269-
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)
268+
// The arrow in FRONT of the title since 2026-09-23, on the owner's word
269+
// from the running window, so it is the arrow that stands on the edge the
270+
// fields stand on and the title follows it.
271+
arrow := arrowIn(t, screen, head)
272+
arrowAt, ok := absoluteOf(screen, arrow)
273+
if !ok {
274+
t.Fatal("the arrow is not on the screen it was found in")
275+
}
276+
if off := arrowAt.X - field.X; off > 1 || off < -1 {
277+
t.Errorf("the arrow of batch 1 starts at %.1f px and the name of its first field at %.1f px - "+
278+
"the arrow stands on the edge everything else starts on", arrowAt.X, field.X)
279+
}
280+
if title.X < arrowAt.X+arrow.Size().Width {
281+
t.Errorf("the title of batch 1 starts at %.1f px, over its arrow, which ends at %.1f px",
282+
title.X, arrowAt.X+arrow.Size().Width)
270283
}
271284
headAt, ok := absoluteOf(screen, head)
272285
if !ok {
273286
t.Fatal("the head row is not on the screen it was found in")
274287
}
275-
if got := title.X - headAt.X; got < parts.TabInset-1 || got > parts.TabInset+1 {
276-
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)
288+
if got := arrowAt.X - headAt.X; got < parts.TabInset-1 || got > parts.TabInset+1 {
289+
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)
277290
}
278291
}
279292

0 commit comments

Comments
 (0)